From 3e2dddeb6ae5744de7ed5dd97976cd9125a187b7 Mon Sep 17 00:00:00 2001 From: "opencode.energia.arti.ee" Date: Tue, 3 Nov 2026 20:07:35 +0000 Subject: [PATCH] docs(termtris): port report, run wrapper and harness write-up --- apps/termtris/README.md | 6 + apps/termtris/VT1200.md | 145 ++++++++++++++++++ apps/termtris/build/termtris.coff | Bin 15157 -> 15157 bytes apps/termtris/build/termtris.map | 2 +- apps/termtris/evidence/termtris_frame3300.png | Bin 20986 -> 19347 bytes apps/termtris/run.sh | 39 +++++ apps/termtris/tests/input.c | 7 + harness/mame/README.md | 1 + harness/mame/TERMTRIS_REPORT.md | 122 +++++++++++++++ 9 files changed, 321 insertions(+), 1 deletion(-) create mode 100644 apps/termtris/VT1200.md create mode 100755 apps/termtris/run.sh create mode 100644 harness/mame/TERMTRIS_REPORT.md diff --git a/apps/termtris/README.md b/apps/termtris/README.md index f45463e..72a1382 100644 --- a/apps/termtris/README.md +++ b/apps/termtris/README.md @@ -1,6 +1,12 @@ Termtris - tetris game for terminals ==================================== +> **VT1200 port:** this tree also builds a native TMS34010 program that runs +> inside the VT1200 and draws through the terminal's own ROM Xlib. Build it with +> `apps/termtris/build.sh` (see `apps/termtris/VT1200.md`) and run it with +> `apps/termtris/run.sh`. The upstream backends (unix/ANSI, DOS, ADM3) are not +> part of that build. + ![shots](http://nuclear.mutantstargoat.com/sw/termtris/img/termtris-banner.png) About diff --git a/apps/termtris/VT1200.md b/apps/termtris/VT1200.md new file mode 100644 index 0000000..0add46b --- /dev/null +++ b/apps/termtris/VT1200.md @@ -0,0 +1,145 @@ +# termtris on the VT1200 + +`apps/termtris/` is a port of John Tsiombikas' `termtris` +(https://github.com/jtsiomb/termtris, commit `7c5435dd4e93ffc10f3b5c68fe1ae9718a365258`) +that runs **inside the VT1200 as a native TMS34010 program**, drawing through +the terminal's own ROM Xlib and reading real X11 key events from the local X +server. There is no host process and no terminal escape output: the COFF is +loaded into VT1200 RAM and executed by the TMS34010. + +![termtris board](evidence/termtris_frame3300.png) + +## What runs where + +| piece | file | role | +|---|---|---| +| game rules | `src/game.c` (upstream) | unchanged logic; one portability edit (`srand((now_ms()))` instead of `time(0)`) | +| term callbacks | `src/vt1200/x11.c` | 80×24 character-cell grid drawn with `XFillRectangle`/`XDrawString` | +| event loop | `src/vt1200/main.c` | `XNextEvent` + `XLookupString`, key map, millisecond clock, `sleep()` | +| scores | `src/vt1200/scoredb.c` | RAM-only high-score table (no filesystem) | +| libc shims | `src/vt1200/compat.c`, `stdio.h`, `inttypes.h` | `sprintf`/`fflush`/`putchar`, fixed-width types | +| Xlib | `src/vt1200/xlib.h` | ROM entry points by bit address, called with the TI C ABI | +| startup | `src/vt1200/entry.asm`, `cinit.asm`, `cinitz.asm` | set `A14 = _cstack`, run the `.cinit` table, `calla _main` | +| toolchain | `build.sh` | TI TMS340 Family Code Generation Tools r5.01 (1990-11) under 2ine | + +## Building + +```sh +apps/termtris/build.sh src/vt1200/main.c src/game.c src/vt1200/scoredb.c \ + src/vt1200/x11.c src/vt1200/compat.c src/vt1200/xgfix.c +``` + +`build.sh` unpacks the r5.01 SDK zips, runs `gspac`/`gspopt`/`gspcg`/`gspa` and +`gsplnk` through `lx_loader`, and writes `build/
.coff` + `.map`. Set +`NINE=` to the 2ine build directory (default `/home/arti/os2-work/2ine/build`). +The 2ine-hosted tools routinely segfault *after* producing their output, so the +script treats an output file as success and retries a missing one; `gspac` needs +`-i.` for the angle-bracket shim headers. + +Link map (relevant addresses are **bit** addresses, like everything the +TMS34010 sees): + +``` +.text 018A0000 .data 018A0000 .cinit 018B4730 +.bss 01A00000 +ENTRY POINT SYMBOL: "_start" address: 018a0000 +``` + +An empty `.cinit` makes the linker point `cinit` at `.text`, and the entry +walker would then read an instruction as a record count; when the map says the +section is empty `build.sh` relinks with the one-word `CINITZ.obj` terminator. + +## The Xlib context (the one thing that had to be discovered) + +A freshly `create_process`ed child has `*(*(proc + 0x100)) == NULL`. The ROM +Xlib's `_XEnq` does `XG = *(*(proc+0x100)); free_list = *(XG + 0x198)`, so +`XNextEvent` reads low-RAM garbage as queue nodes and delivers nothing. The +port installs a zeroed 1 KiB block as the per-process Xlib context before +`XOpenDisplay` (`src/vt1200/xgfix.c`), which is a RAM write only — the ROM is +untouched. With that in place the smoke test receives a real +`KeyPress (type=2, keycode=194)` and `XLookupString` returns `a`. + +## Rendering + +The frame is an 80×24 grid of 6×13 cells (the default GC font, baseline +10), +so the window is 480×312. The VT1200 raster is **one bit deep**, so the backend +follows the game's own monochrome convention (`ansi.c -m`): colour is dropped +and the bold bit (`0x80`) reverses a cell. The playfield is therefore black +spaces and the tetrominoes are the `[`/`]` tiles, exactly what a 1-bit terminal +can show. + +`vt_x11_flush()` repaints whole rows rather than cells: one `XFillRectangle` +for the row background, one `XDrawString` for its 80 characters, plus +fill/string pairs for reversed spans, then a single `XFlush`. This matters. An +earlier per-cell version (four requests per cell) filled the X socket faster +than the in-machine server could drain it, and an `XSync` per row deadlocked +the client: the debug progress marker stopped at row 20 (`0x00010014`) and the +game never reached its main loop. Batching the row makes a full 80×24 redraw a +few hundred bytes of requests and the loop runs (measured: 300+ iterations +between frames 2500 and 3200). + +## Timing and input + +`now_ms()` accumulates the 16-bit counter at bit `0x1E93C0` into a 32-bit +millisecond count; `sleep_ms()` calls the ROM `sleep` at `0x3FB33A20` with a +1–10 ms clamp. The game's own `update()` decides the tick interval (887 ms at +level 0). + +Keys arrive as X11 `KeyPress` events and go through `XLookupString`; letters are +lower-cased and handed to `game_input()`. Arrows are sent as the game's own CSI +codes (`0x9b` + `A`/`B`/`C`/`D`), which `game_input` translates back to +`w`/`s`/`d`/`a`. `0x7F` is Backspace/Delete. `Expose` triggers a full redraw and +`FocusOut` (only after a `FocusIn`) pauses the game. + +## Running it in MAME + +```sh +apps/termtris/tests/play.sh # load, click, snapshot +apps/termtris/tests/play_keys.sh # move / panels / quit acceptance runs +apps/termtris/run.sh # same run, interactive session +``` + +Under the hood the loader payload is dropped at bit `0x01880000` and the COFF at +`0x01900000`; at frame 2500 the script enters `dwt/loader/build/loader.bin`, +which copies the sections to their linked addresses and calls +`create_process`. The X server is already up because the run uses +`VT1200_SKIP_LEVEL1=1` to reach the OS. + +## Evidence + +| file | what it shows | +|---|---| +| `evidence/termtris_frame3300.png` | the board, score/level/lines panels, preview and `Toggle (H)elp` | +| `evidence/termtris_move_3200.png` | after `a d s w 0`: a piece has been moved/rotated/dropped to the bottom-left and a new one spawned | +| `evidence/termtris_panels_3200.png` | after `h` and `r`: the help text and the high-score list (all `VT1200`) | +| `evidence/termtris_quit_3200.png` | after `q`: same screen, and the run reports `quit=1` | +| `evidence/render_frame3400.png` | the cell-grid renderer test pattern | + +Acceptance values from the last full run (`play_keys.sh`): + +``` +move DIAG: loops=333 events=18 focus=00400001 quit=0 +panels DIAG: loops=323 events=12 focus=00400001 quit=0 +quit DIAG: loops=277 events=10 focus=00400001 quit=1 +``` + +`focus=00400001` is the termtris window, so the key events really were +delivered to it, and `quit=1` is the game's own global after `q`. + +## Limits, stated plainly + +* **No sound.** The game's bell is a no-op. +* **Scores are RAM-only.** `read_scores`/`save_score` keep ten entries in + `.bss`; they vanish when the process exits. The VT1200 has no writable + filesystem for a child. +* **Startup is retried.** If `XOpenDisplay`/window creation fails while the + display server is still settling, `main()` retries up to 40 times with a + 50 ms sleep before giving up. Observed as an intermittent early exit. +* **`q` ends the game loop but the window stays on screen.** `quit=1` is + observable in RAM; the created process does not tear down its X connection, + so the server keeps the window. +* **The board is text.** Tetrominoes are `[`/`]` tiles, not filled blocks — the + only thing a 1-bit cell can show without a custom font. + +The ROM (`vt1200_v22.bin`, md5 `b46a3e442ccb86134d2356bd70d22fd2`) is never +modified; the port lives entirely in RAM. diff --git a/apps/termtris/build/termtris.coff b/apps/termtris/build/termtris.coff index 5836bfc488318373bb6ab8263d460e90a57fc24e..5046cde4e5cb1b04a47d6c59684af759117219af 100644 GIT binary patch delta 15 Wcmdm5wzZ6H0s{*}^ZJc!`qlt1#RWtF delta 15 Wcmdm5wzZ6H0s{*J`?`&6`qls}*aXJ_ diff --git a/apps/termtris/build/termtris.map b/apps/termtris/build/termtris.map index af6ca6a..3ecbbd3 100644 --- a/apps/termtris/build/termtris.map +++ b/apps/termtris/build/termtris.map @@ -1,7 +1,7 @@ ***************************************************** TMS340 COFF Linker Version 5.01 ***************************************************** -Tue Nov 03 20:01:11 2026 +Tue Nov 03 20:07:31 2026 OUTPUT FILE NAME: ENTRY POINT SYMBOL: "_start" address: 018a0000 diff --git a/apps/termtris/evidence/termtris_frame3300.png b/apps/termtris/evidence/termtris_frame3300.png index 92dafb50bba599c627d5b9dc22397f265167f427..47128cb0eb6bce2d62a79645d42ae1b074ac3e3a 100644 GIT binary patch literal 19347 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

V=wgul?*b2O_` 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 + exit 1 +fi +if [ ! -f "$LOADER" ]; then + echo "run.sh: no loader payload at $LOADER" >&2 + exit 1 +fi + +# The game is loaded at frame 2500, once the OS X server is up. +exec env VT1200_SKIP_LEVEL1=1 \ + LOADER_AT=2500 LOADER_END=100000000 \ + LOADER_BIN="$LOADER" LOADER_COFF="$COFF" LOADER_OUT="$DIR/build" \ + "$M" vt1200 -rompath "$ROOT/harness/mame/roms" \ + -skip_gameinfo -debug -debugger none \ + -autoboot_script "$DIR/tests/play.lua" -autoboot_delay 0 diff --git a/apps/termtris/tests/input.c b/apps/termtris/tests/input.c index 8735c32..f23231a 100644 --- a/apps/termtris/tests/input.c +++ b/apps/termtris/tests/input.c @@ -7,8 +7,15 @@ #include "xlib.h" int term_width, term_height; +/* game.c globals main.c sets or reads; the real game supplies these */ +int onlyascii, use_gfxchar, monochrome; +int quit; +long tick_interval; volatile vt_u32 mark[128]; +void cleanup_game(void) {} +void wait_display(void); + int init_game(void) { mark[0] = 0x494E5054UL; /* INPT */ diff --git a/harness/mame/README.md b/harness/mame/README.md index c4b24c7..f1deecb 100644 --- a/harness/mame/README.md +++ b/harness/mame/README.md @@ -76,6 +76,7 @@ Reports in this directory: | `BRINGUP_LOG.md` | the whole bring-up narrative, **including withdrawn claims** — read it sceptically | | `ROMPATCH_REPORT.md` | runtime ROM patching (debug only — see the caveat) | | `HELLO_WORLD_REPORT.md` | the TMS34010 SDK assembles an 82-byte payload of **our own** code; it runs, writes RAM markers and paints a VRAM block. Debugger entry, not a delivery mechanism | +| `TERMTRIS_REPORT.md` | the upstream tetris game built with the 1990 r5.01 C toolchain and run **inside** the terminal: ROM Xlib rendering, real X11 key events, the XG-context fix and the row-batched flush | ## Address model (the important part) diff --git a/harness/mame/TERMTRIS_REPORT.md b/harness/mame/TERMTRIS_REPORT.md new file mode 100644 index 0000000..5d3f80a --- /dev/null +++ b/harness/mame/TERMTRIS_REPORT.md @@ -0,0 +1,122 @@ +# TERMTRIS_REPORT — the upstream tetris game running inside the VT1200 + +`apps/termtris/` builds **our own TMS34010 program** from John Tsiombikas' +`termtris` (upstream commit `7c5435dd4e93ffc10f3b5c68fe1ae9718a365258`) with the +1990 TI r5.01 C toolchain under 2ine, loads it into VT1200 RAM and runs it on +the emulated terminal's own X server. It draws through the ROM Xlib and reads +real X11 `KeyPress` events. No host client, no terminal escape output, no ROM +patches. + +Full port documentation: `apps/termtris/VT1200.md`. + +## Build + +``` +apps/termtris/build.sh src/vt1200/main.c src/game.c src/vt1200/scoredb.c \ + src/vt1200/x11.c src/vt1200/compat.c src/vt1200/xgfix.c +``` + +`build.sh` runs `gspac`/`gspopt`/`gspcg`/`gspa` and `gsplnk` through +`/home/arti/os2-work/2ine/build/lx_loader` against the r5.01 SDK zips under +`tools/tms34010-sdk/1990-11 TMS340 Family SDK/`. Those 2ine-hosted tools +frequently segfault after writing their output, so the wrapper treats an output +file as success and retries a missing one. Two quirks are handled there: + +* `gspac` needs `-i.` for the angle-bracket shim headers; +* an empty `.cinit` makes `cinit` point at `.text`, whose first instruction the + entry walker would read as a record count — when the map reports `.cinit` + length 0 the link is repeated with the one-word `CINITZ.obj` terminator. + +Result (map): + +``` +.text 018A0000 .cinit 018B4730 .bss 01A00000 +ENTRY POINT SYMBOL: "_start" address: 018a0000 +``` + +These are **bit** addresses, as everywhere on the TMS34010. + +## The one required Xlib fix + +A child created by `create_process` has a NULL per-process Xlib context +(`*(*(proc + 0x100))`). The ROM Xlib's `_XEnq` treats that as the queue head, so +`XNextEvent` copies unmapped garbage and never returns an event. +`src/vt1200/xgfix.c` writes a zeroed 1 KiB block into that per-process word +before `XOpenDisplay`. RAM only; the ROM is unchanged. + +Smoke test evidence (`apps/termtris/tests/smoke.sh`): +`type=2 keycode=194 lookup_n=1 ch=61 ks=0061` — a real `KeyPress` and +`XLookupString` returning `a`. + +## Rendering and the flush deadlock + +80×24 cells of 6×13 px (the default GC font, baseline +10) → a 480×312 window. +The raster is 1 bit deep, so the backend follows the game's own monochrome +convention: colour is dropped and bold (`0x80`) reverses a cell. The playfield +is black spaces; tetrominoes are the `[`/`]` tiles. + +`vt_x11_flush()` repaints **whole rows** — one fill for the row background, one +`XDrawString` for its 80 characters, fill/string pairs for reversed spans, then +a single `XFlush`. This is not an optimisation for looks: + +* the per-cell version (4 requests/cell) outran the in-machine X server; +* an `XSync` per row **deadlocked the client** — the debug marker + `_vt_x11_progress` froze at `0x00010014` (row 20), the main-loop counter + stayed 0 and the game window never got a preview piece. Batching a row makes + a full redraw a few hundred bytes and `loops` reached 300+ between frames + 2500 and 3200. + +## Timing and keys + +`now_ms()` accumulates the 16-bit counter at bit `0x1E93C0` into 32 bits; +`sleep_ms()` calls the ROM `sleep` (`0x3FB33A20`) with a 1–10 ms clamp. The +game's own `update()` picks the tick (887 ms at level 0). + +`KeyPress` → `XLookupString` → lower-cased letters to `game_input()`; arrows as +the game's CSI codes (`0x9b` + `A`/`B`/`C`/`D`), which it maps back to +`w`/`s`/`d`/`a`; `0x7F` is Backspace/Delete; `Expose` forces a full redraw. + +## Reproduce + +```sh +cd /home/arti/.local/share/opencode/worktree/26e92e/termtris +apps/termtris/tests/play.sh # load, click, snapshot +apps/termtris/tests/play_keys.sh # move / panels / quit +apps/termtris/run.sh # interactive MAME session +``` + +The run is `VT1200_SKIP_LEVEL1=1 LOADER_AT=2500` with the loader payload at bit +`0x01880000` and the COFF at `0x01900000`; at frame 2500 the script enters +`dwt/loader/build/loader.bin`, which places the sections and calls +`create_process`. + +Last full acceptance run: + +``` +move DIAG: loops=333 events=18 focus=00400001 quit=0 +panels DIAG: loops=323 events=12 focus=00400001 quit=0 +quit DIAG: loops=277 events=10 focus=00400001 quit=1 +``` + +`focus=00400001` is the termtris window, and `quit=1` is the game's global after +`q` — so the keys reached the game, not just the window. + +| evidence | file | +|---|---| +| board, panels, preview | `apps/termtris/evidence/termtris_frame3300.png` | +| after a/d/s/w/0 | `apps/termtris/evidence/termtris_move_3200.png` | +| after h and r | `apps/termtris/evidence/termtris_panels_3200.png` | +| after q (`quit=1`) | `apps/termtris/evidence/termtris_quit_3200.png` | +| renderer test pattern | `apps/termtris/evidence/render_frame3400.png` | + +## Limits + +* No sound, no filesystem: scores live in `.bss` and vanish with the process. +* `q` sets the game's `quit` and the loop exits, but the window stays on screen + — the created process does not tear down its X connection. +* Startup is retried (up to 40 × 50 ms) because `XOpenDisplay`/window creation + intermittently fails while the display server settles. +* Tetrominoes are text tiles; a 1-bit cell has no other shape to offer. + +The ROM `vt1200_v22.bin` is never modified — `md5 b46a3e442ccb86134d2356bd70d22fd2` +before and after.