forked from opencode.energia.arti.ee/vt1200
docs(termtris): port report, run wrapper and harness write-up
This commit is contained in:
1 parent
41d3cecd49
commit
3e2dddeb6a
9 files changed
+321
-1
No files matched your search
@@ -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.
|
||||
|
||||

|
||||
|
||||
About
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
## 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,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 |
Executable
+39
@@ -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,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 */
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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.
|
||||
Reference in new issue
Block a user