From 0a16d3d3ec0287e14f4c8bde8cd47cc89817a58a Mon Sep 17 00:00:00 2001 From: Mathieu Moran Date: Tue, 15 Sep 2026 23:45:30 -0400 Subject: [PATCH] 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. --- .../dev/quoije/umd/media/YuvConverter.java | 11 ++++-- .../java/dev/quoije/umd/ui/PlayerApp.java | 36 +++++++++++++------ .../java/dev/quoije/umd/ui/RcoRenderer.java | 9 ++++- 3 files changed, 43 insertions(+), 13 deletions(-) diff --git a/core/src/main/java/dev/quoije/umd/media/YuvConverter.java b/core/src/main/java/dev/quoije/umd/media/YuvConverter.java index a93544e..911577f 100644 --- a/core/src/main/java/dev/quoije/umd/media/YuvConverter.java +++ b/core/src/main/java/dev/quoije/umd/media/YuvConverter.java @@ -19,7 +19,16 @@ public final class YuvConverter { byte[] uPlane, int uStride, byte[] vPlane, int vStride) { 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++) { int yRow = row * yStride; int uvRow = (row >> 1) * uStride; @@ -43,8 +52,6 @@ public final class YuvConverter { | clamp(b); } } - - return argb; } private static int clamp(int value) { diff --git a/ui/src/main/java/dev/quoije/umd/ui/PlayerApp.java b/ui/src/main/java/dev/quoije/umd/ui/PlayerApp.java index f16a255..90f8055 100644 --- a/ui/src/main/java/dev/quoije/umd/ui/PlayerApp.java +++ b/ui/src/main/java/dev/quoije/umd/ui/PlayerApp.java @@ -35,6 +35,7 @@ import javax.sound.sampled.SourceDataLine; import java.io.IOException; import java.nio.file.Path; +import java.util.concurrent.locks.LockSupport; /** JavaFX application entry point for the UMD player. */ public final class PlayerApp extends Application { @@ -60,6 +61,10 @@ public final class PlayerApp extends Application { private volatile long clockOffsetNanos = 0; private long pauseStartNanos = 0; + private final WritableImage[] videoImages = new WritableImage[2]; + private final int[][] argbBuffers = new int[2][]; + private int videoBufferIndex = 0; + @Override public void start(Stage stage) { // 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; 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.uPlane(), frame.uStride(), - frame.vPlane(), frame.vStride()); - blitVideo(argb, frame.width(), frame.height()); + frame.vPlane(), frame.vStride(), argb); + blitVideo(argb, index, frame.width(), frame.height()); checkPaused(); 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 { - long sleepNanos = targetNanos - System.nanoTime(); - if (sleepNanos > 0) { - Thread.sleep(sleepNanos / 1_000_000, (int) (sleepNanos % 1_000_000)); + private static void sleepUntil(long targetNanos) { + long remaining; + while ((remaining = targetNanos - System.nanoTime()) > 0) { + LockSupport.parkNanos(remaining); } } @@ -330,10 +341,15 @@ public final class PlayerApp extends Application { } } - private void blitVideo(int[] argb, int width, int height) { - WritableImage image = new WritableImage(width, height); + private void blitVideo(int[] argb, int index, int width, int 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); + final WritableImage current = image; Platform.runLater(() -> { GraphicsContext g = videoCanvas.getGraphicsContext2D(); double canvasWidth = videoCanvas.getWidth(); @@ -357,7 +373,7 @@ public final class PlayerApp extends Application { } double x = (canvasWidth - drawW) / 2.0; double y = (canvasHeight - drawH) / 2.0; - g.drawImage(image, x, y, drawW, drawH); + g.drawImage(current, x, y, drawW, drawH); }); } diff --git a/ui/src/main/java/dev/quoije/umd/ui/RcoRenderer.java b/ui/src/main/java/dev/quoije/umd/ui/RcoRenderer.java index e6c3788..d8d422d 100644 --- a/ui/src/main/java/dev/quoije/umd/ui/RcoRenderer.java +++ b/ui/src/main/java/dev/quoije/umd/ui/RcoRenderer.java @@ -129,7 +129,14 @@ public final class RcoRenderer implements Display { int drawHeight = display.getHeight(); double w = drawWidth > 0 ? drawWidth : image.getWidth(); 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); }