diff --git a/assets/disk0/tvdos/bin/playtev.js b/assets/disk0/tvdos/bin/playtev.js index 68884b5..d3012e0 100644 --- a/assets/disk0/tvdos/bin/playtev.js +++ b/assets/disk0/tvdos/bin/playtev.js @@ -2,6 +2,7 @@ // TSVM Enhanced Video (TEV) Format Decoder - YCoCg-R 4:2:0 Version // Usage: playtev moviefile.tev [options] // Options: -i (interactive), -debug-mv (show motion vector debug visualization) +// -deinterlace=algorithm (yadif or bwdif, default: yadif) const WIDTH = 560 const HEIGHT = 448 @@ -41,6 +42,7 @@ let subtitlePosition = 0 // 0=bottom center (default) const interactive = exec_args[2] && exec_args[2].toLowerCase() == "-i" const debugMotionVectors = exec_args[2] && exec_args[2].toLowerCase() == "-debug-mv" +const deinterlaceAlgorithm = "yadif" const fullFilePath = _G.shell.resolvePathInput(exec_args[1]) const FILE_LENGTH = files.open(fullFilePath.full).size @@ -387,12 +389,13 @@ let hasAudio = !!(flags & 1) let hasSubtitle = !!(flags & 2) let videoFlags = seqread.readOneByte() let isInterlaced = !!(videoFlags & 1) +let isNTSC = !!(videoFlags & 2) let unused2 = seqread.readOneByte() serial.println(`Video metadata:`) serial.println(` Frames: ${totalFrames}`) -serial.println(` FPS: ${fps}`) +serial.println(` FPS: ${(isNTSC) ? (fps * 1000 / 1001) : fps}`) serial.println(` Duration: ${totalFrames / fps}`) serial.println(` Audio: ${hasAudio ? "Yes" : "No"}`) serial.println(` Resolution: ${width}x${height}, ${isInterlaced ? "interlaced" : "progressive"}`) @@ -460,6 +463,7 @@ sys.memset(DISPLAY_RG_ADDR, 0, FRAME_PIXELS) // Black in RG plane sys.memset(DISPLAY_BA_ADDR, 15, FRAME_PIXELS) // Black with alpha=15 (opaque) in BA plane let frameCount = 0 +let trueFrameCount = 0 let stopPlay = false let akku = FRAME_TIME let akku2 = 0.0 @@ -537,10 +541,12 @@ function rotateFieldBuffers() { nextFieldAddr = temp } +let frameDuped = false + // Main decoding loop - simplified for performance try { let t1 = sys.nanoTime() - while (!stopPlay && seqread.getReadCount() < FILE_LENGTH && frameCount < totalFrames) { + while (!stopPlay && seqread.getReadCount() < FILE_LENGTH && trueFrameCount < totalFrames) { // Handle interactive controls if (interactive) { @@ -560,6 +566,7 @@ try { // Sync packet - frame complete frameCount++ + trueFrameCount++ // Swap ping-pong buffers instead of expensive memcpy (752KB copy eliminated!) let temp = CURRENT_RGB_ADDR @@ -603,29 +610,39 @@ try { // Hardware-accelerated TEV decoding to RGB buffers (YCoCg-R or XYB based on version) try { - let decodeStart = sys.nanoTime() - let decodingHeight = isInterlaced ? (height / 2)|0 : height - - if (isInterlaced) { - // For interlaced: decode current frame into currentFieldAddr - // For display: use prevFieldAddr as current, currentFieldAddr as next - graphics.tevDecode(blockDataPtr, nextFieldAddr, currentFieldAddr, width, decodingHeight, [qualityY, qualityCo, qualityCg], frameCount, debugMotionVectors, version) - graphics.tevDeinterlace(frameCount, width, decodingHeight, prevFieldAddr, currentFieldAddr, nextFieldAddr, CURRENT_RGB_ADDR) + // duplicate every 1000th frame (pass a turn every 1000n+501st) if NTSC + if (!isInterlaced || frameCount % 1000 != 501 || frameDuped) { + frameDuped = false - // Rotate field buffers for next frame: NEXT -> CURRENT -> PREV - rotateFieldBuffers() - } else { - // Progressive or first frame: normal decoding without temporal prediction - graphics.tevDecode(blockDataPtr, CURRENT_RGB_ADDR, PREV_RGB_ADDR, width, decodingHeight, [qualityY, qualityCo, qualityCg], frameCount, debugMotionVectors, version) + let decodeStart = sys.nanoTime() + let decodingHeight = isInterlaced ? (height / 2)|0 : height + + if (isInterlaced) { + // For interlaced: decode current frame into currentFieldAddr + // For display: use prevFieldAddr as current, currentFieldAddr as next + graphics.tevDecode(blockDataPtr, nextFieldAddr, currentFieldAddr, width, decodingHeight, [qualityY, qualityCo, qualityCg], trueFrameCount, debugMotionVectors, version) + graphics.tevDeinterlace(trueFrameCount, width, decodingHeight, prevFieldAddr, currentFieldAddr, nextFieldAddr, CURRENT_RGB_ADDR, deinterlaceAlgorithm) + + // Rotate field buffers for next frame: NEXT -> CURRENT -> PREV + rotateFieldBuffers() + } else { + // Progressive or first frame: normal decoding without temporal prediction + graphics.tevDecode(blockDataPtr, CURRENT_RGB_ADDR, PREV_RGB_ADDR, width, decodingHeight, [qualityY, qualityCo, qualityCg], trueFrameCount, debugMotionVectors, version) + } + + decodeTime = (sys.nanoTime() - decodeStart) / 1000000.0 // Convert to milliseconds + + + // Upload RGB buffer to display framebuffer with dithering + let uploadStart = sys.nanoTime() + graphics.uploadRGBToFramebuffer(CURRENT_RGB_ADDR, width, height, frameCount) + uploadTime = (sys.nanoTime() - uploadStart) / 1000000.0 // Convert to milliseconds + } + else { + frameCount -= 1 + frameDuped = true + serial.println(`Frame ${frameCount}: Duplicating previous frame`) } - - decodeTime = (sys.nanoTime() - decodeStart) / 1000000.0 // Convert to milliseconds - - // Upload RGB buffer to display framebuffer with dithering - let uploadStart = sys.nanoTime() - graphics.uploadRGBToFramebuffer(CURRENT_RGB_ADDR, width, height, frameCount) - uploadTime = (sys.nanoTime() - uploadStart) / 1000000.0 // Convert to milliseconds - // Defer audio playback until a first frame is sent if (isInterlaced) { diff --git a/terranmon.txt b/terranmon.txt index fb4386a..9a3e785 100644 --- a/terranmon.txt +++ b/terranmon.txt @@ -708,6 +708,7 @@ DCT-based compression, motion compensation, and efficient temporal coding. - bit 1 = has subtitle uint8 Video Flags - bit 0 = is interlaced (should be default for most non-archival TEV videos) + - bit 1 = is NTSC framerate (repeat every 1000th frame) uint8 Reserved, fill with zero ## Packet Types diff --git a/tsvm_core/src/net/torvald/tsvm/GraphicsJSR223Delegate.kt b/tsvm_core/src/net/torvald/tsvm/GraphicsJSR223Delegate.kt index 8c509a7..b93c8ed 100644 --- a/tsvm_core/src/net/torvald/tsvm/GraphicsJSR223Delegate.kt +++ b/tsvm_core/src/net/torvald/tsvm/GraphicsJSR223Delegate.kt @@ -1625,6 +1625,123 @@ class GraphicsJSR223Delegate(private val vm: VM) { vm.memsetI24(outputRGBAddr.toInt() + destB, col, width * 6) } + /** + * BWDIF (Bob Weaver Deinterlacing with Interpolation and Filtering) implementation + * Advanced motion-adaptive deinterlacing with better temporal prediction than YADIF + */ + fun bwdifDeinterlace(fieldRGBAddr: Long, outputRGBAddr: Long, width: Int, height: Int, + prevFieldAddr: Long, nextFieldAddr: Long, fieldParity: Int, + fieldIncVec: Int, outputIncVec: Int) { + + val fieldHeight = height / 2 + + for (y in 0 until fieldHeight) { + for (x in 0 until width) { + val fieldOffset = (y * width + x) * 3 + val outputOffset = ((y * 2 + fieldParity) * width + x) * 3 + + // Copy current field lines directly (no interpolation needed) with loop unrolling + vm.poke(outputRGBAddr + (outputOffset + 0) * outputIncVec, vm.peek(fieldRGBAddr + (fieldOffset + 0) * fieldIncVec)!!) + vm.poke(outputRGBAddr + (outputOffset + 1) * outputIncVec, vm.peek(fieldRGBAddr + (fieldOffset + 1) * fieldIncVec)!!) + vm.poke(outputRGBAddr + (outputOffset + 2) * outputIncVec, vm.peek(fieldRGBAddr + (fieldOffset + 2) * fieldIncVec)!!) + + // Interpolate missing lines using BWDIF algorithm + if (y > 0 && y < fieldHeight - 1) { + val interpLine = if (fieldParity == 0) { + y * 2 + 1 // Even field: interpolate odd progressive lines (1,3,5...) + } else { + y * 2 + 2 // Odd field: interpolate even progressive lines (2,4,6...) + } + + if (interpLine < height) { + val interpOutputOffset = (interpLine * width + x) * 3 + + for (c in 0..2) { + // Get spatial neighbors from sequential field data + val fieldStride = width * 3 + val aboveOffset = fieldOffset - fieldStride + c + val belowOffset = fieldOffset + fieldStride + c + val currentOffset = fieldOffset + c + + // Ensure we don't read out of bounds + val above = if (y > 0) { + vm.peek(fieldRGBAddr + aboveOffset * fieldIncVec)!!.toInt() and 0xFF + } else { + vm.peek(fieldRGBAddr + currentOffset * fieldIncVec)!!.toInt() and 0xFF + } + + val below = if (y < fieldHeight - 1) { + vm.peek(fieldRGBAddr + belowOffset * fieldIncVec)!!.toInt() and 0xFF + } else { + vm.peek(fieldRGBAddr + currentOffset * fieldIncVec)!!.toInt() and 0xFF + } + + val current = vm.peek(fieldRGBAddr + currentOffset * fieldIncVec)!!.toInt() and 0xFF + + // BWDIF temporal prediction - more sophisticated than YADIF + var interpolatedValue = (above + below) / 2 // Default spatial interpolation + + if (prevFieldAddr != 0L && nextFieldAddr != 0L) { + // Get temporal neighbors + val tempFieldOffset = (y * width + x) * 3 + c + val prevPixel = (vm.peek(prevFieldAddr + tempFieldOffset * fieldIncVec)?.toInt() ?: current) and 0xFF + val nextPixel = (vm.peek(nextFieldAddr + tempFieldOffset * fieldIncVec)?.toInt() ?: current) and 0xFF + + // BWDIF-inspired temporal differences (adapted for 3-frame window) + // Note: True BWDIF uses 5 frames, we adapt to 3-frame constraint + + // Get spatial neighbors from previous and next fields for temporal comparison + // Use same addressing pattern as working YADIF implementation + val prevAboveOffset = if (y > 0) ((y-1) * width + x) * 3 + c else tempFieldOffset + val prevBelowOffset = if (y < fieldHeight - 1) ((y+1) * width + x) * 3 + c else tempFieldOffset + val nextAboveOffset = if (y > 0) ((y-1) * width + x) * 3 + c else tempFieldOffset + val nextBelowOffset = if (y < fieldHeight - 1) ((y+1) * width + x) * 3 + c else tempFieldOffset + + val prevAbove = (vm.peek(prevFieldAddr + prevAboveOffset * fieldIncVec)?.toInt() ?: above) and 0xFF + val prevBelow = (vm.peek(prevFieldAddr + prevBelowOffset * fieldIncVec)?.toInt() ?: below) and 0xFF + val nextAbove = (vm.peek(nextFieldAddr + nextAboveOffset * fieldIncVec)?.toInt() ?: above) and 0xFF + val nextBelow = (vm.peek(nextFieldAddr + nextBelowOffset * fieldIncVec)?.toInt() ?: below) and 0xFF + + // BWDIF temporal differences adapted to 3-frame window + val temporalDiff0 = kotlin.math.abs(prevPixel - nextPixel) // Main temporal difference + val temporalDiff1 = (kotlin.math.abs(prevAbove - above) + kotlin.math.abs(prevBelow - below)) / 2 // Previous frame spatial consistency + val temporalDiff2 = (kotlin.math.abs(nextAbove - above) + kotlin.math.abs(nextBelow - below)) / 2 // Next frame spatial consistency + val maxTemporalDiff = kotlin.math.max(kotlin.math.max(temporalDiff0 / 2, temporalDiff1), temporalDiff2) + + val spatialDiff = kotlin.math.abs(above - below) + + if (maxTemporalDiff > 16) { // Conservative threshold + val temporalInterp = (prevPixel + nextPixel) / 2 + val spatialInterp = (above + below) / 2 + + // BWDIF-style decision making + interpolatedValue = if (spatialDiff < maxTemporalDiff) { + temporalInterp // Trust temporal when spatial is stable + } else { + spatialInterp // Trust spatial when temporal is unreliable + } + } else { + // Low temporal variation: use spatial like YADIF + interpolatedValue = (above + below) / 2 + } + } + + vm.poke(outputRGBAddr + (interpOutputOffset + c) * outputIncVec, + interpolatedValue.coerceIn(0, 255).toByte()) + } + } + } + } + } + + // Cover up border lines like YADIF + val destT = 0 + val destB = (height - 2) * width * 3 + val col = (vm.peek(-1299457)!!.toUint() shl 16) or (vm.peek(-1299458)!!.toUint() shl 8) or vm.peek(-1299459)!!.toUint() + vm.memsetI24(outputRGBAddr.toInt() + destT, col, width * 6) + vm.memsetI24(outputRGBAddr.toInt() + destB, col, width * 6) + } + fun tevYcocgToRGB(yBlock: IntArray, coBlock: IntArray, cgBlock: IntArray): IntArray { val rgbData = IntArray(16 * 16 * 3) // R,G,B for 16x16 pixels @@ -2236,17 +2353,37 @@ class GraphicsJSR223Delegate(private val vm: VM) { } } - fun tevDeinterlace(frameCounter: Int, width: Int, height: Int, prevField: Long, currentField: Long, nextField: Long, outputRGB: Long) { - // Apply Yadif deinterlacing: field -> progressive frame + fun tevDeinterlace(frameCounter: Int, width: Int, height: Int, prevField: Long, currentField: Long, nextField: Long, outputRGB: Long, algorithm: String = "yadif") { + // Apply selected deinterlacing algorithm: field -> progressive frame val fieldParity = (frameCounter + 1) % 2 - yadifDeinterlace( - currentField, outputRGB, width, height * 2, - prevField, nextField, // Now we have next field for temporal prediction! - fieldParity, - 1, 1 - ) - + when (algorithm.lowercase()) { + "bwdif" -> { + bwdifDeinterlace( + currentField, outputRGB, width, height * 2, + prevField, nextField, + fieldParity, + 1, 1 + ) + } + "yadif", "" -> { + yadifDeinterlace( + currentField, outputRGB, width, height * 2, + prevField, nextField, + fieldParity, + 1, 1 + ) + } + else -> { + // Default to YADIF for unknown algorithms + yadifDeinterlace( + currentField, outputRGB, width, height * 2, + prevField, nextField, + fieldParity, + 1, 1 + ) + } + } } diff --git a/video_encoder/encoder_tev.c b/video_encoder/encoder_tev.c index 7627a32..c46d28e 100644 --- a/video_encoder/encoder_tev.c +++ b/video_encoder/encoder_tev.c @@ -162,6 +162,7 @@ typedef struct { int has_subtitles; int output_to_stdout; int progressive_mode; // 0 = interlaced (default), 1 = progressive + int is_ntsc_framerate; // 1 if framerate denominator is 1001, 0 otherwise int qualityIndex; // -q option int qualityY; int qualityCo; @@ -1418,6 +1419,7 @@ static tev_encoder_t* init_encoder(void) { enc->height = DEFAULT_HEIGHT; enc->fps = 0; // Will be detected from input enc->output_fps = 0; // No frame rate conversion by default + enc->is_ntsc_framerate = 0; // Will be detected from input enc->verbose = 0; enc->subtitle_file = NULL; enc->has_subtitles = 0; @@ -1531,7 +1533,7 @@ static int write_tev_header(FILE *output, tev_encoder_t *enc) { uint8_t qualityCo = enc->qualityCo; uint8_t qualityCg = enc->qualityCg; uint8_t flags = (enc->has_audio) | (enc->has_subtitles << 1); - uint8_t video_flags = enc->progressive_mode ? 0 : 1; // bit 0 = is_interlaced (inverted from progressive) + uint8_t video_flags = (enc->progressive_mode ? 0 : 1) | (enc->is_ntsc_framerate ? 2 : 0); // bit 0 = is_interlaced, bit 1 = is_ntsc_framerate uint8_t reserved = 0; fwrite(&width, 2, 1, output); @@ -1741,7 +1743,7 @@ static int get_video_metadata(tev_encoder_t *config) { while (line && line_num < 2) { switch (line_num) { - case 0: // Line format: "framerate,framecount" (e.g., "24000/1001,4423") + case 0: // Line format: "framerate,framecount" (e.g., "30000/1001,4423") { char *comma = strchr(line, ','); if (comma) { @@ -1750,8 +1752,10 @@ static int get_video_metadata(tev_encoder_t *config) { int num, den; if (sscanf(line, "%d/%d", &num, &den) == 2) { config->fps = (den > 0) ? (int)round((float)num/(float)den) : 30; + config->is_ntsc_framerate = (den == 1001) ? 1 : 0; } else { config->fps = (int)round(atof(line)); + config->is_ntsc_framerate = 0; } // Parse frame count (second part) config->total_frames = atoi(comma + 1); @@ -1778,7 +1782,11 @@ static int get_video_metadata(tev_encoder_t *config) { fprintf(stderr, "Video metadata:\n"); fprintf(stderr, " Frames: %d\n", config->total_frames); - fprintf(stderr, " FPS: %d\n", config->fps); + if (config->is_ntsc_framerate) { + fprintf(stderr, " FPS: %.2f\n", config->fps * 1000.f / 1001.f); + } else { + fprintf(stderr, " FPS: %d\n", config->fps); + } fprintf(stderr, " Duration: %.2fs\n", config->duration); fprintf(stderr, " Audio: %s\n", config->has_audio ? "Yes" : "No"); fprintf(stderr, " Resolution: %dx%d (%s)\n", config->width, config->height,