forked from arti/vt1200
docs(termtris): VT1200 X11 port design; local Xlib XG fix
This commit is contained in:
1 parent
17dfd24340
commit
989b8a11e1
1 file changed
+138
@@ -0,0 +1,138 @@
|
||||
# termtris for the VT1200 — design
|
||||
|
||||
Date: 2026-11-03. Status: ready for implementation (input path proven).
|
||||
|
||||
## Intent
|
||||
|
||||
Run `jtsiomb/termtris` as a **local VT1200 program**: loaded into main RAM by the
|
||||
existing COFF loader, displayed through the terminal's **ROM X11 server**, and
|
||||
controlled from the real X keyboard events. End-to-end proof is a playable game in
|
||||
MAME, with the real-unit path documented.
|
||||
|
||||
## What is already proven (2026-11-03)
|
||||
|
||||
- **Compiler**: the 1990 r5.01 OS/2 toolchain under the patched 2ine fork builds
|
||||
ANSI C. Pipeline: `gspac -> gspopt -O2 -> gspcg -> gspa -> gsplnk` -> absolute
|
||||
COFF, linked at bit `0x018A0000` (`.bss` at `0x01A00000`).
|
||||
- **Loader**: `dwt/loader` pokes the COFF, `create_process`es the entry, and the
|
||||
program runs. `.cinit` static initialisers run via our own `var_init` in
|
||||
`entry.asm`; the linker's `cinit` symbol is at the start of `.cinit`.
|
||||
- **ROM Xlib**: `XOpenDisplay`, `XCreateSimpleWindow`, `XSelectInput`,
|
||||
`XMapWindow` (`0x3FD64F60`; `0x3FD645B0` is `XMapRaised`), `XSync`,
|
||||
`XCreateGC`, `XSetForeground`, `XFillRectangle` (`0x3F99E8F0`),
|
||||
`XDrawString` (`0x3FDA0000`), `XNextEvent` (`0x3F9C1CF0`),
|
||||
`XLookupString` (`0x3F9A9C30`) all work from C with the TI ABI.
|
||||
- **Rendering**: the default GC's font is **6×13** (baseline +10); an 80×24 cell
|
||||
grid is 480×312. White rectangle + text were rendered in a WM-framed window.
|
||||
- **Input (the blocker, now solved)**: a `create_process` child has an
|
||||
**uninitialised per-process Xlib context**. `create_process` leaves
|
||||
`*(proc+0x100)` pointing at a zeroed word; `_XEnq` derives
|
||||
`XG = *(*(proc+0x100))`, so `XG == NULL` and its event free list at
|
||||
`XG+0xCC0` (bit) reads low-RAM garbage. KeyPress nodes were therefore invalid
|
||||
and `XNextEvent` copied unmapped zeros.
|
||||
**Fix**: before `XOpenDisplay`, set
|
||||
`*(u32 *)(*(u32 *)(proc + 0x100)) = (u32)zeroed_block`, where `proc` is
|
||||
`*(u32 *)0x1B3900` and `zeroed_block` is a static `.bss` buffer (1 KiB is
|
||||
enough). With that, the local client receives real events:
|
||||
KeyPress `type=2`, `keycode=194` (LK401 `C2`), `window=0x00400001`, and
|
||||
`XLookupString` returns `n=1, char='a', keysym=0x61`.
|
||||
- **Timing**: `sleep(ms)` at `0x3FB33A20` blocks and yields; the raw millisecond
|
||||
counter is the 16-bit `0x1E93C0` (+17 per display interrupt). Use a 32-bit
|
||||
accumulator with `(u16)(now - last)` to avoid the 84 s wrap.
|
||||
- **TCP X** also works (`xdpyinfo`/`xev` against `100.103.100.103:0` with the
|
||||
real unit's NVRAM security settings); it is useful for debugging, but the game
|
||||
uses the local service (`"0::0"`), so it needs no network.
|
||||
|
||||
## Goals
|
||||
|
||||
- `termtris` game logic (pieces, scoring, levels, spawn, collision) unchanged in
|
||||
behaviour.
|
||||
- X11 cell-grid renderer: 80×24 characters, 6×13 cells, monochrome pens.
|
||||
- Real X11 input: `KeyPress` -> `XLookupString`/keysym -> `game_input`; arrows
|
||||
mapped to the game's CSI sequences, letters lower-cased.
|
||||
- Runs from local RAM under `dwt/loader`, with a screenshot and a playable run.
|
||||
- A build script and a short report documenting the toolchain, the leak/loader
|
||||
addresses, the XG fix, and the limits.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- No host-side X client for the game (the x86 `xev` was a diagnostic only).
|
||||
- No terminal/ANSI backend on the VT1200 (the game is X11).
|
||||
- No modification of the ROM or MAME. All fixes are runtime RAM writes.
|
||||
- No file-backed high-score table (the VT1200 has no filesystem); scores live in
|
||||
RAM for the session.
|
||||
|
||||
## Approaches considered
|
||||
|
||||
1. **Local Xlib + XG initialisation (chosen).** Proven; uses the terminal's real
|
||||
X server and keyboard; no network; the game is a local `create_process` child.
|
||||
2. Local Xlib + OS pointer/keycode polling. Reliable fallback, but the keyboard
|
||||
global is lossy (no release/repeat) and the pointer is an odd controller.
|
||||
Keep as a compile-time fallback only.
|
||||
3. Host x86 X client over TCP. Works and was used for diagnosis, but does not
|
||||
satisfy "run from VT1200 RAM".
|
||||
4. Terminal/ANSI backend. Not X11 and not the request.
|
||||
|
||||
## Architecture / files
|
||||
|
||||
```
|
||||
apps/termtris/ vendored upstream (GPLv3, COPYING kept)
|
||||
src/game.c, pieces.h upstream, small VT1200 patches
|
||||
src/vt1200/
|
||||
main.c X11 event loop, timer, key mapping
|
||||
x11.c term_* callbacks -> cell grid -> XFillRectangle/XDrawString
|
||||
xlib.h ROM entry-point addresses + TI-ABI prototypes
|
||||
xgfix.c per-process Xlib context installation
|
||||
scoredb.c RAM-only high scores
|
||||
compat.c sprintf subset, fflush/stdout stubs
|
||||
entry.asm _start + cinit var_init + rets
|
||||
build.sh 2ine/r5.01 pipeline with stage retries
|
||||
README.md build/run/real-unit instructions
|
||||
```
|
||||
|
||||
Rendering is a small virtual terminal: `term_setcursor`/`term_putstr`/
|
||||
`term_setcolor`/`term_ibmchar` update a cell buffer; `wait_display()` flushes
|
||||
changed cells with `XFillRectangle` (background) and `XDrawString` (glyph).
|
||||
The 6×13 metrics are measured, not assumed. A full redraw is 80×24 cells.
|
||||
|
||||
Input: `XSelectInput` with `KeyPressMask|KeyReleaseMask|ButtonPressMask|
|
||||
ExposureMask|StructureNotifyMask|FocusChangeMask`; the event loop drains
|
||||
`XNextEvent`; `XLookupString` gives the character/keysym. Arrow keys become
|
||||
`ESC [ A..D` for `game_input`; `FocusOut` pauses.
|
||||
|
||||
Timing: `now_ms()` accumulates `0x1E93C0` deltas; the idle path is
|
||||
`sleep(min(next_tick - now, 10))`, so the CPU yields to the X server and the OS.
|
||||
|
||||
## Build
|
||||
|
||||
`entry.asm` supplies the process entry and runs the `cinit` table (the 1990
|
||||
runtime's `var_init` is local, so the entry implements the 16-bit-length/32-bit-
|
||||
destination copy itself). The linker command file places `.text`/`.data`/`.cinit`
|
||||
at `0x018A0000` and `.bss` at `0x01A00000`. The build script retries the 2ine
|
||||
stage crashes (they exit 139 after producing output) and checks for outputs, not
|
||||
exit codes.
|
||||
|
||||
## Acceptance
|
||||
|
||||
In MAME, with the loader:
|
||||
- the window appears with the termtris board and score panels;
|
||||
- `A/D/S/W`, arrows, `0`/Enter, `P`, `R`, `H`, `Q`/Backspace behave as upstream;
|
||||
- a screenshot of the playfield and a short play session are captured;
|
||||
- the ROM md5 is unchanged.
|
||||
|
||||
Real unit: the same COFF can be delivered with `dwt/loader/hostload.py` once the
|
||||
OS is up; the document will state that the XG fix is required there too and that
|
||||
the real unit's X server supplies the same keycodes (LK401).
|
||||
|
||||
## Risks
|
||||
|
||||
- The 2ine toolchain intermittently segfaults after writing outputs; the builder
|
||||
retries and verifies every artifact.
|
||||
- The per-process XG block size is inferred (we used 1 KiB); if the Xlib uses
|
||||
more, enlarge the buffer. It worked through 9 events in the test.
|
||||
- `sprintf` is absent from the 1990 runtime; `compat.c` implements only the
|
||||
formats the game uses (`%d`, `%ld`, `%s`, `%c`, width, left-justify, `%%`).
|
||||
- Font metrics are the ROM default GC's 6×13; do not use a different GC/font
|
||||
without re-measuring.
|
||||
- GPLv3: the vendored tree keeps `COPYING` and the copyright headers; the repo's
|
||||
own license is unchanged.
|
||||
Reference in new issue
Block a user