docs(termtris): port report, run wrapper and harness write-up

This commit is contained in:
opencode.energia.arti.ee committed 2026-11-03 20:07:35 +00:00
1 parent 41d3cecd49
commit 3e2dddeb6a
9 files changed
+321 -1

No files matched your search

+6
View File
@@ -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
+145
View File
@@ -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/<main>.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.
Binary file not shown.
+1 -1
View File
@@ -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: <MAIN.out>
ENTRY POINT SYMBOL: "_start" address: 018a0000
Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 19 KiB

+39
View File
@@ -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
+7
View File
@@ -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 */
+1
View File
@@ -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)
+122
View File
@@ -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.