diff --git a/assets/disk0/tvdos/bin/playtav.js b/assets/disk0/tvdos/bin/playtav.js index 71d223b..2334e6d 100644 --- a/assets/disk0/tvdos/bin/playtav.js +++ b/assets/disk0/tvdos/bin/playtav.js @@ -53,6 +53,8 @@ let subtitlePosition = 0 // 0=bottom center (default) // Parse command line options let interactive = false +let userDefinedFilmGrain = false +let filmGrainLevel = null if (exec_args.length > 2) { for (let i = 2; i < exec_args.length; i++) { @@ -60,6 +62,26 @@ if (exec_args.length > 2) { if (arg === "-i") { interactive = true } + else if (arg.startsWith("--filter-film-grain")) { + // Extract noise level from argument + const parts = arg.split(/[=\s]/) + if (parts.length > 1) { + const level = parseInt(parts[1]) + if (!isNaN(level) && level >= 1 && level <= 32767) { + filmGrainLevel = level + userDefinedFilmGrain = true + } + } + // Try next argument if no '=' found + else if (i + 1 < exec_args.length) { + const level = parseInt(exec_args[i + 1]) + if (!isNaN(level) && level >= 1 && level <= 32767) { + filmGrainLevel = level + userDefinedFilmGrain = true + i++ // Skip next arg + } + } + } } } @@ -271,6 +293,13 @@ if (header.version < 1 || header.version > 8) { return } +function setFilmGrainLevel(header) { + // decide film grain strength by quality level + filmGrainLevel = [9,6,-4,3,-2,-2,-2][header.qualityLevel - 1] +} + +setFilmGrainLevel(header) + // Helper function to decode channel layout name function getChannelLayoutName(layout) { switch (layout) { @@ -670,6 +699,7 @@ try { currentCueIndex++ } totalFilesProcessed++ + setFilmGrainLevel(header) console.log(`\nStarting file ${currentFileIndex}:`) console.log(`Resolution: ${header.width}x${header.height}`) @@ -726,6 +756,8 @@ try { serial.println(` FIELD_SIZE: ${FIELD_SIZE}`) } + let thisFrameNoiseLevel = (filmGrainLevel >= 0) ? filmGrainLevel : -(filmGrainLevel - (trueFrameCount % 2)) + // Call new TAV hardware decoder that handles Zstd decompression internally // Note: No longer using JS gzip.decompFromTo - Kotlin handles Zstd natively decoderDbgInfo = graphics.tavDecodeCompressed( @@ -739,7 +771,8 @@ try { header.waveletFilter, // TAV-specific parameter header.decompLevels, // TAV-specific parameter isLossless, - header.version // TAV version for colour space detection + header.version, // TAV version for colour space detection + thisFrameNoiseLevel // Undocumented spooky noise filter ) decodeTime = (sys.nanoTime() - decodeStart) / 1000000.0 diff --git a/tsvm_core/src/net/torvald/tsvm/GraphicsJSR223Delegate.kt b/tsvm_core/src/net/torvald/tsvm/GraphicsJSR223Delegate.kt index dad3f0c..7ea747f 100644 --- a/tsvm_core/src/net/torvald/tsvm/GraphicsJSR223Delegate.kt +++ b/tsvm_core/src/net/torvald/tsvm/GraphicsJSR223Delegate.kt @@ -4450,8 +4450,8 @@ class GraphicsJSR223Delegate(private val vm: VM) { // New tavDecode function that accepts compressed data and decompresses internally fun tavDecodeCompressed(compressedDataPtr: Long, compressedSize: Int, currentRGBAddr: Long, prevRGBAddr: Long, - width: Int, height: Int, qIndex: Int, qYGlobal: Int, qCoGlobal: Int, qCgGlobal: Int, channelLayout: Int, - frameCount: Int, waveletFilter: Int = 1, decompLevels: Int = 6, isLossless: Boolean = false, tavVersion: Int = 1): HashMap { + width: Int, height: Int, qIndex: Int, qYGlobal: Int, qCoGlobal: Int, qCgGlobal: Int, channelLayout: Int, + frameCount: Int, waveletFilter: Int = 1, decompLevels: Int = 6, isLossless: Boolean = false, tavVersion: Int = 1, filmGrainLevel: Int = 0): HashMap { // Read compressed data from VM memory into byte array val compressedData = ByteArray(compressedSize) @@ -4481,7 +4481,7 @@ class GraphicsJSR223Delegate(private val vm: VM) { // Call the existing tavDecode function with decompressed data tavDecode(decompressedBuffer.toLong(), currentRGBAddr, prevRGBAddr, width, height, qIndex, qYGlobal, qCoGlobal, qCgGlobal, channelLayout, - frameCount, waveletFilter, decompLevels, isLossless, tavVersion) + frameCount, waveletFilter, decompLevels, isLossless, tavVersion, filmGrainLevel) } finally { // Clean up allocated buffer @@ -4497,7 +4497,7 @@ class GraphicsJSR223Delegate(private val vm: VM) { // Original tavDecode function for backward compatibility (now handles decompressed data) fun tavDecode(blockDataPtr: Long, currentRGBAddr: Long, prevRGBAddr: Long, width: Int, height: Int, qIndex: Int, qYGlobal: Int, qCoGlobal: Int, qCgGlobal: Int, channelLayout: Int, - frameCount: Int, waveletFilter: Int = 1, decompLevels: Int = 6, isLossless: Boolean = false, tavVersion: Int = 1): HashMap { + frameCount: Int, waveletFilter: Int = 1, decompLevels: Int = 6, isLossless: Boolean = false, tavVersion: Int = 1, filmGrainLevel: Int = 0): HashMap { val dbgOut = HashMap() @@ -4552,13 +4552,13 @@ class GraphicsJSR223Delegate(private val vm: VM) { // Decode DWT coefficients directly to RGB buffer readPtr = tavDecodeDWTIntraTileRGB(qIndex, qYGlobal, channelLayout, readPtr, tileX, tileY, currentRGBAddr, width, height, qY, qCo, qCg, - waveletFilter, decompLevels, isLossless, tavVersion, isMonoblock) + waveletFilter, decompLevels, isLossless, tavVersion, isMonoblock, filmGrainLevel) } 0x02 -> { // TAV_MODE_DELTA // Coefficient delta encoding for efficient P-frames readPtr = tavDecodeDeltaTileRGB(readPtr, channelLayout, tileX, tileY, currentRGBAddr, width, height, qY, qCo, qCg, - waveletFilter, decompLevels, isLossless, tavVersion, isMonoblock) + waveletFilter, decompLevels, isLossless, tavVersion, isMonoblock, filmGrainLevel) } } } @@ -4573,7 +4573,7 @@ class GraphicsJSR223Delegate(private val vm: VM) { private fun tavDecodeDWTIntraTileRGB(qIndex: Int, qYGlobal: Int, channelLayout: Int, readPtr: Long, tileX: Int, tileY: Int, currentRGBAddr: Long, width: Int, height: Int, qY: Int, qCo: Int, qCg: Int, - waveletFilter: Int, decompLevels: Int, isLossless: Boolean, tavVersion: Int, isMonoblock: Boolean = false): Long { + waveletFilter: Int, decompLevels: Int, isLossless: Boolean, tavVersion: Int, isMonoblock: Boolean = false, filmGrainLevel: Int = 0): Long { // Determine coefficient count based on mode val coeffCount = if (isMonoblock) { // Monoblock mode: entire frame @@ -4674,6 +4674,16 @@ class GraphicsJSR223Delegate(private val vm: VM) { dequantiseDWTSubbandsPerceptual(qIndex, qYGlobal, quantisedCo, coTile, subbands, qCo.toFloat(), true, decompLevels) dequantiseDWTSubbandsPerceptual(qIndex, qYGlobal, quantisedCg, cgTile, subbands, qCg.toFloat(), true, decompLevels) + // Apply spooky noise filter if enabled + if (filmGrainLevel > 0) { + val random = java.util.Random() + for (i in 0 until coeffCount) { + yTile[i] += (random.nextInt(filmGrainLevel * 2 + 1) - filmGrainLevel).toFloat() + coTile[i] += (random.nextInt(filmGrainLevel * 2 + 1) - filmGrainLevel).toFloat() + cgTile[i] += (random.nextInt(filmGrainLevel * 2 + 1) - filmGrainLevel).toFloat() + } + } + // Debug: Check coefficient values before inverse DWT if (tavDebugCurrentFrameNumber == tavDebugFrameTarget) { var maxYDequant = 0.0f @@ -4730,6 +4740,16 @@ class GraphicsJSR223Delegate(private val vm: VM) { cgTile[i] = quantisedCg[i] * qCg.toFloat() } + // Apply spooky noise filter if enabled + if (filmGrainLevel > 0) { + val random = java.util.Random() + for (i in 0 until coeffCount) { + yTile[i] += (random.nextInt(filmGrainLevel * 2 + 1) - filmGrainLevel).toFloat() + coTile[i] += (random.nextInt(filmGrainLevel * 2 + 1) - filmGrainLevel).toFloat() + cgTile[i] += (random.nextInt(filmGrainLevel * 2 + 1) - filmGrainLevel).toFloat() + } + } + // Debug: Uniform quantisation subband analysis for comparison if (tavDebugCurrentFrameNumber == tavDebugFrameTarget) { val tileWidth = if (isMonoblock) width else TAV_PADDED_TILE_SIZE_X @@ -5185,7 +5205,7 @@ class GraphicsJSR223Delegate(private val vm: VM) { private fun tavDecodeDeltaTileRGB(readPtr: Long, channelLayout: Int, tileX: Int, tileY: Int, currentRGBAddr: Long, width: Int, height: Int, qY: Int, qCo: Int, qCg: Int, - waveletFilter: Int, decompLevels: Int, isLossless: Boolean, tavVersion: Int, isMonoblock: Boolean = false): Long { + waveletFilter: Int, decompLevels: Int, isLossless: Boolean, tavVersion: Int, isMonoblock: Boolean = false, filmGrainLevel: Int = 0): Long { val tileIdx = if (isMonoblock) { 0 // Single tile index for monoblock @@ -5302,6 +5322,16 @@ class GraphicsJSR223Delegate(private val vm: VM) { currentCg[i] = prevCg[i] + (deltaCg[i].toFloat() * qCg) } + // Apply spooky noise filter if enabled + if (filmGrainLevel > 0) { + val random = java.util.Random() + for (i in 0 until coeffCount) { + currentY[i] += (random.nextInt(filmGrainLevel * 2 + 1) - filmGrainLevel).toFloat() + currentCo[i] += (random.nextInt(filmGrainLevel * 2 + 1) - filmGrainLevel).toFloat() + currentCg[i] += (random.nextInt(filmGrainLevel * 2 + 1) - filmGrainLevel).toFloat() + } + } + // Store current coefficients as previous for next frame tavPreviousCoeffsY!![tileIdx] = currentY.clone() tavPreviousCoeffsCo!![tileIdx] = currentCo.clone()