29 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
tsvm is a virtual machine that mimics 8-bit era computer architecture and runs programs written in JavaScript. The project includes:
- The virtual machine core
- Reference BIOS implementation
- TVDOS (operating system)
- Videotron2K video display controller emulator
- 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
Reference Materials
Third-party source-code references that inform TSVM implementations live in
reference_materials/<topic>/. Each topic folder has a README.md that
summarises the takeaway and points back into the verbatim source files.
Consult these before reimplementing tracker / codec / DSP behaviour from
memory — TSVM aims to match the audible behaviour of the originals.
Current topics:
reference_materials/tracker_filter/— Impulse Tracker / OpenMPT / Schism Tracker resonant low-pass filter source. Defines the cutoff formula, the resonance damping curve, and the IIR-only 2-pole topology (NOT a biquad — no feedforward x[n−1] / x[n−2] terms) thatAudioAdapter.ktuses for Taud playback.reference_materials/ft2-clone— Modernised clone for the original FastTracker 2reference_materials/impulse-tracker— The original source code for ImpulseTrackerreference_materials/MilkyTracker— FastTracker 2 compatible trackerreference_materials/schismtracker— Open-source re-implementation of ImpulseTrackerreference_materials/pt2-clone— Open-source re-implementation of ProTracker 2
When fetching new references, copy the relevant upstream files verbatim into
a topic folder, write a README.md summarising the relevant maths /
algorithms with file:line citations, and add an entry here.
Architecture
Core Components
-
tsvm_core/: Core virtual machine implementation in Kotlin
VM.kt: Main virtual machine class with memory management and peripheral slotsperipheral/: Hardware peripherals (graphics adapters, disk drives, TTY, audio, etc.)vdc/: Videotron2K video display controller- Various delegates for JavaScript integration via GraalVM
-
tsvm_executable/: Main emulator application
VMGUI.kt: LibGDX-based GUI implementationTsvmEmulator.java: Main application entry point- Menu systems for configuration, audio, memory management
-
TerranBASICexecutable/: TerranBASIC interpreter application
TerranBASIC.java: Entry point for BASIC interpreterVMGUI.kt: GUI for BASIC environment
Key Technologies
- Kotlin/Java: Primary implementation language
- LibGDX: Graphics and windowing framework
- GraalVM: JavaScript execution engine for running programs in the VM
- LWJGL: Native library bindings
- IntelliJ IDEA: Development environment (*.iml module files)
Virtual Hardware
The VM emulates various peripherals through the peripheral/ package:
- Graphics adapters with different capabilities
- Disk drives (including TevdDiskDrive for custom disk format)
- TTY terminals and character LCD displays
- Audio devices and MP2 audio environment
- Network modems and serial interfaces
- Memory management units
Build and Development
Building Applications
Use the build scripts in buildapp/:
build_app_linux_x86.sh- Linux x86_64 AppImagebuild_app_linux_arm.sh- Linux ARM64 AppImagebuild_app_mac_x86.sh- macOS Intelbuild_app_mac_arm.sh- macOS Apple Siliconbuild_app_windows_x86.sh- Windows x86
Prerequisites
-
Download JDK 21 runtimes to
~/Documents/openjdk/*with specific naming:jdk-21.0.1-x86(Linux AMD64)jdk-21.0.1-arm(Linux Aarch64)jdk-21.0.1-windows(Windows AMD64)jdk-21.0.1.jdk-arm(macOS Apple Silicon)jdk-21.0.1.jdk-x86(macOS Intel)
-
Run
jlinkcommands to create custom Java runtimes inout/runtime-*directories
Development Commands
- Build JAR: Use IntelliJ IDEA build system to compile modules
- Run Emulator: Execute
TsvmEmulator.javamain method or use built JAR - Run TerranBASIC: Execute
TerranBASIC.javamain method - Package Apps: Run appropriate build script from
buildapp/directory
Assets and File System
assets/disk0/: Virtual disk content including TVDOS system filesassets/bios/: BIOS ROM files and implementationsMy_BASIC_Programs/: Example BASIC programs for testing- TVDOS filesystem uses custom format with specialised drivers
TSVM JavaScript Source Encoding
Do not normalise \uXXXX or \xXX escapes in .js / .mjs files that run inside
TSVM. TSVM's character set is not Unicode, and the JS string literal parser
behaves differently for raw bytes vs. escape sequences. Both forms appear in
existing code intentionally — leave each one as-is. When writing new content,
prefer raw UTF-8 characters in string literals (e.g. write the character ù
directly, rather than a \uXXXX-style escape) unless you are matching a
pattern already established in the surrounding code.
Videotron2K
The Videotron2K is a specialised video display controller with:
- Assembly-like programming language
- 6 general registers (r1-r6) and special registers (tmr, frm, px, py, c1-c6)
- Scene-based programming model
- Drawing commands (plot, fillin, goto, fillscr)
- Conditional execution with postfixes (zr, nz, gt, ls, ge, le)
Programs are structured with SCENE blocks and executed with perform commands.
Memory Management
- VM supports up to USER_SPACE_SIZE memory
- 64-byte malloc units with reserved blocks
- Peripheral slots (1-8 configurable)
- Memory-mapped I/O for peripheral access
- JavaScript programs run in sandboxed GraalVM context
Peripheral Memory Addressing
Peripheral memories can be accessed using vm.peek() and vm.poke() functions, which takes absolute address.
- Peripherals take up negative number of the memory space, and their addressing is in backwards (e.g. Slot 1 starts at -1048577 and ends at -2097152)
- Peripherals take up two memory regions: MMIO area and Memory Space area; MMIO is accessed by PeriBase (and its children) using
mmio_read()andmmio_write(), and the Memory Space is accessed usingpeek()andpoke().- Peripheral at slot n takes following addresses
- MMIO area (-131072×n)-1 to -131072×(n+1)
- Memory Space area -(1048576×n)-1 to (-1048576×(n+1))
- Peripheral at slot n takes following addresses
Testing
- Use example programs in
My_BASIC_Programs/for BASIC testing - JavaScript test programs available in
assets/disk0/ - Videotron2K assembly examples in documentation
Notes
- The 'gzip' namespace in TSVM's JS programs is a misnomer: the actual 'gzip' functions (defined in CompressorDelegate.kt) call Zstd functions.
Taud Tracker Engine
The Taud playback engine lives in tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt.
Critical Implementation Notes
Re-bind the local inst after any mid-tick triggerNote. applyTrackerTick binds var inst = instruments[voice.instrumentId] once at the top of the per-voice loop. When the note-delay (S$Dx) deferred trigger fires mid-tick, triggerNote swaps the voice's instrumentId — but the rest of that tick (playback-rate recompute at the computePlaybackRate(inst, finalPitch) line, advanceEnvelope, advancePfEnvelope, advanceAutoVibrato, and the fadeout / filter-env reads of inst.*) keeps using the captured binding. The damage on a never-triggered voice (instrumentId == 0 → stale inst = instruments[0], whose samplingRate == 0) is that playbackRate is overwritten with 0.0, freezing the sample at its start for the trigger tick — perceived as "the first delayed note on a fresh channel doesn't fire" (canonical: WHEN.taud cue 0 voice 13 pattern 0x0A row 16, inst 0x11 SD2 on a fresh play). On a warm voice the stale inst is a real instrument with non-zero rate, so the note sounds (at the wrong rate for one tick — a sub-perceptual glitch). Re-bind inst = instruments[voice.instrumentId] immediately after the note-delay fire block. Any future in-tick trigger paths (currently only S$Dx) must do the same.
TVDOS
TVDOS Movie Formats
Legacy iPF Format
- Format documentation on
terranmon.txt(search for "TSVM MOV file format" and "TSVM Interchangeable Picture Format (aka iPF Type 1/2)") - Video Encoder implementation on
assets/disk0/tvdos/bin/encodemov.js(iPF Format 1 and 2) andassets/disk0/tvdos/bin/encodemov2.js(iPF Format 1-delta)- Actual encoding/decoding code is in
GraphicsJSR223Delegate.kt
- Actual encoding/decoding code is in
- Audio uses standard MP2
TEV Format (TSVM Enhanced Video)
- Modern video codec optimized for TSVM hardware with 60-80% better compression than iPF
- C Encoder:
video_encoder/encoder_tev.c- Hardware-accelerated encoder with motion compensation and DCT- How to build:
make clean && make - Rate Control: Supports both quality mode (
-q 0-4) and bitrate mode (-b Nkbps)
- How to build:
- JS Decoder:
assets/disk0/tvdos/bin/playtev.js- Native decoder for TEV format playback- How to build:
must be done manually by the user; the TSVM is not machine-interactable
- How to build:
- Hardware accelerated decoding: Extended GraphicsJSR223Delegate.kt with TEV functions:
tevDecode()- The main decoding function (now accepts rate control factor)tevIdct8x8()- Fast 8×8 DCT transformstevMotionCopy8x8()- Sub-pixel motion compensation
- Features:
- 16×16 DCT blocks (vs 4×4 in iPF) for better compression
- Motion compensation with ±8 pixel search range
- YCoCg-R 4:2:0 Chroma subsampling (more aggressive quantisation on Cg channel)
- Full 8-Bit RGB colour for increased visual fidelity, rendered down to TSVM-compliant 4-Bit RGB with dithering upon playback
- Usage Examples:
# Quality mode ./encoder_tev -i input.mp4 -o output.tev -q 3 # Playback playtev output.tev - Format documentation:
terranmon.txt(search for "TSVM Enhanced Video (TEV) Format") - Version: 2.1 (includes rate control factor in all video packets)
TAV Format (TSVM Advanced Video)
- Successor to TEV: DWT-based video codec using wavelet transforms instead of DCT
- C Encoder:
video_encoder/encoder_tav.c- Multi-wavelet encoder with perceptual quantisation- How to build:
make tav - Wavelet Support: Multiple wavelet types for different compression characteristics
- How to build:
- JS Decoder:
assets/disk0/tvdos/bin/playtav.js- Native decoder for TAV format playback - Hardware accelerated decoding: Extended GraphicsJSR223Delegate.kt with TAV functions
- Packet analyser:
video_encoder/tav_inspector.c- Debugging tool that parses TAV packets into human-readable form - Features:
- Multiple Wavelet Types: 5/3 reversible, 9/7 irreversible, CDF 13/7, DD-4, Haar
- Single-tile encoding: One large DWT tile for optimal quality (no blocking artifacts)
- Perceptual quantisation: HVS-optimized coefficient scaling
- YCoCg-R colour space: Efficient chroma representation with "simulated" subsampling using anisotropic quantisation (search for "ANISOTROPY_MULT_CHROMA" on the encoder)
- 6-level DWT decomposition: Deep frequency analysis for better compression (deeper levels possible but 6 is the maximum for the default TSVM size)
- Significance Map Compression: Improved coefficient storage format exploiting sparsity for 16-18% additional compression (2025-09-29 update)
- Concatenated Maps Layout: Cross-channel compression optimisation for additional 1.6% improvement (2025-09-29 enhanced)
- Usage Examples:
# Different wavelets ./encoder_tav -i input.mp4 -w 0 -o output.tav # 5/3 reversible (lossless capable) ./encoder_tav -i input.mp4 -w 1 -o output.tav # 9/7 irreversible (default, best compression) ./encoder_tav -i input.mp4 -w 2 -o output.tav # CDF 13/7 (experimental) ./encoder_tav -i input.mp4 -w 16 -o output.tav # DD-4 (four-point interpolating) ./encoder_tav -i input.mp4 -w 255 -o output.tav # Haar (demonstration) # Quality levels (0-5) ./encoder_tav -i input.mp4 -q 0 -o output.tav # Lowest quality, smallest file ./encoder_tav -i input.mp4 -q 5 -o output.tav # Highest quality, largest file # Temporal 3D DWT (GOP-based encoding) ./encoder_tav -i input.mp4 --temporal-dwt -o output.tav # Playback playtav output.tav
CRITICAL IMPLEMENTATION NOTES:
Wavelet Coefficient Layout:
- TAV uses 2D Spatial Layout in memory:
[LL, LH, HL, HH, LH, HL, HH, ...]for each decomposition level - Forward transform must output:
temp[0...half-1] = low-pass,temp[half...length-1] = high-pass - Inverse transform must expect: Same 2D spatial layout and exactly reverse forward operations
- Common mistake: Assuming linear layout leads to grid/checkerboard artifacts
Wavelet Implementation Pattern:
- All wavelets must follow the exact same structure as the working 5/3 implementation:
// Forward: 1. Predict step, 2. Update step temp[half + i] = data[odd_index] - prediction; // High-pass temp[i] = data[even_index] + update; // Low-pass // Inverse: Reverse order - 1. Undo update, 2. Undo predict temp[i] -= update; // Undo low-pass update temp[half + i] += prediction; // Undo high-pass predict - Boundary handling: Use symmetric extension for filter taps beyond array bounds
- Reconstruction: Interleave even/odd samples:
data[2*i] = low[i], data[2*i+1] = high[i]
Debugging Grid Artifacts:
- Symptom: Checkerboard or grid patterns in decoded video
- Cause: Mismatch between encoder/decoder coefficient layout or lifting step operations
- Solution: Ensure forward and inverse transforms use identical coefficient indexing and reverse operations exactly
Supported Wavelets:
-
0: 5/3 reversible (lossless when unquantised, JPEG 2000 standard)
-
1: 9/7 irreversible (best compression, CDF 9/7 variant, default choice)
-
2: CDF 13/7 (experimental, simplified implementation)
-
16: DD-4 (four-point interpolating Deslauriers-Dubuc, for still images)
-
255: Haar (demonstration only, simplest possible wavelet)
-
Format documentation:
terranmon.txt(search for "TSVM Advanced Video (TAV) Format") -
Version: Current (perceptual quantisation, multi-wavelet support, EZBC compression)
TAV Temporal 3D DWT (GOP Unified Encoding)
Implemented on 2025-10-15 for improved temporal compression through group-of-pictures (GOP) encoding:
Key Features:
- 3D DWT: Applies DWT in both spatial (2D) and temporal (1D) dimensions for optimal spacetime compression
- Unified GOP Preprocessing: Single EZBC tree for all frames and channels in a GOP (width×height×N_frames×3_channels)
- GOP Size: Typically 8 frames (configurable), with scene change detection for adaptive GOPs
- Single-frame Fallback: GOP size of 1 automatically uses traditional I-frame encoding
Packet Format:
- 0x12 (GOP_UNIFIED):
[gop_size][compressed_size][compressed_data] - 0xFC (GOP_SYNC):
[frame_count]- Indicates N frames were decoded from GOP block - Timecode Emission: One timecode packet per GOP (not per frame)
Technical Implementation:
// Unified preprocessing structure (encoder_tav.c:2371-2509)
[All_Y_maps][All_Co_maps][All_Cg_maps][All_Y_values][All_Co_values][All_Cg_values]
// Where maps are grouped by channel across all GOP frames for optimal Zstd compression
Usage:
# Enable temporal 3D DWT
./encoder_tav -i input.mp4 --temporal-dwt -o output.tav
# Inspect GOP structure
./tav_inspector output.tav -v
Compression Benefits:
- Temporal Coherence: Exploits similarity across consecutive frames
- Unified Compression: Zstd compresses entire GOP as single block, finding patterns across time
- Adaptive GOPs: Scene change detection ensures optimal GOP boundaries
TAD Format (TSVM Advanced Audio)
- Perceptual audio codec for TSVM using CDF 9/7 biorthogonal wavelets
- C Encoder:
video_encoder/encoder_tad.c- Core Encoder library;video_encoder/encoder_tad_standalone.c- Standalone encoder with FFmpeg integration- How to build:
make tad - Quality Levels: 0-5 (0=lowest quality/smallest, 5=highest quality/largest; designed to be in sync with TAV encoder)
- How to build:
- C Decoders:
video_encoder/decoder_tad.c- Shared decoder library withtad32_decode_chunk()functionvideo_encoder/decoder_tad.h- Exports shared decoder APIvideo_encoder/decoder_tav.c- TAV decoder that uses shared TAD decoder for audio packets- Shared Architecture (Fixed 2025-11-10): Both standalone TAD and TAV decoders now use the same
tad32_decode_chunk()implementation, eliminating code duplication and ensuring identical output
- Kotlin Decoder:
AudioAdapter.kt- Hardware-accelerated TAD decoder for TSVM runtime- Quantisation Fix (2025-11-10): Fixed BASE_QUANTISER_WEIGHTS to use channel-specific 2D array (Mid/Side) instead of single 1D array, resolving severe audio distortion
- Features:
- 32 KHz stereo: TSVM audio hardware native format
- Variable chunk sizes: Any size ≥1024 samples, including non-power-of-2 (e.g., 32016 for TAV 1-second GOPs)
- Pre-emphasis filter: First-order IIR filter (α=0.5) shifts quantisation noise to lower frequencies
- Gamma compression: Dynamic range compression (γ=0.5) before quantisation
- M/S stereo decorrelation: Exploits stereo correlation for better compression
- 9-level CDF 9/7 DWT: Fixed 9 decomposition levels for all chunk sizes
- Perceptual quantisation: Channel-specific (Mid/Side) frequency-dependent weights with lambda companding (λ=6.0)
- EZBC encoding: Binary tree embedded zero block coding exploits coefficient sparsity (86.9% Mid, 97.8% Side)
- Zstd compression: Level 7 on concatenated EZBC bitstreams for additional compression
- Non-power-of-2 support: Fixed 2025-10-30 to handle arbitrary chunk sizes correctly
- Usage Examples:
# Encode with default quality (Q3) encoder_tad -i input.mp4 -o output.tad # Encode with highest quality encoder_tad -i input.mp4 -o output.tad -q 5 # Encode without Zstd compression encoder_tad -i input.mp4 -o output.tad --no-zstd # Verbose output with statistics encoder_tad -i input.mp4 -o output.tad -v # Decode back to PCM16 decoder_tad -i input.tad -o output.pcm - Format documentation:
terranmon.txt(search for "TSVM Advanced Audio (TAD) Format") - Version: 1.1 (EZBC encoding with non-power-of-2 support, updated 2025-10-30; decoder architecture and Kotlin quantisation weights fixed 2025-11-10; documentation updated 2025-11-10 to reflect pre-emphasis and EZBC)
TAD Encoding Pipeline:
- Pre-emphasis filter (α=0.5) - Shifts quantisation noise toward lower frequencies
- Gamma compression (γ=0.5) - Dynamic range compression
- M/S decorrelation - Transforms L/R to Mid/Side
- 9-level CDF 9/7 DWT - Wavelet decomposition (fixed 9 levels)
- Perceptual quantisation - Lambda companding (λ=6.0) with channel-specific weights
- EZBC encoding - Binary tree embedded zero block coding per channel
- Zstd compression (level 7) - Additional compression on concatenated EZBC bitstreams
TAD Compression Performance:
- Target Compression: 2:1 against PCMu8 baseline (4:1 against PCM16LE input)
- Achieved Compression: 2.51:1 against PCMu8 at quality level 3
- Audio Quality: Preserves full 0-16 KHz bandwidth
- Coefficient Sparsity: 86.9% zeros in Mid channel, 97.8% in Side channel (typical)
- EZBC Benefits: Exploits sparsity, progressive refinement, spatial clustering
TAD Integration with TAV: TAD is designed as an includable API for TAV video encoder integration. The variable chunk size support enables synchronized audio/video encoding where audio chunks can match video GOP boundaries. TAV embeds TAD-compressed audio using packet type 0x24 with Zstd compression.
TAD Hardware Acceleration: TSVM accelerates TAD decoding with AudioAdapter.kt (backend) and AudioJSR223Delegate.kt (API):
- Backend decoder in AudioAdapter.kt with non-power-of-2 chunk size support (fixed 2025-10-30)
- API functions in AudioJSR223Delegate.kt for JavaScript access
- Supports chunk sizes from 1024 to 32768+ samples (any size ≥1024)
- Fixed 9-level CDF 9/7 inverse DWT with correct length tracking for non-power-of-2 sizes
Critical Implementation Note (Fixed 2025-10-30): Multi-level inverse DWT must pre-calculate the exact sequence of lengths from forward transform:
val lengths = IntArray(levels + 1)
lengths[0] = chunk_size
for (i in 1..levels) {
lengths[i] = (lengths[i - 1] + 1) / 2
}
// Apply inverse DWT using lengths[level] for each level
Using simple doubling (length *= 2) is incorrect for non-power-of-2 sizes and causes
mirrored subband artifacts.
TAD Decoding Pipeline:
- Zstd decompression - Decompress concatenated EZBC bitstreams
- EZBC decoding - Binary tree decoder reconstructs quantised int8 coefficients per channel
- Lambda decompanding - Inverse Laplacian CDF mapping with channel-specific weights
- 9-level inverse CDF 9/7 DWT - Wavelet reconstruction with proper non-power-of-2 length tracking
- M/S to L/R conversion - Transform Mid/Side back to Left/Right
- Gamma expansion (γ⁻¹=2.0) - Restore dynamic range
- De-emphasis filter (α=0.5) - Reverse pre-emphasis, remove frequency shaping
- PCM32f to PCM8 - Noise-shaped dithering for final 8-bit output
Critical Quantisation Weights Note (Fixed 2025-11-10): The TAD decoder MUST use channel-specific quantisation weights for Mid (channel 0) and Side (channel 1) channels. The Kotlin decoder (AudioAdapter.kt) originally used a single 1D weight array, which caused severe audio distortion. The correct implementation uses a 2D array:
// CORRECT (Fixed 2025-11-10)
private val BASE_QUANTISER_WEIGHTS = arrayOf(
floatArrayOf( // Mid channel (0)
4.0f, 2.0f, 1.8f, 1.6f, 1.4f, 1.2f, 1.0f, 1.0f, 1.3f, 2.0f
),
floatArrayOf( // Side channel (1)
6.0f, 5.0f, 2.6f, 2.4f, 1.8f, 1.3f, 1.0f, 1.0f, 1.6f, 3.2f
)
)
// During dequantisation:
val weight = BASE_QUANTISER_WEIGHTS[channel][sideband] * quantiserScale
coeffs[i] = normalisedVal * TAD32_COEFF_SCALARS[sideband] * weight
The different weights for Mid and Side channels reflect the perceptual importance of different frequency bands in each channel. Using incorrect weights causes:
- DC frequency underamplification (using 1.0 instead of 4.0/6.0)
- Incorrect stereo imaging and extreme side channel distortion
- Severe frequency response errors that manifest as "clipping-like" distortion
Virtual Consoles (vtmgr)
Linux-style virtual consoles for TVDOS: up to 6 independent shell sessions,
switched with Alt-1..Alt-6 or the chvt N builtin, Alt-0 to exit.
Implemented entirely in JS — no tsvm_core changes.
Architecture
- Dispatcher:
assets/disk0/tvdos/sbin/vtmgr.js. Launched directly by theTVDOS.SYSboot block (only when!_TVDOS_IS_VT_PANE); when it exits (Alt-0) the boot block runsAUTOEXEC.BATas the bare fallback shell. Owns the physical keyboard and screen. Each VT runs in its own GraalVM context/thread via the existingparallel.spawnNewContext/attachProgram/launchAPI (seeVMJSR223Delegate.ktclass Parallel). VT 1 spawns at boot; VT 2-6 are lazy-spawned on first switch and re-spawned if their shell exits. - Concurrency model: truly concurrent — switching works mid-command, not
just at the prompt. Background panes keep running (no
Thread.suspend; it is unusable on JDK 21). A cooperative gate inside the shimmedcon.getchparks panes blocked on input; CPU-bound background panes are allowed to run. - Shared memory: one
sys.mallocregion holds a control block (active VT, switch request, debounce, spawned-bits) plus, per VT, an input ring buffer and a 7682-byte text-plane buffer mirroring the GPU text-area layout (cursor 2 + fore 2560 + back 2560 + char 2560). - Compositor (30 Hz): blits the active VT's text plane to the physical GPU
text area via
sys.memcpy, and pushes that VT's cursor-visibility into the GPU blink bit (MMIO attribute byte 6, addressed at-1 - (131072*gpuSlot + 6)). - Boot config split (
commandrc+AUTOEXEC.BAT): environment setup and app-launch are split into two files so panes can replay one without the other.\commandrcholds thesetcommands (PATH/INCLPATH/HELPPATH/KEYBOARD) and is run by theTVDOS.SYSboot block in every context (boot and pane) — it has no.BATextension, so the boot block runs it line-by-line (setmutates the shared_TVDOS.variables, so the effect persists).\AUTOEXEC.BATis the per-console launch script (Korean IMEtvdos/i18n/korean, thencommand -fancy); it is run once per console — by each pane's bootstrap, and by the boot block as the post-vtmgr fallback. No env snapshot/replay anymore; each pane gets PATH/KEYBOARD/etc. natively fromcommandrc, and Korean IME (a per-contextunicode.uniprinthandler) now registers in every pane. - Per-pane bootstrap: each pane re-evals
TVDOS.SYS(with_TVDOS_IS_VT_PANEset — which makes the boot block runcommandrcbut skip the vtmgr/AUTOEXEC launch — and a_BIOSstub captured live from the main context) then runscommand -c \AUTOEXEC.BAT, all in ONE directevalso the launcher shares scope with_TVDOS/files/execApp.
Output/input shimming (in the pane bootstrap)
con and the global print/println family are plain JS, so the bootstrap
overrides them to read/write the per-VT shared-memory buffers instead of the
physical GPU. sys and graphics are host objects and CANNOT be overridden
from JS — this is the key constraint that shapes everything below.
- The shimmed
printis a faithful JS port of the GPU's TTY interpreter (GlassTty.acceptChar+GraphicsAdapterhandlers): control bytes, the\x84<decimal>u"emit char by code" escape (used bycon.prnch), CSI cursor moves / erase / SGR colours, and the?25cursor-visibility private sequence. A swallow-only parser is NOT enough — TVDOS apps drive the screen through theseprintescapes. con.move/con.getyxare 1-based (mirroringgraphics.setCursorYX'scx-1andgetCursorYX'scx+1);con.addchdoes NOT advance the cursor (matchesgraphics.putSymbol), whilecon.prnchDOES.command.js'sshell.executereassigns the global print family toshell.stdio.out.*, which callsys.print(→ physical GPU).shell.stdio.outwas made to delegate to aglobalThis.__VT_OUThook when present (set by the bootstrap); outside a VT the hook is absent and the path is byte-identical.
Direct-VRAM apps need a VT-aware base (the vaddr pattern)
Apps that write the text area directly via graphics.getGpuMemBase() (rather
than con.*/print) bypass the shims and paint the physical screen, invading
whatever VT is visible. They must resolve text-area byte m through a
VT-aware base:
// physical: backward (byte m at gpuBase - m) — getDev inverts to forward-native
// VT pane: forward (byte m at VT_TEXT_PLANE + m, the pane buffer the compositor blits)
const VT = (typeof globalThis.VT_TEXT_PLANE !== 'undefined')
const VRAM_BASE = VT ? globalThis.VT_TEXT_PLANE : (graphics.getGpuMemBase() - 253950)
const VRAM_SGN = VT ? 1 : -1
function vaddr(m) { return VRAM_BASE + VRAM_SGN * m }
sys.memcpy/sys.pokeBytes copy forward in the resolved native memory, so this
works for both directions. The physical branch is identical to the original
arithmetic (no regression outside vtmgr). Applied so far in
assets/disk0/tvdos/bin/taut.js and assets/disk0/hopper/include/aa.mjs
(used by bb.js). Any future direct-VRAM app needs the same one-line vaddr.
Files
- New:
assets/disk0/tvdos/sbin/vtmgr.js(dispatcher + per-pane bootstrap) assets/disk0/tvdos/bin/command.js:chvtbuiltin,[N]prompt prefix for VT 2-6,shell.stdio.out→__VT_OUTdelegationassets/disk0/tvdos/TVDOS.SYS: boot block runs\commandrc(env) in every context, then — only when!_TVDOS_IS_VT_PANE— launchestvdos/sbin/vtmgrand, on its exit,\AUTOEXEC.BATas the fallback shellassets/disk0/commandrc: env-onlysetcommands (PATH/INCLPATH/HELPPATH/KEYBOARD)assets/disk0/AUTOEXEC.BAT: per-console launch (Korean IME +command -fancy)assets/disk0/tvdos/bin/taut.js,assets/disk0/hopper/include/aa.mjs:vaddrVT-aware direct-VRAM addressing
Gotcha: injectIntChk vs. embedded source
execApp/require run a program's source through injectIntChk (TVDOS.SYS),
which sed-rewrites the first while/for/do of each kind to call a
per-exec tvdosSIGTERM_<hash>() SIGTERM check. When vtmgr embeds the pane
bootstrap as a string literal, one of those rewrites can land inside the literal
— and the pane context has no such symbol. vtmgr strips them from the bootstrap
string with raw.replace(/tvdosSIGTERM_[A-Za-z0-9_]+\(\);?/g, ''). Any future
code that builds executable source as a string literal must do the same.