mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-08 06:14:04 +09:00
libmediadec: fb on ram
This commit is contained in:
@@ -5,10 +5,11 @@
|
|||||||
//
|
//
|
||||||
// loop:
|
// loop:
|
||||||
// read input (quit / pause / seek / volume / cue / ASCII-toggle)
|
// 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
|
// [player] hold the frame
|
||||||
// [postprocessor] subtitle state resolved by the library
|
// [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
|
// then subtitle overlay + playgui chrome
|
||||||
//
|
//
|
||||||
// Usage: playmov FILE [-i] [-ascii] [-colour] [-deblock] [-boundaryaware]
|
// Usage: playmov FILE [-i] [-ascii] [-colour] [-deblock] [-boundaryaware]
|
||||||
@@ -234,6 +235,7 @@ try {
|
|||||||
function enterAsciiVisual() {
|
function enterAsciiVisual() {
|
||||||
ensureAscii()
|
ensureAscii()
|
||||||
graphics.setBackground(0, 0, 0)
|
graphics.setBackground(0, 0, 0)
|
||||||
|
graphics.clearPixelsAll(0, 0, 0, 0)
|
||||||
con.clear()
|
con.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,19 +286,20 @@ try {
|
|||||||
lastKey = key
|
lastKey = key
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Draw a decoded frame: framebuffer -> screen -> overlays -> chrome ──────
|
// ── Draw a decoded frame: RAM frame -> screen / ASCII -> overlays -> chrome ─
|
||||||
function draw() {
|
function draw() {
|
||||||
if (asciiMode) {
|
if (asciiMode) {
|
||||||
// Sample the frame off the framebuffer, then cover the picture with
|
// The decoded frame already sits in RAM (TEV/TAV) or on the display
|
||||||
// solid-black (240) text cells — cheaper than clearing the pixel planes.
|
// planes (iPF), so sample it WITHOUT uploading to the video adapter,
|
||||||
dec.blit() // frame -> framebuffer (so sample* can read it)
|
// then cover the picture with solid-black (240) text cells (cheaper
|
||||||
|
// than clearing the pixel planes).
|
||||||
dec.sampleGray(aaCtx.imagebuffer, aaCtx.imgW, aaCtx.imgH)
|
dec.sampleGray(aaCtx.imagebuffer, aaCtx.imgW, aaCtx.imgH)
|
||||||
aa.render(aaCtx, aaParams)
|
aa.render(aaCtx, aaParams)
|
||||||
aa.flush(aaCtx)
|
aa.flush(aaCtx)
|
||||||
if (colourMode) applyColourFore(dec) // recolour the FG plane from the video's RGB
|
if (colourMode) applyColourFore(dec) // recolour the FG plane from the video's RGB
|
||||||
paintAsciiBgOpaque() // cover with opaque 240 (not transparent 255)
|
paintAsciiBgOpaque() // cover with opaque 240 (not transparent 255)
|
||||||
} else {
|
} 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)
|
dec.bias() // bias lighting (player-owned stage; graphics only)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,21 +7,30 @@
|
|||||||
* const mediadec = require("mediadec")
|
* const mediadec = require("mediadec")
|
||||||
* const dec = mediadec.open("A:\\film.tav", { interactive: true })
|
* const dec = mediadec.open("A:\\film.tav", { interactive: true })
|
||||||
* while (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 === 'eof') break
|
||||||
* if (ev.type !== 'frame') { sys.sleep(1); continue }
|
* if (ev.type !== 'frame') { sys.sleep(1); continue }
|
||||||
* dec.blit() // [draw] copy the frame to the screen
|
* dec.blit() // [draw] upload the RAM frame to the screen
|
||||||
* // ...or in ASCII mode: dec.blit(); dec.sampleGray(buf,w,h); aa.render/flush
|
* // ...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()
|
* 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:
|
* The decoder object every backend returns exposes a uniform interface:
|
||||||
* .info {format,width,height,fps,totalFrames,hasAudio,hasSubtitles,
|
* .info {format,width,height,fps,totalFrames,hasAudio,hasSubtitles,
|
||||||
* isInterlaced,colourSpace,graphicsMode,isStill}
|
* isInterlaced,colourSpace,graphicsMode,isStill}
|
||||||
* .step() -> { type:'frame'|'idle'|'eof'|'newfile'|'error', frameCount }
|
* .step() -> { type:'frame'|'idle'|'eof'|'newfile'|'error', frameCount }
|
||||||
* .blit() present the current native frame to the screen
|
* .frameBuffer RAM RGB888 address of the current frame (0 for iPF; see above)
|
||||||
* .sampleGray(dst,w,h) fill an ASCII brightness buffer from the framebuffer
|
* .frameWidth/.frameHeight dimensions of the frame in .frameBuffer
|
||||||
* .sampleColour(dst,w,h) fill a per-cell RGB buffer (w*h*3) from the 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)
|
* .subtitle {visible,text,position,useUnicode,dirty} (resolved by the lib)
|
||||||
* .pause(b)/.isPaused() .setVolume(v)/.getVolume()
|
* .pause(b)/.isPaused() .setVolume(v)/.getVolume()
|
||||||
* .seekSeconds(n) .cue(d) .cues
|
* .seekSeconds(n) .cue(d) .cues
|
||||||
|
|||||||
@@ -4,8 +4,10 @@
|
|||||||
* Holds everything the three movie backends (iPF/MOV, TEV, TAV) duplicated in
|
* Holds everything the three movie backends (iPF/MOV, TEV, TAV) duplicated in
|
||||||
* the old standalone players: magic constants, packet-type / SSF-opcode tables,
|
* the old standalone players: magic constants, packet-type / SSF-opcode tables,
|
||||||
* the TAV quality LUT, seqread selection, the audio router, the subtitle
|
* the TAV quality LUT, seqread selection, the audio router, the subtitle
|
||||||
* engine, bias lighting, and the two `sampleGray` source samplers used by the
|
* engine, bias lighting, and the `sampleGray` / `sampleColour` source samplers
|
||||||
* player's ASCII-render path.
|
* 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
|
* 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
|
* (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 = {
|
exports = {
|
||||||
MAGIC_MOV, MAGIC_TEV, MAGIC_TAV, MAGIC_TAP, MAGIC_UCF,
|
MAGIC_MOV, MAGIC_TEV, MAGIC_TAV, MAGIC_TAP, MAGIC_UCF,
|
||||||
MP2_FRAME_SIZE, QLUT,
|
MP2_FRAME_SIZE, QLUT,
|
||||||
@@ -405,5 +443,6 @@ exports = {
|
|||||||
openSeqread, readMagic, detectFormat, magicEquals,
|
openSeqread, readMagic, detectFormat, magicEquals,
|
||||||
luma8,
|
luma8,
|
||||||
makeAudioRouter, makeSubtitleEngine, makeBias,
|
makeAudioRouter, makeSubtitleEngine, makeBias,
|
||||||
sampleGrayScreen, sampleColourScreen
|
sampleGrayScreen, sampleColourScreen,
|
||||||
|
sampleGrayRGB, sampleColourRGB
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,7 +148,10 @@ function create(magic, sr, fileLength, opts, common) {
|
|||||||
// (bias() below) and is skipped when an explicit background packet disabled it.
|
// (bias() below) and is skipped when an explicit background packet disabled it.
|
||||||
function blit() { }
|
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 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) }
|
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 ' ' },
|
get frameMode() { return ' ' },
|
||||||
cues: [],
|
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,
|
step: step,
|
||||||
blit: blit,
|
blit: blit,
|
||||||
bias() { if (autoBg) applyBias() }, // skipped when an explicit bg packet set the colour
|
bias() { if (autoBg) applyBias() }, // skipped when an explicit bg packet set the colour
|
||||||
|
|||||||
@@ -9,11 +9,13 @@
|
|||||||
* headers (XFPS) and timecode-driven subtitles.
|
* headers (XFPS) and timecode-driven subtitles.
|
||||||
*
|
*
|
||||||
* The original main-loop body becomes step(): each call performs one iteration
|
* The original main-loop body becomes step(): each call performs one iteration
|
||||||
* (optional packet read + GOP state machine + a time-gated display) and returns
|
* (optional packet read + GOP state machine + a time-gated display) and, when a
|
||||||
* 'frame' when a frame is displayed. The actual upload is deferred to blit()
|
* frame is due, materialises it into PRESENT_RGB (an RGB888 RAM buffer) before
|
||||||
* (or sampleGray() in ASCII mode), which is the only structural change from the
|
* returning 'frame'. This is the one structural change from the original: every
|
||||||
* original — it lets the same decoded frame feed either the graphics path or
|
* source (I/P ping-pong, progressive GOP in the Java-heap videoBuffer, interlaced
|
||||||
* the ASCII path.
|
* 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
|
const TAV_VERSION = 1
|
||||||
@@ -107,6 +109,15 @@ function create(magic, sr, fileLength, opts, common, isTap) {
|
|||||||
let CURRENT_RGB = RGB_BUFFER_A
|
let CURRENT_RGB = RGB_BUFFER_A
|
||||||
let PREV_RGB = RGB_BUFFER_B
|
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 FIELD_SIZE = width * decodeHeight * 3
|
||||||
const CURR_FIELD = isInterlaced ? sys.malloc(FIELD_SIZE) : 0
|
const CURR_FIELD = isInterlaced ? sys.malloc(FIELD_SIZE) : 0
|
||||||
const PREV_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() {
|
function step() {
|
||||||
// TAP still: show the pre-decoded frame once, then idle.
|
// TAP still: show the pre-decoded frame once, then idle.
|
||||||
if (isTap) {
|
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' }
|
return { type: 'idle' }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -539,6 +550,7 @@ function create(magic, sr, fileLength, opts, common, isTap) {
|
|||||||
while (sys.nanoTime() < nextFrameTime && !paused) sys.sleep(1)
|
while (sys.nanoTime() < nextFrameTime && !paused) sys.sleep(1)
|
||||||
if (!paused) {
|
if (!paused) {
|
||||||
pending = { kind: 'rgb', src: CURRENT_RGB, frameNo: trueFrameCount }
|
pending = { kind: 'rgb', src: CURRENT_RGB, frameNo: trueFrameCount }
|
||||||
|
materializeFrame()
|
||||||
audioR.fire()
|
audioR.fire()
|
||||||
firstFrameIssued = true
|
firstFrameIssued = true
|
||||||
frameCount++; trueFrameCount++; iframeReady = false
|
frameCount++; trueFrameCount++; iframeReady = false
|
||||||
@@ -556,6 +568,7 @@ function create(magic, sr, fileLength, opts, common, isTap) {
|
|||||||
if (!paused) {
|
if (!paused) {
|
||||||
if (isInterlaced) pending = { kind: 'gop-interlaced', frameIndex: currentGopFrameIndex, bufferOffset: currentGopBufferSlot * SLOT_SIZE, frameNo: trueFrameCount, gopSize: currentGopSize }
|
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 }
|
else pending = { kind: 'gop', frameIndex: currentGopFrameIndex, bufferOffset: currentGopBufferSlot * SLOT_SIZE, frameNo: trueFrameCount, gopSize: currentGopSize }
|
||||||
|
materializeFrame()
|
||||||
audioR.fire()
|
audioR.fire()
|
||||||
firstFrameIssued = true
|
firstFrameIssued = true
|
||||||
currentGopFrameIndex++; frameCount++; trueFrameCount++
|
currentGopFrameIndex++; frameCount++; trueFrameCount++
|
||||||
@@ -606,24 +619,35 @@ function create(magic, sr, fileLength, opts, common, isTap) {
|
|||||||
return displayed ? { type: 'frame', frameCount: frameCount } : { type: 'idle' }
|
return displayed ? { type: 'frame', frameCount: frameCount } : { type: 'idle' }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Present / sample ─────────────────────────────────────────────────────
|
// ── Materialise / present / sample ───────────────────────────────────────
|
||||||
function blit() {
|
// 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') {
|
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') {
|
} else if (pending.kind === 'gop') {
|
||||||
graphics.uploadVideoBufferFrameToFramebuffer(pending.frameIndex, width, height, pending.frameNo, pending.bufferOffset)
|
graphics.tavCopyGopFrameToRGB(pending.frameIndex, width, height, pending.bufferOffset, PRESENT_RGB)
|
||||||
updateScreenMask(frameCount); fillMaskedRegions()
|
|
||||||
} else if (pending.kind === 'gop-interlaced') {
|
} else if (pending.kind === 'gop-interlaced') {
|
||||||
graphics.uploadInterlacedGopFrameToFramebuffer(pending.frameIndex, pending.gopSize, width, decodeHeight, height, pending.frameNo, pending.bufferOffset, prevField, curField, nextField, CURRENT_RGB)
|
graphics.tavDeinterlaceGopFrameToRGB(pending.frameIndex, pending.gopSize, width, decodeHeight, height, pending.frameNo, pending.bufferOffset, prevField, curField, nextField, PRESENT_RGB)
|
||||||
updateScreenMask(frameCount); fillMaskedRegions()
|
|
||||||
}
|
}
|
||||||
// bias lighting is a separate, player-driven stage (bias() below)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Player calls blit() before sampleGray() in ASCII mode, so the framebuffer
|
// Present the materialised RAM frame to the display planes (with dithering).
|
||||||
// already holds the current frame regardless of kind.
|
// bias lighting is a separate, player-driven stage (bias() below).
|
||||||
function sampleGray(dst, w, h) { common.sampleGrayScreen(width, height, dst, w, h, gpuGraphicsMode) }
|
function blit() {
|
||||||
function sampleColour(dst, w, h) { common.sampleColourScreen(width, height, dst, w, h, gpuGraphicsMode) }
|
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 ──────────────────────────────
|
// ── TAP still: decode the single image now ──────────────────────────────
|
||||||
if (isTap) {
|
if (isTap) {
|
||||||
@@ -663,6 +687,12 @@ function create(magic, sr, fileLength, opts, common, isTap) {
|
|||||||
get currentCueIndex() { return currentCueIndex },
|
get currentCueIndex() { return currentCueIndex },
|
||||||
get currentFileIndex() { return currentFileIndex },
|
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,
|
step: step,
|
||||||
blit: blit,
|
blit: blit,
|
||||||
bias() { applyBias() },
|
bias() { applyBias() },
|
||||||
@@ -714,7 +744,7 @@ function create(magic, sr, fileLength, opts, common, isTap) {
|
|||||||
|
|
||||||
close() {
|
close() {
|
||||||
cleanupAsyncDecode()
|
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) }
|
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) }
|
while (overflowQueue.length > 0) { const ov = overflowQueue.shift(); sys.free(ov.compressedPtr) }
|
||||||
audioR.close()
|
audioR.close()
|
||||||
|
|||||||
@@ -4,8 +4,9 @@
|
|||||||
* Ported from assets/disk0/tvdos/bin/playtev.js. DCT codec, YCoCg-R / ICtCp,
|
* Ported from assets/disk0/tvdos/bin/playtev.js. DCT codec, YCoCg-R / ICtCp,
|
||||||
* motion compensation, optional deblock / boundary-aware decoding, interlaced
|
* motion compensation, optional deblock / boundary-aware decoding, interlaced
|
||||||
* (yadif/bwdif) support, NTSC frame duplication, MP2 audio, SSF + SSF-TC
|
* (yadif/bwdif) support, NTSC frame duplication, MP2 audio, SSF + SSF-TC
|
||||||
* subtitles. Decodes into an off-screen RGB888 ping-pong buffer; blit() uploads
|
* subtitles. Decodes into an off-screen RGB888 ping-pong buffer (the generic
|
||||||
* it (deferred from decode so the ASCII path can sample the same buffer).
|
* 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
|
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() {
|
function blit() {
|
||||||
graphics.uploadRGBToFramebuffer(currentFrameSrc, width, height, frameCount, false)
|
graphics.uploadRGBToFramebuffer(currentFrameSrc, width, height, frameCount, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Player calls blit() (which uploads currentFrameSrc) before sampleGray in
|
// The decoded frame already sits in currentFrameSrc (RGB888 RAM), so sampling
|
||||||
// ASCII mode, so we read the framebuffer the upload just produced.
|
// reads RAM directly — ASCII mode needs no blit() / display-plane round-trip.
|
||||||
function sampleGray(dst, w, h) { common.sampleGrayScreen(width, height, dst, w, h, 4) }
|
function sampleGray(dst, w, h) { common.sampleGrayRGB(currentFrameSrc, width, height, dst, w, h) }
|
||||||
function sampleColour(dst, w, h) { common.sampleColourScreen(width, height, dst, w, h, 4) }
|
function sampleColour(dst, w, h) { common.sampleColourRGB(currentFrameSrc, width, height, dst, w, h) }
|
||||||
|
|
||||||
return {
|
return {
|
||||||
info: info,
|
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 },
|
get qY() { return qualityY }, get qCo() { return qualityCo }, get qCg() { return qualityCg },
|
||||||
cues: [],
|
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,
|
step: step,
|
||||||
blit: blit,
|
blit: blit,
|
||||||
bias() { applyBias() },
|
bias() { applyBias() },
|
||||||
|
|||||||
@@ -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
|
* 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.
|
* Copy a single progressive GOP frame out of videoBuffer (Java heap) into a
|
||||||
* Handles field extraction and temporal deinterlacing for GOP frames.
|
* 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 frameIndex Current frame index in GOP (0-based)
|
||||||
* @param gopSize Total number of frames in GOP
|
* @param gopSize Total number of frames in GOP
|
||||||
* @param width Frame width
|
* @param width Frame width
|
||||||
* @param fieldHeight Height of each field (half of display height)
|
* @param fieldHeight Height of each field (half of display height)
|
||||||
* @param fullHeight Full display height (2 * fieldHeight)
|
* @param fullHeight Full display height (2 * fieldHeight) — unused here, kept for call symmetry
|
||||||
* @param frameCount Global frame counter for dithering
|
* @param frameCount Global frame counter (deinterlacer cadence)
|
||||||
* @param bufferOffset Start offset of GOP in videoBuffer
|
* @param bufferOffset Start offset of GOP in videoBuffer
|
||||||
* @param prevFieldAddr Memory address for previous field buffer
|
* @param prevFieldAddr Memory address for previous field buffer (scratch)
|
||||||
* @param currentFieldAddr Memory address for current field buffer
|
* @param currentFieldAddr Memory address for current field buffer (scratch)
|
||||||
* @param nextFieldAddr Memory address for next field buffer
|
* @param nextFieldAddr Memory address for next field buffer (scratch)
|
||||||
* @param deinterlaceOutputAddr Memory address for deinterlaced output
|
* @param deinterlaceOutputAddr Destination RGB888 buffer (width*fullHeight*3 bytes)
|
||||||
*/
|
*/
|
||||||
fun uploadInterlacedGopFrameToFramebuffer(
|
fun tavDeinterlaceGopFrameToRGB(
|
||||||
frameIndex: Int,
|
frameIndex: Int,
|
||||||
gopSize: Int,
|
gopSize: Int,
|
||||||
width: Int,
|
width: Int,
|
||||||
@@ -7158,8 +7202,43 @@ class GraphicsJSR223Delegate(private val vm: VM) {
|
|||||||
prevFieldAddr, currentFieldAddr, nextFieldAddr,
|
prevFieldAddr, currentFieldAddr, nextFieldAddr,
|
||||||
deinterlaceOutputAddr, "yadif"
|
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)
|
uploadRGBToFramebuffer(deinterlaceOutputAddr, width, fullHeight, frameCount, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user