Files
vt1200/apps/termtris/VT1200.md
T
opencode 8df2320d6d docs(termtris): re-run acceptance at the VR319 default
The branch's evidence was captured on an older MAME binary whose frames
were VR150 (1024x864).  Rebuild MAME from this tree's driver and re-run
verify.sh --skip-mame at the VR319 default (1280x1024 active).

All six tests pass.  Event counts, focus=00400001, tick=887 and quit=1
are unchanged; only the main-loop counts moved with the frame cadence:
move 650, panels 648, quit 555 (was 354/346/300).  The evidence PNGs are
regenerated at 1280x1024.  Record the numbers and the geometry change in
VT1200.md, TERMTRIS_REPORT.md and HANDOFF.md, and note the qemu-i386
toolchain fallback.
2026-11-12 05:56:56 +00:00

9.1 KiB
Raw Blame History

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

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

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.

The r5.01 programs are 16-bit OS/2 NE executables. lx_loader is 32-bit but maps them through 16-bit LDT segments, and a Linux kernel built without CONFIG_X86_16BIT rejects those (modify_ldt returns EINVAL) — the loader then aborts at its first segment even though it starts. On such a host build.sh probes for that failure and runs the same loader through qemu-i386, whose linux-user modify_ldt does implement 16-bit segments. Set LX_RUNNER=qemu-i386 to force a runner, or install qemu-user so the fallback is available. The generated COFF is unchanged; only its header timestamp differs between builds.

Link map (relevant addresses are bit addresses, like everything the TMS34010 sees):

.text   018A0000   .data 018A0000   .cinit 018B4B10
.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_processed 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 renders like the game's monochrome mode (colour is dropped) and uses the bold bit (0x80) as reverse video for emphasis — a choice of this backend, not the upstream ANSI convention (which emits SGR bold and leaves the colours alone). 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 at the VR319 default: 650 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. FocusOut pauses and FocusIn resumes (only if we paused it).

ExposureMask is deliberately not selected: the map-time Expose arrives before the game has a current piece, and the game's full-redraw path then draws a non-existent piece over the top row — visible as a blanked border with stray X tiles. The window is never obscured in this session (the Terminal Manager sits beside it), so the mask buys nothing. See HANDOFF.md.

Running it in MAME

The emulator must be built from the driver in this tree; an older /home/arti/code/k-space/mame0289/vt1200 may still carry the pre-VR319 geometry:

harness/mame/build.sh          # rebuild the vt1200 subtarget (JOBS=4 on small hosts)

Then:

apps/termtris/verify.sh              # build MAME + run every test (see HANDOFF.md)
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, MAME 0.289 with this tree's driver at the VR319 default, 1280×1024 frames). The evidence PNGs above were regenerated in the same run; they are 1280×1024, where the earlier ones were 1024×864 VR150 captures.

move    DIAG: loops=650 events=18 last=3 focus=00400001 rev=0 quit=0
panels  DIAG: loops=648 events=12 last=3 focus=00400001 rev=0 quit=0
quit    DIAG: loops=555 events=10 last=3 focus=00400001 rev=0 quit=1

focus=00400001 is the window this process created (it is the only one in the run), so the key events were delivered to it, and quit=1 is the game's own global after q. The loop counts are higher than the old VR150 figures (354/346/300) only because the VR319 raster runs at a different emulated frame rate; the event counts, window, and quit result are unchanged, and the loader/keys still act at frames 2500/3095–3190.

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 and main() returns; the frame captured about a second later still shows the window, so the X connection is not torn down by process exit in this OS. Treat that as an observation, not a designed behaviour.
  • 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.