Fix RCO menu coordinates and smooth video pacing

RCO object positions are screen-center with +Y up, but the renderer drew
them as top-left, pushing menu items off-screen. Convert to top-left in
the 480x272 design space before drawing.

Also use precise parkNanos pacing and reuse per-frame ARGB/WritableImage
buffers to remove the 1.4MB allocations per video frame.
This commit is contained in:
2026-09-15 23:45:30 -04:00
parent 7d601dd1fd
commit 0a16d3d3ec
3 changed files with 43 additions and 13 deletions
@@ -19,7 +19,16 @@ public final class YuvConverter {
byte[] uPlane, int uStride, byte[] uPlane, int uStride,
byte[] vPlane, int vStride) { byte[] vPlane, int vStride) {
int[] argb = new int[width * height]; int[] argb = new int[width * height];
yuvj420ToArgb(width, height, yPlane, yStride, uPlane, uStride, vPlane, vStride, argb);
return argb;
}
/** Converts into a caller-provided buffer of at least {@code width*height} pixels. */
public static void yuvj420ToArgb(int width, int height,
byte[] yPlane, int yStride,
byte[] uPlane, int uStride,
byte[] vPlane, int vStride,
int[] argb) {
for (int row = 0; row < height; row++) { for (int row = 0; row < height; row++) {
int yRow = row * yStride; int yRow = row * yStride;
int uvRow = (row >> 1) * uStride; int uvRow = (row >> 1) * uStride;
@@ -43,8 +52,6 @@ public final class YuvConverter {
| clamp(b); | clamp(b);
} }
} }
return argb;
} }
private static int clamp(int value) { private static int clamp(int value) {
@@ -35,6 +35,7 @@ import javax.sound.sampled.SourceDataLine;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.concurrent.locks.LockSupport;
/** JavaFX application entry point for the UMD player. */ /** JavaFX application entry point for the UMD player. */
public final class PlayerApp extends Application { public final class PlayerApp extends Application {
@@ -60,6 +61,10 @@ public final class PlayerApp extends Application {
private volatile long clockOffsetNanos = 0; private volatile long clockOffsetNanos = 0;
private long pauseStartNanos = 0; private long pauseStartNanos = 0;
private final WritableImage[] videoImages = new WritableImage[2];
private final int[][] argbBuffers = new int[2][];
private int videoBufferIndex = 0;
@Override @Override
public void start(Stage stage) { public void start(Stage stage) {
// Gradle's --args splits on whitespace; rejoin so paths with spaces work. // Gradle's --args splits on whitespace; rejoin so paths with spaces work.
@@ -197,11 +202,17 @@ public final class PlayerApp extends Application {
PlaybackEngine.VideoFrame frame; PlaybackEngine.VideoFrame frame;
while ((frame = engine.nextVideoFrame()) != null) { while ((frame = engine.nextVideoFrame()) != null) {
int[] argb = YuvConverter.yuvj420ToArgb(frame.width(), frame.height(), int index = videoBufferIndex++ & 1;
int[] argb = argbBuffers[index];
int pixelCount = frame.width() * frame.height();
if (argb == null || argb.length < pixelCount) {
argb = argbBuffers[index] = new int[pixelCount];
}
YuvConverter.yuvj420ToArgb(frame.width(), frame.height(),
frame.yPlane(), frame.yStride(), frame.yPlane(), frame.yStride(),
frame.uPlane(), frame.uStride(), frame.uPlane(), frame.uStride(),
frame.vPlane(), frame.vStride()); frame.vPlane(), frame.vStride(), argb);
blitVideo(argb, frame.width(), frame.height()); blitVideo(argb, index, frame.width(), frame.height());
checkPaused(); checkPaused();
long targetNanos = baseNanos + clockOffsetNanos + (frame.pts() - basePts) * 1_000_000_000L / PlaybackEngine.MPEG_TIMESTAMP; long targetNanos = baseNanos + clockOffsetNanos + (frame.pts() - basePts) * 1_000_000_000L / PlaybackEngine.MPEG_TIMESTAMP;
@@ -275,10 +286,10 @@ public final class PlayerApp extends Application {
} }
} }
private static void sleepUntil(long targetNanos) throws InterruptedException { private static void sleepUntil(long targetNanos) {
long sleepNanos = targetNanos - System.nanoTime(); long remaining;
if (sleepNanos > 0) { while ((remaining = targetNanos - System.nanoTime()) > 0) {
Thread.sleep(sleepNanos / 1_000_000, (int) (sleepNanos % 1_000_000)); LockSupport.parkNanos(remaining);
} }
} }
@@ -330,10 +341,15 @@ public final class PlayerApp extends Application {
} }
} }
private void blitVideo(int[] argb, int width, int height) { private void blitVideo(int[] argb, int index, int width, int height) {
WritableImage image = new WritableImage(width, height); WritableImage image = videoImages[index];
if (image == null || (int) image.getWidth() != width || (int) image.getHeight() != height) {
image = new WritableImage(width, height);
videoImages[index] = image;
}
image.getPixelWriter().setPixels(0, 0, width, height, PixelFormat.getIntArgbInstance(), argb, 0, width); image.getPixelWriter().setPixels(0, 0, width, height, PixelFormat.getIntArgbInstance(), argb, 0, width);
final WritableImage current = image;
Platform.runLater(() -> { Platform.runLater(() -> {
GraphicsContext g = videoCanvas.getGraphicsContext2D(); GraphicsContext g = videoCanvas.getGraphicsContext2D();
double canvasWidth = videoCanvas.getWidth(); double canvasWidth = videoCanvas.getWidth();
@@ -357,7 +373,7 @@ public final class PlayerApp extends Application {
} }
double x = (canvasWidth - drawW) / 2.0; double x = (canvasWidth - drawW) / 2.0;
double y = (canvasHeight - drawH) / 2.0; double y = (canvasHeight - drawH) / 2.0;
g.drawImage(image, x, y, drawW, drawH); g.drawImage(current, x, y, drawW, drawH);
}); });
} }
@@ -129,7 +129,14 @@ public final class RcoRenderer implements Display {
int drawHeight = display.getHeight(); int drawHeight = display.getHeight();
double w = drawWidth > 0 ? drawWidth : image.getWidth(); double w = drawWidth > 0 ? drawWidth : image.getWidth();
double h = drawHeight > 0 ? drawHeight : image.getHeight(); double h = drawHeight > 0 ? drawHeight : image.getHeight();
g.drawImage(image, display.getX(), display.getY(), w, h);
// RCO positions are screen-center coordinates with +Y up (OpenGL-like).
// Convert to top-left within the 480x272 design space.
double cx = display.getX();
double cy = display.getY();
double x = 240.0 + (cx - w / 2.0);
double y = 136.0 - (cy + h / 2.0);
g.drawImage(image, x, y, w, h);
g.setGlobalAlpha(1.0); g.setGlobalAlpha(1.0);
} }