diff --git a/assets/disk0/tvdos/bin/playtev.js b/assets/disk0/tvdos/bin/playtev.js index 6132784..7561cdc 100644 --- a/assets/disk0/tvdos/bin/playtev.js +++ b/assets/disk0/tvdos/bin/playtev.js @@ -3,6 +3,7 @@ // Usage: playtev moviefile.tev [options] // Options: -i (interactive), -debug-mv (show motion vector debug visualization) // -deinterlace=algorithm (yadif or bwdif, default: yadif) +// -nodeblock (disble deblocking filter) const WIDTH = 560 const HEIGHT = 448 @@ -40,9 +41,26 @@ let subtitleVisible = false let subtitleText = "" 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" +// Parse command line options +let interactive = false +let debugMotionVectors = false +let deinterlaceAlgorithm = "yadif" +let enableDeblocking = true // Default: enabled (use -nodeblock to disable) + +if (exec_args.length > 2) { + for (let i = 2; i < exec_args.length; i++) { + const arg = exec_args[i].toLowerCase() + if (arg === "-i") { + interactive = true + } else if (arg === "-debug-mv") { + debugMotionVectors = true + } else if (arg === "-nodeblock") { + enableDeblocking = false + } else if (arg.startsWith("-deinterlace=")) { + deinterlaceAlgorithm = arg.substring(13) + } + } +} const fullFilePath = _G.shell.resolvePathInput(exec_args[1]) const FILE_LENGTH = files.open(fullFilePath.full).size @@ -374,7 +392,7 @@ if (version !== TEV_VERSION_YCOCG && version !== TEV_VERSION_XYB) { let colorSpace = (version === TEV_VERSION_XYB) ? "XYB" : "YCoCg-R" if (interactive) { con.move(1,1) - println(`Push and hold Backspace to exit | TEV Format ${version} (${colorSpace})`) + println(`Push and hold Backspace to exit | TEV Format ${version} (${colorSpace}) | Deblocking: ${enableDeblocking ? 'ON' : 'OFF'}`) } let width = seqread.readShort() @@ -628,7 +646,7 @@ try { // Hardware-accelerated TEV decoding to RGB buffers (YCoCg-R or XYB based on version) try { // duplicate every 1000th frame (pass a turn every 1000n+501st) if NTSC - if (!isInterlaced || frameCount % 1000 != 501 || frameDuped) { + if (!isNTSC || frameCount % 1000 != 501 || frameDuped) { frameDuped = false let decodeStart = sys.nanoTime() @@ -637,14 +655,14 @@ try { 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.tevDecode(blockDataPtr, nextFieldAddr, currentFieldAddr, width, decodingHeight, [qualityY, qualityCo, qualityCg], trueFrameCount, debugMotionVectors, version, enableDeblocking) 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) + graphics.tevDecode(blockDataPtr, CURRENT_RGB_ADDR, PREV_RGB_ADDR, width, decodingHeight, [qualityY, qualityCo, qualityCg], trueFrameCount, debugMotionVectors, version, enableDeblocking) } decodeTime = (sys.nanoTime() - decodeStart) / 1000000.0 // Convert to milliseconds diff --git a/tsvm_core/src/net/torvald/tsvm/GraphicsJSR223Delegate.kt b/tsvm_core/src/net/torvald/tsvm/GraphicsJSR223Delegate.kt index 12dc784..73d5ad1 100644 --- a/tsvm_core/src/net/torvald/tsvm/GraphicsJSR223Delegate.kt +++ b/tsvm_core/src/net/torvald/tsvm/GraphicsJSR223Delegate.kt @@ -1964,6 +1964,167 @@ class GraphicsJSR223Delegate(private val vm: VM) { return xybData } + /** + * Advanced TEV Deblocking Filter - Reduces blocking artifacts from 16x16 macroblocks + * + * Uses gradient analysis and adaptive filtering to handle: + * - Quantized smooth gradients appearing as discrete blocks + * - Diagonal edges crossing block boundaries causing color banding + * - Texture preservation to avoid over-smoothing genuine edges + * + * @param rgbAddr RGB frame buffer address (24-bit: R,G,B per pixel) + * @param width Frame width in pixels + * @param height Frame height in pixels + * @param blockSize Size of blocks (16 for TEV format) + * @param strength Filter strength (0.0-1.0, higher = more smoothing) + */ + private fun tevDeblockingFilter(rgbAddr: Long, width: Int, height: Int, + blockSize: Int = 16, strength: Float = 0.4f) { + val blocksX = (width + blockSize - 1) / blockSize + val blocksY = (height + blockSize - 1) / blockSize + val thisAddrIncVec: Long = if (rgbAddr < 0) -1 else 1 + + // Helper function to get pixel value safely + fun getPixel(x: Int, y: Int, c: Int): Int { + if (x < 0 || y < 0 || x >= width || y >= height) return 0 + val offset = (y.toLong() * width + x) * 3 + c + return vm.peek(rgbAddr + offset * thisAddrIncVec)!!.toUint().toInt() + } + + // Helper function to set pixel value safely + fun setPixel(x: Int, y: Int, c: Int, value: Int) { + if (x < 0 || y < 0 || x >= width || y >= height) return + val offset = (y.toLong() * width + x) * 3 + c + vm.poke(rgbAddr + offset * thisAddrIncVec, value.coerceIn(0, 255).toByte()) + } + + // Detect if pixels form a smooth gradient (quantized) + fun isQuantizedGradient(p0: Int, p1: Int, p2: Int, p3: Int): Boolean { + // Check for step-like transitions typical of quantized gradients + val d01 = kotlin.math.abs(p1 - p0) + val d12 = kotlin.math.abs(p2 - p1) + val d23 = kotlin.math.abs(p3 - p2) + + // Look for consistent small steps (quantized gradient) + val avgStep = (d01 + d12 + d23) / 3.0f + val stepVariance = kotlin.math.abs(d01 - avgStep) + kotlin.math.abs(d12 - avgStep) + kotlin.math.abs(d23 - avgStep) + + return avgStep in 3.0f..25.0f && stepVariance < avgStep * 0.8f + } + + // Apply horizontal deblocking (vertical edges between blocks) + for (by in 0 until blocksY) { + for (bx in 1 until blocksX) { + val blockEdgeX = bx * blockSize + if (blockEdgeX >= width) continue + + for (y in (by * blockSize) until minOf((by + 1) * blockSize, height)) { + for (c in 0..2) { // RGB components + // Sample 4 pixels across the block boundary: [left2][left1] | [right1][right2] + val left2 = getPixel(blockEdgeX - 2, y, c) + val left1 = getPixel(blockEdgeX - 1, y, c) + val right1 = getPixel(blockEdgeX, y, c) + val right2 = getPixel(blockEdgeX + 1, y, c) + + val edgeDiff = kotlin.math.abs(right1 - left1) + + // Skip strong edges (likely genuine features) + if (edgeDiff > 50) continue + + // Check for quantized gradient pattern + if (isQuantizedGradient(left2, left1, right1, right2)) { + // Apply gradient-preserving smoothing + val gradientLeft = left1 - left2 + val gradientRight = right2 - right1 + val avgGradient = (gradientLeft + gradientRight) / 2.0f + + val smoothedLeft1 = (left2 + avgGradient).toInt() + val smoothedRight1 = (right2 - avgGradient).toInt() + + // Blend with original based on strength + val blendLeft = (left1 * (1.0f - strength) + smoothedLeft1 * strength).toInt() + val blendRight = (right1 * (1.0f - strength) + smoothedRight1 * strength).toInt() + + setPixel(blockEdgeX - 1, y, c, blendLeft) + setPixel(blockEdgeX, y, c, blendRight) + } + // Check for color banding on diagonal features + else if (edgeDiff in 8..35) { + // Look at diagonal context to detect banding + val diagContext = kotlin.math.abs(getPixel(blockEdgeX - 1, y - 1, c) - getPixel(blockEdgeX, y + 1, c)) + + if (diagContext < edgeDiff * 1.5f) { + // Likely diagonal banding - apply directional smoothing + val blend = 0.3f * strength + val blendLeft = (left1 * (1.0f - blend) + right1 * blend).toInt() + val blendRight = (right1 * (1.0f - blend) + left1 * blend).toInt() + + setPixel(blockEdgeX - 1, y, c, blendLeft) + setPixel(blockEdgeX, y, c, blendRight) + } + } + } + } + } + } + + // Apply vertical deblocking (horizontal edges between blocks) + for (by in 1 until blocksY) { + for (bx in 0 until blocksX) { + val blockEdgeY = by * blockSize + if (blockEdgeY >= height) continue + + for (x in (bx * blockSize) until minOf((bx + 1) * blockSize, width)) { + for (c in 0..2) { // RGB components + // Sample 4 pixels across the block boundary: [top2][top1] | [bottom1][bottom2] + val top2 = getPixel(x, blockEdgeY - 2, c) + val top1 = getPixel(x, blockEdgeY - 1, c) + val bottom1 = getPixel(x, blockEdgeY, c) + val bottom2 = getPixel(x, blockEdgeY + 1, c) + + val edgeDiff = kotlin.math.abs(bottom1 - top1) + + // Skip strong edges (likely genuine features) + if (edgeDiff > 50) continue + + // Check for quantized gradient pattern + if (isQuantizedGradient(top2, top1, bottom1, bottom2)) { + // Apply gradient-preserving smoothing + val gradientTop = top1 - top2 + val gradientBottom = bottom2 - bottom1 + val avgGradient = (gradientTop + gradientBottom) / 2.0f + + val smoothedTop1 = (top2 + avgGradient).toInt() + val smoothedBottom1 = (bottom2 - avgGradient).toInt() + + // Blend with original based on strength + val blendTop = (top1 * (1.0f - strength) + smoothedTop1 * strength).toInt() + val blendBottom = (bottom1 * (1.0f - strength) + smoothedBottom1 * strength).toInt() + + setPixel(x, blockEdgeY - 1, c, blendTop) + setPixel(x, blockEdgeY, c, blendBottom) + } + // Check for color banding on diagonal features + else if (edgeDiff in 8..35) { + // Look at diagonal context to detect banding + val diagContext = kotlin.math.abs(getPixel(x - 1, blockEdgeY - 1, c) - getPixel(x + 1, blockEdgeY, c)) + + if (diagContext < edgeDiff * 1.5f) { + // Likely diagonal banding - apply directional smoothing + val blend = 0.3f * strength + val blendTop = (top1 * (1.0f - blend) + bottom1 * blend).toInt() + val blendBottom = (bottom1 * (1.0f - blend) + top1 * blend).toInt() + + setPixel(x, blockEdgeY - 1, c, blendTop) + setPixel(x, blockEdgeY, c, blendBottom) + } + } + } + } + } + } + } + /** * Hardware-accelerated TEV frame decoder for YCoCg-R 4:2:0 format * Decodes compressed TEV block data directly to framebuffer @@ -1978,7 +2139,8 @@ 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) { + debugMotionVectors: Boolean = false, tevVersion: Int = 2, + enableDeblocking: Boolean = true) { // height doesn't change when interlaced, because that's the encoder's output @@ -2369,6 +2531,11 @@ class GraphicsJSR223Delegate(private val vm: VM) { } } } + + // Apply deblocking filter if enabled to reduce blocking artifacts + if (enableDeblocking) { + tevDeblockingFilter(currentRGBAddr, width, height) + } } fun tevDeinterlace(frameCounter: Int, width: Int, height: Int, prevField: Long, currentField: Long, nextField: Long, outputRGB: Long, algorithm: String = "yadif") {