From 96d697e1583bb16dba4e8f8339ef0830cf84c652 Mon Sep 17 00:00:00 2001 From: minjaesong Date: Fri, 19 Dec 2025 21:41:51 +0900 Subject: [PATCH] iPF progressive mode decoder --- assets/disk0/tvdos/TVDOS.SYS | 16 +- assets/disk0/tvdos/bin/zfm.js | 2 + terranmon.txt | 6 +- .../torvald/tsvm/GraphicsJSR223Delegate.kt | 188 ++++++++++++++++++ 4 files changed, 206 insertions(+), 6 deletions(-) diff --git a/assets/disk0/tvdos/TVDOS.SYS b/assets/disk0/tvdos/TVDOS.SYS index c76e39d..2c30b5a 100644 --- a/assets/disk0/tvdos/TVDOS.SYS +++ b/assets/disk0/tvdos/TVDOS.SYS @@ -870,11 +870,21 @@ Object.freeze(_TVDOS.DRV.FS.DEVPT) _TVDOS.DRV.FS.DEVFBIPF = {} _TVDOS.DRV.FS.DEVFBIPF.pwrite = (fd, infilePtr, count, _2) => { - let decodefun = ([graphics.decodeIpf1, graphics.decodeIpf2])[sys.peek(infilePtr + 13)] + let flags = sys.peek(infilePtr+12) + let ipfType = sys.peek(infilePtr+13) + let isProgressive = (flags & 0x80) != 0 + let hasAlpha = (flags & 0x01) != 0 + + // Select decode function based on type and progressive flag + let decodefun + if (isProgressive) { + decodefun = ([graphics.decodeIpf1Progressive, graphics.decodeIpf2Progressive])[ipfType] + } else { + decodefun = ([graphics.decodeIpf1, graphics.decodeIpf2])[ipfType] + } + let width = sys.peek(infilePtr+8) | (sys.peek(infilePtr+9) << 8) let height = sys.peek(infilePtr+10) | (sys.peek(infilePtr+11) << 8) - let hasAlpha = (sys.peek(infilePtr+12) != 0) - let ipfType = sys.peek(infilePtr+13) let imgLen = sys.peek(infilePtr+24) | (sys.peek(infilePtr+25) << 8) | (sys.peek(infilePtr+26) << 16) | (sys.peek(infilePtr+27) << 24) let ipfbuf = sys.malloc(imgLen) diff --git a/assets/disk0/tvdos/bin/zfm.js b/assets/disk0/tvdos/bin/zfm.js index 56d9887..cd8a851 100644 --- a/assets/disk0/tvdos/bin/zfm.js +++ b/assets/disk0/tvdos/bin/zfm.js @@ -33,6 +33,7 @@ const COL_HL_EXT = { "mv2": 213, "mv3": 213, "tav": 213, + "ipf": 190, "ipf1": 190, "ipf2": 190, "txt": 223, @@ -51,6 +52,7 @@ const EXEC_FUNS = { "tav": (f) => _G.shell.execute(`playtav "${f}" -i`), "tad": (f) => _G.shell.execute(`playtad "${f}" -i`), "pcm": (f) => _G.shell.execute(`playpcm "${f}" -i`), + "ipf": (f) => _G.shell.execute(`decodeipf "${f}" -i`), "ipf1": (f) => _G.shell.execute(`decodeipf "${f}" -i`), "ipf2": (f) => _G.shell.execute(`decodeipf "${f}" -i`), "bas": (f) => _G.shell.execute(`basic "${f}"`), diff --git a/terranmon.txt b/terranmon.txt index 612a936..d9c5adb 100644 --- a/terranmon.txt +++ b/terranmon.txt @@ -562,7 +562,7 @@ NOTE FROM DEVELOPER TSVM Interchangeable Picture Format (aka iPF Type 1/2) -Image is divided into 4x4 blocks and each block is serialised, then the entire iPF blocks are gzipped +Image is divided into 4x4 blocks and each block is serialised, then the entire iPF blocks are Zstd-compressed # File Structure @@ -576,7 +576,7 @@ Image is divided into 4x4 blocks and each block is serialised, then the entire i uint8 Flags 0b p00z 000a - a: has alpha - - z: gzipped (p flag always sets this flag) + - z: Zstd-compressed (p flag always sets this flag) - p: progressive ordering (Adam7) uint8 iPF Type/Colour Mode 0: Type 1 (4:2:0 chroma subsampling; 2048 colours?) @@ -585,7 +585,7 @@ Image is divided into 4x4 blocks and each block is serialised, then the entire i uint32 UNCOMPRESSED SIZE (somewhat redundant but included for convenience) - Chroma Subsampled Blocks - Gzipped unless the z-flag is not set. + Zstd-compressed unless the z-flag is not set. 4x4 pixels are sampled, then divided into YCoCg planes. CoCg planes are "chroma subsampled" by 4:2:0, then quantised to 4 bits (8 bits for CoCg combined) Y plane is quantised to 4 bits diff --git a/tsvm_core/src/net/torvald/tsvm/GraphicsJSR223Delegate.kt b/tsvm_core/src/net/torvald/tsvm/GraphicsJSR223Delegate.kt index e565d03..68e029e 100644 --- a/tsvm_core/src/net/torvald/tsvm/GraphicsJSR223Delegate.kt +++ b/tsvm_core/src/net/torvald/tsvm/GraphicsJSR223Delegate.kt @@ -1339,6 +1339,194 @@ class GraphicsJSR223Delegate(private val vm: VM) { } + // Adam7 interlace pattern - pass number (1-7) for each pixel in 8x8 block + private val ADAM7_PASS = arrayOf( + intArrayOf(1, 6, 4, 6, 2, 6, 4, 6), + intArrayOf(7, 7, 7, 7, 7, 7, 7, 7), + intArrayOf(5, 6, 5, 6, 5, 6, 5, 6), + intArrayOf(7, 7, 7, 7, 7, 7, 7, 7), + intArrayOf(3, 6, 4, 6, 3, 6, 4, 6), + intArrayOf(7, 7, 7, 7, 7, 7, 7, 7), + intArrayOf(5, 6, 5, 6, 5, 6, 5, 6), + intArrayOf(7, 7, 7, 7, 7, 7, 7, 7) + ) + + /** + * Get Adam7 pass number for a block at (blockX, blockY). + * Uses the block's top-left pixel position to determine pass. + */ + private fun getAdam7Pass(blockX: Int, blockY: Int): Int { + val px = (blockX * 4) % 8 + val py = (blockY * 4) % 8 + return ADAM7_PASS[py][px] + } + + /** + * Build a mapping from progressive input order to (blockX, blockY) positions. + * Returns a list of Pair(blockX, blockY) in the order they appear in progressive data. + */ + private fun buildAdam7BlockOrder(blocksX: Int, blocksY: Int): List> { + val result = mutableListOf>() + for (pass in 1..7) { + for (by in 0 until blocksY) { + for (bx in 0 until blocksX) { + if (getAdam7Pass(bx, by) == pass) { + result.add(Pair(bx, by)) + } + } + } + } + return result + } + + /** + * Decode iPF1 with Adam7 progressive ordering. + * Blocks are stored in pass order (1-7), not raster order. + */ + fun decodeIpf1Progressive(srcPtr: Int, destRG: Int, destBA: Int, width: Int, height: Int, hasAlpha: Boolean) { + val sign = if (destRG >= 0) 1 else -1 + if (destRG * destBA < 0) throw IllegalArgumentException("Both destination memories must be on the same domain (both being Usermem or HWmem)") + val sptr = srcPtr.toLong() + val dptr1 = destRG.toLong() + val dptr2 = destBA.toLong() + var readCount = 0 + fun readShort() = + vm.peek(sptr + readCount++)!!.toUint() or vm.peek(sptr + readCount++)!!.toUint().shl(8) + + val blocksX = ceil(width / 4f).toInt() + val blocksY = ceil(height / 4f).toInt() + val blockOrder = buildAdam7BlockOrder(blocksX, blocksY) + + for ((blockX, blockY) in blockOrder) { + val rg = IntArray(16) + val ba = IntArray(16) + + val co = readShort() + val cg = readShort() + val y1 = readShort() + val y2 = readShort() + val y3 = readShort() + val y4 = readShort() + + var a1 = 65535; var a2 = 65535; var a3 = 65535; var a4 = 65535 + + if (hasAlpha) { + a1 = readShort() + a2 = readShort() + a3 = readShort() + a4 = readShort() + } + + var corner = ipf1YcocgToRGB(co and 15, cg and 15, y1, a1) + rg[0] = corner[0];ba[0] = corner[1] + rg[1] = corner[2];ba[1] = corner[3] + rg[4] = corner[4];ba[4] = corner[5] + rg[5] = corner[6];ba[5] = corner[7] + + corner = ipf1YcocgToRGB((co shr 4) and 15, (cg shr 4) and 15, y2, a2) + rg[2] = corner[0];ba[2] = corner[1] + rg[3] = corner[2];ba[3] = corner[3] + rg[6] = corner[4];ba[6] = corner[5] + rg[7] = corner[6];ba[7] = corner[7] + + corner = ipf1YcocgToRGB((co shr 8) and 15, (cg shr 8) and 15, y3, a3) + rg[8] = corner[0];ba[8] = corner[1] + rg[9] = corner[2];ba[9] = corner[3] + rg[12] = corner[4];ba[12] = corner[5] + rg[13] = corner[6];ba[13] = corner[7] + + corner = ipf1YcocgToRGB((co shr 12) and 15, (cg shr 12) and 15, y4, a4) + rg[10] = corner[0];ba[10] = corner[1] + rg[11] = corner[2];ba[11] = corner[3] + rg[14] = corner[4];ba[14] = corner[5] + rg[15] = corner[6];ba[15] = corner[7] + + // move decoded pixels into memory + for (py in 0..3) { for (px in 0..3) { + val ox = blockX * 4 + px + val oy = blockY * 4 + py + val offset = oy * 560 + ox + vm.poke(dptr1 + offset*sign, rg[py * 4 + px].toByte()) + vm.poke(dptr2 + offset*sign, ba[py * 4 + px].toByte()) + }} + } + } + + /** + * Decode iPF2 with Adam7 progressive ordering. + * Blocks are stored in pass order (1-7), not raster order. + */ + fun decodeIpf2Progressive(srcPtr: Int, destRG: Int, destBA: Int, width: Int, height: Int, hasAlpha: Boolean) { + val sign = if (destRG >= 0) 1 else -1 + if (destRG * destBA < 0) throw IllegalArgumentException("Both destination memories must be on the same domain (both being Usermem or HWmem)") + val sptr = srcPtr.toLong() + val dptr1 = destRG.toLong() + val dptr2 = destBA.toLong() + var readCount = 0 + fun readShort() = + vm.peek(sptr + readCount++)!!.toUint() or vm.peek(sptr + readCount++)!!.toUint().shl(8) + fun readInt() = + vm.peek(sptr + readCount++)!!.toUint() or vm.peek(sptr + readCount++)!!.toUint().shl(8) or vm.peek(sptr + readCount++)!!.toUint().shl(16) or vm.peek(sptr + readCount++)!!.toUint().shl(24) + + val blocksX = ceil(width / 4f).toInt() + val blocksY = ceil(height / 4f).toInt() + val blockOrder = buildAdam7BlockOrder(blocksX, blocksY) + + for ((blockX, blockY) in blockOrder) { + val rg = IntArray(16) + val ba = IntArray(16) + + val co = readInt() + val cg = readInt() + val y1 = readShort() + val y2 = readShort() + val y3 = readShort() + val y4 = readShort() + + var a1 = 65535; var a2 = 65535; var a3 = 65535; var a4 = 65535 + + if (hasAlpha) { + a1 = readShort() + a2 = readShort() + a3 = readShort() + a4 = readShort() + } + + var corner = ipf2YcocgToRGB(co and 15, (co shr 8) and 15, cg and 15, (cg shr 8) and 15, y1, a1) + rg[0] = corner[0];ba[0] = corner[1] + rg[1] = corner[2];ba[1] = corner[3] + rg[4] = corner[4];ba[4] = corner[5] + rg[5] = corner[6];ba[5] = corner[7] + + corner = ipf2YcocgToRGB((co shr 4) and 15, (co shr 12) and 15, (cg shr 4) and 15, (cg shr 12) and 15, y2, a2) + rg[2] = corner[0];ba[2] = corner[1] + rg[3] = corner[2];ba[3] = corner[3] + rg[6] = corner[4];ba[6] = corner[5] + rg[7] = corner[6];ba[7] = corner[7] + + corner = ipf2YcocgToRGB((co shr 16) and 15, (co shr 24) and 15, (cg shr 16) and 15, (cg shr 24) and 15, y3, a3) + rg[8] = corner[0];ba[8] = corner[1] + rg[9] = corner[2];ba[9] = corner[3] + rg[12] = corner[4];ba[12] = corner[5] + rg[13] = corner[6];ba[13] = corner[7] + + corner = ipf2YcocgToRGB((co shr 20) and 15, (co shr 28) and 15, (cg shr 20) and 15, (cg shr 28) and 15, y4, a4) + rg[10] = corner[0];ba[10] = corner[1] + rg[11] = corner[2];ba[11] = corner[3] + rg[14] = corner[4];ba[14] = corner[5] + rg[15] = corner[6];ba[15] = corner[7] + + // move decoded pixels into memory + for (py in 0..3) { for (px in 0..3) { + val ox = blockX * 4 + px + val oy = blockY * 4 + py + val offset = oy * 560 + ox + vm.poke(dptr1 + offset*sign, rg[py * 4 + px].toByte()) + vm.poke(dptr2 + offset*sign, ba[py * 4 + px].toByte()) + }} + } + } + private val SKIP = 0x00.toByte() private val PATCH = 0x01.toByte() private val REPEAT = 0x02.toByte()