From ca57ce61188ab8bba4d04f8a5fe63856ecfbb568 Mon Sep 17 00:00:00 2001 From: AShiningRay Date: Sun, 17 Nov 2024 16:01:25 -0300 Subject: [PATCH] PlatformImage: Use MUCH faster transforms. This commit is centered entirely on fixing Sonic 2 Crash's terrible performance. It makes very heavy use of pretty much all of the available transforms to construct everything from the backgrounds, to stage tiles, to the general sprites. Previously the amount of calls to AffineTransform would slow this down considerably, often making it drop to 1-2 FPS ingame. This is no longer the case, as using direct byte manipulation on these transforms allows the game to maintain its (still choppy) 12-ish FPS with a lot of CPU time to spare. As a small bonus, there is a simple image dumping function in there for when i decide to implement it on a larger scale. --- src/org/recompile/mobile/PlatformImage.java | 236 +++++++++++++++----- 1 file changed, 183 insertions(+), 53 deletions(-) diff --git a/src/org/recompile/mobile/PlatformImage.java b/src/org/recompile/mobile/PlatformImage.java index fb707d45..05b9792c 100644 --- a/src/org/recompile/mobile/PlatformImage.java +++ b/src/org/recompile/mobile/PlatformImage.java @@ -17,10 +17,13 @@ package org.recompile.mobile; import java.net.URL; +import java.security.MessageDigest; import java.util.Arrays; import java.io.IOException; import java.io.InputStream; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; import javax.microedition.lcdui.Image; import javax.microedition.lcdui.Graphics; @@ -118,7 +121,6 @@ public PlatformImage(InputStream stream) gc.drawImage2(temp, 0, 0); - platformImage = this; } @@ -226,71 +228,199 @@ public void setPixel(int x, int y, int color) public static BufferedImage transformImage(BufferedImage image, int transform) { - int width = (int)image.getWidth(); - int height = (int)image.getHeight(); - int out_width = width; - int out_height = height; + // Return early if no transform is specified. + if(transform == Sprite.TRANS_NONE) { return image; } - AffineTransform af = new AffineTransform(); + final int width = (int)image.getWidth(); + final int height = (int)image.getHeight(); - switch (transform) { - case Sprite.TRANS_NONE: - break; + BufferedImage transimage = null; + if(transform == Sprite.TRANS_ROT90 || transform == Sprite.TRANS_ROT270 || transform == Sprite.TRANS_MIRROR_ROT90 || transform == Sprite.TRANS_MIRROR_ROT270) + { + transimage = new BufferedImage(height, width, BufferedImage.TYPE_INT_ARGB); // Non-Math.PI rotations require width and height to be swapped + } + else { transimage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); } + final WritableRaster sourceRaster = image.getRaster(); + final WritableRaster targetRaster = transimage.getRaster(); + + switch (transform) + { case Sprite.TRANS_ROT90: - af.translate(height, 0); - af.rotate(Math.PI / 2); - out_width = height; - out_height = width; - break; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + // Map original pixel (x, y) to new position (width - 1 - y, x) + int targetX = height - 1 - y; // New x position in the rotated image + int targetY = x; // New y position in the rotated image + + int[] pixelData = new int[4]; // TYPE_INT_ARGB has 4 components + sourceRaster.getDataElements(x, y, pixelData); // Get pixel from original image + targetRaster.setDataElements(targetX, targetY, pixelData); // Set pixel in rotated image + } + } + //dumpImage(image, ""); + //dumpImage(transimage, "_rot90"); + return transimage; case Sprite.TRANS_ROT180: - af.translate(width, height); - af.rotate(Math.PI); - break; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + int transformedX = width - 1 - x; + int transformedY = height - 1 - y; + + int[] pixelData = new int[4]; + sourceRaster.getDataElements(x, y, pixelData); + targetRaster.setDataElements(transformedX, transformedY, pixelData); + } + } + //dumpImage(image, ""); + //dumpImage(transimage, "_rot180"); + return transimage; case Sprite.TRANS_ROT270: - af.translate(0, width); - af.rotate(Math.PI * 3 / 2); - out_width = height; - out_height = width; - break; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + // Map original pixel (x, y) to new position (y, originalWidth - 1 - x) + int targetX = y; // New x position in the rotated image + int targetY = width - 1 - x; // New y position in the rotated image + + // Copy the pixel from the original raster to the new position in the rotated raster + int[] pixelData = new int[4]; + sourceRaster.getDataElements(x, y, pixelData); // Get pixel from original image + targetRaster.setDataElements(targetX, targetY, pixelData); // Set pixel in rotated image + } + } + //dumpImage(image, ""); + //dumpImage(transimage, "_rot270"); + return transimage; case Sprite.TRANS_MIRROR: - af.translate(width, 0); - af.scale(-1, 1); - break; - - case Sprite.TRANS_MIRROR_ROT90: - af.translate(height, 0); - af.rotate(Math.PI / 2); - af.translate(width, 0); - af.scale(-1, 1); - out_width = height; - out_height = width; - break; - - case Sprite.TRANS_MIRROR_ROT180: - af.translate(width, 0); - af.scale(-1, 1); - af.translate(width, height); - af.rotate(Math.PI); - break; + for (int y = 0; y < height; y++) + { + int[] rowData = new int[width]; + + // Get the pixel data for the current row to be mirrored + sourceRaster.getDataElements(0, y, width, 1, rowData); + + for (int x = 0; x < width; x++) { + // Set each mirrored pixel + targetRaster.setDataElements(width - 1 - x, y, new int[]{rowData[x]}); + } + } + //dumpImage(image, ""); + //dumpImage(transimage, "_mirror"); + return transimage; + + case Sprite.TRANS_MIRROR_ROT90: + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + // Get the pixel from the original image + int[] pixelData = new int[4]; // Assuming 4 channels (RGBA) + sourceRaster.getDataElements(x, y, pixelData); + + // Calculate the mirrored position + int mirroredX = width - 1 - x; // Mirrored x position + + // Calculate the new positions after 90-degree rotation + int targetX = y; // New x position in the rotated image + int targetY = width - 1 - mirroredX; // New y position in the rotated image + + // Set the pixel in the target raster + targetRaster.setDataElements(targetX, targetY, pixelData); + } + } + //dumpImage(image, ""); + //dumpImage(transimage, "_mirror90"); + return transimage; + + case Sprite.TRANS_MIRROR_ROT180: // Basically mirror vertically (an arrow pointing up will then point down) + for (int y = 0; y < height; y++) + { + int[] pixelData = new int[width * 4]; + + // Get the entire row + sourceRaster.getDataElements(0, y, width, 1, pixelData); + + // Set the row in the target raster at the flipped position + targetRaster.setDataElements(0, height - 1 - y, width, 1, pixelData); + } + //dumpImage(image, ""); + //dumpImage(transimage, "_mirror180"); + return transimage; case Sprite.TRANS_MIRROR_ROT270: - af.translate(0, width); - af.rotate(Math.PI * 3 / 2); - af.translate(width, 0); - af.scale(-1, 1); - out_width = height; - out_height = width; - break; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + // Get the pixel from the original image + int[] pixelData = new int[4]; // Assuming 4 channels (RGBA) + sourceRaster.getDataElements(x, y, pixelData); + + // Calculate the new positions after 270-degree rotation + int targetX = height - 1 - y; // New x position in the rotated image + int targetY = width - 1 - x; // New y position in the rotated image + + // Set the pixel in the target raster + targetRaster.setDataElements(targetX, targetY, pixelData); + } + } + //dumpImage(image, ""); + //dumpImage(transimage, "_mirror270"); + return transimage; } - BufferedImage transimage = new BufferedImage(out_width, out_height, BufferedImage.TYPE_INT_ARGB); - Graphics2D gc = transimage.createGraphics(); - gc.drawImage(image, af, null); - - return transimage; + return image; } + + // TODO: Turn this into a setting. Being able to dump image data would be nice. + public static void dumpImage(BufferedImage image, String append) + { + try + { + String imageMD5 = generateMD5Hash(image); + String dumpPath = "." + File.separatorChar + "FreeJ2MEDumps" + File.separatorChar + "Image" + File.separatorChar + Mobile.getPlatform().loader.suitename + File.separatorChar; + File dumpFile = new File(dumpPath); + + if (!dumpFile.isDirectory()) { dumpFile.mkdirs(); } + + dumpPath = dumpPath + "Image_" + imageMD5 + append + ".png"; + + dumpFile = new File(dumpPath); + if(dumpFile.exists()) { return; } // Don't overwrite an image that already exists + ImageIO.write(image, "png", dumpFile); + System.out.println("Image saved successfully: " + dumpPath); + } catch (IOException e) { System.err.println("Error saving image: " + e.getMessage()); } + } + + private static String generateMD5Hash(BufferedImage image) + { + try { + // Convert BufferedImage to byte array + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ImageIO.write(image, "png", baos); // Change format as needed + byte[] imageBytes = baos.toByteArray(); + + // Create MD5 hash + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] hashBytes = md.digest(imageBytes); + + // Convert byte array to hex string + StringBuilder sb = new StringBuilder(); + for (byte b : hashBytes) { + sb.append(String.format("%02x", b)); + } + + return sb.toString(); // Return the MD5 hash as a hex string + } catch (Exception e) { + e.printStackTrace(); // Handle exceptions as needed + return null; + } + } }