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:
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user