Skip to content

Commit

Permalink
Display: Graphics: Clean up screen render, and improve frame sync
Browse files Browse the repository at this point in the history
Right out of the gate, massive optimization to Motorola's FunLights
implementation, while the LCD color mask is also slightly faster
by being integrated directly into the main render pass. Other
than that, Display's callSerially() method for render synchronization
has been overhauled to use a thread to process screen repaints
instead of relying on a fixed timer interval, improving efficiency
and performance by locking better to the game's intended framerate.

Furthermore, Canvas now does the entire rendering pass before
setting the "isPainting" flag to false, fixing flickering and, as
of now, rendering serviceRepaints() kind of useless since repaint()
will already run callSerially() if the jar requests a repaint while
another is under way.

Helps #58 and #59 (Boombakas Pro is fully playable and doesn't
flicker anymore, same for the games presented by issue 58).
  • Loading branch information
AShiningRay committed Feb 20, 2025
1 parent 913e2d5 commit dd54747
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 83 deletions.
27 changes: 12 additions & 15 deletions src/javax/microedition/lcdui/Canvas.java
Original file line number Diff line number Diff line change
Expand Up @@ -183,32 +183,29 @@ public void repaint(int x, int y, int width, int height)
graphics.reset();
isPainting = true;

try { paint(graphics); }
try
{
paint(graphics);

// Draw command bar whenever the canvas is not fullscreen and there are commands in the bar
if (!fullscreen && !commands.isEmpty()) { paintCommandsBar(); }

Mobile.getPlatform().flushGraphics(platformImage, x, y, width, height);
}
catch (Exception e)
{
Mobile.log(Mobile.LOG_WARNING, Canvas.class.getPackage().getName() + "." + Canvas.class.getSimpleName() + ": " + "Exception hit in paint(graphics)" + e.getMessage());
Mobile.log(Mobile.LOG_WARNING, Canvas.class.getPackage().getName() + "." + Canvas.class.getSimpleName() + ": " + "Exception hit when painting graphics: " + e.getMessage());
}
finally { isPainting = false; }

// Draw command bar whenever the canvas is not fullscreen and there are commands in the bar
if (!fullscreen && !commands.isEmpty()) { paintCommandsBar(); }

Mobile.getPlatform().flushGraphics(platformImage, x, y, width, height);
}
catch (Exception e)
{
Mobile.log(Mobile.LOG_ERROR, Canvas.class.getPackage().getName() + "." + Canvas.class.getSimpleName() + ": " + "Serious Exception hit in repaint()" + e.getMessage());
Mobile.log(Mobile.LOG_ERROR, Canvas.class.getPackage().getName() + "." + Canvas.class.getSimpleName() + ": " + "Serious Exception hit in repaint(): " + e.getMessage());
e.printStackTrace();
}
}

public void serviceRepaints()
{
if (Mobile.getDisplay().getCurrent() == this)
{
Mobile.getPlatform().flushGraphics(platformImage, 0, 0, width, height);
}
}
public void serviceRepaints() { } // repaint() should synchronize itself whenever needed (at least that's the plan).

public void setFullScreenMode(boolean mode)
{
Expand Down
50 changes: 27 additions & 23 deletions src/javax/microedition/lcdui/Display.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,9 @@

import javax.microedition.lcdui.Image;

import java.util.Vector;
import java.util.concurrent.locks.ReentrantLock;
import java.util.Timer;
import java.util.TimerTask;
import java.util.LinkedList;
import java.util.Queue;

import org.recompile.mobile.Mobile;

Expand All @@ -45,11 +44,9 @@ public class Display

private static Display display;

public Vector<Runnable> serialCalls;

private Timer timer;

private SerialCallTimerTask timertask;
private final Queue<Runnable> serialCalls;
private final Thread serialThread;
private volatile boolean processingCalls;

private boolean isSettingCurrent = false;

Expand All @@ -61,33 +58,40 @@ public Display()

Mobile.setDisplay(this);

serialCalls = new Vector<Runnable>(16);
timer = new Timer();
timertask = new SerialCallTimerTask();
timer.schedule(timertask, 0, 17);
serialCalls = new LinkedList<>();
processingCalls = true;
serialThread = new Thread(this::processSerialCalls);
serialThread.start();
}

public void callSerially(Runnable r)
public void callSerially(Runnable r)
{
serialCalls.add(r);
synchronized (serialCalls) { serialCalls.add(r); }
}

private class SerialCallTimerTask extends TimerTask
private void processSerialCalls()
{
public void run()
while (processingCalls)
{
if(!serialCalls.isEmpty())
Runnable runnable = null;

synchronized (serialCalls)
{
try
{
serialCalls.get(0).run();
serialCalls.removeElement(0);
}
catch (Exception e) { }
if (!serialCalls.isEmpty()) { runnable = serialCalls.poll(); }

if (runnable != null) { runnable.run(); }
}
}
}

public void stop() // Didn't find an use for this yet
{
processingCalls = false;
serialThread.interrupt();
try { serialThread.join(); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}

public boolean flashBacklight(int duration)
{
try
Expand Down
96 changes: 51 additions & 45 deletions src/org/recompile/mobile/PlatformGraphics.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public class PlatformGraphics extends javax.microedition.lcdui.Graphics implemen
protected Color awtColor;

// Gaussian blur kernel (7x7) for Motorola's FunLights
final float[] gaussianKernel =
protected final float[] gaussianKernel =
{
1f / 159, 2f / 159, 3f / 159, 2f / 159, 1f / 159, 0, 0,
2f / 159, 5f / 159, 8f / 159, 5f / 159, 2f / 159, 0, 0,
Expand Down Expand Up @@ -224,44 +224,56 @@ public void drawImage2(BufferedImage image, int x, int y) // Internal use method
public void flushGraphics(Image image, int x, int y, int width, int height)
{
// called by MobilePlatform.flushGraphics/repaint

// Ensure image's width and height are still positive
if (width <= 0 || height <= 0) { return; }

try
{
BufferedImage sub = image.platformImage.getCanvas().getSubimage(x, y, width, height);
final int[] pixels = ((DataBufferInt) sub.getRaster().getDataBuffer()).getData();
int[] overlayData = null;

// Apply the backlight mask if Display, nokia's DeviceControl, or others request it for backlight effects.
if(Mobile.renderLCDMask)
// This one is rather costly, as it has to draw overlays on the corners of the screen with gaussian filtering applied.
if(Mobile.funLightsEnabled)
{
for(int i = 0; i < pixels.length; i++) { pixels[i] = pixels[i] & Mobile.lcdMaskColors[Mobile.maskIndex]; }
overlayData = new int[width * height];
drawFunLights(overlayData, width, height);
}

// Render the resulting image
// Ensure adjusted width and height are still positive
if (width <= 0 || height <= 0) { return; }

// Render the resulting image
for (int j = 0; j < height; j++)
{
for (int i = 0; i < width; i++)
{
int destIndex = j * canvas.getWidth() + i;
int srcIndex = j * width + i;

// The image data CAN go out of the destination bounds, we just can't draw it whenever it does.
if (x + i < 0 || x + i >= canvas.getWidth()) { continue; }
if (y + j < 0 || y + j >= canvas.getHeight()) { continue; }
if (destIndex < 0 || destIndex >= canvasData.length) { continue; }
if (srcIndex < 0 || srcIndex >= pixels.length) { continue; }

canvasData[destIndex] = pixels[srcIndex];
}
}
// Only apply the backlight mask if Display, nokia's DeviceControl, or others request it for backlight effects.
canvasData[destIndex] = pixels[srcIndex] & (Mobile.renderLCDMask ? Mobile.lcdMaskColors[Mobile.maskIndex] : 0xFFFFFFFF);

// This one is rather costly, as it has to draw overlays on the corners of the screen with gaussian filtering applied.
// TODO: Also has flickering, this shouldn't happen.
if(Mobile.funLightsEnabled)
{
int[] overlayData = new int[width * height];
drawFunLights(overlayData, width, height);
System.arraycopy(overlayData, 0, canvasData, y * canvas.getWidth() + x, overlayData.length);
// If funLights overlay is requested by the game, apply its pixels to the screen area
if(Mobile.funLightsEnabled)
{
int srcAlpha = (overlayData[srcIndex] >> 24) & 0xFF; // Source alpha
int existingPixel = canvasData[destIndex]; // Current pixel in the canvas
int destAlpha = (existingPixel >> 24) & 0xFF;

// Blend alpha and color values using the srcOver alpha compositing method
int newAlpha = Math.min(255, srcAlpha + destAlpha);
int newRed = (((overlayData[srcIndex] >> 16) & 0xFF) * srcAlpha + ((existingPixel >> 16) & 0xFF) * (255 - srcAlpha)) / newAlpha;
int newGreen = (((overlayData[srcIndex] >> 8) & 0xFF) * srcAlpha + ((existingPixel >> 8) & 0xFF) * (255 - srcAlpha)) / newAlpha;
int newBlue = ((overlayData[srcIndex] & 0xFF) * srcAlpha + (existingPixel & 0xFF) * (255 - srcAlpha)) / newAlpha;

canvasData[destIndex] = (newAlpha << 24) | (newRed << 16) | (newGreen << 8) | newBlue;
}
}
}
}
catch (Exception e)
Expand Down Expand Up @@ -332,7 +344,8 @@ public void drawRGB(int[] rgbData, int offset, int scanlength, int x, int y, int
{
int rowOffset = offset + (i * scanlength); // Calculate the starting index for the current row

for (int j = 0; j < width; j++) {
for (int j = 0; j < width; j++)
{
int pixelIndex = rowOffset + j; // Source index in rgbData
int destIndex = (y + i) * canvasWidth + (x + j);

Expand Down Expand Up @@ -1002,14 +1015,17 @@ else if (x >= width - Mobile.funLightRegionSize / 2) // Right Sideband Region
private void applyGaussianBlur(int[] pixels, int width, int height)
{
final int[] result = new int[pixels.length];

final int kernelSize = 7;
final int kernelRadius = kernelSize / 2;

int kernelSize = 7;
int kernelRadius = kernelSize / 2;

// Horizontal blur
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
if(x > Mobile.funLightRegionSize - kernelRadius && x < width - Mobile.funLightRegionSize + kernelRadius && y < height - Mobile.funLightRegionSize + kernelRadius) { continue; }

float r = 0, g = 0, b = 0, a = 0;
float weightSum = 0;

Expand All @@ -1020,20 +1036,12 @@ private void applyGaussianBlur(int[] pixels, int width, int height)
if (pixelX >= 0 && pixelX < width)
{
int pixelColor = pixels[y * width + pixelX];
int alpha = (pixelColor >> 24) & 0xff;
int red = (pixelColor >> 16) & 0xff;
int green = (pixelColor >> 8) & 0xff;
int blue = pixelColor & 0xff;

int premultipliedRed = (red * alpha) / 255;
int premultipliedGreen = (green * alpha) / 255;
int premultipliedBlue = (blue * alpha) / 255;

float kernelWeight = gaussianKernel[kx + kernelRadius];
r += premultipliedRed * kernelWeight;
g += premultipliedGreen * kernelWeight;
b += premultipliedBlue * kernelWeight;
a += alpha * kernelWeight;

r += ((pixelColor >> 16) & 0xff) * kernelWeight;
g += ((pixelColor >> 8) & 0xff) * kernelWeight;
b += (pixelColor & 0xff) * kernelWeight;
a += ((pixelColor >> 24) & 0xff) * kernelWeight;
weightSum += kernelWeight;
}
}
Expand All @@ -1047,10 +1055,13 @@ private void applyGaussianBlur(int[] pixels, int width, int height)
}
}

// vertical blur
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
if(x > Mobile.funLightRegionSize - kernelRadius && x < width - Mobile.funLightRegionSize + kernelRadius && y < height - Mobile.funLightRegionSize + kernelRadius) { continue; }

float r = 0, g = 0, b = 0, a = 0;
float weightSum = 0;

Expand All @@ -1061,16 +1072,12 @@ private void applyGaussianBlur(int[] pixels, int width, int height)
if (pixelY >= 0 && pixelY < height)
{
int pixelColor = result[pixelY * width + x];
int alpha = (pixelColor >> 24) & 0xff;
int red = (pixelColor >> 16) & 0xff;
int green = (pixelColor >> 8) & 0xff;
int blue = pixelColor & 0xff;

float kernelWeight = gaussianKernel[ky + kernelRadius];
r += red * kernelWeight;
g += green * kernelWeight;
b += blue * kernelWeight;
a += alpha * kernelWeight;

r += ((pixelColor >> 16) & 0xff) * kernelWeight;
g += ((pixelColor >> 8) & 0xff) * kernelWeight;
b += (pixelColor & 0xff) * kernelWeight;
a += ((pixelColor >> 24) & 0xff) * kernelWeight;
weightSum += kernelWeight;
}
}
Expand All @@ -1083,7 +1090,6 @@ private void applyGaussianBlur(int[] pixels, int width, int height)
result[y * width + x] = (newAlpha << 24) | (newRed << 16) | (newGreen << 8) | newBlue;
}
}

System.arraycopy(result, 0, pixels, 0, pixels.length);
}
}

0 comments on commit dd54747

Please sign in to comment.