forked from arti/vt1200
docs(termtris): implementation plan for the VT1200 X11 port
This commit is contained in:
1 parent
989b8a11e1
commit
2c80547cc3
1 file changed
+153
@@ -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 <source.c> [extra.obj...]` -> `apps/termtris/build/<name>.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 `<stdio.h>`/`<inttypes.h>` 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.
|
||||
Reference in new issue
Block a user