mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-08 22:34:03 +09:00
More or less working deinterlacer
This commit is contained in:
@@ -438,8 +438,9 @@ const RGB_BUFFER_B = sys.malloc(FRAME_SIZE)
|
|||||||
|
|
||||||
// Static Yadif deinterlacing buffers (half-height field buffers for interlaced mode)
|
// Static Yadif deinterlacing buffers (half-height field buffers for interlaced mode)
|
||||||
const FIELD_SIZE = 560 * 224 * 3 // Half-height field buffer size
|
const FIELD_SIZE = 560 * 224 * 3 // Half-height field buffer size
|
||||||
const TEMP_FIELD_BUFFER = sys.malloc(FIELD_SIZE)
|
const CURR_FIELD_BUFFER = isInterlaced ? sys.malloc(FIELD_SIZE) : 0
|
||||||
const PREV_FIELD_BUFFER = sys.malloc(FIELD_SIZE)
|
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)
|
// Ping-pong buffer pointers (swap instead of copy)
|
||||||
let CURRENT_RGB_ADDR = RGB_BUFFER_A
|
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)
|
sys.memset(RGB_BUFFER_B, 0, FRAME_PIXELS * 3)
|
||||||
|
|
||||||
// Initialize Yadif field buffers to black
|
// 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(PREV_FIELD_BUFFER, 0, FIELD_SIZE)
|
||||||
|
sys.memset(NEXT_FIELD_BUFFER, 0, FIELD_SIZE)
|
||||||
|
|
||||||
// Initialize display framebuffer to black
|
// Initialize display framebuffer to black
|
||||||
sys.memset(DISPLAY_RG_ADDR, 0, FRAME_PIXELS) // Black in RG plane
|
sys.memset(DISPLAY_RG_ADDR, 0, FRAME_PIXELS) // Black in RG plane
|
||||||
@@ -461,6 +463,12 @@ let frameCount = 0
|
|||||||
let stopPlay = false
|
let stopPlay = false
|
||||||
let akku = FRAME_TIME
|
let akku = FRAME_TIME
|
||||||
let akku2 = 0.0
|
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 mp2Initialised = false
|
||||||
let audioFired = false
|
let audioFired = false
|
||||||
|
|
||||||
@@ -515,6 +523,20 @@ function setBiasLighting() {
|
|||||||
|
|
||||||
let blockDataPtr = sys.malloc(FRAME_SIZE)
|
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
|
// Main decoding loop - simplified for performance
|
||||||
try {
|
try {
|
||||||
let t1 = sys.nanoTime()
|
let t1 = sys.nanoTime()
|
||||||
@@ -583,7 +605,20 @@ try {
|
|||||||
try {
|
try {
|
||||||
let decodeStart = sys.nanoTime()
|
let decodeStart = sys.nanoTime()
|
||||||
let decodingHeight = isInterlaced ? (height / 2)|0 : height
|
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
|
decodeTime = (sys.nanoTime() - decodeStart) / 1000000.0 // Convert to milliseconds
|
||||||
|
|
||||||
// Upload RGB buffer to display framebuffer with dithering
|
// Upload RGB buffer to display framebuffer with dithering
|
||||||
@@ -593,9 +628,19 @@ try {
|
|||||||
|
|
||||||
|
|
||||||
// Defer audio playback until a first frame is sent
|
// Defer audio playback until a first frame is sent
|
||||||
if (!audioFired) {
|
if (isInterlaced) {
|
||||||
audio.play(0)
|
// fire audio after frame 1
|
||||||
audioFired = true
|
if (!audioFired && frameCount > 0) {
|
||||||
|
audio.play(0)
|
||||||
|
audioFired = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// fire audio after frame 0
|
||||||
|
if (!audioFired) {
|
||||||
|
audio.play(0)
|
||||||
|
audioFired = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
serial.println(`Frame ${frameCount}: Hardware ${colorSpace} decode failed: ${e}`)
|
serial.println(`Frame ${frameCount}: Hardware ${colorSpace} decode failed: ${e}`)
|
||||||
@@ -674,8 +719,9 @@ finally {
|
|||||||
if (blockDataPtr > 0) sys.free(blockDataPtr)
|
if (blockDataPtr > 0) sys.free(blockDataPtr)
|
||||||
if (RGB_BUFFER_A > 0) sys.free(RGB_BUFFER_A)
|
if (RGB_BUFFER_A > 0) sys.free(RGB_BUFFER_A)
|
||||||
if (RGB_BUFFER_B > 0) sys.free(RGB_BUFFER_B)
|
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 (PREV_FIELD_BUFFER > 0) sys.free(PREV_FIELD_BUFFER)
|
||||||
|
if (NEXT_FIELD_BUFFER > 0) sys.free(NEXT_FIELD_BUFFER)
|
||||||
|
|
||||||
audio.stop(0)
|
audio.stop(0)
|
||||||
audio.purgeQueue(0)
|
audio.purgeQueue(0)
|
||||||
|
|||||||
@@ -1522,6 +1522,7 @@ class GraphicsJSR223Delegate(private val vm: VM) {
|
|||||||
|
|
||||||
for (y in 0 until fieldHeight) {
|
for (y in 0 until fieldHeight) {
|
||||||
for (x in 0 until width) {
|
for (x in 0 until width) {
|
||||||
|
// fieldRGBAddr now contains sequential field data from extractFieldFromProgressive
|
||||||
val fieldOffset = (y * width + x) * 3
|
val fieldOffset = (y * width + x) * 3
|
||||||
val outputOffset = ((y * 2 + fieldParity) * 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...)
|
// 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!
|
// Odd field (1,3,5...) interpolates even lines (2,4,6...) - skip line 0!
|
||||||
if (y > 0 && y < fieldHeight - 1) {
|
if (y > 0 && y < fieldHeight - 1) {
|
||||||
val interpLine = if (fieldParity == 0) {
|
// Even field: interpolate odd progressive lines (1,3,5...)
|
||||||
y * 2 + 1 // Even field: interpolate odd progressive lines (1,3,5...)
|
// Odd field: interpolate even progressive lines (2,4,6...)
|
||||||
} else {
|
val interpLine = y * 2 + (1 - fieldParity)
|
||||||
y * 2 + 2 // Odd field: interpolate even progressive lines (2,4,6...)
|
|
||||||
}
|
|
||||||
// Skip interpolation if the line would be out of bounds
|
// Skip interpolation if the line would be out of bounds
|
||||||
if (interpLine < height) {
|
if (interpLine < height) {
|
||||||
val interpOutputOffset = (interpLine * width + x) * 3
|
val interpOutputOffset = (interpLine * width + x) * 3
|
||||||
|
|
||||||
for (c in 0..2) {
|
for (c in 0..2) {
|
||||||
// Get spatial neighbors
|
// Get spatial neighbors from sequential field data
|
||||||
val above = vm.peek(fieldRGBAddr + (fieldOffset - width * 3 + c) * fieldIncVec)!!.toInt() and 0xFF
|
val fieldStride = width * 3
|
||||||
val below = vm.peek(fieldRGBAddr + (fieldOffset + width * 3 + c) * fieldIncVec)!!.toInt() and 0xFF
|
val aboveOffset = fieldOffset - fieldStride + c
|
||||||
val current = vm.peek(fieldRGBAddr + (fieldOffset + c) * fieldIncVec)!!.toInt() and 0xFF
|
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
|
// Spatial interpolation
|
||||||
val spatialInterp = (above + below) / 2
|
val spatialInterp = (above + below) / 2
|
||||||
@@ -1556,8 +1574,9 @@ class GraphicsJSR223Delegate(private val vm: VM) {
|
|||||||
var temporalPred = spatialInterp
|
var temporalPred = spatialInterp
|
||||||
if (prevFieldAddr != 0L && nextFieldAddr != 0L) {
|
if (prevFieldAddr != 0L && nextFieldAddr != 0L) {
|
||||||
// Get temporal neighbors from same spatial position
|
// Get temporal neighbors from same spatial position
|
||||||
val prevPixel = (vm.peek(prevFieldAddr + (fieldOffset + c) * fieldIncVec)?.toInt() ?: current) and 0xFF
|
val tempFieldOffset = (y * width + x) * 3 + c // Compact field addressing
|
||||||
val nextPixel = (vm.peek(nextFieldAddr + (fieldOffset + c) * fieldIncVec)?.toInt() ?: current) and 0xFF
|
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
|
// Simple temporal interpolation
|
||||||
val tempInterp = (prevPixel + nextPixel) / 2
|
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
|
// cover up top two and bottom two lines with current border colour
|
||||||
// Even field: interpolate line 1 (first odd line)
|
val lines = arrayOf(0, 1, height - 2, height - 1)
|
||||||
// Odd field: interpolate line 0 using simple duplication (since no spatial neighbors exist)
|
for (x in 0 until width) {
|
||||||
if (fieldParity == 0) {
|
for (y in lines) {
|
||||||
// Even field: interpolate line 1 using line 0 and 2
|
val dest = (y * width + x) * 3
|
||||||
for (x in 0 until width) {
|
vm.poke(outputRGBAddr + dest + 0, vm.peek(-1299457)!!)
|
||||||
val outputOffset = (1 * width + x) * 3
|
vm.poke(outputRGBAddr + dest + 1, vm.peek(-1299458)!!)
|
||||||
val ref0Offset = (0 * width + x) * 3 // Line 0
|
vm.poke(outputRGBAddr + dest + 2, vm.peek(-1299459)!!)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1847,8 +1847,7 @@ class GraphicsJSR223Delegate(private val vm: VM) {
|
|||||||
*/
|
*/
|
||||||
fun tevDecode(blockDataPtr: Long, currentRGBAddr: Long, prevRGBAddr: Long,
|
fun tevDecode(blockDataPtr: Long, currentRGBAddr: Long, prevRGBAddr: Long,
|
||||||
width: Int, height: Int, qualityIndices: IntArray, frameCounter: Int,
|
width: Int, height: Int, qualityIndices: IntArray, frameCounter: Int,
|
||||||
debugMotionVectors: Boolean = false, tevVersion: Int = 2, isInterlaced: Boolean = false,
|
debugMotionVectors: Boolean = false, tevVersion: Int = 2) {
|
||||||
tempFieldBuffer: Long = 0L, prevFieldBuffer: Long = 0L) {
|
|
||||||
|
|
||||||
// height doesn't change when interlaced, because that's the encoder's output
|
// 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) {
|
fun tevDeinterlace(frameCounter: Int, width: Int, height: Int, prevField: Long, currentField: Long, nextField: Long, outputRGB: Long) {
|
||||||
// Use static buffers provided by playtev.js for better performance
|
// Apply Yadif deinterlacing: field -> progressive frame
|
||||||
// require(tempFieldBuffer != 0L) { "tempFieldBuffer must be provided for interlaced decoding" }
|
val fieldParity = (frameCounter + 1) % 2
|
||||||
// require(prevFieldBuffer != 0L) { "prevFieldBuffer must be provided for interlaced decoding" }
|
|
||||||
|
yadifDeinterlace(
|
||||||
// Copy the decoded field to temporary buffer
|
currentField, outputRGB, width, height * 2,
|
||||||
vm.memcpy(currentRGBAddr.toInt(), tempFieldBuffer.toInt(), width * height * 3)
|
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user