From da084c0074f1591310cc342a0373bdad55a9f6b9 Mon Sep 17 00:00:00 2001 From: minjaesong Date: Tue, 7 Oct 2025 17:51:47 +0900 Subject: [PATCH] TAV: interlaced mode --- assets/disk0/tvdos/bin/playtav.js | 83 ++++++++++++++++++- .../torvald/tsvm/GraphicsJSR223Delegate.kt | 5 ++ video_encoder/encoder_tav.c | 17 +++- 3 files changed, 100 insertions(+), 5 deletions(-) diff --git a/assets/disk0/tvdos/bin/playtav.js b/assets/disk0/tvdos/bin/playtav.js index c0793a1..bc46651 100644 --- a/assets/disk0/tvdos/bin/playtav.js +++ b/assets/disk0/tvdos/bin/playtav.js @@ -305,6 +305,13 @@ console.log(`Channel layout: ${getChannelLayoutName(header.channelLayout)}`) console.log(`Tiles: ${tilesX}x${tilesY} (${numTiles} total)`) console.log(`Colour space: ${header.version % 2 == 0 ? "ICtCp" : "YCoCg-R"}`) console.log(`Features: ${hasAudio ? "Audio " : ""}${hasSubtitles ? "Subtitles " : ""}${progressiveTransmission ? "Progressive " : ""}${roiCoding ? "ROI " : ""}`) +console.log(`Video flags raw: 0x${header.videoFlags.toString(16)}`) +console.log(`Scan type: ${isInterlaced ? "Interlaced" : "Progressive"}`) + +// Adjust decode height for interlaced content +// For interlaced: header.height is display height (448) +// Each field is half of display height (448/2 = 224) +let decodeHeight = isInterlaced ? (header.height >> 1) : header.height // Frame buffer addresses - same as TEV const FRAME_PIXELS = header.width * header.height @@ -313,10 +320,28 @@ const FRAME_SIZE = FRAME_PIXELS * 3 // RGB buffer size const RGB_BUFFER_A = sys.malloc(FRAME_SIZE) const RGB_BUFFER_B = sys.malloc(FRAME_SIZE) +// Field buffers for interlaced mode (half-height fields) +const FIELD_SIZE = header.width * decodeHeight * 3 +const CURR_FIELD_BUFFER = isInterlaced ? sys.malloc(FIELD_SIZE) : 0 +const PREV_FIELD_BUFFER = isInterlaced ? sys.malloc(FIELD_SIZE) : 0 +const NEXT_FIELD_BUFFER = isInterlaced ? sys.malloc(FIELD_SIZE) : 0 + // Ping-pong buffer pointers (swap instead of copy) let CURRENT_RGB_ADDR = RGB_BUFFER_A let PREV_RGB_ADDR = RGB_BUFFER_B +// Initialize field buffers to black for interlaced mode +if (isInterlaced) { + sys.memset(CURR_FIELD_BUFFER, 0, FIELD_SIZE) + sys.memset(PREV_FIELD_BUFFER, 0, FIELD_SIZE) + sys.memset(NEXT_FIELD_BUFFER, 0, FIELD_SIZE) +} + +// Field buffer pointers for temporal deinterlacing +let prevFieldAddr = PREV_FIELD_BUFFER +let currentFieldAddr = CURR_FIELD_BUFFER +let nextFieldAddr = NEXT_FIELD_BUFFER + // Audio state let audioBufferBytesLastFrame = 0 let frame_cnt = 0 @@ -631,13 +656,26 @@ try { try { let decodeStart = sys.nanoTime() + // For interlaced mode, decode to field buffer at half height + let decodeTarget = isInterlaced ? currentFieldAddr : CURRENT_RGB_ADDR + + // Debug interlaced mode + if (frameCount === 0 && isInterlaced) { + serial.println(`[DEBUG] Interlaced mode active:`) + serial.println(` decodeHeight: ${decodeHeight}`) + serial.println(` currentFieldAddr: ${currentFieldAddr}`) + serial.println(` prevFieldAddr: ${prevFieldAddr}`) + serial.println(` nextFieldAddr: ${nextFieldAddr}`) + serial.println(` FIELD_SIZE: ${FIELD_SIZE}`) + } + // Call new TAV hardware decoder that handles Zstd decompression internally // Note: No longer using JS gzip.decompFromTo - Kotlin handles Zstd natively decoderDbgInfo = graphics.tavDecodeCompressed( compressedPtr, // Pass compressed data directly compressedSize, // Size of compressed data - CURRENT_RGB_ADDR, PREV_RGB_ADDR, // RGB buffer pointers - header.width, header.height, + decodeTarget, PREV_RGB_ADDR, // RGB buffer pointers (field buffer for interlaced) + header.width, decodeHeight, // Use half height for interlaced header.qualityLevel, QLUT[header.qualityY], QLUT[header.qualityCo], QLUT[header.qualityCg], header.channelLayout, // Channel layout for variable processing trueFrameCount, @@ -650,8 +688,38 @@ try { decodeTime = (sys.nanoTime() - decodeStart) / 1000000.0 decompressTime = 0 // Decompression time now included in decode time - // Upload RGB buffer to display framebuffer (like TEV) + // For interlaced: deinterlace fields into full frame, otherwise upload directly let uploadStart = sys.nanoTime() + if (isInterlaced) { + if (frameCount === 0) { + serial.println(`[DEBUG] Calling tavDeinterlace for first frame`) + } + // Weave fields using temporal deinterlacing (yadif algorithm) + try { + graphics.tavDeinterlace(trueFrameCount, header.width, decodeHeight, + prevFieldAddr, currentFieldAddr, nextFieldAddr, + CURRENT_RGB_ADDR, "yadif") + if (frameCount === 0) { + serial.println(`[DEBUG] tavDeinterlace succeeded`) + } + } catch (deinterlaceError) { + serial.printerr(`[ERROR] tavDeinterlace failed: ${deinterlaceError}`) + serial.printerr(` frame: ${trueFrameCount}, width: ${header.width}, height: ${decodeHeight}`) + serial.printerr(` prevField: ${prevFieldAddr}, currField: ${currentFieldAddr}, nextField: ${nextFieldAddr}`) + throw deinterlaceError + } + + // Rotate field buffers for next frame: NEXT -> CURRENT -> PREV + let tempField = prevFieldAddr + prevFieldAddr = currentFieldAddr + currentFieldAddr = nextFieldAddr + nextFieldAddr = tempField + } else { + if (frameCount === 0) { + serial.println(`[DEBUG] Progressive mode - no deinterlacing`) + } + } + graphics.uploadRGBToFramebuffer(CURRENT_RGB_ADDR, header.width, header.height, trueFrameCount, false) uploadTime = (sys.nanoTime() - uploadStart) / 1000000.0 @@ -739,7 +807,7 @@ try { akku: akku2, fileName: fullFilePathStr, fileOrd: currentFileIndex, - resolution: `${header.width}x${header.height}`, + resolution: `${header.width}x${header.height}${(isInterlaced) ? 'i' : ''}`, colourSpace: header.version % 2 == 0 ? "ICtCp" : "YCoCg", currentStatus: 1 } @@ -759,6 +827,13 @@ finally { sys.free(RGB_BUFFER_A) sys.free(RGB_BUFFER_B) + // Free field buffers if interlaced + if (isInterlaced) { + sys.free(CURR_FIELD_BUFFER) + sys.free(PREV_FIELD_BUFFER) + sys.free(NEXT_FIELD_BUFFER) + } + con.curs_set(1) con.clear() diff --git a/tsvm_core/src/net/torvald/tsvm/GraphicsJSR223Delegate.kt b/tsvm_core/src/net/torvald/tsvm/GraphicsJSR223Delegate.kt index 1a3821b..dad3f0c 100644 --- a/tsvm_core/src/net/torvald/tsvm/GraphicsJSR223Delegate.kt +++ b/tsvm_core/src/net/torvald/tsvm/GraphicsJSR223Delegate.kt @@ -3139,6 +3139,11 @@ class GraphicsJSR223Delegate(private val vm: VM) { } } + fun tavDeinterlace(frameCount: Int, width: Int, height: Int, prevField: Long, currentField: Long, nextField: Long, outputRGB: Long, algorithm: String = "yadif") { + // TAV deinterlacing - same logic as TEV + tevDeinterlace(frameCount, width, height, prevField, currentField, nextField, outputRGB, algorithm) + } + // Helper functions for motion compensation and block handling in two-pass mode private fun tevHandleSkipBlockTwoPass(startX: Int, startY: Int, currentRGBAddr: Long, prevRGBAddr: Long, width: Int, height: Int, thisAddrIncVec: Int, prevAddrIncVec: Int) { diff --git a/video_encoder/encoder_tav.c b/video_encoder/encoder_tav.c index 5a1cada..4b39477 100644 --- a/video_encoder/encoder_tav.c +++ b/video_encoder/encoder_tav.c @@ -2370,6 +2370,7 @@ static int write_tav_header(tav_encoder_t *enc) { fputc(version, enc->output_fp); // Video parameters + // For interlaced: enc->height is already halved internally, so double it back for display height uint16_t height = enc->progressive_mode ? enc->height : enc->height * 2; fwrite(&enc->width, sizeof(uint16_t), 1, enc->output_fp); fwrite(&height, sizeof(uint16_t), 1, enc->output_fp); @@ -3367,6 +3368,7 @@ int main(int argc, char *argv[]) { {"input", required_argument, 0, 'i'}, {"output", required_argument, 0, 'o'}, {"size", required_argument, 0, 's'}, + {"dimension", required_argument, 0, 's'}, {"fps", required_argument, 0, 'f'}, {"quality", required_argument, 0, 'q'}, {"quantizer", required_argument, 0, 'Q'}, @@ -3381,13 +3383,17 @@ int main(int argc, char *argv[]) { {"test", no_argument, 0, 't'}, {"lossless", no_argument, 0, 1000}, {"intra-only", no_argument, 0, 1006}, + {"intraonly", no_argument, 0, 1006}, {"ictcp", no_argument, 0, 1005}, {"no-perceptual-tuning", no_argument, 0, 1007}, {"no-dead-zone", no_argument, 0, 1013}, + {"no-deadzone", no_argument, 0, 1013}, {"encode-limit", required_argument, 0, 1008}, {"dump-frame", required_argument, 0, 1009}, {"fontrom-lo", required_argument, 0, 1011}, + {"fontrom-low", required_argument, 0, 1011}, {"fontrom-hi", required_argument, 0, 1012}, + {"fontrom-high", required_argument, 0, 1012}, {"zstd-level", required_argument, 0, 1014}, {"interlace", no_argument, 0, 1015}, {"interlaced", no_argument, 0, 1015}, @@ -3396,7 +3402,7 @@ int main(int argc, char *argv[]) { }; int c, option_index = 0; - while ((c = getopt_long(argc, argv, "i:o:s:f:q:Q:a:w:c:d:b:pS:vt", long_options, &option_index)) != -1) { + while ((c = getopt_long(argc, argv, "i:o:s:f:q:Q:a:w:c:d:b:S:vt?", long_options, &option_index)) != -1) { switch (c) { case 'i': enc->input_file = strdup(optarg); @@ -3570,6 +3576,15 @@ int main(int argc, char *argv[]) { enc->quantiser_cg = enc->quantiser_co; } + // Halve internal height for interlaced mode (FFmpeg will output half-height fields) + if (!enc->progressive_mode) { + enc->height = enc->height / 2; + if (enc->verbose) { + printf("Interlaced mode: internal height adjusted to %d\n", enc->height); + } + enc->intra_only = 1; + } + // disable perceptual tuning if wavelet filter is not CDF 9/7 if (enc->wavelet_filter != WAVELET_9_7_IRREVERSIBLE) { enc->perceptual_tuning = 0;