diff --git a/CLAUDE.md b/CLAUDE.md index cbf8d64..7bc4624 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,6 +12,12 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - TerranBASIC integration - Multiple platform build system +## Documentations + +Documentation for TSVM and TVDOS are available on `./doc/*.tex` as machine-readable format. + +Documentatino for TSVM architecture is available on `terranmon.txt` + ## Architecture ### Core Components diff --git a/assets/disk0/tvdos/bin/playtav.js b/assets/disk0/tvdos/bin/playtav.js index 15acc5d..e00315a 100644 --- a/assets/disk0/tvdos/bin/playtav.js +++ b/assets/disk0/tvdos/bin/playtav.js @@ -368,6 +368,8 @@ let header = { width: 0, height: 0, fps: 0, + fps_num: 0, // Fractional FPS numerator (from XFPS or derived from fps) + fps_den: 1, // Fractional FPS denominator (from XFPS, default 1) totalFrames: 0, waveletFilter: 0, // TAV-specific: wavelet filter type decompLevels: 0, // TAV-specific: decomposition levels @@ -382,6 +384,22 @@ let header = { fileRole: 0 } +// Helper function to parse XFPS string ("num/den" format) and update header +function parseXFPS(xfpsStr) { + let parts = xfpsStr.split("/") + if (parts.length === 2) { + let num = parseInt(parts[0], 10) + let den = parseInt(parts[1], 10) + if (!isNaN(num) && !isNaN(den) && den > 0) { + header.fps_num = num + header.fps_den = den + header.fps = num / den + return true + } + } + return false +} + // Read and validate header for (let i = 0; i < 8; i++) { header.magic[i] = seqread.readOneByte() @@ -409,6 +427,9 @@ header.version = seqread.readOneByte() header.width = seqread.readShort() header.height = seqread.readShort() header.fps = seqread.readOneByte() +// Set default fractional fps (will be overridden by XFPS if present) +header.fps_num = header.fps +header.fps_den = 1 header.totalFrames = seqread.readInt() header.waveletFilter = seqread.readOneByte() header.decompLevels = seqread.readOneByte() @@ -461,7 +482,7 @@ const isLossless = (header.videoFlags & 0x04) !== 0 console.log(`TAV Decoder`) console.log(`Resolution: ${header.width}x${header.height}`) -console.log(`FPS: ${header.fps}`) +console.log(`FPS: ${header.fps === 255 ? "(see XFPS)" : header.fps}`) console.log(`Total frames: ${header.totalFrames}`) console.log(`Wavelet filter: ${header.waveletFilter === WAVELET_5_3_REVERSIBLE ? "5/3 reversible" : header.waveletFilter === WAVELET_9_7_IRREVERSIBLE ? "9/7 irreversible" : header.waveletFilter === WAVELET_BIORTHOGONAL_13_7 ? "Biorthogonal 13/7" : header.waveletFilter === WAVELET_DD4 ? "DD-4" : header.waveletFilter === WAVELET_HAAR ? "Haar" : "unknown"}`) console.log(`Decomposition levels: ${header.decompLevels}`) @@ -492,20 +513,32 @@ if (isTapFile) { // Skip non-video packets until we find the image data while (packetType !== TAV_PACKET_IFRAME) { if (packetType === TAV_PACKET_EXTENDED_HDR) { - // Skip extended header - parse key-value pairs properly + // Parse extended header - look for XFPS let numPairs = seqread.readShort() for (let i = 0; i < numPairs; i++) { - // Skip key (4 bytes) + // Read key (4 bytes) let keyBytes = seqread.readBytes(4) + let key = "" + for (let j = 0; j < 4; j++) { + key += String.fromCharCode(sys.peek(keyBytes + j)) + } sys.free(keyBytes) - // Read value type and skip value + // Read value type and value let valueType = seqread.readOneByte() if (valueType === 0x04) { // Uint64 - 8 bytes seqread.skip(8) } else if (valueType === 0x10) { // Bytes - length-prefixed let length = seqread.readShort() let dataBytes = seqread.readBytes(length) + // Check for XFPS key + if (key === "XFPS") { + let xfpsStr = "" + for (let j = 0; j < length; j++) { + xfpsStr += String.fromCharCode(sys.peek(dataBytes + j)) + } + parseXFPS(xfpsStr) + } sys.free(dataBytes) } } @@ -1291,7 +1324,7 @@ try { console.log(`\nStarting file ${currentFileIndex}:`) console.log(`Resolution: ${header.width}x${header.height}`) - console.log(`FPS: ${header.fps}`) + console.log(`FPS: ${header.fps === 255 ? "(see XFPS)" : header.fps}`) console.log(`Total frames: ${header.totalFrames}`) console.log(`Wavelet filter: ${header.waveletFilter === WAVELET_5_3_REVERSIBLE ? "5/3 reversible" : header.waveletFilter === WAVELET_9_7_IRREVERSIBLE ? "9/7 irreversible" : header.waveletFilter === WAVELET_BIORTHOGONAL_13_7 ? "Biorthogonal 13/7" : header.waveletFilter === WAVELET_DD4 ? "DD-4" : header.waveletFilter === WAVELET_HAAR ? "Haar" : "unknown"}`) console.log(`Quality: Y=${header.qualityY}, Co=${header.qualityCo}, Cg=${header.qualityCg}`) @@ -1825,7 +1858,19 @@ try { } sys.free(dataBytes) - if (interactive) { + // Parse XFPS if present (always try, not just when fps=255) + if (key === "XFPS") { + if (parseXFPS(dataStr)) { + // Update frame timing with new fps + frametime = 1000000000.0 / header.fps + FRAME_TIME = 1.0 / header.fps + if (interactive) { + serial.println(` ${key}: ${dataStr} -> ${header.fps.toFixed(3)} fps`) + } + } else if (interactive) { + serial.println(` ${key}: "${dataStr}" (parse failed)`) + } + } else if (interactive) { serial.println(` ${key}: "${dataStr}"`) } } else { diff --git a/terranmon.txt b/terranmon.txt index 65b4e6d..194474c 100644 --- a/terranmon.txt +++ b/terranmon.txt @@ -303,8 +303,10 @@ MMIO 0: 560x448, 256 Colours, 1 layer 1: 280x224, 256 Colours, 4 layers 2: 280x224, 4096 Colours, 2 layers - 3: 560x448, 256 Colours, 2 layers (if bank 2 is not installed, will fall back to mode 0) - 4: 560x448, 4096 Colours, 1 layer (if bank 2 is not installed, will fall back to mode 0) + 3: 560x448, 256 Colours, 2 layers (if bank 2 is not installed, mode change will not happen) + 4: 560x448, 4096 Colours, 1 layer (if bank 2 is not installed, mode change will not happen) + 5: 560x448, 15-bit colour, 1 layer (if bank 2 is not installed, mode change will not happen) + 8: 560x448, 24-bit colour, 1 layer (if bank 3 and 4 are not installed, mode change will not happen) 4096 is also known as "direct colour mode" (4096 colours * 16 transparency -> 65536 colours) Two layers are grouped to make a frame, "low layer" contains RG colours and "high layer" has BA colours, Red and Blue occupies MSBs diff --git a/tsvm_core/src/net/torvald/tsvm/GraphicsJSR223Delegate.kt b/tsvm_core/src/net/torvald/tsvm/GraphicsJSR223Delegate.kt index 68e029e..522cdf0 100644 --- a/tsvm_core/src/net/torvald/tsvm/GraphicsJSR223Delegate.kt +++ b/tsvm_core/src/net/torvald/tsvm/GraphicsJSR223Delegate.kt @@ -149,6 +149,16 @@ class GraphicsJSR223Delegate(private val vm: VM) { } } + fun plotPixelMode1(x: Int, y: Int, colour: Int, plane: Int) { + getFirstGPU()?.let { + val planesize = it.config.width * it.config.height / 4 + if (x in 0 until it.config.width/2 && y in 0 until it.config.height/2) { + it.poke(y.toLong() * it.config.width/2 + x + planesize * plane, colour.toByte()) + it.applyDelay() + } + } + } + /** * Sets absolute position of scrolling */ diff --git a/tsvm_core/src/net/torvald/tsvm/VM.kt b/tsvm_core/src/net/torvald/tsvm/VM.kt index 8f1f0ef..6c130b9 100644 --- a/tsvm_core/src/net/torvald/tsvm/VM.kt +++ b/tsvm_core/src/net/torvald/tsvm/VM.kt @@ -624,6 +624,8 @@ class VM( return null } + private val zeroBlock = ByteArray(MALLOC_UNIT) + internal fun malloc(size: Int): Int { if (size <= 0) throw IllegalArgumentException("Invalid malloc size: $size") @@ -635,6 +637,22 @@ class VM( return blockStart * MALLOC_UNIT } + internal fun calloc(size: Int): Int { + if (size <= 0) throw IllegalArgumentException("Invalid malloc size: $size") + + val allocBlocks = ceil(size.toDouble() / MALLOC_UNIT).toInt() + val blockStart = findEmptySpace(allocBlocks) ?: throw OutOfMemoryError("No space for $allocBlocks blocks ($size bytes requested)") + + allocatedBlockCount += allocBlocks + mallocSizes[blockStart] = allocBlocks + + for (i in 0 until allocBlocks) { + UnsafeHelper.memcpyRaw(zeroBlock, UnsafeHelper.getArrayOffset(zeroBlock), null, usermem.ptr + (blockStart + i) * MALLOC_UNIT, MALLOC_UNIT.toLong()) + } + + return blockStart * MALLOC_UNIT + } + internal fun free(ptr: Int) { val index = ptr / MALLOC_UNIT val count = mallocSizes[index] ?: throw OutOfMemoryError("No allocation for pointer 0x${ptr.toHex()}") diff --git a/tsvm_core/src/net/torvald/tsvm/VMJSR223Delegate.kt b/tsvm_core/src/net/torvald/tsvm/VMJSR223Delegate.kt index e55ec4a..73822b0 100644 --- a/tsvm_core/src/net/torvald/tsvm/VMJSR223Delegate.kt +++ b/tsvm_core/src/net/torvald/tsvm/VMJSR223Delegate.kt @@ -109,6 +109,7 @@ class VMJSR223Delegate(private val vm: VM) { fun nanoTime() = System.nanoTime() fun malloc(size: Int) = vm.malloc(size) + fun calloc(size: Int) = vm.calloc(size) fun memset(dest: Int, ch: Int, count: Int) = vm.memset(dest, ch, count) fun free(ptr: Int) = vm.free(ptr) fun forceAlloc(ptr: Int, size: Int) = vm.forceAlloc(ptr, size)