Files
vt1200/apps/termtris/VT1200.md
T
opencode.energia.arti.ee 5dd01629b9 fix(termtris): do not select ExposureMask
A map-time Expose arrives before the game has a current piece, and the
game's full-redraw path then draws a bogus piece over the top row: visible
as a blanked well border with stray X tiles (the previous commit's
screenshots showed it). The window is never obscured in this session, so
drop the mask, drop the Expose branch, and note the reason in HANDOFF.md.

Refreshed with verify.sh: move 354/18, panels 346/12, quit 300/10, all
with focus=00400001; evidence re-captured and clean.
2026-11-12 05:19:52 +00:00

7.9 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.

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: 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. 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):

move    DIAG: loops=354 events=18 focus=00400001 quit=0
panels  DIAG: loops=346 events=12 focus=00400001 quit=0
quit    DIAG: loops=300 events=10 focus=00400001 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.

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.