mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-08 14:24:05 +09:00
libmediadec: fb on ram
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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() },
|
||||
|
||||
Reference in New Issue
Block a user