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.
7.9 KiB
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
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_scorekeep 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. qends the game loop but the window stays on screen.quit=1is observable in RAM andmain()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.
