From 8abdb8b9a6cf97c15bde90067de5af7fa7f1dfdf Mon Sep 17 00:00:00 2001 From: minjaesong Date: Tue, 2 Sep 2025 22:23:27 +0900 Subject: [PATCH] More or less working deinterlacer --- assets/disk0/tvdos/bin/playtev.js | 62 +++++++-- .../torvald/tsvm/GraphicsJSR223Delegate.kt | 120 ++++++++---------- 2 files changed, 105 insertions(+), 77 deletions(-) diff --git a/assets/disk0/tvdos/bin/playtev.js b/assets/disk0/tvdos/bin/playtev.js index 7982406..68884b5 100644 --- a/assets/disk0/tvdos/bin/playtev.js +++ b/assets/disk0/tvdos/bin/playtev.js @@ -438,8 +438,9 @@ const RGB_BUFFER_B = sys.malloc(FRAME_SIZE) // Static Yadif deinterlacing buffers (half-height field buffers for interlaced mode) const FIELD_SIZE = 560 * 224 * 3 // Half-height field buffer size -const TEMP_FIELD_BUFFER = sys.malloc(FIELD_SIZE) -const PREV_FIELD_BUFFER = sys.malloc(FIELD_SIZE) +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 // For temporal prediction // Ping-pong buffer pointers (swap instead of copy) let CURRENT_RGB_ADDR = RGB_BUFFER_A @@ -450,8 +451,9 @@ sys.memset(RGB_BUFFER_A, 0, FRAME_PIXELS * 3) sys.memset(RGB_BUFFER_B, 0, FRAME_PIXELS * 3) // Initialize Yadif field buffers to black -sys.memset(TEMP_FIELD_BUFFER, 0, FIELD_SIZE) +sys.memset(CURR_FIELD_BUFFER, 0, FIELD_SIZE) sys.memset(PREV_FIELD_BUFFER, 0, FIELD_SIZE) +sys.memset(NEXT_FIELD_BUFFER, 0, FIELD_SIZE) // Initialize display framebuffer to black sys.memset(DISPLAY_RG_ADDR, 0, FRAME_PIXELS) // Black in RG plane @@ -461,6 +463,12 @@ let frameCount = 0 let stopPlay = false let akku = FRAME_TIME let akku2 = 0.0 + +// Frame buffering for temporal prediction (interlaced mode only) +let bufferedFrames = [] // Queue of decoded frames for temporal prediction +let frameBuffer1 = null // Current frame data +let frameBuffer2 = null // Previous frame data +let frameDisplayDelay = 1 // Display frames 1 frame delayed for temporal prediction let mp2Initialised = false let audioFired = false @@ -515,6 +523,20 @@ function setBiasLighting() { let blockDataPtr = sys.malloc(FRAME_SIZE) +// Streaming frame buffer rotation for temporal prediction +// Buffers rotate: NEXT -> CURRENT -> PREV each frame +let currentFieldAddr = CURR_FIELD_BUFFER // Currently being decoded +let prevFieldAddr = PREV_FIELD_BUFFER // Previous field for temporal prediction +let nextFieldAddr = NEXT_FIELD_BUFFER // Next field for temporal prediction + +function rotateFieldBuffers() { + // Rotate buffers: NEXT -> CURRENT -> PREV + let temp = prevFieldAddr + prevFieldAddr = currentFieldAddr + currentFieldAddr = nextFieldAddr + nextFieldAddr = temp +} + // Main decoding loop - simplified for performance try { let t1 = sys.nanoTime() @@ -583,7 +605,20 @@ try { try { let decodeStart = sys.nanoTime() let decodingHeight = isInterlaced ? (height / 2)|0 : height - graphics.tevDecode(blockDataPtr, CURRENT_RGB_ADDR, PREV_RGB_ADDR, width, decodingHeight, [qualityY, qualityCo, qualityCg], frameCount, debugMotionVectors, version, isInterlaced, TEMP_FIELD_BUFFER, PREV_FIELD_BUFFER) + + 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) + + // 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) + } + decodeTime = (sys.nanoTime() - decodeStart) / 1000000.0 // Convert to milliseconds // Upload RGB buffer to display framebuffer with dithering @@ -593,9 +628,19 @@ try { // Defer audio playback until a first frame is sent - if (!audioFired) { - audio.play(0) - audioFired = true + if (isInterlaced) { + // fire audio after frame 1 + if (!audioFired && frameCount > 0) { + audio.play(0) + audioFired = true + } + } + else { + // fire audio after frame 0 + if (!audioFired) { + audio.play(0) + audioFired = true + } } } catch (e) { serial.println(`Frame ${frameCount}: Hardware ${colorSpace} decode failed: ${e}`) @@ -674,8 +719,9 @@ finally { if (blockDataPtr > 0) sys.free(blockDataPtr) if (RGB_BUFFER_A > 0) sys.free(RGB_BUFFER_A) if (RGB_BUFFER_B > 0) sys.free(RGB_BUFFER_B) - if (TEMP_FIELD_BUFFER > 0) sys.free(TEMP_FIELD_BUFFER) + if (CURR_FIELD_BUFFER > 0) sys.free(CURR_FIELD_BUFFER) if (PREV_FIELD_BUFFER > 0) sys.free(PREV_FIELD_BUFFER) + if (NEXT_FIELD_BUFFER > 0) sys.free(NEXT_FIELD_BUFFER) audio.stop(0) audio.purgeQueue(0) diff --git a/tsvm_core/src/net/torvald/tsvm/GraphicsJSR223Delegate.kt b/tsvm_core/src/net/torvald/tsvm/GraphicsJSR223Delegate.kt index 0fa49b6..bbd3663 100644 --- a/tsvm_core/src/net/torvald/tsvm/GraphicsJSR223Delegate.kt +++ b/tsvm_core/src/net/torvald/tsvm/GraphicsJSR223Delegate.kt @@ -1522,6 +1522,7 @@ class GraphicsJSR223Delegate(private val vm: VM) { for (y in 0 until fieldHeight) { for (x in 0 until width) { + // fieldRGBAddr now contains sequential field data from extractFieldFromProgressive val fieldOffset = (y * width + x) * 3 val outputOffset = ((y * 2 + fieldParity) * width + x) * 3 @@ -1534,20 +1535,37 @@ class GraphicsJSR223Delegate(private val vm: VM) { // Even field (0,2,4...) interpolates odd lines (1,3,5...) // Odd field (1,3,5...) interpolates even lines (2,4,6...) - skip line 0! 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...) - } + // Even field: interpolate odd progressive lines (1,3,5...) + // Odd field: interpolate even progressive lines (2,4,6...) + val interpLine = y * 2 + (1 - fieldParity) + // Skip interpolation if the line would be out of bounds if (interpLine < height) { val interpOutputOffset = (interpLine * width + x) * 3 for (c in 0..2) { - // Get spatial neighbors - val above = vm.peek(fieldRGBAddr + (fieldOffset - width * 3 + c) * fieldIncVec)!!.toInt() and 0xFF - val below = vm.peek(fieldRGBAddr + (fieldOffset + width * 3 + c) * fieldIncVec)!!.toInt() and 0xFF - val current = vm.peek(fieldRGBAddr + (fieldOffset + c) * fieldIncVec)!!.toInt() and 0xFF + // 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 { + // Use current pixel for top edge + vm.peek(fieldRGBAddr + currentOffset * fieldIncVec)!!.toInt() and 0xFF + } + + val below = if (y < fieldHeight - 1) { + vm.peek(fieldRGBAddr + belowOffset * fieldIncVec)!!.toInt() and 0xFF + } else { + // Use current pixel for bottom edge + vm.peek(fieldRGBAddr + currentOffset * fieldIncVec)!!.toInt() and 0xFF + } + + val current = vm.peek(fieldRGBAddr + currentOffset * fieldIncVec)!!.toInt() and 0xFF // Spatial interpolation val spatialInterp = (above + below) / 2 @@ -1556,8 +1574,9 @@ class GraphicsJSR223Delegate(private val vm: VM) { var temporalPred = spatialInterp if (prevFieldAddr != 0L && nextFieldAddr != 0L) { // Get temporal neighbors from same spatial position - val prevPixel = (vm.peek(prevFieldAddr + (fieldOffset + c) * fieldIncVec)?.toInt() ?: current) and 0xFF - val nextPixel = (vm.peek(nextFieldAddr + (fieldOffset + c) * fieldIncVec)?.toInt() ?: current) and 0xFF + val tempFieldOffset = (y * width + x) * 3 + c // Compact field addressing + val prevPixel = (vm.peek(prevFieldAddr + tempFieldOffset * fieldIncVec)?.toInt() ?: current) and 0xFF + val nextPixel = (vm.peek(nextFieldAddr + tempFieldOffset * fieldIncVec)?.toInt() ?: current) and 0xFF // Simple temporal interpolation val tempInterp = (prevPixel + nextPixel) / 2 @@ -1598,33 +1617,14 @@ class GraphicsJSR223Delegate(private val vm: VM) { } } - // Handle edge cases: interpolate first missing line for each field - // Even field: interpolate line 1 (first odd line) - // Odd field: interpolate line 0 using simple duplication (since no spatial neighbors exist) - if (fieldParity == 0) { - // Even field: interpolate line 1 using line 0 and 2 - for (x in 0 until width) { - val outputOffset = (1 * width + x) * 3 - val ref0Offset = (0 * width + x) * 3 // Line 0 - val ref2Offset = (2 * width + x) * 3 // Line 2 - - for (c in 0..2) { - val pixel0 = vm.peek(outputRGBAddr + (ref0Offset + c) * outputIncVec)!!.toInt() and 0xFF - val pixel2 = vm.peek(outputRGBAddr + (ref2Offset + c) * outputIncVec)!!.toInt() and 0xFF - val interpValue = (pixel0 + pixel2) / 2 - vm.poke(outputRGBAddr + (outputOffset + c) * outputIncVec, interpValue.toByte()) - } - } - } else { - // Odd field: interpolate line 0 by duplicating line 1 - for (x in 0 until width) { - val outputOffset = (0 * width + x) * 3 - val ref1Offset = (1 * width + x) * 3 // Line 1 (first odd line) - - for (c in 0..2) { - val refPixel = vm.peek(outputRGBAddr + (ref1Offset + c) * outputIncVec)!! - vm.poke(outputRGBAddr + (outputOffset + c) * outputIncVec, refPixel) - } + // cover up top two and bottom two lines with current border colour + val lines = arrayOf(0, 1, height - 2, height - 1) + for (x in 0 until width) { + for (y in lines) { + val dest = (y * width + x) * 3 + vm.poke(outputRGBAddr + dest + 0, vm.peek(-1299457)!!) + vm.poke(outputRGBAddr + dest + 1, vm.peek(-1299458)!!) + vm.poke(outputRGBAddr + dest + 2, vm.peek(-1299459)!!) } } } @@ -1847,8 +1847,7 @@ class GraphicsJSR223Delegate(private val vm: VM) { */ fun tevDecode(blockDataPtr: Long, currentRGBAddr: Long, prevRGBAddr: Long, width: Int, height: Int, qualityIndices: IntArray, frameCounter: Int, - debugMotionVectors: Boolean = false, tevVersion: Int = 2, isInterlaced: Boolean = false, - tempFieldBuffer: Long = 0L, prevFieldBuffer: Long = 0L) { + debugMotionVectors: Boolean = false, tevVersion: Int = 2) { // height doesn't change when interlaced, because that's the encoder's output @@ -2239,36 +2238,19 @@ class GraphicsJSR223Delegate(private val vm: VM) { } } } - - // Apply Yadif deinterlacing if this is an interlaced frame - if (isInterlaced) { - // Use static buffers provided by playtev.js for better performance -// require(tempFieldBuffer != 0L) { "tempFieldBuffer must be provided for interlaced decoding" } -// require(prevFieldBuffer != 0L) { "prevFieldBuffer must be provided for interlaced decoding" } - - // Copy the decoded field to temporary buffer - vm.memcpy(currentRGBAddr.toInt(), tempFieldBuffer.toInt(), width * height * 3) + } + + fun tevDeinterlace(frameCounter: Int, width: Int, height: Int, prevField: Long, currentField: Long, nextField: Long, outputRGB: Long) { + // Apply Yadif deinterlacing: 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 + ) - // Apply Yadif deinterlacing: field -> progressive frame - // For temporal prediction, we need proper field management - val fieldParity = frameCounter % 2 - val prevFieldAddr = if (prevRGBAddr != 0L) { - // Extract the corresponding field from the previous progressive frame - // Even field lines: y = 0, 2, 4, 6... - // Odd field lines: y = 1, 3, 5, 7... - extractFieldFromProgressive(prevRGBAddr, prevFieldBuffer, width, height * 2, fieldParity, thisAddrIncVec) - prevFieldBuffer - } else { - 0L - } - - yadifDeinterlace( - tempFieldBuffer, currentRGBAddr, width, height * 2, - prevFieldAddr, 0L, // Use previous field, no next field available - fieldParity, - thisAddrIncVec, thisAddrIncVec - ) - } }