From 2c80547cc3f87e98e60303a895253b8d17555263 Mon Sep 17 00:00:00 2001 From: "opencode.energia.arti.ee" Date: Tue, 3 Nov 2026 18:42:47 +0000 Subject: [PATCH] docs(termtris): implementation plan for the VT1200 X11 port --- .../plans/2026-11-03-termtris-vt1200.md | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 docs/superpowers/plans/2026-11-03-termtris-vt1200.md diff --git a/docs/superpowers/plans/2026-11-03-termtris-vt1200.md b/docs/superpowers/plans/2026-11-03-termtris-vt1200.md new file mode 100644 index 0000000..606b55a --- /dev/null +++ b/docs/superpowers/plans/2026-11-03-termtris-vt1200.md @@ -0,0 +1,153 @@ +# termtris VT1200 X11 port — implementation plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Build a playable `termtris` that runs as a VT1200 `create_process` child, renders through the ROM X11 server, and accepts the real X keyboard events. + +**Architecture:** Vendored upstream game logic + a small VT1200 backend. The backend installs a zeroed per-process Xlib context (`XG`), opens `"0::0"`, maps a 480×312 window, and emulates an 80×24 cell terminal with 6×13 cells. `XNextEvent`/`XLookupString` feed `game_input`; `0x1E93C0` + `sleep(0x3FB33A20)` pace `update`. A 1990 r5.01/2ine build produces absolute COFF for `dwt/loader`. + +**Tech Stack:** 1990 TI TMS34010 C tools (r5.01) under 2ine; VT1200 ROM Xlib; MAME `vt1200` harness; GSPLNK COFF. + +**Spec:** `docs/superpowers/specs/2026-11-03-termtris-vt1200-design.md` + +## Global Constraints + +- ROM `vt1200_v22.bin` stays byte-identical (`md5 b46a3e442ccb86134d2356bd70d22fd2`); no MAME or ROM edits. +- The game runs from VT1200 main RAM via `dwt/loader`; no network and no host-side game client. +- Link `.text`/`.data`/`.cinit` at bit `0x018A0000`, `.bss` at `0x01A00000`; entry `_start` from `entry.asm`; the `cinit` table is processed by our `var_init`. +- All Xlib calls use the TI C ABI and the addresses in the spec; re-establish the CPU field state after calls if the compiler's generated code requires it. +- The per-process Xlib context fix is mandatory **before** `XOpenDisplay`: + `proc = *(u32 *)0x1B3900; pp = *(u32 *)(proc + 0x100); *(u32 *)pp = (u32)zeroed_xg_block;` +- Build stages are retried: 2ine exits 139 after producing output; check artifacts, not exit codes. +- Vendor `termtris` under `apps/termtris/` with `COPYING` and the GPLv3 headers intact. +- Commit only `apps/termtris/**`, `harness/mame/TERMTRIS_REPORT.md`, and `docs/superpowers/**` in this work. + +## Review Focus + +- **A late client's event queue:** the XG block must be installed before `XOpenDisplay`; if it is not, KeyPress events are zero. The acceptance test must inject a key and assert `type=2`. +- **Cell coordinates:** 6×13, baseline +10; a wrong pitch shears the whole board. The screenshot must show aligned columns. +- **Timer wrap:** `0x1E93C0` is 16-bit; use `(u16)(now-last)` deltas, never a raw monotonic assumption. +- **Toolchain output loss:** `gspcg`/`gspa` can crash after writing; the build must detect missing artifacts and retry, not treat exit 139 as failure. +- **No stdio:** `sprintf`/`fflush`/`stdout` are absent; the compat layer must cover every format/use the game actually has. + +--- + +### Task 1: Vendor and smoke-test the toolchain + +**Files:** +- Create: `apps/termtris/` (upstream `src/`, `COPYING`, `README.md`) +- Create: `apps/termtris/src/vt1200/entry.asm` +- Create: `apps/termtris/src/vt1200/xlib.h` +- Create: `apps/termtris/src/vt1200/xgfix.c` +- Create: `apps/termtris/build.sh` +- Test: `apps/termtris/tests/smoke.c` + `apps/termtris/tests/smoke.sh` + +**Interfaces:** +- Produces: `vt_install_xg(void)` in `xgfix.c`; `xlib.h` with the ROM addresses and `vt_xopen`, `vt_xcreate`, `vt_xselect`, `vt_xmap`, `vt_xsync`, `vt_xflush`. +- Produces: `build.sh` usage `build.sh [extra.obj...]` -> `apps/termtris/build/.coff` and `.map`. + +- [ ] **Step 1: Copy upstream** `termtris` into `apps/termtris/` (keep `COPYING`, `src/game.c`, `pieces.h`, `game.h`, `term.h`, `scoredb.h`; do not copy the DOS/Windows/unix backends). Record the upstream commit hash in `apps/termtris/UPSTREAM`. +- [ ] **Step 2: Write `entry.asm`** — set `A14` to `_cstack`, `A13=A14`; run the cinit loop (length word at `*A9++`, destination long at `*A9++`, copy `length` words), then `CALLA _main`, `RETS`; `.bss _cstack, 32768, 16`. +- [ ] **Step 3: Write `xlib.h`** with the addresses from the spec and TI-ABI prototypes: + `XOpenDisplay 0x3F9C2570`, `XCreateSimpleWindow 0x3FD41B90`, `XSelectInput 0x3FD97D50`, `XMapWindow 0x3FD64F60`, `XSync 0x3FD9F6B0`, `XFlush 0x3F9B1030`, `XNextEvent 0x3F9C1CF0`, `XLookupString 0x3F9A9C30`, `XCreateGC 0x3F997000`, `XSetForeground 0x3FD99A60`, `XFillRectangle 0x3F99E8F0`, `XDrawString 0x3FDA0000`, `sleep 0x3FB33A20`. +- [ ] **Step 4: Write `xgfix.c`** exactly as the spec's fix; return 0 if `proc`/`pp` is outside RAM, else install a static zeroed `u32 xg[256]` and return 1. +- [ ] **Step 5: Write `build.sh`** — extract the r5.01 tools/libs into `apps/termtris/work/`; run `gspac -k`, `gspopt -k -O2`, `gspcg -m`, `gspa`, `gsplnk`; retry each stage up to 5 times and verify its output before proceeding; `gsplnk` command file with `.text/.data/.cinit` at `018A0000h` and `.bss` at `01A00000h`; fail if `termtris.out`/`.map` absent. +- [ ] **Step 6: Smoke test** — `tests/smoke.c` calls `vt_install_xg`, `XOpenDisplay("0::0")`, creates/maps a 480×312 window, selects KeyPress, loops `XNextEvent`, stores `type/keycode` in `.bss` markers, and breaks on `type==2`. `tests/smoke.sh` builds it and runs `dwt/loader/loader.lua` with `LOADER_AT=2500 LOADER_END=3400`, injecting a tablet click at 3000 and `A` at 3100 via a copy of the validated input script. +- [ ] **Step 7: Verify smoke** — the run prints `type=2`, `keycode=194`, `XLookupString` `n=1, ch=0x61, ks=0x0061`. If not, do not proceed. +- [ ] **Step 8: Commit** `apps/termtris` scaffold + smoke. + +### Task 2: Compatibility layer + +**Files:** +- Create: `apps/termtris/src/vt1200/stdio.h` +- Create: `apps/termtris/src/vt1200/inttypes.h` +- Create: `apps/termtris/src/vt1200/compat.c` +- Test: `apps/termtris/tests/fmt.c` + loader run + +**Interfaces:** +- Produces: `int sprintf(char *buf, const char *fmt, ...)` supporting `%d`, `%ld`, `%u`, `%lu`, `%s`, `%c`, `%%`, optional `-`/`0`, decimal width; `int fflush(void *f)` returns 0; `extern void *stdout` is a null pointer; `time_t time(time_t *)` returns `now_ms()/1000`. +- Produces: `uint16_t`/`uint32_t` typedefs in `inttypes.h`. + +- [ ] **Step 1: Write the test** `fmt.c` producing the exact strings used by `game.c`: `"%10ld"` 12345 -> `" 12345"`, `"%2ld"` 7 -> `" 7"`, `"%8ld"` 42 -> `" 42"`, `"%2d. %s %ld"` (1,"ab",99) -> `" 1. ab 99"`, `"%-3s"` "x" -> `"x "`, `"%%"` -> `"%"`. Store a checksum/bytes in markers. +- [ ] **Step 2: Implement `compat.c`** with a manual varargs walk (`char *ap = (char *)&fmt; ap -= 4;` per argument) and a decimal/string formatter; no `stdio.h` from the SDK. +- [ ] **Step 3: Write the shims** so `game.c`'s ``/`` resolve to them; `FILE` is an `int`, `stdout` is 0. +- [ ] **Step 4: Run the loader test** and compare the marker bytes to the expected strings; fix until exact. +- [ ] **Step 5: Commit** compat + test. + +### Task 3: Cell-grid X11 backend + +**Files:** +- Create: `apps/termtris/src/vt1200/x11.c` +- Create: `apps/termtris/src/vt1200/x11.h` +- Test: `apps/termtris/tests/render.c` + screenshot + +**Interfaces:** +- Produces: `int vt_x11_init(void)` — open display, install XG, create 480×312 window at (80,80), select input, map, create GC, `XSync`; returns 0 on success. +- Produces: `void vt_x11_flush(void)` — draw dirty cells. +- Produces: `term_*` callback implementations: `x11_reset`, `x11_clearscr`, `x11_setcursor(row,col)`, `x11_cursor(show)`, `x11_setcolor(fg,bg)`, `x11_ibmchar(c,attr)`; `term_putstr` loops `x11_ibmchar`. +- Produces: `vt_x11_key(int *ks, int *ch)`? No — input is in Task 4; this task only exposes the window/display globals via `x11.h`. + +- [ ] **Step 1: Write `render.c`** that calls `vt_x11_init`, fills an 80×24 cell buffer with a border pattern and `TERMTRIS` text, marks all cells dirty, flushes, and exits via a marker. +- [ ] **Step 2: Implement the cell grid** with `unsigned short ch; unsigned char fg, bg;` per cell; `x11_ibmchar` writes at the cursor and advances; `attr` decode `fg=(attr>>4)&0xf`, `bg=attr&0xf`; mono pixel `(fg==0?0:1)`, `(bg==0?0:1)`. +- [ ] **Step 3: Implement flush** per dirty cell: `XSetForeground(bg)`, `XFillRectangle(x,y,6,13)`, `XSetForeground(fg)`, `XDrawString(x,y+10,ch,1)`; then `XFlush`. +- [ ] **Step 4: Metrics test** — render a row of `'#'` at columns 0..79, a 20×18 playfield outline, and the word `TERMTRIS`; capture the frame at 3300 with `LOADER_SNAP=1`; visually verify 6-px columns and 13-px rows, no shear. +- [ ] **Step 5: Commit** backend + screenshot evidence. + +### Task 4: Input, timer, and main loop + +**Files:** +- Create: `apps/termtris/src/vt1200/main.c` +- Test: `apps/termtris/tests/input.c` + loader run with injected keys + +**Interfaces:** +- Produces: `long now_ms(void)` — 32-bit accumulator over the 16-bit `0x1E93C0`; `void sleep_ms(int ms)` — `sleep(0x3FB33A20)`. +- Produces: `int vt_poll_event(void)` — `XPending` (`0x3FD67620`) > 0; `void vt_handle_event(void)` — `XNextEvent`, `XLookupString`, map to `game_input`. +- Produces: `int main(void)` — `vt_install_xg`, `vt_x11_init`, `init_game`, loop: drain events, `now=now_ms()`, `next=update(now)`, `sleep_ms(min(next-now,10))`; Expose -> `full_redraw` via `game_input('`')`; FocusOut -> `game_input('p')`. + +- [ ] **Step 1: Write `input.c`** that initializes the game/window, then on the first `KeyPress` calls `XLookupString` and stores `keycode`, `keysym`, and the mapped `game_input` byte in markers; inject `A`, Up, Down, Left, Right, `0`, Enter, Backspace. +- [ ] **Step 2: Implement the key map**: letters lower-cased via `XLookupString`; `XK_Left 0xFF51` -> `game_input(0x9b); game_input('A')`; Right 0xFF53 -> `'D'`; Up 0xFF52 -> `'A'?` (use `'w'`); Down 0xFF54 -> `'B'`? Use the game's CSI codes: `0x9b` + `A/B/C/D` for up/down/right/left. Return/Enter -> `'\n'`; Backspace -> `'\b'`; space -> `' '`; Escape -> `27`; digits/letters direct. +- [ ] **Step 3: Implement the timer** and idle sleep; test that the game's `update` is called ~every 50–900 ms by recording a counter in markers. +- [ ] **Step 4: Run with the injected keys**; verify the mapped bytes (A -> 'a', Up -> CSI 'A', etc.) and that the process keeps running. +- [ ] **Step 5: Commit** main/input. + +### Task 5: Game integration, score table, and COFF + +**Files:** +- Modify: `apps/termtris/src/game.c` (only `#ifdef VT1200` includes/compat if needed; game logic unchanged) +- Create: `apps/termtris/src/vt1200/scoredb.c` +- Modify: `apps/termtris/build.sh` to compile `game.c`, `pieces` (header), backend, compat, scoredb, entry +- Test: loader run + screenshot + play + +**Interfaces:** +- Consumes: all Task 2–4 interfaces. +- Produces: `build/termtris.coff`, `build/termtris.map`. +- `scoredb.c` implements `read_scores`, `save_score`, `print_scores` with a static 10-entry list; `username` default `"VT1200"`. + +- [ ] **Step 1: Compile `game.c`** with the 1990 compiler; fix only portability issues (missing `fflush`/`sprintf` via shims, no `unistd`/`fcntl`); do not change game rules. +- [ ] **Step 2: Implement `scoredb.c`** with a static array; `read_scores` returns it; `save_score` sorts/inserts; `print_scores` no-op/fills the list. +- [ ] **Step 3: Build the full COFF**; verify the map: `.text/.data/.cinit` at `018A0000`, `.bss` at `01A00000`, entry `_start`, no undefined symbols. +- [ ] **Step 4: Load and screenshot** at frame 3000 with `dwt/loader/loader.lua`; the Terminal Manager is replaced/covered by the termtris board window. +- [ ] **Step 5: Play** — inject `A/D/S/W`, `0`, `P`, `R`, `H`, `Q`; capture frames showing piece movement, score update, help/score panels; verify quit/restart. +- [ ] **Step 6: Commit** game + scores. + +### Task 6: Documentation and real-unit path + +**Files:** +- Create: `harness/mame/TERMTRIS_REPORT.md` +- Modify: `apps/termtris/README.md` (build/run/real-unit) +- Maybe: `apps/termtris/run.sh` convenience wrapper + +- [ ] **Step 1: Write the report** with the toolchain, the XG root cause/fix, the 6×13/80×24 geometry, the timer, the keyboard map, the loader command, evidence screenshots, and the ROM md5 check. +- [ ] **Step 2: Document the real unit** — same COFF via `dwt/loader/hostload.py`; the XG fix is required; the X server must accept the local connection (default); click the window to focus. +- [ ] **Step 3: Add a `run.sh`** that builds (optional) and runs MAME with the loader and a deterministic input script for CI. +- [ ] **Step 4: Final verification** — `md5sum vt1200_v22.bin` unchanged; no MAME/ROM edits; `git status` only our paths. +- [ ] **Step 5: Commit** docs/report. + +--- + +## Self-review notes + +- Spec coverage: vendoring, build, XG fix, renderer, input, timer, scores, acceptance, docs are each a task. +- The one open implementation choice left to the builder is the exact `sprintf` formatter internals; the required formats are pinned by Task 2's tests. +- The XG block size (1 KiB) is a deliberate value; if the Xlib writes beyond it, enlarge the static buffer. +- The input map uses `XLookupString` for letters/digits and keysyms for arrows/editing keys; the exact game escape codes are the game's existing `game_input` codes.