22 Commits

Author SHA1 Message Date
minjaesong
2ac084acd7 psg.mjs 2026-04-16 02:05:21 +09:00
minjaesong
1208690c4f graal update with graal compiler 2026-04-13 22:20:50 +09:00
minjaesong
ca977b074d vm update 2026-04-10 20:36:55 +09:00
minjaesong
102801d8b0 tav: vendor string update 2026-01-21 22:00:47 +09:00
minjaesong
10351bafb1 tav fix: fractional framerate breaking audio encoding 2026-01-21 21:41:03 +09:00
minjaesong
9310885260 tav fix: webm being recognised as still image 2026-01-21 21:21:11 +09:00
minjaesong
b10d5d3a34 tav: vendor string update 2025-12-30 09:36:19 +09:00
minjaesong
86b44565e0 tav: revived adaptive gop alloc 2025-12-30 09:27:14 +09:00
minjaesong
54b61fb436 tav: support for fractional framerate 2025-12-25 18:26:56 +09:00
minjaesong
4afe3816c7 tav: extended header XFPS 2025-12-25 13:26:38 +09:00
minjaesong
f09dd66185 playtav: fixed using wrong flag 2025-12-25 11:20:21 +09:00
minjaesong
b590415231 playtav: still picture playback 2025-12-25 11:13:34 +09:00
minjaesong
237d3d6fd2 playtav: playing next file must not work with still images 2025-12-25 03:21:07 +09:00
minjaesong
3421d71012 TAV: still picture impl 2025-12-23 04:00:53 +09:00
minjaesong
5d5576c077 fix: mmio-based readKey() having delayed event because of race condition 2025-12-20 11:17:50 +09:00
minjaesong
64026be133 disabling broken code until fixed 2025-12-19 23:18:16 +09:00
minjaesong
96d697e158 iPF progressive mode decoder 2025-12-19 21:41:51 +09:00
minjaesong
1680137b7d external iPF encoder 2025-12-19 21:36:48 +09:00
minjaesong
c71920b95d TAV fix 13/7 wavelet not decoding correctly 2025-12-18 20:09:06 +09:00
minjaesong
629ed5fb12 tsvm "compiler" update 2025-12-18 10:29:46 +09:00
minjaesong
4f6efbe000 Update terranmon.txt 2025-12-18 10:28:56 +09:00
minjaesong
4362610c70 rm readme 2025-12-18 10:07:08 +09:00
106 changed files with 4224 additions and 671 deletions

2
.gitignore vendored
View File

@@ -66,3 +66,5 @@ assets/disk0/home/basic/*
assets/disk0/movtestimg/*.jpg
assets/disk0/*.mov
assets/diskMediabin/*
video_encoder/*

View File

@@ -21,36 +21,54 @@
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-reflect/1.8.21/kotlin-reflect-1.8.21.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-test/1.8.21/kotlin-test-1.8.21.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-common/1.8.21/kotlin-stdlib-common-1.8.21.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/graal-sdk-22.3.1.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/icu4j-71.1.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-22.3.1-edit.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-scriptengine-22.3.1.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/regex-22.3.1-edit.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/truffle-api-22.3.1.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/polyglot-23.1.10.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-language-23.1.10.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-scriptengine-23.1.10.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/truffle-api-23.1.10.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/truffle-runtime-23.1.10.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/truffle-compiler-23.1.10.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/compiler-23.1.10.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/compiler-management-23.1.10.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/word-23.1.10.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/regex-23.1.10.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/icu4j-23.1.10.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/collections-23.1.10.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/nativeimage-23.1.10.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/jniutils-23.1.10.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/gdx-1.12.1.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-3.3.3.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-stb-3.3.3.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-glfw-3.3.3.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-22.3.1-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-22.3.1-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-language-23.1.10-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-language-23.1.10-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/gdx-1.12.1-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/gdx-1.12.1-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/icu4j-71.1-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/icu4j-71.1-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/icu4j-23.1.10-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/icu4j-23.1.10-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-openal-3.3.3.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-opengl-3.3.3.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-3.3.3-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-3.3.3-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-jemalloc-3.3.3.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/regex-22.3.1-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/regex-22.3.1-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/regex-23.1.10-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/regex-23.1.10-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/TerranVirtualDisk-src.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/jorbis-0.0.17-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/jorbis-0.0.17-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-stb-3.3.3-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-stb-3.3.3-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/graal-sdk-22.3.1-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/graal-sdk-22.3.1-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/polyglot-23.1.10-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/polyglot-23.1.10-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/truffle-api-23.1.10-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/truffle-api-23.1.10-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/truffle-runtime-23.1.10-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/truffle-runtime-23.1.10-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/collections-23.1.10-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/collections-23.1.10-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/nativeimage-23.1.10-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/nativeimage-23.1.10-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/jniutils-23.1.10-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/jniutils-23.1.10-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/jlayer-1.0.1-gdx-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/jlayer-1.0.1-gdx-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-glfw-3.3.3-javadoc.jar" path-in-jar="/" />
@@ -62,15 +80,15 @@
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-openal-3.3.3-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-opengl-3.3.3-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-opengl-3.3.3-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/truffle-api-22.3.1-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/truffle-api-22.3.1-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/regex-23.1.10-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/regex-23.1.10-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-3.3.3-natives-windows.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-jemalloc-3.3.3-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-jemalloc-3.3.3-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-stb-3.3.3-natives-linux.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-stb-3.3.3-natives-macos.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-scriptengine-22.3.1-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-scriptengine-22.3.1-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-scriptengine-23.1.10-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-scriptengine-23.1.10-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-glfw-3.3.3-natives-linux.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-glfw-3.3.3-natives-macos.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/gdx-jnigen-loader-2.3.1-javadoc.jar" path-in-jar="/" />

2
.idea/misc.xml generated
View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

2
.idea/vcs.xml generated
View File

@@ -2,5 +2,7 @@
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$/assets/disk0/home/tetrino" vcs="Git" />
<mapping directory="$PROJECT_DIR$/assets/disk0/home/tvnes" vcs="Git" />
</component>
</project>

View File

@@ -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

View File

@@ -5,5 +5,6 @@ set PATH=\tvdos\installer;\tvdos\tuidev;$PATH
set KEYBOARD=us_colemak
rem this line specifies which shell to be presented after the boot precess:
tvdos/i18n/korean
zfm
command -fancy

View File

@@ -1,6 +1,6 @@
if (exec_args[1] === undefined) {
println("Usage: compile -le/-lo myfile.js")
println(" The compiled and linked file will be myfile.out")
println(" The compiled and linked file will be myfile.exc")
return 1
}
@@ -14,7 +14,7 @@ if (exec_args[2]) {
_G.shell.execute(`rm ${tempFilename}.gz`)
_G.shell.execute(`link -${exec_args[1][2]} ${tempFilename}.bin`)
_G.shell.execute(`mv ${tempFilename}.out ${filenameWithoutExt}.out`)
_G.shell.execute(`mv ${tempFilename}.exc ${filenameWithoutExt}.exc`)
_G.shell.execute(`rm ${tempFilename}.bin`)
}
// with no linking

View File

@@ -1,6 +1,6 @@
if (exec_args[1] === undefined) {
println("Usage: decompile myfile.bin")
println("The compiled file will be myfile.bin.js")
println("Usage: decompile myfile.exc")
println("The compiled file will be myfile.exc.js")
return 1
}
_G.shell.execute(`enc ${exec_args[1]} ${exec_args[1]}.gz`)

View File

@@ -1,3 +1,5 @@
// a simple, symmetric obfuscator with infinite-length key
function seq(s) {
let out = ""
let cnt = 0

View File

@@ -11,7 +11,6 @@ let infile = files.open(infilePath)
if (!infile.exists) throw Error("No such file: " + infilePath)
let outfile = files.open(infilePath.substringBeforeLast(".") + ".out")
let outMode = exec_args[1].toLowerCase()
let type = {
@@ -21,6 +20,13 @@ let type = {
"-c": "\x04"
}
let ext = {
"-r": ".exc", // executable
"-e": ".exc", // executable
"-o": ".lib", // library
"-c": ".cob" // core object
}
function toI32(num) {
const buffer = new ArrayBuffer(4)
const view = new DataView(buffer)
@@ -40,6 +46,7 @@ let addr = 0
if (exec_args[3] !== undefined && exec_args[3].toLowerCase() == "-a" && exec_args[4] !== undefined)
addr = parseInt(exec_args[4], 16)
let outfile = files.open(infilePath.substringBeforeLast(".") + ext[exec_args[3].toLowerCase()])
outfile.sappend("\x20\xC0\xCC\x0A")
outfile.sappend(type[outMode] || "\x00")
outfile.bappend(toI24(addr))

View File

@@ -1,5 +1,5 @@
if (exec_args[1] === undefined) {
println("Usage: load myfile.out")
println("Usage: load myfile.exc")
println(" This will load the binary image onto the Core Memory")
return 1
}

View File

@@ -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)

View File

@@ -35,7 +35,7 @@ function print_prompt_text() {
print(" "+CURRENT_DRIVE+":")
con.color_pair(161,253)
con.addch(16);con.curs_right()
con.color_pair(0,253)
con.color_pair(240,253)
print(" \\"+shell_pwd.join("\\").substring(1)+" ")
if (errorlevel != 0 && errorlevel != "undefined" && errorlevel != undefined) {
con.color_pair(166,253)
@@ -61,7 +61,7 @@ function greet() {
con.clear()
con.color_pair(253,255)
print(' ');con.addch(17);con.curs_right()
con.color_pair(0,253)
con.color_pair(240,253)
print(" ".repeat(greetLeftPad)+welcome_text+" ".repeat(greetRightPad))
con.color_pair(253,255)
con.addch(16);con.curs_right();print(' ')

View File

@@ -9,6 +9,7 @@ const MAXMEM = sys.maxmem()
const WIDTH = 560
const HEIGHT = 448
const TAV_MAGIC = [0x1F, 0x54, 0x53, 0x56, 0x4D, 0x54, 0x41, 0x56] // "\x1FTSVM TAV"
const TAP_MAGIC = [0x1F, 0x54, 0x53, 0x56, 0x4D, 0x54, 0x41, 0x50] // "\x1FTSVM TAP"
const UCF_MAGIC = [0x1F, 0x54, 0x53, 0x56, 0x4D, 0x55, 0x43, 0x46] // "\x1FTSVM UCF"
const TAV_VERSION = 1 // Initial DWT version
const UCF_VERSION = 1
@@ -367,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
@@ -381,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()
@@ -389,7 +408,7 @@ for (let i = 0; i < 8; i++) {
// Validate magic number
let magicValid = true
for (let i = 0; i < 8; i++) {
if (header.magic[i] !== TAV_MAGIC[i]) {
if (header.magic[i] !== TAV_MAGIC[i] &&header.magic[i] !== TAP_MAGIC[i] ) {
magicValid = false
break
}
@@ -401,10 +420,16 @@ if (!magicValid) {
return
}
// Check if this is a TAP still image file (magic ends with 'P' instead of 'V')
const isTapFile = (header.magic[7] === TAP_MAGIC[7])
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()
@@ -457,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}`)
@@ -469,6 +494,135 @@ console.log(`Features: ${hasAudio ? "Audio " : ""}${hasSubtitles ? "Subtitles "
console.log(`Video flags raw: 0x${header.videoFlags.toString(16)}`)
console.log(`Scan type: ${isInterlaced ? "Interlaced" : "Progressive"}`)
// Handle TAP still image file
if (isTapFile) {
console.log("TAP still image detected")
// Allocate single frame buffer for still image
const FRAME_PIXELS = header.width * header.height
const FRAME_SIZE = FRAME_PIXELS * 3
const RGB_BUFFER = sys.malloc(FRAME_SIZE)
const PREV_RGB_BUFFER = sys.malloc(FRAME_SIZE)
sys.memset(RGB_BUFFER, 0, FRAME_SIZE)
sys.memset(PREV_RGB_BUFFER, 0, FRAME_SIZE)
// Read the image packet (should be I-frame)
let packetType = seqread.readOneByte()
// Skip non-video packets until we find the image data
while (packetType !== TAV_PACKET_IFRAME) {
if (packetType === TAV_PACKET_EXTENDED_HDR) {
// Parse extended header - look for XFPS
let numPairs = seqread.readShort()
for (let i = 0; i < numPairs; i++) {
// 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 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)
}
}
} else if (packetType === TAV_PACKET_SCREEN_MASK) {
// Skip screen mask packet - single entry: frame_num(4) + top(2) + right(2) + bottom(2) + left(2)
seqread.skip(12)
} else if (packetType === TAV_PACKET_TIMECODE) {
seqread.skip(8)
} else {
console.log(`got unknown packet type 0x${packetType.toString(16)}`)
let size = seqread.readInt()
seqread.skip(size)
}
packetType = seqread.readOneByte()
}
if (packetType === TAV_PACKET_IFRAME) {
// Read and decode I-frame
const compressedSize = seqread.readInt()
const compressedPtr = seqread.readBytes(compressedSize)
// Decode using TAV hardware decoder
graphics.tavDecodeCompressed(
compressedPtr, compressedSize,
RGB_BUFFER, PREV_RGB_BUFFER,
header.width, header.height,
header.qualityLevel,
QLUT[header.qualityY], QLUT[header.qualityCo], QLUT[header.qualityCg],
header.channelLayout, 0, header.waveletFilter, header.decompLevels,
isLossless, header.version, header.entropyCoder, 2
)
sys.free(compressedPtr)
// Upload to framebuffer
graphics.uploadRGBToFramebuffer(RGB_BUFFER, header.width, header.height, 0, false)
}
// Free buffers
sys.free(RGB_BUFFER)
sys.free(PREV_RGB_BUFFER)
// Show "backspace to exit" message
con.clear()
con.curs_set(0)
con.move(1, 1)
println("Push and hold Backspace to exit")
// Wait loop for still image viewing (similar to decodeipf.js)
let wait = true
let t1 = sys.nanoTime()
let tapNotifHideTimer = 0
const TAP_NOTIF_SHOWUPTIME = 3000000000 // 3 seconds
while (wait) {
sys.poke(-40, 1)
if (sys.peek(-41) == 67) { // Backspace
wait = false
con.curs_set(1)
}
sys.sleep(50)
let t2 = sys.nanoTime()
tapNotifHideTimer += (t2 - t1)
if (tapNotifHideTimer > TAP_NOTIF_SHOWUPTIME) {
con.clear()
}
t1 = t2
}
// Clean up and exit (matching normal video playback cleanup)
con.clear()
con.curs_set(1)
// Reset font ROM
sys.poke(-1299460, 20)
sys.poke(-1299460, 21)
graphics.setPalette(0, 0, 0, 0, 0)
con.move(cy, cx) // restore cursor
return errorlevel
}
// Adjust decode height for interlaced content
// For interlaced: header.height is display height (448)
// Each field is half of display height (448/2 = 224)
@@ -1170,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}`)
@@ -1704,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 {

View File

@@ -26,15 +26,18 @@ const COL_HL_EXT = {
"wav": 31,
"adpcm": 31,
"pcm": 32,
"mp3": 33,
// "mp3": 33,
"tad": 33,
"mp2": 34,
"mv1": 213,
"mv2": 213,
"mv3": 213,
"tav": 213,
"ipf": 190,
"ipf1": 190,
"ipf2": 190,
"im3": 190,
"tap": 190,
"txt": 223,
"md": 223,
"log": 223
@@ -43,14 +46,17 @@ const COL_HL_EXT = {
const EXEC_FUNS = {
"wav": (f) => _G.shell.execute(`playwav "${f}" -i`),
"adpcm": (f) => _G.shell.execute(`playwav "${f}" -i`),
"mp3": (f) => _G.shell.execute(`playmp3 "${f}" -i`),
// "mp3": (f) => _G.shell.execute(`playmp3 "${f}" -i`),
"mp2": (f) => _G.shell.execute(`playmp2 "${f}" -i`),
"mv1": (f) => _G.shell.execute(`playmv1 "${f}" -i`),
"mv2": (f) => _G.shell.execute(`playtev "${f}" -i`),
"mv3": (f) => _G.shell.execute(`playtav "${f}" -i`),
"tav": (f) => _G.shell.execute(`playtav "${f}" -i`),
"im3": (f) => _G.shell.execute(`playtav "${f}" -i`),
"tap": (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}"`),

View File

@@ -0,0 +1,305 @@
/*
* getopt.js: node.js implementation of POSIX getopt() (and then some)
*
* Copyright 2011 David Pacheco. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
var ASSERT = require('assert').ok;
function goError(msg)
{
return (new Error('getopt: ' + msg));
}
/*
* The BasicParser is our primary interface to the outside world. The
* documentation for this object and its public methods is contained in
* the included README.md.
*/
function goBasicParser(optstring, argv, optind)
{
var ii;
ASSERT(optstring || optstring === '', 'optstring is required');
ASSERT(optstring.constructor === String, 'optstring must be a string');
ASSERT(argv, 'argv is required');
ASSERT(argv.constructor === Array, 'argv must be an array');
this.gop_argv = new Array(argv.length);
this.gop_options = {};
this.gop_aliases = {};
this.gop_optind = optind !== undefined ? optind : 2;
this.gop_subind = 0;
for (ii = 0; ii < argv.length; ii++) {
ASSERT(argv[ii].constructor === String,
'argv must be string array');
this.gop_argv[ii] = argv[ii];
}
this.parseOptstr(optstring);
}
exports.BasicParser = goBasicParser;
/*
* Parse the option string and update the following fields:
*
* gop_silent Whether to log errors to stderr. Silent mode is
* indicated by a leading ':' in the option string.
*
* gop_options Maps valid single-letter-options to booleans indicating
* whether each option is required.
*
* gop_aliases Maps valid long options to the corresponding
* single-letter short option.
*/
goBasicParser.prototype.parseOptstr = function (optstr)
{
var chr, cp, alias, arg, ii;
ii = 0;
if (optstr.length > 0 && optstr[0] == ':') {
this.gop_silent = true;
ii++;
} else {
this.gop_silent = false;
}
while (ii < optstr.length) {
chr = optstr[ii];
arg = false;
if (!/^[\w\d\u1000-\u1100]$/.test(chr))
throw (goError('invalid optstring: only alphanumeric ' +
'characters and unicode characters between ' +
'\\u1000-\\u1100 may be used as options: ' + chr));
if (ii + 1 < optstr.length && optstr[ii + 1] == ':') {
arg = true;
ii++;
}
this.gop_options[chr] = arg;
while (ii + 1 < optstr.length && optstr[ii + 1] == '(') {
ii++;
cp = optstr.indexOf(')', ii + 1);
if (cp == -1)
throw (goError('invalid optstring: missing ' +
'")" to match "(" at char ' + ii));
alias = optstr.substring(ii + 1, cp);
this.gop_aliases[alias] = chr;
ii = cp;
}
ii++;
}
};
goBasicParser.prototype.optind = function ()
{
return (this.gop_optind);
};
/*
* For documentation on what getopt() does, see README.md. The following
* implementation invariants are maintained by getopt() and its helper methods:
*
* this.gop_optind Refers to the element of gop_argv that contains
* the next argument to be processed. This may
* exceed gop_argv, in which case the end of input
* has been reached.
*
* this.gop_subind Refers to the character inside
* this.gop_options[this.gop_optind] which begins
* the next option to be processed. This may never
* exceed the length of gop_argv[gop_optind], so
* when incrementing this value we must always
* check if we should instead increment optind and
* reset subind to 0.
*
* That is, when any of these functions is entered, the above indices' values
* are as described above. getopt() itself and getoptArgument() may both be
* called at the end of the input, so they check whether optind exceeds
* argv.length. getoptShort() and getoptLong() are called only when the indices
* already point to a valid short or long option, respectively.
*
* getopt() processes the next option as follows:
*
* o If gop_optind > gop_argv.length, then we already parsed all arguments.
*
* o If gop_subind == 0, then we're looking at the start of an argument:
*
* o Check for special cases like '-', '--', and non-option arguments.
* If present, update the indices and return the appropriate value.
*
* o Check for a long-form option (beginning with '--'). If present,
* delegate to getoptLong() and return the result.
*
* o Otherwise, advance subind past the argument's leading '-' and
* continue as though gop_subind != 0 (since that's now the case).
*
* o Delegate to getoptShort() and return the result.
*/
goBasicParser.prototype.getopt = function ()
{
if (this.gop_optind >= this.gop_argv.length)
/* end of input */
return (undefined);
var arg = this.gop_argv[this.gop_optind];
if (this.gop_subind === 0) {
if (arg == '-' || arg === '' || arg[0] != '-')
return (undefined);
if (arg == '--') {
this.gop_optind++;
this.gop_subind = 0;
return (undefined);
}
if (arg[1] == '-')
return (this.getoptLong());
this.gop_subind++;
ASSERT(this.gop_subind < arg.length);
}
return (this.getoptShort());
};
/*
* Implements getopt() for the case where optind/subind point to a short option.
*/
goBasicParser.prototype.getoptShort = function ()
{
var arg, chr;
ASSERT(this.gop_optind < this.gop_argv.length);
arg = this.gop_argv[this.gop_optind];
ASSERT(this.gop_subind < arg.length);
chr = arg[this.gop_subind];
if (++this.gop_subind >= arg.length) {
this.gop_optind++;
this.gop_subind = 0;
}
if (!(chr in this.gop_options))
return (this.errInvalidOption(chr));
if (!this.gop_options[chr])
return ({ option: chr });
return (this.getoptArgument(chr));
};
/*
* Implements getopt() for the case where optind/subind point to a long option.
*/
goBasicParser.prototype.getoptLong = function ()
{
var arg, alias, chr, eq;
ASSERT(this.gop_subind === 0);
ASSERT(this.gop_optind < this.gop_argv.length);
arg = this.gop_argv[this.gop_optind];
ASSERT(arg.length > 2 && arg[0] == '-' && arg[1] == '-');
eq = arg.indexOf('=');
alias = arg.substring(2, eq == -1 ? arg.length : eq);
if (!(alias in this.gop_aliases))
return (this.errInvalidOption(alias));
chr = this.gop_aliases[alias];
ASSERT(chr in this.gop_options);
if (!this.gop_options[chr]) {
if (eq != -1)
return (this.errExtraArg(alias));
this.gop_optind++; /* eat this argument */
return ({ option: chr });
}
/*
* Advance optind/subind for the argument value and retrieve it.
*/
if (eq == -1)
this.gop_optind++;
else
this.gop_subind = eq + 1;
return (this.getoptArgument(chr));
};
/*
* For the given option letter 'chr' that takes an argument, assumes that
* optind/subind point to the argument (or denote the end of input) and return
* the appropriate getopt() return value for this option and argument (or return
* the appropriate error).
*/
goBasicParser.prototype.getoptArgument = function (chr)
{
var arg;
if (this.gop_optind >= this.gop_argv.length)
return (this.errMissingArg(chr));
arg = this.gop_argv[this.gop_optind].substring(this.gop_subind);
this.gop_optind++;
this.gop_subind = 0;
return ({ option: chr, optarg: arg });
};
goBasicParser.prototype.errMissingArg = function (chr)
{
if (this.gop_silent)
return ({ option: ':', optopt: chr });
process.stderr.write('option requires an argument -- ' + chr + '\n');
return ({ option: '?', optopt: chr, error: true });
};
goBasicParser.prototype.errInvalidOption = function (chr)
{
if (!this.gop_silent)
process.stderr.write('illegal option -- ' + chr + '\n');
return ({ option: '?', optopt: chr, error: true });
};
/*
* This error is not specified by POSIX, but neither is the notion of specifying
* long option arguments using "=" in the same argv-argument, but it's common
* practice and pretty convenient.
*/
goBasicParser.prototype.errExtraArg = function (chr)
{
if (!this.gop_silent)
process.stderr.write('option expects no argument -- ' +
chr + '\n');
return ({ option: '?', optopt: chr, error: true });
};

View File

@@ -1,7 +1,7 @@
/*
TVDOS Graphics Library
Has no affiliation with OpenGL by Khronos Group
/**
* LibGL — TVDOS Graphics Library
* Has no affiliation with OpenGL by Khronos Group
* @author CuriousTorvald
*/

View File

@@ -1,3 +1,8 @@
/**
* LibPCM — PCM decoder for TSVM
* @author CuriousTorvald
*/
const HW_SAMPLING_RATE = 32000
function printdbg(s) { if (0) serial.println(s) }
function printvis(s) { if (0) println(s) }
@@ -29,7 +34,7 @@ function s16Tou8(i) {
}
function u16Tos16(i) { return (i > 32767) ? i - 65536 : i }
function randomRound(k) {
let rnd = (Math.random() + Math.random()) / 2.0 // this produces triangular distribution
let rnd = Math.random() // note to self: no triangular here
return (rnd < (k - (k|0))) ? Math.ceil(k) : Math.floor(k)
}
function lerp(start, end, x) {

View File

@@ -0,0 +1,341 @@
/**
* LibPSG — PSG emulator and mixer for TSVM
* Software-mixes various PSG channels and sends them to sound device as PCM
* @author CuriousTorvald
*/
const HW_SAMPLING_RATE = 32000
function clamp(val, low, hi) { return (val < low) ? low : (val > hi) ? hi : val }
function clampS16(i) { return clamp(i, -32768, 32767) }
const uNybToSnyb = [0,1,2,3,4,5,6,7,-8,-7,-6,-5,-4,-3,-2,-1]
// returns: [unsigned high, unsigned low, signed high, signed low]
function getNybbles(b) { return [b >> 4, b & 15, uNybToSnyb[b >> 4], uNybToSnyb[b & 15]] }
function s8Tou8(i) { return i + 128 }
function s16Tou8(i) {
// return s8Tou8((i >> 8) & 255)
// apply dithering
let ufval = (i / 65536.0) + 0.5
let ival = randomRound(ufval * 255.0)
return ival|0
}
function u16Tos16(i) { return (i > 32767) ? i - 65536 : i }
function randomRound(k) {
let rnd = Math.random() // note to self: no triangular here
return (rnd < (k - (k|0))) ? Math.ceil(k) : Math.floor(k)
}
function lerp(start, end, x) {
return (1 - x) * start + x * end
}
function lerpAndRound(start, end, x) {
return Math.round(lerp(start, end, x))
}
// output format: immediately uploadable into TSVM audio adapter
// ── Internal helpers ────────────────────────────────────────────────────────
function secToSamples(sec) { return Math.round(HW_SAMPLING_RATE * sec) }
function isNative(buf) { return buf.native }
function readU8(buf, ch, i) {
return isNative(buf) ? (sys.peek(buf[ch] + i) & 255) : buf[ch][i]
}
function writeU8(buf, ch, i, v) {
if (isNative(buf)) sys.poke(buf[ch] + i, v)
else buf[ch][i] = v
}
// ── Buffer management ───────────────────────────────────────────────────────
function makeBuffer(length) {
// returns [Uint8Array, Uint8Array] (stereo) that will be used to collect samples made by LibPSG.
// Length: seconds. Number of elements: round(HW_SAMPLING_RATE * length)
const n = secToSamples(length)
const L = new Uint8Array(n)
const R = new Uint8Array(n)
L.fill(128)
R.fill(128)
return { 0: L, 1: R, samples: n, native: false }
}
function makeBufferNative(length) {
// returns native buffer object (stereo) that will be used to collect samples made by LibPSG.
// Length: seconds. Number of elements: round(HW_SAMPLING_RATE * length)
// Free with freeBufferNative() when done.
const n = secToSamples(length)
const L = sys.malloc(n); sys.memset(L, 128, n)
const R = sys.malloc(n); sys.memset(R, 128, n)
return { 0: L, 1: R, samples: n, native: true }
}
function freeBufferNative(buf) {
sys.free(buf[0])
sys.free(buf[1])
}
function clearBuffer(buf, offsetSec, lengthSec) {
// Re-silence a buffer region (fill with 128) for re-use across frames.
const start = (offsetSec != null) ? secToSamples(offsetSec) : 0
const total = (lengthSec != null) ? secToSamples(lengthSec) : (buf.samples - start)
for (let i = 0; i < total; i++) {
writeU8(buf, 0, start + i, 128)
writeU8(buf, 1, start + i, 128)
}
}
// ── Shared mix core ─────────────────────────────────────────────────────────
// sampleFn(i) must return a float in [-1, 1].
// Mixing maths: decode u8 → s16, apply op, clamp, dither back to u8.
function mixInto(buf, lengthSec, offsetSec, op, amp, pan, sampleFn) {
const startIdx = secToSamples(offsetSec)
const n = secToSamples(lengthSec)
// Linear pan law: centre (pan=0) → both channels at full amp
const gainL = Math.max(0, Math.min(1, 1.0 - pan))
const gainR = Math.max(0, Math.min(1, 1.0 + pan))
const opCode = (op === 'sub') ? 1 : (op === 'mul') ? 2 : 0 // default: add
for (let i = 0; i < n; i++) {
const v = sampleFn(i) // oscillator value in [-1, 1]
const oscBase = v * amp * 32767
const oscL = Math.round(oscBase * gainL) | 0
const oscR = Math.round(oscBase * gainR) | 0
for (let ch = 0; ch < 2; ch++) {
const osc = (ch === 0) ? oscL : oscR
const cur = (readU8(buf, ch, startIdx + i) - 128) << 8
let out
switch (opCode) {
case 0: out = cur + osc; break
case 1: out = cur - osc; break
case 2: out = (cur * osc) >> 15; break
}
writeU8(buf, ch, startIdx + i, s16Tou8(clampS16(out)))
}
}
}
// ── Waveform generators ─────────────────────────────────────────────────────
function makeSquare(buf, length, offset, freq, duty, op, amp, pan) {
// buffer: [Uint8Array, Uint8Array] or native buffer
// length: in seconds
// offset: in seconds
// duty: 0.0 to 1.0. default 0.5 (fraction of period where output is +1)
// freq: Hz
// op: add / mul / sub; default: add
// amp: 0.0 to 1.0; default: 0.5
// pan: -1.0 to 1.0; default: 0.0
if (duty == null) duty = 0.5
if (op == null) op = 'add'
if (amp == null) amp = 0.5
if (pan == null) pan = 0.0
mixInto(buf, length, offset, op, amp, pan, function(i) {
const t = offset + i / HW_SAMPLING_RATE
const phase = (t * freq) % 1
return (phase < duty) ? 1.0 : -1.0
})
}
function makeTriangle(buf, length, offset, freq, duty, op, amp, pan) {
// buffer: [Uint8Array, Uint8Array] or native buffer
// length: in seconds
// offset: in seconds
// duty: skew. -1.0 = falling sawtooth, 0.0 = symmetric triangle, 1.0 = rising sawtooth
// freq: Hz
// op: add / mul / sub; default: add
// amp: 0.0 to 1.0; default: 0.5
// pan: -1.0 to 1.0; default: 0.0
if (duty == null) duty = 0.0
if (op == null) op = 'add'
if (amp == null) amp = 0.5
if (pan == null) pan = 0.0
// riseFrac: fraction of period spent rising from -1 to +1
// 0.0 → falling saw, 0.5 → symmetric triangle, 1.0 → rising saw
const riseFrac = (duty + 1.0) * 0.5
mixInto(buf, length, offset, op, amp, pan, function(i) {
const t = offset + i / HW_SAMPLING_RATE
const phase = (t * freq) % 1
if (riseFrac <= 0) {
return 1.0 - 2.0 * phase // falling saw
} else if (riseFrac >= 1) {
return -1.0 + 2.0 * phase // rising saw
} else if (phase < riseFrac) {
return -1.0 + 2.0 * (phase / riseFrac) // rising slope
} else {
return 1.0 - 2.0 * ((phase - riseFrac) / (1.0 - riseFrac)) // falling slope
}
})
}
function makeAliasedTriangle(buf, length, offset, freq, duty, op, amp, pan) {
// buffer: [Uint8Array, Uint8Array] or native buffer
// Famicom-style triangle — output is quantised to 16 DAC levels (4-bit, NES APU style).
// The staircase quantisation introduces harmonics that mimic NES character.
// length: in seconds
// offset: in seconds
// duty: skew. -1.0 = falling sawtooth, 0.0 = symmetric triangle, 1.0 = rising sawtooth
// freq: Hz
// op: add / mul / sub; default: add
// amp: 0.0 to 1.0; default: 0.5
// pan: -1.0 to 1.0; default: 0.0
if (duty == null) duty = 0.0
if (op == null) op = 'add'
if (amp == null) amp = 0.5
if (pan == null) pan = 0.0
const riseFrac = (duty + 1.0) * 0.5
mixInto(buf, length, offset, op, amp, pan, function(i) {
const t = offset + i / HW_SAMPLING_RATE
const phase = (t * freq) % 1
let v
if (riseFrac <= 0) {
v = 1.0 - 2.0 * phase
} else if (riseFrac >= 1) {
v = -1.0 + 2.0 * phase
} else if (phase < riseFrac) {
v = -1.0 + 2.0 * (phase / riseFrac)
} else {
v = 1.0 - 2.0 * ((phase - riseFrac) / (1.0 - riseFrac))
}
// Quantise to 16 levels (NES triangle 4-bit DAC: 0..15 → -1..+1)
const level = Math.max(0, Math.min(15, Math.round((v + 1.0) * 7.5)))
return level / 7.5 - 1.0
})
}
// ── LFSR helpers (for noise types 1 and 2) ─────────────────────────────────
function lfsrStep(state, mode) {
// mode 0 (long/NES mode 0): feedback tap at bit 1; period 32767
// mode 1 (short/NES mode 1): feedback tap at bit 6; period 93 (metallic/tonal)
const bit0 = state & 1
const bitTap = (mode === 0) ? (state >> 1) & 1 : (state >> 6) & 1
const feed = bit0 ^ bitTap
return ((feed << 14) | (state >> 1)) & 0x7FFF
}
function lfsrAdvance(state, steps, mode) {
for (let k = 0; k < steps; k++) state = lfsrStep(state, mode)
return state
}
// NES APU documented LFSR periods
const LFSR_PERIOD_LONG = 32767 // mode 0
const LFSR_PERIOD_SHORT = 93 // mode 1
function makeNoise(buf, length, offset, freq, type, op, amp, pan) {
// buffer: [Uint8Array, Uint8Array] or native buffer
// length: in seconds
// offset: in seconds
// type:
// -1: 8-bit white noise (random float per period, sample-and-hold)
// 0: 1-bit white noise (random ±1 per period, sample-and-hold)
// 1: 1-bit LFSR long mode — NES mode 0, tap=bit0^bit1, period 32767 (full-spectrum)
// 2: 1-bit LFSR short mode — NES mode 1, tap=bit0^bit6, period 93 (metallic/tonal)
// freq: Hz (clock rate of the noise generator)
// op: add / mul / sub; default: add
// amp: 0.0 to 1.0; default: 0.5
// pan: -1.0 to 1.0; default: 0.0
//
// LFSR types (1 and 2) are deterministic given (offset, freq): calling with
// monotonically advancing offset values produces a seamless noise stream
// across frames. White noise types (-1, 0) are random per call.
if (op == null) op = 'add'
if (amp == null) amp = 0.5
if (pan == null) pan = 0.0
if (type === -1) {
// 8-bit white: new random float in [-1, 1] each clock period
let prevClock = -1
let noiseVal = 0.0
mixInto(buf, length, offset, op, amp, pan, function(i) {
const currentClock = Math.floor((offset + i / HW_SAMPLING_RATE) * freq) | 0
if (currentClock !== prevClock) {
prevClock = currentClock
noiseVal = Math.random() * 2.0 - 1.0
}
return noiseVal
})
} else if (type === 0) {
// 1-bit white: random ±1 each clock period
let prevClock = -1
let noiseVal = 1.0
mixInto(buf, length, offset, op, amp, pan, function(i) {
const currentClock = Math.floor((offset + i / HW_SAMPLING_RATE) * freq) | 0
if (currentClock !== prevClock) {
prevClock = currentClock
noiseVal = (Math.random() >= 0.5) ? 1.0 : -1.0
}
return noiseVal
})
} else {
// LFSR-based noise (types 1 and 2)
const mode = (type === 2) ? 1 : 0
const period = (mode === 0) ? LFSR_PERIOD_LONG : LFSR_PERIOD_SHORT
// Advance to deterministic position for this offset so consecutive frame
// calls with advancing offsets produce a seamless noise stream.
const startClock = Math.floor(offset * freq) | 0
let lfsr = lfsrAdvance(1, startClock % period, mode)
let prevClock = startClock
mixInto(buf, length, offset, op, amp, pan, function(i) {
const currentClock = Math.floor((offset + i / HW_SAMPLING_RATE) * freq) | 0
const delta = currentClock - prevClock
if (delta > 0) {
const steps = delta % period
if (steps > 0) lfsr = lfsrAdvance(lfsr, steps, mode)
prevClock = currentClock
}
return (lfsr & 1) ? 1.0 : -1.0
})
}
}
// ── Send to audio hardware ──────────────────────────────────────────────────
function sendBuffer(buf, playhead, offsetSec, lengthSec, stagingPtr) {
// Interleaves the L and R channels into a staging region (LRLRLR…) and uploads
// to the audio adapter pcmBin via the standard putPcmDataByPtr pipeline.
//
// offsetSec: start of region to send (default: 0)
// lengthSec: duration to send (default: entire buffer from offsetSec)
// stagingPtr: optional caller-owned native buffer (≥ min(chunk, 32768) * 2 bytes).
// Pass a pre-allocated pointer to avoid malloc/free per call —
// useful for the per-frame tvnes pattern.
//
// The function auto-chunks at 32768 stereo samples (pcmBin capacity).
// Blocks briefly if the audio queue is saturated (queue depth > 2).
const start = (offsetSec != null) ? secToSamples(offsetSec) : 0
const total = (lengthSec != null) ? secToSamples(lengthSec) : (buf.samples - start)
const MAX_CHUNK = 32768 // pcmBin = 65536 bytes; stereo → max 32768 samples per upload
const ownsStaging = (stagingPtr == null)
if (ownsStaging) stagingPtr = sys.malloc(Math.min(total, MAX_CHUNK) * 2)
let remaining = total
let cursor = start
while (remaining > 0) {
const take = Math.min(remaining, MAX_CHUNK)
// Interleave L, R into staging buffer
for (let i = 0; i < take; i++) {
sys.poke(stagingPtr + 2 * i, readU8(buf, 0, cursor + i))
sys.poke(stagingPtr + 2 * i + 1, readU8(buf, 1, cursor + i))
}
// Wait for room in the playback queue (mirrors playwav.js idiom)
while (audio.getPosition(playhead) > 2) sys.sleep(2)
audio.putPcmDataByPtr(stagingPtr, take * 2, 0)
audio.setSampleUploadLength(playhead, take * 2)
audio.startSampleUpload(playhead)
remaining -= take
cursor += take
}
if (ownsStaging) sys.free(stagingPtr)
}
exports = {
HW_SAMPLING_RATE,
makeBuffer, makeBufferNative, freeBufferNative, clearBuffer,
makeSquare, makeTriangle, makeAliasedTriangle, makeNoise,
sendBuffer
}

View File

@@ -1,4 +1,7 @@
/**
* LibSeqread — sequentially read files from disk drive
* @author CuriousTorvald
*/
let readCount = 0
let port = undefined
let fileHeader = new Uint8Array(4096)

View File

@@ -1,3 +1,8 @@
/**
* LibSeqread extension for Tape Drive — sequentially read tape
* @author CuriousTorvald
*/
// Sequential reader for HSDPA TAPE devices
// Unlike seqread.mjs which is limited to 4096 bytes per read due to serial communication,
// this module can read larger chunks efficiently from HSDPA devices.

View File

@@ -1,3 +1,8 @@
/**
* WinTex — TUI window management and renderer
* @author CuriousTorvald
*/
class WindowObject {
constructor(x, y, w, h, inputProcessor, drawContents, title, drawFrame) {

View File

@@ -22,6 +22,7 @@ cp -r "../out/$RUNTIME" $DESTDIR/
# Copy over all the assets and a jarfile
cp -r "../out/TerranBASIC.jar" $DESTDIR/
cp "../lib/compiler-23.1.10.jar" "../lib/compiler-management-23.1.10.jar" "../lib/truffle-compiler-23.1.10.jar" "../lib/truffle-api-23.1.10.jar" "../lib/truffle-runtime-23.1.10.jar" "../lib/polyglot-23.1.10.jar" "../lib/collections-23.1.10.jar" "../lib/word-23.1.10.jar" "../lib/nativeimage-23.1.10.jar" "../lib/jniutils-23.1.10.jar" $DESTDIR/
# Pack everything to AppImage
ARCH=arm_aarch64 "./$APPIMAGETOOL" $DESTDIR "out/$DESTDIR.AppImage" || { echo 'Building AppImage failed' >&2; exit 1; }

View File

@@ -22,6 +22,7 @@ cp -r "../out/$RUNTIME" $DESTDIR/
# Copy over all the assets and a jarfile
cp -r "../out/TerranBASIC.jar" $DESTDIR/
cp "../lib/compiler-23.1.10.jar" "../lib/compiler-management-23.1.10.jar" "../lib/truffle-compiler-23.1.10.jar" "../lib/truffle-api-23.1.10.jar" "../lib/truffle-runtime-23.1.10.jar" "../lib/polyglot-23.1.10.jar" "../lib/collections-23.1.10.jar" "../lib/word-23.1.10.jar" "../lib/nativeimage-23.1.10.jar" "../lib/jniutils-23.1.10.jar" $DESTDIR/
# Pack everything to AppImage
"./$APPIMAGETOOL" $DESTDIR "out/$DESTDIR.AppImage" || { echo 'Building AppImage failed' >&2; exit 1; }

View File

@@ -23,5 +23,6 @@ cp -r "../out/$RUNTIME" $DESTDIR/Contents/MacOS/
# Copy over all the assets and a jarfile
cp -r "../out/TerranBASIC.jar" $DESTDIR/Contents/MacOS/
cp "../lib/compiler-23.1.10.jar" "../lib/compiler-management-23.1.10.jar" "../lib/truffle-compiler-23.1.10.jar" "../lib/truffle-api-23.1.10.jar" "../lib/truffle-runtime-23.1.10.jar" "../lib/polyglot-23.1.10.jar" "../lib/collections-23.1.10.jar" "../lib/word-23.1.10.jar" "../lib/nativeimage-23.1.10.jar" "../lib/jniutils-23.1.10.jar" $DESTDIR/Contents/MacOS/
echo "Build successful: $DESTDIR"

View File

@@ -23,5 +23,6 @@ cp -r "../out/$RUNTIME" $DESTDIR/Contents/MacOS/
# Copy over all the assets and a jarfile
cp -r "../out/TerranBASIC.jar" $DESTDIR/Contents/MacOS/
cp "../lib/compiler-23.1.10.jar" "../lib/compiler-management-23.1.10.jar" "../lib/truffle-compiler-23.1.10.jar" "../lib/truffle-api-23.1.10.jar" "../lib/truffle-runtime-23.1.10.jar" "../lib/polyglot-23.1.10.jar" "../lib/collections-23.1.10.jar" "../lib/word-23.1.10.jar" "../lib/nativeimage-23.1.10.jar" "../lib/jniutils-23.1.10.jar" $DESTDIR/Contents/MacOS/
echo "Build successful: $DESTDIR"

View File

@@ -18,6 +18,7 @@ cp -r "../out/$RUNTIME" $DESTDIR/
# Copy over all the assets and a jarfile
cp -r "../out/TerranBASIC.jar" $DESTDIR/
cp "../lib/compiler-23.1.10.jar" "../lib/compiler-management-23.1.10.jar" "../lib/truffle-compiler-23.1.10.jar" "../lib/truffle-api-23.1.10.jar" "../lib/truffle-runtime-23.1.10.jar" "../lib/polyglot-23.1.10.jar" "../lib/collections-23.1.10.jar" "../lib/word-23.1.10.jar" "../lib/nativeimage-23.1.10.jar" "../lib/jniutils-23.1.10.jar" $DESTDIR/
# Temporary solution: zip everything
zip -r -9 -l "out/$DESTDIR.zip" $DESTDIR

View File

@@ -1,3 +1,4 @@
#!/bin/bash
cd "${0%/*}"
./runtime-linux-arm/bin/java -Xms128M -Xmx2G -Dswing.aatext=true -Dawt.useSystemAAFontSettings=lcd -jar ./TerranBASIC.jar
GRAAL_MODULE_PATH=compiler-23.1.10.jar:compiler-management-23.1.10.jar:truffle-compiler-23.1.10.jar:truffle-api-23.1.10.jar:truffle-runtime-23.1.10.jar:polyglot-23.1.10.jar:collections-23.1.10.jar:word-23.1.10.jar:nativeimage-23.1.10.jar:jniutils-23.1.10.jar
./runtime-linux-arm/bin/java --upgrade-module-path=$GRAAL_MODULE_PATH -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI --add-exports=java.base/jdk.internal.misc=jdk.internal.vm.compiler -Xms128M -Xmx2G -Dswing.aatext=true -Dawt.useSystemAAFontSettings=lcd -jar ./TerranBASIC.jar

View File

@@ -1,3 +1,4 @@
#!/bin/bash
cd "${0%/*}"
./runtime-linux-x86/bin/java -Xms128M -Xmx2G -Dswing.aatext=true -Dawt.useSystemAAFontSettings=lcd -jar ./TerranBASIC.jar
GRAAL_MODULE_PATH=compiler-23.1.10.jar:compiler-management-23.1.10.jar:truffle-compiler-23.1.10.jar:truffle-api-23.1.10.jar:truffle-runtime-23.1.10.jar:polyglot-23.1.10.jar:collections-23.1.10.jar:word-23.1.10.jar:nativeimage-23.1.10.jar:jniutils-23.1.10.jar
./runtime-linux-x86/bin/java --upgrade-module-path=$GRAAL_MODULE_PATH -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI --add-exports=java.base/jdk.internal.misc=jdk.internal.vm.compiler -Xms128M -Xmx2G -Dswing.aatext=true -Dawt.useSystemAAFontSettings=lcd -jar ./TerranBASIC.jar

View File

@@ -1,3 +1,4 @@
#!/bin/bash
cd "${0%/*}"
./runtime-osx-arm/bin/java -XstartOnFirstThread -Xms128M -Xmx2G -jar ./TerranBASIC.jar
GRAAL_MODULE_PATH=compiler-23.1.10.jar:compiler-management-23.1.10.jar:truffle-compiler-23.1.10.jar:truffle-api-23.1.10.jar:truffle-runtime-23.1.10.jar:polyglot-23.1.10.jar:collections-23.1.10.jar:word-23.1.10.jar:nativeimage-23.1.10.jar:jniutils-23.1.10.jar
./runtime-osx-arm/bin/java --upgrade-module-path=$GRAAL_MODULE_PATH -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI --add-exports=java.base/jdk.internal.misc=jdk.internal.vm.compiler -XstartOnFirstThread -Xms128M -Xmx2G -jar ./TerranBASIC.jar

View File

@@ -1,3 +1,4 @@
#!/bin/bash
cd "${0%/*}"
./runtime-osx-x86/bin/java -XstartOnFirstThread -Xms128M -Xmx2G -jar ./TerranBASIC.jar
GRAAL_MODULE_PATH=compiler-23.1.10.jar:compiler-management-23.1.10.jar:truffle-compiler-23.1.10.jar:truffle-api-23.1.10.jar:truffle-runtime-23.1.10.jar:polyglot-23.1.10.jar:collections-23.1.10.jar:word-23.1.10.jar:nativeimage-23.1.10.jar:jniutils-23.1.10.jar
./runtime-osx-x86/bin/java --upgrade-module-path=$GRAAL_MODULE_PATH -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI --add-exports=java.base/jdk.internal.misc=jdk.internal.vm.compiler -XstartOnFirstThread -Xms128M -Xmx2G -jar ./TerranBASIC.jar

View File

@@ -1,2 +1,3 @@
cd /D "%~dp0"
.\runtime-windows-x86\bin\java -Xms128M -Xmx2G -jar .\TerranBASIC.jar
set GRAAL_MODULE_PATH=compiler-23.1.10.jar;compiler-management-23.1.10.jar;truffle-compiler-23.1.10.jar;truffle-api-23.1.10.jar;truffle-runtime-23.1.10.jar;polyglot-23.1.10.jar;collections-23.1.10.jar;word-23.1.10.jar;nativeimage-23.1.10.jar;jniutils-23.1.10.jar
.\runtime-windows-x86\bin\java --upgrade-module-path=%GRAAL_MODULE_PATH% -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI --add-exports=java.base/jdk.internal.misc=jdk.internal.vm.compiler -Xms128M -Xmx2G -jar .\TerranBASIC.jar

81
ipf_encoder/Makefile Normal file
View File

@@ -0,0 +1,81 @@
# Makefile for iPF (TSVM Interchangeable Picture Format) Encoder
# Created by CuriousTorvald and Claude on 2025-12-19.
CC = gcc
CFLAGS = -std=c99 -Wall -Wextra -O2 -D_GNU_SOURCE
DBGFLAGS =
PREFIX = /usr/local
# Zstd flags (use pkg-config if available, fallback for cross-platform compatibility)
ZSTD_CFLAGS = $(shell pkg-config --cflags libzstd 2>/dev/null || echo "")
ZSTD_LIBS = $(shell pkg-config --libs libzstd 2>/dev/null || echo "-lzstd")
LIBS = -lm $(ZSTD_LIBS)
# Targets
TARGETS = encoder_ipf decoder_ipf
# Build all (default)
all: $(TARGETS)
encoder_ipf: encoder_ipf.c
rm -f encoder_ipf
$(CC) $(CFLAGS) $(ZSTD_CFLAGS) -o encoder_ipf encoder_ipf.c $(LIBS)
@echo "iPF encoder built: encoder_ipf"
decoder_ipf: decoder_ipf.c
rm -f decoder_ipf
$(CC) $(CFLAGS) $(ZSTD_CFLAGS) -o decoder_ipf decoder_ipf.c $(LIBS)
@echo "iPF decoder built: decoder_ipf"
# Build with debug symbols
debug: CFLAGS += -g -DDEBUG -fsanitize=address -fno-omit-frame-pointer
debug: DBGFLAGS += -fsanitize=address -fno-omit-frame-pointer
debug: clean $(TARGETS)
# Build with optimizations
release: CFLAGS = -std=c99 -Wall -Wextra -O3 -D_GNU_SOURCE -march=native
release: clean $(TARGETS)
# Clean build artifacts
clean:
rm -f $(TARGETS) *.o
# Install
install: $(TARGETS)
cp encoder_ipf $(PREFIX)/bin/
cp decoder_ipf $(PREFIX)/bin/
# Check for required dependencies
check-deps:
@echo "Checking dependencies..."
@pkg-config --exists libzstd || (echo "Error: libzstd-dev not found. Install libzstd-dev or equivalent" && exit 1)
@which ffmpeg >/dev/null 2>&1 || (echo "Error: ffmpeg not found in PATH" && exit 1)
@which ffprobe >/dev/null 2>&1 || (echo "Error: ffprobe not found in PATH" && exit 1)
@echo "All dependencies found."
# Help
help:
@echo "iPF (TSVM Interchangeable Picture Format) Tools"
@echo ""
@echo "Targets:"
@echo " all - Build encoder and decoder (default)"
@echo " encoder_ipf - Build encoder only"
@echo " decoder_ipf - Build decoder only"
@echo " debug - Build with debug symbols and AddressSanitizer"
@echo " release - Build with full optimizations"
@echo " clean - Remove build artifacts"
@echo " install - Install to /usr/local/bin"
@echo " check-deps - Check for required dependencies"
@echo " help - Show this help"
@echo ""
@echo "Requirements:"
@echo " - GCC with C99 support"
@echo " - libzstd-dev (Zstd compression library)"
@echo " - FFmpeg (for image encoding/decoding)"
@echo ""
@echo "Usage:"
@echo " make # Build all"
@echo " ./encoder_ipf -i input.png -o output.ipf # Encode"
@echo " ./decoder_ipf -i output.ipf -o decoded.png # Decode"
.PHONY: all clean install check-deps help debug release

592
ipf_encoder/decoder_ipf.c Normal file
View File

@@ -0,0 +1,592 @@
/**
* iPF Decoder - TSVM Interchangeable Picture Format Decoder
*
* Decodes iPF format (Type 1 or Type 2) images to standard formats via FFmpeg.
*
* Created by CuriousTorvald and Claude on 2025-12-19.
*/
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <math.h>
#include <getopt.h>
#include <zstd.h>
// =============================================================================
// Constants
// =============================================================================
#define IPF_MAGIC "\x1F\x54\x53\x56\x4D\x69\x50\x46" // "\x1FTSVMiPF"
#define IPF_HEADER_SIZE 28 // 8 magic + 2 width + 2 height + 1 flags + 1 type + 10 reserved + 4 uncompressed
#define IPF_TYPE_1 0 // 4:2:0 chroma subsampling
#define IPF_TYPE_2 1 // 4:2:2 chroma subsampling
#define IPF_FLAG_ALPHA 0x01
#define IPF_FLAG_ZSTD 0x10
#define IPF_FLAG_PROGRESSIVE 0x80
#define MAX_PATH 4096
// =============================================================================
// Structures
// =============================================================================
typedef struct {
uint16_t width;
uint16_t height;
uint8_t flags;
uint8_t type;
uint32_t uncompressed_size;
} ipf_header_t;
typedef struct {
char *input_file;
char *output_file;
int verbose;
int raw_output; // Output raw RGB instead of using FFmpeg
} decoder_config_t;
// =============================================================================
// Utility Functions
// =============================================================================
static void print_usage(const char *program) {
printf("iPF Decoder - TSVM Interchangeable Picture Format\n");
printf("\nUsage: %s -i input.ipf -o output.png [options]\n\n", program);
printf("Required:\n");
printf(" -i, --input FILE Input iPF file\n");
printf(" -o, --output FILE Output image file (any format FFmpeg supports)\n");
printf("\nOptions:\n");
printf(" --raw Output raw RGB24/RGBA data instead of image file\n");
printf(" -v, --verbose Verbose output\n");
printf(" -h, --help Show this help\n");
printf("\nExamples:\n");
printf(" %s -i photo.ipf -o photo.png\n", program);
printf(" %s -i logo.ipf -o logo.jpg -v\n", program);
}
static float clampf(float v, float lo, float hi) {
return v < lo ? lo : (v > hi ? hi : v);
}
// =============================================================================
// iPF File Reading
// =============================================================================
static int read_ipf_header(FILE *fp, ipf_header_t *header) {
uint8_t magic[8];
if (fread(magic, 1, 8, fp) != 8) {
fprintf(stderr, "Error: Failed to read magic\n");
return -1;
}
if (memcmp(magic, IPF_MAGIC, 8) != 0) {
fprintf(stderr, "Error: Invalid iPF magic\n");
return -1;
}
// Read width (uint16 LE)
if (fread(&header->width, 2, 1, fp) != 1) return -1;
// Read height (uint16 LE)
if (fread(&header->height, 2, 1, fp) != 1) return -1;
// Read flags
if (fread(&header->flags, 1, 1, fp) != 1) return -1;
// Read type
if (fread(&header->type, 1, 1, fp) != 1) return -1;
// Skip reserved (10 bytes)
fseek(fp, 10, SEEK_CUR);
// Read uncompressed size (uint32 LE)
if (fread(&header->uncompressed_size, 4, 1, fp) != 1) return -1;
return 0;
}
// =============================================================================
// YCoCg to RGB Conversion
// =============================================================================
/**
* Convert YCoCg to RGB for 4 pixels sharing the same chroma.
* y_values: 4 Y values packed as nibbles (Y0|Y1 in low byte, Y2|Y3 in high byte style)
* a_values: 4 alpha values packed similarly
* co, cg: 4-bit chroma values [0..15]
*
* Output: fills rgb array with R,G,B[,A] values for 4 pixels
*/
static void ycocg_to_rgb_quad(int co, int cg, int y0, int y1, int y2, int y3,
int a0, int a1, int a2, int a3,
int has_alpha, uint8_t *rgb) {
// Convert chroma from [0..15] to [-1..1]
float co_f = (co - 7) / 8.0f;
float cg_f = (cg - 7) / 8.0f;
int ys[4] = {y0, y1, y2, y3};
int as[4] = {a0, a1, a2, a3};
int stride = has_alpha ? 4 : 3;
for (int i = 0; i < 4; i++) {
float y = ys[i] / 15.0f;
// YCoCg to RGB conversion
float tmp = y - cg_f / 2.0f;
float g = clampf(cg_f + tmp, 0.0f, 1.0f);
float b = clampf(tmp - co_f / 2.0f, 0.0f, 1.0f);
float r = clampf(b + co_f, 0.0f, 1.0f);
rgb[i * stride + 0] = (uint8_t)(r * 255.0f + 0.5f);
rgb[i * stride + 1] = (uint8_t)(g * 255.0f + 0.5f);
rgb[i * stride + 2] = (uint8_t)(b * 255.0f + 0.5f);
if (has_alpha) {
rgb[i * stride + 3] = (uint8_t)(as[i] * 17); // Scale 0-15 to 0-255
}
}
}
/**
* Decode iPF1 block (4:2:0 chroma subsampling).
* Input: 12 bytes (or 20 with alpha)
* Output: 16 pixels in RGB24/RGBA format
*/
static void decode_ipf1_block(const uint8_t *block, int has_alpha, uint8_t *pixels, int stride) {
// Read chroma (4 values for 2x2 regions)
int co1 = block[0] & 0x0F;
int co2 = (block[0] >> 4) & 0x0F;
int co3 = block[1] & 0x0F;
int co4 = (block[1] >> 4) & 0x0F;
int cg1 = block[2] & 0x0F;
int cg2 = (block[2] >> 4) & 0x0F;
int cg3 = block[3] & 0x0F;
int cg4 = (block[3] >> 4) & 0x0F;
// Read Y values (16 values)
// Layout: [Y1|Y0|Y5|Y4], [Y3|Y2|Y7|Y6], [Y9|Y8|YD|YC], [YB|YA|YF|YE]
int Y[16];
Y[0] = block[4] & 0x0F;
Y[1] = (block[4] >> 4) & 0x0F;
Y[4] = block[5] & 0x0F;
Y[5] = (block[5] >> 4) & 0x0F;
Y[2] = block[6] & 0x0F;
Y[3] = (block[6] >> 4) & 0x0F;
Y[6] = block[7] & 0x0F;
Y[7] = (block[7] >> 4) & 0x0F;
Y[8] = block[8] & 0x0F;
Y[9] = (block[8] >> 4) & 0x0F;
Y[12] = block[9] & 0x0F;
Y[13] = (block[9] >> 4) & 0x0F;
Y[10] = block[10] & 0x0F;
Y[11] = (block[10] >> 4) & 0x0F;
Y[14] = block[11] & 0x0F;
Y[15] = (block[11] >> 4) & 0x0F;
// Read alpha values if present
int A[16];
if (has_alpha) {
A[0] = block[12] & 0x0F;
A[1] = (block[12] >> 4) & 0x0F;
A[4] = block[13] & 0x0F;
A[5] = (block[13] >> 4) & 0x0F;
A[2] = block[14] & 0x0F;
A[3] = (block[14] >> 4) & 0x0F;
A[6] = block[15] & 0x0F;
A[7] = (block[15] >> 4) & 0x0F;
A[8] = block[16] & 0x0F;
A[9] = (block[16] >> 4) & 0x0F;
A[12] = block[17] & 0x0F;
A[13] = (block[17] >> 4) & 0x0F;
A[10] = block[18] & 0x0F;
A[11] = (block[18] >> 4) & 0x0F;
A[14] = block[19] & 0x0F;
A[15] = (block[19] >> 4) & 0x0F;
} else {
for (int i = 0; i < 16; i++) A[i] = 15;
}
int channels = has_alpha ? 4 : 3;
uint8_t quad[16]; // 4 pixels max
// Decode 4 quads (2x2 regions), each sharing one chroma pair
// Top-left quad (pixels 0,1,4,5) uses co1/cg1
ycocg_to_rgb_quad(co1, cg1, Y[0], Y[1], Y[4], Y[5], A[0], A[1], A[4], A[5], has_alpha, quad);
memcpy(pixels + 0 * stride + 0 * channels, quad + 0 * channels, channels);
memcpy(pixels + 0 * stride + 1 * channels, quad + 1 * channels, channels);
memcpy(pixels + 1 * stride + 0 * channels, quad + 2 * channels, channels);
memcpy(pixels + 1 * stride + 1 * channels, quad + 3 * channels, channels);
// Top-right quad (pixels 2,3,6,7) uses co2/cg2
ycocg_to_rgb_quad(co2, cg2, Y[2], Y[3], Y[6], Y[7], A[2], A[3], A[6], A[7], has_alpha, quad);
memcpy(pixels + 0 * stride + 2 * channels, quad + 0 * channels, channels);
memcpy(pixels + 0 * stride + 3 * channels, quad + 1 * channels, channels);
memcpy(pixels + 1 * stride + 2 * channels, quad + 2 * channels, channels);
memcpy(pixels + 1 * stride + 3 * channels, quad + 3 * channels, channels);
// Bottom-left quad (pixels 8,9,12,13) uses co3/cg3
ycocg_to_rgb_quad(co3, cg3, Y[8], Y[9], Y[12], Y[13], A[8], A[9], A[12], A[13], has_alpha, quad);
memcpy(pixels + 2 * stride + 0 * channels, quad + 0 * channels, channels);
memcpy(pixels + 2 * stride + 1 * channels, quad + 1 * channels, channels);
memcpy(pixels + 3 * stride + 0 * channels, quad + 2 * channels, channels);
memcpy(pixels + 3 * stride + 1 * channels, quad + 3 * channels, channels);
// Bottom-right quad (pixels 10,11,14,15) uses co4/cg4
ycocg_to_rgb_quad(co4, cg4, Y[10], Y[11], Y[14], Y[15], A[10], A[11], A[14], A[15], has_alpha, quad);
memcpy(pixels + 2 * stride + 2 * channels, quad + 0 * channels, channels);
memcpy(pixels + 2 * stride + 3 * channels, quad + 1 * channels, channels);
memcpy(pixels + 3 * stride + 2 * channels, quad + 2 * channels, channels);
memcpy(pixels + 3 * stride + 3 * channels, quad + 3 * channels, channels);
}
/**
* Decode iPF2 block (4:2:2 chroma subsampling).
* Input: 16 bytes (or 24 with alpha)
* Output: 16 pixels in RGB24/RGBA format
*/
static void decode_ipf2_block(const uint8_t *block, int has_alpha, uint8_t *pixels, int stride) {
// Read chroma (8 values for horizontal pairs)
int co[8], cg[8];
co[0] = block[0] & 0x0F;
co[1] = (block[0] >> 4) & 0x0F;
co[2] = block[1] & 0x0F;
co[3] = (block[1] >> 4) & 0x0F;
co[4] = block[2] & 0x0F;
co[5] = (block[2] >> 4) & 0x0F;
co[6] = block[3] & 0x0F;
co[7] = (block[3] >> 4) & 0x0F;
cg[0] = block[4] & 0x0F;
cg[1] = (block[4] >> 4) & 0x0F;
cg[2] = block[5] & 0x0F;
cg[3] = (block[5] >> 4) & 0x0F;
cg[4] = block[6] & 0x0F;
cg[5] = (block[6] >> 4) & 0x0F;
cg[6] = block[7] & 0x0F;
cg[7] = (block[7] >> 4) & 0x0F;
// Read Y values (16 values) - same layout as iPF1
int Y[16];
Y[0] = block[8] & 0x0F;
Y[1] = (block[8] >> 4) & 0x0F;
Y[4] = block[9] & 0x0F;
Y[5] = (block[9] >> 4) & 0x0F;
Y[2] = block[10] & 0x0F;
Y[3] = (block[10] >> 4) & 0x0F;
Y[6] = block[11] & 0x0F;
Y[7] = (block[11] >> 4) & 0x0F;
Y[8] = block[12] & 0x0F;
Y[9] = (block[12] >> 4) & 0x0F;
Y[12] = block[13] & 0x0F;
Y[13] = (block[13] >> 4) & 0x0F;
Y[10] = block[14] & 0x0F;
Y[11] = (block[14] >> 4) & 0x0F;
Y[14] = block[15] & 0x0F;
Y[15] = (block[15] >> 4) & 0x0F;
// Read alpha values if present
int A[16];
if (has_alpha) {
A[0] = block[16] & 0x0F;
A[1] = (block[16] >> 4) & 0x0F;
A[4] = block[17] & 0x0F;
A[5] = (block[17] >> 4) & 0x0F;
A[2] = block[18] & 0x0F;
A[3] = (block[18] >> 4) & 0x0F;
A[6] = block[19] & 0x0F;
A[7] = (block[19] >> 4) & 0x0F;
A[8] = block[20] & 0x0F;
A[9] = (block[20] >> 4) & 0x0F;
A[12] = block[21] & 0x0F;
A[13] = (block[21] >> 4) & 0x0F;
A[10] = block[22] & 0x0F;
A[11] = (block[22] >> 4) & 0x0F;
A[14] = block[23] & 0x0F;
A[15] = (block[23] >> 4) & 0x0F;
} else {
for (int i = 0; i < 16; i++) A[i] = 15;
}
int channels = has_alpha ? 4 : 3;
// iPF2: 4:2:2 - each horizontal pair shares chroma
// Row 0: pixels 0,1 share co[0]/cg[0], pixels 2,3 share co[1]/cg[1]
// Row 1: pixels 4,5 share co[2]/cg[2], pixels 6,7 share co[3]/cg[3]
// Row 2: pixels 8,9 share co[4]/cg[4], pixels 10,11 share co[5]/cg[5]
// Row 3: pixels 12,13 share co[6]/cg[6], pixels 14,15 share co[7]/cg[7]
int pixel_map[8][4] = {
{0, 1, 0, 1}, // co/cg index 0: pixels 0,1
{2, 3, 2, 3}, // co/cg index 1: pixels 2,3
{4, 5, 4, 5}, // co/cg index 2: pixels 4,5
{6, 7, 6, 7}, // co/cg index 3: pixels 6,7
{8, 9, 8, 9}, // co/cg index 4: pixels 8,9
{10, 11, 10, 11}, // co/cg index 5: pixels 10,11
{12, 13, 12, 13}, // co/cg index 6: pixels 12,13
{14, 15, 14, 15} // co/cg index 7: pixels 14,15
};
for (int ci = 0; ci < 8; ci++) {
int p0 = pixel_map[ci][0];
int p1 = pixel_map[ci][1];
uint8_t quad[16]; // 4 pixels max (ycocg_to_rgb_quad writes 4 pixels)
ycocg_to_rgb_quad(co[ci], cg[ci], Y[p0], Y[p1], Y[p0], Y[p1],
A[p0], A[p1], A[p0], A[p1], has_alpha, quad);
int row = p0 / 4;
int col0 = p0 % 4;
int col1 = p1 % 4;
memcpy(pixels + row * stride + col0 * channels, quad + 0 * channels, channels);
memcpy(pixels + row * stride + col1 * channels, quad + 1 * channels, channels);
}
}
// =============================================================================
// Main Decoding
// =============================================================================
static int decode_ipf(const decoder_config_t *cfg) {
FILE *fp = fopen(cfg->input_file, "rb");
if (!fp) {
fprintf(stderr, "Error: Failed to open input file: %s\n", cfg->input_file);
return -1;
}
// Read header
ipf_header_t header;
if (read_ipf_header(fp, &header) < 0) {
fclose(fp);
return -1;
}
int has_alpha = (header.flags & IPF_FLAG_ALPHA) != 0;
int use_zstd = (header.flags & IPF_FLAG_ZSTD) != 0;
int progressive = (header.flags & IPF_FLAG_PROGRESSIVE) != 0;
if (cfg->verbose) {
printf("iPF Header:\n");
printf(" Size: %dx%d\n", header.width, header.height);
printf(" Type: iPF%d (%s)\n", header.type + 1,
header.type == 0 ? "4:2:0" : "4:2:2");
printf(" Flags: %s%s%s\n",
has_alpha ? "alpha " : "",
use_zstd ? "zstd " : "",
progressive ? "progressive " : "");
printf(" Uncompressed size: %u bytes\n", header.uncompressed_size);
}
if (progressive) {
fprintf(stderr, "Warning: Progressive mode not implemented, decoding as sequential\n");
}
// Read compressed/raw block data
fseek(fp, 0, SEEK_END);
long file_size = ftell(fp);
fseek(fp, IPF_HEADER_SIZE, SEEK_SET);
size_t compressed_size = file_size - IPF_HEADER_SIZE;
uint8_t *compressed_data = malloc(compressed_size);
if (!compressed_data) {
fclose(fp);
fprintf(stderr, "Error: Failed to allocate memory\n");
return -1;
}
if (fread(compressed_data, 1, compressed_size, fp) != compressed_size) {
free(compressed_data);
fclose(fp);
fprintf(stderr, "Error: Failed to read block data\n");
return -1;
}
fclose(fp);
// Decompress if needed
uint8_t *block_data;
size_t block_data_size;
if (use_zstd) {
block_data_size = header.uncompressed_size;
block_data = malloc(block_data_size);
if (!block_data) {
free(compressed_data);
fprintf(stderr, "Error: Failed to allocate decompression buffer\n");
return -1;
}
size_t result = ZSTD_decompress(block_data, block_data_size,
compressed_data, compressed_size);
if (ZSTD_isError(result)) {
fprintf(stderr, "Error: Zstd decompression failed: %s\n",
ZSTD_getErrorName(result));
free(block_data);
free(compressed_data);
return -1;
}
if (cfg->verbose) {
printf("Decompressed: %zu -> %zu bytes\n", compressed_size, block_data_size);
}
free(compressed_data);
} else {
block_data = compressed_data;
block_data_size = compressed_size;
}
// Allocate output image
int channels = has_alpha ? 4 : 3;
size_t image_size = (size_t)header.width * header.height * channels;
uint8_t *image = malloc(image_size);
if (!image) {
free(block_data);
fprintf(stderr, "Error: Failed to allocate image buffer\n");
return -1;
}
// Decode blocks
int blocks_x = (header.width + 3) / 4;
int blocks_y = (header.height + 3) / 4;
int block_size = (header.type == IPF_TYPE_1) ? (has_alpha ? 20 : 12) : (has_alpha ? 24 : 16);
int row_stride = header.width * channels;
int block_stride = 4 * channels; // 4 pixels per block row
size_t block_offset = 0;
for (int by = 0; by < blocks_y; by++) {
for (int bx = 0; bx < blocks_x; bx++) {
// Calculate output position
uint8_t *block_pixels = image + by * 4 * row_stride + bx * block_stride;
if (header.type == IPF_TYPE_1) {
decode_ipf1_block(block_data + block_offset, has_alpha, block_pixels, row_stride);
} else {
decode_ipf2_block(block_data + block_offset, has_alpha, block_pixels, row_stride);
}
block_offset += block_size;
}
}
free(block_data);
if (cfg->verbose) {
printf("Decoded %d blocks (%dx%d)\n", blocks_x * blocks_y, blocks_x, blocks_y);
}
// Output image
int result = 0;
if (cfg->raw_output) {
// Write raw RGB/RGBA data
FILE *out = fopen(cfg->output_file, "wb");
if (!out) {
fprintf(stderr, "Error: Failed to open output file: %s\n", cfg->output_file);
result = -1;
} else {
fwrite(image, 1, image_size, out);
fclose(out);
if (cfg->verbose) {
printf("Wrote %zu bytes raw %s data\n", image_size, has_alpha ? "RGBA" : "RGB24");
}
}
} else {
// Use FFmpeg to write output image
char cmd[MAX_PATH * 2];
const char *pix_fmt = has_alpha ? "rgba" : "rgb24";
snprintf(cmd, sizeof(cmd),
"ffmpeg -hide_banner -v quiet -y -f rawvideo -pix_fmt %s -s %dx%d "
"-i - \"%s\"",
pix_fmt, header.width, header.height, cfg->output_file);
if (cfg->verbose) {
printf("FFmpeg command: %s\n", cmd);
}
FILE *pipe = popen(cmd, "w");
if (!pipe) {
fprintf(stderr, "Error: Failed to start FFmpeg\n");
result = -1;
} else {
fwrite(image, 1, image_size, pipe);
int status = pclose(pipe);
if (status != 0) {
fprintf(stderr, "Error: FFmpeg failed with status %d\n", status);
result = -1;
}
}
}
free(image);
return result;
}
// =============================================================================
// Main Entry Point
// =============================================================================
int main(int argc, char *argv[]) {
decoder_config_t cfg = {
.input_file = NULL,
.output_file = NULL,
.verbose = 0,
.raw_output = 0
};
static struct option long_options[] = {
{"input", required_argument, 0, 'i'},
{"output", required_argument, 0, 'o'},
{"raw", no_argument, 0, 'R'},
{"verbose", no_argument, 0, 'v'},
{"help", no_argument, 0, 'h'},
{0, 0, 0, 0}
};
int opt;
while ((opt = getopt_long(argc, argv, "i:o:vh", long_options, NULL)) != -1) {
switch (opt) {
case 'i':
cfg.input_file = optarg;
break;
case 'o':
cfg.output_file = optarg;
break;
case 'R':
cfg.raw_output = 1;
break;
case 'v':
cfg.verbose = 1;
break;
case 'h':
print_usage(argv[0]);
return 0;
default:
print_usage(argv[0]);
return 1;
}
}
// Validate required arguments
if (!cfg.input_file || !cfg.output_file) {
fprintf(stderr, "Error: Input and output files are required\n\n");
print_usage(argv[0]);
return 1;
}
int result = decode_ipf(&cfg);
if (result == 0) {
printf("Successfully decoded: %s\n", cfg.output_file);
}
return result == 0 ? 0 : 1;
}

787
ipf_encoder/encoder_ipf.c Normal file
View File

@@ -0,0 +1,787 @@
/**
* iPF Encoder - TSVM Interchangeable Picture Format Encoder
*
* Encodes images to iPF format (Type 1 or Type 2) with:
* - YCoCg colour space with chroma subsampling
* - 4x4 block encoding
* - Optional Zstd compression
* - Optional alpha channel
* - Optional Adam7 progressive ordering
*
* Created by CuriousTorvald and Claude on 2025-12-19.
*/
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <math.h>
#include <getopt.h>
#include <zstd.h>
// =============================================================================
// Constants
// =============================================================================
#define IPF_MAGIC "\x1F\x54\x53\x56\x4D\x69\x50\x46" // "\x1FTSVMiPF"
#define IPF_HEADER_SIZE 28 // 8 magic + 2 width + 2 height + 1 flags + 1 type + 10 reserved + 4 uncompressed size
#define DEFAULT_WIDTH 560
#define DEFAULT_HEIGHT 448
#define IPF_TYPE_1 0 // 4:2:0 chroma subsampling (12 bytes per block, +8 with alpha)
#define IPF_TYPE_2 1 // 4:2:2 chroma subsampling (16 bytes per block, +8 with alpha)
#define IPF_FLAG_ALPHA 0x01 // Has alpha channel
#define IPF_FLAG_ZSTD 0x10 // Zstd compressed
#define IPF_FLAG_PROGRESSIVE 0x80 // Adam7 progressive ordering
#define MAX_PATH 4096
// Bayer dithering kernel (4x4)
static const float BAYER_4X4[16] = {
0.0f/16.0f, 8.0f/16.0f, 2.0f/16.0f, 10.0f/16.0f,
12.0f/16.0f, 4.0f/16.0f, 14.0f/16.0f, 6.0f/16.0f,
3.0f/16.0f, 11.0f/16.0f, 1.0f/16.0f, 9.0f/16.0f,
15.0f/16.0f, 7.0f/16.0f, 13.0f/16.0f, 5.0f/16.0f
};
// Adam7 interlace pattern - pass number (1-7) for each pixel in 8x8 block
// 0 = not in this standard pattern, we'll adapt for 4x4 blocks
static const int ADAM7_PASS[8][8] = {
{1, 6, 4, 6, 2, 6, 4, 6},
{7, 7, 7, 7, 7, 7, 7, 7},
{5, 6, 5, 6, 5, 6, 5, 6},
{7, 7, 7, 7, 7, 7, 7, 7},
{3, 6, 4, 6, 3, 6, 4, 6},
{7, 7, 7, 7, 7, 7, 7, 7},
{5, 6, 5, 6, 5, 6, 5, 6},
{7, 7, 7, 7, 7, 7, 7, 7}
};
// =============================================================================
// Structures
// =============================================================================
typedef struct {
char *input_file;
char *output_file;
int width;
int height;
int ipf_type; // 0 = iPF1, 1 = iPF2
int use_zstd; // 1 = compress with Zstd
int force_alpha; // 1 = force alpha channel in output
int no_alpha; // 1 = strip alpha even if present in input
int progressive; // 1 = Adam7 progressive ordering
int dither; // Bayer dither pattern index (-1 = no dithering)
int verbose;
} encoder_config_t;
typedef struct {
uint8_t *data; // RGB or RGBA data
int width;
int height;
int channels; // 3 = RGB, 4 = RGBA
int has_alpha; // 1 if input image has meaningful alpha
} image_t;
// =============================================================================
// Utility Functions
// =============================================================================
static void print_usage(const char *program) {
printf("iPF Encoder - TSVM Interchangeable Picture Format\n");
printf("\nUsage: %s -i input.png -o output.ipf [options]\n\n", program);
printf("Required:\n");
printf(" -i, --input FILE Input image file (any format FFmpeg supports)\n");
printf(" -o, --output FILE Output iPF file\n");
printf("\nOptions:\n");
printf(" -s, --size WxH Output size (default: %dx%d)\n", DEFAULT_WIDTH, DEFAULT_HEIGHT);
printf(" -t, --type N iPF type: 1 (4:2:0, default) or 2 (4:2:2)\n");
printf(" --no-zstd Disable Zstd compression (default: enabled)\n");
printf(" --alpha Force alpha channel in output\n");
printf(" --no-alpha Strip alpha channel from input\n");
printf(" -p, --progressive Use Adam7 progressive ordering\n");
printf(" -d, --dither N Bayer dither pattern (0=4x4, -1=none, default: 0)\n");
printf(" -v, --verbose Verbose output\n");
printf(" -h, --help Show this help\n");
printf("\nExamples:\n");
printf(" %s -i photo.jpg -o photo.ipf\n", program);
printf(" %s -i logo.png -o logo.ipf --alpha\n", program);
printf(" %s -i image.png -o image.ipf -s 280x224 -t 2\n", program);
}
static int clampi(int v, int lo, int hi) {
return v < lo ? lo : (v > hi ? hi : v);
}
// Convert chroma value [-1..1] to 4-bit [0..15]
static int chroma_to_four_bits(float f) {
return clampi((int)roundf(f * 8.0f) + 7, 0, 15);
}
// =============================================================================
// Image Loading via FFmpeg
// =============================================================================
/**
* Probe input image dimensions using FFmpeg.
* Returns 0 on success, -1 on error.
*/
static int probe_image_dimensions(const char *input_file, int *width, int *height, int *has_alpha) {
char cmd[MAX_PATH * 2];
// Use ffprobe to get dimensions and pixel format
snprintf(cmd, sizeof(cmd),
"ffprobe -v quiet -select_streams v:0 -show_entries stream=width,height,pix_fmt "
"-of csv=p=0:s=x \"%s\" 2>/dev/null",
input_file);
FILE *fp = popen(cmd, "r");
if (!fp) {
fprintf(stderr, "Error: Failed to run ffprobe\n");
return -1;
}
char buffer[256];
if (fgets(buffer, sizeof(buffer), fp) == NULL) {
pclose(fp);
fprintf(stderr, "Error: Failed to read image info\n");
return -1;
}
pclose(fp);
// Parse "width x height x pix_fmt"
char pix_fmt[64] = "";
if (sscanf(buffer, "%dx%dx%63s", width, height, pix_fmt) < 2) {
// Try alternate format without pix_fmt
if (sscanf(buffer, "%dx%d", width, height) != 2) {
fprintf(stderr, "Error: Failed to parse image dimensions\n");
return -1;
}
}
// Check if pixel format indicates alpha
*has_alpha = (strstr(pix_fmt, "rgba") != NULL ||
strstr(pix_fmt, "argb") != NULL ||
strstr(pix_fmt, "bgra") != NULL ||
strstr(pix_fmt, "abgr") != NULL ||
strstr(pix_fmt, "ya") != NULL ||
strstr(pix_fmt, "pal8") != NULL || // palette may have alpha
strstr(pix_fmt, "yuva") != NULL);
return 0;
}
/**
* Load and resize image using FFmpeg.
* Maintains aspect ratio and crops to target size.
* Returns image data or NULL on error.
*/
static image_t* load_image(const char *input_file, int target_width, int target_height,
int want_alpha, int verbose) {
int src_width, src_height, src_has_alpha;
// Probe source dimensions
if (probe_image_dimensions(input_file, &src_width, &src_height, &src_has_alpha) < 0) {
return NULL;
}
if (verbose) {
printf("Source image: %dx%d, alpha: %s\n",
src_width, src_height, src_has_alpha ? "yes" : "no");
}
// Determine if we need alpha channel
int use_alpha = want_alpha || src_has_alpha;
int channels = use_alpha ? 4 : 3;
const char *pix_fmt = use_alpha ? "rgba" : "rgb24";
// Build FFmpeg command with scale and crop filter
char cmd[MAX_PATH * 2];
snprintf(cmd, sizeof(cmd),
"ffmpeg -hide_banner -v quiet -i \"%s\" -f rawvideo -pix_fmt %s -vf "
"\"scale=%d:%d:force_original_aspect_ratio=increase,crop=%d:%d\" -frames:v 1 -",
input_file, pix_fmt, target_width, target_height, target_width, target_height);
if (verbose) {
printf("FFmpeg command: %s\n", cmd);
}
FILE *fp = popen(cmd, "r");
if (!fp) {
fprintf(stderr, "Error: Failed to start FFmpeg\n");
return NULL;
}
// Allocate image
image_t *img = malloc(sizeof(image_t));
if (!img) {
pclose(fp);
return NULL;
}
size_t data_size = (size_t)target_width * target_height * channels;
img->data = malloc(data_size);
if (!img->data) {
free(img);
pclose(fp);
return NULL;
}
img->width = target_width;
img->height = target_height;
img->channels = channels;
img->has_alpha = use_alpha;
// Read image data
size_t bytes_read = fread(img->data, 1, data_size, fp);
pclose(fp);
if (bytes_read != data_size) {
fprintf(stderr, "Error: Expected %zu bytes, got %zu\n", data_size, bytes_read);
free(img->data);
free(img);
return NULL;
}
if (verbose) {
printf("Loaded %dx%d image, %d channels, %zu bytes\n",
img->width, img->height, img->channels, data_size);
}
return img;
}
static void free_image(image_t *img) {
if (img) {
free(img->data);
free(img);
}
}
// =============================================================================
// iPF Block Encoding
// =============================================================================
/**
* Encode a 4x4 block to YCoCg with dithering.
* Returns arrays of Y (16 values), A (16 values), Co (16 values), Cg (16 values).
*/
static void encode_block_to_ycocg(const image_t *img, int block_x, int block_y,
int dither_pattern,
int *Y_out, int *A_out, float *Co_out, float *Cg_out) {
for (int py = 0; py < 4; py++) {
for (int px = 0; px < 4; px++) {
int ox = block_x * 4 + px;
int oy = block_y * 4 + py;
// Handle out-of-bounds (extend edge pixels)
ox = clampi(ox, 0, img->width - 1);
oy = clampi(oy, 0, img->height - 1);
// Get dither threshold
float t = 0.0f;
if (dither_pattern >= 0) {
t = BAYER_4X4[(py % 4) * 4 + (px % 4)];
}
// Read pixel
int offset = (oy * img->width + ox) * img->channels;
float r0 = img->data[offset + 0] / 255.0f;
float g0 = (img->channels >= 3) ? img->data[offset + 1] / 255.0f : r0;
float b0 = (img->channels >= 3) ? img->data[offset + 2] / 255.0f : r0;
float a0 = (img->channels == 4) ? img->data[offset + 3] / 255.0f : 1.0f;
// Apply dithering
float r = floorf((t / 15.0f + r0) * 15.0f) / 15.0f;
float g = floorf((t / 15.0f + g0) * 15.0f) / 15.0f;
float b = floorf((t / 15.0f + b0) * 15.0f) / 15.0f;
float a = floorf((t / 15.0f + a0) * 15.0f) / 15.0f;
// Convert to YCoCg
float co = r - b; // [-1..1]
float tmp = b + co / 2.0f;
float cg = g - tmp; // [-1..1]
float y = tmp + cg / 2.0f; // [0..1]
int index = py * 4 + px;
Y_out[index] = (int)roundf(y * 15.0f);
A_out[index] = (int)roundf(a * 15.0f);
Co_out[index] = co;
Cg_out[index] = cg;
}
}
}
/**
* Encode iPF1 block (4:2:0 chroma subsampling).
* Returns 12 bytes (or 20 with alpha).
*/
static int encode_ipf1_block(const int *Ys, const int *As, const float *COs, const float *CGs,
int has_alpha, uint8_t *out) {
// Subsample Co/Cg by averaging 2x2 regions (4:2:0)
int cos1 = chroma_to_four_bits((COs[0] + COs[1] + COs[4] + COs[5]) / 4.0f);
int cos2 = chroma_to_four_bits((COs[2] + COs[3] + COs[6] + COs[7]) / 4.0f);
int cos3 = chroma_to_four_bits((COs[8] + COs[9] + COs[12] + COs[13]) / 4.0f);
int cos4 = chroma_to_four_bits((COs[10] + COs[11] + COs[14] + COs[15]) / 4.0f);
int cgs1 = chroma_to_four_bits((CGs[0] + CGs[1] + CGs[4] + CGs[5]) / 4.0f);
int cgs2 = chroma_to_four_bits((CGs[2] + CGs[3] + CGs[6] + CGs[7]) / 4.0f);
int cgs3 = chroma_to_four_bits((CGs[8] + CGs[9] + CGs[12] + CGs[13]) / 4.0f);
int cgs4 = chroma_to_four_bits((CGs[10] + CGs[11] + CGs[14] + CGs[15]) / 4.0f);
// Pack according to iPF1 format
// uint16 [Co4 | Co3 | Co2 | Co1]
out[0] = (cos2 << 4) | cos1;
out[1] = (cos4 << 4) | cos3;
// uint16 [Cg4 | Cg3 | Cg2 | Cg1]
out[2] = (cgs2 << 4) | cgs1;
out[3] = (cgs4 << 4) | cgs3;
// Y values: [Y1|Y0|Y5|Y4], [Y3|Y2|Y7|Y6], [Y9|Y8|YD|YC], [YB|YA|YF|YE]
out[4] = (Ys[1] << 4) | Ys[0];
out[5] = (Ys[5] << 4) | Ys[4];
out[6] = (Ys[3] << 4) | Ys[2];
out[7] = (Ys[7] << 4) | Ys[6];
out[8] = (Ys[9] << 4) | Ys[8];
out[9] = (Ys[13] << 4) | Ys[12];
out[10] = (Ys[11] << 4) | Ys[10];
out[11] = (Ys[15] << 4) | Ys[14];
int block_size = 12;
if (has_alpha) {
// Alpha values: same layout as Y
out[12] = (As[1] << 4) | As[0];
out[13] = (As[5] << 4) | As[4];
out[14] = (As[3] << 4) | As[2];
out[15] = (As[7] << 4) | As[6];
out[16] = (As[9] << 4) | As[8];
out[17] = (As[13] << 4) | As[12];
out[18] = (As[11] << 4) | As[10];
out[19] = (As[15] << 4) | As[14];
block_size = 20;
}
return block_size;
}
/**
* Encode iPF2 block (4:2:2 chroma subsampling).
* Returns 16 bytes (or 24 with alpha).
*/
static int encode_ipf2_block(const int *Ys, const int *As, const float *COs, const float *CGs,
int has_alpha, uint8_t *out) {
// Subsample Co/Cg horizontally only (4:2:2) - 8 values each
int cos1 = chroma_to_four_bits((COs[0] + COs[1]) / 2.0f);
int cos2 = chroma_to_four_bits((COs[2] + COs[3]) / 2.0f);
int cos3 = chroma_to_four_bits((COs[4] + COs[5]) / 2.0f);
int cos4 = chroma_to_four_bits((COs[6] + COs[7]) / 2.0f);
int cos5 = chroma_to_four_bits((COs[8] + COs[9]) / 2.0f);
int cos6 = chroma_to_four_bits((COs[10] + COs[11]) / 2.0f);
int cos7 = chroma_to_four_bits((COs[12] + COs[13]) / 2.0f);
int cos8 = chroma_to_four_bits((COs[14] + COs[15]) / 2.0f);
int cgs1 = chroma_to_four_bits((CGs[0] + CGs[1]) / 2.0f);
int cgs2 = chroma_to_four_bits((CGs[2] + CGs[3]) / 2.0f);
int cgs3 = chroma_to_four_bits((CGs[4] + CGs[5]) / 2.0f);
int cgs4 = chroma_to_four_bits((CGs[6] + CGs[7]) / 2.0f);
int cgs5 = chroma_to_four_bits((CGs[8] + CGs[9]) / 2.0f);
int cgs6 = chroma_to_four_bits((CGs[10] + CGs[11]) / 2.0f);
int cgs7 = chroma_to_four_bits((CGs[12] + CGs[13]) / 2.0f);
int cgs8 = chroma_to_four_bits((CGs[14] + CGs[15]) / 2.0f);
// Pack according to iPF2 format
// uint32 [Co8 | Co7 | Co6 | Co5 | Co4 | Co3 | Co2 | Co1]
out[0] = (cos2 << 4) | cos1;
out[1] = (cos4 << 4) | cos3;
out[2] = (cos6 << 4) | cos5;
out[3] = (cos8 << 4) | cos7;
// uint32 [Cg8 | Cg7 | Cg6 | Cg5 | Cg4 | Cg3 | Cg2 | Cg1]
out[4] = (cgs2 << 4) | cgs1;
out[5] = (cgs4 << 4) | cgs3;
out[6] = (cgs6 << 4) | cgs5;
out[7] = (cgs8 << 4) | cgs7;
// Y values: same as iPF1
out[8] = (Ys[1] << 4) | Ys[0];
out[9] = (Ys[5] << 4) | Ys[4];
out[10] = (Ys[3] << 4) | Ys[2];
out[11] = (Ys[7] << 4) | Ys[6];
out[12] = (Ys[9] << 4) | Ys[8];
out[13] = (Ys[13] << 4) | Ys[12];
out[14] = (Ys[11] << 4) | Ys[10];
out[15] = (Ys[15] << 4) | Ys[14];
int block_size = 16;
if (has_alpha) {
// Alpha values: same layout as Y
out[16] = (As[1] << 4) | As[0];
out[17] = (As[5] << 4) | As[4];
out[18] = (As[3] << 4) | As[2];
out[19] = (As[7] << 4) | As[6];
out[20] = (As[9] << 4) | As[8];
out[21] = (As[13] << 4) | As[12];
out[22] = (As[11] << 4) | As[10];
out[23] = (As[15] << 4) | As[14];
block_size = 24;
}
return block_size;
}
// =============================================================================
// Adam7 Progressive Ordering
// =============================================================================
/**
* Get Adam7 pass number for a block at (block_x, block_y).
* For blocks, we use a simplified version based on block position.
*/
static int get_adam7_pass(int block_x, int block_y) {
// Use Adam7 pattern for 8x8 blocks, but adapt for 4x4 block indices
int px = (block_x * 4) % 8;
int py = (block_y * 4) % 8;
return ADAM7_PASS[py][px];
}
/**
* Encode blocks in Adam7 progressive order.
* Returns the encoded block data in progressive order.
*/
static uint8_t* encode_progressive(const image_t *img, const encoder_config_t *cfg,
int has_alpha, size_t *out_size) {
int blocks_x = (img->width + 3) / 4;
int blocks_y = (img->height + 3) / 4;
int total_blocks = blocks_x * blocks_y;
int block_size = (cfg->ipf_type == IPF_TYPE_1) ? (has_alpha ? 20 : 12) : (has_alpha ? 24 : 16);
size_t max_size = (size_t)total_blocks * block_size;
uint8_t *output = malloc(max_size);
if (!output) return NULL;
// Temporary storage for all encoded blocks
uint8_t *all_blocks = malloc(max_size);
if (!all_blocks) {
free(output);
return NULL;
}
// Encode all blocks first
size_t offset = 0;
for (int by = 0; by < blocks_y; by++) {
for (int bx = 0; bx < blocks_x; bx++) {
int Ys[16], As[16];
float COs[16], CGs[16];
encode_block_to_ycocg(img, bx, by, cfg->dither, Ys, As, COs, CGs);
if (cfg->ipf_type == IPF_TYPE_1) {
encode_ipf1_block(Ys, As, COs, CGs, has_alpha, all_blocks + offset);
} else {
encode_ipf2_block(Ys, As, COs, CGs, has_alpha, all_blocks + offset);
}
offset += block_size;
}
}
// Reorder blocks according to Adam7 progressive order (7 passes)
size_t out_offset = 0;
for (int pass = 1; pass <= 7; pass++) {
for (int by = 0; by < blocks_y; by++) {
for (int bx = 0; bx < blocks_x; bx++) {
if (get_adam7_pass(bx, by) == pass) {
int block_idx = by * blocks_x + bx;
memcpy(output + out_offset, all_blocks + block_idx * block_size, block_size);
out_offset += block_size;
}
}
}
}
free(all_blocks);
*out_size = out_offset;
return output;
}
/**
* Encode blocks in sequential (raster) order.
*/
static uint8_t* encode_sequential(const image_t *img, const encoder_config_t *cfg,
int has_alpha, size_t *out_size) {
int blocks_x = (img->width + 3) / 4;
int blocks_y = (img->height + 3) / 4;
int total_blocks = blocks_x * blocks_y;
int block_size = (cfg->ipf_type == IPF_TYPE_1) ? (has_alpha ? 20 : 12) : (has_alpha ? 24 : 16);
size_t max_size = (size_t)total_blocks * block_size;
uint8_t *output = malloc(max_size);
if (!output) return NULL;
size_t offset = 0;
for (int by = 0; by < blocks_y; by++) {
for (int bx = 0; bx < blocks_x; bx++) {
int Ys[16], As[16];
float COs[16], CGs[16];
encode_block_to_ycocg(img, bx, by, cfg->dither, Ys, As, COs, CGs);
if (cfg->ipf_type == IPF_TYPE_1) {
offset += encode_ipf1_block(Ys, As, COs, CGs, has_alpha, output + offset);
} else {
offset += encode_ipf2_block(Ys, As, COs, CGs, has_alpha, output + offset);
}
}
}
*out_size = offset;
return output;
}
// =============================================================================
// iPF File Writing
// =============================================================================
static int write_ipf_file(const char *output_file, const encoder_config_t *cfg,
const image_t *img, int verbose) {
// Determine if we use alpha
int has_alpha = 0;
if (cfg->force_alpha) {
has_alpha = 1;
} else if (!cfg->no_alpha && img->has_alpha) {
has_alpha = 1;
}
// Encode blocks
size_t block_data_size;
uint8_t *block_data;
if (cfg->progressive) {
block_data = encode_progressive(img, cfg, has_alpha, &block_data_size);
} else {
block_data = encode_sequential(img, cfg, has_alpha, &block_data_size);
}
if (!block_data) {
fprintf(stderr, "Error: Failed to encode image blocks\n");
return -1;
}
if (verbose) {
printf("Encoded %zu bytes of block data\n", block_data_size);
}
// Prepare output data (may be compressed)
uint8_t *output_data = block_data;
size_t output_size = block_data_size;
uint8_t *compressed_data = NULL;
if (cfg->use_zstd) {
size_t max_compressed = ZSTD_compressBound(block_data_size);
compressed_data = malloc(max_compressed);
if (!compressed_data) {
free(block_data);
fprintf(stderr, "Error: Failed to allocate compression buffer\n");
return -1;
}
output_size = ZSTD_compress(compressed_data, max_compressed,
block_data, block_data_size, 7);
if (ZSTD_isError(output_size)) {
fprintf(stderr, "Error: Zstd compression failed: %s\n",
ZSTD_getErrorName(output_size));
free(block_data);
free(compressed_data);
return -1;
}
output_data = compressed_data;
if (verbose) {
printf("Compressed: %zu -> %zu bytes (%.1f%%)\n",
block_data_size, output_size,
100.0 * output_size / block_data_size);
}
}
// Open output file
FILE *fp = fopen(output_file, "wb");
if (!fp) {
fprintf(stderr, "Error: Failed to open output file: %s\n", output_file);
free(block_data);
if (compressed_data) free(compressed_data);
return -1;
}
// Build flags byte
uint8_t flags = 0;
if (has_alpha) flags |= IPF_FLAG_ALPHA;
if (cfg->use_zstd) flags |= IPF_FLAG_ZSTD;
if (cfg->progressive) flags |= IPF_FLAG_PROGRESSIVE | IPF_FLAG_ZSTD; // Progressive always sets zstd flag
// Write header
// Magic: "\x1FTSVMiPF" (8 bytes)
fwrite(IPF_MAGIC, 1, 8, fp);
// Width (uint16 LE)
uint16_t width_le = (uint16_t)cfg->width;
fwrite(&width_le, 2, 1, fp);
// Height (uint16 LE)
uint16_t height_le = (uint16_t)cfg->height;
fwrite(&height_le, 2, 1, fp);
// Flags (uint8)
fwrite(&flags, 1, 1, fp);
// Type (uint8)
uint8_t type_byte = (uint8_t)cfg->ipf_type;
fwrite(&type_byte, 1, 1, fp);
// Reserved (10 bytes)
uint8_t reserved[10] = {0};
fwrite(reserved, 1, 10, fp);
// Uncompressed size (uint32 LE)
uint32_t uncompressed_size_le = (uint32_t)block_data_size;
fwrite(&uncompressed_size_le, 4, 1, fp);
// Write block data
fwrite(output_data, 1, output_size, fp);
fclose(fp);
if (verbose) {
printf("Wrote %zu bytes to %s\n", IPF_HEADER_SIZE + output_size, output_file);
printf(" Format: iPF%d, %dx%d\n", cfg->ipf_type + 1, cfg->width, cfg->height);
printf(" Flags: %s%s%s\n",
has_alpha ? "alpha " : "",
cfg->use_zstd ? "zstd " : "",
cfg->progressive ? "progressive " : "");
}
free(block_data);
if (compressed_data) free(compressed_data);
return 0;
}
// =============================================================================
// Main Entry Point
// =============================================================================
static int parse_size(const char *arg, int *width, int *height) {
return sscanf(arg, "%dx%d", width, height) == 2 ? 0 : -1;
}
int main(int argc, char *argv[]) {
encoder_config_t cfg = {
.input_file = NULL,
.output_file = NULL,
.width = DEFAULT_WIDTH,
.height = DEFAULT_HEIGHT,
.ipf_type = IPF_TYPE_1,
.use_zstd = 1,
.force_alpha = 0,
.no_alpha = 0,
.progressive = 0,
.dither = 0,
.verbose = 0
};
static struct option long_options[] = {
{"input", required_argument, 0, 'i'},
{"output", required_argument, 0, 'o'},
{"size", required_argument, 0, 's'},
{"type", required_argument, 0, 't'},
{"no-zstd", no_argument, 0, 'Z'},
{"alpha", no_argument, 0, 'A'},
{"no-alpha", no_argument, 0, 'N'},
{"progressive", no_argument, 0, 'p'},
{"dither", required_argument, 0, 'd'},
{"verbose", no_argument, 0, 'v'},
{"help", no_argument, 0, 'h'},
{0, 0, 0, 0}
};
int opt;
while ((opt = getopt_long(argc, argv, "i:o:s:t:pd:vh", long_options, NULL)) != -1) {
switch (opt) {
case 'i':
cfg.input_file = optarg;
break;
case 'o':
cfg.output_file = optarg;
break;
case 's':
if (parse_size(optarg, &cfg.width, &cfg.height) != 0) {
fprintf(stderr, "Error: Invalid size format (use WxH)\n");
return 1;
}
break;
case 't':
cfg.ipf_type = atoi(optarg) - 1; // User specifies 1 or 2
if (cfg.ipf_type < 0 || cfg.ipf_type > 1) {
fprintf(stderr, "Error: Invalid iPF type (use 1 or 2)\n");
return 1;
}
break;
case 'Z':
cfg.use_zstd = 0;
break;
case 'A':
cfg.force_alpha = 1;
break;
case 'N':
cfg.no_alpha = 1;
break;
case 'p':
cfg.progressive = 1;
break;
case 'd':
cfg.dither = atoi(optarg);
break;
case 'v':
cfg.verbose = 1;
break;
case 'h':
print_usage(argv[0]);
return 0;
default:
print_usage(argv[0]);
return 1;
}
}
// Validate required arguments
if (!cfg.input_file || !cfg.output_file) {
fprintf(stderr, "Error: Input and output files are required\n\n");
print_usage(argv[0]);
return 1;
}
// Load image
if (cfg.verbose) {
printf("Loading image: %s\n", cfg.input_file);
}
image_t *img = load_image(cfg.input_file, cfg.width, cfg.height,
cfg.force_alpha, cfg.verbose);
if (!img) {
fprintf(stderr, "Error: Failed to load image\n");
return 1;
}
// Encode and write iPF file
int result = write_ipf_file(cfg.output_file, &cfg, img, cfg.verbose);
free_image(img);
if (result == 0) {
printf("Successfully encoded: %s\n", cfg.output_file);
}
return result == 0 ? 0 : 1;
}

View File

@@ -1,20 +1,8 @@
## How To Edit the Graaljs Jars
## GraalJS JAR Editing (OBSOLETE)
0. Download following from Maven:
The META-INF/services cross-registration hack was needed for GraalJS 22.3.1 where
`js` and `regex` JARs each needed the other's `TruffleLanguage$Provider` registered.
org.graalvm.js:js:00.0.0
org.graalvm.js:js-scriptengine:00.0.0
1. grab `js-00.0.0.jar`
2. on `META-INF/services/com.oracle.truffle.api.TruffleLanguage$Provider`, edit as shown:
com.oracle.truffle.js.lang.JavaScriptLanguageProvider (existing line)
com.oracle.truffle.regex.RegexLanguageProvider (<< add this line)
3. grab `regex-00.0.0.jar`
4. on `META-INF/services/com.oracle.truffle.api.TruffleLanguage$Provider`, edit as shown:
com.oracle.truffle.regex.RegexLanguageProvider (existing line)
com.oracle.truffle.js.lang.JavaScriptLanguageProvider (<< add this line)
5. Re-zip two files
As of GraalJS 24.1.2, the service discovery mechanism changed to
`TruffleLanguageProvider` and each JAR registers its own provider independently.
No JAR editing is required.

Binary file not shown.

Binary file not shown.

BIN
lib/collections-23.1.10.jar Normal file

Binary file not shown.

Binary file not shown.

BIN
lib/compiler-23.1.10.jar Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
lib/icu4j-23.1.10.jar Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
lib/jniutils-23.1.10.jar Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
lib/js-language-23.1.10.jar Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
lib/nativeimage-23.1.10.jar Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
lib/polyglot-23.1.10.jar Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
lib/regex-23.1.10.jar Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
lib/word-23.1.10.jar Normal file

Binary file not shown.

View File

@@ -235,45 +235,6 @@ Memory Space
Palette stored in following pattern: 0b rrrr gggg, 0b bbbb aaaa, ....
Palette number 255 is always full transparent (bits being all zero)
(DRAFT) Optional Sprite Card (VRAM Bank 1 (256 kB))
250880 bytes
One of:
Secondary layer
Other 8-bit of the primary framebuffer (4K colour mode)
SPRITE FORMAT DRAFT 1
533 bytes: Sprite attribute table
(41 sprites total, of which 1 is GUI cursor)
12 bytes - signed fixed point
X-position
Y-position
Transform matrix A..D
1 bytes
0b 0000 00vp
(p: 0 for above-all, 1 for below-text, v: show/hide)
10496 bytes: Sprite table
256 bytes
16x16 texture for the sprite
235 bytes:
unused
SPRITE FORMAT DRAFT 2
DMA Sprite Area - 18 bytes each, total of ??? sprites
1 byte
Sprite width
1 byte
Sprite height
12 bytes - signed fixed point
Affine transformation A,B,C,D,X,Y
1 byte
Attributes
0b 0000 00vp
(p: 0 for above-all, 1 for below-text, v: show/hide)
3 bytes
Pointer to raw pixmap data in Core Memory
MMIO
0..1 RO
@@ -303,8 +264,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
@@ -387,21 +350,6 @@ MMIO
Text-mode-font-ROM is immutable and does not belong to VRAM
Even in the text mode framebuffer is still being drawn onto the screen, and the texts are drawn on top of it
Copper Commands (suggestion withdrawn)
WAITFOR 3,32
80·03 46 00 (0x004603: offset on the framebuffer)
SCROLLX 569
A0·39 02 00
SCROLLY 321
B0·41 01 00
SETPAL 5 (15 2 8 15)
C0·05·F2 8F (0x05: Palette number, 0xF28F: RGBA colour)
SETBG (15 2 8 15)
D0·00·F2 8F (0xF28F: RGBA colour)
END (pseudocommand of WAITFOR)
80·FF FF FF
--------------------------------------------------------------------------------
TSVM MOV file format
@@ -562,7 +510,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 +524,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 +533,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
@@ -920,6 +868,7 @@ transmission capability, and region-of-interest coding.
uint16 Height: picture height in pixels. Rows count for Videotex-only file.
If either width or height exceeds 65535 pixels, above two fields must be filled with zero and the dimension must be sourced from XDIM entry of the Extended Header
uint8 FPS: frames per second. Use 0x00 for still pictures
If FPS is greater than 254 or fractional (excl. NTSC), the value must be 0xFF and the true framerate must be sourced from the XFPS entry of the Extended Header
uint32 Total Frames: number of video frames
- use 0 to denote not-finalised video stream
- use 0xFFFFFFFF to denote still picture (.im3 file)
@@ -942,7 +891,6 @@ transmission capability, and region-of-interest coding.
- bit 0 = interlaced
- bit 1 = is NTSC framerate
- bit 2 = is lossless mode
(shorthand for `-q 6 -Q0,0,0 -w 0 --intra-only --no-perceptual-tuning --arate 384`)
- bit 3 = has region-of-interest coding (for still pictures only)
- bit 4 = no Zstd compression
- bit 7 = has no video
@@ -1100,6 +1048,9 @@ The encoder supports following presets:
- Bytes VNDR: Name and version of the encoder (for Reference encoder: "Encoder-TAV 20251014 (list,of,features)")
- Bytes FMPG: FFmpeg version (typically "ffmpeg version 8.0 Copyright (c) 2000-2025 the FFmpeg developers"; the first line of text FFmpeg emits)
- Bytes XDIM: Video dimension in '<width>,<height>' format. Mandatory if either width or height exceeds 65535
- Bytes XFPS: Framerate in '<numerator>/<denominator>' format. Mandatory if either:
1. FPS exceeds 254
2. denominator is not 1 or 1001
## Extensible Packet Structure
uint8 Packet Type
@@ -1166,8 +1117,6 @@ The encoder supports following presets:
* Background colours
* Characters
## GOP Unified Packet Structure (0x12)
Implemented on 2025-10-15 for temporal 3D DWT with unified preprocessing.
@@ -1207,12 +1156,6 @@ The entire GOP (width×height×N_frames×3_channels) is preprocessed as a single
This layout enables Zstd to find patterns across both spatial and temporal dimensions,
resulting in superior compression compared to per-frame encoding.
### Motion Vectors
- Stored in 1/4-pixel units (divide by 4.0 for pixel displacement)
- Computed using dense optical flow
- Cumulative relative to frame 0 (not frame-to-frame deltas)
- First frame (frame 0) always has motion vector (0, 0)
### Temporal 3D DWT Process
1. Detect where the scene change is happening on the first pass
2. Determine GOP slicing from the scene detection
@@ -1333,12 +1276,6 @@ The encoder expects linear alpha.
- Better frequency localisation than DCT
- Reduced blocking artifacts due to overlapping basis functions
## Hardware Acceleration Functions
TAV decoder requires new GraphicsJSR223Delegate functions:
- tavDecode(): Main DWT decoding function
- tavDWT2D(): 2D DWT/IDWT transforms
- tavQuantise(): Multi-band quantisation
## Audio Support
MP2 frames, raw PCMu8, and TAD formats are supported.
@@ -1992,34 +1929,6 @@ TAD encoder uses two-pass FFmpeg extraction for optimal quality:
This ensures resampling happens after extraction with optimal quality parameters.
## Hardware Acceleration API
TAD decoder is accelerated through AudioAdapter.kt peripheral (backend) and
AudioJSR223Delegate.kt (JavaScript API):
Backend (AudioAdapter.kt):
- decodeTad(): Main decoding function (chunk-based, reads from tadInputBin)
- dwt97Inverse1d(): Single-level inverse CDF 9/7 DWT
- dwt97InverseMultilevel(): 9-level inverse DWT with non-power-of-2 support
JavaScript API (audio.* functions):
- audio.tadDecode(): Trigger TAD decoding from peripheral input buffer
- audio.tadUploadDecoded(offset, count): Upload decoded PCMu8 to playback buffer
- audio.getMemAddr(): Get peripheral memory base address for buffer access
## Usage Examples
# Encode with default quality (Q3)
tad_encoder -i input.mp4 -o output.tad
# Encode with highest quality
tad_encoder -i input.mp4 -o output.tad -q 5
# Encode without Zstd compression
tad_encoder -i input.mp4 -o output.tad --no-zstd
# Verbose output with statistics
tad_encoder -i input.mp4 -o output.tad -v
--------------------------------------------------------------------------------
TSVM Universal Cue format

View File

@@ -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
*/
@@ -1339,6 +1349,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<Pair<Int, Int>> {
val result = mutableListOf<Pair<Int, Int>>()
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()

View File

@@ -375,6 +375,9 @@ Array.prototype.shuffle = function() {
return this;
}
Array.prototype.shuffled = function() {
return this.slice(0).shuffle()
}
Array.prototype.sum = function(selector) {
return this.reduce((acc,val) => acc + ((selector === undefined) ? val : selector(val)), 0)
}

View File

@@ -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()}")

View File

@@ -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)
@@ -140,6 +141,26 @@ class VMJSR223Delegate(private val vm: VM) {
for (i in 0 until len) vm.poke(to + i*toVector, vm.peek(from + i*fromVector)!!)
}
}
fun pokeBytes(dest: Int, src: org.graalvm.polyglot.Value, len: Int) {
val destL = dest.toLong()
val lenL = len.toLong()
val devPtr = getDev(destL, lenL, true)
if (devPtr != null) {
var i = 0
while (i < len) {
UnsafeHelper.unsafe.putByte(devPtr + i, src.getArrayElement(i.toLong()).asInt().toByte())
i++
}
} else {
val vector = if (destL >= 0) 1L else -1L
var i = 0
while (i < len) {
vm.poke(destL + i * vector, src.getArrayElement(i.toLong()).asInt().toByte())
i++
}
}
}
fun mapRom(slot: Int) {
vm.romMapping = slot.and(255)
}
@@ -208,23 +229,18 @@ class VMJSR223Delegate(private val vm: VM) {
* ^A-^Z: 1 through 26
*/
fun readKey(): Int {
/*val inputStream = vm.getInputStream()
var key: Int = inputStream.read()
inputStream.close()
return key*/
// impl that doesn't rely on InputStream
// MMIO-based implementation that can be replicated by JS via MMIO read/writes
vm.getIO().let {
it.mmio_write(38, 1)
it.mmio_write(38, 1) // Set keyboardInputRequested = true (also clears buffer)
vm.isIdle.set(true)
while (it.mmio_read(49L) == 0.toByte()) {
while (it.mmio_read(49L) == 0.toByte()) { // Wait for keyPushed flag
Thread.sleep(6L)
}
vm.isIdle.set(false)
it.mmio_write(38, 0)
return it.mmio_read(37L)!!.toUint()
it.mmio_write(38, 0) // Clear keyboardInputRequested
return it.mmio_read(37L)!!.toUint() // Read and remove key from buffer
}
}

View File

@@ -4,7 +4,7 @@ import net.torvald.tsvm.VM
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
abstract class BlockTransferInterface(val isMaster: Boolean, val isSlave: Boolean, val baudRate: Int = 20_000_000) {
abstract class BlockTransferInterface(val isMaster: Boolean, val isSlave: Boolean, val baudRate: Int = 0) {
var recipient: BlockTransferInterface? = null; protected set

View File

@@ -99,7 +99,11 @@ class IOSpace(val vm: VM) : PeriBase("io"), InputProcessor {
in 32..33 -> (mouseX.toInt() shr (adi - 32).times(8)).toByte()
in 34..35 -> (mouseY.toInt() shr (adi - 34).times(8)).toByte()
36L -> mouseDown.toInt().toByte()
37L -> keyboardBuffer.removeTail() ?: -1
37L -> {
val key = keyboardBuffer.removeTail() ?: -1
keyPushed = !keyboardBuffer.isEmpty // Clear flag when buffer becomes empty
key
}
38L -> keyboardInputRequested.toInt().toByte()
39L -> rawInputFunctionLatched.toInt().toByte()
in 40..47 -> keyEventBuffers[adi - 40]
@@ -174,6 +178,7 @@ class IOSpace(val vm: VM) : PeriBase("io"), InputProcessor {
keyboardInputRequested = (byte.isNonZero())
if (keyboardInputRequested) {
keyboardBuffer.clear()
keyPushed = false // Reset flag when buffer is cleared
vm.isIdle.set(true)
}
else
@@ -344,14 +349,11 @@ class IOSpace(val vm: VM) : PeriBase("io"), InputProcessor {
override fun keyTyped(p0: Char): Boolean {
if (keyboardInputRequested && p0.toInt() > 0) {
//println("[IO] key typed = ${p0.toInt()}")
keyPushed = true
keyboardBuffer.appendHead(p0.toByte())
Thread.sleep(6L)
keyPushed = false
keyPushed = true // Set after append; cleared when buffer is emptied via mmio_read(37)
return true
}
else {
keyPushed = false
return false
}
}
@@ -389,11 +391,13 @@ class IOSpace(val vm: VM) : PeriBase("io"), InputProcessor {
if (keyboardInputRequested) {
if (p0 in Input.Keys.A..Input.Keys.Z && (Gdx.input.isKeyPressed(Input.Keys.CONTROL_LEFT) || Gdx.input.isKeyPressed(Input.Keys.CONTROL_RIGHT))) {
keyboardBuffer.appendHead((p0 - 28).toByte())
keyPushed = true
}
else {
specialKeys[p0]?.let {
//println("[IO] key special = ${it.toUInt()}")
keyboardBuffer.appendHead(it)
keyPushed = true
}
}
return true

Some files were not shown because too many files have changed in this diff Show More