From ffc1d420cdd2ff21c5b25bd49bb6350bc0adf87f Mon Sep 17 00:00:00 2001 From: minjaesong Date: Mon, 8 Jun 2026 02:26:23 +0900 Subject: [PATCH] libmediadec: fb on ram --- assets/disk0/tvdos/bin/playmov.js | 17 ++-- assets/disk0/tvdos/include/mediadec.mjs | 21 ++-- .../disk0/tvdos/include/mediadec_common.mjs | 45 ++++++++- assets/disk0/tvdos/include/mediadec_ipf.mjs | 11 ++- assets/disk0/tvdos/include/mediadec_tav.mjs | 68 +++++++++---- assets/disk0/tvdos/include/mediadec_tev.mjs | 22 +++-- .../torvald/tsvm/GraphicsJSR223Delegate.kt | 99 +++++++++++++++++-- 7 files changed, 230 insertions(+), 53 deletions(-) diff --git a/assets/disk0/tvdos/bin/playmov.js b/assets/disk0/tvdos/bin/playmov.js index ac41546..a1dda0f 100644 --- a/assets/disk0/tvdos/bin/playmov.js +++ b/assets/disk0/tvdos/bin/playmov.js @@ -5,10 +5,11 @@ // // loop: // read input (quit / pause / seek / volume / cue / ASCII-toggle) -// [backend] dec.step() -> decode the next due frame into the framebuffer +// [backend] dec.step() -> decode the next due frame into a RAM RGB888 frame // [player] hold the frame // [postprocessor] subtitle state resolved by the library -// [draw] dec.blit() (graphics) OR sampleGray + aa.mjs (ASCII), +// [draw] graphics: dec.blit() (upload RAM frame to adapter) + dec.bias() +// ASCII: dec.sampleGray + aa.mjs straight off the RAM frame (no upload) // then subtitle overlay + playgui chrome // // Usage: playmov FILE [-i] [-ascii] [-colour] [-deblock] [-boundaryaware] @@ -234,6 +235,7 @@ try { function enterAsciiVisual() { ensureAscii() graphics.setBackground(0, 0, 0) + graphics.clearPixelsAll(0, 0, 0, 0) con.clear() } @@ -284,19 +286,20 @@ try { lastKey = key } - // ── Draw a decoded frame: framebuffer -> screen -> overlays -> chrome ────── + // ── Draw a decoded frame: RAM frame -> screen / ASCII -> overlays -> chrome ─ function draw() { if (asciiMode) { - // Sample the frame off the framebuffer, then cover the picture with - // solid-black (240) text cells — cheaper than clearing the pixel planes. - dec.blit() // frame -> framebuffer (so sample* can read it) + // The decoded frame already sits in RAM (TEV/TAV) or on the display + // planes (iPF), so sample it WITHOUT uploading to the video adapter, + // then cover the picture with solid-black (240) text cells (cheaper + // than clearing the pixel planes). dec.sampleGray(aaCtx.imagebuffer, aaCtx.imgW, aaCtx.imgH) aa.render(aaCtx, aaParams) aa.flush(aaCtx) if (colourMode) applyColourFore(dec) // recolour the FG plane from the video's RGB paintAsciiBgOpaque() // cover with opaque 240 (not transparent 255) } else { - dec.blit() // copy the frame to the framebuffer + dec.blit() // upload the RAM frame to the video adapter dec.bias() // bias lighting (player-owned stage; graphics only) } diff --git a/assets/disk0/tvdos/include/mediadec.mjs b/assets/disk0/tvdos/include/mediadec.mjs index 65543c0..e273f92 100644 --- a/assets/disk0/tvdos/include/mediadec.mjs +++ b/assets/disk0/tvdos/include/mediadec.mjs @@ -7,21 +7,30 @@ * const mediadec = require("mediadec") * const dec = mediadec.open("A:\\film.tav", { interactive: true }) * while (true) { - * const ev = dec.step() // [backend] decode the next due frame + * const ev = dec.step() // [backend] decode the next due frame to RAM * if (ev.type === 'eof') break * if (ev.type !== 'frame') { sys.sleep(1); continue } - * dec.blit() // [draw] copy the frame to the screen - * // ...or in ASCII mode: dec.blit(); dec.sampleGray(buf,w,h); aa.render/flush + * dec.blit() // [draw] upload the RAM frame to the screen + * // ...or in ASCII mode (no upload): dec.sampleGray(buf,w,h); aa.render/flush + * // ...or grab the frame yourself: sys.peek(dec.frameBuffer + ...) * } * dec.close() * + * step() decodes the next due frame into a generic RAM RGB888 buffer (exposed as + * .frameBuffer); the caller decides what to do with it — upload it with .blit(), + * sample it for ASCII, or read it directly. (iPF is the exception: it decodes + * straight to the 4bpp display planes, so .frameBuffer is 0 and .sampleGray/.blit + * operate on the planes — see mediadec_ipf.mjs.) + * * The decoder object every backend returns exposes a uniform interface: * .info {format,width,height,fps,totalFrames,hasAudio,hasSubtitles, * isInterlaced,colourSpace,graphicsMode,isStill} * .step() -> { type:'frame'|'idle'|'eof'|'newfile'|'error', frameCount } - * .blit() present the current native frame to the screen - * .sampleGray(dst,w,h) fill an ASCII brightness buffer from the framebuffer - * .sampleColour(dst,w,h) fill a per-cell RGB buffer (w*h*3) from the framebuffer + * .frameBuffer RAM RGB888 address of the current frame (0 for iPF; see above) + * .frameWidth/.frameHeight dimensions of the frame in .frameBuffer + * .blit() upload the current RAM frame to the screen (adapter) + * .sampleGray(dst,w,h) fill an ASCII brightness buffer from the RAM frame + * .sampleColour(dst,w,h) fill a per-cell RGB buffer (w*h*3) from the RAM frame * .subtitle {visible,text,position,useUnicode,dirty} (resolved by the lib) * .pause(b)/.isPaused() .setVolume(v)/.getVolume() * .seekSeconds(n) .cue(d) .cues diff --git a/assets/disk0/tvdos/include/mediadec_common.mjs b/assets/disk0/tvdos/include/mediadec_common.mjs index 3f67f78..ca22358 100644 --- a/assets/disk0/tvdos/include/mediadec_common.mjs +++ b/assets/disk0/tvdos/include/mediadec_common.mjs @@ -4,8 +4,10 @@ * Holds everything the three movie backends (iPF/MOV, TEV, TAV) duplicated in * the old standalone players: magic constants, packet-type / SSF-opcode tables, * the TAV quality LUT, seqread selection, the audio router, the subtitle - * engine, bias lighting, and the two `sampleGray` source samplers used by the - * player's ASCII-render path. + * engine, bias lighting, and the `sampleGray` / `sampleColour` source samplers + * used by the player's ASCII-render path — both a *Screen pair (read the GPU + * display planes, for iPF) and a *RGB pair (read a RAM RGB888 frame, for the + * decode-into-RAM backends TEV / TAV). * * Runs in the same GraalVM context as the player, so the host globals * (sys/graphics/audio/con/serial/files/gzip) are visible directly, exactly as @@ -396,6 +398,42 @@ function sampleColourScreen(width, height, dst, dstW, dstH, mode) { } } +// ── sampleGray / sampleColour from a RAM RGB888 frame ───────────────────────── +// Companions to the *Screen samplers that read a decoded frame straight out of a +// JS-addressable RGB888 RAM buffer (3 bytes/pixel, forward-addressed) instead of +// the GPU display planes. Backends that decode into RAM (TEV / TAV) use these so +// the ASCII renderer can sample the frame WITHOUT it ever being uploaded to the +// video adapter — the whole point of the generic RAM-frame model. Same cheap +// ~dstW·dstH·3 peek count and the same nearest-sampling geometry as the *Screen +// versions (sampleGrayRGB row-aligned; sampleColourRGB at the cell centre). +function sampleGrayRGB(srcPtr, width, height, dst, dstW, dstH) { + for (let y = 0; y < dstH; y++) { + let sy = (y * height / dstH) | 0 + let dstRow = y * dstW + for (let x = 0; x < dstW; x++) { + let sx = (x * width / dstW) | 0 + let o = srcPtr + (sy * width + sx) * 3 + let r = sys.peek(o) & 255, g = sys.peek(o + 1) & 255, b = sys.peek(o + 2) & 255 + dst[dstRow + x] = luma8(r, g, b) + } + } +} + +function sampleColourRGB(srcPtr, width, height, dst, dstW, dstH) { + for (let y = 0; y < dstH; y++) { + let sy = ((y + 0.5) * height / dstH) | 0 + if (sy >= height) sy = height - 1 + let dstRow = y * dstW * 3 + for (let x = 0; x < dstW; x++) { + let sx = ((x + 0.5) * width / dstW) | 0 + if (sx >= width) sx = width - 1 + let o = srcPtr + (sy * width + sx) * 3 + let di = dstRow + x * 3 + dst[di] = sys.peek(o) & 255; dst[di + 1] = sys.peek(o + 1) & 255; dst[di + 2] = sys.peek(o + 2) & 255 + } + } +} + exports = { MAGIC_MOV, MAGIC_TEV, MAGIC_TAV, MAGIC_TAP, MAGIC_UCF, MP2_FRAME_SIZE, QLUT, @@ -405,5 +443,6 @@ exports = { openSeqread, readMagic, detectFormat, magicEquals, luma8, makeAudioRouter, makeSubtitleEngine, makeBias, - sampleGrayScreen, sampleColourScreen + sampleGrayScreen, sampleColourScreen, + sampleGrayRGB, sampleColourRGB } diff --git a/assets/disk0/tvdos/include/mediadec_ipf.mjs b/assets/disk0/tvdos/include/mediadec_ipf.mjs index 31794f7..73e7798 100644 --- a/assets/disk0/tvdos/include/mediadec_ipf.mjs +++ b/assets/disk0/tvdos/include/mediadec_ipf.mjs @@ -148,7 +148,10 @@ function create(magic, sr, fileLength, opts, common) { // (bias() below) and is skipped when an explicit background packet disabled it. function blit() { } - // Frame is already on the display planes, so the player can sample the screen. + // iPF decodes straight to the 4bpp display planes (no fast JS planar->RGB + // path), so — unlike TEV / TAV — there is no RAM RGB888 frame: the planes ARE + // the frame. sampleGray/sampleColour therefore read the planes back; this still + // costs no extra upload in ASCII mode, since decoding already wrote the planes. function sampleGray(dst, w, h) { common.sampleGrayScreen(width, height, dst, w, h, 4) } function sampleColour(dst, w, h) { common.sampleColourScreen(width, height, dst, w, h, 4) } @@ -161,6 +164,12 @@ function create(magic, sr, fileLength, opts, common) { get frameMode() { return ' ' }, cues: [], + // No generic RAM frame for iPF: it decodes straight to the display planes, + // so frameBuffer is 0. Use sampleGray/sampleColour to read the frame instead. + get frameBuffer() { return 0 }, + get frameWidth() { return width }, + get frameHeight() { return height }, + step: step, blit: blit, bias() { if (autoBg) applyBias() }, // skipped when an explicit bg packet set the colour diff --git a/assets/disk0/tvdos/include/mediadec_tav.mjs b/assets/disk0/tvdos/include/mediadec_tav.mjs index f82df50..961f995 100644 --- a/assets/disk0/tvdos/include/mediadec_tav.mjs +++ b/assets/disk0/tvdos/include/mediadec_tav.mjs @@ -9,11 +9,13 @@ * headers (XFPS) and timecode-driven subtitles. * * The original main-loop body becomes step(): each call performs one iteration - * (optional packet read + GOP state machine + a time-gated display) and returns - * 'frame' when a frame is displayed. The actual upload is deferred to blit() - * (or sampleGray() in ASCII mode), which is the only structural change from the - * original — it lets the same decoded frame feed either the graphics path or - * the ASCII path. + * (optional packet read + GOP state machine + a time-gated display) and, when a + * frame is due, materialises it into PRESENT_RGB (an RGB888 RAM buffer) before + * returning 'frame'. This is the one structural change from the original: every + * source (I/P ping-pong, progressive GOP in the Java-heap videoBuffer, interlaced + * GOP) is funnelled into one RAM frame, so blit() (upload to the adapter) and the + * ASCII sampler both read from RAM — neither reads pixels back off the display + * planes, and `frameBuffer` exposes the frame for arbitrary reuse. */ const TAV_VERSION = 1 @@ -107,6 +109,15 @@ function create(magic, sr, fileLength, opts, common, isTap) { let CURRENT_RGB = RGB_BUFFER_A let PREV_RGB = RGB_BUFFER_B + // Canonical decoded-frame buffer: every displayed frame is materialised here + // as RGB888, whatever its source (I/P ping-pong, progressive GOP in the + // Java-heap videoBuffer, or an interlaced GOP that needs deinterlacing). This + // is the one ~735 kB buffer the generic RAM-frame model costs: blit() uploads + // it, the ASCII path samples it, and `frameBuffer` exposes it to callers — so + // a frame can be reused without ever round-tripping through the display planes. + const PRESENT_RGB = sys.malloc(FRAME_SIZE) + sys.memset(PRESENT_RGB, 0, FRAME_SIZE) + const FIELD_SIZE = width * decodeHeight * 3 const CURR_FIELD = isInterlaced ? sys.malloc(FIELD_SIZE) : 0 const PREV_FIELD = isInterlaced ? sys.malloc(FIELD_SIZE) : 0 @@ -488,7 +499,7 @@ function create(magic, sr, fileLength, opts, common, isTap) { function step() { // TAP still: show the pre-decoded frame once, then idle. if (isTap) { - if (!firstFrameIssued) { firstFrameIssued = true; pending = { kind: 'rgb', src: CURRENT_RGB, frameNo: 0 }; return { type: 'frame', frameCount: 1 } } + if (!firstFrameIssued) { firstFrameIssued = true; pending = { kind: 'rgb', src: CURRENT_RGB, frameNo: 0 }; materializeFrame(); return { type: 'frame', frameCount: 1 } } return { type: 'idle' } } @@ -539,6 +550,7 @@ function create(magic, sr, fileLength, opts, common, isTap) { while (sys.nanoTime() < nextFrameTime && !paused) sys.sleep(1) if (!paused) { pending = { kind: 'rgb', src: CURRENT_RGB, frameNo: trueFrameCount } + materializeFrame() audioR.fire() firstFrameIssued = true frameCount++; trueFrameCount++; iframeReady = false @@ -556,6 +568,7 @@ function create(magic, sr, fileLength, opts, common, isTap) { if (!paused) { if (isInterlaced) pending = { kind: 'gop-interlaced', frameIndex: currentGopFrameIndex, bufferOffset: currentGopBufferSlot * SLOT_SIZE, frameNo: trueFrameCount, gopSize: currentGopSize } else pending = { kind: 'gop', frameIndex: currentGopFrameIndex, bufferOffset: currentGopBufferSlot * SLOT_SIZE, frameNo: trueFrameCount, gopSize: currentGopSize } + materializeFrame() audioR.fire() firstFrameIssued = true currentGopFrameIndex++; frameCount++; trueFrameCount++ @@ -606,24 +619,35 @@ function create(magic, sr, fileLength, opts, common, isTap) { return displayed ? { type: 'frame', frameCount: frameCount } : { type: 'idle' } } - // ── Present / sample ───────────────────────────────────────────────────── - function blit() { + // ── Materialise / present / sample ─────────────────────────────────────── + // Land the just-decoded frame in PRESENT_RGB (RGB888 RAM), whatever its source. + // Called by step() the moment a frame becomes due, so blit() (upload) and the + // ASCII sampler can both consume it from RAM and neither path has to read the + // pixels back off the display planes. + // rgb : I/P (or TAP still) — already RGB888 in CURRENT_RGB; copy in. + // gop : progressive GOP frame in the Java-heap videoBuffer; copy out. + // gop-interlaced : interlaced GOP fields; deinterlace into PRESENT_RGB. + function materializeFrame() { if (pending.kind === 'rgb') { - graphics.uploadRGBToFramebuffer(pending.src, width, height, pending.frameNo, false) + sys.memcpy(pending.src, PRESENT_RGB, FRAME_SIZE) } else if (pending.kind === 'gop') { - graphics.uploadVideoBufferFrameToFramebuffer(pending.frameIndex, width, height, pending.frameNo, pending.bufferOffset) - updateScreenMask(frameCount); fillMaskedRegions() + graphics.tavCopyGopFrameToRGB(pending.frameIndex, width, height, pending.bufferOffset, PRESENT_RGB) } else if (pending.kind === 'gop-interlaced') { - graphics.uploadInterlacedGopFrameToFramebuffer(pending.frameIndex, pending.gopSize, width, decodeHeight, height, pending.frameNo, pending.bufferOffset, prevField, curField, nextField, CURRENT_RGB) - updateScreenMask(frameCount); fillMaskedRegions() + graphics.tavDeinterlaceGopFrameToRGB(pending.frameIndex, pending.gopSize, width, decodeHeight, height, pending.frameNo, pending.bufferOffset, prevField, curField, nextField, PRESENT_RGB) } - // bias lighting is a separate, player-driven stage (bias() below) } - // Player calls blit() before sampleGray() in ASCII mode, so the framebuffer - // already holds the current frame regardless of kind. - function sampleGray(dst, w, h) { common.sampleGrayScreen(width, height, dst, w, h, gpuGraphicsMode) } - function sampleColour(dst, w, h) { common.sampleColourScreen(width, height, dst, w, h, gpuGraphicsMode) } + // Present the materialised RAM frame to the display planes (with dithering). + // bias lighting is a separate, player-driven stage (bias() below). + function blit() { + graphics.uploadRGBToFramebuffer(PRESENT_RGB, width, height, pending.frameNo, false) + if (pending.kind === 'gop' || pending.kind === 'gop-interlaced') { updateScreenMask(frameCount); fillMaskedRegions() } + } + + // The current frame already sits in PRESENT_RGB (materialised in step()), so + // sampling never touches the display planes — ASCII mode needs no blit(). + function sampleGray(dst, w, h) { common.sampleGrayRGB(PRESENT_RGB, width, height, dst, w, h) } + function sampleColour(dst, w, h) { common.sampleColourRGB(PRESENT_RGB, width, height, dst, w, h) } // ── TAP still: decode the single image now ────────────────────────────── if (isTap) { @@ -663,6 +687,12 @@ function create(magic, sr, fileLength, opts, common, isTap) { get currentCueIndex() { return currentCueIndex }, get currentFileIndex() { return currentFileIndex }, + // Generic RAM frame: RGB888 buffer holding the current decoded frame, + // valid after step() returns 'frame'. Callers may read it for their own use. + get frameBuffer() { return PRESENT_RGB }, + get frameWidth() { return width }, + get frameHeight() { return height }, + step: step, blit: blit, bias() { applyBias() }, @@ -714,7 +744,7 @@ function create(magic, sr, fileLength, opts, common, isTap) { close() { cleanupAsyncDecode() - sys.free(RGB_BUFFER_A); sys.free(RGB_BUFFER_B) + sys.free(RGB_BUFFER_A); sys.free(RGB_BUFFER_B); sys.free(PRESENT_RGB) if (isInterlaced) { sys.free(CURR_FIELD); sys.free(PREV_FIELD); sys.free(NEXT_FIELD) } while (overflowQueue.length > 0) { const ov = overflowQueue.shift(); sys.free(ov.compressedPtr) } audioR.close() diff --git a/assets/disk0/tvdos/include/mediadec_tev.mjs b/assets/disk0/tvdos/include/mediadec_tev.mjs index a40696e..7daa318 100644 --- a/assets/disk0/tvdos/include/mediadec_tev.mjs +++ b/assets/disk0/tvdos/include/mediadec_tev.mjs @@ -4,8 +4,9 @@ * Ported from assets/disk0/tvdos/bin/playtev.js. DCT codec, YCoCg-R / ICtCp, * motion compensation, optional deblock / boundary-aware decoding, interlaced * (yadif/bwdif) support, NTSC frame duplication, MP2 audio, SSF + SSF-TC - * subtitles. Decodes into an off-screen RGB888 ping-pong buffer; blit() uploads - * it (deferred from decode so the ASCII path can sample the same buffer). + * subtitles. Decodes into an off-screen RGB888 ping-pong buffer (the generic + * RAM frame): blit() uploads it to the adapter, while the ASCII path samples it + * straight from RAM, and `frameBuffer` exposes it for arbitrary reuse. */ const TEV_VERSION_YCOCG = 2 @@ -181,15 +182,16 @@ function create(magic, sr, fileLength, opts, common) { } } - // Present only; bias lighting is a separate, player-driven stage (bias() below). + // Present the decoded RAM frame to the display planes (with dithering). + // bias lighting is a separate, player-driven stage (bias() below). function blit() { graphics.uploadRGBToFramebuffer(currentFrameSrc, width, height, frameCount, false) } - // Player calls blit() (which uploads currentFrameSrc) before sampleGray in - // ASCII mode, so we read the framebuffer the upload just produced. - function sampleGray(dst, w, h) { common.sampleGrayScreen(width, height, dst, w, h, 4) } - function sampleColour(dst, w, h) { common.sampleColourScreen(width, height, dst, w, h, 4) } + // The decoded frame already sits in currentFrameSrc (RGB888 RAM), so sampling + // reads RAM directly — ASCII mode needs no blit() / display-plane round-trip. + function sampleGray(dst, w, h) { common.sampleGrayRGB(currentFrameSrc, width, height, dst, w, h) } + function sampleColour(dst, w, h) { common.sampleColourRGB(currentFrameSrc, width, height, dst, w, h) } return { info: info, @@ -201,6 +203,12 @@ function create(magic, sr, fileLength, opts, common) { get qY() { return qualityY }, get qCo() { return qualityCo }, get qCg() { return qualityCg }, cues: [], + // Generic RAM frame: the current decoded frame as RGB888 (the live + // ping-pong buffer), valid after step() returns 'frame'. Callers may read it. + get frameBuffer() { return currentFrameSrc }, + get frameWidth() { return width }, + get frameHeight() { return height }, + step: step, blit: blit, bias() { applyBias() }, diff --git a/tsvm_core/src/net/torvald/tsvm/GraphicsJSR223Delegate.kt b/tsvm_core/src/net/torvald/tsvm/GraphicsJSR223Delegate.kt index 967ec94..74098ee 100644 --- a/tsvm_core/src/net/torvald/tsvm/GraphicsJSR223Delegate.kt +++ b/tsvm_core/src/net/torvald/tsvm/GraphicsJSR223Delegate.kt @@ -426,6 +426,20 @@ class GraphicsJSR223Delegate(private val vm: VM) { } } + fun clearPixelsAll(col1: Int, col2: Int, col3: Int, col4: Int) { + getFirstGPU()?.let { + it.poke(250884L, col1.toByte()) + it.poke(250883L, 2) + it.poke(250884L, col2.toByte()) + it.poke(250883L, 4) + it.poke(250884L, col3.toByte()) + it.poke(250883L, 6) + it.poke(250884L, col4.toByte()) + it.poke(250883L, 8) + it.applyDelay() + } + } + /** * prints a char as-is; won't interpret them as an escape sequence */ @@ -7088,22 +7102,52 @@ class GraphicsJSR223Delegate(private val vm: VM) { } /** - * Upload interlaced GOP frame from videoBuffer with deinterlacing. - * Handles field extraction and temporal deinterlacing for GOP frames. + * Copy a single progressive GOP frame out of videoBuffer (Java heap) into a + * JS-addressable RGB888 RAM buffer, without dithering or uploading. Lets the + * mediadec library expose every decoded frame as a generic RAM frame: the + * caller then uploads it with uploadRGBToFramebuffer (graphics path) or + * samples it for the ASCII renderer — never going through the display planes + * just to read the pixels back. + * + * @param frameIndex Which frame in the GOP to copy (0-based) + * @param width Frame width + * @param height Frame height + * @param bufferOffset Byte offset of the GOP slot in videoBuffer + * @param dstRGBAddr Destination RGB888 buffer in VM user memory (width*height*3 bytes) + */ + fun tavCopyGopFrameToRGB(frameIndex: Int, width: Int, height: Int, bufferOffset: Long, dstRGBAddr: Long) { + val gpu = (vm.peripheralTable[1].peripheral as GraphicsAdapter) + val frameSize = width * height * 3L + val videoBufferOffset = bufferOffset + (frameIndex * frameSize) + UnsafeHelper.memcpyRaw( + null, + gpu.videoBuffer.ptr + videoBufferOffset, + null, + vm.usermem.ptr + dstRGBAddr, + frameSize + ) + } + + /** + * Extract three consecutive fields of an interlaced GOP frame from videoBuffer + * and deinterlace them into an RGB888 RAM buffer — the field-copy + deinterlace + * half of uploadInterlacedGopFrameToFramebuffer, WITHOUT the final upload. Used + * by the mediadec library to land the decoded frame in RAM (where it can then be + * uploaded or sampled), mirroring tavCopyGopFrameToRGB for the progressive case. * * @param frameIndex Current frame index in GOP (0-based) * @param gopSize Total number of frames in GOP * @param width Frame width * @param fieldHeight Height of each field (half of display height) - * @param fullHeight Full display height (2 * fieldHeight) - * @param frameCount Global frame counter for dithering + * @param fullHeight Full display height (2 * fieldHeight) — unused here, kept for call symmetry + * @param frameCount Global frame counter (deinterlacer cadence) * @param bufferOffset Start offset of GOP in videoBuffer - * @param prevFieldAddr Memory address for previous field buffer - * @param currentFieldAddr Memory address for current field buffer - * @param nextFieldAddr Memory address for next field buffer - * @param deinterlaceOutputAddr Memory address for deinterlaced output + * @param prevFieldAddr Memory address for previous field buffer (scratch) + * @param currentFieldAddr Memory address for current field buffer (scratch) + * @param nextFieldAddr Memory address for next field buffer (scratch) + * @param deinterlaceOutputAddr Destination RGB888 buffer (width*fullHeight*3 bytes) */ - fun uploadInterlacedGopFrameToFramebuffer( + fun tavDeinterlaceGopFrameToRGB( frameIndex: Int, gopSize: Int, width: Int, @@ -7158,8 +7202,43 @@ class GraphicsJSR223Delegate(private val vm: VM) { prevFieldAddr, currentFieldAddr, nextFieldAddr, deinterlaceOutputAddr, "yadif" ) + } - // Upload deinterlaced full-height frame + /** + * Upload interlaced GOP frame from videoBuffer with deinterlacing. + * Handles field extraction and temporal deinterlacing for GOP frames. + * + * @param frameIndex Current frame index in GOP (0-based) + * @param gopSize Total number of frames in GOP + * @param width Frame width + * @param fieldHeight Height of each field (half of display height) + * @param fullHeight Full display height (2 * fieldHeight) + * @param frameCount Global frame counter for dithering + * @param bufferOffset Start offset of GOP in videoBuffer + * @param prevFieldAddr Memory address for previous field buffer + * @param currentFieldAddr Memory address for current field buffer + * @param nextFieldAddr Memory address for next field buffer + * @param deinterlaceOutputAddr Memory address for deinterlaced output + */ + fun uploadInterlacedGopFrameToFramebuffer( + frameIndex: Int, + gopSize: Int, + width: Int, + fieldHeight: Int, + fullHeight: Int, + frameCount: Int, + bufferOffset: Long, + prevFieldAddr: Long, + currentFieldAddr: Long, + nextFieldAddr: Long, + deinterlaceOutputAddr: Long + ) { + // Field copy + deinterlace into the RGB output buffer ... + tavDeinterlaceGopFrameToRGB( + frameIndex, gopSize, width, fieldHeight, fullHeight, frameCount, bufferOffset, + prevFieldAddr, currentFieldAddr, nextFieldAddr, deinterlaceOutputAddr + ) + // ... then upload the deinterlaced full-height frame. uploadRGBToFramebuffer(deinterlaceOutputAddr, width, fullHeight, frameCount, false) }