diff --git a/docs/superpowers/specs/2026-11-03-termtris-vt1200-design.md b/docs/superpowers/specs/2026-11-03-termtris-vt1200-design.md new file mode 100644 index 0000000..bc4ef38 --- /dev/null +++ b/docs/superpowers/specs/2026-11-03-termtris-vt1200-design.md @@ -0,0 +1,138 @@ +# termtris for the VT1200 — design + +Date: 2026-11-03. Status: ready for implementation (input path proven). + +## Intent + +Run `jtsiomb/termtris` as a **local VT1200 program**: loaded into main RAM by the +existing COFF loader, displayed through the terminal's **ROM X11 server**, and +controlled from the real X keyboard events. End-to-end proof is a playable game in +MAME, with the real-unit path documented. + +## What is already proven (2026-11-03) + +- **Compiler**: the 1990 r5.01 OS/2 toolchain under the patched 2ine fork builds + ANSI C. Pipeline: `gspac -> gspopt -O2 -> gspcg -> gspa -> gsplnk` -> absolute + COFF, linked at bit `0x018A0000` (`.bss` at `0x01A00000`). +- **Loader**: `dwt/loader` pokes the COFF, `create_process`es the entry, and the + program runs. `.cinit` static initialisers run via our own `var_init` in + `entry.asm`; the linker's `cinit` symbol is at the start of `.cinit`. +- **ROM Xlib**: `XOpenDisplay`, `XCreateSimpleWindow`, `XSelectInput`, + `XMapWindow` (`0x3FD64F60`; `0x3FD645B0` is `XMapRaised`), `XSync`, + `XCreateGC`, `XSetForeground`, `XFillRectangle` (`0x3F99E8F0`), + `XDrawString` (`0x3FDA0000`), `XNextEvent` (`0x3F9C1CF0`), + `XLookupString` (`0x3F9A9C30`) all work from C with the TI ABI. +- **Rendering**: the default GC's font is **6×13** (baseline +10); an 80×24 cell + grid is 480×312. White rectangle + text were rendered in a WM-framed window. +- **Input (the blocker, now solved)**: a `create_process` child has an + **uninitialised per-process Xlib context**. `create_process` leaves + `*(proc+0x100)` pointing at a zeroed word; `_XEnq` derives + `XG = *(*(proc+0x100))`, so `XG == NULL` and its event free list at + `XG+0xCC0` (bit) reads low-RAM garbage. KeyPress nodes were therefore invalid + and `XNextEvent` copied unmapped zeros. + **Fix**: before `XOpenDisplay`, set + `*(u32 *)(*(u32 *)(proc + 0x100)) = (u32)zeroed_block`, where `proc` is + `*(u32 *)0x1B3900` and `zeroed_block` is a static `.bss` buffer (1 KiB is + enough). With that, the local client receives real events: + KeyPress `type=2`, `keycode=194` (LK401 `C2`), `window=0x00400001`, and + `XLookupString` returns `n=1, char='a', keysym=0x61`. +- **Timing**: `sleep(ms)` at `0x3FB33A20` blocks and yields; the raw millisecond + counter is the 16-bit `0x1E93C0` (+17 per display interrupt). Use a 32-bit + accumulator with `(u16)(now - last)` to avoid the 84 s wrap. +- **TCP X** also works (`xdpyinfo`/`xev` against `100.103.100.103:0` with the + real unit's NVRAM security settings); it is useful for debugging, but the game + uses the local service (`"0::0"`), so it needs no network. + +## Goals + +- `termtris` game logic (pieces, scoring, levels, spawn, collision) unchanged in + behaviour. +- X11 cell-grid renderer: 80×24 characters, 6×13 cells, monochrome pens. +- Real X11 input: `KeyPress` -> `XLookupString`/keysym -> `game_input`; arrows + mapped to the game's CSI sequences, letters lower-cased. +- Runs from local RAM under `dwt/loader`, with a screenshot and a playable run. +- A build script and a short report documenting the toolchain, the leak/loader + addresses, the XG fix, and the limits. + +## Non-goals + +- No host-side X client for the game (the x86 `xev` was a diagnostic only). +- No terminal/ANSI backend on the VT1200 (the game is X11). +- No modification of the ROM or MAME. All fixes are runtime RAM writes. +- No file-backed high-score table (the VT1200 has no filesystem); scores live in + RAM for the session. + +## Approaches considered + +1. **Local Xlib + XG initialisation (chosen).** Proven; uses the terminal's real + X server and keyboard; no network; the game is a local `create_process` child. +2. Local Xlib + OS pointer/keycode polling. Reliable fallback, but the keyboard + global is lossy (no release/repeat) and the pointer is an odd controller. + Keep as a compile-time fallback only. +3. Host x86 X client over TCP. Works and was used for diagnosis, but does not + satisfy "run from VT1200 RAM". +4. Terminal/ANSI backend. Not X11 and not the request. + +## Architecture / files + +``` +apps/termtris/ vendored upstream (GPLv3, COPYING kept) + src/game.c, pieces.h upstream, small VT1200 patches + src/vt1200/ + main.c X11 event loop, timer, key mapping + x11.c term_* callbacks -> cell grid -> XFillRectangle/XDrawString + xlib.h ROM entry-point addresses + TI-ABI prototypes + xgfix.c per-process Xlib context installation + scoredb.c RAM-only high scores + compat.c sprintf subset, fflush/stdout stubs + entry.asm _start + cinit var_init + rets + build.sh 2ine/r5.01 pipeline with stage retries + README.md build/run/real-unit instructions +``` + +Rendering is a small virtual terminal: `term_setcursor`/`term_putstr`/ +`term_setcolor`/`term_ibmchar` update a cell buffer; `wait_display()` flushes +changed cells with `XFillRectangle` (background) and `XDrawString` (glyph). +The 6×13 metrics are measured, not assumed. A full redraw is 80×24 cells. + +Input: `XSelectInput` with `KeyPressMask|KeyReleaseMask|ButtonPressMask| +ExposureMask|StructureNotifyMask|FocusChangeMask`; the event loop drains +`XNextEvent`; `XLookupString` gives the character/keysym. Arrow keys become +`ESC [ A..D` for `game_input`; `FocusOut` pauses. + +Timing: `now_ms()` accumulates `0x1E93C0` deltas; the idle path is +`sleep(min(next_tick - now, 10))`, so the CPU yields to the X server and the OS. + +## Build + +`entry.asm` supplies the process entry and runs the `cinit` table (the 1990 +runtime's `var_init` is local, so the entry implements the 16-bit-length/32-bit- +destination copy itself). The linker command file places `.text`/`.data`/`.cinit` +at `0x018A0000` and `.bss` at `0x01A00000`. The build script retries the 2ine +stage crashes (they exit 139 after producing output) and checks for outputs, not +exit codes. + +## Acceptance + +In MAME, with the loader: +- the window appears with the termtris board and score panels; +- `A/D/S/W`, arrows, `0`/Enter, `P`, `R`, `H`, `Q`/Backspace behave as upstream; +- a screenshot of the playfield and a short play session are captured; +- the ROM md5 is unchanged. + +Real unit: the same COFF can be delivered with `dwt/loader/hostload.py` once the +OS is up; the document will state that the XG fix is required there too and that +the real unit's X server supplies the same keycodes (LK401). + +## Risks + +- The 2ine toolchain intermittently segfaults after writing outputs; the builder + retries and verifies every artifact. +- The per-process XG block size is inferred (we used 1 KiB); if the Xlib uses + more, enlarge the buffer. It worked through 9 events in the test. +- `sprintf` is absent from the 1990 runtime; `compat.c` implements only the + formats the game uses (`%d`, `%ld`, `%s`, `%c`, width, left-justify, `%%`). +- Font metrics are the ROM default GC's 6×13; do not use a different GC/font + without re-measuring. +- GPLv3: the vendored tree keeps `COPYING` and the copyright headers; the repo's + own license is unchanged.