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 5836bfc..5046cde 100644 Binary files a/apps/termtris/build/termtris.coff and b/apps/termtris/build/termtris.coff differ 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 92dafb5..47128cb 100644 Binary files a/apps/termtris/evidence/termtris_frame3300.png and b/apps/termtris/evidence/termtris_frame3300.png differ diff --git a/apps/termtris/run.sh b/apps/termtris/run.sh new file mode 100755 index 0000000..bc80057 --- /dev/null +++ b/apps/termtris/run.sh @@ -0,0 +1,39 @@ +#!/bin/sh +# run.sh -- load termtris into a running VT1200 OS under MAME and play it. +# +# apps/termtris/run.sh # click the window active, then play +# apps/termtris/run.sh --build # rebuild the COFF first +# +# The emulated keyboard and tablet are driven by MAME's host input, so the +# terminal's X cursor follows the host pointer. Quit with Ctrl-C in the +# terminal running MAME, or press 'q' in the game (it stops the game loop). +set -eu + +DIR=$(cd "$(dirname "$0")" && pwd) +ROOT=$(cd "$DIR/../.." && pwd) +M=${MAME:-/home/arti/code/k-space/mame0289/vt1200} +LOADER="$ROOT/dwt/loader/build/loader.bin" +COFF="$DIR/build/termtris.coff" + +if [ "${1:-}" = "--build" ]; then + "$DIR/build.sh" "$DIR/src/vt1200/main.c" "$DIR/src/game.c" \ + "$DIR/src/vt1200/scoredb.c" "$DIR/src/vt1200/x11.c" \ + "$DIR/src/vt1200/compat.c" "$DIR/src/vt1200/xgfix.c" +fi + +if [ ! -f "$COFF" ]; then + echo "run.sh: no $COFF (run with --build)" >&2 + 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.