libmediadec: fb on ram

This commit is contained in:
minjaesong
2026-06-08 02:26:23 +09:00
parent e32f7565ba
commit ffc1d420cd
7 changed files with 230 additions and 53 deletions

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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

View File

@@ -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()

View File

@@ -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() },

View File

@@ -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)
}