From 989b8a11e17b37059c2b5b8db6914578e0652ae4 Mon Sep 17 00:00:00 2001 From: "opencode.energia.arti.ee" Date: Tue, 3 Nov 2026 17:51:23 +0000 Subject: [PATCH 01/18] docs(termtris): VT1200 X11 port design; local Xlib XG fix --- .../2026-11-03-termtris-vt1200-design.md | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 docs/superpowers/specs/2026-11-03-termtris-vt1200-design.md diff --git a/docs/superpowers/specs/2026-11-03-termtris-vt1200-design.md b/docs/superpowers/specs/2026-11-03-termtris-vt1200-design.md new file mode 100644 index 0000000..bc4ef38 --- /dev/null +++ b/docs/superpowers/specs/2026-11-03-termtris-vt1200-design.md @@ -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. -- 2.47.3 From 2c80547cc3f87e98e60303a895253b8d17555263 Mon Sep 17 00:00:00 2001 From: "opencode.energia.arti.ee" Date: Tue, 3 Nov 2026 18:42:47 +0000 Subject: [PATCH 02/18] docs(termtris): implementation plan for the VT1200 X11 port --- .../plans/2026-11-03-termtris-vt1200.md | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 docs/superpowers/plans/2026-11-03-termtris-vt1200.md diff --git a/docs/superpowers/plans/2026-11-03-termtris-vt1200.md b/docs/superpowers/plans/2026-11-03-termtris-vt1200.md new file mode 100644 index 0000000..606b55a --- /dev/null +++ b/docs/superpowers/plans/2026-11-03-termtris-vt1200.md @@ -0,0 +1,153 @@ +# termtris VT1200 X11 port — implementation plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Build a playable `termtris` that runs as a VT1200 `create_process` child, renders through the ROM X11 server, and accepts the real X keyboard events. + +**Architecture:** Vendored upstream game logic + a small VT1200 backend. The backend installs a zeroed per-process Xlib context (`XG`), opens `"0::0"`, maps a 480×312 window, and emulates an 80×24 cell terminal with 6×13 cells. `XNextEvent`/`XLookupString` feed `game_input`; `0x1E93C0` + `sleep(0x3FB33A20)` pace `update`. A 1990 r5.01/2ine build produces absolute COFF for `dwt/loader`. + +**Tech Stack:** 1990 TI TMS34010 C tools (r5.01) under 2ine; VT1200 ROM Xlib; MAME `vt1200` harness; GSPLNK COFF. + +**Spec:** `docs/superpowers/specs/2026-11-03-termtris-vt1200-design.md` + +## Global Constraints + +- ROM `vt1200_v22.bin` stays byte-identical (`md5 b46a3e442ccb86134d2356bd70d22fd2`); no MAME or ROM edits. +- The game runs from VT1200 main RAM via `dwt/loader`; no network and no host-side game client. +- Link `.text`/`.data`/`.cinit` at bit `0x018A0000`, `.bss` at `0x01A00000`; entry `_start` from `entry.asm`; the `cinit` table is processed by our `var_init`. +- All Xlib calls use the TI C ABI and the addresses in the spec; re-establish the CPU field state after calls if the compiler's generated code requires it. +- The per-process Xlib context fix is mandatory **before** `XOpenDisplay`: + `proc = *(u32 *)0x1B3900; pp = *(u32 *)(proc + 0x100); *(u32 *)pp = (u32)zeroed_xg_block;` +- Build stages are retried: 2ine exits 139 after producing output; check artifacts, not exit codes. +- Vendor `termtris` under `apps/termtris/` with `COPYING` and the GPLv3 headers intact. +- Commit only `apps/termtris/**`, `harness/mame/TERMTRIS_REPORT.md`, and `docs/superpowers/**` in this work. + +## Review Focus + +- **A late client's event queue:** the XG block must be installed before `XOpenDisplay`; if it is not, KeyPress events are zero. The acceptance test must inject a key and assert `type=2`. +- **Cell coordinates:** 6×13, baseline +10; a wrong pitch shears the whole board. The screenshot must show aligned columns. +- **Timer wrap:** `0x1E93C0` is 16-bit; use `(u16)(now-last)` deltas, never a raw monotonic assumption. +- **Toolchain output loss:** `gspcg`/`gspa` can crash after writing; the build must detect missing artifacts and retry, not treat exit 139 as failure. +- **No stdio:** `sprintf`/`fflush`/`stdout` are absent; the compat layer must cover every format/use the game actually has. + +--- + +### Task 1: Vendor and smoke-test the toolchain + +**Files:** +- Create: `apps/termtris/` (upstream `src/`, `COPYING`, `README.md`) +- Create: `apps/termtris/src/vt1200/entry.asm` +- Create: `apps/termtris/src/vt1200/xlib.h` +- Create: `apps/termtris/src/vt1200/xgfix.c` +- Create: `apps/termtris/build.sh` +- Test: `apps/termtris/tests/smoke.c` + `apps/termtris/tests/smoke.sh` + +**Interfaces:** +- Produces: `vt_install_xg(void)` in `xgfix.c`; `xlib.h` with the ROM addresses and `vt_xopen`, `vt_xcreate`, `vt_xselect`, `vt_xmap`, `vt_xsync`, `vt_xflush`. +- Produces: `build.sh` usage `build.sh [extra.obj...]` -> `apps/termtris/build/.coff` and `.map`. + +- [ ] **Step 1: Copy upstream** `termtris` into `apps/termtris/` (keep `COPYING`, `src/game.c`, `pieces.h`, `game.h`, `term.h`, `scoredb.h`; do not copy the DOS/Windows/unix backends). Record the upstream commit hash in `apps/termtris/UPSTREAM`. +- [ ] **Step 2: Write `entry.asm`** — set `A14` to `_cstack`, `A13=A14`; run the cinit loop (length word at `*A9++`, destination long at `*A9++`, copy `length` words), then `CALLA _main`, `RETS`; `.bss _cstack, 32768, 16`. +- [ ] **Step 3: Write `xlib.h`** with the addresses from the spec and TI-ABI prototypes: + `XOpenDisplay 0x3F9C2570`, `XCreateSimpleWindow 0x3FD41B90`, `XSelectInput 0x3FD97D50`, `XMapWindow 0x3FD64F60`, `XSync 0x3FD9F6B0`, `XFlush 0x3F9B1030`, `XNextEvent 0x3F9C1CF0`, `XLookupString 0x3F9A9C30`, `XCreateGC 0x3F997000`, `XSetForeground 0x3FD99A60`, `XFillRectangle 0x3F99E8F0`, `XDrawString 0x3FDA0000`, `sleep 0x3FB33A20`. +- [ ] **Step 4: Write `xgfix.c`** exactly as the spec's fix; return 0 if `proc`/`pp` is outside RAM, else install a static zeroed `u32 xg[256]` and return 1. +- [ ] **Step 5: Write `build.sh`** — extract the r5.01 tools/libs into `apps/termtris/work/`; run `gspac -k`, `gspopt -k -O2`, `gspcg -m`, `gspa`, `gsplnk`; retry each stage up to 5 times and verify its output before proceeding; `gsplnk` command file with `.text/.data/.cinit` at `018A0000h` and `.bss` at `01A00000h`; fail if `termtris.out`/`.map` absent. +- [ ] **Step 6: Smoke test** — `tests/smoke.c` calls `vt_install_xg`, `XOpenDisplay("0::0")`, creates/maps a 480×312 window, selects KeyPress, loops `XNextEvent`, stores `type/keycode` in `.bss` markers, and breaks on `type==2`. `tests/smoke.sh` builds it and runs `dwt/loader/loader.lua` with `LOADER_AT=2500 LOADER_END=3400`, injecting a tablet click at 3000 and `A` at 3100 via a copy of the validated input script. +- [ ] **Step 7: Verify smoke** — the run prints `type=2`, `keycode=194`, `XLookupString` `n=1, ch=0x61, ks=0x0061`. If not, do not proceed. +- [ ] **Step 8: Commit** `apps/termtris` scaffold + smoke. + +### Task 2: Compatibility layer + +**Files:** +- Create: `apps/termtris/src/vt1200/stdio.h` +- Create: `apps/termtris/src/vt1200/inttypes.h` +- Create: `apps/termtris/src/vt1200/compat.c` +- Test: `apps/termtris/tests/fmt.c` + loader run + +**Interfaces:** +- Produces: `int sprintf(char *buf, const char *fmt, ...)` supporting `%d`, `%ld`, `%u`, `%lu`, `%s`, `%c`, `%%`, optional `-`/`0`, decimal width; `int fflush(void *f)` returns 0; `extern void *stdout` is a null pointer; `time_t time(time_t *)` returns `now_ms()/1000`. +- Produces: `uint16_t`/`uint32_t` typedefs in `inttypes.h`. + +- [ ] **Step 1: Write the test** `fmt.c` producing the exact strings used by `game.c`: `"%10ld"` 12345 -> `" 12345"`, `"%2ld"` 7 -> `" 7"`, `"%8ld"` 42 -> `" 42"`, `"%2d. %s %ld"` (1,"ab",99) -> `" 1. ab 99"`, `"%-3s"` "x" -> `"x "`, `"%%"` -> `"%"`. Store a checksum/bytes in markers. +- [ ] **Step 2: Implement `compat.c`** with a manual varargs walk (`char *ap = (char *)&fmt; ap -= 4;` per argument) and a decimal/string formatter; no `stdio.h` from the SDK. +- [ ] **Step 3: Write the shims** so `game.c`'s ``/`` resolve to them; `FILE` is an `int`, `stdout` is 0. +- [ ] **Step 4: Run the loader test** and compare the marker bytes to the expected strings; fix until exact. +- [ ] **Step 5: Commit** compat + test. + +### Task 3: Cell-grid X11 backend + +**Files:** +- Create: `apps/termtris/src/vt1200/x11.c` +- Create: `apps/termtris/src/vt1200/x11.h` +- Test: `apps/termtris/tests/render.c` + screenshot + +**Interfaces:** +- Produces: `int vt_x11_init(void)` — open display, install XG, create 480×312 window at (80,80), select input, map, create GC, `XSync`; returns 0 on success. +- Produces: `void vt_x11_flush(void)` — draw dirty cells. +- Produces: `term_*` callback implementations: `x11_reset`, `x11_clearscr`, `x11_setcursor(row,col)`, `x11_cursor(show)`, `x11_setcolor(fg,bg)`, `x11_ibmchar(c,attr)`; `term_putstr` loops `x11_ibmchar`. +- Produces: `vt_x11_key(int *ks, int *ch)`? No — input is in Task 4; this task only exposes the window/display globals via `x11.h`. + +- [ ] **Step 1: Write `render.c`** that calls `vt_x11_init`, fills an 80×24 cell buffer with a border pattern and `TERMTRIS` text, marks all cells dirty, flushes, and exits via a marker. +- [ ] **Step 2: Implement the cell grid** with `unsigned short ch; unsigned char fg, bg;` per cell; `x11_ibmchar` writes at the cursor and advances; `attr` decode `fg=(attr>>4)&0xf`, `bg=attr&0xf`; mono pixel `(fg==0?0:1)`, `(bg==0?0:1)`. +- [ ] **Step 3: Implement flush** per dirty cell: `XSetForeground(bg)`, `XFillRectangle(x,y,6,13)`, `XSetForeground(fg)`, `XDrawString(x,y+10,ch,1)`; then `XFlush`. +- [ ] **Step 4: Metrics test** — render a row of `'#'` at columns 0..79, a 20×18 playfield outline, and the word `TERMTRIS`; capture the frame at 3300 with `LOADER_SNAP=1`; visually verify 6-px columns and 13-px rows, no shear. +- [ ] **Step 5: Commit** backend + screenshot evidence. + +### Task 4: Input, timer, and main loop + +**Files:** +- Create: `apps/termtris/src/vt1200/main.c` +- Test: `apps/termtris/tests/input.c` + loader run with injected keys + +**Interfaces:** +- Produces: `long now_ms(void)` — 32-bit accumulator over the 16-bit `0x1E93C0`; `void sleep_ms(int ms)` — `sleep(0x3FB33A20)`. +- Produces: `int vt_poll_event(void)` — `XPending` (`0x3FD67620`) > 0; `void vt_handle_event(void)` — `XNextEvent`, `XLookupString`, map to `game_input`. +- Produces: `int main(void)` — `vt_install_xg`, `vt_x11_init`, `init_game`, loop: drain events, `now=now_ms()`, `next=update(now)`, `sleep_ms(min(next-now,10))`; Expose -> `full_redraw` via `game_input('`')`; FocusOut -> `game_input('p')`. + +- [ ] **Step 1: Write `input.c`** that initializes the game/window, then on the first `KeyPress` calls `XLookupString` and stores `keycode`, `keysym`, and the mapped `game_input` byte in markers; inject `A`, Up, Down, Left, Right, `0`, Enter, Backspace. +- [ ] **Step 2: Implement the key map**: letters lower-cased via `XLookupString`; `XK_Left 0xFF51` -> `game_input(0x9b); game_input('A')`; Right 0xFF53 -> `'D'`; Up 0xFF52 -> `'A'?` (use `'w'`); Down 0xFF54 -> `'B'`? Use the game's CSI codes: `0x9b` + `A/B/C/D` for up/down/right/left. Return/Enter -> `'\n'`; Backspace -> `'\b'`; space -> `' '`; Escape -> `27`; digits/letters direct. +- [ ] **Step 3: Implement the timer** and idle sleep; test that the game's `update` is called ~every 50–900 ms by recording a counter in markers. +- [ ] **Step 4: Run with the injected keys**; verify the mapped bytes (A -> 'a', Up -> CSI 'A', etc.) and that the process keeps running. +- [ ] **Step 5: Commit** main/input. + +### Task 5: Game integration, score table, and COFF + +**Files:** +- Modify: `apps/termtris/src/game.c` (only `#ifdef VT1200` includes/compat if needed; game logic unchanged) +- Create: `apps/termtris/src/vt1200/scoredb.c` +- Modify: `apps/termtris/build.sh` to compile `game.c`, `pieces` (header), backend, compat, scoredb, entry +- Test: loader run + screenshot + play + +**Interfaces:** +- Consumes: all Task 2–4 interfaces. +- Produces: `build/termtris.coff`, `build/termtris.map`. +- `scoredb.c` implements `read_scores`, `save_score`, `print_scores` with a static 10-entry list; `username` default `"VT1200"`. + +- [ ] **Step 1: Compile `game.c`** with the 1990 compiler; fix only portability issues (missing `fflush`/`sprintf` via shims, no `unistd`/`fcntl`); do not change game rules. +- [ ] **Step 2: Implement `scoredb.c`** with a static array; `read_scores` returns it; `save_score` sorts/inserts; `print_scores` no-op/fills the list. +- [ ] **Step 3: Build the full COFF**; verify the map: `.text/.data/.cinit` at `018A0000`, `.bss` at `01A00000`, entry `_start`, no undefined symbols. +- [ ] **Step 4: Load and screenshot** at frame 3000 with `dwt/loader/loader.lua`; the Terminal Manager is replaced/covered by the termtris board window. +- [ ] **Step 5: Play** — inject `A/D/S/W`, `0`, `P`, `R`, `H`, `Q`; capture frames showing piece movement, score update, help/score panels; verify quit/restart. +- [ ] **Step 6: Commit** game + scores. + +### Task 6: Documentation and real-unit path + +**Files:** +- Create: `harness/mame/TERMTRIS_REPORT.md` +- Modify: `apps/termtris/README.md` (build/run/real-unit) +- Maybe: `apps/termtris/run.sh` convenience wrapper + +- [ ] **Step 1: Write the report** with the toolchain, the XG root cause/fix, the 6×13/80×24 geometry, the timer, the keyboard map, the loader command, evidence screenshots, and the ROM md5 check. +- [ ] **Step 2: Document the real unit** — same COFF via `dwt/loader/hostload.py`; the XG fix is required; the X server must accept the local connection (default); click the window to focus. +- [ ] **Step 3: Add a `run.sh`** that builds (optional) and runs MAME with the loader and a deterministic input script for CI. +- [ ] **Step 4: Final verification** — `md5sum vt1200_v22.bin` unchanged; no MAME/ROM edits; `git status` only our paths. +- [ ] **Step 5: Commit** docs/report. + +--- + +## Self-review notes + +- Spec coverage: vendoring, build, XG fix, renderer, input, timer, scores, acceptance, docs are each a task. +- The one open implementation choice left to the builder is the exact `sprintf` formatter internals; the required formats are pinned by Task 2's tests. +- The XG block size (1 KiB) is a deliberate value; if the Xlib writes beyond it, enlarge the static buffer. +- The input map uses `XLookupString` for letters/digits and keysyms for arrows/editing keys; the exact game escape codes are the game's existing `game_input` codes. -- 2.47.3 From a8612681ba2d7222768625799a823d674304a82a Mon Sep 17 00:00:00 2001 From: "opencode.energia.arti.ee" Date: Tue, 3 Nov 2026 18:49:44 +0000 Subject: [PATCH 03/18] feat(termtris): vendor source and VT1200 Xlib XG smoke test --- .gitignore | 3 + apps/termtris/COPYING | 674 ++++++++++++++++ apps/termtris/README.md | 150 ++++ apps/termtris/UPSTREAM | 1 + apps/termtris/build.sh | 175 +++++ apps/termtris/build/smoke.coff | Bin 0 -> 1337 bytes apps/termtris/build/smoke.map | 56 ++ apps/termtris/src/game.c | 1114 +++++++++++++++++++++++++++ apps/termtris/src/game.h | 43 ++ apps/termtris/src/pieces.h | 84 ++ apps/termtris/src/scoredb.h | 45 ++ apps/termtris/src/term.h | 62 ++ apps/termtris/src/vt1200/cinit.asm | 4 + apps/termtris/src/vt1200/cinitz.asm | 5 + apps/termtris/src/vt1200/entry.asm | 28 + apps/termtris/src/vt1200/xgfix.c | 30 + apps/termtris/src/vt1200/xlib.h | 51 ++ apps/termtris/tests/smoke.c | 53 ++ apps/termtris/tests/smoke.lua | 355 +++++++++ apps/termtris/tests/smoke.sh | 37 + 20 files changed, 2970 insertions(+) create mode 100644 apps/termtris/COPYING create mode 100644 apps/termtris/README.md create mode 100644 apps/termtris/UPSTREAM create mode 100755 apps/termtris/build.sh create mode 100644 apps/termtris/build/smoke.coff create mode 100644 apps/termtris/build/smoke.map create mode 100644 apps/termtris/src/game.c create mode 100644 apps/termtris/src/game.h create mode 100644 apps/termtris/src/pieces.h create mode 100644 apps/termtris/src/scoredb.h create mode 100644 apps/termtris/src/term.h create mode 100644 apps/termtris/src/vt1200/cinit.asm create mode 100644 apps/termtris/src/vt1200/cinitz.asm create mode 100644 apps/termtris/src/vt1200/entry.asm create mode 100644 apps/termtris/src/vt1200/xgfix.c create mode 100644 apps/termtris/src/vt1200/xlib.h create mode 100644 apps/termtris/tests/smoke.c create mode 100644 apps/termtris/tests/smoke.lua create mode 100755 apps/termtris/tests/smoke.sh diff --git a/.gitignore b/.gitignore index b61ae38..e4c7b3c 100644 --- a/.gitignore +++ b/.gitignore @@ -69,3 +69,6 @@ ghidra/vt1200.lock tools/tms34010-sdk/Docs/ tools/tms34010-sdk/Patents/ tools/tms34010-sdk/1991-07 TIGA Promo Kit and Art Software/ + +# termtris build scratch (TI tools + 2ine libs) +apps/termtris/work/ diff --git a/apps/termtris/COPYING b/apps/termtris/COPYING new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/apps/termtris/COPYING @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/apps/termtris/README.md b/apps/termtris/README.md new file mode 100644 index 0000000..f45463e --- /dev/null +++ b/apps/termtris/README.md @@ -0,0 +1,150 @@ +Termtris - tetris game for terminals +==================================== + +![shots](http://nuclear.mutantstargoat.com/sw/termtris/img/termtris-banner.png) + +About +----- + +Termtris is a text-based tetris game, initially written for UNIX systems and +ANSI/VT100-compatible terminals, but later extended to support more terminal +types, operating systems, and text-mode video hardware. + +I always liked the GameBoy tetris best. So I made termtris as close as possible +to that version gameplay-wise. + +Termtris can run in color or monochrome mode, and can load custom character sets +for graphical blocks, if there is support for that. + +There are two main flavors of termtris which can be compiled from the same +source tree: + + 1. The original **UNIX version** which runs on terminals, and can also be + compiled for windows using a POSIX emulation layer such as *msys2* or + *cygwin*. It has been tested on multiple UNIX systems (GNU/Linux, FreeBSD, IRIX, + Solaris, MacOSX), and multiple terminals (VT52, VT100, VT220, VT420, + MS 6102), and terminal emulators (xterm, linux console, kermit, iTerm2). + + 2. The **DOS version** which uses the PC BIOS to access display and keyboard, + but can also optionally work with the same terminals supported by the UNIX + version. + +### Links + - website: http://nuclear.mutantstargoat.com/sw/termtris + - source repository: https://github.com/jtsiomb/termtris + + +Controls +-------- + - `A` or *left arrow* moves the block left. + - `D` or *right arrow* moves the block right. + - `S` or *down arrow* drops the block faster. + - `W`, *up arrow*, or *space* rotates the block. + - *enter*, *tab*, or `0` drops the block immediately. + - `P` pauses and unpauses the game. + - *backspace*, or *delete* starts a new game. + - `Q` or hitting escape twice, quits immediately. + - `H` shows or hides the help panel which lists these keybindings. + - `R` shows or hides the high score table. + +Additionally on GNU/Linux systems any joystick can be used to control the game. +The first available joystick is used automatically, or you can specify a +joystick device with the `-j` commandline option. If you'd rather disable +joystick control, use the option `-j /dev/null`. + +When using a joystick, even axes are mapped to left/right, Odd axes are mapped +to up/down, the first four buttons rotate, and the rest of the buttons pause +and unpause. + +There is no way to remap the controls without changing the source code at this +time. + + +Custom character set +-------------------- +For terminals with support for custom loadable character sets, you can instruct +termtris to use graphical blocks instead of the bracket characters with the `-g` +command-line argument. If `TERM` is set to a known supported terminal, graphical +blocks are enabled automatically, use the `-T` option to disable and inhibit +auto-detection. + +Different terminals have different capabilities when it comes to soft character +sets. Termtris will try to detect the terminal type and choose the correct soft +font to load, but if it fails or you want to force a different terminal type, +make sure to define the `TERM` environent variable. If `TERM` is undefined, or +an unknown or unsupported terminal type, termtris will try to interrogate the +terminal itself about its capabilities. + +The DOS version of termtris will automatically enable graphical blocks when it +detects an EGA or VGA/SVGA graphics card. Use `-g` or `-T` to enable or disable +it explicitly. + + +License +------- +Copyright (C) 2019-2023 John Tsiombikas + +This program is free software. Feel free to use, modify, and/or redistribute it +under the terms of the GNU General Public License version 3, or at your option, +any later version published by the Free Software Foundation. See COPYING for +details. + + +Download +-------- +### Source code +Current release (1.9): http://nuclear.mutantstargoat.com/sw/termtris/termtris-1.9.tar.gz + +Releases are also mirrored on github: https://github.com/jtsiomb/termtris/releases + +Alternatively you may clone the latest version of the source code from the git +repository: + + git clone https://github.com/jtsiomb/termtris + +### Pre-compiled +Building from source is highly recommended. If however you want to try a +pre-compiled binary, you might be able to find something for your system in: +http://nuclear.mutantstargoat.com/sw/termtris/bin +Not all releases are compiled for all platforms. Any binaries available here are +provided purely for convenience. Do not open bug reports about a pre-compiled +binary not running, or misbehaving on your system; compile it yourself first. + +Another source of binaries, is the github automated builds. Head over to the +"actions" page (https://github.com/jtsiomb/termtris/actions), choose the latest +successful build for your platform, and download the binary from the "artifacts" +section at the bottom. + +Pre-compiled binaries might also be available in your package management system of +choice, but please do not submit any bug reports without first trying to build +termtris yourself from the latest git source code. + - Debian/Ubuntu/Mint: `apt-get install termtris` + +The latest pre-compiled DOS binary (currently 1.9) is always available at: +http://nuclear.mutantstargoat.com/sw/termtris/termtris.com + +Build (UNIX) +------------ +There are no external dependencies. Simply type `make` to build, and `make +install` if you wish to install termtris. + +Default installation prefix is `/usr/local`. Change the first line of the +`Makefile` if you'd rather install somewhere else, or simply invoke the install +rule like so: + + make PREFIX=/some/other/prefix install + +If you don't have GNU make and GCC installed, or you'd rather use the default +make utility and C compiler on your UNIX system, you can use `Makefile.sgi`: + + make -f Makefile.sgi + +(originally included for building on SGI IRIX with MIPSPro and the native make). + + +Build (DOS) +----------- +To build the DOS version of termtris you'll need some version of the Watcom or +OpenWatcom C compiler, and the NASM assembler: + + make -f Makefile.wat diff --git a/apps/termtris/UPSTREAM b/apps/termtris/UPSTREAM new file mode 100644 index 0000000..442ef4f --- /dev/null +++ b/apps/termtris/UPSTREAM @@ -0,0 +1 @@ +7c5435dd4e93ffc10f3b5c68fe1ae9718a365258 diff --git a/apps/termtris/build.sh b/apps/termtris/build.sh new file mode 100755 index 0000000..601be0b --- /dev/null +++ b/apps/termtris/build.sh @@ -0,0 +1,175 @@ +#!/bin/sh +# build.sh -- build a VT1200 COFF with TI TMS34010 r5.01 under 2ine. +# +# apps/termtris/build.sh SOURCE.c [SOURCE.c ...] [EXTRA.obj ...] +# +# The first .c is the main program; the output is build/
.coff. entry.asm +# and cinit.asm are always assembled and linked. The 2ine loader is outside the +# repo: point NINE at its build directory (default /home/arti/os2-work/2ine/build). +set -eu + +DIR=$(cd "$(dirname "$0")" && pwd) +ROOT=$(cd "$DIR/../.." && pwd) +WORK="$DIR/work" +OUT="$DIR/build" +NINE="${NINE:-/home/arti/os2-work/2ine/build}" +LX="$NINE/lx_loader" +SDK="$ROOT/tools/tms34010-sdk/1990-11 TMS340 Family SDK" +Z1="$SDK/1990-11-19 TMS340 Family Code Generation Tools (1 of 2) r5.01 rev2564062-1641.zip" +Z2="$SDK/1990-11-19 TMS340 Family Code Generation Tools (2 of 2) r5.01 rev2564062-1641.zip" + +if [ ! -x "$LX" ]; then + echo "build.sh: no 2ine lx_loader at $LX (set NINE=...)" >&2 + exit 1 +fi +if [ ! -f "$Z1" ] || [ ! -f "$Z2" ]; then + echo "build.sh: r5.01 SDK zips not found under $SDK" >&2 + exit 1 +fi + +mkdir -p "$WORK" "$OUT" +if [ ! -x "$WORK/gspac.exe" ]; then + unzip -oq "$Z1" -d "$WORK" + unzip -oq "$Z2" -d "$WORK" +fi +for so in "$NINE"/lib*.so; do + [ -e "$so" ] && ln -sf "$so" "$WORK/" +done +cp "$DIR/src/vt1200/"*.h "$WORK/" 2>/dev/null || true +cp "$DIR/src/"*.h "$WORK/" 2>/dev/null || true +cp "$DIR/src/vt1200/entry.asm" "$DIR/src/vt1200/cinit.asm" "$DIR/src/vt1200/cinitz.asm" "$WORK/" + +# Collect objects. C sources are copied under their uppercase basename because +# the OS/2 tools are case-sensitive on this host. +OBJS="" +MAIN="" +for arg in "$@"; do + case "$arg" in + *.c) + base=$(basename "$arg" .c | tr 'a-z' 'A-Z') + [ -n "$MAIN" ] || MAIN="$base" + cp "$arg" "$WORK/$base.C" + OBJS="$OBJS $base" + ;; + *.obj) + cp "$arg" "$WORK/" + OBJS="$OBJS $(basename "$arg")" + ;; + *.asm) + base=$(basename "$arg" .asm | tr 'a-z' 'A-Z') + cp "$arg" "$WORK/$base.ASM" + OBJS="$OBJS $base" + ;; + *) + echo "build.sh: unsupported input: $arg" >&2 + exit 1 + ;; + esac +done +if [ -z "$MAIN" ]; then + echo "build.sh: give at least one .c source" >&2 + exit 1 +fi + +# The 2ine-hosted tools frequently crash during teardown after doing their work +# (exit 139 / 134). Treat an output file as success, and retry a missing one. +run() { + _out=$1 + shift + i=0 + while [ "$i" -lt 5 ]; do + ( cd "$WORK" && "$LX" "$@" ) >/dev/null 2>&1 || true + if [ -f "$WORK/$_out" ]; then + return 0 + fi + i=$((i + 1)) + done + echo "build.sh: stage failed to produce $WORK/$_out: $*" >&2 + return 1 +} + +compile_c() { + base=$1 + rm -f "$WORK/$base.IF" "$WORK/$base.OPT" "$WORK/$base.ASM" "$WORK/$base.obj" + run "$base.IF" ./gspac.exe -k "$base.C" "$base.IF" + run "$base.OPT" ./gspopt.exe -k -O2 "$base.IF" "$base.OPT" + run "$base.ASM" ./gspcg.exe -m "$base.OPT" "$base.ASM" "$base.TMP" + run "$base.obj" ./gspa.exe "$base.ASM" +} + +compile_asm() { + base=$1 + rm -f "$WORK/$base.obj" + run "$base.obj" ./gspa.exe "$base.ASM" +} + +for base in $OBJS; do + case " $* " in + *" $base "*) ;; # handled below by extension scan + esac +done + +# Compile every collected object exactly once, in the order given. +for arg in "$@"; do + case "$arg" in + *.c) compile_c "$(basename "$arg" .c | tr 'a-z' 'A-Z')" ;; + *.asm) compile_asm "$(basename "$arg" .asm | tr 'a-z' 'A-Z')" ;; + esac +done +compile_asm ENTRY +compile_asm CINIT +compile_asm CINITZ + +OUTNAME=$(printf '%s' "$MAIN" | tr 'A-Z' 'a-z') +CMD="$WORK/$MAIN.CMD" +{ + printf '%s\n' '-a' + printf '%s\n' "-o $MAIN.out" + printf '%s\n' "-m $MAIN.map" + printf '%s\n' '-e _start' + printf '%s\n' 'CINIT.obj' + printf '%s\n' 'ENTRY.obj' + for arg in "$@"; do + case "$arg" in + *.c) printf '%s\n' "$(basename "$arg" .c | tr 'a-z' 'A-Z').obj" ;; + *.asm) printf '%s\n' "$(basename "$arg" .asm | tr 'a-z' 'A-Z').obj" ;; + *.obj) printf '%s\n' "$(basename "$arg")" ;; + esac + done + printf '%s\n' '-l rts.lib' + printf '%s\n' 'MEMORY' + printf '%s\n' '{' + printf '%s\n' ' prog: origin = 018A0000h, length = 20000h' + printf '%s\n' ' ram: origin = 01A00000h, length = 40000h' + printf '%s\n' '}' + printf '%s\n' 'SECTIONS' + printf '%s\n' '{' + printf '%s\n' ' .text: {} > prog' + printf '%s\n' ' .data: {} > prog' + printf '%s\n' ' .cinit: {} > prog' + printf '%s\n' ' .bss: {} > ram' + printf '%s\n' '}' +} > "$CMD" +sed -i 's/$/\r/' "$CMD" + +rm -f "$WORK/$MAIN.out" "$WORK/$MAIN.map" +run "$MAIN.out" ./gsplnk.exe "$MAIN.CMD" +if [ ! -f "$WORK/$MAIN.map" ]; then + echo "build.sh: no link map" >&2 + exit 1 +fi + +# An empty .cinit makes the linker's `cinit` symbol point at .text; the entry's +# cinit walker would then treat an instruction as a record count. Relink with +# a one-word terminator when the map says the section is empty. +CSIZE=$(awk '$1 == ".cinit" {print $4; exit}' "$WORK/$MAIN.map") +if [ "$CSIZE" = "00000000" ]; then + sed -i 's/CINIT\.obj/CINITZ.obj/' "$CMD" + rm -f "$WORK/$MAIN.out" "$WORK/$MAIN.map" + run "$MAIN.out" ./gsplnk.exe "$MAIN.CMD" +fi + +cp "$WORK/$MAIN.out" "$OUT/$OUTNAME.coff" +sed 's/\r$//' "$WORK/$MAIN.map" > "$OUT/$OUTNAME.map" +echo "build.sh: $OUT/$OUTNAME.coff ($(wc -c < "$OUT/$OUTNAME.coff") bytes)" +sed -n '1,40p' "$OUT/$OUTNAME.map" diff --git a/apps/termtris/build/smoke.coff b/apps/termtris/build/smoke.coff new file mode 100644 index 0000000000000000000000000000000000000000..f03990d2e3d56267cbc59eada6ea8f948f773802 GIT binary patch literal 1337 zcmbVML1+^}6#cWwl1$gsfF2_4Ay{i`gwUfFF^eQvsY3&L@w7>8tkD{ziKU1_NDF~@ zXxSp7R1Bo02)dOP>A}k)h*BvL5uvvpEVWUC2nt?W|LkVTw$(!&nBo6FfByUb&zl(s zC$v1QT)8O#90q`9Xd!O$lgJ)a_?Y2Kfmkz`NepMWsb=GT?+m@Z_?-a3i^)_H9c51e zV-<)Ow}%nKK4esaz3H@RDZL)_X793PyE(pm&O!NloUvej?r6Bmsq0|g9?Rgk*v|~9(JyhQ#>6~fw&ngjzGEe znF4B!6N=}0>tsk`{j4(Qhklk`ESJ{8RkhWpsS%v7=rN+HujvY@q8tTH)zrj7)~UUh zA%&;Mn?8gn3(U)K9#5{)y>JkHf)ji%dV4ukE}3$y=OfC~VNK;b&wee3&ayq2#T?C(-%@+LOh_U z(@gYoOssJ70cLY(UWYDmX7tnC3QWDg=8;tzy%6c1n)4HPMAR?)9D0|J=j#kBvc&fBD$h$Vuoh__!5pXFgHPh)1lC|I8;_k6=m`7H6Va z`wDi1S6Cmr!b1MmYIoHmn39FXS#4r@u?1V3y(=ud#~PM@J65gD#;fOj7C=d7;)BMA zVWOkuT0EJ;J*#%ZGI*7Ts#SrO#8B^zOK3r^b1WXq?2lE2u!BDWBx-*XfA_pT^s)UK lyu0YL!(#t|OsqtzkHg{LXQA&yMH$K{$rP3|FrW +ENTRY POINT SYMBOL: "_start" address: 018a0000 + + +MEMORY CONFIGURATION + + name origin length attributes + -------- -------- --------- ---------- + prog 018a0000 000020000 RWIX + ram 01a00000 000040000 RWIX + + +SECTION ALLOCATION MAP + + output attributes/ +section page origin length input sections +-------- ---- ---------- ---------- ---------------- +.text 0 018a0000 00000d40 + 018a0000 00000150 ENTRY.obj (.text) + 018a0150 000009e0 SMOKE.obj (.text) + 018a0b30 00000210 XGFIX.obj (.text) + +.cinit 0 018a0d40 00000010 + 018a0d40 00000010 CINITZ.obj (.cinit) + +.bss 0 01a00000 0000a700 UNINITIALIZED + 01a00000 00008000 ENTRY.obj (.bss) + 01a08000 00000700 SMOKE.obj (.bss) + 01a08700 00002000 XGFIX.obj (.bss) + +.data 0 018a0000 00000000 UNINITIALIZED + + +GLOBAL SYMBOLS + +address name address name +-------- ---- -------- ---- +01a00000 .bss 018a0000 .text +018a0000 .data 018a0000 .data +018a0000 .text 018a0000 edata +01a08400 _evbuf 018a0000 _start +018a0150 _main 018a0150 _main +01a08000 _mark 018a0b30 _vt_install_xg +018a0000 _start 018a0d40 cinit +018a0b30 _vt_install_xg 018a0d40 etext +018a0d40 cinit 01a00000 .bss +018a0000 edata 01a08000 _mark +01a0a700 end 01a08400 _evbuf +018a0d40 etext 01a0a700 end + +[12 symbols] diff --git a/apps/termtris/src/game.c b/apps/termtris/src/game.c new file mode 100644 index 0000000..6209491 --- /dev/null +++ b/apps/termtris/src/game.c @@ -0,0 +1,1114 @@ +/* +Termtris - a tetris game for ANSI/VT100 terminals +Copyright (C) 2019-2023 John Tsiombikas + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ +#include +#include +#include +#include +#include +#include "game.h" +#include "pieces.h" +#include "term.h" +#include "scoredb.h" + + +int quit; +long tick_interval; +int use_bell; +int monochrome; +int use_gfxchar; +int onlyascii; +int blackpf; +int rotstep = 1; +int term_width, term_height; + + +enum { ERASE_PIECE, DRAW_PIECE }; + +/* dimensions of the whole screen */ +#define SCR_ROWS 20 +#define SCR_COLS 20 + +/* dimensions of the playfield */ +#define PF_ROWS 18 +#define PF_COLS 10 +/* offset of the playfield from the left side of the screen */ +#define PF_XOFFS 2 +#define PF_YOFFS 0 + +#define CHAR(c, fg, bg) \ + ((uint16_t)(c) | ((uint16_t)(fg) << 12) | ((uint16_t)(bg) << 8)) + +int scr[SCR_COLS * SCR_ROWS]; + +static void update_cur_piece(void); +static void addscore(int nlines); +static void print_numbers(void); +static void print_help(void); +static void print_slist(void); +static void full_redraw(void); +static int spawn(void); +static int collision(int piece, const int *pos); +static void stick(int piece, const int *pos); +static void erase_completed(void); +static void draw_piece(int piece, const int *pos, int rot, int mode); +static void clear(void); +static void drawbg(void); +static void drawpf(int start_row); +static void draw_line(int row, int blink); +static void wrtile(int tileid); + + +static int pos[2], next_pos[2]; +static int cur_piece, next_piece, prev_piece; +static int cur_rot, prev_rot; +static int complines[4]; +static int num_complines; +static int gameover; +static int pause; +static int just_spawned; +static int show_help, show_highscores; + +static struct score_entry *scores; +static struct score_entry cur_score; + +static int term_xoffs = 20, term_yoffs = 0; /* TODO detect terminal size to set offsets */ + +static const int preview_pos[] = {13, 13}; + +enum { + TILE_BLACK, + TILE_PF, + TILE_PFSEP, + TILE_GAMEOVER, + TILE_LPIECE, + TILE_JPIECE, + TILE_IPIECE, + TILE_OPIECE, + TILE_ZPIECE, + TILE_SPIECE, + TILE_TPIECE, + TILE_FRM_TL, + TILE_FRM_TR, + TILE_FRM_BL, + TILE_FRM_BR, + TILE_FRM_LTEE, + TILE_FRM_RTEE, + TILE_FRM_HLINE, + TILE_FRM_LVLINE, + TILE_FRM_RVLINE +}; +#define FIRST_PIECE_TILE TILE_LPIECE + +static uint16_t tiles[][2] = { + { CHAR(' ', BLACK, BLACK), CHAR(' ', BLACK, BLACK) }, /* black tile */ + { CHAR(' ', WHITE, WHITE), CHAR(' ', WHITE, WHITE) }, /* playfield background */ + { CHAR(G_CHECKER, WHITE, BLACK), CHAR(G_CHECKER, WHITE, BLACK) }, /* well separator */ + { CHAR(G_CROSS, RED, BLACK), CHAR(G_CROSS, RED, BLACK) }, /* gameover fill */ + { CHAR('[', WHITE, YELLOW), CHAR(']', WHITE, YELLOW) }, /* L */ + { CHAR('[', WHITE, GREEN), CHAR(']', WHITE, GREEN) }, /* J */ + { CHAR('[', WHITE, CYAN), CHAR(']', WHITE, CYAN) }, /* I */ + { CHAR('[', WHITE, BLUE), CHAR(']', WHITE, BLUE) }, /* O */ + { CHAR('[', WHITE, RED), CHAR(']', WHITE, RED) }, /* Z */ + { CHAR('[', WHITE, MAGENTA), CHAR(']', WHITE, MAGENTA) }, /* S */ + { CHAR('[', WHITE, BLACK), CHAR(']', WHITE, BLACK) }, /* T */ + /* frame tiles */ + { CHAR(G_UL_CORNER, WHITE, BLACK), CHAR(G_HLINE, WHITE, BLACK) }, /* top-left corner */ + { CHAR(G_HLINE, WHITE, BLACK), CHAR(G_UR_CORNER, WHITE, BLACK) }, /* top-right corner */ + { CHAR(G_LL_CORNER, WHITE, BLACK), CHAR(G_HLINE, WHITE, BLACK) }, /* bottom-left corner */ + { CHAR(G_HLINE, WHITE, BLACK), CHAR(G_LR_CORNER, WHITE, BLACK) }, /* bottom-right corner */ + { CHAR(G_L_TEE, WHITE, BLACK), CHAR(G_HLINE, WHITE, BLACK) }, /* left-T */ + { CHAR(G_HLINE, WHITE, BLACK), CHAR(G_R_TEE, WHITE, BLACK) }, /* right-T */ + { CHAR(G_HLINE, WHITE, BLACK), CHAR(G_HLINE, WHITE, BLACK) }, /* horizontal line */ + { CHAR(G_VLINE, WHITE, BLACK), CHAR(' ', WHITE, BLACK) }, /* left vertical line */ + { CHAR(' ', WHITE, BLACK), CHAR(G_VLINE, WHITE, BLACK) } /* right vertical line */ +}; + +static const char *bgdata[SCR_ROWS] = { + " #..........#{-----}", + " #..........#(.....)", + " #..........#[-----]", + " #..........#.......", + " #..........# ", + " #..........#{-----}", + " #..........#(.....)", + " #..........#(.....)", + " #..........#>-----<", + " #..........#(.....)", + " #..........#(.....)", + " #..........#[-----]", + " #..........# {----}", + " #..........# (....)", + " #..........# (....)", + " #..........# (....)", + " #..........# (....)", + " #..........# [----]", + " ############ ", + " " +}; + +#define NUM_LEVELS 21 +static const long level_speed[NUM_LEVELS] = { + 887, 820, 753, 686, 619, 552, 469, 368, 285, 184, + 167, 151, 134, 117, 107, 98, 88, 79, 69, 60, 50 +}; + +static const char *helpstr[] = { + " Toggle (H)elp", + "", + "left,right,down/asd:", + " move piece", + " up/w/space: rotate", + "enter/tab/0: drop", + " backsp/del: restart", + " P: pause", + " Q: quit", + "", + " High scores:", + " termtris -s" +}; + +int init_game(void) +{ + int i, j; + int *row = scr; + + scores = read_scores(0, 10); + memset(&cur_score, 0, sizeof cur_score); + + srand(time(0)); + + pause = 0; + gameover = 0; + num_complines = 0; + tick_interval = level_speed[0]; + cur_piece = -1; + prev_piece = 0; + next_piece = rand() % NUM_PIECES; + + term_init(); + term_setcolor(WHITE, BLACK); + term_clearscr(); + term_cursor(0); + + if(blackpf) { + tiles[TILE_PF][0] = CHAR(' ', BLACK, BLACK); + tiles[TILE_PF][1] = CHAR(' ', BLACK, BLACK); + tiles[TILE_TPIECE][0] = CHAR('[', BLACK, WHITE); + tiles[TILE_TPIECE][1] = CHAR(']', BLACK, WHITE); + } + + /* fill the screen buffer, and draw */ + for(i=0; i': + tile = TILE_FRM_LTEE; + break; + case '<': + tile = TILE_FRM_RTEE; + break; + case '-': + tile = TILE_FRM_HLINE; + break; + case '(': + tile = TILE_FRM_LVLINE; + break; + case ')': + tile = TILE_FRM_RVLINE; + break; + case ' ': + default: + tile = TILE_BLACK; + } + row[j] = tile; + } + row += SCR_COLS; + } + + if(onlyascii) { + for(i=0; i= 0) { + ptr = scr + (row + PF_YOFFS) * SCR_COLS + PF_XOFFS; + for(i=0; i> 8; + + if(blink > 6) { + erase_completed(); + wait_display(); + num_complines = 0; + return 0; + } + + for(i=0; i= tick_interval) { + if(cur_piece >= 0) { + just_spawned = 0; + next_pos[0] = pos[0] + 1; + if(collision(cur_piece, next_pos)) { + next_pos[0] = pos[0]; + stick(cur_piece, next_pos); + return 0; + } + } else { + /* respawn */ + if(spawn() == -1) { + gameover = 1; + return 0; + } + } + + dt -= tick_interval; + prev_tick = msec; + } + + update_cur_piece(); + return tick_interval - dt; +} + +#define MAX_TILE_OPS 32 +struct tileop { + int op; + int x, y; +}; + +static int diff_piece(int pp, const int *ppos, int prot, int np, const int *npos, int nrot, struct tileop *ops) +{ + int i, j, num, inval = 0; + unsigned char *p = pieces[pp][prot]; + struct tileop tmpop, *op = ops; + + for(i=0; i<4; i++) { + op->op = ERASE_PIECE; + op->x = ppos[1] + BLKX(*p); + op->y = ppos[0] + BLKY(*p); + if(op->y >= 0) { + op++; + } + p++; + } + p = pieces[np][nrot]; + for(i=0; i<4; i++) { + op->op = DRAW_PIECE; + op->x = npos[1] + BLKX(*p); + op->y = npos[0] + BLKY(*p); + if(op->y >= 0) { + op++; + } + p++; + } + + /* find double-changes to the same tile */ + num = op - ops; + for(i=0; iop == -1) { + op++; + continue; + } + + /* if the new op is not under the cursor, handle cursor movement */ + if(op->x != x || op->y != y) { + term_setcursor(term_yoffs + op->y + PF_YOFFS, term_xoffs + + (op->x + PF_XOFFS) * 2); + x = op->x; + y = op->y; + } + tile = op->op == ERASE_PIECE ? TILE_PF : FIRST_PIECE_TILE + piece; + wrtile(tile); + x++; + op++; + } +} + +static void update_cur_piece(void) +{ + int numops; + struct tileop ops[MAX_TILE_OPS]; + + if(cur_piece < 0) return; + + if((numops = diff_piece(cur_piece, pos, prev_rot, cur_piece, next_pos, cur_rot, ops)) > 0) { + draw_tileops(cur_piece, ops, numops); + + /* for terminals which can't hide the cursor, move it out of the way */ + term_setcursor(0, 0); + fflush(stdout); + + memcpy(pos, next_pos, sizeof pos); + prev_rot = cur_rot; + } +} + + +static void addscore(int nlines) +{ + static const int stab[] = {40, 100, 300, 1200}; /* bonus per line completed */ + + cur_score.score += stab[nlines - 1] * (cur_score.level + 1); + cur_score.lines += nlines; + + cur_score.level = cur_score.lines / 10; + if(cur_score.level > NUM_LEVELS - 1) { + cur_score.level = NUM_LEVELS - 1; + } + + tick_interval = level_speed[cur_score.level]; + + print_numbers(); + + if(show_highscores) { + print_slist(); + } +} + +static void print_numbers(void) +{ + char buf[16]; + + term_setcolor(BLACK, WHITE); + + term_setcursor(term_yoffs + 3, term_xoffs + 14 * 2); + sprintf(buf, "%10ld", cur_score.score); + term_putstr(buf, 7); + + term_setcursor(term_yoffs + 7, term_xoffs + 17 * 2); + sprintf(buf, "%2ld", cur_score.level); + term_putstr(buf, 7); + + term_setcursor(term_yoffs + 10, term_xoffs + 14 * 2); + sprintf(buf, "%8ld", cur_score.lines); + term_putstr(buf, 7); +} + +static void print_help(void) +{ + int i; + + for(i=0; i 63) len = 63; + + if(len <= 0) { + buf[0] = 0; + return buf; + } + + if(strlen(s) <= len) { + strcpy(buf, s); + return buf; + } + + if(len < 3) { + memset(buf, ' ', len); + buf[len] = 0; + return buf; + } + + for(i=0; i 127) maxlen = 127; + + term_setcursor(1, x); + if(maxlen < 15) { + term_putstr("Sco(r)es", 0x70); + } else { + term_putstr("Toggle Sco(r)es", 0x70); + } + + sc = scores; + for(i=0; i<10; i++) { + term_setcursor(i + 3, x); + if(!show_highscores) { +fillblank: memset(fmtbuf, ' ', maxlen); + fmtbuf[maxlen] = 0; + term_putstr(fmtbuf, 0x70); + if(sc) sc = sc->next; + continue; + } + + color = 0x70; + if(!printed_cur) { + if(!sc || cur_score.score > sc->score) { + cur_score.next = sc; + sc = &cur_score; + printed_cur = 1; + color |= 0x80; + } + } else { + if(!sc) goto fillblank; + } + + user = (sc->user && *sc->user) ? sc->user : "***"; + + sclen = sprintf(fmtbuf, "%ld", sc->score); + if(sclen > maxlen - 5) goto fillblank; + namelen = maxlen - sclen - 5; + + if(strlen(user) <= namelen) { + sprintf(fmt, "%%2d. %%-%ds %%ld", namelen); + len = sprintf(fmtbuf, fmt, i + 1, user, sc->score); + } else { + len = sprintf(fmtbuf, "%2d. %s %ld", i + 1, clampstr(user, namelen), sc->score); + } + if(len < maxlen) { + memset(fmtbuf + len, ' ', maxlen - len); + } + fmtbuf[maxlen] = 0; + sc = sc->next; + + term_putstr(fmtbuf, color); + } +} + +#define C0 0x9b +#define SS3 0x8f + +static void runesc(int csi, char *buf) +{ + if(csi != C0) return; + + if(buf[1] == 0) { + switch(buf[0]) { + case 'A': + game_input('w'); /* up */ + break; + case 'B': + game_input('s'); /* down */ + break; + case 'C': + game_input('d'); /* right */ + break; + case 'D': + game_input('a'); /* left */ + break; + default: + break; + } + return; + } + + if(strcmp(buf, "28~") == 0) { + /* help key */ + game_input('h'); + } +} + +void game_input(int c) +{ + static int esc, csi; + static int esctop; + static char escbuf[64]; + + if(esc) { + esc = 0; + if(c == 27) { + quit = 1; + return; + } + + switch(c) { + case '[': + csi = C0; + return; + case 'O': + csi = SS3; + return; + default: + break; + } + } + + if(csi) { + if(c < 0x20 || c >= 0x80) { + csi = 0; + esctop = 0; + } + + escbuf[esctop++] = c; + + if(c >= 0x40) { + int prevcsi = csi; + escbuf[esctop] = 0; + csi = 0; + esctop = 0; + runesc(prevcsi, escbuf); + } + return; + } + + switch(c) { + case 27: + esc = 1; + break; + + case C0: + esc = 1; + csi = C0; + break; + + case 'q': + quit = 1; + break; + + case 'a': + if(!pause) { + next_pos[1] = pos[1] - 1; + if(collision(cur_piece, next_pos)) { + next_pos[1] = pos[1]; + } + } + break; + + case 'd': + if(!pause) { + next_pos[1] = pos[1] + 1; + if(collision(cur_piece, next_pos)) { + next_pos[1] = pos[1]; + } + } + break; + + case 'w': + case ' ': + if(!pause) { + prev_rot = cur_rot; + cur_rot = (cur_rot + rotstep) & 3; + if(collision(cur_piece, next_pos)) { + cur_rot = prev_rot; + } + } + break; + + case 's': + /* ignore drops until the first update after a spawn */ + if(cur_piece >= 0 && !just_spawned && !pause && !gameover) { + next_pos[0] = pos[0] + 1; + if(collision(cur_piece, next_pos)) { + next_pos[0] = pos[0]; + update_cur_piece(); + stick(cur_piece, next_pos); /* stick immediately */ + } + } + break; + + case '\t': + case '\n': + case '\r': + case '0': + if(!pause && !gameover && cur_piece >= 0) { + next_pos[0] = pos[0] + 1; + while(!collision(cur_piece, next_pos)) { + next_pos[0]++; + } + next_pos[0]--; + update_cur_piece(); + stick(cur_piece, next_pos); /* stick immediately */ + } + break; + + case 'p': + if(gameover) { + init_game(); + } else { + pause ^= 1; + } + break; + + case '\b': + case 127: + init_game(); + break; + + case 'h': + show_help ^= 1; + print_help(); + fflush(stdout); + break; + + case 'r': + show_highscores ^= 1; + print_slist(); + fflush(stdout); + break; + + case '`': + full_redraw(); + break; + + default: + /*fprintf(stderr, "unhandled input: %x\n", c);*/ + break; + } +} + +static void full_redraw(void) +{ + clear(); + print_help(); + drawbg(); + print_slist(); + print_numbers(); + drawpf(0); + if(!gameover) { + draw_piece(next_piece, preview_pos, 0, DRAW_PIECE); + draw_piece(cur_piece, next_pos, cur_rot, DRAW_PIECE); + term_setcursor(0, 0); + fflush(stdout); + } + wait_display(); +} + +static int spawn(void) +{ + int r, tries = 2; + int numops; + struct tileop ops[MAX_TILE_OPS]; + + do { + r = rand() % NUM_PIECES; + } while(tries-- > 0 && (r | prev_piece | next_piece) == prev_piece); + + if((numops = diff_piece(next_piece, preview_pos, 0, r, preview_pos, 0, ops)) > 0) { + draw_tileops(r, ops, numops); + term_setcursor(0, 0); + fflush(stdout); + } + + cur_piece = next_piece; + next_piece = r; + + prev_rot = cur_rot = 0; + pos[0] = piece_spawnpos[cur_piece][0]; + next_pos[0] = pos[0] + 1; + pos[1] = next_pos[1] = PF_COLS / 2 + piece_spawnpos[cur_piece][1]; + + if(collision(cur_piece, next_pos)) { + return -1; + } + + just_spawned = 1; + return 0; +} + +static int collision(int piece, const int *pos) +{ + int i; + unsigned char *p = pieces[piece][cur_rot]; + + for(i=0; i<4; i++) { + int x = PF_XOFFS + pos[1] + BLKX(*p); + int y = PF_YOFFS + pos[0] + BLKY(*p); + p++; + + if(y < 0) continue; + + if(scr[y * SCR_COLS + x] != TILE_PF) return 1; + } + + return 0; +} + +static void stick(int piece, const int *pos) +{ + int i, j, nblank; + int *pfline; + unsigned char *p = pieces[piece][cur_rot]; + + num_complines = 0; + prev_piece = cur_piece; /* used by the spawn routine */ + cur_piece = -1; + + for(i=0; i<4; i++) { + int x = pos[1] + BLKX(*p); + int y = pos[0] + BLKY(*p); + p++; + + pfline = scr + (y + PF_YOFFS) * SCR_COLS + PF_XOFFS; + pfline[x] = piece + FIRST_PIECE_TILE; + + nblank = 0; + for(j=0; j complines[i]) { + int tmp = complines[j]; + complines[j] = complines[i]; + complines[i] = tmp; + } + } + } + + toprow = srow = drow = PF_ROWS - 1; + dptr = pfstart + drow * SCR_COLS; + + for(i=0; i= sizeof tiles / sizeof *tiles) { + return; + } + + for(i=0; i<2; i++) { + uint16_t c = tiles[tileid][i]; + unsigned char cc = c & 0xff; + unsigned char ca = c >> 8; + + if(use_gfxchar && (cc == '[' || cc == ']')) { + ca <<= 4; /* for gfx blocks bg->fg and bg=0 */ +#if defined(MSDOS) || defined(__COM__) + if(term_type == TERM_PCBIOS) { + if(!ca) ca = 0x80; /* special case for T which has black bg */ + } else +#endif + if(!ca) ca = 0x70; /* special case for T which has black bg */ + } + + term_ibmchar(cc, ca); + } +} diff --git a/apps/termtris/src/game.h b/apps/termtris/src/game.h new file mode 100644 index 0000000..30139e7 --- /dev/null +++ b/apps/termtris/src/game.h @@ -0,0 +1,43 @@ +/* +Termtris - a tetris game for ANSI/VT100 terminals +Copyright (C) 2019-2023 John Tsiombikas + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ +#ifndef GAME_H_ +#define GAME_H_ + +extern int quit; +extern long tick_interval; +extern int use_bell; +extern int monochrome; +extern int use_gfxchar; +extern int onlyascii; +extern int blackpf; +extern int rotstep; + +extern int term_width, term_height; + +int init_game(void); +void cleanup_game(void); + +long update(long msec); +void game_input(int c); + +/* wait for any pending drawing to be completed before proceeding + * implemented in main.c + */ +void wait_display(void); + +#endif /* GAME_H_ */ diff --git a/apps/termtris/src/pieces.h b/apps/termtris/src/pieces.h new file mode 100644 index 0000000..2fec2f0 --- /dev/null +++ b/apps/termtris/src/pieces.h @@ -0,0 +1,84 @@ +/* +Termtris - a tetris game for ANSI/VT100 terminals +Copyright (C) 2019-2022 John Tsiombikas + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ +#ifndef PIECES_H_ +#define PIECES_H_ + +#define BLK(x, y) ((x) | ((y) << 4)) +#define BLKX(c) ((unsigned char)(c) & 0xf) +#define BLKY(c) ((unsigned char)(c) >> 4) + +#define NUM_PIECES 7 + +static unsigned char pieces[NUM_PIECES][4][4] = { + /* L block */ + { + {BLK(0, 1), BLK(0, 2), BLK(1, 1), BLK(2, 1)}, + {BLK(0, 0), BLK(1, 0), BLK(1, 1), BLK(1, 2)}, + {BLK(0, 1), BLK(1, 1), BLK(2, 1), BLK(2, 0)}, + {BLK(1, 0), BLK(1, 1), BLK(1, 2), BLK(2, 2)} + }, + /* J block */ + { + {BLK(0, 1), BLK(1, 1), BLK(2, 1), BLK(2, 2)}, + {BLK(1, 0), BLK(1, 1), BLK(1, 2), BLK(0, 2)}, + {BLK(0, 0), BLK(0, 1), BLK(1, 1), BLK(2, 1)}, + {BLK(1, 0), BLK(2, 0), BLK(1, 1), BLK(1, 2)} + }, + /* I block */ + { + {BLK(0, 2), BLK(1, 2), BLK(2, 2), BLK(3, 2)}, + {BLK(1, 0), BLK(1, 1), BLK(1, 2), BLK(1, 3)}, + {BLK(0, 2), BLK(1, 2), BLK(2, 2), BLK(3, 2)}, + {BLK(1, 0), BLK(1, 1), BLK(1, 2), BLK(1, 3)} + }, + /* O block */ + { + {BLK(1, 1), BLK(2, 1), BLK(1, 2), BLK(2, 2)}, + {BLK(1, 1), BLK(2, 1), BLK(1, 2), BLK(2, 2)}, + {BLK(1, 1), BLK(2, 1), BLK(1, 2), BLK(2, 2)}, + {BLK(1, 1), BLK(2, 1), BLK(1, 2), BLK(2, 2)} + }, + /* Z block */ + { + {BLK(0, 1), BLK(1, 1), BLK(1, 2), BLK(2, 2)}, + {BLK(0, 1), BLK(1, 1), BLK(1, 0), BLK(0, 2)}, + {BLK(0, 1), BLK(1, 1), BLK(1, 2), BLK(2, 2)}, + {BLK(0, 1), BLK(1, 1), BLK(1, 0), BLK(0, 2)} + }, + /* S block */ + { + {BLK(1, 1), BLK(2, 1), BLK(0, 2), BLK(1, 2)}, + {BLK(0, 0), BLK(0, 1), BLK(1, 1), BLK(1, 2)}, + {BLK(1, 1), BLK(2, 1), BLK(0, 2), BLK(1, 2)}, + {BLK(0, 0), BLK(0, 1), BLK(1, 1), BLK(1, 2)} + }, + /* T block */ + { + {BLK(0, 1), BLK(1, 1), BLK(2, 1), BLK(1, 2)}, + {BLK(1, 0), BLK(1, 1), BLK(1, 2), BLK(0, 1)}, + {BLK(0, 1), BLK(1, 1), BLK(2, 1), BLK(1, 0)}, + {BLK(1, 0), BLK(1, 1), BLK(1, 2), BLK(2, 1)} + } +}; + +static int piece_spawnpos[NUM_PIECES][2] = { + {-1, -2}, {-1, -2}, {-2, -2}, {-1, -2}, {-1, -2}, {-1, -2}, {-1, -2} +}; + + +#endif /* PIECES_H_ */ diff --git a/apps/termtris/src/scoredb.h b/apps/termtris/src/scoredb.h new file mode 100644 index 0000000..d9d3706 --- /dev/null +++ b/apps/termtris/src/scoredb.h @@ -0,0 +1,45 @@ +/* +Termtris - a tetris game for ANSI/VT100 terminals +Copyright (C) 2019-2023 John Tsiombikas + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ +#ifndef SCOREDB_H_ +#define SCOREDB_H_ + +#include + +#if defined(MSDOS) || defined(__COM__) +/* the DOS version of score_entry is simpler, because scores are embedded in + * the executable itself in binary format. + */ +#define MAX_NAME 36 +struct score_entry { + char user[MAX_NAME]; + long score, lines, level; + struct score_entry *next; +}; +#else +struct score_entry { + char *user; + long score, lines, level; + struct score_entry *next; +}; +#endif + +struct score_entry *read_scores(FILE *fp, int max_scores); +int save_score(struct score_entry *sc); +int print_scores(int num); + +#endif /* SCOREDB_H_ */ diff --git a/apps/termtris/src/term.h b/apps/termtris/src/term.h new file mode 100644 index 0000000..ddfbf21 --- /dev/null +++ b/apps/termtris/src/term.h @@ -0,0 +1,62 @@ +/* +Termtris - a tetris game for TERM/VT100 terminals +Copyright (C) 2019-2022 John Tsiombikas + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ +#ifndef TERM_H_ +#define TERM_H_ + +enum { + G_DIAMOND = 0x04, + G_CHECKER = 0xb1, + G_LR_CORNER = 0xd9, + G_UR_CORNER = 0xbf, + G_UL_CORNER = 0xda, + G_LL_CORNER = 0xc0, + G_CROSS = 0xc5, + G_HLINE = 0xc4, + G_L_TEE = 0xc3, + G_R_TEE = 0xb4, + G_B_TEE = 0xc1, + G_T_TEE = 0xc2, + G_VLINE = 0xb3, + G_CDOT = 0xf8 +}; + +enum { BLACK, BLUE, GREEN, CYAN, RED, MAGENTA, YELLOW, WHITE }; +#define BOLD 8 + +enum { + TERM_ANSI, + TERM_VT52, + TERM_ADM3, + TERM_PCBIOS = 0xb105 +}; + +extern char *termenv; +extern int term_type; + +extern void (*term_reset)(void); +extern void (*term_clearscr)(void); +extern void (*term_setcursor)(int row, int col); +extern void (*term_cursor)(int show); +extern void (*term_setcolor)(int fg, int bg); +/* convert a PC cga/ega/vga char+attr to an TERM sequence and write it to stdout */ +extern void (*term_ibmchar)(unsigned char c, unsigned char attr); + +void term_init(void); +void term_putstr(const char *s, unsigned char attr); + +#endif /* TERM_H_ */ diff --git a/apps/termtris/src/vt1200/cinit.asm b/apps/termtris/src/vt1200/cinit.asm new file mode 100644 index 0000000..f3a3a9a --- /dev/null +++ b/apps/termtris/src/vt1200/cinit.asm @@ -0,0 +1,4 @@ + .sect ".cinit" + .globl cinit +cinit: + .end diff --git a/apps/termtris/src/vt1200/cinitz.asm b/apps/termtris/src/vt1200/cinitz.asm new file mode 100644 index 0000000..9966837 --- /dev/null +++ b/apps/termtris/src/vt1200/cinitz.asm @@ -0,0 +1,5 @@ + .sect ".cinit" + .globl cinit +cinit: + .word 0 + .end diff --git a/apps/termtris/src/vt1200/entry.asm b/apps/termtris/src/vt1200/entry.asm new file mode 100644 index 0000000..a4700ad --- /dev/null +++ b/apps/termtris/src/vt1200/entry.asm @@ -0,0 +1,28 @@ + .text + .globl _start + .globl _main + .globl cinit +_start: + setf 32,0,1 + movi _cstack,a14 + move a14,a13 + + ; process the .cinit table: u16 word count, u32 destination, data words + movi cinit,a9 +ci_loop: + setf 16,0,0 + move *a9+,a11 + jrz ci_done + move *a9+,a10,1 +ci_copy: + move *a9+,*a10+ + dsjs a11,ci_copy + jruc ci_loop +ci_done: + setf 32,0,1 + move a13,a14 + calla _main + rets + + .bss _cstack,32768,16 + .end diff --git a/apps/termtris/src/vt1200/xgfix.c b/apps/termtris/src/vt1200/xgfix.c new file mode 100644 index 0000000..1a847e4 --- /dev/null +++ b/apps/termtris/src/vt1200/xgfix.c @@ -0,0 +1,30 @@ +/* + * xgfix.c -- install a per-process Xlib context before XOpenDisplay. + * + * `create_process` leaves the per-process user word at *(proc+0x100) pointing + * at a zeroed cell. The ROM Xlib's _XEnq does + * + * XG = *(*(proc+0x100)); + * free_list = *(XG + 0x198 bytes); + * + * so a loader child with XG == NULL reads low-RAM garbage as a queue-node + * pointer and XNextEvent copies unmapped zeros. Giving it a zeroed block + * makes the event queue real. This is a RAM write only; the ROM is untouched. + */ +typedef unsigned long vt_u32; + +static vt_u32 vt_xg[256]; /* 1 KiB: > the 0x198-byte free-list field */ + +int vt_install_xg(void) +{ + vt_u32 proc, pp; + + proc = *(volatile vt_u32 *)0x001B3900UL; + if(proc < 0x100UL || proc > 0x01FFFFFFUL) + return 0; + pp = *(volatile vt_u32 *)(proc + 0x100UL); + if(pp < 0x100UL || pp > 0x01FFFFFFUL) + return 0; + *(volatile vt_u32 *)pp = (vt_u32)vt_xg; + return 1; +} diff --git a/apps/termtris/src/vt1200/xlib.h b/apps/termtris/src/vt1200/xlib.h new file mode 100644 index 0000000..a51293e --- /dev/null +++ b/apps/termtris/src/vt1200/xlib.h @@ -0,0 +1,51 @@ +/* + * xlib.h -- the VT1200 ROM Xlib entry points, called with the TI C ABI. + * + * Addresses are TMS34010 bit addresses. The caller reserves the A14 slot and + * pushes arguments last-to-first; the callee `rets 2` restores A14. The C + * compiler emits exactly this sequence for a call through a function pointer. + */ +#ifndef VT_XLIB_H_ +#define VT_XLIB_H_ + +typedef unsigned long vt_u32; + +typedef void *(*vt_xopen_t)(const char *); +typedef vt_u32 (*vt_xcreate_t)(void *, vt_u32, int, int, unsigned int, + unsigned int, unsigned int, vt_u32, vt_u32); +typedef int (*vt_xselect_t)(void *, vt_u32, long); +typedef int (*vt_xmap_t)(void *, vt_u32); +typedef int (*vt_xsync_t)(void *, int); +typedef int (*vt_xflush_t)(void *); +typedef int (*vt_xnext_t)(void *, void *); +typedef int (*vt_xlookup_t)(void *, char *, int, vt_u32 *, void *); +typedef vt_u32 (*vt_xgc_t)(void *, vt_u32, vt_u32, vt_u32 *); +typedef int (*vt_xsetfg_t)(void *, vt_u32, vt_u32); +typedef int (*vt_xfill_t)(void *, vt_u32, vt_u32, int, int, + unsigned int, unsigned int); +typedef int (*vt_xdraw_t)(void *, vt_u32, vt_u32, int, int, + const char *, int); +typedef int (*vt_sleep_t)(int); + +#define VT_XOPEN ((vt_xopen_t)0x3F9C2570UL) +#define VT_XCREATE ((vt_xcreate_t)0x3FD41B90UL) +#define VT_XSELECT ((vt_xselect_t)0x3FD97D50UL) +#define VT_XMAP ((vt_xmap_t)0x3FD64F60UL) +#define VT_XSYNC ((vt_xsync_t)0x3FD9F6B0UL) +#define VT_XFLUSH ((vt_xflush_t)0x3F9B1030UL) +#define VT_XNEXT ((vt_xnext_t)0x3F9C1CF0UL) +#define VT_XLOOKUP ((vt_xlookup_t)0x3F9A9C30UL) +#define VT_XGC ((vt_xgc_t)0x3F997000UL) +#define VT_XSETFG ((vt_xsetfg_t)0x3FD99A60UL) +#define VT_XFILL ((vt_xfill_t)0x3F99E8F0UL) +#define VT_XDRAW ((vt_xdraw_t)0x3FDA0000UL) +#define VT_XPENDING ((int (*)(void *))0x3FD67620UL) +#define VT_SLEEP ((vt_sleep_t)0x3FB33A20UL) + +/* display->screens[] is at bit 0x480 / byte 0x90; screen->root at bit 0x40 / + * byte 8 (see dwt/notes/GUI_APPS.md and dwt/gui/xhello.asm). */ +vt_u32 vt_root_window(void *dpy); + +int vt_install_xg(void); + +#endif /* VT_XLIB_H_ */ diff --git a/apps/termtris/tests/smoke.c b/apps/termtris/tests/smoke.c new file mode 100644 index 0000000..74161fc --- /dev/null +++ b/apps/termtris/tests/smoke.c @@ -0,0 +1,53 @@ +/* + * smoke.c -- Task 1 acceptance: a local Xlib client installs the per-process + * Xlib context, opens the display, maps a window, and receives a real KeyPress. + */ +#include "xlib.h" + +extern int vt_install_xg(void); + +volatile vt_u32 mark[32]; +volatile vt_u32 evbuf[24]; + +int main(void) +{ + void *dpy; + vt_u32 root, win; + vt_u32 ks; + char buf[8]; + int n; + + mark[0] = 0x534D4F4BUL; /* SMOK */ + if(!vt_install_xg()) return 1; + dpy = VT_XOPEN("0::0"); + mark[1] = (vt_u32)dpy; + if(!dpy) return 2; + + root = *(vt_u32 *)((char *)dpy + 0x90); + root = *(vt_u32 *)((char *)root + 8); + win = VT_XCREATE(dpy, root, 80, 80, 480, 312, 2, 1, 0); + mark[2] = win; + if(!win) return 3; + + VT_XSELECT(dpy, win, 0x22000FUL); + VT_XMAP(dpy, win); + VT_XSYNC(dpy, 0); + mark[3] = 0; + + for(;;) { + VT_XNEXT(dpy, (void *)evbuf); + mark[3] = mark[3] + 1; + mark[4] = evbuf[0]; + mark[5] = evbuf[13]; + if(evbuf[0] == 2) { + ks = 0; + n = VT_XLOOKUP((void *)evbuf, buf, 8, &ks, 0); + mark[6] = (vt_u32)n; + mark[7] = (vt_u32)(unsigned char)buf[0]; + mark[8] = ks; + mark[9] = 0x4B455950UL; /* KEYP */ + break; + } + } + return 0; +} diff --git a/apps/termtris/tests/smoke.lua b/apps/termtris/tests/smoke.lua new file mode 100644 index 0000000..9e05357 --- /dev/null +++ b/apps/termtris/tests/smoke.lua @@ -0,0 +1,355 @@ +-- loader.lua -- debugger-assisted COFF loader acceptance harness. +-- +-- Boots the firmware with VT1200_SKIP_LEVEL1=1 (the documented divergence) to +-- the OS, then at frame LOADER_AT pokes: +-- * the loader payload at bit 0x01880000 (raw dwt/loader/build/loader.bin) +-- * a GSPLNK COFF image at LOADER_BUF (default bit 0x01900000) +-- * the loader descriptor at 0x01882C00 +-- and sets PC = 0x01880000. It does not use the network, the rdebug service +-- or any firmware transport: this is the debugger-assisted milestone. +-- +-- The loader itself copies the COFF sections to their linked bit addresses and +-- calls create_process(entry, "LOADER", 0x6000, 1, 0). This script only pokes +-- the input and reads the result; it never writes the app's code or markers. +-- +-- Env: +-- LOADER_BIN loader payload (default dwt/loader/build/loader.bin) +-- LOADER_COFF app COFF (default dwt/loader/build/marker.coff) +-- LOADER_BUF COFF buffer bit address (default 0x01900000) +-- LOADER_AT injection frame (default 2500) +-- LOADER_END inspection/exit frame (default 2700) +-- LOADER_OUT evidence directory (default dwt/loader/evidence) +-- LOADER_SNAP 1 = write a PNG at LOADER_END (default 0) +-- LOADER_VERBOSE 1 = print the result block every 25 frames + +local function hex(v, w) return string.format("%0" .. (w or 8) .. "X", v) end + +local machine = manager.machine +local MARK = tonumber(os.getenv("SMOKE_MARK") or "0x01A00000") +local ports = machine.ioport.ports +local key_a = ports[":lk401:K26"].fields["A"] +local tabx = ports[":tablet:TABLET_X"].fields["Tablet X"] +local taby = ports[":tablet:TABLET_Y"].fields["Tablet Y"] +local tabb = ports[":tablet:TABLET_BTN"].fields["P1 Button 1"] +local cpu = machine.devices[':maincpu'] +local dbg = machine.debugger +local prog = cpu.spaces['program'] + +if not dbg then + print("LOADER: debugger not enabled; pass -debug") + machine:exit() + return +end + +local AT = tonumber(os.getenv("LOADER_AT") or "2500") +local ENDF = tonumber(os.getenv("LOADER_END") or "2700") +local BIN = os.getenv("LOADER_BIN") or "dwt/loader/build/loader.bin" +local COFF = os.getenv("LOADER_COFF") or "dwt/loader/build/marker.coff" +local BUF = tonumber(os.getenv("LOADER_BUF") or "0x01900000") +local OUT = os.getenv("LOADER_OUT") or "dwt/loader/evidence" +local SNAP = os.getenv("LOADER_SNAP") == "1" +local MODE = tonumber(os.getenv("LOADER_MODE") or "0") +local A8DESC = os.getenv("LOADER_A8_DESC") == "1" +local VERBOSE = os.getenv("LOADER_VERBOSE") == "1" + +local LOADER_BASE = 0x01880000 +local DESC = 0x01882C00 +local RESULT = 0x01882D00 +local RETURN_STUB = 0x01883100 +local RETURN_SP = 0x01883110 + +local function rd(p) return io.open(p, "rb") end +local function slurp(p) + local f, err = rd(p) + if not f then error("cannot open " .. p .. ": " .. tostring(err)) end + local s = f:read("*a") + f:close() + return s +end + +local function u16(s, o) + return s:byte(o + 1) + s:byte(o + 2) * 256 +end + +local function u32(s, o) + return s:byte(o + 1) + s:byte(o + 2) * 256 + + s:byte(o + 3) * 65536 + s:byte(o + 4) * 16777216 +end + +local function parse_coff(d) + local c = { data = d } + c.magic = u16(d, 0) + c.nscns = u16(d, 2) + c.opthdr = u16(d, 16) + c.sections = {} + if c.opthdr > 0 then + c.optmagic = u16(d, 20) + c.entry = u32(d, 20 + 16) + c.textstart = u32(d, 20 + 20) + c.datastart = u32(d, 20 + 24) + end + local base = 20 + c.opthdr + for i = 0, c.nscns - 1 do + local o = base + i * 40 + local name = d:sub(o + 1, o + 8):gsub("%z", "") + c.sections[#c.sections + 1] = { + name = name, + paddr = u32(d, o + 8), + size = u32(d, o + 16), -- bits + scnptr = u32(d, o + 20), + flags = u32(d, o + 36), + } + end + return c +end + +local function poke_bytes(bitaddr, data) + local n = math.floor((#data + 1) / 2) + for i = 0, n - 1 do + local lo = data:byte(i * 2 + 1) or 0 + local hi = data:byte(i * 2 + 2) or 0 + prog:write_u16(bitaddr + i * 16, lo + hi * 256) + end + return n +end + +local function verify_bytes(bitaddr, data) + local n = math.floor((#data + 1) / 2) + local bad = 0 + for i = 0, n - 1 do + local lo = data:byte(i * 2 + 1) or 0 + local hi = data:byte(i * 2 + 2) or 0 + if prog:read_u16(bitaddr + i * 16) ~= lo + hi * 256 then + bad = bad + 1 + end + end + return n, bad +end + +local function w32(a, v) prog:write_u32(a, v) end +local function r32(a) + local ok, v = pcall(function() return prog:read_u32(a) end) + if ok then return v end + return 0xDEADBEEF +end + +local loader = slurp(BIN) +local coff = slurp(COFF) +local app = parse_coff(coff) + +print(string.format("LOADER: payload %s (%d B) -> bit %s", BIN, #loader, hex(LOADER_BASE))) +print(string.format("LOADER: COFF %s (%d B) -> bit %s", COFF, #coff, hex(BUF))) +print(string.format("LOADER: file magic=%04X nscns=%d opthdr=%d optmagic=%s entry=%s textstart=%s", + app.magic, app.nscns, app.opthdr, + app.optmagic and string.format("%04X", app.optmagic) or "none", + hex(app.entry or 0), hex(app.textstart or 0))) +for _, s in ipairs(app.sections) do + print(string.format("LOADER: sec %-8s paddr=%s size=%d bits (%d B) scnptr=%#x flags=%#x", + s.name, hex(s.paddr), s.size, math.floor(s.size / 8), s.scnptr, s.flags)) +end + +local TRACE = os.getenv("LOADER_TRACE") == "1" + +local frames = 0 +local injected = false +local done = false +local last_count = -1 + +local function status_name(v) + local t = { + [0] = "OK", + [1] = "bad descriptor", + [2] = "truncated COFF / section headers", + [3] = "bad COFF magic", + [4] = "section size not whole words", + [5] = "target outside RAM", + [6] = "target overlaps loader page or COFF buffer", + [7] = "relocation records present (unsupported)", + [8] = "target not in a free-list block", + [9] = "no entry point", + } + return t[v] or ("error " .. tostring(v)) +end + +local function result_dump() + local st = r32(RESULT + 0x000) + print(string.format( + "LOADER: result status=%d (%s) entry=%s text=%s/%d bits data=%s/%d bits bss=%s/%d bits relocs=%d pcb=%s nscns=%d len=%d", + st, status_name(st), hex(r32(RESULT + 0x020)), + hex(r32(RESULT + 0x040)), r32(RESULT + 0x060), + hex(r32(RESULT + 0x080)), r32(RESULT + 0x0A0), + hex(r32(RESULT + 0x0C0)), r32(RESULT + 0x0E0), + r32(RESULT + 0x100), hex(r32(RESULT + 0x120)), + r32(RESULT + 0x140), r32(RESULT + 0x160))) +end + +local function inject() + -- 1. loader itself + local n, bad = 0, 0 + poke_bytes(LOADER_BASE, loader) + n, bad = verify_bytes(LOADER_BASE, loader) + print(string.format("LOADER: loader poked %d words, readback %d bad at %s", + n, bad, hex(LOADER_BASE))) + + -- 2. COFF image + poke_bytes(BUF, coff) + n, bad = verify_bytes(BUF, coff) + print(string.format("LOADER: COFF poked %d words, readback %d bad at %s", + n, bad, hex(BUF))) + + -- 3. pre-fill every .bss section with A5A5, so a loaded zero is meaningful + for _, s in ipairs(app.sections) do + if s.flags & 0x80 ~= 0 then + for i = 0, math.floor(s.size / 16) - 1 do + prog:write_u16(s.paddr + i * 16, 0xA5A5) + end + print(string.format("LOADER: pre-filled .bss %s..%s with A5A5", + hex(s.paddr), hex(s.paddr + s.size))) + end + end + + -- 4. descriptor: magic "LD" "R1", buf, len, mode=0 (spin) + w32(DESC + 0x00, 0) + prog:write_u16(DESC + 0x00, 0x4C44) + prog:write_u16(DESC + 0x10, 0x5231) + w32(DESC + 0x20, BUF) + w32(DESC + 0x40, #coff) + w32(DESC + 0x60, MODE) + print(string.format("LOADER: descriptor @%s magic=%04X/%04X buf=%s len=%d mode=%d", + hex(DESC), prog:read_u16(DESC + 0x00), prog:read_u16(DESC + 0x10), + hex(r32(DESC + 0x20)), r32(DESC + 0x40), r32(DESC + 0x60))) + + -- 5. return stub. mode 0 ignores it (the loader jumps to ORIG_PC + -- itself); mode 1 returns here, and the stub resumes the interrupted + -- PC so the caller yields and the scheduler can run the child. + local orig_pc = cpu.state['PC'].value + if MODE == 1 then + prog:write_u16(RETURN_STUB, 0xC080) -- jauc + prog:write_u16(RETURN_STUB + 0x10, orig_pc & 0xFFFF) + prog:write_u16(RETURN_STUB + 0x20, math.floor(orig_pc / 0x10000) & 0xFFFF) + else + prog:write_u16(RETURN_STUB, 0xC0FF) -- jruc self + end + + -- 6. enter the loader. A8=0 selects the fixed descriptor; SP points at + -- the return slot so mode=1 would return to the spin stub. + -- Leave SP alone for mode 0: the loader pushes onto the current + -- process stack, then restores it and resumes at ORIG_PC. (mode 1 + -- would use the return stub.) + w32(0x01882A20, orig_pc) -- LD_ORIG_PC + print(string.format("LOADER: will resume at %s after create_process", hex(orig_pc))) + cpu.state['A8'].value = A8DESC and DESC or 0 + if MODE == 1 then + -- Simulate a CALLA from the current process: push the return + -- address on the *firmware* stack (a real transport would do this). + local sp = cpu.state['SP'].value - 0x20 + w32(sp, RETURN_STUB) + cpu.state['SP'].value = sp + end + cpu.state['PC'].value = LOADER_BASE + injected = true + print(string.format("LOADER: frame %d: entered loader at %s (caller SP=%s)", + frames, hex(LOADER_BASE), hex(cpu.state['SP'].value))) +end + +local function inspect() + result_dump() + + -- Verify the placement from RAM: .text/.data must equal the file bytes. + for _, s in ipairs(app.sections) do + if s.scnptr ~= 0 and s.size > 0 then + local nb = math.floor(s.size / 8) + local bytes = coff:sub(s.scnptr + 1, s.scnptr + nb) + local _, bad = verify_bytes(s.paddr, bytes) + print(string.format("LOADER: placed %s at %s: %d bytes, %d differing", + s.name, hex(s.paddr), nb, bad)) + end + end + + local base = COFF:match("([^/]+)$") or COFF + if base:find("marker") then + print(string.format("LOADER: marker app: start=%s data=%s data2=%s bss=%s count=%d", + hex(r32(0x018E0000)), hex(r32(0x018E0020)), hex(r32(0x018E0040)), + hex(r32(0x018E0060)), r32(0x018E0080))) + elseif base:find("xhello") then + print(string.format("LOADER: xhello: stage=%d strlen=%d display=%s root=%s window=%s sync=%s", + r32(0x01890140), r32(0x01890160), hex(r32(0x01890100)), + hex(r32(0x01890110)), hex(r32(0x01890120)), hex(r32(0x01890130)))) + end + if base:find("smoke") then + print(string.format("SMOKE: magic=%08X dpy=%08X win=%08X n=%d type=%d keycode=%d lookup_n=%d ch=%02X ks=%04X keymark=%08X", + r32(MARK + 0x00), r32(MARK + 0x20), r32(MARK + 0x40), r32(MARK + 0x60), + r32(MARK + 0x80), r32(MARK + 0xA0), r32(MARK + 0xC0), r32(MARK + 0xE0), + r32(MARK + 0x100), r32(MARK + 0x120))) + end + print(string.format("LOADER: PC at inspection = %s", hex(cpu.state['PC'].value))) +end + +emu.register_frame_done(function() + frames = frames + 1 + if frames == 2900 then tabx:set_value(700); taby:set_value(500); print("SMOKE: tablet move") end + if frames == 3000 then tabb:set_value(1); print("SMOKE: click down") end + if frames == 3020 then tabb:set_value(0); print("SMOKE: click up") end + if frames == 3100 then key_a:set_value(1); print("SMOKE: A down") end + if frames == 3120 then key_a:set_value(0); print("SMOKE: A up") end + if frames == AT and not injected then + inject() + if TRACE then dbg:command("trace /tmp/opencode/ld1.tr") end + end + if injected and TRACE and frames == AT + 1 then + dbg:command("trace off") + print("LOADER: trace written to /tmp/opencode/ld.tr") + end + if injected and not done then + if TRACE and frames <= AT + 40 then + print(string.format("LOADER: trace f=%d PC=%s SP=%s A14=%s status=%s entry=%s", + frames, hex(cpu.state['PC'].value), hex(cpu.state['SP'].value), + hex(cpu.state['A14'].value), hex(r32(RESULT + 0x000)), + hex(r32(RESULT + 0x020)))) + end + local st = r32(RESULT + 0x000) + -- status 0 means OK; nonzero is final too once parsing has begun + if st ~= 0xFFFFFFFF and not done then + -- The loader writes 0 on success. A nonzero status is final + -- only after it has been written once (it is 0 from power-on, + -- so require the loader to have run: PC no longer at entry or + -- the app counter moved). + end + if VERBOSE and frames % 25 == 0 then + result_dump() + end + if base_contains_marker then + local c = r32(0x018E0080) + if c ~= last_count then + if last_count ~= -1 then + print(string.format("LOADER: marker counter %d -> %d (child running)", + last_count, c)) + end + last_count = c + end + end + end + if frames >= ENDF then + if injected then + done = true + inspect() + if SNAP then + local screen = nil + for _, s in pairs(machine.screens) do screen = s break end + if screen then + local out = OUT .. "/loader_frame" .. frames .. ".png" + local ok, err = pcall(function() return screen:snapshot(out) end) + print(string.format("LOADER: snapshot %s ok=%s %s", + out, tostring(ok), tostring(err or ""))) + end + end + end + machine:exit() + end +end, "vt1200-loader") + +-- (Lua has no forward use of a name defined later; set a small flag here.) +base_contains_marker = (COFF:match("([^/]+)$") or ""):find("marker") ~= nil + +dbg.visible_cpu = cpu +dbg.execution_state = 'run' diff --git a/apps/termtris/tests/smoke.sh b/apps/termtris/tests/smoke.sh new file mode 100755 index 0000000..a35d03d --- /dev/null +++ b/apps/termtris/tests/smoke.sh @@ -0,0 +1,37 @@ +#!/bin/sh +# smoke.sh -- Task 1 acceptance. Builds the local Xlib smoke payload, loads it +# into the emulated VT1200 RAM, clicks the window, presses A, and requires a +# real KeyPress + XLookupString('a'). +set -eu + +DIR=$(cd "$(dirname "$0")" && pwd) +APP=$(cd "$DIR/.." && pwd) +ROOT=$(cd "$APP/../.." && pwd) +M=${MAME:-/home/arti/code/k-space/mame0289/vt1200} +LOG="$APP/build/smoke.log" + +mkdir -p "$APP/build" +"$APP/build.sh" "$DIR/smoke.c" "$APP/src/vt1200/xgfix.c" >"$APP/build/smoke-build.log" 2>&1 + +MARK=$(awk '/_mark/ {print $1; exit}' "$APP/build/smoke.map") +VT1200_SKIP_LEVEL1=1 SMOKE_MARK="0x$MARK" \ +LOADER_AT=2500 LOADER_END=3400 \ +LOADER_BIN="$ROOT/dwt/loader/build/loader.bin" \ +LOADER_COFF="$APP/build/smoke.coff" \ +LOADER_OUT="$APP/build" \ +"$M" vt1200 -rompath "$ROOT/harness/mame/roms" \ + -video none -sound none -nothrottle -skip_gameinfo \ + -debug -debugger none \ + -autoboot_script "$DIR/smoke.lua" -autoboot_delay 0 \ + >"$LOG" 2>&1 || true + +grep 'SMOKE:' "$LOG" | tail -5 +grep -q 'SMOKE: .*type=2 .*keymark=4B455950' "$LOG" || { + echo "smoke.sh: no real KeyPress (see $LOG)" >&2 + exit 1 +} +grep -q 'SMOKE: .*lookup_n=1 ch=61 ks=0061' "$LOG" || { + echo "smoke.sh: XLookupString did not return 'a' (see $LOG)" >&2 + exit 1 +} +echo "smoke: PASS" -- 2.47.3 From a8041818bb62d43a33972371c0522ee643d4ad88 Mon Sep 17 00:00:00 2001 From: "opencode.energia.arti.ee" Date: Tue, 3 Nov 2026 18:51:22 +0000 Subject: [PATCH 04/18] feat(termtris): compat sprintf/stdio for the TI r5.01 runtime --- .gitignore | 1 + apps/termtris/build/fmt.coff | Bin 0 -> 2050 bytes apps/termtris/build/fmt.map | 59 +++++ apps/termtris/src/vt1200/compat.c | 131 ++++++++++ apps/termtris/src/vt1200/inttypes.h | 13 + apps/termtris/src/vt1200/stdio.h | 15 ++ apps/termtris/tests/fmt.c | 18 ++ apps/termtris/tests/fmt.lua | 354 ++++++++++++++++++++++++++++ apps/termtris/tests/fmt.sh | 35 +++ 9 files changed, 626 insertions(+) create mode 100644 apps/termtris/build/fmt.coff create mode 100644 apps/termtris/build/fmt.map create mode 100644 apps/termtris/src/vt1200/compat.c create mode 100644 apps/termtris/src/vt1200/inttypes.h create mode 100644 apps/termtris/src/vt1200/stdio.h create mode 100644 apps/termtris/tests/fmt.c create mode 100644 apps/termtris/tests/fmt.lua create mode 100755 apps/termtris/tests/fmt.sh diff --git a/.gitignore b/.gitignore index e4c7b3c..138174f 100644 --- a/.gitignore +++ b/.gitignore @@ -72,3 +72,4 @@ tools/tms34010-sdk/1991-07 TIGA Promo Kit and Art Software/ # termtris build scratch (TI tools + 2ine libs) apps/termtris/work/ +apps/termtris/build/*.log diff --git a/apps/termtris/build/fmt.coff b/apps/termtris/build/fmt.coff new file mode 100644 index 0000000000000000000000000000000000000000..b00469fdfcd7303aaeff410ecc2abd028ed8f9bf GIT binary patch literal 2050 zcmbtVUuaup6hAkAmVUikjnKkX#0A@s{&AsG9~6sZ(m+G+H~XL#Bx}=R!niTrr zL+E6!p=7b4%)x|U7?}-N@g;Bbf@{KZS5o#QJ{4gstu!Dw&!u?KJN46}L;Tyv@C&&I&JhpG?^tmY^Da?@TWJ)+BkjM(voJ!spjb>^j zVM_`<8Fyy-+`c`D`o~gzvqrwtv@U8B6HwnYY-KWxQjPr7w1UuXoztw)#Ti{1kEE|T zPaCpERy2K$tT*joYB8bzrasZscX@Y6qI-roFOEh6;CzKoujAyOMIqpJIAabciYwr} z@M;X4PJo@<9_{khiyk<>F^k!My0u4pLW~U%7ag)+ z8OUGqJ~u~mQL{J-S2hYC{H_*?SvKMo$A#TRZ@xIgUQ=_$1e4T7YAKzSLe}ILlVfJt zQYn)Q5V9Vn`{|pNkaeinMLF9MUYCCQhk8)~#O!LA?R1b@OAp8>;`!J8^rch2kQC1P zqxQuDvXWCUbI5a63RBMG8=Ih*udqYDc}vT92Q#+we6#dG{DIl79i~DmD0d-``5%3G z{Jp|HRG_z-i)K=rsm$fgo`(RJ@iur!Qs|}}WqX#RCJ|-3cD!m)ACsvUlfv<8HOEgZ zjq5X1mLK5WYMe;DBr3k;i%IYxx(k%35AIUzM3P4m|>PSPe~@IBhnzPiMQCT z%3v>4-ea{ss07&ruHRw1S{B>sM=4O>X1B+;H*VvZOwc>ycSh>P1fJIuJg>p%awIYY zaGku1uepGT`-EYpcqh01QU~#5vw&}4coyG#dsV_R0%ks)80{5=7ot-!ZPm#9ZW p&bzG66KwQd-qUo=$=pp4Z0nKyh9l33`;3@q04=<)>pO3PzX8eb7xDlA literal 0 HcmV?d00001 diff --git a/apps/termtris/build/fmt.map b/apps/termtris/build/fmt.map new file mode 100644 index 0000000..9f887e2 --- /dev/null +++ b/apps/termtris/build/fmt.map @@ -0,0 +1,59 @@ +***************************************************** +TMS340 COFF Linker Version 5.01 +***************************************************** +Tue Nov 03 18:50:58 2026 + +OUTPUT FILE NAME: +ENTRY POINT SYMBOL: "_start" address: 018a0000 + + +MEMORY CONFIGURATION + + name origin length attributes + -------- -------- --------- ---------- + prog 018a0000 000020000 RWIX + ram 01a00000 000040000 RWIX + + +SECTION ALLOCATION MAP + + output attributes/ +section page origin length input sections +-------- ---- ---------- ---------- ---------------- +.text 0 018a0000 00001fa0 + 018a0000 00000150 ENTRY.obj (.text) + 018a0150 00000780 FMT.obj (.text) + 018a08d0 000015c0 COMPAT.obj (.text) + 018a1e90 00000110 rts.lib (.text) + +.cinit 0 018a1fa0 00000010 + 018a1fa0 00000010 CINITZ.obj (.cinit) + +.bss 0 01a00000 00008320 UNINITIALIZED + 01a00000 00008000 ENTRY.obj (.bss) + 01a08000 00000300 FMT.obj (.bss) + 01a08300 00000020 COMPAT.obj (.bss) + +.data 0 018a0000 00000000 UNINITIALIZED + + +GLOBAL SYMBOLS + +address name address name +-------- ---- -------- ---- +01a00000 .bss 018a0000 .text +018a0000 .data 018a0000 .data +018a0000 .text 018a0000 edata +018a08d0 _fflush 018a0000 _start +018a0150 _main 018a0150 _main +01a08000 _out 018a08d0 _fflush +018a10d0 _sprintf 018a10d0 _sprintf +018a0000 _start 018a1e90 _strlen +01a08300 _stdout 018a1fa0 cinit +018a1e90 _strlen 018a1fa0 etext +018a1fa0 cinit 01a00000 .bss +018a0000 edata 01a08000 _out +01a08320 end 01a08300 _stdout +018a1fa0 etext 01a08320 end + +[14 symbols] diff --git a/apps/termtris/src/vt1200/compat.c b/apps/termtris/src/vt1200/compat.c new file mode 100644 index 0000000..02c67b7 --- /dev/null +++ b/apps/termtris/src/vt1200/compat.c @@ -0,0 +1,131 @@ +/* + * compat.c -- the small libc surface termtris needs on the VT1200. + * + * The TI r5.01 runtime has no stdio and no sprintf. This implements exactly + * the decimal/string formats game.c uses. Arguments are walked manually + * because the SDK's va_arg macro does not survive the 1990 compiler's + * preprocessor here; the TI ABI lays the varargs contiguously below &fmt. + */ +#include + +FILE *stdout; +extern int strlen(char *); + +int fflush(FILE *fp) +{ + (void)fp; + return 0; +} + +static char *fmt_num(char *out, long v, int width, int zero, int left, int unsig) +{ + char tmp[24]; + int n = 0, neg = 0, pad, i; + unsigned long u; + + if(v < 0 && !unsig) { + neg = 1; + u = (unsigned long)(-v); + } else { + u = (unsigned long)v; + } + do { + tmp[n++] = (char)('0' + (u % 10UL)); + u /= 10UL; + } while(u); + + pad = width - (n + neg); + if(pad < 0) pad = 0; + + if(left) { + if(neg) *out++ = '-'; + while(n) *out++ = tmp[--n]; + for(i = 0; i < pad; i++) *out++ = ' '; + } else if(zero) { + if(neg) *out++ = '-'; + for(i = 0; i < pad; i++) *out++ = '0'; + while(n) *out++ = tmp[--n]; + } else { + for(i = 0; i < pad; i++) *out++ = ' '; + if(neg) *out++ = '-'; + while(n) *out++ = tmp[--n]; + } + return out; +} + +int sprintf(char *buf, char *fmt, ...) +{ + char *out = buf; + char *ap = (char *)&fmt; + + while(*fmt) { + int left = 0, zero = 0, width = 0, unsig = 0, islong = 0; + + if(*fmt != '%') { + *out++ = *fmt++; + continue; + } + fmt++; + if(*fmt == '%') { + *out++ = '%'; + fmt++; + continue; + } + if(*fmt == '-') { left = 1; fmt++; } + if(*fmt == '0') { zero = 1; fmt++; } + while(*fmt >= '0' && *fmt <= '9') { + width = width * 10 + (*fmt - '0'); + fmt++; + } + if(*fmt == 'l') { islong = 1; fmt++; } + if(*fmt == 'u') { unsig = 1; } + + switch(*fmt) { + case 'd': + case 'u': + { + long v; + ap -= sizeof(long); + v = *(long *)ap; + (void)islong; + out = fmt_num(out, v, width, zero, left, unsig); + fmt++; + } + break; + + case 's': + { + char *s; + int len, i, pad; + ap -= sizeof(char *); + s = *(char **)ap; + if(!s) s = "(null)"; + len = strlen(s); + pad = width - len; + if(pad < 0) pad = 0; + if(!left) for(i = 0; i < pad; i++) *out++ = ' '; + for(i = 0; i < len; i++) *out++ = s[i]; + if(left) for(i = 0; i < pad; i++) *out++ = ' '; + fmt++; + } + break; + + case 'c': + { + int c; + ap -= sizeof(int); + c = *(int *)ap; + *out++ = (char)c; + fmt++; + } + break; + + default: + *out++ = '%'; + if(*fmt) *out++ = *fmt++; + break; + } + } + *out = 0; + return (int)(out - buf); +} diff --git a/apps/termtris/src/vt1200/inttypes.h b/apps/termtris/src/vt1200/inttypes.h new file mode 100644 index 0000000..18189af --- /dev/null +++ b/apps/termtris/src/vt1200/inttypes.h @@ -0,0 +1,13 @@ +/* + * inttypes.h -- the fixed-width types game.c uses, for the 1990 TI compiler. + */ +#ifndef VT_INTTYPES_H_ +#define VT_INTTYPES_H_ + +typedef unsigned char uint8_t; +typedef signed short int16_t; +typedef unsigned short uint16_t; +typedef signed long int32_t; +typedef unsigned long uint32_t; + +#endif diff --git a/apps/termtris/src/vt1200/stdio.h b/apps/termtris/src/vt1200/stdio.h new file mode 100644 index 0000000..66f6fe9 --- /dev/null +++ b/apps/termtris/src/vt1200/stdio.h @@ -0,0 +1,15 @@ +/* + * stdio.h -- the tiny subset termtris needs on the VT1200. + * There is no stdio in the TI r5.01 runtime; compat.c provides sprintf. + */ +#ifndef VT_STDIO_H_ +#define VT_STDIO_H_ + +typedef int FILE; + +extern FILE *stdout; + +int sprintf(char *buf, char *fmt, ...); +int fflush(FILE *fp); + +#endif diff --git a/apps/termtris/tests/fmt.c b/apps/termtris/tests/fmt.c new file mode 100644 index 0000000..07fe8dd --- /dev/null +++ b/apps/termtris/tests/fmt.c @@ -0,0 +1,18 @@ +/* + * fmt.c -- Task 2 acceptance: the exact sprintf formats game.c uses. + * Results are NUL-terminated in out[6][16]; tests/fmt.lua prints them. + */ +#include + +char out[6][16]; + +int main(void) +{ + sprintf(out[0], "%10ld", 12345L); + sprintf(out[1], "%2ld", 7L); + sprintf(out[2], "%8ld", 42L); + sprintf(out[3], "%2d. %s %ld", 1, "ab", 99L); + sprintf(out[4], "%-3s", "x"); + sprintf(out[5], "%%"); + return 0; +} diff --git a/apps/termtris/tests/fmt.lua b/apps/termtris/tests/fmt.lua new file mode 100644 index 0000000..0af6df3 --- /dev/null +++ b/apps/termtris/tests/fmt.lua @@ -0,0 +1,354 @@ +-- loader.lua -- debugger-assisted COFF loader acceptance harness. +-- +-- Boots the firmware with VT1200_SKIP_LEVEL1=1 (the documented divergence) to +-- the OS, then at frame LOADER_AT pokes: +-- * the loader payload at bit 0x01880000 (raw dwt/loader/build/loader.bin) +-- * a GSPLNK COFF image at LOADER_BUF (default bit 0x01900000) +-- * the loader descriptor at 0x01882C00 +-- and sets PC = 0x01880000. It does not use the network, the rdebug service +-- or any firmware transport: this is the debugger-assisted milestone. +-- +-- The loader itself copies the COFF sections to their linked bit addresses and +-- calls create_process(entry, "LOADER", 0x6000, 1, 0). This script only pokes +-- the input and reads the result; it never writes the app's code or markers. +-- +-- Env: +-- LOADER_BIN loader payload (default dwt/loader/build/loader.bin) +-- LOADER_COFF app COFF (default dwt/loader/build/marker.coff) +-- LOADER_BUF COFF buffer bit address (default 0x01900000) +-- LOADER_AT injection frame (default 2500) +-- LOADER_END inspection/exit frame (default 2700) +-- LOADER_OUT evidence directory (default dwt/loader/evidence) +-- LOADER_SNAP 1 = write a PNG at LOADER_END (default 0) +-- LOADER_VERBOSE 1 = print the result block every 25 frames + +local function hex(v, w) return string.format("%0" .. (w or 8) .. "X", v) end + +local machine = manager.machine +local FMT_MARK = tonumber(os.getenv("FMT_MARK") or "0x01A00000") +local cpu = machine.devices[':maincpu'] +local dbg = machine.debugger +local prog = cpu.spaces['program'] + +if not dbg then + print("LOADER: debugger not enabled; pass -debug") + machine:exit() + return +end + +local AT = tonumber(os.getenv("LOADER_AT") or "2500") +local ENDF = tonumber(os.getenv("LOADER_END") or "2700") +local BIN = os.getenv("LOADER_BIN") or "dwt/loader/build/loader.bin" +local COFF = os.getenv("LOADER_COFF") or "dwt/loader/build/marker.coff" +local BUF = tonumber(os.getenv("LOADER_BUF") or "0x01900000") +local OUT = os.getenv("LOADER_OUT") or "dwt/loader/evidence" +local SNAP = os.getenv("LOADER_SNAP") == "1" +local MODE = tonumber(os.getenv("LOADER_MODE") or "0") +local A8DESC = os.getenv("LOADER_A8_DESC") == "1" +local VERBOSE = os.getenv("LOADER_VERBOSE") == "1" + +local LOADER_BASE = 0x01880000 +local DESC = 0x01882C00 +local RESULT = 0x01882D00 +local RETURN_STUB = 0x01883100 +local RETURN_SP = 0x01883110 + +local function rd(p) return io.open(p, "rb") end +local function slurp(p) + local f, err = rd(p) + if not f then error("cannot open " .. p .. ": " .. tostring(err)) end + local s = f:read("*a") + f:close() + return s +end + +local function u16(s, o) + return s:byte(o + 1) + s:byte(o + 2) * 256 +end + +local function u32(s, o) + return s:byte(o + 1) + s:byte(o + 2) * 256 + + s:byte(o + 3) * 65536 + s:byte(o + 4) * 16777216 +end + +local function parse_coff(d) + local c = { data = d } + c.magic = u16(d, 0) + c.nscns = u16(d, 2) + c.opthdr = u16(d, 16) + c.sections = {} + if c.opthdr > 0 then + c.optmagic = u16(d, 20) + c.entry = u32(d, 20 + 16) + c.textstart = u32(d, 20 + 20) + c.datastart = u32(d, 20 + 24) + end + local base = 20 + c.opthdr + for i = 0, c.nscns - 1 do + local o = base + i * 40 + local name = d:sub(o + 1, o + 8):gsub("%z", "") + c.sections[#c.sections + 1] = { + name = name, + paddr = u32(d, o + 8), + size = u32(d, o + 16), -- bits + scnptr = u32(d, o + 20), + flags = u32(d, o + 36), + } + end + return c +end + +local function poke_bytes(bitaddr, data) + local n = math.floor((#data + 1) / 2) + for i = 0, n - 1 do + local lo = data:byte(i * 2 + 1) or 0 + local hi = data:byte(i * 2 + 2) or 0 + prog:write_u16(bitaddr + i * 16, lo + hi * 256) + end + return n +end + +local function verify_bytes(bitaddr, data) + local n = math.floor((#data + 1) / 2) + local bad = 0 + for i = 0, n - 1 do + local lo = data:byte(i * 2 + 1) or 0 + local hi = data:byte(i * 2 + 2) or 0 + if prog:read_u16(bitaddr + i * 16) ~= lo + hi * 256 then + bad = bad + 1 + end + end + return n, bad +end + +local function w32(a, v) prog:write_u32(a, v) end +local function r32(a) + local ok, v = pcall(function() return prog:read_u32(a) end) + if ok then return v end + return 0xDEADBEEF +end + +local function rstr(a, n) + local t = {} + for i = 0, n - 1 do + local c = prog:read_u8(a + i * 8) + if c == 0 then break end + t[#t + 1] = string.char(c) + end + return table.concat(t) +end + +local loader = slurp(BIN) +local coff = slurp(COFF) +local app = parse_coff(coff) + +print(string.format("LOADER: payload %s (%d B) -> bit %s", BIN, #loader, hex(LOADER_BASE))) +print(string.format("LOADER: COFF %s (%d B) -> bit %s", COFF, #coff, hex(BUF))) +print(string.format("LOADER: file magic=%04X nscns=%d opthdr=%d optmagic=%s entry=%s textstart=%s", + app.magic, app.nscns, app.opthdr, + app.optmagic and string.format("%04X", app.optmagic) or "none", + hex(app.entry or 0), hex(app.textstart or 0))) +for _, s in ipairs(app.sections) do + print(string.format("LOADER: sec %-8s paddr=%s size=%d bits (%d B) scnptr=%#x flags=%#x", + s.name, hex(s.paddr), s.size, math.floor(s.size / 8), s.scnptr, s.flags)) +end + +local TRACE = os.getenv("LOADER_TRACE") == "1" + +local frames = 0 +local injected = false +local done = false +local last_count = -1 + +local function status_name(v) + local t = { + [0] = "OK", + [1] = "bad descriptor", + [2] = "truncated COFF / section headers", + [3] = "bad COFF magic", + [4] = "section size not whole words", + [5] = "target outside RAM", + [6] = "target overlaps loader page or COFF buffer", + [7] = "relocation records present (unsupported)", + [8] = "target not in a free-list block", + [9] = "no entry point", + } + return t[v] or ("error " .. tostring(v)) +end + +local function result_dump() + local st = r32(RESULT + 0x000) + print(string.format( + "LOADER: result status=%d (%s) entry=%s text=%s/%d bits data=%s/%d bits bss=%s/%d bits relocs=%d pcb=%s nscns=%d len=%d", + st, status_name(st), hex(r32(RESULT + 0x020)), + hex(r32(RESULT + 0x040)), r32(RESULT + 0x060), + hex(r32(RESULT + 0x080)), r32(RESULT + 0x0A0), + hex(r32(RESULT + 0x0C0)), r32(RESULT + 0x0E0), + r32(RESULT + 0x100), hex(r32(RESULT + 0x120)), + r32(RESULT + 0x140), r32(RESULT + 0x160))) +end + +local function inject() + -- 1. loader itself + local n, bad = 0, 0 + poke_bytes(LOADER_BASE, loader) + n, bad = verify_bytes(LOADER_BASE, loader) + print(string.format("LOADER: loader poked %d words, readback %d bad at %s", + n, bad, hex(LOADER_BASE))) + + -- 2. COFF image + poke_bytes(BUF, coff) + n, bad = verify_bytes(BUF, coff) + print(string.format("LOADER: COFF poked %d words, readback %d bad at %s", + n, bad, hex(BUF))) + + -- 3. pre-fill every .bss section with A5A5, so a loaded zero is meaningful + for _, s in ipairs(app.sections) do + if s.flags & 0x80 ~= 0 then + for i = 0, math.floor(s.size / 16) - 1 do + prog:write_u16(s.paddr + i * 16, 0xA5A5) + end + print(string.format("LOADER: pre-filled .bss %s..%s with A5A5", + hex(s.paddr), hex(s.paddr + s.size))) + end + end + + -- 4. descriptor: magic "LD" "R1", buf, len, mode=0 (spin) + w32(DESC + 0x00, 0) + prog:write_u16(DESC + 0x00, 0x4C44) + prog:write_u16(DESC + 0x10, 0x5231) + w32(DESC + 0x20, BUF) + w32(DESC + 0x40, #coff) + w32(DESC + 0x60, MODE) + print(string.format("LOADER: descriptor @%s magic=%04X/%04X buf=%s len=%d mode=%d", + hex(DESC), prog:read_u16(DESC + 0x00), prog:read_u16(DESC + 0x10), + hex(r32(DESC + 0x20)), r32(DESC + 0x40), r32(DESC + 0x60))) + + -- 5. return stub. mode 0 ignores it (the loader jumps to ORIG_PC + -- itself); mode 1 returns here, and the stub resumes the interrupted + -- PC so the caller yields and the scheduler can run the child. + local orig_pc = cpu.state['PC'].value + if MODE == 1 then + prog:write_u16(RETURN_STUB, 0xC080) -- jauc + prog:write_u16(RETURN_STUB + 0x10, orig_pc & 0xFFFF) + prog:write_u16(RETURN_STUB + 0x20, math.floor(orig_pc / 0x10000) & 0xFFFF) + else + prog:write_u16(RETURN_STUB, 0xC0FF) -- jruc self + end + + -- 6. enter the loader. A8=0 selects the fixed descriptor; SP points at + -- the return slot so mode=1 would return to the spin stub. + -- Leave SP alone for mode 0: the loader pushes onto the current + -- process stack, then restores it and resumes at ORIG_PC. (mode 1 + -- would use the return stub.) + w32(0x01882A20, orig_pc) -- LD_ORIG_PC + print(string.format("LOADER: will resume at %s after create_process", hex(orig_pc))) + cpu.state['A8'].value = A8DESC and DESC or 0 + if MODE == 1 then + -- Simulate a CALLA from the current process: push the return + -- address on the *firmware* stack (a real transport would do this). + local sp = cpu.state['SP'].value - 0x20 + w32(sp, RETURN_STUB) + cpu.state['SP'].value = sp + end + cpu.state['PC'].value = LOADER_BASE + injected = true + print(string.format("LOADER: frame %d: entered loader at %s (caller SP=%s)", + frames, hex(LOADER_BASE), hex(cpu.state['SP'].value))) +end + +local function inspect() + result_dump() + + -- Verify the placement from RAM: .text/.data must equal the file bytes. + for _, s in ipairs(app.sections) do + if s.scnptr ~= 0 and s.size > 0 then + local nb = math.floor(s.size / 8) + local bytes = coff:sub(s.scnptr + 1, s.scnptr + nb) + local _, bad = verify_bytes(s.paddr, bytes) + print(string.format("LOADER: placed %s at %s: %d bytes, %d differing", + s.name, hex(s.paddr), nb, bad)) + end + end + + local base = COFF:match("([^/]+)$") or COFF + if base:find("marker") then + print(string.format("LOADER: marker app: start=%s data=%s data2=%s bss=%s count=%d", + hex(r32(0x018E0000)), hex(r32(0x018E0020)), hex(r32(0x018E0040)), + hex(r32(0x018E0060)), r32(0x018E0080))) + elseif base:find("xhello") then + print(string.format("LOADER: xhello: stage=%d strlen=%d display=%s root=%s window=%s sync=%s", + r32(0x01890140), r32(0x01890160), hex(r32(0x01890100)), + hex(r32(0x01890110)), hex(r32(0x01890120)), hex(r32(0x01890130)))) + end + if base:find("fmt") then + for i = 0, 5 do + print(string.format("FMT:%d [%s]", i, rstr(FMT_MARK + i * 0x80, 16))) + end + end + print(string.format("LOADER: PC at inspection = %s", hex(cpu.state['PC'].value))) +end + +emu.register_frame_done(function() + frames = frames + 1 + if frames == AT and not injected then + inject() + if TRACE then dbg:command("trace /tmp/opencode/ld1.tr") end + end + if injected and TRACE and frames == AT + 1 then + dbg:command("trace off") + print("LOADER: trace written to /tmp/opencode/ld.tr") + end + if injected and not done then + if TRACE and frames <= AT + 40 then + print(string.format("LOADER: trace f=%d PC=%s SP=%s A14=%s status=%s entry=%s", + frames, hex(cpu.state['PC'].value), hex(cpu.state['SP'].value), + hex(cpu.state['A14'].value), hex(r32(RESULT + 0x000)), + hex(r32(RESULT + 0x020)))) + end + local st = r32(RESULT + 0x000) + -- status 0 means OK; nonzero is final too once parsing has begun + if st ~= 0xFFFFFFFF and not done then + -- The loader writes 0 on success. A nonzero status is final + -- only after it has been written once (it is 0 from power-on, + -- so require the loader to have run: PC no longer at entry or + -- the app counter moved). + end + if VERBOSE and frames % 25 == 0 then + result_dump() + end + if base_contains_marker then + local c = r32(0x018E0080) + if c ~= last_count then + if last_count ~= -1 then + print(string.format("LOADER: marker counter %d -> %d (child running)", + last_count, c)) + end + last_count = c + end + end + end + if frames >= ENDF then + if injected then + done = true + inspect() + if SNAP then + local screen = nil + for _, s in pairs(machine.screens) do screen = s break end + if screen then + local out = OUT .. "/loader_frame" .. frames .. ".png" + local ok, err = pcall(function() return screen:snapshot(out) end) + print(string.format("LOADER: snapshot %s ok=%s %s", + out, tostring(ok), tostring(err or ""))) + end + end + end + machine:exit() + end +end, "vt1200-loader") + +-- (Lua has no forward use of a name defined later; set a small flag here.) +base_contains_marker = (COFF:match("([^/]+)$") or ""):find("marker") ~= nil + +dbg.visible_cpu = cpu +dbg.execution_state = 'run' diff --git a/apps/termtris/tests/fmt.sh b/apps/termtris/tests/fmt.sh new file mode 100755 index 0000000..acb354b --- /dev/null +++ b/apps/termtris/tests/fmt.sh @@ -0,0 +1,35 @@ +#!/bin/sh +# fmt.sh -- Task 2 acceptance. Builds tests/fmt.c with compat.c, loads it, and +# checks the six formatted strings byte-for-byte. +set -eu + +DIR=$(cd "$(dirname "$0")" && pwd) +APP=$(cd "$DIR/.." && pwd) +ROOT=$(cd "$APP/../.." && pwd) +M=${MAME:-/home/arti/code/k-space/mame0289/vt1200} +LOG="$APP/build/fmt.log" + +mkdir -p "$APP/build" +"$APP/build.sh" "$DIR/fmt.c" "$APP/src/vt1200/compat.c" >"$APP/build/fmt-build.log" 2>&1 + +FMT=$(awk '/_out/ {print $1; exit}' "$APP/build/fmt.map") +VT1200_SKIP_LEVEL1=1 FMT_MARK="0x$FMT" \ +LOADER_AT=2500 LOADER_END=2700 \ +LOADER_BIN="$ROOT/dwt/loader/build/loader.bin" \ +LOADER_COFF="$APP/build/fmt.coff" \ +LOADER_OUT="$APP/build" \ +"$M" vt1200 -rompath "$ROOT/harness/mame/roms" \ + -video none -sound none -nothrottle -skip_gameinfo \ + -debug -debugger none \ + -autoboot_script "$DIR/fmt.lua" -autoboot_delay 0 \ + >"$LOG" 2>&1 || true + +grep 'FMT:' "$LOG" | tail -8 +for want in 'FMT:0 [ 12345]' 'FMT:1 [ 7]' 'FMT:2 [ 42]' \ + 'FMT:3 [ 1. ab 99]' 'FMT:4 [x ]' 'FMT:5 [%]'; do + grep -Fq "$want" "$LOG" || { + echo "fmt.sh: missing: $want (see $LOG)" >&2 + exit 1 + } +done +echo "fmt: PASS" -- 2.47.3 From 264b53a6a08dcda85415667f015343c02af58590 Mon Sep 17 00:00:00 2001 From: "opencode.energia.arti.ee" Date: Tue, 3 Nov 2026 19:00:42 +0000 Subject: [PATCH 05/18] feat(termtris): 80x24 cell-grid X11 renderer (6x13 cells) --- apps/termtris/build/render.coff | Bin 0 -> 3788 bytes apps/termtris/build/render.map | 85 +++++ apps/termtris/evidence/render_frame3400.png | Bin 0 -> 19845 bytes apps/termtris/src/vt1200/x11.c | 190 +++++++++++ apps/termtris/src/vt1200/x11.h | 25 ++ apps/termtris/tests/render.c | 59 ++++ apps/termtris/tests/render.lua | 347 ++++++++++++++++++++ apps/termtris/tests/render.sh | 29 ++ 8 files changed, 735 insertions(+) create mode 100644 apps/termtris/build/render.coff create mode 100644 apps/termtris/build/render.map create mode 100644 apps/termtris/evidence/render_frame3400.png create mode 100644 apps/termtris/src/vt1200/x11.c create mode 100644 apps/termtris/src/vt1200/x11.h create mode 100644 apps/termtris/tests/render.c create mode 100644 apps/termtris/tests/render.lua create mode 100755 apps/termtris/tests/render.sh diff --git a/apps/termtris/build/render.coff b/apps/termtris/build/render.coff new file mode 100644 index 0000000000000000000000000000000000000000..e00108eea8a5beaf981a4766b570c1bd1a3f4ed0 GIT binary patch literal 3788 zcmbVPe@t7~6~52lnOoP??J~&;?dG(2j$va-)@}*ihMVF*h+pCo+GRf^n}7!{#317& zRN5q$33-^Ui9l7GsZu==hG-Iu)*=5j&6+?oCX`H_kQ!~0|6#;yW3g$OHf`(MckZ+C zdu-9xJ;{%A&yVk%d+vAdyIf+G?B{R(;l(3WjI}Yw9%WBRRnk3as}Tb{v0D<}3B1%_ zLo_%X6&7_&ez(N9{IXXv*3cCShv2x?m;hEnO2XWL9NNc?LBpA$Ayt<|j-;gK(|Vx> zBmBBMqn$!sszvWZBRBm_1x`4ta-LIOpRxYc%52?hb(fES3cX-@WA1}hR$fM4Vt z@S060l~&``g_yE7HTPadnR_p%_$}7YBqZlWb9QYiJlSh>zUnj&6JtyS({qsgRC&I%%8wpt%f`{_RMX2 zEWu-LqGYYNj#+-Ew0U^V+7#iOm@FZwetM)OeiE@eeM(q$!1}GyW;?QH+~(fxvbCB2 zsXUNvGk>UcV_QtoH{+dbg=}!lEho@ccXg_v}?LTnv zi%&lFr7s^k%$_;&Y_sQROY5<=<6im1N#Aqrr#ha0;p?Zr@lD1q@?C6IzM9S{ed$T}b_7P8f$eTMv%M9hN>%c+zPrOtj#)%nwP0 zi4~rc!jsG3LjfzhHr0TrTc4z8k}lQ!2Jyr!E+0hvCVz-M4$p?k60>~Yd%sCt|H4G| zI(e2XZ16H-?a)-oyAZ3l22QRcj$%}X&j028raN9k<{hpmVE+TOgN9%JoUPB3=K-t` zvt@bA+WmR zr;cI|$4yCi&ZaZUm?;CPkd9F=#hxqqkhM=XEgm;-D$?Qydt7qm63_H`T=FLtkC{_S z<>C!+TFSUQ<09{vs6mSqHLik%Z!Kh$tHR@HfputUR(L)sP!22w8T@ybPlsiHi!<0Qz<7-rHt~wokHJDt;*HrK!wXwFFS2HQhX?YQ0{=(PkzhRqi-~+RtKzq zTyc3A*j;Cxi92`Q9bqf75|lY&b|PT;)OvjQLx|<)|EwogMo^{F z-091s=88Q+cPQ=iv2x*Sq|QA+)6+fL8b;V*tNBgT%?A$aZ)`7<{yf7 zM#N=R%?{1k&QKUOx&cxZYWJuMv7&>#d ztG5#aghd7hqR1NV9=Na#GZ-1@i3IVFm|7FWAj+VX_eWnE3>HNVs4(eMYV&CUC~Bae sDiJ|y#cF)9LRDWXQH%!9MTeqly);ctBn``um`^B-UEbgCAMRoQ0wPc!NdN!< literal 0 HcmV?d00001 diff --git a/apps/termtris/build/render.map b/apps/termtris/build/render.map new file mode 100644 index 0000000..da7b319 --- /dev/null +++ b/apps/termtris/build/render.map @@ -0,0 +1,85 @@ +***************************************************** +TMS340 COFF Linker Version 5.01 +***************************************************** +Tue Nov 03 19:00:04 2026 + +OUTPUT FILE NAME: +ENTRY POINT SYMBOL: "_start" address: 018a0000 + + +MEMORY CONFIGURATION + + name origin length attributes + -------- -------- --------- ---------- + prog 018a0000 000020000 RWIX + ram 01a00000 000040000 RWIX + + +SECTION ALLOCATION MAP + + output attributes/ +section page origin length input sections +-------- ---- ---------- ---------- ---------------- +.text 0 018a0000 00003af0 + 018a0000 00000150 ENTRY.obj (.text) + 018a0150 00001280 RENDER.obj (.text) + 018a13d0 00002510 X11.obj (.text) + 018a38e0 00000210 XGFIX.obj (.text) + +.cinit 0 018a3af0 000000a0 + 018a3af0 00000050 RENDER.obj (.cinit) + 018a3b40 00000050 X11.obj (.cinit) + +.bss 0 01a00000 0001d080 UNINITIALIZED + 01a00000 00008000 ENTRY.obj (.bss) + 01a08000 00000260 RENDER.obj (.bss) + 01a08260 00012e20 X11.obj (.bss) + 01a1b080 00002000 XGFIX.obj (.bss) + +.data 0 018a0000 00000000 UNINITIALIZED + + +GLOBAL SYMBOLS + +address name address name +-------- ---- -------- ---- +01a00000 .bss 018a0000 .text +018a0000 .data 018a0000 .data +018a0000 .text 018a0000 edata +018a0370 _main 018a0000 _start +01a08000 _mark 018a0370 _main +018a0000 _start 018a13d0 _vt_root_window +01a1afa0 _term_clearscr 018a14b0 _vt_x11_init +01a1afe0 _term_cursor 018a1b60 _x11_clearscr +01a08220 _term_height 018a1d50 _x11_reset +01a1b020 _term_ibmchar 018a1d90 _x11_cursor +018a3870 _term_init 018a1df0 _x11_setcolor +018a2600 _term_putstr 018a1f10 _x11_setcursor +01a1af80 _term_reset 018a2160 _x11_ibmchar +01a1b000 _term_setcolor 018a2600 _term_putstr +01a1afc0 _term_setcursor 018a27a0 _vt_x11_flush +01a1b060 _term_type 018a3550 _vt_x11_term_init +01a08200 _term_width 018a3870 _term_init +01a1b040 _termenv 018a38e0 _vt_install_xg +01a1aee0 _vt_dpy 018a3af0 etext +01a1af20 _vt_gc 018a3af0 cinit +018a38e0 _vt_install_xg 01a00000 .bss +018a13d0 _vt_root_window 01a08000 _mark +01a1af00 _vt_win 01a08200 _term_width +01a1af60 _vt_x11_drain_events 01a08220 _term_height +018a27a0 _vt_x11_flush 01a1aee0 _vt_dpy +018a14b0 _vt_x11_init 01a1af00 _vt_win +01a1af40 _vt_x11_progress 01a1af20 _vt_gc +018a3550 _vt_x11_term_init 01a1af40 _vt_x11_progress +018a1b60 _x11_clearscr 01a1af60 _vt_x11_drain_events +018a1d90 _x11_cursor 01a1af80 _term_reset +018a2160 _x11_ibmchar 01a1afa0 _term_clearscr +018a1d50 _x11_reset 01a1afc0 _term_setcursor +018a1df0 _x11_setcolor 01a1afe0 _term_cursor +018a1f10 _x11_setcursor 01a1b000 _term_setcolor +018a3af0 cinit 01a1b020 _term_ibmchar +018a0000 edata 01a1b040 _termenv +01a1d080 end 01a1b060 _term_type +018a3af0 etext 01a1d080 end + +[38 symbols] diff --git a/apps/termtris/evidence/render_frame3400.png b/apps/termtris/evidence/render_frame3400.png new file mode 100644 index 0000000000000000000000000000000000000000..dfc882f79cf41180dd315f52b75d34e04c2c59cd GIT binary patch literal 19845 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 literal 0 HcmV?d00001 diff --git a/apps/termtris/src/vt1200/x11.c b/apps/termtris/src/vt1200/x11.c new file mode 100644 index 0000000..d2ea2bc --- /dev/null +++ b/apps/termtris/src/vt1200/x11.c @@ -0,0 +1,190 @@ +/* + * x11.c -- a small character-cell terminal that draws through the VT1200 ROM + * Xlib. The game's term_* callbacks feed an 80x24 grid; vt_x11_flush() paints + * only the dirty cells with XFillRectangle (background) + XDrawString (glyph). + * + * Geometry is the default GC's font, measured on the real server: 6x13 cells, + * baseline +10. Do not change the cell metrics without re-measuring. + */ +#include "xlib.h" +#include "x11.h" +#include "term.h" +#include "game.h" + +#define COLS 80 +#define ROWS 24 +#define CELL_W 6 +#define CELL_H 13 +#define BASE_OFF 10 + +struct vt_cell { + unsigned short ch; + unsigned char fg, bg, dirty; +}; + +static struct vt_cell cells[ROWS * COLS]; +static int cur_row, cur_col; +static int cur_fg, cur_bg; + +void *vt_dpy; +vt_u32 vt_win; +vt_u32 vt_gc; +volatile vt_u32 vt_x11_progress; +int vt_x11_drain_events; + +/* term.h's callback objects (term.c is not compiled for the VT1200). */ +void (*term_reset)(void); +void (*term_clearscr)(void); +void (*term_setcursor)(int row, int col); +void (*term_cursor)(int show); +void (*term_setcolor)(int fg, int bg); +void (*term_ibmchar)(unsigned char c, unsigned char attr); +char *termenv; +int term_type = TERM_ANSI; + +vt_u32 vt_root_window(void *dpy) +{ + vt_u32 screen; + screen = *(vt_u32 *)((char *)dpy + 0x90); + return *(vt_u32 *)((char *)screen + 8); +} + +int vt_x11_init(void) +{ + if(!vt_install_xg()) + return -1; + vt_dpy = VT_XOPEN("0::0"); + if(!vt_dpy) + return -1; + vt_win = VT_XCREATE(vt_dpy, vt_root_window(vt_dpy), 80, 80, 480, 312, 2, 1, 0); + if(!vt_win) + return -1; + VT_XSELECT(vt_dpy, vt_win, 0x22000FUL); + vt_gc = VT_XGC(vt_dpy, vt_win, 0, 0); + if(!vt_gc) + return -1; + VT_XMAP(vt_dpy, vt_win); + VT_XSYNC(vt_dpy, 0); + return 0; +} + +void x11_clearscr(void) +{ + int i; + for(i = 0; i < ROWS * COLS; i++) { + cells[i].ch = ' '; + cells[i].fg = 0; + cells[i].bg = 0; + cells[i].dirty = 1; + } + cur_row = cur_col = 0; +} + +void x11_reset(void) +{ + x11_clearscr(); +} + +void x11_cursor(int show) +{ + (void)show; +} + +void x11_setcolor(int fg, int bg) +{ + cur_fg = fg; + cur_bg = bg; +} + +void x11_setcursor(int row, int col) +{ + if(row < 0) row = 0; + if(row >= ROWS) row = ROWS - 1; + if(col < 0) col = 0; + if(col >= COLS) col = COLS - 1; + cur_row = row; + cur_col = col; +} + +void x11_ibmchar(unsigned char c, unsigned char attr) +{ + struct vt_cell *p; + if(cur_row < 0 || cur_row >= ROWS) return; + if(cur_col < 0 || cur_col >= COLS) return; + p = &cells[cur_row * COLS + cur_col]; + p->ch = c; + p->fg = (attr >> 4) & 0xf; + p->bg = attr & 0xf; + p->dirty = 1; + cur_col++; + if(cur_col >= COLS) { + cur_col = 0; + cur_row++; + } +} + +void term_putstr(const char *s, unsigned char attr) +{ + while(*s) { + x11_ibmchar((unsigned char)*s++, attr); + } +} + +void vt_x11_flush(void) +{ + int i, row_dirty = 0, dirty_any = 0; + char one[2]; + vt_x11_progress = 1; + for(i = 0; i < ROWS * COLS; i++) { + struct vt_cell *p = &cells[i]; + vt_x11_progress = 0x100 + (vt_u32)i; + if(p->dirty) { + int x, y, bgpx, fgpx; + x = (i % COLS) * CELL_W; + y = (i / COLS) * CELL_H; + bgpx = p->bg == 0 ? 0 : 1; + fgpx = p->fg == 0 ? 0 : 1; + VT_XSETFG(vt_dpy, vt_gc, (vt_u32)bgpx); + VT_XFILL(vt_dpy, vt_win, vt_gc, x, y, CELL_W, CELL_H); + VT_XSETFG(vt_dpy, vt_gc, (vt_u32)fgpx); + one[0] = (char)p->ch; + one[1] = 0; + VT_XDRAW(vt_dpy, vt_win, vt_gc, x, y + BASE_OFF, one, 1); + p->dirty = 0; + row_dirty = 1; + dirty_any = 1; + } + if((i % COLS) == COLS - 1 && row_dirty) { + vt_u32 ev[24]; + vt_x11_progress = 0x10000 + (vt_u32)(i / COLS); + VT_XSYNC(vt_dpy, 0); + if(vt_x11_drain_events) { + while(*(vt_u32 *)((char *)vt_dpy + 0x54) != 0) { + VT_XNEXT(vt_dpy, (void *)ev); + } + } + row_dirty = 0; + } + } + if(dirty_any) { + VT_XSYNC(vt_dpy, 0); + } + vt_x11_progress = 0xFFFFFFUL; +} + +void vt_x11_term_init(void) +{ + term_width = COLS; + term_height = ROWS; + term_reset = x11_reset; + term_clearscr = x11_clearscr; + term_setcursor = x11_setcursor; + term_cursor = x11_cursor; + term_setcolor = x11_setcolor; + term_ibmchar = x11_ibmchar; +} + +void term_init(void) +{ + vt_x11_term_init(); +} diff --git a/apps/termtris/src/vt1200/x11.h b/apps/termtris/src/vt1200/x11.h new file mode 100644 index 0000000..6ac8351 --- /dev/null +++ b/apps/termtris/src/vt1200/x11.h @@ -0,0 +1,25 @@ +/* + * x11.h -- the VT1200 cell-grid X11 backend. + */ +#ifndef VT_X11_H_ +#define VT_X11_H_ + +#include "xlib.h" + +extern void *vt_dpy; +extern vt_u32 vt_win; +extern vt_u32 vt_gc; +extern int vt_x11_drain_events; + +int vt_x11_init(void); +void vt_x11_flush(void); +void vt_x11_term_init(void); + +void x11_reset(void); +void x11_clearscr(void); +void x11_setcursor(int row, int col); +void x11_cursor(int show); +void x11_setcolor(int fg, int bg); +void x11_ibmchar(unsigned char c, unsigned char attr); + +#endif diff --git a/apps/termtris/tests/render.c b/apps/termtris/tests/render.c new file mode 100644 index 0000000..ccb4e2c --- /dev/null +++ b/apps/termtris/tests/render.c @@ -0,0 +1,59 @@ +/* + * render.c -- Task 3 acceptance: draw a border, a playfield outline and + * TERMTRIS through the cell-grid backend, then keep the window alive so the + * loader can snapshot it. + */ +#include "xlib.h" +#include "x11.h" + +extern int vt_install_xg(void); + +volatile vt_u32 mark[16]; +int term_width, term_height; +static const char *word = "TERMTRIS"; + +static void put(int row, int col, const char *s, unsigned char attr) +{ + x11_setcursor(row, col); + while(*s) x11_ibmchar((unsigned char)*s++, attr); +} + +int main(void) +{ + int r, c; + + mark[0] = 0x524E4452UL; /* RNDR */ + if(vt_x11_init() != 0) return 1; + mark[1] = (vt_u32)vt_dpy; + mark[2] = vt_win; + mark[3] = vt_gc; + mark[5] = (vt_u32)VT_XPENDING(vt_dpy); + + x11_clearscr(); + for(c = 0; c < 80; c++) { + x11_setcursor(0, c); x11_ibmchar('#', 0x70); + x11_setcursor(23, c); x11_ibmchar('#', 0x70); + } + for(r = 0; r < 24; r++) { + x11_setcursor(r, 0); x11_ibmchar('#', 0x70); + x11_setcursor(r, 79); x11_ibmchar('#', 0x70); + } + for(r = 2; r < 20; r++) { + for(c = 4; c <= 13; c++) { + int edge = (r == 2 || r == 19 || c == 4 || c == 13); + x11_setcursor(r, c); + x11_ibmchar(edge ? '#' : '.', edge ? 0x70 : 0x07); + } + } + put(1, 20, word, 0x70); + put(21, 20, "0123456789", 0x70); + put(22, 20, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 0x70); + vt_x11_drain_events = 1; + vt_x11_flush(); + mark[4] = 0x464C5553UL; /* FLUS */ + + for(;;) { + VT_SLEEP(100); + } + return 0; +} diff --git a/apps/termtris/tests/render.lua b/apps/termtris/tests/render.lua new file mode 100644 index 0000000..2931358 --- /dev/null +++ b/apps/termtris/tests/render.lua @@ -0,0 +1,347 @@ +-- loader.lua -- debugger-assisted COFF loader acceptance harness. +-- +-- Boots the firmware with VT1200_SKIP_LEVEL1=1 (the documented divergence) to +-- the OS, then at frame LOADER_AT pokes: +-- * the loader payload at bit 0x01880000 (raw dwt/loader/build/loader.bin) +-- * a GSPLNK COFF image at LOADER_BUF (default bit 0x01900000) +-- * the loader descriptor at 0x01882C00 +-- and sets PC = 0x01880000. It does not use the network, the rdebug service +-- or any firmware transport: this is the debugger-assisted milestone. +-- +-- The loader itself copies the COFF sections to their linked bit addresses and +-- calls create_process(entry, "LOADER", 0x6000, 1, 0). This script only pokes +-- the input and reads the result; it never writes the app's code or markers. +-- +-- Env: +-- LOADER_BIN loader payload (default dwt/loader/build/loader.bin) +-- LOADER_COFF app COFF (default dwt/loader/build/marker.coff) +-- LOADER_BUF COFF buffer bit address (default 0x01900000) +-- LOADER_AT injection frame (default 2500) +-- LOADER_END inspection/exit frame (default 2700) +-- LOADER_OUT evidence directory (default dwt/loader/evidence) +-- LOADER_SNAP 1 = write a PNG at LOADER_END (default 0) +-- LOADER_VERBOSE 1 = print the result block every 25 frames + +local function hex(v, w) return string.format("%0" .. (w or 8) .. "X", v) end + +local machine = manager.machine +local RENDER_MARK = tonumber(os.getenv("RENDER_MARK") or "0x01A08000") +local X11P = tonumber(os.getenv("X11_PROGRESS") or "0") +local cpu = machine.devices[':maincpu'] +local dbg = machine.debugger +local prog = cpu.spaces['program'] + +if not dbg then + print("LOADER: debugger not enabled; pass -debug") + machine:exit() + return +end + +local AT = tonumber(os.getenv("LOADER_AT") or "2500") +local ENDF = tonumber(os.getenv("LOADER_END") or "2700") +local BIN = os.getenv("LOADER_BIN") or "dwt/loader/build/loader.bin" +local COFF = os.getenv("LOADER_COFF") or "dwt/loader/build/marker.coff" +local BUF = tonumber(os.getenv("LOADER_BUF") or "0x01900000") +local OUT = os.getenv("LOADER_OUT") or "dwt/loader/evidence" +local SNAP = os.getenv("LOADER_SNAP") == "1" +local MODE = tonumber(os.getenv("LOADER_MODE") or "0") +local A8DESC = os.getenv("LOADER_A8_DESC") == "1" +local VERBOSE = os.getenv("LOADER_VERBOSE") == "1" + +local LOADER_BASE = 0x01880000 +local DESC = 0x01882C00 +local RESULT = 0x01882D00 +local RETURN_STUB = 0x01883100 +local RETURN_SP = 0x01883110 + +local function rd(p) return io.open(p, "rb") end +local function slurp(p) + local f, err = rd(p) + if not f then error("cannot open " .. p .. ": " .. tostring(err)) end + local s = f:read("*a") + f:close() + return s +end + +local function u16(s, o) + return s:byte(o + 1) + s:byte(o + 2) * 256 +end + +local function u32(s, o) + return s:byte(o + 1) + s:byte(o + 2) * 256 + + s:byte(o + 3) * 65536 + s:byte(o + 4) * 16777216 +end + +local function parse_coff(d) + local c = { data = d } + c.magic = u16(d, 0) + c.nscns = u16(d, 2) + c.opthdr = u16(d, 16) + c.sections = {} + if c.opthdr > 0 then + c.optmagic = u16(d, 20) + c.entry = u32(d, 20 + 16) + c.textstart = u32(d, 20 + 20) + c.datastart = u32(d, 20 + 24) + end + local base = 20 + c.opthdr + for i = 0, c.nscns - 1 do + local o = base + i * 40 + local name = d:sub(o + 1, o + 8):gsub("%z", "") + c.sections[#c.sections + 1] = { + name = name, + paddr = u32(d, o + 8), + size = u32(d, o + 16), -- bits + scnptr = u32(d, o + 20), + flags = u32(d, o + 36), + } + end + return c +end + +local function poke_bytes(bitaddr, data) + local n = math.floor((#data + 1) / 2) + for i = 0, n - 1 do + local lo = data:byte(i * 2 + 1) or 0 + local hi = data:byte(i * 2 + 2) or 0 + prog:write_u16(bitaddr + i * 16, lo + hi * 256) + end + return n +end + +local function verify_bytes(bitaddr, data) + local n = math.floor((#data + 1) / 2) + local bad = 0 + for i = 0, n - 1 do + local lo = data:byte(i * 2 + 1) or 0 + local hi = data:byte(i * 2 + 2) or 0 + if prog:read_u16(bitaddr + i * 16) ~= lo + hi * 256 then + bad = bad + 1 + end + end + return n, bad +end + +local function w32(a, v) prog:write_u32(a, v) end +local function r32(a) + local ok, v = pcall(function() return prog:read_u32(a) end) + if ok then return v end + return 0xDEADBEEF +end + +local loader = slurp(BIN) +local coff = slurp(COFF) +local app = parse_coff(coff) + +print(string.format("LOADER: payload %s (%d B) -> bit %s", BIN, #loader, hex(LOADER_BASE))) +print(string.format("LOADER: COFF %s (%d B) -> bit %s", COFF, #coff, hex(BUF))) +print(string.format("LOADER: file magic=%04X nscns=%d opthdr=%d optmagic=%s entry=%s textstart=%s", + app.magic, app.nscns, app.opthdr, + app.optmagic and string.format("%04X", app.optmagic) or "none", + hex(app.entry or 0), hex(app.textstart or 0))) +for _, s in ipairs(app.sections) do + print(string.format("LOADER: sec %-8s paddr=%s size=%d bits (%d B) scnptr=%#x flags=%#x", + s.name, hex(s.paddr), s.size, math.floor(s.size / 8), s.scnptr, s.flags)) +end + +local TRACE = os.getenv("LOADER_TRACE") == "1" + +local frames = 0 +local injected = false +local done = false +local last_count = -1 + +local function status_name(v) + local t = { + [0] = "OK", + [1] = "bad descriptor", + [2] = "truncated COFF / section headers", + [3] = "bad COFF magic", + [4] = "section size not whole words", + [5] = "target outside RAM", + [6] = "target overlaps loader page or COFF buffer", + [7] = "relocation records present (unsupported)", + [8] = "target not in a free-list block", + [9] = "no entry point", + } + return t[v] or ("error " .. tostring(v)) +end + +local function result_dump() + local st = r32(RESULT + 0x000) + print(string.format( + "LOADER: result status=%d (%s) entry=%s text=%s/%d bits data=%s/%d bits bss=%s/%d bits relocs=%d pcb=%s nscns=%d len=%d", + st, status_name(st), hex(r32(RESULT + 0x020)), + hex(r32(RESULT + 0x040)), r32(RESULT + 0x060), + hex(r32(RESULT + 0x080)), r32(RESULT + 0x0A0), + hex(r32(RESULT + 0x0C0)), r32(RESULT + 0x0E0), + r32(RESULT + 0x100), hex(r32(RESULT + 0x120)), + r32(RESULT + 0x140), r32(RESULT + 0x160))) +end + +local function inject() + -- 1. loader itself + local n, bad = 0, 0 + poke_bytes(LOADER_BASE, loader) + n, bad = verify_bytes(LOADER_BASE, loader) + print(string.format("LOADER: loader poked %d words, readback %d bad at %s", + n, bad, hex(LOADER_BASE))) + + -- 2. COFF image + poke_bytes(BUF, coff) + n, bad = verify_bytes(BUF, coff) + print(string.format("LOADER: COFF poked %d words, readback %d bad at %s", + n, bad, hex(BUF))) + + -- 3. pre-fill every .bss section with A5A5, so a loaded zero is meaningful + for _, s in ipairs(app.sections) do + if s.flags & 0x80 ~= 0 then + for i = 0, math.floor(s.size / 16) - 1 do + prog:write_u16(s.paddr + i * 16, 0xA5A5) + end + print(string.format("LOADER: pre-filled .bss %s..%s with A5A5", + hex(s.paddr), hex(s.paddr + s.size))) + end + end + + -- 4. descriptor: magic "LD" "R1", buf, len, mode=0 (spin) + w32(DESC + 0x00, 0) + prog:write_u16(DESC + 0x00, 0x4C44) + prog:write_u16(DESC + 0x10, 0x5231) + w32(DESC + 0x20, BUF) + w32(DESC + 0x40, #coff) + w32(DESC + 0x60, MODE) + print(string.format("LOADER: descriptor @%s magic=%04X/%04X buf=%s len=%d mode=%d", + hex(DESC), prog:read_u16(DESC + 0x00), prog:read_u16(DESC + 0x10), + hex(r32(DESC + 0x20)), r32(DESC + 0x40), r32(DESC + 0x60))) + + -- 5. return stub. mode 0 ignores it (the loader jumps to ORIG_PC + -- itself); mode 1 returns here, and the stub resumes the interrupted + -- PC so the caller yields and the scheduler can run the child. + local orig_pc = cpu.state['PC'].value + if MODE == 1 then + prog:write_u16(RETURN_STUB, 0xC080) -- jauc + prog:write_u16(RETURN_STUB + 0x10, orig_pc & 0xFFFF) + prog:write_u16(RETURN_STUB + 0x20, math.floor(orig_pc / 0x10000) & 0xFFFF) + else + prog:write_u16(RETURN_STUB, 0xC0FF) -- jruc self + end + + -- 6. enter the loader. A8=0 selects the fixed descriptor; SP points at + -- the return slot so mode=1 would return to the spin stub. + -- Leave SP alone for mode 0: the loader pushes onto the current + -- process stack, then restores it and resumes at ORIG_PC. (mode 1 + -- would use the return stub.) + w32(0x01882A20, orig_pc) -- LD_ORIG_PC + print(string.format("LOADER: will resume at %s after create_process", hex(orig_pc))) + cpu.state['A8'].value = A8DESC and DESC or 0 + if MODE == 1 then + -- Simulate a CALLA from the current process: push the return + -- address on the *firmware* stack (a real transport would do this). + local sp = cpu.state['SP'].value - 0x20 + w32(sp, RETURN_STUB) + cpu.state['SP'].value = sp + end + cpu.state['PC'].value = LOADER_BASE + injected = true + print(string.format("LOADER: frame %d: entered loader at %s (caller SP=%s)", + frames, hex(LOADER_BASE), hex(cpu.state['SP'].value))) +end + +local function inspect() + result_dump() + + -- Verify the placement from RAM: .text/.data must equal the file bytes. + for _, s in ipairs(app.sections) do + if s.scnptr ~= 0 and s.size > 0 then + local nb = math.floor(s.size / 8) + local bytes = coff:sub(s.scnptr + 1, s.scnptr + nb) + local _, bad = verify_bytes(s.paddr, bytes) + print(string.format("LOADER: placed %s at %s: %d bytes, %d differing", + s.name, hex(s.paddr), nb, bad)) + end + end + + local base = COFF:match("([^/]+)$") or COFF + if base:find("marker") then + print(string.format("LOADER: marker app: start=%s data=%s data2=%s bss=%s count=%d", + hex(r32(0x018E0000)), hex(r32(0x018E0020)), hex(r32(0x018E0040)), + hex(r32(0x018E0060)), r32(0x018E0080))) + elseif base:find("xhello") then + print(string.format("LOADER: xhello: stage=%d strlen=%d display=%s root=%s window=%s sync=%s", + r32(0x01890140), r32(0x01890160), hex(r32(0x01890100)), + hex(r32(0x01890110)), hex(r32(0x01890120)), hex(r32(0x01890130)))) + end + if base:find("render") then + print(string.format("RENDER: magic=%08X dpy=%08X win=%08X gc=%08X flus=%08X progress=%08X", + r32(RENDER_MARK), r32(RENDER_MARK + 0x20), r32(RENDER_MARK + 0x40), + r32(RENDER_MARK + 0x60), r32(RENDER_MARK + 0x80), + X11P ~= 0 and r32(X11P) or 0)) + print(string.format("RENDER: pending=%d", r32(RENDER_MARK + 0xA0))) + end + print(string.format("LOADER: PC at inspection = %s", hex(cpu.state['PC'].value))) +end + +emu.register_frame_done(function() + frames = frames + 1 + if frames == AT and not injected then + inject() + if TRACE then dbg:command("trace /tmp/opencode/ld1.tr") end + end + if injected and TRACE and frames == AT + 1 then + dbg:command("trace off") + print("LOADER: trace written to /tmp/opencode/ld.tr") + end + if injected and not done then + if TRACE and frames <= AT + 40 then + print(string.format("LOADER: trace f=%d PC=%s SP=%s A14=%s status=%s entry=%s", + frames, hex(cpu.state['PC'].value), hex(cpu.state['SP'].value), + hex(cpu.state['A14'].value), hex(r32(RESULT + 0x000)), + hex(r32(RESULT + 0x020)))) + end + local st = r32(RESULT + 0x000) + -- status 0 means OK; nonzero is final too once parsing has begun + if st ~= 0xFFFFFFFF and not done then + -- The loader writes 0 on success. A nonzero status is final + -- only after it has been written once (it is 0 from power-on, + -- so require the loader to have run: PC no longer at entry or + -- the app counter moved). + end + if VERBOSE and frames % 25 == 0 then + result_dump() + end + if base_contains_marker then + local c = r32(0x018E0080) + if c ~= last_count then + if last_count ~= -1 then + print(string.format("LOADER: marker counter %d -> %d (child running)", + last_count, c)) + end + last_count = c + end + end + end + if frames >= ENDF then + if injected then + done = true + inspect() + if SNAP then + local screen = nil + for _, s in pairs(machine.screens) do screen = s break end + if screen then + local out = OUT .. "/loader_frame" .. frames .. ".png" + local ok, err = pcall(function() return screen:snapshot(out) end) + print(string.format("LOADER: snapshot %s ok=%s %s", + out, tostring(ok), tostring(err or ""))) + end + end + end + machine:exit() + end +end, "vt1200-loader") + +-- (Lua has no forward use of a name defined later; set a small flag here.) +base_contains_marker = (COFF:match("([^/]+)$") or ""):find("marker") ~= nil + +dbg.visible_cpu = cpu +dbg.execution_state = 'run' diff --git a/apps/termtris/tests/render.sh b/apps/termtris/tests/render.sh new file mode 100755 index 0000000..a088feb --- /dev/null +++ b/apps/termtris/tests/render.sh @@ -0,0 +1,29 @@ +#!/bin/sh +# render.sh -- Task 3 acceptance: paint the cell-grid test pattern, then +# snapshot the window. The PNG is the evidence. +set -eu +DIR=$(cd "$(dirname "$0")" && pwd) +APP=$(cd "$DIR/.." && pwd) +ROOT=$(cd "$APP/../.." && pwd) +M=${MAME:-/home/arti/code/k-space/mame0289/vt1200} +EV="$APP/evidence" +LOG="$APP/build/render.log" +mkdir -p "$EV" "$APP/build" +"$APP/build.sh" "$DIR/render.c" "$APP/src/vt1200/x11.c" "$APP/src/vt1200/xgfix.c" >"$APP/build/render-build.log" 2>&1 +rm -f "$EV/loader_frame3400.png" +MARK=$(awk '/_mark/ {print $1; exit}' "$APP/build/render.map") +PROG=$(awk '/_vt_x11_progress/ {print $1; exit}' "$APP/build/render.map") +VT1200_SKIP_LEVEL1=1 LOADER_SNAP=1 RENDER_MARK="0x$MARK" X11_PROGRESS="0x$PROG" \ +LOADER_AT=2500 LOADER_END=3400 \ +LOADER_BIN="$ROOT/dwt/loader/build/loader.bin" \ +LOADER_COFF="$APP/build/render.coff" \ +LOADER_OUT="$EV" \ +"$M" vt1200 -rompath "$ROOT/harness/mame/roms" \ + -video none -sound none -nothrottle -skip_gameinfo \ + -debug -debugger none \ + -autoboot_script "$DIR/render.lua" -autoboot_delay 0 \ + >"$LOG" 2>&1 || true +grep -E 'LOADER: snapshot|LOADER: result' "$LOG" | tail -4 +[ -s "$EV/loader_frame3400.png" ] || { echo "render.sh: no snapshot (see $LOG)" >&2; exit 1; } +cp "$EV/loader_frame3400.png" "$EV/render_frame3400.png" +echo "render: $EV/render_frame3400.png" -- 2.47.3 From c1945cfc7310d2e4d5579cbaa8d9b3a68def0385 Mon Sep 17 00:00:00 2001 From: "opencode.energia.arti.ee" Date: Tue, 3 Nov 2026 19:03:14 +0000 Subject: [PATCH 06/18] feat(termtris): X11 key map, millisecond clock and main loop --- apps/termtris/build/input.coff | Bin 0 -> 4399 bytes apps/termtris/build/input.map | 95 ++++++++ apps/termtris/src/vt1200/main.c | 130 +++++++++++ apps/termtris/tests/input.c | 32 +++ apps/termtris/tests/input.lua | 381 ++++++++++++++++++++++++++++++++ apps/termtris/tests/input.sh | 27 +++ 6 files changed, 665 insertions(+) create mode 100644 apps/termtris/build/input.coff create mode 100644 apps/termtris/build/input.map create mode 100644 apps/termtris/src/vt1200/main.c create mode 100644 apps/termtris/tests/input.c create mode 100644 apps/termtris/tests/input.lua create mode 100755 apps/termtris/tests/input.sh diff --git a/apps/termtris/build/input.coff b/apps/termtris/build/input.coff new file mode 100644 index 0000000000000000000000000000000000000000..fc02e28b1e911bf1ce0cd2767f89753c76747da8 GIT binary patch literal 4399 zcmbVPZ%iBK8Gp}kJuWvAwJ<6Yw%Zg+YAd7Cs%YIO`hfvuu{v$(Hfiha_q^w` zeOdA$vvlY8{XPGm_j#Z9y)&$qshQ7S{;tf}A;#Etwo|ARZVU7KFu_mzRKPopm+`bG z!p9SwV$6x}BR;RZoVAR#_eEk6FwXBwLsweJfczY6RR27fv>zTDGq4P7WCX)rH5X$^ z5q$l3?Y+>53YxpLe8%kUTQI zAs-C9&@&-q+$}Z|QXlE;ZXU(A6!@TwT_)?}wO6b=f~VSuhdW z7eDAxZtFE4^lTHfR>#tG`xIi@^UKbX*E-Kz0!`A$(4G-t9Z+$esrhKUH1k!=$89dMD4{1x&%C;CRglf`47&CWy_I=C_ zlxN?;Y^c%X3}9A}-(u#RX5R9w>n_x;GIjS|sQb#)op+(`EmIq`i@A*cATso8{ef#e z%8XvKDBA}`S%l|mx9Ap4kBYe@t#KQz)IrqJW+lYdWzU(6KpEY9lkyj=|9ty}PPf{M zHUE#kDc_^~O@9pUFLXEFxAmtBk87*mTPK&Mi<4KTq;18?tSZ@ylVqdg!ud``wJo;# z1a1BGrRms|OC1y?5uATuELg9}X<<8JS(HPpIBDd!L324$QWCYXMa%MkDNh{nG6&TT z(TmE@^*K4&7Lp;^LA8#0gVy-hQQN95-{RP<1?vMsT4=G`3f&GjX2gdz^2N&0&42Mp zCw2uqsZ;Cn!TRJwv_ELBsQ)Z1O(P01Eo??K0b5#-RioDCCE_A&f7FmcUYmR$p{j;* z$=bo3$FhS0;LdcBjUb{~lj9hZ&Uo{M&R2Pz%Idd4AU9N>(*Ibh183X!wG2_oU=Ji zAD0+Bv{Y(|hp0oUbnSq$steaP9&S}@7i4Y7*Q!$0&*`<-K9pZ%=nuXLZg-sfP_2pj zTDSP#RZYLhIljepJ-IN;IiKKKb}fX}tbd^?)wJSi?#lXSng%eM^|vY%brzj3Ag(8U z>Zxz8Zd(b=Ub3_OFRfp4FqdQfl9SDFP0jXG{ND$!l81KM%ua`XBe3L4PYBF@5DlUsou!t&tee(CHA);C9>+`j9 z;Ci7~Pg@*@J{t?6<`Io{QggWk`)cc$c&xN7{k^w z3_W0ByDwX)03&vf-J%2&(-xgZhON-o7;CYx-H$CgAYC{?Brg%^m^!t)Z4kOc_>(b`STLFDilYRY5rCkh@q1Mr91 zpx+NMq|>5;jAKY=Lj`#gLvyFt%DsiYehr>*sf*ILrGebJcWf+N^{q-(3_W-BaPY+X zxlh`s=DN>79nU^u=}!!@Abmgw2YQFYC3r9#85m434uKdyg7NWPyMleuaPR0?-zcLz z5I{@Fr8yEGiAICrqv06X$D-kIoQjlK2YX}v(XdH6)(dO>k+FER_e5FK528oL;7Lg@ z|DvbK=*S4qc>l<;GBG|nGB6s($Gl-4!!Qh%B>P9P=M76HL*hi-3=EuoM@Gj+5CM0+ z5)cosM51v?E9WAqk!V@SaL$mgRu(xt+&9>3sz%br5}1&(iUM<1WvMKN2qG~wF(mQ$ G0Q)buthGA; literal 0 HcmV?d00001 diff --git a/apps/termtris/build/input.map b/apps/termtris/build/input.map new file mode 100644 index 0000000..2ee2198 --- /dev/null +++ b/apps/termtris/build/input.map @@ -0,0 +1,95 @@ +***************************************************** +TMS340 COFF Linker Version 5.01 +***************************************************** +Tue Nov 03 19:02:37 2026 + +OUTPUT FILE NAME: +ENTRY POINT SYMBOL: "_start" address: 018a0000 + + +MEMORY CONFIGURATION + + name origin length attributes + -------- -------- --------- ---------- + prog 018a0000 000020000 RWIX + ram 01a00000 000040000 RWIX + + +SECTION ALLOCATION MAP + + output attributes/ +section page origin length input sections +-------- ---- ---------- ---------- ---------------- +.text 0 018a0000 000043b0 + 018a0000 00000150 ENTRY.obj (.text) + 018a0150 000003e0 INPUT.obj (.text) + 018a0530 00001760 MAIN.obj (.text) + 018a1c90 00002510 X11.obj (.text) + 018a41a0 00000210 XGFIX.obj (.text) + +.cinit 0 018a43b0 000000a0 + 018a43b0 00000050 MAIN.obj (.cinit) + 018a4400 00000050 X11.obj (.cinit) + +.bss 0 01a00000 0001e1c0 UNINITIALIZED + 01a00000 00008000 ENTRY.obj (.bss) + 01a08000 00001040 INPUT.obj (.bss) + 01a09040 00000360 MAIN.obj (.bss) + 01a093a0 00012e20 X11.obj (.bss) + 01a1c1c0 00002000 XGFIX.obj (.bss) + +.data 0 018a0000 00000000 UNINITIALIZED + + +GLOBAL SYMBOLS + +address name address name +-------- ---- -------- ---- +01a00000 .bss 018a0000 .data +018a0000 .data 018a0000 edata +018a0000 .text 018a0000 _start +018a0360 _game_input 018a0000 .text +018a0150 _init_game 018a0150 _init_game +018a18b0 _main 018a0250 _update +01a08040 _mark 018a0360 _game_input +018a0530 _now_ms 018a0530 _now_ms +018a07b0 _sleep_ms 018a07b0 _sleep_ms +018a0000 _start 018a0910 _vt_poll_event +01a1c0e0 _term_clearscr 018a1280 _vt_handle_event +01a1c120 _term_cursor 018a1870 _wait_display +01a08020 _term_height 018a18b0 _main +01a1c160 _term_ibmchar 018a1c90 _vt_root_window +018a4130 _term_init 018a1d70 _vt_x11_init +018a2ec0 _term_putstr 018a2420 _x11_clearscr +01a1c0c0 _term_reset 018a2610 _x11_reset +01a1c140 _term_setcolor 018a2650 _x11_cursor +01a1c100 _term_setcursor 018a26b0 _x11_setcolor +01a1c1a0 _term_type 018a27d0 _x11_setcursor +01a08000 _term_width 018a2a20 _x11_ibmchar +01a1c180 _termenv 018a2ec0 _term_putstr +018a0250 _update 018a3060 _vt_x11_flush +01a1c020 _vt_dpy 018a3e10 _vt_x11_term_init +01a1c060 _vt_gc 018a4130 _term_init +018a1280 _vt_handle_event 018a41a0 _vt_install_xg +018a41a0 _vt_install_xg 018a43b0 cinit +018a0910 _vt_poll_event 018a43b0 etext +018a1c90 _vt_root_window 01a00000 .bss +01a1c040 _vt_win 01a08000 _term_width +01a1c0a0 _vt_x11_drain_events 01a08020 _term_height +018a3060 _vt_x11_flush 01a08040 _mark +018a1d70 _vt_x11_init 01a1c020 _vt_dpy +01a1c080 _vt_x11_progress 01a1c040 _vt_win +018a3e10 _vt_x11_term_init 01a1c060 _vt_gc +018a1870 _wait_display 01a1c080 _vt_x11_progress +018a2420 _x11_clearscr 01a1c0a0 _vt_x11_drain_events +018a2650 _x11_cursor 01a1c0c0 _term_reset +018a2a20 _x11_ibmchar 01a1c0e0 _term_clearscr +018a2610 _x11_reset 01a1c100 _term_setcursor +018a26b0 _x11_setcolor 01a1c120 _term_cursor +018a27d0 _x11_setcursor 01a1c140 _term_setcolor +018a43b0 cinit 01a1c160 _term_ibmchar +018a0000 edata 01a1c180 _termenv +01a1e1c0 end 01a1c1a0 _term_type +018a43b0 etext 01a1e1c0 end + +[46 symbols] diff --git a/apps/termtris/src/vt1200/main.c b/apps/termtris/src/vt1200/main.c new file mode 100644 index 0000000..ce75f2b --- /dev/null +++ b/apps/termtris/src/vt1200/main.c @@ -0,0 +1,130 @@ +/* + * main.c -- the VT1200 termtris event loop. + * + * The game's update()/init_game() draw through the backend's term_* callbacks; + * this file owns the X11 event drain, the real-time clock, and the key map. + */ +#include "game.h" +#include "x11.h" +#include "xlib.h" + +extern int init_game(void); +extern long update(long msec); +extern void game_input(int c); + +extern int vt_install_xg(void); + +static vt_u32 evbuf[24]; +static long ms_base; +static int ms_last = -1; +static int focused; + +long now_ms(void) +{ + int now; + if(ms_last < 0) { + ms_last = *(volatile unsigned short *)0x001E93C0UL; + ms_base = 0; + return 0; + } + now = *(volatile unsigned short *)0x001E93C0UL; + ms_base += (long)(unsigned short)(now - ms_last); + ms_last = now; + return ms_base; +} + +void sleep_ms(int ms) +{ + if(ms < 1) ms = 1; + if(ms > 10) ms = 10; + VT_SLEEP(ms); +} + +int vt_poll_event(void) +{ + return *(vt_u32 *)((char *)vt_dpy + 0x54) != 0; +} + +static void map_keysym(vt_u32 ks, char ch, int n) +{ + switch(ks) { + case 0xFF51UL: /* Left */ + game_input(0x9b); game_input('D'); + return; + case 0xFF53UL: /* Right */ + game_input(0x9b); game_input('C'); + return; + case 0xFF52UL: /* Up */ + game_input(0x9b); game_input('A'); + return; + case 0xFF54UL: /* Down */ + game_input(0x9b); game_input('B'); + return; + case 0xFF0DUL: /* Return */ + game_input('\n'); + return; + case 0xFF08UL: /* BackSpace */ + game_input('\b'); + return; + case 0xFF09UL: /* Tab */ + game_input('\t'); + return; + case 0xFF1BUL: /* Escape */ + game_input(27); + return; + default: + break; + } + if(n > 0) { + if(ch >= 'A' && ch <= 'Z') ch = (char)(ch + ('a' - 'A')); + game_input((int)(unsigned char)ch); + } +} + +void vt_handle_event(void) +{ + char buf[8]; + vt_u32 ks; + int n; + + VT_XNEXT(vt_dpy, (void *)evbuf); + if(evbuf[0] == 2) { /* KeyPress */ + ks = 0; + n = VT_XLOOKUP((void *)evbuf, buf, 8, &ks, 0); + map_keysym(ks, n > 0 ? buf[0] : 0, n); + } else if(evbuf[0] == 12) { /* Expose */ + game_input('`'); + } else if(evbuf[0] == 9) { /* FocusIn */ + focused = 1; + } else if(evbuf[0] == 10) { /* FocusOut */ + if(focused) game_input('p'); + focused = 0; + } +} + +void wait_display(void) +{ + vt_x11_flush(); +} + +int main(void) +{ + long d; + + if(vt_x11_init() != 0) + return 1; + if(init_game() == -1) + return 2; + vt_x11_flush(); + + for(;;) { + VT_XSYNC(vt_dpy, 0); + while(vt_poll_event()) { + vt_handle_event(); + } + d = update(now_ms()); + sleep_ms((int)d); + vt_x11_flush(); + } + return 0; +} diff --git a/apps/termtris/tests/input.c b/apps/termtris/tests/input.c new file mode 100644 index 0000000..8735c32 --- /dev/null +++ b/apps/termtris/tests/input.c @@ -0,0 +1,32 @@ +/* + * input.c -- Task 4 acceptance: link main.c against stubs for the game, inject + * keys, and record the bytes main.c sends to game_input(). + */ +#include "game.h" +#include "x11.h" +#include "xlib.h" + +int term_width, term_height; +volatile vt_u32 mark[128]; + +int init_game(void) +{ + mark[0] = 0x494E5054UL; /* INPT */ + vt_x11_term_init(); + x11_clearscr(); + return 0; +} + +long update(long msec) +{ + (void)msec; + mark[1] = mark[1] + 1; + return 100; +} + +void game_input(int c) +{ + int n = (int)mark[2]; + if(n < 100) mark[3 + n] = (vt_u32)(unsigned char)c; + mark[2] = (vt_u32)(n + 1); +} diff --git a/apps/termtris/tests/input.lua b/apps/termtris/tests/input.lua new file mode 100644 index 0000000..2f619ee --- /dev/null +++ b/apps/termtris/tests/input.lua @@ -0,0 +1,381 @@ +-- loader.lua -- debugger-assisted COFF loader acceptance harness. +-- +-- Boots the firmware with VT1200_SKIP_LEVEL1=1 (the documented divergence) to +-- the OS, then at frame LOADER_AT pokes: +-- * the loader payload at bit 0x01880000 (raw dwt/loader/build/loader.bin) +-- * a GSPLNK COFF image at LOADER_BUF (default bit 0x01900000) +-- * the loader descriptor at 0x01882C00 +-- and sets PC = 0x01880000. It does not use the network, the rdebug service +-- or any firmware transport: this is the debugger-assisted milestone. +-- +-- The loader itself copies the COFF sections to their linked bit addresses and +-- calls create_process(entry, "LOADER", 0x6000, 1, 0). This script only pokes +-- the input and reads the result; it never writes the app's code or markers. +-- +-- Env: +-- LOADER_BIN loader payload (default dwt/loader/build/loader.bin) +-- LOADER_COFF app COFF (default dwt/loader/build/marker.coff) +-- LOADER_BUF COFF buffer bit address (default 0x01900000) +-- LOADER_AT injection frame (default 2500) +-- LOADER_END inspection/exit frame (default 2700) +-- LOADER_OUT evidence directory (default dwt/loader/evidence) +-- LOADER_SNAP 1 = write a PNG at LOADER_END (default 0) +-- LOADER_VERBOSE 1 = print the result block every 25 frames + +local function hex(v, w) return string.format("%0" .. (w or 8) .. "X", v) end + +local machine = manager.machine +local INPUT_MARK = tonumber(os.getenv("INPUT_MARK") or "0x01A00000") +local ports = machine.ioport.ports +local key_a = ports[":lk401:K26"].fields["A"] +local key_up = ports[":lk401:K77"].fields["Up"] +local key_down = ports[":lk401:K76"].fields["Down"] +local key_left = ports[":lk401:K74"].fields["Left"] +local key_right = ports[":lk401:K75"].fields["Right"] +local key_zero = ports[":lk401:K10"].fields["0 )"] +local key_ret = ports[":lk401:K68"].fields["Return"] +local key_del = ports[":lk401:K67"].fields["Delete"] +local tabx = ports[":tablet:TABLET_X"].fields["Tablet X"] +local taby = ports[":tablet:TABLET_Y"].fields["Tablet Y"] +local tabb = ports[":tablet:TABLET_BTN"].fields["P1 Button 1"] +local cpu = machine.devices[':maincpu'] +local dbg = machine.debugger +local prog = cpu.spaces['program'] + +if not dbg then + print("LOADER: debugger not enabled; pass -debug") + machine:exit() + return +end + +local AT = tonumber(os.getenv("LOADER_AT") or "2500") +local ENDF = tonumber(os.getenv("LOADER_END") or "2700") +local BIN = os.getenv("LOADER_BIN") or "dwt/loader/build/loader.bin" +local COFF = os.getenv("LOADER_COFF") or "dwt/loader/build/marker.coff" +local BUF = tonumber(os.getenv("LOADER_BUF") or "0x01900000") +local OUT = os.getenv("LOADER_OUT") or "dwt/loader/evidence" +local SNAP = os.getenv("LOADER_SNAP") == "1" +local MODE = tonumber(os.getenv("LOADER_MODE") or "0") +local A8DESC = os.getenv("LOADER_A8_DESC") == "1" +local VERBOSE = os.getenv("LOADER_VERBOSE") == "1" + +local LOADER_BASE = 0x01880000 +local DESC = 0x01882C00 +local RESULT = 0x01882D00 +local RETURN_STUB = 0x01883100 +local RETURN_SP = 0x01883110 + +local function rd(p) return io.open(p, "rb") end +local function slurp(p) + local f, err = rd(p) + if not f then error("cannot open " .. p .. ": " .. tostring(err)) end + local s = f:read("*a") + f:close() + return s +end + +local function u16(s, o) + return s:byte(o + 1) + s:byte(o + 2) * 256 +end + +local function u32(s, o) + return s:byte(o + 1) + s:byte(o + 2) * 256 + + s:byte(o + 3) * 65536 + s:byte(o + 4) * 16777216 +end + +local function parse_coff(d) + local c = { data = d } + c.magic = u16(d, 0) + c.nscns = u16(d, 2) + c.opthdr = u16(d, 16) + c.sections = {} + if c.opthdr > 0 then + c.optmagic = u16(d, 20) + c.entry = u32(d, 20 + 16) + c.textstart = u32(d, 20 + 20) + c.datastart = u32(d, 20 + 24) + end + local base = 20 + c.opthdr + for i = 0, c.nscns - 1 do + local o = base + i * 40 + local name = d:sub(o + 1, o + 8):gsub("%z", "") + c.sections[#c.sections + 1] = { + name = name, + paddr = u32(d, o + 8), + size = u32(d, o + 16), -- bits + scnptr = u32(d, o + 20), + flags = u32(d, o + 36), + } + end + return c +end + +local function poke_bytes(bitaddr, data) + local n = math.floor((#data + 1) / 2) + for i = 0, n - 1 do + local lo = data:byte(i * 2 + 1) or 0 + local hi = data:byte(i * 2 + 2) or 0 + prog:write_u16(bitaddr + i * 16, lo + hi * 256) + end + return n +end + +local function verify_bytes(bitaddr, data) + local n = math.floor((#data + 1) / 2) + local bad = 0 + for i = 0, n - 1 do + local lo = data:byte(i * 2 + 1) or 0 + local hi = data:byte(i * 2 + 2) or 0 + if prog:read_u16(bitaddr + i * 16) ~= lo + hi * 256 then + bad = bad + 1 + end + end + return n, bad +end + +local function w32(a, v) prog:write_u32(a, v) end +local function key_step(frames, key) + if frames == 3100 then key_a:set_value(1) end + if frames == 3110 then key_a:set_value(0) end + if frames == 3120 then key_up:set_value(1) end + if frames == 3130 then key_up:set_value(0) end + if frames == 3140 then key_down:set_value(1) end + if frames == 3150 then key_down:set_value(0) end + if frames == 3160 then key_left:set_value(1) end + if frames == 3170 then key_left:set_value(0) end + if frames == 3180 then key_right:set_value(1) end + if frames == 3190 then key_right:set_value(0) end + if frames == 3200 then key_zero:set_value(1) end + if frames == 3210 then key_zero:set_value(0) end + if frames == 3220 then key_ret:set_value(1) end + if frames == 3230 then key_ret:set_value(0) end + if frames == 3240 then key_del:set_value(1) end + if frames == 3250 then key_del:set_value(0) end +end + +local function r32(a) + local ok, v = pcall(function() return prog:read_u32(a) end) + if ok then return v end + return 0xDEADBEEF +end + +local loader = slurp(BIN) +local coff = slurp(COFF) +local app = parse_coff(coff) + +print(string.format("LOADER: payload %s (%d B) -> bit %s", BIN, #loader, hex(LOADER_BASE))) +print(string.format("LOADER: COFF %s (%d B) -> bit %s", COFF, #coff, hex(BUF))) +print(string.format("LOADER: file magic=%04X nscns=%d opthdr=%d optmagic=%s entry=%s textstart=%s", + app.magic, app.nscns, app.opthdr, + app.optmagic and string.format("%04X", app.optmagic) or "none", + hex(app.entry or 0), hex(app.textstart or 0))) +for _, s in ipairs(app.sections) do + print(string.format("LOADER: sec %-8s paddr=%s size=%d bits (%d B) scnptr=%#x flags=%#x", + s.name, hex(s.paddr), s.size, math.floor(s.size / 8), s.scnptr, s.flags)) +end + +local TRACE = os.getenv("LOADER_TRACE") == "1" + +local frames = 0 +local injected = false +local done = false +local last_count = -1 + +local function status_name(v) + local t = { + [0] = "OK", + [1] = "bad descriptor", + [2] = "truncated COFF / section headers", + [3] = "bad COFF magic", + [4] = "section size not whole words", + [5] = "target outside RAM", + [6] = "target overlaps loader page or COFF buffer", + [7] = "relocation records present (unsupported)", + [8] = "target not in a free-list block", + [9] = "no entry point", + } + return t[v] or ("error " .. tostring(v)) +end + +local function result_dump() + local st = r32(RESULT + 0x000) + print(string.format( + "LOADER: result status=%d (%s) entry=%s text=%s/%d bits data=%s/%d bits bss=%s/%d bits relocs=%d pcb=%s nscns=%d len=%d", + st, status_name(st), hex(r32(RESULT + 0x020)), + hex(r32(RESULT + 0x040)), r32(RESULT + 0x060), + hex(r32(RESULT + 0x080)), r32(RESULT + 0x0A0), + hex(r32(RESULT + 0x0C0)), r32(RESULT + 0x0E0), + r32(RESULT + 0x100), hex(r32(RESULT + 0x120)), + r32(RESULT + 0x140), r32(RESULT + 0x160))) +end + +local function inject() + -- 1. loader itself + local n, bad = 0, 0 + poke_bytes(LOADER_BASE, loader) + n, bad = verify_bytes(LOADER_BASE, loader) + print(string.format("LOADER: loader poked %d words, readback %d bad at %s", + n, bad, hex(LOADER_BASE))) + + -- 2. COFF image + poke_bytes(BUF, coff) + n, bad = verify_bytes(BUF, coff) + print(string.format("LOADER: COFF poked %d words, readback %d bad at %s", + n, bad, hex(BUF))) + + -- 3. pre-fill every .bss section with A5A5, so a loaded zero is meaningful + for _, s in ipairs(app.sections) do + if s.flags & 0x80 ~= 0 then + for i = 0, math.floor(s.size / 16) - 1 do + prog:write_u16(s.paddr + i * 16, 0xA5A5) + end + print(string.format("LOADER: pre-filled .bss %s..%s with A5A5", + hex(s.paddr), hex(s.paddr + s.size))) + end + end + + -- 4. descriptor: magic "LD" "R1", buf, len, mode=0 (spin) + w32(DESC + 0x00, 0) + prog:write_u16(DESC + 0x00, 0x4C44) + prog:write_u16(DESC + 0x10, 0x5231) + w32(DESC + 0x20, BUF) + w32(DESC + 0x40, #coff) + w32(DESC + 0x60, MODE) + print(string.format("LOADER: descriptor @%s magic=%04X/%04X buf=%s len=%d mode=%d", + hex(DESC), prog:read_u16(DESC + 0x00), prog:read_u16(DESC + 0x10), + hex(r32(DESC + 0x20)), r32(DESC + 0x40), r32(DESC + 0x60))) + + -- 5. return stub. mode 0 ignores it (the loader jumps to ORIG_PC + -- itself); mode 1 returns here, and the stub resumes the interrupted + -- PC so the caller yields and the scheduler can run the child. + local orig_pc = cpu.state['PC'].value + if MODE == 1 then + prog:write_u16(RETURN_STUB, 0xC080) -- jauc + prog:write_u16(RETURN_STUB + 0x10, orig_pc & 0xFFFF) + prog:write_u16(RETURN_STUB + 0x20, math.floor(orig_pc / 0x10000) & 0xFFFF) + else + prog:write_u16(RETURN_STUB, 0xC0FF) -- jruc self + end + + -- 6. enter the loader. A8=0 selects the fixed descriptor; SP points at + -- the return slot so mode=1 would return to the spin stub. + -- Leave SP alone for mode 0: the loader pushes onto the current + -- process stack, then restores it and resumes at ORIG_PC. (mode 1 + -- would use the return stub.) + w32(0x01882A20, orig_pc) -- LD_ORIG_PC + print(string.format("LOADER: will resume at %s after create_process", hex(orig_pc))) + cpu.state['A8'].value = A8DESC and DESC or 0 + if MODE == 1 then + -- Simulate a CALLA from the current process: push the return + -- address on the *firmware* stack (a real transport would do this). + local sp = cpu.state['SP'].value - 0x20 + w32(sp, RETURN_STUB) + cpu.state['SP'].value = sp + end + cpu.state['PC'].value = LOADER_BASE + injected = true + print(string.format("LOADER: frame %d: entered loader at %s (caller SP=%s)", + frames, hex(LOADER_BASE), hex(cpu.state['SP'].value))) +end + +local function inspect() + result_dump() + + -- Verify the placement from RAM: .text/.data must equal the file bytes. + for _, s in ipairs(app.sections) do + if s.scnptr ~= 0 and s.size > 0 then + local nb = math.floor(s.size / 8) + local bytes = coff:sub(s.scnptr + 1, s.scnptr + nb) + local _, bad = verify_bytes(s.paddr, bytes) + print(string.format("LOADER: placed %s at %s: %d bytes, %d differing", + s.name, hex(s.paddr), nb, bad)) + end + end + + local base = COFF:match("([^/]+)$") or COFF + if base:find("marker") then + print(string.format("LOADER: marker app: start=%s data=%s data2=%s bss=%s count=%d", + hex(r32(0x018E0000)), hex(r32(0x018E0020)), hex(r32(0x018E0040)), + hex(r32(0x018E0060)), r32(0x018E0080))) + elseif base:find("xhello") then + print(string.format("LOADER: xhello: stage=%d strlen=%d display=%s root=%s window=%s sync=%s", + r32(0x01890140), r32(0x01890160), hex(r32(0x01890100)), + hex(r32(0x01890110)), hex(r32(0x01890120)), hex(r32(0x01890130)))) + end + if base:find("input") then + local n = r32(INPUT_MARK + 0x40) + local t = {} + for i = 0, n - 1 do t[#t + 1] = string.format("%02X", r32(INPUT_MARK + (3 + i) * 0x20)) end + print(string.format("INPUT: magic=%08X updates=%d n=%d bytes=%s", + r32(INPUT_MARK), r32(INPUT_MARK + 0x20), n, table.concat(t, " "))) + end + print(string.format("LOADER: PC at inspection = %s", hex(cpu.state['PC'].value))) +end + +emu.register_frame_done(function() + frames = frames + 1 + if frames == 2900 then tabx:set_value(700); taby:set_value(500); print("INPUT: tablet move") end + if frames == 3000 then tabb:set_value(1); print("INPUT: click down") end + if frames == 3020 then tabb:set_value(0); print("INPUT: click up") end + key_step(frames, nil) + if frames == AT and not injected then + inject() + if TRACE then dbg:command("trace /tmp/opencode/ld1.tr") end + end + if injected and TRACE and frames == AT + 1 then + dbg:command("trace off") + print("LOADER: trace written to /tmp/opencode/ld.tr") + end + if injected and not done then + if TRACE and frames <= AT + 40 then + print(string.format("LOADER: trace f=%d PC=%s SP=%s A14=%s status=%s entry=%s", + frames, hex(cpu.state['PC'].value), hex(cpu.state['SP'].value), + hex(cpu.state['A14'].value), hex(r32(RESULT + 0x000)), + hex(r32(RESULT + 0x020)))) + end + local st = r32(RESULT + 0x000) + -- status 0 means OK; nonzero is final too once parsing has begun + if st ~= 0xFFFFFFFF and not done then + -- The loader writes 0 on success. A nonzero status is final + -- only after it has been written once (it is 0 from power-on, + -- so require the loader to have run: PC no longer at entry or + -- the app counter moved). + end + if VERBOSE and frames % 25 == 0 then + result_dump() + end + if base_contains_marker then + local c = r32(0x018E0080) + if c ~= last_count then + if last_count ~= -1 then + print(string.format("LOADER: marker counter %d -> %d (child running)", + last_count, c)) + end + last_count = c + end + end + end + if frames >= ENDF then + if injected then + done = true + inspect() + if SNAP then + local screen = nil + for _, s in pairs(machine.screens) do screen = s break end + if screen then + local out = OUT .. "/loader_frame" .. frames .. ".png" + local ok, err = pcall(function() return screen:snapshot(out) end) + print(string.format("LOADER: snapshot %s ok=%s %s", + out, tostring(ok), tostring(err or ""))) + end + end + end + machine:exit() + end +end, "vt1200-loader") + +-- (Lua has no forward use of a name defined later; set a small flag here.) +base_contains_marker = (COFF:match("([^/]+)$") or ""):find("marker") ~= nil + +dbg.visible_cpu = cpu +dbg.execution_state = 'run' diff --git a/apps/termtris/tests/input.sh b/apps/termtris/tests/input.sh new file mode 100755 index 0000000..c43cd69 --- /dev/null +++ b/apps/termtris/tests/input.sh @@ -0,0 +1,27 @@ +#!/bin/sh +# input.sh -- Task 4 acceptance: main.c's key map against injected keys. +set -eu +DIR=$(cd "$(dirname "$0")" && pwd) +APP=$(cd "$DIR/.." && pwd) +ROOT=$(cd "$APP/../.." && pwd) +M=${MAME:-/home/arti/code/k-space/mame0289/vt1200} +LOG="$APP/build/input.log" +mkdir -p "$APP/build" +"$APP/build.sh" "$DIR/input.c" "$APP/src/vt1200/main.c" "$APP/src/vt1200/x11.c" "$APP/src/vt1200/xgfix.c" >"$APP/build/input-build.log" 2>&1 +MARK=$(awk '/_mark/ {print $1; exit}' "$APP/build/input.map") +VT1200_SKIP_LEVEL1=1 INPUT_MARK="0x$MARK" \ +LOADER_AT=2500 LOADER_END=3400 \ +LOADER_BIN="$ROOT/dwt/loader/build/loader.bin" \ +LOADER_COFF="$APP/build/input.coff" \ +LOADER_OUT="$APP/build" \ +"$M" vt1200 -rompath "$ROOT/harness/mame/roms" \ + -video none -sound none -nothrottle -skip_gameinfo \ + -debug -debugger none \ + -autoboot_script "$DIR/input.lua" -autoboot_delay 0 \ + >"$LOG" 2>&1 || true +grep 'INPUT:' "$LOG" | tail -3 +grep -q 'INPUT: .*bytes=.*61 9B 41 9B 42 9B 44 9B 43 30 0A 7F' "$LOG" || { + echo "input.sh: mapped key sequence missing (see $LOG)" >&2 + exit 1 +} +echo "input: PASS" -- 2.47.3 From 41d3cecd49f7fab047c9149308b4d94f0cf1af08 Mon Sep 17 00:00:00 2001 From: "opencode.energia.arti.ee" Date: Tue, 3 Nov 2026 20:03:47 +0000 Subject: [PATCH 07/18] feat(termtris): run the game on the VT1200 X server Board, score panel, preview and help/scores toggles draw through the ROM Xlib cell grid; keys arrive as real X KeyPress events. The backend is now monochrome (the VT1200 raster is one bit deep) and flushes whole rows so the in-machine server can drain the request stream. --- apps/termtris/build.sh | 4 +- apps/termtris/build/render.coff | Bin 3788 -> 3922 bytes apps/termtris/build/render.map | 82 ++-- apps/termtris/build/termtris.coff | Bin 0 -> 15157 bytes apps/termtris/build/termtris.map | 141 +++++++ apps/termtris/evidence/render_frame3400.png | Bin 19845 -> 18967 bytes apps/termtris/evidence/termtris_frame3300.png | Bin 0 -> 20986 bytes apps/termtris/evidence/termtris_move_3200.png | Bin 0 -> 19458 bytes .../evidence/termtris_panels_3200.png | Bin 0 -> 22396 bytes apps/termtris/evidence/termtris_quit_3200.png | Bin 0 -> 19407 bytes apps/termtris/src/game.c | 4 +- apps/termtris/src/vt1200/compat.c | 6 + apps/termtris/src/vt1200/main.c | 29 +- apps/termtris/src/vt1200/scoredb.c | 76 ++++ apps/termtris/src/vt1200/stdio.h | 1 + apps/termtris/src/vt1200/x11.c | 90 ++-- apps/termtris/src/vt1200/xlib.h | 2 + apps/termtris/tests/input.lua | 3 + apps/termtris/tests/input.sh | 2 +- apps/termtris/tests/play.lua | 345 ++++++++++++++++ apps/termtris/tests/play.sh | 25 ++ apps/termtris/tests/play_keys.lua | 390 ++++++++++++++++++ apps/termtris/tests/play_keys.sh | 35 ++ 23 files changed, 1151 insertions(+), 84 deletions(-) create mode 100644 apps/termtris/build/termtris.coff create mode 100644 apps/termtris/build/termtris.map create mode 100644 apps/termtris/evidence/termtris_frame3300.png create mode 100644 apps/termtris/evidence/termtris_move_3200.png create mode 100644 apps/termtris/evidence/termtris_panels_3200.png create mode 100644 apps/termtris/evidence/termtris_quit_3200.png create mode 100644 apps/termtris/src/vt1200/scoredb.c create mode 100644 apps/termtris/tests/play.lua create mode 100755 apps/termtris/tests/play.sh create mode 100644 apps/termtris/tests/play_keys.lua create mode 100755 apps/termtris/tests/play_keys.sh diff --git a/apps/termtris/build.sh b/apps/termtris/build.sh index 601be0b..39c3b06 100755 --- a/apps/termtris/build.sh +++ b/apps/termtris/build.sh @@ -91,7 +91,7 @@ run() { compile_c() { base=$1 rm -f "$WORK/$base.IF" "$WORK/$base.OPT" "$WORK/$base.ASM" "$WORK/$base.obj" - run "$base.IF" ./gspac.exe -k "$base.C" "$base.IF" + run "$base.IF" ./gspac.exe -k -i. "$base.C" "$base.IF" run "$base.OPT" ./gspopt.exe -k -O2 "$base.IF" "$base.OPT" run "$base.ASM" ./gspcg.exe -m "$base.OPT" "$base.ASM" "$base.TMP" run "$base.obj" ./gspa.exe "$base.ASM" @@ -120,7 +120,7 @@ compile_asm ENTRY compile_asm CINIT compile_asm CINITZ -OUTNAME=$(printf '%s' "$MAIN" | tr 'A-Z' 'a-z') +OUTNAME=${OUTNAME:-$(printf '%s' "$MAIN" | tr 'A-Z' 'a-z')} CMD="$WORK/$MAIN.CMD" { printf '%s\n' '-a' diff --git a/apps/termtris/build/render.coff b/apps/termtris/build/render.coff index e00108eea8a5beaf981a4766b570c1bd1a3f4ed0..20171e734748afebaf5b0de6bc4b34170d983d17 100644 GIT binary patch delta 1979 zcmZ`)VQf=X6h8NDkJsC`jt9Z`U~E1{ySf3(Btcy=Y>BOmtxx*HKSp9-ltl)*EVCjS zO$)M=6$7ph6HsG{wJwcYO9sn+(jRMpPN-2sghXvjmN5y9F^*p*_1yQIZ4sN?`|kbD zch3FpIrrXv)BIga=p(3ywX2LKykGqI3mqPTE^zwjA?eH1&hbp{8H1PzNh zXS{EXd8Je80XknD92!KT;)-IdD2b!_60TAGa|4Wr){-;hu|7_5jsN`gqs5jeah?|Q zg~X74fOmfEEYo-9o(Fd@L3Y~;p~VvIB>BpPoPNA%;QHN+wm5?AOj zlww!tX6(^_6T^n|g$-$(nT#AV(yC%OHrP-4L5*O$4sH%dFB#G(dpcXw#9CrTP|m0`zct`h=^KT+oj zA(yJ#(Y{DmLn~_U)Y-<22=Cu=nc7OR#+xh!D;5|&hY?sREb2P+t+i@jg zf6H~p$7Qe5aydi8R*jyfM~*6TpLdD2X~*KEL7t~!!OxE!-7_Ttos@w}cY78+5h091 zB7!!ekJPt|I8M!G%VR=zME|P(*$~dh3`S`-3>gX?uVCO8`0Hv4J^t*J>(rk zMGiTInvzBwhcs$@y<%&oEY7WnOd3lvBU`#On?*tK4*e8w53!@*JiR<$nvEkEHdMZs zxvn;f?z%@ZKK%;FRhNlzNn&I+@Vd@c0JEmvy&*giC_Pseh!NYYs2Y9dt8f4_6i zwe6w;ZR&Gtk}5AMbyL$CfM-%|5aZDA%cB@SKR8;Kr%u{VvQ$36ZygtFG^XdUD|{5`W(xS1o!6)38&1C-GBD)Q%6;w-C>_}n_1v}quz8Z{k;OG@U)xs z2ea_dWEvZBs9yB;ssr}qu9eL^JPzARNc>0V**UY8i=Ml9nnWqpGZDcGTJIgROUYR` zIMz*J&0&63If>_4Fs_w(fx4tmT?=@5$}nnFyCh!zD?^u`VHe2{^Vm)bfagYRIw>gl zU7|{Jl-&(1?2?yiO>7cp6Uts9ygn(c&YHV25jMO{a3-& zgvj6rkIN>?fg6|duZav~=q& zR+0Rne{PXi4>K}8&2_|&J<*?_k8sjX{eX2h3F;TDt<3ia>!GJmUL6t`6`&cLR)cIu z#P5s}_Lspy;BYXofA}51$L+AV!(PLD2^nnrCKD%c5L=gC5bf+0tSoXC;cyM|B_bN` z-sA*=10!z?3>|j=i=Y`Y*jQdGaV{eguR(4g!m?hGa2pY&IaCcTAjAq$3G^odaeTZh zZe>J@HK7~O$Vyi6+JMLfN41v|k#Ki4(v1j~eoa;A-3VbFIGa&{HrsA;=Hig;W%4%= CRA~1A delta 1801 zcmZ`(Z)jUp6hHU1F?oGWmyOBNwY6!N?5%$maoE{)XyQyv?eezxK^QvBLl<1vT8fl8 zMM0rUYtBkUtOvPnukxWHdvjo>?kFZHpG_%4hn z9(7H5Ys$L(GF1auU+*94M`Ecbgt$C)09vHrY8E7eV!fuSM^ol3fF^pgIX|u^6h?6|00|b_U&Hhxnwvte=F5eYd;{aI zlcP(=xxC!HJgYFDTzY{^$WJW$gqVBT95mn25O`xyYsaFt3t5(GSi!2?9j$e{1T6 z2{o|pxd?6U;LkR0C#M8#~)bLHe-jdbuuTHln<_2gpM?x^OS`HATXxX zAQPQE;YMk}JoN7L$A`Nasc1ILnXE-EAK)t4HtI&7-jg6=<8+nj&{)^3Oe`Q3L% zVO5ZQ0*gTpr_SNj%J(-ea@VB4wap$+Q-}^7ZdqW(25LtXn||jwn2LQ#;baxNq}Y&D z_$`!ULK}w_zXklxx}>u@TLU8fA09BOCq>ft{fGYb)O2bL z766;{tL})NPD&NtaaF`*B~nYx9>#uDO4qb9*>0&=<047vFxN`MUgid#LOGPnD0gBL zzQqdU5F!yiMCb*~fcN}}_ssA+fLF!8T$j0m`3f@VaaATR;2b`p^ir*v-YMlxK0`QA zflN~(o>t{}M|^L+=^Hw4e}JGH68NMlqP{~UQi0q;gjTsM;SM66CjWLQhY&4%S;@pNrRjv#`q-nlKc3nA>oxL)bNoZdy&zP_h_ GmHY>q6h@r@ diff --git a/apps/termtris/build/render.map b/apps/termtris/build/render.map index da7b319..50449f4 100644 --- a/apps/termtris/build/render.map +++ b/apps/termtris/build/render.map @@ -1,7 +1,7 @@ ***************************************************** TMS340 COFF Linker Version 5.01 ***************************************************** -Tue Nov 03 19:00:04 2026 +Tue Nov 03 19:54:29 2026 OUTPUT FILE NAME: ENTRY POINT SYMBOL: "_start" address: 018a0000 @@ -20,21 +20,21 @@ SECTION ALLOCATION MAP output attributes/ section page origin length input sections -------- ---- ---------- ---------- ---------------- -.text 0 018a0000 00003af0 +.text 0 018a0000 00003f20 018a0000 00000150 ENTRY.obj (.text) 018a0150 00001280 RENDER.obj (.text) - 018a13d0 00002510 X11.obj (.text) - 018a38e0 00000210 XGFIX.obj (.text) + 018a13d0 00002940 X11.obj (.text) + 018a3d10 00000210 XGFIX.obj (.text) -.cinit 0 018a3af0 000000a0 - 018a3af0 00000050 RENDER.obj (.cinit) - 018a3b40 00000050 X11.obj (.cinit) +.cinit 0 018a3f20 000000a0 + 018a3f20 00000050 RENDER.obj (.cinit) + 018a3f70 00000050 X11.obj (.cinit) -.bss 0 01a00000 0001d080 UNINITIALIZED +.bss 0 01a00000 00019480 UNINITIALIZED 01a00000 00008000 ENTRY.obj (.bss) 01a08000 00000260 RENDER.obj (.bss) - 01a08260 00012e20 X11.obj (.bss) - 01a1b080 00002000 XGFIX.obj (.bss) + 01a08260 0000f220 X11.obj (.bss) + 01a17480 00002000 XGFIX.obj (.bss) .data 0 018a0000 00000000 UNINITIALIZED @@ -49,37 +49,37 @@ address name address name 018a0370 _main 018a0000 _start 01a08000 _mark 018a0370 _main 018a0000 _start 018a13d0 _vt_root_window -01a1afa0 _term_clearscr 018a14b0 _vt_x11_init -01a1afe0 _term_cursor 018a1b60 _x11_clearscr -01a08220 _term_height 018a1d50 _x11_reset -01a1b020 _term_ibmchar 018a1d90 _x11_cursor -018a3870 _term_init 018a1df0 _x11_setcolor -018a2600 _term_putstr 018a1f10 _x11_setcursor -01a1af80 _term_reset 018a2160 _x11_ibmchar -01a1b000 _term_setcolor 018a2600 _term_putstr -01a1afc0 _term_setcursor 018a27a0 _vt_x11_flush -01a1b060 _term_type 018a3550 _vt_x11_term_init -01a08200 _term_width 018a3870 _term_init -01a1b040 _termenv 018a38e0 _vt_install_xg -01a1aee0 _vt_dpy 018a3af0 etext -01a1af20 _vt_gc 018a3af0 cinit -018a38e0 _vt_install_xg 01a00000 .bss +01a173a0 _term_clearscr 018a14b0 _vt_x11_init +01a173e0 _term_cursor 018a1b60 _x11_clearscr +01a08220 _term_height 018a1d20 _x11_reset +01a17420 _term_ibmchar 018a1d60 _x11_cursor +018a3ca0 _term_init 018a1dc0 _x11_setcolor +018a25b0 _term_putstr 018a1ee0 _x11_setcursor +01a17380 _term_reset 018a2130 _x11_ibmchar +01a17400 _term_setcolor 018a25b0 _term_putstr +01a173c0 _term_setcursor 018a2750 _vt_x11_flush +01a17460 _term_type 018a3980 _vt_x11_term_init +01a08200 _term_width 018a3ca0 _term_init +01a17440 _termenv 018a3d10 _vt_install_xg +01a172e0 _vt_dpy 018a3f20 etext +01a17320 _vt_gc 018a3f20 cinit +018a3d10 _vt_install_xg 01a00000 .bss 018a13d0 _vt_root_window 01a08000 _mark -01a1af00 _vt_win 01a08200 _term_width -01a1af60 _vt_x11_drain_events 01a08220 _term_height -018a27a0 _vt_x11_flush 01a1aee0 _vt_dpy -018a14b0 _vt_x11_init 01a1af00 _vt_win -01a1af40 _vt_x11_progress 01a1af20 _vt_gc -018a3550 _vt_x11_term_init 01a1af40 _vt_x11_progress -018a1b60 _x11_clearscr 01a1af60 _vt_x11_drain_events -018a1d90 _x11_cursor 01a1af80 _term_reset -018a2160 _x11_ibmchar 01a1afa0 _term_clearscr -018a1d50 _x11_reset 01a1afc0 _term_setcursor -018a1df0 _x11_setcolor 01a1afe0 _term_cursor -018a1f10 _x11_setcursor 01a1b000 _term_setcolor -018a3af0 cinit 01a1b020 _term_ibmchar -018a0000 edata 01a1b040 _termenv -01a1d080 end 01a1b060 _term_type -018a3af0 etext 01a1d080 end +01a17300 _vt_win 01a08200 _term_width +01a17360 _vt_x11_drain_events 01a08220 _term_height +018a2750 _vt_x11_flush 01a172e0 _vt_dpy +018a14b0 _vt_x11_init 01a17300 _vt_win +01a17340 _vt_x11_progress 01a17320 _vt_gc +018a3980 _vt_x11_term_init 01a17340 _vt_x11_progress +018a1b60 _x11_clearscr 01a17360 _vt_x11_drain_events +018a1d60 _x11_cursor 01a17380 _term_reset +018a2130 _x11_ibmchar 01a173a0 _term_clearscr +018a1d20 _x11_reset 01a173c0 _term_setcursor +018a1dc0 _x11_setcolor 01a173e0 _term_cursor +018a1ee0 _x11_setcursor 01a17400 _term_setcolor +018a3f20 cinit 01a17420 _term_ibmchar +018a0000 edata 01a17440 _termenv +01a19480 end 01a17460 _term_type +018a3f20 etext 01a19480 end [38 symbols] diff --git a/apps/termtris/build/termtris.coff b/apps/termtris/build/termtris.coff new file mode 100644 index 0000000000000000000000000000000000000000..5836bfc488318373bb6ab8263d460e90a57fc24e GIT binary patch literal 15157 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 W&JYU~Q^P>tu3l`-(3t%l?EeGx5qy*Y literal 0 HcmV?d00001 diff --git a/apps/termtris/build/termtris.map b/apps/termtris/build/termtris.map new file mode 100644 index 0000000..af6ca6a --- /dev/null +++ b/apps/termtris/build/termtris.map @@ -0,0 +1,141 @@ +***************************************************** +TMS340 COFF Linker Version 5.01 +***************************************************** +Tue Nov 03 20:01:11 2026 + +OUTPUT FILE NAME: +ENTRY POINT SYMBOL: "_start" address: 018a0000 + + +MEMORY CONFIGURATION + + name origin length attributes + -------- -------- --------- ---------- + prog 018a0000 000020000 RWIX + ram 01a00000 000040000 RWIX + + +SECTION ALLOCATION MAP + + output attributes/ +section page origin length input sections +-------- ---- ---------- ---------- ---------------- +.text 0 018a0000 00014940 + 018a0000 00000150 ENTRY.obj (.text) + 018a0150 00001c90 MAIN.obj (.text) + 018a1de0 0000ce50 GAME.obj (.text) + 018aec30 00000d20 SCOREDB.obj (.text) + 018af950 00002a40 X11.obj (.text) + 018b2390 00001640 COMPAT.obj (.text) + 018b39d0 00000210 XGFIX.obj (.text) + 018b3be0 00000450 rts.lib (.text) + 018b4030 000001a0 rts.lib (.text) + 018b41d0 00000230 rts.lib (.text) + 018b4400 00000150 rts.lib (.text) + 018b4550 00000120 rts.lib (.text) + 018b4670 00000110 rts.lib (.text) + 018b4780 000001c0 rts.lib (.text) + +.cinit 0 018b4940 000012d0 + 018b4940 00000050 MAIN.obj (.cinit) + 018b4990 00001190 GAME.obj (.cinit) + 018b5b20 00000050 SCOREDB.obj (.cinit) + 018b5b70 00000050 X11.obj (.cinit) + 018b5bc0 00000050 rts.lib (.cinit) + +.bss 0 01a00000 0001ec20 UNINITIALIZED + 01a00000 00008000 ENTRY.obj (.bss) + 01a08000 00000400 MAIN.obj (.bss) + 01a08400 00004a40 GAME.obj (.bss) + 01a0ce40 00000b80 SCOREDB.obj (.bss) + 01a0d9c0 0000f220 X11.obj (.bss) + 01a1cbe0 00000020 COMPAT.obj (.bss) + 01a1cc00 00002000 XGFIX.obj (.bss) + 01a1ec00 00000020 rts.lib (.bss) + +.data 0 018a0000 00000000 UNINITIALIZED + + +GLOBAL SYMBOLS + +address name address name +-------- ---- -------- ---- +01a00000 .bss 018a0000 .text +018a0000 .data 018a0000 .data +018a0000 .text 018a0000 edata +01a08a00 _blackpf 018a0000 _start +018a2f40 _cleanup_game 018a0150 _now_ms +01a08380 _dbg_events 018a03d0 _sleep_ms +01a083c0 _dbg_focus 018a0530 _vt_poll_event +01a083a0 _dbg_last 018a0ea0 _vt_handle_event +01a08360 _dbg_loops 018a1700 _wait_display +01a083e0 _dbg_rev 018a1740 _main +018b2390 _fflush 018a1de0 _init_game +018a84e0 _game_input 018a2f40 _cleanup_game +018a1de0 _init_game 018a3130 _update +018a1740 _main 018a84e0 _game_input +018b3be0 _memcpy 018aef90 _read_scores +018b3d80 _memmove 018af310 _save_score +018b4030 _memset 018af8a0 _print_scores +01a089a0 _monochrome 018af950 _vt_root_window +018a0150 _now_ms 018afa30 _vt_x11_init +01a089e0 _onlyascii 018b01e0 _x11_clearscr +018af8a0 _print_scores 018b03a0 _x11_reset +018b2400 _putchar 018b03e0 _x11_cursor +01a08940 _quit 018b0440 _x11_setcolor +018b41d0 _rand 018b0560 _x11_setcursor +018aef90 _read_scores 018b07b0 _x11_ibmchar +01a08a20 _rotstep 018b0c30 _term_putstr +018af310 _save_score 018b0dd0 _vt_x11_flush +01a08a80 _scr 018b2000 _vt_x11_term_init +018a03d0 _sleep_ms 018b2320 _term_init +018b2c10 _sprintf 018b2390 _fflush +018b4330 _srand 018b2400 _putchar +018a0000 _start 018b2c10 _sprintf +01a1cbe0 _stdout 018b39d0 _vt_install_xg +018b4400 _strcmp 018b3be0 _memcpy +018b4550 _strcpy 018b3d80 _memmove +018b4670 _strlen 018b4030 _memset +018b4780 _strncpy 018b41d0 _rand +01a1cb00 _term_clearscr 018b4330 _srand +01a1cb40 _term_cursor 018b4400 _strcmp +01a08a60 _term_height 018b4550 _strcpy +01a1cb80 _term_ibmchar 018b4670 _strlen +018b2320 _term_init 018b4780 _strncpy +018b0c30 _term_putstr 018b4940 cinit +01a1cae0 _term_reset 018b4940 etext +01a1cb60 _term_setcolor 01a00000 .bss +01a1cb20 _term_setcursor 01a08360 _dbg_loops +01a1cbc0 _term_type 01a08380 _dbg_events +01a08a40 _term_width 01a083a0 _dbg_last +01a1cba0 _termenv 01a083c0 _dbg_focus +01a08960 _tick_interval 01a083e0 _dbg_rev +018a3130 _update 01a08940 _quit +01a08980 _use_bell 01a08960 _tick_interval +01a089c0 _use_gfxchar 01a08980 _use_bell +01a0ce40 _username 01a089a0 _monochrome +01a1ca40 _vt_dpy 01a089c0 _use_gfxchar +01a1ca80 _vt_gc 01a089e0 _onlyascii +018a0ea0 _vt_handle_event 01a08a00 _blackpf +018b39d0 _vt_install_xg 01a08a20 _rotstep +018a0530 _vt_poll_event 01a08a40 _term_width +018af950 _vt_root_window 01a08a60 _term_height +01a1ca60 _vt_win 01a08a80 _scr +01a1cac0 _vt_x11_drain_events 01a0ce40 _username +018b0dd0 _vt_x11_flush 01a1ca40 _vt_dpy +018afa30 _vt_x11_init 01a1ca60 _vt_win +01a1caa0 _vt_x11_progress 01a1ca80 _vt_gc +018b2000 _vt_x11_term_init 01a1caa0 _vt_x11_progress +018a1700 _wait_display 01a1cac0 _vt_x11_drain_events +018b01e0 _x11_clearscr 01a1cae0 _term_reset +018b03e0 _x11_cursor 01a1cb00 _term_clearscr +018b07b0 _x11_ibmchar 01a1cb20 _term_setcursor +018b03a0 _x11_reset 01a1cb40 _term_cursor +018b0440 _x11_setcolor 01a1cb60 _term_setcolor +018b0560 _x11_setcursor 01a1cb80 _term_ibmchar +018b4940 cinit 01a1cba0 _termenv +018a0000 edata 01a1cbc0 _term_type +01a1ec20 end 01a1cbe0 _stdout +018b4940 etext 01a1ec20 end + +[77 symbols] diff --git a/apps/termtris/evidence/render_frame3400.png b/apps/termtris/evidence/render_frame3400.png index dfc882f79cf41180dd315f52b75d34e04c2c59cd..05de4424d7c9a7f45ab2e5ee6964311a032c87ec 100644 GIT binary patch literal 18967 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

>kRa&J@12$Jwu-IArdyf83cw<$& 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 literal 19845 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 diff --git a/apps/termtris/evidence/termtris_frame3300.png b/apps/termtris/evidence/termtris_frame3300.png new file mode 100644 index 0000000000000000000000000000000000000000..92dafb50bba599c627d5b9dc22397f265167f427 GIT binary patch literal 20986 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 literal 0 HcmV?d00001 diff --git a/apps/termtris/evidence/termtris_quit_3200.png b/apps/termtris/evidence/termtris_quit_3200.png new file mode 100644 index 0000000000000000000000000000000000000000..098ab58ebd90278d98366160625b29c0b94e38d7 GIT binary patch literal 19407 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 YgV)+DzKH!1%u_*H+wCpbed7E70MbA&RsaA1 literal 0 HcmV?d00001 diff --git a/apps/termtris/src/game.c b/apps/termtris/src/game.c index 6209491..4eb2cb6 100644 --- a/apps/termtris/src/game.c +++ b/apps/termtris/src/game.c @@ -25,6 +25,8 @@ along with this program. If not, see . #include "term.h" #include "scoredb.h" +extern long now_ms(void); + int quit; long tick_interval; @@ -190,7 +192,7 @@ int init_game(void) scores = read_scores(0, 10); memset(&cur_score, 0, sizeof cur_score); - srand(time(0)); + srand((unsigned)now_ms()); pause = 0; gameover = 0; diff --git a/apps/termtris/src/vt1200/compat.c b/apps/termtris/src/vt1200/compat.c index 02c67b7..b216082 100644 --- a/apps/termtris/src/vt1200/compat.c +++ b/apps/termtris/src/vt1200/compat.c @@ -17,6 +17,12 @@ int fflush(FILE *fp) return 0; } +int putchar(int c) +{ + (void)c; + return c; +} + static char *fmt_num(char *out, long v, int width, int zero, int left, int unsig) { char tmp[24]; diff --git a/apps/termtris/src/vt1200/main.c b/apps/termtris/src/vt1200/main.c index ce75f2b..650b978 100644 --- a/apps/termtris/src/vt1200/main.c +++ b/apps/termtris/src/vt1200/main.c @@ -18,6 +18,7 @@ static vt_u32 evbuf[24]; static long ms_base; static int ms_last = -1; static int focused; +volatile vt_u32 dbg_loops, dbg_events, dbg_last, dbg_focus, dbg_rev; long now_ms(void) { @@ -88,10 +89,18 @@ void vt_handle_event(void) int n; VT_XNEXT(vt_dpy, (void *)evbuf); + dbg_events = dbg_events + 1; + dbg_last = evbuf[0]; if(evbuf[0] == 2) { /* KeyPress */ ks = 0; n = VT_XLOOKUP((void *)evbuf, buf, 8, &ks, 0); map_keysym(ks, n > 0 ? buf[0] : 0, n); + } else if(evbuf[0] == 5) { /* ButtonRelease: sample focus */ + vt_u32 fw; + int rv; + VT_XGETFOCUS(vt_dpy, &fw, &rv); + dbg_focus = fw; + dbg_rev = (vt_u32)rv; } else if(evbuf[0] == 12) { /* Expose */ game_input('`'); } else if(evbuf[0] == 9) { /* FocusIn */ @@ -111,17 +120,33 @@ int main(void) { long d; - if(vt_x11_init() != 0) - return 1; + /* The display server may still be coming up when this process is + * created; retry for a while before giving up. */ + { + int tries; + for(tries = 0; tries < 40; tries++) { + if(vt_x11_init() == 0) break; + VT_SLEEP(50); + } + if(tries == 40) return 1; + } + onlyascii = 1; + use_gfxchar = 0; + monochrome = 1; /* the VT1200 raster is 1 bit deep */ if(init_game() == -1) return 2; vt_x11_flush(); for(;;) { + dbg_loops = dbg_loops + 1; VT_XSYNC(vt_dpy, 0); while(vt_poll_event()) { vt_handle_event(); } + if(quit) { + cleanup_game(); + break; + } d = update(now_ms()); sleep_ms((int)d); vt_x11_flush(); diff --git a/apps/termtris/src/vt1200/scoredb.c b/apps/termtris/src/vt1200/scoredb.c new file mode 100644 index 0000000..0c227af --- /dev/null +++ b/apps/termtris/src/vt1200/scoredb.c @@ -0,0 +1,76 @@ +/* + * scoredb.c -- RAM-only high scores (the VT1200 has no filesystem). + */ +#include +#include +#include "scoredb.h" + +#define NENT 10 + +char *username = "VT1200"; + +static struct score_entry list[NENT]; +static char names[NENT][16]; +static int inited; + +static void init_list(void) +{ + int i; + if(inited) return; + inited = 1; + for(i = 0; i < NENT; i++) { + strcpy(names[i], "VT1200"); + list[i].user = names[i]; + list[i].score = 0; + list[i].lines = 0; + list[i].level = 0; + list[i].next = (i + 1 < NENT) ? &list[i + 1] : 0; + } +} + +struct score_entry *read_scores(FILE *fp, int max_scores) +{ + (void)fp; + (void)max_scores; + init_list(); + return &list[0]; +} + +static void copy_entry(struct score_entry *dst, struct score_entry *src) +{ + long sc = src->score, ln = src->lines, lv = src->level; + char *u = (src->user && *src->user) ? src->user : username; + strncpy(dst->user, u, 15); + dst->user[15] = 0; + dst->score = sc; + dst->lines = ln; + dst->level = lv; +} + +int save_score(struct score_entry *sc) +{ + int pos, i; + if(!sc) return 0; + init_list(); + for(pos = 0; pos < NENT; pos++) { + if(sc->score > list[pos].score) break; + } + if(pos >= NENT) return 0; + for(i = NENT - 1; i > pos; i--) { + long s = list[i - 1].score, l = list[i - 1].lines, v = list[i - 1].level; + char tmp[16]; + strcpy(tmp, list[i - 1].user); + strcpy(list[i].user, tmp); + list[i].score = s; + list[i].lines = l; + list[i].level = v; + } + copy_entry(&list[pos], sc); + return 1; +} + +int print_scores(int num) +{ + (void)num; + return 0; +} diff --git a/apps/termtris/src/vt1200/stdio.h b/apps/termtris/src/vt1200/stdio.h index 66f6fe9..7a3b79c 100644 --- a/apps/termtris/src/vt1200/stdio.h +++ b/apps/termtris/src/vt1200/stdio.h @@ -11,5 +11,6 @@ extern FILE *stdout; int sprintf(char *buf, char *fmt, ...); int fflush(FILE *fp); +int putchar(int c); #endif diff --git a/apps/termtris/src/vt1200/x11.c b/apps/termtris/src/vt1200/x11.c index d2ea2bc..a79053e 100644 --- a/apps/termtris/src/vt1200/x11.c +++ b/apps/termtris/src/vt1200/x11.c @@ -19,7 +19,7 @@ struct vt_cell { unsigned short ch; - unsigned char fg, bg, dirty; + unsigned char inv, dirty; }; static struct vt_cell cells[ROWS * COLS]; @@ -51,14 +51,18 @@ vt_u32 vt_root_window(void *dpy) int vt_x11_init(void) { + vt_x11_progress = 11; if(!vt_install_xg()) return -1; + vt_x11_progress = 12; vt_dpy = VT_XOPEN("0::0"); if(!vt_dpy) return -1; + vt_x11_progress = 13; vt_win = VT_XCREATE(vt_dpy, vt_root_window(vt_dpy), 80, 80, 480, 312, 2, 1, 0); if(!vt_win) return -1; + vt_x11_progress = 14; VT_XSELECT(vt_dpy, vt_win, 0x22000FUL); vt_gc = VT_XGC(vt_dpy, vt_win, 0, 0); if(!vt_gc) @@ -73,8 +77,7 @@ void x11_clearscr(void) int i; for(i = 0; i < ROWS * COLS; i++) { cells[i].ch = ' '; - cells[i].fg = 0; - cells[i].bg = 0; + cells[i].inv = 0; cells[i].dirty = 1; } cur_row = cur_col = 0; @@ -113,8 +116,9 @@ void x11_ibmchar(unsigned char c, unsigned char attr) if(cur_col < 0 || cur_col >= COLS) return; p = &cells[cur_row * COLS + cur_col]; p->ch = c; - p->fg = (attr >> 4) & 0xf; - p->bg = attr & 0xf; + /* The VT1200 raster is one bit deep, so follow the game's monochrome + * convention (ansi.c -m): colour is dropped, the bold bit reverses. */ + p->inv = (attr & 0x80) ? 1 : 0; p->dirty = 1; cur_col++; if(cur_col >= COLS) { @@ -132,42 +136,54 @@ void term_putstr(const char *s, unsigned char attr) void vt_x11_flush(void) { - int i, row_dirty = 0, dirty_any = 0; - char one[2]; + int r, c, any = 0; + char line[COLS + 1]; + vt_x11_progress = 1; - for(i = 0; i < ROWS * COLS; i++) { - struct vt_cell *p = &cells[i]; - vt_x11_progress = 0x100 + (vt_u32)i; - if(p->dirty) { - int x, y, bgpx, fgpx; - x = (i % COLS) * CELL_W; - y = (i / COLS) * CELL_H; - bgpx = p->bg == 0 ? 0 : 1; - fgpx = p->fg == 0 ? 0 : 1; - VT_XSETFG(vt_dpy, vt_gc, (vt_u32)bgpx); - VT_XFILL(vt_dpy, vt_win, vt_gc, x, y, CELL_W, CELL_H); - VT_XSETFG(vt_dpy, vt_gc, (vt_u32)fgpx); - one[0] = (char)p->ch; - one[1] = 0; - VT_XDRAW(vt_dpy, vt_win, vt_gc, x, y + BASE_OFF, one, 1); - p->dirty = 0; - row_dirty = 1; - dirty_any = 1; + for(r = 0; r < ROWS; r++) { + struct vt_cell *row = &cells[r * COLS]; + int dirty = 0; + + for(c = 0; c < COLS; c++) { + if(row[c].dirty) dirty = 1; } - if((i % COLS) == COLS - 1 && row_dirty) { - vt_u32 ev[24]; - vt_x11_progress = 0x10000 + (vt_u32)(i / COLS); - VT_XSYNC(vt_dpy, 0); - if(vt_x11_drain_events) { - while(*(vt_u32 *)((char *)vt_dpy + 0x54) != 0) { - VT_XNEXT(vt_dpy, (void *)ev); - } - } - row_dirty = 0; + if(!dirty) continue; + + vt_x11_progress = 0x100 + (vt_u32)r; + any = 1; + + /* repaint the whole row: black background, then the text. + * One fill + one string per row keeps the request stream small + * enough that the in-machine X server can drain it. */ + VT_XSETFG(vt_dpy, vt_gc, 0); + VT_XFILL(vt_dpy, vt_win, vt_gc, 0, r * CELL_H, COLS * CELL_W, CELL_H); + + for(c = 0; c < COLS; c++) { + line[c] = row[c].ch ? (char)row[c].ch : ' '; } + line[COLS] = 0; + + VT_XSETFG(vt_dpy, vt_gc, 1); + VT_XDRAW(vt_dpy, vt_win, vt_gc, 0, r * CELL_H + BASE_OFF, line, COLS); + + /* inverse (bold) cells: white block with black text */ + for(c = 0; c < COLS; c++) { + int start; + if(!row[c].inv) continue; + start = c; + while(c < COLS && row[c].inv) c++; + VT_XSETFG(vt_dpy, vt_gc, 1); + VT_XFILL(vt_dpy, vt_win, vt_gc, start * CELL_W, + r * CELL_H, (c - start) * CELL_W, CELL_H); + VT_XSETFG(vt_dpy, vt_gc, 0); + VT_XDRAW(vt_dpy, vt_win, vt_gc, start * CELL_W, + r * CELL_H + BASE_OFF, line + start, c - start); + } + + for(c = 0; c < COLS; c++) row[c].dirty = 0; } - if(dirty_any) { - VT_XSYNC(vt_dpy, 0); + if(any) { + VT_XFLUSH(vt_dpy); } vt_x11_progress = 0xFFFFFFUL; } diff --git a/apps/termtris/src/vt1200/xlib.h b/apps/termtris/src/vt1200/xlib.h index a51293e..342c4c8 100644 --- a/apps/termtris/src/vt1200/xlib.h +++ b/apps/termtris/src/vt1200/xlib.h @@ -26,6 +26,7 @@ typedef int (*vt_xfill_t)(void *, vt_u32, vt_u32, int, int, typedef int (*vt_xdraw_t)(void *, vt_u32, vt_u32, int, int, const char *, int); typedef int (*vt_sleep_t)(int); +typedef int (*vt_xgetfocus_t)(void *, vt_u32 *, int *); #define VT_XOPEN ((vt_xopen_t)0x3F9C2570UL) #define VT_XCREATE ((vt_xcreate_t)0x3FD41B90UL) @@ -41,6 +42,7 @@ typedef int (*vt_sleep_t)(int); #define VT_XDRAW ((vt_xdraw_t)0x3FDA0000UL) #define VT_XPENDING ((int (*)(void *))0x3FD67620UL) #define VT_SLEEP ((vt_sleep_t)0x3FB33A20UL) +#define VT_XGETFOCUS ((vt_xgetfocus_t)0x3FD55210UL) /* display->screens[] is at bit 0x480 / byte 0x90; screen->root at bit 0x40 / * byte 8 (see dwt/notes/GUI_APPS.md and dwt/gui/xhello.asm). */ diff --git a/apps/termtris/tests/input.lua b/apps/termtris/tests/input.lua index 2f619ee..ca3c703 100644 --- a/apps/termtris/tests/input.lua +++ b/apps/termtris/tests/input.lua @@ -35,6 +35,7 @@ local key_right = ports[":lk401:K75"].fields["Right"] local key_zero = ports[":lk401:K10"].fields["0 )"] local key_ret = ports[":lk401:K68"].fields["Return"] local key_del = ports[":lk401:K67"].fields["Delete"] +local key_q = ports[":lk401:K13"].fields["Q"] local tabx = ports[":tablet:TABLET_X"].fields["Tablet X"] local taby = ports[":tablet:TABLET_Y"].fields["Tablet Y"] local tabb = ports[":tablet:TABLET_BTN"].fields["P1 Button 1"] @@ -151,6 +152,8 @@ local function key_step(frames, key) if frames == 3230 then key_ret:set_value(0) end if frames == 3240 then key_del:set_value(1) end if frames == 3250 then key_del:set_value(0) end + if frames == 3260 then key_q:set_value(1) end + if frames == 3270 then key_q:set_value(0) end end local function r32(a) diff --git a/apps/termtris/tests/input.sh b/apps/termtris/tests/input.sh index c43cd69..6453f57 100755 --- a/apps/termtris/tests/input.sh +++ b/apps/termtris/tests/input.sh @@ -20,7 +20,7 @@ LOADER_OUT="$APP/build" \ -autoboot_script "$DIR/input.lua" -autoboot_delay 0 \ >"$LOG" 2>&1 || true grep 'INPUT:' "$LOG" | tail -3 -grep -q 'INPUT: .*bytes=.*61 9B 41 9B 42 9B 44 9B 43 30 0A 7F' "$LOG" || { +grep -q 'INPUT: .*bytes=.*61 9B 41 9B 42 9B 44 9B 43 30 0A 7F 71' "$LOG" || { echo "input.sh: mapped key sequence missing (see $LOG)" >&2 exit 1 } diff --git a/apps/termtris/tests/play.lua b/apps/termtris/tests/play.lua new file mode 100644 index 0000000..5f35daf --- /dev/null +++ b/apps/termtris/tests/play.lua @@ -0,0 +1,345 @@ +-- loader.lua -- debugger-assisted COFF loader acceptance harness. +-- +-- Boots the firmware with VT1200_SKIP_LEVEL1=1 (the documented divergence) to +-- the OS, then at frame LOADER_AT pokes: +-- * the loader payload at bit 0x01880000 (raw dwt/loader/build/loader.bin) +-- * a GSPLNK COFF image at LOADER_BUF (default bit 0x01900000) +-- * the loader descriptor at 0x01882C00 +-- and sets PC = 0x01880000. It does not use the network, the rdebug service +-- or any firmware transport: this is the debugger-assisted milestone. +-- +-- The loader itself copies the COFF sections to their linked bit addresses and +-- calls create_process(entry, "LOADER", 0x6000, 1, 0). This script only pokes +-- the input and reads the result; it never writes the app's code or markers. +-- +-- Env: +-- LOADER_BIN loader payload (default dwt/loader/build/loader.bin) +-- LOADER_COFF app COFF (default dwt/loader/build/marker.coff) +-- LOADER_BUF COFF buffer bit address (default 0x01900000) +-- LOADER_AT injection frame (default 2500) +-- LOADER_END inspection/exit frame (default 2700) +-- LOADER_OUT evidence directory (default dwt/loader/evidence) +-- LOADER_SNAP 1 = write a PNG at LOADER_END (default 0) +-- LOADER_VERBOSE 1 = print the result block every 25 frames + +local function hex(v, w) return string.format("%0" .. (w or 8) .. "X", v) end + +local machine = manager.machine +local ports = machine.ioport.ports +local tabx = ports[":tablet:TABLET_X"].fields["Tablet X"] +local taby = ports[":tablet:TABLET_Y"].fields["Tablet Y"] +local tabb = ports[":tablet:TABLET_BTN"].fields["P1 Button 1"] +local cpu = machine.devices[':maincpu'] +local dbg = machine.debugger +local prog = cpu.spaces['program'] + +if not dbg then + print("LOADER: debugger not enabled; pass -debug") + machine:exit() + return +end + +local AT = tonumber(os.getenv("LOADER_AT") or "2500") +local ENDF = tonumber(os.getenv("LOADER_END") or "2700") +local BIN = os.getenv("LOADER_BIN") or "dwt/loader/build/loader.bin" +local COFF = os.getenv("LOADER_COFF") or "dwt/loader/build/marker.coff" +local BUF = tonumber(os.getenv("LOADER_BUF") or "0x01900000") +local OUT = os.getenv("LOADER_OUT") or "dwt/loader/evidence" +local SNAP = os.getenv("LOADER_SNAP") == "1" +local MODE = tonumber(os.getenv("LOADER_MODE") or "0") +local A8DESC = os.getenv("LOADER_A8_DESC") == "1" +local VERBOSE = os.getenv("LOADER_VERBOSE") == "1" + +local LOADER_BASE = 0x01880000 +local DESC = 0x01882C00 +local RESULT = 0x01882D00 +local RETURN_STUB = 0x01883100 +local RETURN_SP = 0x01883110 + +local function rd(p) return io.open(p, "rb") end +local function slurp(p) + local f, err = rd(p) + if not f then error("cannot open " .. p .. ": " .. tostring(err)) end + local s = f:read("*a") + f:close() + return s +end + +local function u16(s, o) + return s:byte(o + 1) + s:byte(o + 2) * 256 +end + +local function u32(s, o) + return s:byte(o + 1) + s:byte(o + 2) * 256 + + s:byte(o + 3) * 65536 + s:byte(o + 4) * 16777216 +end + +local function parse_coff(d) + local c = { data = d } + c.magic = u16(d, 0) + c.nscns = u16(d, 2) + c.opthdr = u16(d, 16) + c.sections = {} + if c.opthdr > 0 then + c.optmagic = u16(d, 20) + c.entry = u32(d, 20 + 16) + c.textstart = u32(d, 20 + 20) + c.datastart = u32(d, 20 + 24) + end + local base = 20 + c.opthdr + for i = 0, c.nscns - 1 do + local o = base + i * 40 + local name = d:sub(o + 1, o + 8):gsub("%z", "") + c.sections[#c.sections + 1] = { + name = name, + paddr = u32(d, o + 8), + size = u32(d, o + 16), -- bits + scnptr = u32(d, o + 20), + flags = u32(d, o + 36), + } + end + return c +end + +local function poke_bytes(bitaddr, data) + local n = math.floor((#data + 1) / 2) + for i = 0, n - 1 do + local lo = data:byte(i * 2 + 1) or 0 + local hi = data:byte(i * 2 + 2) or 0 + prog:write_u16(bitaddr + i * 16, lo + hi * 256) + end + return n +end + +local function verify_bytes(bitaddr, data) + local n = math.floor((#data + 1) / 2) + local bad = 0 + for i = 0, n - 1 do + local lo = data:byte(i * 2 + 1) or 0 + local hi = data:byte(i * 2 + 2) or 0 + if prog:read_u16(bitaddr + i * 16) ~= lo + hi * 256 then + bad = bad + 1 + end + end + return n, bad +end + +local function w32(a, v) prog:write_u32(a, v) end +local function r32(a) + local ok, v = pcall(function() return prog:read_u32(a) end) + if ok then return v end + return 0xDEADBEEF +end + +local loader = slurp(BIN) +local coff = slurp(COFF) +local app = parse_coff(coff) + +print(string.format("LOADER: payload %s (%d B) -> bit %s", BIN, #loader, hex(LOADER_BASE))) +print(string.format("LOADER: COFF %s (%d B) -> bit %s", COFF, #coff, hex(BUF))) +print(string.format("LOADER: file magic=%04X nscns=%d opthdr=%d optmagic=%s entry=%s textstart=%s", + app.magic, app.nscns, app.opthdr, + app.optmagic and string.format("%04X", app.optmagic) or "none", + hex(app.entry or 0), hex(app.textstart or 0))) +for _, s in ipairs(app.sections) do + print(string.format("LOADER: sec %-8s paddr=%s size=%d bits (%d B) scnptr=%#x flags=%#x", + s.name, hex(s.paddr), s.size, math.floor(s.size / 8), s.scnptr, s.flags)) +end + +local TRACE = os.getenv("LOADER_TRACE") == "1" + +local frames = 0 +local injected = false +local done = false +local last_count = -1 + +local function status_name(v) + local t = { + [0] = "OK", + [1] = "bad descriptor", + [2] = "truncated COFF / section headers", + [3] = "bad COFF magic", + [4] = "section size not whole words", + [5] = "target outside RAM", + [6] = "target overlaps loader page or COFF buffer", + [7] = "relocation records present (unsupported)", + [8] = "target not in a free-list block", + [9] = "no entry point", + } + return t[v] or ("error " .. tostring(v)) +end + +local function result_dump() + local st = r32(RESULT + 0x000) + print(string.format( + "LOADER: result status=%d (%s) entry=%s text=%s/%d bits data=%s/%d bits bss=%s/%d bits relocs=%d pcb=%s nscns=%d len=%d", + st, status_name(st), hex(r32(RESULT + 0x020)), + hex(r32(RESULT + 0x040)), r32(RESULT + 0x060), + hex(r32(RESULT + 0x080)), r32(RESULT + 0x0A0), + hex(r32(RESULT + 0x0C0)), r32(RESULT + 0x0E0), + r32(RESULT + 0x100), hex(r32(RESULT + 0x120)), + r32(RESULT + 0x140), r32(RESULT + 0x160))) +end + +local function inject() + -- 1. loader itself + local n, bad = 0, 0 + poke_bytes(LOADER_BASE, loader) + n, bad = verify_bytes(LOADER_BASE, loader) + print(string.format("LOADER: loader poked %d words, readback %d bad at %s", + n, bad, hex(LOADER_BASE))) + + -- 2. COFF image + poke_bytes(BUF, coff) + n, bad = verify_bytes(BUF, coff) + print(string.format("LOADER: COFF poked %d words, readback %d bad at %s", + n, bad, hex(BUF))) + + -- 3. pre-fill every .bss section with A5A5, so a loaded zero is meaningful + for _, s in ipairs(app.sections) do + if s.flags & 0x80 ~= 0 then + for i = 0, math.floor(s.size / 16) - 1 do + prog:write_u16(s.paddr + i * 16, 0xA5A5) + end + print(string.format("LOADER: pre-filled .bss %s..%s with A5A5", + hex(s.paddr), hex(s.paddr + s.size))) + end + end + + -- 4. descriptor: magic "LD" "R1", buf, len, mode=0 (spin) + w32(DESC + 0x00, 0) + prog:write_u16(DESC + 0x00, 0x4C44) + prog:write_u16(DESC + 0x10, 0x5231) + w32(DESC + 0x20, BUF) + w32(DESC + 0x40, #coff) + w32(DESC + 0x60, MODE) + print(string.format("LOADER: descriptor @%s magic=%04X/%04X buf=%s len=%d mode=%d", + hex(DESC), prog:read_u16(DESC + 0x00), prog:read_u16(DESC + 0x10), + hex(r32(DESC + 0x20)), r32(DESC + 0x40), r32(DESC + 0x60))) + + -- 5. return stub. mode 0 ignores it (the loader jumps to ORIG_PC + -- itself); mode 1 returns here, and the stub resumes the interrupted + -- PC so the caller yields and the scheduler can run the child. + local orig_pc = cpu.state['PC'].value + if MODE == 1 then + prog:write_u16(RETURN_STUB, 0xC080) -- jauc + prog:write_u16(RETURN_STUB + 0x10, orig_pc & 0xFFFF) + prog:write_u16(RETURN_STUB + 0x20, math.floor(orig_pc / 0x10000) & 0xFFFF) + else + prog:write_u16(RETURN_STUB, 0xC0FF) -- jruc self + end + + -- 6. enter the loader. A8=0 selects the fixed descriptor; SP points at + -- the return slot so mode=1 would return to the spin stub. + -- Leave SP alone for mode 0: the loader pushes onto the current + -- process stack, then restores it and resumes at ORIG_PC. (mode 1 + -- would use the return stub.) + w32(0x01882A20, orig_pc) -- LD_ORIG_PC + print(string.format("LOADER: will resume at %s after create_process", hex(orig_pc))) + cpu.state['A8'].value = A8DESC and DESC or 0 + if MODE == 1 then + -- Simulate a CALLA from the current process: push the return + -- address on the *firmware* stack (a real transport would do this). + local sp = cpu.state['SP'].value - 0x20 + w32(sp, RETURN_STUB) + cpu.state['SP'].value = sp + end + cpu.state['PC'].value = LOADER_BASE + injected = true + print(string.format("LOADER: frame %d: entered loader at %s (caller SP=%s)", + frames, hex(LOADER_BASE), hex(cpu.state['SP'].value))) +end + +local function inspect() + result_dump() + + -- Verify the placement from RAM: .text/.data must equal the file bytes. + for _, s in ipairs(app.sections) do + if s.scnptr ~= 0 and s.size > 0 then + local nb = math.floor(s.size / 8) + local bytes = coff:sub(s.scnptr + 1, s.scnptr + nb) + local _, bad = verify_bytes(s.paddr, bytes) + print(string.format("LOADER: placed %s at %s: %d bytes, %d differing", + s.name, hex(s.paddr), nb, bad)) + end + end + + local base = COFF:match("([^/]+)$") or COFF + if base:find("marker") then + print(string.format("LOADER: marker app: start=%s data=%s data2=%s bss=%s count=%d", + hex(r32(0x018E0000)), hex(r32(0x018E0020)), hex(r32(0x018E0040)), + hex(r32(0x018E0060)), r32(0x018E0080))) + elseif base:find("xhello") then + print(string.format("LOADER: xhello: stage=%d strlen=%d display=%s root=%s window=%s sync=%s", + r32(0x01890140), r32(0x01890160), hex(r32(0x01890100)), + hex(r32(0x01890110)), hex(r32(0x01890120)), hex(r32(0x01890130)))) + end + print(string.format("LOADER: PC at inspection = %s", hex(cpu.state['PC'].value))) +end + +emu.register_frame_done(function() + frames = frames + 1 + if frames == 2900 then tabx:set_value(700); taby:set_value(500); print("PLAY: tablet move") end + if frames == 3000 then tabb:set_value(1); print("PLAY: click down") end + if frames == 3020 then tabb:set_value(0); print("PLAY: click up") end + if frames == AT and not injected then + inject() + if TRACE then dbg:command("trace /tmp/opencode/ld1.tr") end + end + if injected and TRACE and frames == AT + 1 then + dbg:command("trace off") + print("LOADER: trace written to /tmp/opencode/ld.tr") + end + if injected and not done then + if TRACE and frames <= AT + 40 then + print(string.format("LOADER: trace f=%d PC=%s SP=%s A14=%s status=%s entry=%s", + frames, hex(cpu.state['PC'].value), hex(cpu.state['SP'].value), + hex(cpu.state['A14'].value), hex(r32(RESULT + 0x000)), + hex(r32(RESULT + 0x020)))) + end + local st = r32(RESULT + 0x000) + -- status 0 means OK; nonzero is final too once parsing has begun + if st ~= 0xFFFFFFFF and not done then + -- The loader writes 0 on success. A nonzero status is final + -- only after it has been written once (it is 0 from power-on, + -- so require the loader to have run: PC no longer at entry or + -- the app counter moved). + end + if VERBOSE and frames % 25 == 0 then + result_dump() + end + if base_contains_marker then + local c = r32(0x018E0080) + if c ~= last_count then + if last_count ~= -1 then + print(string.format("LOADER: marker counter %d -> %d (child running)", + last_count, c)) + end + last_count = c + end + end + end + if frames >= ENDF then + if injected then + done = true + inspect() + if SNAP then + local screen = nil + for _, s in pairs(machine.screens) do screen = s break end + if screen then + local out = OUT .. "/loader_frame" .. frames .. ".png" + local ok, err = pcall(function() return screen:snapshot(out) end) + print(string.format("LOADER: snapshot %s ok=%s %s", + out, tostring(ok), tostring(err or ""))) + end + end + end + machine:exit() + end +end, "vt1200-loader") + +-- (Lua has no forward use of a name defined later; set a small flag here.) +base_contains_marker = (COFF:match("([^/]+)$") or ""):find("marker") ~= nil + +dbg.visible_cpu = cpu +dbg.execution_state = 'run' diff --git a/apps/termtris/tests/play.sh b/apps/termtris/tests/play.sh new file mode 100755 index 0000000..56f88fb --- /dev/null +++ b/apps/termtris/tests/play.sh @@ -0,0 +1,25 @@ +#!/bin/sh +# play.sh -- load termtris, click it active, and snapshot the board. +set -eu +DIR=$(cd "$(dirname "$0")" && pwd) +APP=$(cd "$DIR/.." && pwd) +ROOT=$(cd "$APP/../.." && pwd) +M=${MAME:-/home/arti/code/k-space/mame0289/vt1200} +EV="$APP/evidence" +LOG="$APP/build/play.log" +mkdir -p "$EV" "$APP/build" +rm -f "$EV/loader_frame3300.png" +VT1200_SKIP_LEVEL1=1 LOADER_SNAP=1 \ +LOADER_AT=2500 LOADER_END=3300 \ +LOADER_BIN="$ROOT/dwt/loader/build/loader.bin" \ +LOADER_COFF="$APP/build/termtris.coff" \ +LOADER_OUT="$EV" \ +"$M" vt1200 -rompath "$ROOT/harness/mame/roms" \ + -video none -sound none -nothrottle -skip_gameinfo \ + -debug -debugger none \ + -autoboot_script "$DIR/play.lua" -autoboot_delay 0 \ + >"$LOG" 2>&1 || true +grep -E 'PLAY:|LOADER: result|LOADER: snapshot' "$LOG" | tail -6 +[ -s "$EV/loader_frame3300.png" ] || { echo "play.sh: no snapshot (see $LOG)" >&2; exit 1; } +cp "$EV/loader_frame3300.png" "$EV/termtris_frame3300.png" +echo "play: $EV/termtris_frame3300.png" diff --git a/apps/termtris/tests/play_keys.lua b/apps/termtris/tests/play_keys.lua new file mode 100644 index 0000000..38f5f77 --- /dev/null +++ b/apps/termtris/tests/play_keys.lua @@ -0,0 +1,390 @@ +-- loader.lua -- debugger-assisted COFF loader acceptance harness. +-- +-- Boots the firmware with VT1200_SKIP_LEVEL1=1 (the documented divergence) to +-- the OS, then at frame LOADER_AT pokes: +-- * the loader payload at bit 0x01880000 (raw dwt/loader/build/loader.bin) +-- * a GSPLNK COFF image at LOADER_BUF (default bit 0x01900000) +-- * the loader descriptor at 0x01882C00 +-- and sets PC = 0x01880000. It does not use the network, the rdebug service +-- or any firmware transport: this is the debugger-assisted milestone. +-- +-- The loader itself copies the COFF sections to their linked bit addresses and +-- calls create_process(entry, "LOADER", 0x6000, 1, 0). This script only pokes +-- the input and reads the result; it never writes the app's code or markers. +-- +-- Env: +-- LOADER_BIN loader payload (default dwt/loader/build/loader.bin) +-- LOADER_COFF app COFF (default dwt/loader/build/marker.coff) +-- LOADER_BUF COFF buffer bit address (default 0x01900000) +-- LOADER_AT injection frame (default 2500) +-- LOADER_END inspection/exit frame (default 2700) +-- LOADER_OUT evidence directory (default dwt/loader/evidence) +-- LOADER_SNAP 1 = write a PNG at LOADER_END (default 0) +-- LOADER_VERBOSE 1 = print the result block every 25 frames + +local function hex(v, w) return string.format("%0" .. (w or 8) .. "X", v) end + +local machine = manager.machine +local PLAY_MODE = os.getenv("PLAY_MODE") or "move" +print("PLAYKEYS: mode="..PLAY_MODE) +local DL = tonumber(os.getenv("DIAG_LOOPS") or "0") +local DE = tonumber(os.getenv("DIAG_EVENTS") or "0") +local DT = tonumber(os.getenv("DIAG_LAST") or "0") +local DF = tonumber(os.getenv("DIAG_FOCUS") or "0") +local DR = tonumber(os.getenv("DIAG_REV") or "0") +local DQ = tonumber(os.getenv("DIAG_QUIT") or "0") +local DTICK = tonumber(os.getenv("DIAG_TICK") or "0") +local ports = machine.ioport.ports +local key_a = ports[":lk401:K26"].fields["A"] +local key_d = ports[":lk401:K28"].fields["D"] +local key_s = ports[":lk401:K27"].fields["S"] +local key_w = ports[":lk401:K14"].fields["W"] +local key_0 = ports[":lk401:K10"].fields["0 )"] +local key_p = ports[":lk401:K22"].fields["P"] +local key_r = ports[":lk401:K16"].fields["R"] +local key_h = ports[":lk401:K31"].fields["H"] +local key_q = ports[":lk401:K13"].fields["Q"] +local tabx = ports[":tablet:TABLET_X"].fields["Tablet X"] +local taby = ports[":tablet:TABLET_Y"].fields["Tablet Y"] +local tabb = ports[":tablet:TABLET_BTN"].fields["P1 Button 1"] +local cpu = machine.devices[':maincpu'] +local dbg = machine.debugger +local prog = cpu.spaces['program'] + +if not dbg then + print("LOADER: debugger not enabled; pass -debug") + machine:exit() + return +end + +local AT = tonumber(os.getenv("LOADER_AT") or "2500") +local ENDF = tonumber(os.getenv("LOADER_END") or "2700") +local BIN = os.getenv("LOADER_BIN") or "dwt/loader/build/loader.bin" +local COFF = os.getenv("LOADER_COFF") or "dwt/loader/build/marker.coff" +local BUF = tonumber(os.getenv("LOADER_BUF") or "0x01900000") +local OUT = os.getenv("LOADER_OUT") or "dwt/loader/evidence" +local SNAP = os.getenv("LOADER_SNAP") == "1" +local MODE = tonumber(os.getenv("LOADER_MODE") or "0") +local A8DESC = os.getenv("LOADER_A8_DESC") == "1" +local VERBOSE = os.getenv("LOADER_VERBOSE") == "1" + +local LOADER_BASE = 0x01880000 +local DESC = 0x01882C00 +local RESULT = 0x01882D00 +local RETURN_STUB = 0x01883100 +local RETURN_SP = 0x01883110 + +local function rd(p) return io.open(p, "rb") end +local function slurp(p) + local f, err = rd(p) + if not f then error("cannot open " .. p .. ": " .. tostring(err)) end + local s = f:read("*a") + f:close() + return s +end + +local function u16(s, o) + return s:byte(o + 1) + s:byte(o + 2) * 256 +end + +local function u32(s, o) + return s:byte(o + 1) + s:byte(o + 2) * 256 + + s:byte(o + 3) * 65536 + s:byte(o + 4) * 16777216 +end + +local function parse_coff(d) + local c = { data = d } + c.magic = u16(d, 0) + c.nscns = u16(d, 2) + c.opthdr = u16(d, 16) + c.sections = {} + if c.opthdr > 0 then + c.optmagic = u16(d, 20) + c.entry = u32(d, 20 + 16) + c.textstart = u32(d, 20 + 20) + c.datastart = u32(d, 20 + 24) + end + local base = 20 + c.opthdr + for i = 0, c.nscns - 1 do + local o = base + i * 40 + local name = d:sub(o + 1, o + 8):gsub("%z", "") + c.sections[#c.sections + 1] = { + name = name, + paddr = u32(d, o + 8), + size = u32(d, o + 16), -- bits + scnptr = u32(d, o + 20), + flags = u32(d, o + 36), + } + end + return c +end + +local function poke_bytes(bitaddr, data) + local n = math.floor((#data + 1) / 2) + for i = 0, n - 1 do + local lo = data:byte(i * 2 + 1) or 0 + local hi = data:byte(i * 2 + 2) or 0 + prog:write_u16(bitaddr + i * 16, lo + hi * 256) + end + return n +end + +local function verify_bytes(bitaddr, data) + local n = math.floor((#data + 1) / 2) + local bad = 0 + for i = 0, n - 1 do + local lo = data:byte(i * 2 + 1) or 0 + local hi = data:byte(i * 2 + 2) or 0 + if prog:read_u16(bitaddr + i * 16) ~= lo + hi * 256 then + bad = bad + 1 + end + end + return n, bad +end + +local function w32(a, v) prog:write_u32(a, v) end +local function r32(a) + local ok, v = pcall(function() return prog:read_u32(a) end) + if ok then return v end + return 0xDEADBEEF +end + +local loader = slurp(BIN) +local coff = slurp(COFF) +local app = parse_coff(coff) + +print(string.format("LOADER: payload %s (%d B) -> bit %s", BIN, #loader, hex(LOADER_BASE))) +print(string.format("LOADER: COFF %s (%d B) -> bit %s", COFF, #coff, hex(BUF))) +print(string.format("LOADER: file magic=%04X nscns=%d opthdr=%d optmagic=%s entry=%s textstart=%s", + app.magic, app.nscns, app.opthdr, + app.optmagic and string.format("%04X", app.optmagic) or "none", + hex(app.entry or 0), hex(app.textstart or 0))) +for _, s in ipairs(app.sections) do + print(string.format("LOADER: sec %-8s paddr=%s size=%d bits (%d B) scnptr=%#x flags=%#x", + s.name, hex(s.paddr), s.size, math.floor(s.size / 8), s.scnptr, s.flags)) +end + +local TRACE = os.getenv("LOADER_TRACE") == "1" + +local frames = 0 +local injected = false +local done = false +local last_count = -1 + +local function status_name(v) + local t = { + [0] = "OK", + [1] = "bad descriptor", + [2] = "truncated COFF / section headers", + [3] = "bad COFF magic", + [4] = "section size not whole words", + [5] = "target outside RAM", + [6] = "target overlaps loader page or COFF buffer", + [7] = "relocation records present (unsupported)", + [8] = "target not in a free-list block", + [9] = "no entry point", + } + return t[v] or ("error " .. tostring(v)) +end + +local function result_dump() + local st = r32(RESULT + 0x000) + print(string.format( + "LOADER: result status=%d (%s) entry=%s text=%s/%d bits data=%s/%d bits bss=%s/%d bits relocs=%d pcb=%s nscns=%d len=%d", + st, status_name(st), hex(r32(RESULT + 0x020)), + hex(r32(RESULT + 0x040)), r32(RESULT + 0x060), + hex(r32(RESULT + 0x080)), r32(RESULT + 0x0A0), + hex(r32(RESULT + 0x0C0)), r32(RESULT + 0x0E0), + r32(RESULT + 0x100), hex(r32(RESULT + 0x120)), + r32(RESULT + 0x140), r32(RESULT + 0x160))) +end + +local function inject() + -- 1. loader itself + local n, bad = 0, 0 + poke_bytes(LOADER_BASE, loader) + n, bad = verify_bytes(LOADER_BASE, loader) + print(string.format("LOADER: loader poked %d words, readback %d bad at %s", + n, bad, hex(LOADER_BASE))) + + -- 2. COFF image + poke_bytes(BUF, coff) + n, bad = verify_bytes(BUF, coff) + print(string.format("LOADER: COFF poked %d words, readback %d bad at %s", + n, bad, hex(BUF))) + + -- 3. pre-fill every .bss section with A5A5, so a loaded zero is meaningful + for _, s in ipairs(app.sections) do + if s.flags & 0x80 ~= 0 then + for i = 0, math.floor(s.size / 16) - 1 do + prog:write_u16(s.paddr + i * 16, 0xA5A5) + end + print(string.format("LOADER: pre-filled .bss %s..%s with A5A5", + hex(s.paddr), hex(s.paddr + s.size))) + end + end + + -- 4. descriptor: magic "LD" "R1", buf, len, mode=0 (spin) + w32(DESC + 0x00, 0) + prog:write_u16(DESC + 0x00, 0x4C44) + prog:write_u16(DESC + 0x10, 0x5231) + w32(DESC + 0x20, BUF) + w32(DESC + 0x40, #coff) + w32(DESC + 0x60, MODE) + print(string.format("LOADER: descriptor @%s magic=%04X/%04X buf=%s len=%d mode=%d", + hex(DESC), prog:read_u16(DESC + 0x00), prog:read_u16(DESC + 0x10), + hex(r32(DESC + 0x20)), r32(DESC + 0x40), r32(DESC + 0x60))) + + -- 5. return stub. mode 0 ignores it (the loader jumps to ORIG_PC + -- itself); mode 1 returns here, and the stub resumes the interrupted + -- PC so the caller yields and the scheduler can run the child. + local orig_pc = cpu.state['PC'].value + if MODE == 1 then + prog:write_u16(RETURN_STUB, 0xC080) -- jauc + prog:write_u16(RETURN_STUB + 0x10, orig_pc & 0xFFFF) + prog:write_u16(RETURN_STUB + 0x20, math.floor(orig_pc / 0x10000) & 0xFFFF) + else + prog:write_u16(RETURN_STUB, 0xC0FF) -- jruc self + end + + -- 6. enter the loader. A8=0 selects the fixed descriptor; SP points at + -- the return slot so mode=1 would return to the spin stub. + -- Leave SP alone for mode 0: the loader pushes onto the current + -- process stack, then restores it and resumes at ORIG_PC. (mode 1 + -- would use the return stub.) + w32(0x01882A20, orig_pc) -- LD_ORIG_PC + print(string.format("LOADER: will resume at %s after create_process", hex(orig_pc))) + cpu.state['A8'].value = A8DESC and DESC or 0 + if MODE == 1 then + -- Simulate a CALLA from the current process: push the return + -- address on the *firmware* stack (a real transport would do this). + local sp = cpu.state['SP'].value - 0x20 + w32(sp, RETURN_STUB) + cpu.state['SP'].value = sp + end + cpu.state['PC'].value = LOADER_BASE + injected = true + print(string.format("LOADER: frame %d: entered loader at %s (caller SP=%s)", + frames, hex(LOADER_BASE), hex(cpu.state['SP'].value))) +end + +local function inspect() + result_dump() + + -- Verify the placement from RAM: .text/.data must equal the file bytes. + for _, s in ipairs(app.sections) do + if s.scnptr ~= 0 and s.size > 0 then + local nb = math.floor(s.size / 8) + local bytes = coff:sub(s.scnptr + 1, s.scnptr + nb) + local _, bad = verify_bytes(s.paddr, bytes) + print(string.format("LOADER: placed %s at %s: %d bytes, %d differing", + s.name, hex(s.paddr), nb, bad)) + end + end + + local base = COFF:match("([^/]+)$") or COFF + if base:find("marker") then + print(string.format("LOADER: marker app: start=%s data=%s data2=%s bss=%s count=%d", + hex(r32(0x018E0000)), hex(r32(0x018E0020)), hex(r32(0x018E0040)), + hex(r32(0x018E0060)), r32(0x018E0080))) + elseif base:find("xhello") then + print(string.format("LOADER: xhello: stage=%d strlen=%d display=%s root=%s window=%s sync=%s", + r32(0x01890140), r32(0x01890160), hex(r32(0x01890100)), + hex(r32(0x01890110)), hex(r32(0x01890120)), hex(r32(0x01890130)))) + end + if DL ~= 0 then + print(string.format("DIAG: loops=%d events=%d last=%d focus=%08X rev=%d quit=%d", r32(DL), r32(DE), r32(DT), DF ~= 0 and r32(DF) or 0, DR ~= 0 and r32(DR) or 0, DQ ~= 0 and r32(DQ) or 0)) + if DTICK ~= 0 then + print(string.format("DIAG: tick=%d", r32(DTICK))) + end + end + print(string.format("LOADER: PC at inspection = %s", hex(cpu.state['PC'].value))) +end + +emu.register_frame_done(function() + frames = frames + 1 + if frames == 2900 then tabx:set_value(700); taby:set_value(500); print("PLAYKEYS: move") end + if frames == 3000 then tabb:set_value(1); print("PLAYKEYS: click down") end + if frames == 3020 then tabb:set_value(0); print("PLAYKEYS: click up") end + if PLAY_MODE == "move" then + if frames == 3100 then key_a:set_value(1) end + if frames == 3110 then key_a:set_value(0) end + if frames == 3120 then key_d:set_value(1) end + if frames == 3130 then key_d:set_value(0) end + if frames == 3140 then key_s:set_value(1) end + if frames == 3150 then key_s:set_value(0) end + if frames == 3160 then key_w:set_value(1) end + if frames == 3170 then key_w:set_value(0) end + if frames == 3180 then key_0:set_value(1) end + if frames == 3190 then key_0:set_value(0) end + elseif PLAY_MODE == "panels" then + if frames == 3100 then key_h:set_value(1) end + if frames == 3110 then key_h:set_value(0) end + if frames == 3120 then key_r:set_value(1) end + if frames == 3130 then key_r:set_value(0) end + elseif PLAY_MODE == "quit" then + if frames == 3095 then print("PLAYKEYS: quit arm") end + if frames == 3100 then print("PLAYKEYS: Q down"); key_q:set_value(1) end + if frames == 3110 then print("PLAYKEYS: Q up"); key_q:set_value(0) end + end + if frames == AT and not injected then + inject() + if TRACE then dbg:command("trace /tmp/opencode/ld1.tr") end + end + if injected and TRACE and frames == AT + 1 then + dbg:command("trace off") + print("LOADER: trace written to /tmp/opencode/ld.tr") + end + if injected and not done then + if TRACE and frames <= AT + 40 then + print(string.format("LOADER: trace f=%d PC=%s SP=%s A14=%s status=%s entry=%s", + frames, hex(cpu.state['PC'].value), hex(cpu.state['SP'].value), + hex(cpu.state['A14'].value), hex(r32(RESULT + 0x000)), + hex(r32(RESULT + 0x020)))) + end + local st = r32(RESULT + 0x000) + -- status 0 means OK; nonzero is final too once parsing has begun + if st ~= 0xFFFFFFFF and not done then + -- The loader writes 0 on success. A nonzero status is final + -- only after it has been written once (it is 0 from power-on, + -- so require the loader to have run: PC no longer at entry or + -- the app counter moved). + end + if VERBOSE and frames % 25 == 0 then + result_dump() + end + if base_contains_marker then + local c = r32(0x018E0080) + if c ~= last_count then + if last_count ~= -1 then + print(string.format("LOADER: marker counter %d -> %d (child running)", + last_count, c)) + end + last_count = c + end + end + end + if frames >= ENDF then + if injected then + done = true + inspect() + if SNAP then + local screen = nil + for _, s in pairs(machine.screens) do screen = s break end + if screen then + local out = OUT .. "/loader_frame" .. frames .. ".png" + local ok, err = pcall(function() return screen:snapshot(out) end) + print(string.format("LOADER: snapshot %s ok=%s %s", + out, tostring(ok), tostring(err or ""))) + end + end + end + machine:exit() + end +end, "vt1200-loader") + +-- (Lua has no forward use of a name defined later; set a small flag here.) +base_contains_marker = (COFF:match("([^/]+)$") or ""):find("marker") ~= nil + +dbg.visible_cpu = cpu +dbg.execution_state = 'run' diff --git a/apps/termtris/tests/play_keys.sh b/apps/termtris/tests/play_keys.sh new file mode 100755 index 0000000..4d396b9 --- /dev/null +++ b/apps/termtris/tests/play_keys.sh @@ -0,0 +1,35 @@ +#!/bin/sh +# play_keys.sh -- run the real game through move, panels and quit paths. +set -eu +DIR=$(cd "$(dirname "$0")" && pwd) +APP=$(cd "$DIR/.." && pwd) +ROOT=$(cd "$APP/../.." && pwd) +M=${MAME:-/home/arti/code/k-space/mame0289/vt1200} +EV="$APP/evidence" +mkdir -p "$EV" "$APP/build" +for mode in ${MODES:-move panels quit}; do + LOG="$APP/build/play_$mode.log" + rm -f "$EV/loader_frame3200.png" + DL=$(awk '{for(i=2;i<=NF;i++) if($i=="_dbg_loops") print $(i-1)}' "$APP/build/termtris.map" | head -1) + DE=$(awk '{for(i=2;i<=NF;i++) if($i=="_dbg_events") print $(i-1)}' "$APP/build/termtris.map" | head -1) + DT=$(awk '{for(i=2;i<=NF;i++) if($i=="_dbg_last") print $(i-1)}' "$APP/build/termtris.map" | head -1) + DF=$(awk '{for(i=2;i<=NF;i++) if($i=="_dbg_focus") print $(i-1)}' "$APP/build/termtris.map" | head -1) + DR=$(awk '{for(i=2;i<=NF;i++) if($i=="_dbg_rev") print $(i-1)}' "$APP/build/termtris.map" | head -1) + DQ=$(awk '{for(i=2;i<=NF;i++) if($i=="_quit") print $(i-1)}' "$APP/build/termtris.map" | head -1) + DTICK=$(awk '{for(i=2;i<=NF;i++) if($i=="_tick_interval") print $(i-1)}' "$APP/build/termtris.map" | head -1) + VT1200_SKIP_LEVEL1=1 LOADER_SNAP=1 PLAY_MODE=$mode DIAG_LOOPS="0x$DL" DIAG_EVENTS="0x$DE" DIAG_LAST="0x$DT" DIAG_FOCUS="0x$DF" DIAG_REV="0x$DR" DIAG_QUIT="0x$DQ" \ + DIAG_TICK="0x$DTICK" \ + LOADER_AT=2500 LOADER_END=3200 \ + LOADER_BIN="$ROOT/dwt/loader/build/loader.bin" \ + LOADER_COFF="$APP/build/termtris.coff" \ + LOADER_OUT="$EV" \ + "$M" vt1200 -rompath "$ROOT/harness/mame/roms" \ + -video none -sound none -nothrottle -skip_gameinfo \ + -debug -debugger none \ + -autoboot_script "$DIR/play_keys.lua" -autoboot_delay 0 \ + >"$LOG" 2>&1 || true + grep -E 'PLAYKEYS:|LOADER: result|LOADER: snapshot' "$LOG" | tail -5 + [ -s "$EV/loader_frame3200.png" ] || { echo "play_keys.sh: no $mode snapshot" >&2; exit 1; } + cp "$EV/loader_frame3200.png" "$EV/termtris_${mode}_3200.png" +done +echo "play_keys: $EV/termtris_move_3200.png $EV/termtris_panels_3200.png $EV/termtris_quit_3200.png" -- 2.47.3 From 3e2dddeb6ae5744de7ed5dd97976cd9125a187b7 Mon Sep 17 00:00:00 2001 From: "opencode.energia.arti.ee" Date: Tue, 3 Nov 2026 20:07:35 +0000 Subject: [PATCH 08/18] docs(termtris): port report, run wrapper and harness write-up --- apps/termtris/README.md | 6 + apps/termtris/VT1200.md | 145 ++++++++++++++++++ apps/termtris/build/termtris.coff | Bin 15157 -> 15157 bytes apps/termtris/build/termtris.map | 2 +- apps/termtris/evidence/termtris_frame3300.png | Bin 20986 -> 19347 bytes apps/termtris/run.sh | 39 +++++ apps/termtris/tests/input.c | 7 + harness/mame/README.md | 1 + harness/mame/TERMTRIS_REPORT.md | 122 +++++++++++++++ 9 files changed, 321 insertions(+), 1 deletion(-) create mode 100644 apps/termtris/VT1200.md create mode 100755 apps/termtris/run.sh create mode 100644 harness/mame/TERMTRIS_REPORT.md diff --git a/apps/termtris/README.md b/apps/termtris/README.md index f45463e..72a1382 100644 --- a/apps/termtris/README.md +++ b/apps/termtris/README.md @@ -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. + ![shots](http://nuclear.mutantstargoat.com/sw/termtris/img/termtris-banner.png) About diff --git a/apps/termtris/VT1200.md b/apps/termtris/VT1200.md new file mode 100644 index 0000000..0add46b --- /dev/null +++ b/apps/termtris/VT1200.md @@ -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. + +![termtris board](evidence/termtris_frame3300.png) + +## 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/

.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. diff --git a/apps/termtris/build/termtris.coff b/apps/termtris/build/termtris.coff index 5836bfc488318373bb6ab8263d460e90a57fc24e..5046cde4e5cb1b04a47d6c59684af759117219af 100644 GIT binary patch delta 15 Wcmdm5wzZ6H0s{*}^ZJc!`qlt1#RWtF delta 15 Wcmdm5wzZ6H0s{*J`?`&6`qls}*aXJ_ diff --git a/apps/termtris/build/termtris.map b/apps/termtris/build/termtris.map index af6ca6a..3ecbbd3 100644 --- a/apps/termtris/build/termtris.map +++ b/apps/termtris/build/termtris.map @@ -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: ENTRY POINT SYMBOL: "_start" address: 018a0000 diff --git a/apps/termtris/evidence/termtris_frame3300.png b/apps/termtris/evidence/termtris_frame3300.png index 92dafb50bba599c627d5b9dc22397f265167f427..47128cb0eb6bce2d62a79645d42ae1b074ac3e3a 100644 GIT binary patch literal 19347 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

V=wgul?*b2O_` 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 + 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 diff --git a/apps/termtris/tests/input.c b/apps/termtris/tests/input.c index 8735c32..f23231a 100644 --- a/apps/termtris/tests/input.c +++ b/apps/termtris/tests/input.c @@ -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 */ diff --git a/harness/mame/README.md b/harness/mame/README.md index c4b24c7..f1deecb 100644 --- a/harness/mame/README.md +++ b/harness/mame/README.md @@ -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) diff --git a/harness/mame/TERMTRIS_REPORT.md b/harness/mame/TERMTRIS_REPORT.md new file mode 100644 index 0000000..5d3f80a --- /dev/null +++ b/harness/mame/TERMTRIS_REPORT.md @@ -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. -- 2.47.3 From 64bd781aa698b8d8505cb58dd73ea131163bfa0e Mon Sep 17 00:00:00 2001 From: "opencode.energia.arti.ee" Date: Tue, 3 Nov 2026 20:07:41 +0000 Subject: [PATCH 09/18] chore(termtris): stop tracking per-test build outputs --- .gitignore | 6 ++ apps/termtris/build/fmt.coff | Bin 2050 -> 0 bytes apps/termtris/build/fmt.map | 59 -------------------- apps/termtris/build/input.coff | Bin 4399 -> 0 bytes apps/termtris/build/input.map | 95 -------------------------------- apps/termtris/build/render.coff | Bin 3922 -> 0 bytes apps/termtris/build/render.map | 85 ---------------------------- apps/termtris/build/smoke.coff | Bin 1337 -> 0 bytes apps/termtris/build/smoke.map | 56 ------------------- 9 files changed, 6 insertions(+), 295 deletions(-) delete mode 100644 apps/termtris/build/fmt.coff delete mode 100644 apps/termtris/build/fmt.map delete mode 100644 apps/termtris/build/input.coff delete mode 100644 apps/termtris/build/input.map delete mode 100644 apps/termtris/build/render.coff delete mode 100644 apps/termtris/build/render.map delete mode 100644 apps/termtris/build/smoke.coff delete mode 100644 apps/termtris/build/smoke.map diff --git a/.gitignore b/.gitignore index 138174f..7bc69e1 100644 --- a/.gitignore +++ b/.gitignore @@ -73,3 +73,9 @@ tools/tms34010-sdk/1991-07 TIGA Promo Kit and Art Software/ # termtris build scratch (TI tools + 2ine libs) apps/termtris/work/ apps/termtris/build/*.log +apps/termtris/build/*.log +apps/termtris/build/smoke.* +apps/termtris/build/fmt.* +apps/termtris/build/render.* +apps/termtris/build/input.* +apps/termtris/evidence/loader_frame*.png diff --git a/apps/termtris/build/fmt.coff b/apps/termtris/build/fmt.coff deleted file mode 100644 index b00469fdfcd7303aaeff410ecc2abd028ed8f9bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2050 zcmbtVUuaup6hAkAmVUikjnKkX#0A@s{&AsG9~6sZ(m+G+H~XL#Bx}=R!niTrr zL+E6!p=7b4%)x|U7?}-N@g;Bbf@{KZS5o#QJ{4gstu!Dw&!u?KJN46}L;Tyv@C&&I&JhpG?^tmY^Da?@TWJ)+BkjM(voJ!spjb>^j zVM_`<8Fyy-+`c`D`o~gzvqrwtv@U8B6HwnYY-KWxQjPr7w1UuXoztw)#Ti{1kEE|T zPaCpERy2K$tT*joYB8bzrasZscX@Y6qI-roFOEh6;CzKoujAyOMIqpJIAabciYwr} z@M;X4PJo@<9_{khiyk<>F^k!My0u4pLW~U%7ag)+ z8OUGqJ~u~mQL{J-S2hYC{H_*?SvKMo$A#TRZ@xIgUQ=_$1e4T7YAKzSLe}ILlVfJt zQYn)Q5V9Vn`{|pNkaeinMLF9MUYCCQhk8)~#O!LA?R1b@OAp8>;`!J8^rch2kQC1P zqxQuDvXWCUbI5a63RBMG8=Ih*udqYDc}vT92Q#+we6#dG{DIl79i~DmD0d-``5%3G z{Jp|HRG_z-i)K=rsm$fgo`(RJ@iur!Qs|}}WqX#RCJ|-3cD!m)ACsvUlfv<8HOEgZ zjq5X1mLK5WYMe;DBr3k;i%IYxx(k%35AIUzM3P4m|>PSPe~@IBhnzPiMQCT z%3v>4-ea{ss07&ruHRw1S{B>sM=4O>X1B+;H*VvZOwc>ycSh>P1fJIuJg>p%awIYY zaGku1uepGT`-EYpcqh01QU~#5vw&}4coyG#dsV_R0%ks)80{5=7ot-!ZPm#9ZW p&bzG66KwQd-qUo=$=pp4Z0nKyh9l33`;3@q04=<)>pO3PzX8eb7xDlA diff --git a/apps/termtris/build/fmt.map b/apps/termtris/build/fmt.map deleted file mode 100644 index 9f887e2..0000000 --- a/apps/termtris/build/fmt.map +++ /dev/null @@ -1,59 +0,0 @@ -***************************************************** -TMS340 COFF Linker Version 5.01 -***************************************************** -Tue Nov 03 18:50:58 2026 - -OUTPUT FILE NAME: -ENTRY POINT SYMBOL: "_start" address: 018a0000 - - -MEMORY CONFIGURATION - - name origin length attributes - -------- -------- --------- ---------- - prog 018a0000 000020000 RWIX - ram 01a00000 000040000 RWIX - - -SECTION ALLOCATION MAP - - output attributes/ -section page origin length input sections --------- ---- ---------- ---------- ---------------- -.text 0 018a0000 00001fa0 - 018a0000 00000150 ENTRY.obj (.text) - 018a0150 00000780 FMT.obj (.text) - 018a08d0 000015c0 COMPAT.obj (.text) - 018a1e90 00000110 rts.lib (.text) - -.cinit 0 018a1fa0 00000010 - 018a1fa0 00000010 CINITZ.obj (.cinit) - -.bss 0 01a00000 00008320 UNINITIALIZED - 01a00000 00008000 ENTRY.obj (.bss) - 01a08000 00000300 FMT.obj (.bss) - 01a08300 00000020 COMPAT.obj (.bss) - -.data 0 018a0000 00000000 UNINITIALIZED - - -GLOBAL SYMBOLS - -address name address name --------- ---- -------- ---- -01a00000 .bss 018a0000 .text -018a0000 .data 018a0000 .data -018a0000 .text 018a0000 edata -018a08d0 _fflush 018a0000 _start -018a0150 _main 018a0150 _main -01a08000 _out 018a08d0 _fflush -018a10d0 _sprintf 018a10d0 _sprintf -018a0000 _start 018a1e90 _strlen -01a08300 _stdout 018a1fa0 cinit -018a1e90 _strlen 018a1fa0 etext -018a1fa0 cinit 01a00000 .bss -018a0000 edata 01a08000 _out -01a08320 end 01a08300 _stdout -018a1fa0 etext 01a08320 end - -[14 symbols] diff --git a/apps/termtris/build/input.coff b/apps/termtris/build/input.coff deleted file mode 100644 index fc02e28b1e911bf1ce0cd2767f89753c76747da8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4399 zcmbVPZ%iBK8Gp}kJuWvAwJ<6Yw%Zg+YAd7Cs%YIO`hfvuu{v$(Hfiha_q^w` zeOdA$vvlY8{XPGm_j#Z9y)&$qshQ7S{;tf}A;#Etwo|ARZVU7KFu_mzRKPopm+`bG z!p9SwV$6x}BR;RZoVAR#_eEk6FwXBwLsweJfczY6RR27fv>zTDGq4P7WCX)rH5X$^ z5q$l3?Y+>53YxpLe8%kUTQI zAs-C9&@&-q+$}Z|QXlE;ZXU(A6!@TwT_)?}wO6b=f~VSuhdW z7eDAxZtFE4^lTHfR>#tG`xIi@^UKbX*E-Kz0!`A$(4G-t9Z+$esrhKUH1k!=$89dMD4{1x&%C;CRglf`47&CWy_I=C_ zlxN?;Y^c%X3}9A}-(u#RX5R9w>n_x;GIjS|sQb#)op+(`EmIq`i@A*cATso8{ef#e z%8XvKDBA}`S%l|mx9Ap4kBYe@t#KQz)IrqJW+lYdWzU(6KpEY9lkyj=|9ty}PPf{M zHUE#kDc_^~O@9pUFLXEFxAmtBk87*mTPK&Mi<4KTq;18?tSZ@ylVqdg!ud``wJo;# z1a1BGrRms|OC1y?5uATuELg9}X<<8JS(HPpIBDd!L324$QWCYXMa%MkDNh{nG6&TT z(TmE@^*K4&7Lp;^LA8#0gVy-hQQN95-{RP<1?vMsT4=G`3f&GjX2gdz^2N&0&42Mp zCw2uqsZ;Cn!TRJwv_ELBsQ)Z1O(P01Eo??K0b5#-RioDCCE_A&f7FmcUYmR$p{j;* z$=bo3$FhS0;LdcBjUb{~lj9hZ&Uo{M&R2Pz%Idd4AU9N>(*Ibh183X!wG2_oU=Ji zAD0+Bv{Y(|hp0oUbnSq$steaP9&S}@7i4Y7*Q!$0&*`<-K9pZ%=nuXLZg-sfP_2pj zTDSP#RZYLhIljepJ-IN;IiKKKb}fX}tbd^?)wJSi?#lXSng%eM^|vY%brzj3Ag(8U z>Zxz8Zd(b=Ub3_OFRfp4FqdQfl9SDFP0jXG{ND$!l81KM%ua`XBe3L4PYBF@5DlUsou!t&tee(CHA);C9>+`j9 z;Ci7~Pg@*@J{t?6<`Io{QggWk`)cc$c&xN7{k^w z3_W0ByDwX)03&vf-J%2&(-xgZhON-o7;CYx-H$CgAYC{?Brg%^m^!t)Z4kOc_>(b`STLFDilYRY5rCkh@q1Mr91 zpx+NMq|>5;jAKY=Lj`#gLvyFt%DsiYehr>*sf*ILrGebJcWf+N^{q-(3_W-BaPY+X zxlh`s=DN>79nU^u=}!!@Abmgw2YQFYC3r9#85m434uKdyg7NWPyMleuaPR0?-zcLz z5I{@Fr8yEGiAICrqv06X$D-kIoQjlK2YX}v(XdH6)(dO>k+FER_e5FK528oL;7Lg@ z|DvbK=*S4qc>l<;GBG|nGB6s($Gl-4!!Qh%B>P9P=M76HL*hi-3=EuoM@Gj+5CM0+ z5)cosM51v?E9WAqk!V@SaL$mgRu(xt+&9>3sz%br5}1&(iUM<1WvMKN2qG~wF(mQ$ G0Q)buthGA; diff --git a/apps/termtris/build/input.map b/apps/termtris/build/input.map deleted file mode 100644 index 2ee2198..0000000 --- a/apps/termtris/build/input.map +++ /dev/null @@ -1,95 +0,0 @@ -***************************************************** -TMS340 COFF Linker Version 5.01 -***************************************************** -Tue Nov 03 19:02:37 2026 - -OUTPUT FILE NAME: -ENTRY POINT SYMBOL: "_start" address: 018a0000 - - -MEMORY CONFIGURATION - - name origin length attributes - -------- -------- --------- ---------- - prog 018a0000 000020000 RWIX - ram 01a00000 000040000 RWIX - - -SECTION ALLOCATION MAP - - output attributes/ -section page origin length input sections --------- ---- ---------- ---------- ---------------- -.text 0 018a0000 000043b0 - 018a0000 00000150 ENTRY.obj (.text) - 018a0150 000003e0 INPUT.obj (.text) - 018a0530 00001760 MAIN.obj (.text) - 018a1c90 00002510 X11.obj (.text) - 018a41a0 00000210 XGFIX.obj (.text) - -.cinit 0 018a43b0 000000a0 - 018a43b0 00000050 MAIN.obj (.cinit) - 018a4400 00000050 X11.obj (.cinit) - -.bss 0 01a00000 0001e1c0 UNINITIALIZED - 01a00000 00008000 ENTRY.obj (.bss) - 01a08000 00001040 INPUT.obj (.bss) - 01a09040 00000360 MAIN.obj (.bss) - 01a093a0 00012e20 X11.obj (.bss) - 01a1c1c0 00002000 XGFIX.obj (.bss) - -.data 0 018a0000 00000000 UNINITIALIZED - - -GLOBAL SYMBOLS - -address name address name --------- ---- -------- ---- -01a00000 .bss 018a0000 .data -018a0000 .data 018a0000 edata -018a0000 .text 018a0000 _start -018a0360 _game_input 018a0000 .text -018a0150 _init_game 018a0150 _init_game -018a18b0 _main 018a0250 _update -01a08040 _mark 018a0360 _game_input -018a0530 _now_ms 018a0530 _now_ms -018a07b0 _sleep_ms 018a07b0 _sleep_ms -018a0000 _start 018a0910 _vt_poll_event -01a1c0e0 _term_clearscr 018a1280 _vt_handle_event -01a1c120 _term_cursor 018a1870 _wait_display -01a08020 _term_height 018a18b0 _main -01a1c160 _term_ibmchar 018a1c90 _vt_root_window -018a4130 _term_init 018a1d70 _vt_x11_init -018a2ec0 _term_putstr 018a2420 _x11_clearscr -01a1c0c0 _term_reset 018a2610 _x11_reset -01a1c140 _term_setcolor 018a2650 _x11_cursor -01a1c100 _term_setcursor 018a26b0 _x11_setcolor -01a1c1a0 _term_type 018a27d0 _x11_setcursor -01a08000 _term_width 018a2a20 _x11_ibmchar -01a1c180 _termenv 018a2ec0 _term_putstr -018a0250 _update 018a3060 _vt_x11_flush -01a1c020 _vt_dpy 018a3e10 _vt_x11_term_init -01a1c060 _vt_gc 018a4130 _term_init -018a1280 _vt_handle_event 018a41a0 _vt_install_xg -018a41a0 _vt_install_xg 018a43b0 cinit -018a0910 _vt_poll_event 018a43b0 etext -018a1c90 _vt_root_window 01a00000 .bss -01a1c040 _vt_win 01a08000 _term_width -01a1c0a0 _vt_x11_drain_events 01a08020 _term_height -018a3060 _vt_x11_flush 01a08040 _mark -018a1d70 _vt_x11_init 01a1c020 _vt_dpy -01a1c080 _vt_x11_progress 01a1c040 _vt_win -018a3e10 _vt_x11_term_init 01a1c060 _vt_gc -018a1870 _wait_display 01a1c080 _vt_x11_progress -018a2420 _x11_clearscr 01a1c0a0 _vt_x11_drain_events -018a2650 _x11_cursor 01a1c0c0 _term_reset -018a2a20 _x11_ibmchar 01a1c0e0 _term_clearscr -018a2610 _x11_reset 01a1c100 _term_setcursor -018a26b0 _x11_setcolor 01a1c120 _term_cursor -018a27d0 _x11_setcursor 01a1c140 _term_setcolor -018a43b0 cinit 01a1c160 _term_ibmchar -018a0000 edata 01a1c180 _termenv -01a1e1c0 end 01a1c1a0 _term_type -018a43b0 etext 01a1e1c0 end - -[46 symbols] diff --git a/apps/termtris/build/render.coff b/apps/termtris/build/render.coff deleted file mode 100644 index 20171e734748afebaf5b0de6bc4b34170d983d17..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3922 zcmbVPZ){sv6~E7k_wxGcU`9W<4I75VxK1Hz3xlP~ush;5fBagLX5+79r-@12q)ueJ zZ6<-_2BHuL(cv(y-S}WIRur{$s+Pzf_k?1#b=rBTC{imB{t~63)S4zbfq*@J=RG^m zPFKNr(tAGl{6FWObMEzPte%~^^p_Ji8W`(ljD4Ct$2D+|II9y2Jh5MJ_{QPit1}K=aV`n+Z(*bQabeMVJQC4xN!Unon!Rc+_+SLz zV1KkO<)M{}UcMxdp4-xfKz6;xJ+Ig2Ti$57(9`}8*~MohpJ}`<&iRFNLLaym zvPq@WcyTGF6jGTxvXZ$|RD7o5vvJOTPRtilp}AqB{dK#sl<=AMewMJ0R!Z3LLQ^3{dXCIhG--UbnsUNX$ZqAS=xTwkKPugpmv+rsTss|>Zt>sB zwtTnvSB1~_i2qdT@qJ(6@(wroRMBEX9&W*3gM3S9Wwg2p4>@_mQb^gQP9x=YTGF$- z#~d^LUTHB;nr>k~w0Ij=U6V;}3Q^iD6seb^T$B>`h4W9xxyCu7q}3egl=n6%xlM}A zGR6CmcWAK3xi#U~%br71d<=U>mIl?FS`S=v{#{-a($D&ZK6kIw*V}cB?cKNkOJ9Ee zD_?z~jU9aH>mBYxhdaBvdpy#Squy`y9_xGgm2ZCQ+uvdAoUnu4mM&(C%1Aa=Kb&>3 zQr3?;VY{hLys&G2kt)N7s=6-}+sSv-qV1Q?pQtq=NWGwpxI=SxODS;}?{(n?u9R32 zid<;!I_jZ>oh_tVA@%O3NSdJ6n*IQJsw_d;5BYWB6YO)~Y@MrAmiLEuo!b7F5{;|G zS)s7;3y`H+Y>;+9wwCHYx(Ydx(JJ)H2X~$KJqG!PpeU&RZPY;%XRVfQ_lDrd4sExr z2r=GkoH^Td+02AX7ABYpk62IIdt}EQ~gIx}(J}1Z{Zin>La+kQM)GyyeHFL7V zJ*(QCQGKYV*`#?-u6eq7%@sJ9ZeH*>M2fM~IKNa> zyvB`Kr!l=GE5^?l@_Q^CEC&&@&9Z=~)2beu8MOuP-Owg3SF(^+EfS5gQOe4QEU87N z&Z43^i)7`0n>{~Rye&0~uDbP(hgW0FZkgs?*vlVxtf~8Pd6I8>h-vfd>27zAhvrGA zacIfHxO|I6G~aA@ODz9qHtpd>hTS1ayM^==Qt6HicAe^?viJTs?>ZND&U_EFotZ=q z6`4VWRr9eb?)UCraDMTcm3>}1^vSej6s>iaulgD5xFl^CldQ*+hc8}ZaH=jZ*aZhC z3zlKy7k)09PrNvfj^Te=NKvOh?m;1ygCx2gI+OT)C-Ec?7`x|O@bq{(jiXCu=F7K8 zE{XTJn(fU+S2uQKdu~eFmKV*k*+Q`Di^9iCFgX{Ia%>>m3hJXQFwbeSR9d zx9#C%vpJ1EvhBEx{bnmqDQWZ!uXc11b%wLT{FZYKeXFo3b*D?>+y7b=9(|2^*vnS% z&hiZPEjG>aR{Y}Zd?wDJYp|>6Zk$Lnd{Tw!BtlXM=xf)f%!p!JgdH!5TZ`S5=DQi?-016d^AtheWdP5JXLoa!;^Tb?n#({z)oem<5hoQYcOehRPVslL}~v3o(UWvlo@r-#lIu&DSmj=P@j4_Vcpia&JzYW|qPx`pRs_(RoLt_`bhMT5$v zq27DR9|QA6qWxj@%B)!j=45{`1RC8#$qOxelm+TR(yU?3fT$Za45Y#%#8ciK=s6wr z4Gn+?9jHq^0~UV`Wk6U@DcM#Wf9#t47axU}80x-vC3sU5-5(2%ijuPP;ZSAhSiha*fp6z$9xh z?*QY+Y|{O?2MkR&9kT`utI_;7AdbCty;d zYqHv2wE~hF8zoOvA_2La BHz)uA diff --git a/apps/termtris/build/render.map b/apps/termtris/build/render.map deleted file mode 100644 index 50449f4..0000000 --- a/apps/termtris/build/render.map +++ /dev/null @@ -1,85 +0,0 @@ -***************************************************** -TMS340 COFF Linker Version 5.01 -***************************************************** -Tue Nov 03 19:54:29 2026 - -OUTPUT FILE NAME: -ENTRY POINT SYMBOL: "_start" address: 018a0000 - - -MEMORY CONFIGURATION - - name origin length attributes - -------- -------- --------- ---------- - prog 018a0000 000020000 RWIX - ram 01a00000 000040000 RWIX - - -SECTION ALLOCATION MAP - - output attributes/ -section page origin length input sections --------- ---- ---------- ---------- ---------------- -.text 0 018a0000 00003f20 - 018a0000 00000150 ENTRY.obj (.text) - 018a0150 00001280 RENDER.obj (.text) - 018a13d0 00002940 X11.obj (.text) - 018a3d10 00000210 XGFIX.obj (.text) - -.cinit 0 018a3f20 000000a0 - 018a3f20 00000050 RENDER.obj (.cinit) - 018a3f70 00000050 X11.obj (.cinit) - -.bss 0 01a00000 00019480 UNINITIALIZED - 01a00000 00008000 ENTRY.obj (.bss) - 01a08000 00000260 RENDER.obj (.bss) - 01a08260 0000f220 X11.obj (.bss) - 01a17480 00002000 XGFIX.obj (.bss) - -.data 0 018a0000 00000000 UNINITIALIZED - - -GLOBAL SYMBOLS - -address name address name --------- ---- -------- ---- -01a00000 .bss 018a0000 .text -018a0000 .data 018a0000 .data -018a0000 .text 018a0000 edata -018a0370 _main 018a0000 _start -01a08000 _mark 018a0370 _main -018a0000 _start 018a13d0 _vt_root_window -01a173a0 _term_clearscr 018a14b0 _vt_x11_init -01a173e0 _term_cursor 018a1b60 _x11_clearscr -01a08220 _term_height 018a1d20 _x11_reset -01a17420 _term_ibmchar 018a1d60 _x11_cursor -018a3ca0 _term_init 018a1dc0 _x11_setcolor -018a25b0 _term_putstr 018a1ee0 _x11_setcursor -01a17380 _term_reset 018a2130 _x11_ibmchar -01a17400 _term_setcolor 018a25b0 _term_putstr -01a173c0 _term_setcursor 018a2750 _vt_x11_flush -01a17460 _term_type 018a3980 _vt_x11_term_init -01a08200 _term_width 018a3ca0 _term_init -01a17440 _termenv 018a3d10 _vt_install_xg -01a172e0 _vt_dpy 018a3f20 etext -01a17320 _vt_gc 018a3f20 cinit -018a3d10 _vt_install_xg 01a00000 .bss -018a13d0 _vt_root_window 01a08000 _mark -01a17300 _vt_win 01a08200 _term_width -01a17360 _vt_x11_drain_events 01a08220 _term_height -018a2750 _vt_x11_flush 01a172e0 _vt_dpy -018a14b0 _vt_x11_init 01a17300 _vt_win -01a17340 _vt_x11_progress 01a17320 _vt_gc -018a3980 _vt_x11_term_init 01a17340 _vt_x11_progress -018a1b60 _x11_clearscr 01a17360 _vt_x11_drain_events -018a1d60 _x11_cursor 01a17380 _term_reset -018a2130 _x11_ibmchar 01a173a0 _term_clearscr -018a1d20 _x11_reset 01a173c0 _term_setcursor -018a1dc0 _x11_setcolor 01a173e0 _term_cursor -018a1ee0 _x11_setcursor 01a17400 _term_setcolor -018a3f20 cinit 01a17420 _term_ibmchar -018a0000 edata 01a17440 _termenv -01a19480 end 01a17460 _term_type -018a3f20 etext 01a19480 end - -[38 symbols] diff --git a/apps/termtris/build/smoke.coff b/apps/termtris/build/smoke.coff deleted file mode 100644 index f03990d2e3d56267cbc59eada6ea8f948f773802..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1337 zcmbVML1+^}6#cWwl1$gsfF2_4Ay{i`gwUfFF^eQvsY3&L@w7>8tkD{ziKU1_NDF~@ zXxSp7R1Bo02)dOP>A}k)h*BvL5uvvpEVWUC2nt?W|LkVTw$(!&nBo6FfByUb&zl(s zC$v1QT)8O#90q`9Xd!O$lgJ)a_?Y2Kfmkz`NepMWsb=GT?+m@Z_?-a3i^)_H9c51e zV-<)Ow}%nKK4esaz3H@RDZL)_X793PyE(pm&O!NloUvej?r6Bmsq0|g9?Rgk*v|~9(JyhQ#>6~fw&ngjzGEe znF4B!6N=}0>tsk`{j4(Qhklk`ESJ{8RkhWpsS%v7=rN+HujvY@q8tTH)zrj7)~UUh zA%&;Mn?8gn3(U)K9#5{)y>JkHf)ji%dV4ukE}3$y=OfC~VNK;b&wee3&ayq2#T?C(-%@+LOh_U z(@gYoOssJ70cLY(UWYDmX7tnC3QWDg=8;tzy%6c1n)4HPMAR?)9D0|J=j#kBvc&fBD$h$Vuoh__!5pXFgHPh)1lC|I8;_k6=m`7H6Va z`wDi1S6Cmr!b1MmYIoHmn39FXS#4r@u?1V3y(=ud#~PM@J65gD#;fOj7C=d7;)BMA zVWOkuT0EJ;J*#%ZGI*7Ts#SrO#8B^zOK3r^b1WXq?2lE2u!BDWBx-*XfA_pT^s)UK lyu0YL!(#t|OsqtzkHg{LXQA&yMH$K{$rP3|FrW -ENTRY POINT SYMBOL: "_start" address: 018a0000 - - -MEMORY CONFIGURATION - - name origin length attributes - -------- -------- --------- ---------- - prog 018a0000 000020000 RWIX - ram 01a00000 000040000 RWIX - - -SECTION ALLOCATION MAP - - output attributes/ -section page origin length input sections --------- ---- ---------- ---------- ---------------- -.text 0 018a0000 00000d40 - 018a0000 00000150 ENTRY.obj (.text) - 018a0150 000009e0 SMOKE.obj (.text) - 018a0b30 00000210 XGFIX.obj (.text) - -.cinit 0 018a0d40 00000010 - 018a0d40 00000010 CINITZ.obj (.cinit) - -.bss 0 01a00000 0000a700 UNINITIALIZED - 01a00000 00008000 ENTRY.obj (.bss) - 01a08000 00000700 SMOKE.obj (.bss) - 01a08700 00002000 XGFIX.obj (.bss) - -.data 0 018a0000 00000000 UNINITIALIZED - - -GLOBAL SYMBOLS - -address name address name --------- ---- -------- ---- -01a00000 .bss 018a0000 .text -018a0000 .data 018a0000 .data -018a0000 .text 018a0000 edata -01a08400 _evbuf 018a0000 _start -018a0150 _main 018a0150 _main -01a08000 _mark 018a0b30 _vt_install_xg -018a0000 _start 018a0d40 cinit -018a0b30 _vt_install_xg 018a0d40 etext -018a0d40 cinit 01a00000 .bss -018a0000 edata 01a08000 _mark -01a0a700 end 01a08400 _evbuf -018a0d40 etext 01a0a700 end - -[12 symbols] -- 2.47.3 From eb896f7b8c23256bbb944d04311df4d2c7168bdf Mon Sep 17 00:00:00 2001 From: "opencode.energia.arti.ee" Date: Tue, 3 Nov 2026 20:11:57 +0000 Subject: [PATCH 10/18] test(termtris): assert the play acceptance results; drop the dead drain hook --- apps/termtris/build/termtris.coff | Bin 15157 -> 15118 bytes apps/termtris/build/termtris.map | 63 +++++++++++++++--------------- apps/termtris/src/vt1200/x11.c | 9 +++-- apps/termtris/src/vt1200/x11.h | 1 - apps/termtris/tests/play_keys.sh | 11 ++++++ apps/termtris/tests/render.c | 1 - 6 files changed, 47 insertions(+), 38 deletions(-) diff --git a/apps/termtris/build/termtris.coff b/apps/termtris/build/termtris.coff index 5046cde4e5cb1b04a47d6c59684af759117219af..6c515d9717a183721e60d921df7d5405f808769b 100644 GIT binary patch delta 829 zcmY*XUr1A76#u@v>2~+WK&`o};k6+;HrnReT1jvX7YZ2^fe#U`XoM}5Z0(^S>>((m zr5>UOUkr&61FbINLkNSQLWu=MLQvi}S(;#f-ime3T~oUc=lh-Cckb{0&N+83l}PZxJ&lZIRCW^lInEM8+hm z?G*{mbC9VFR?Ar85woTome8p!!g}Q}UiO)u@SG}|R|U>s(=usgx>$a?>SGQbTJv0i zi?+RZciR@lxMb_VXt$#Ywbvf!m@HJC!=!Onv;yVgwI?yo*S^K$h7`zNGN6GrX%4B?aAU>EH*SFvh}jBh+2p&aCmX}`dN&CuWNpS@iY8uoZfAr(daKv0rT5AMRETSkz zAM+&&{Ns=qG0@^7{t|@1Ad5vogFw3fmLWo4{|bB0ow+(c-t)ZAdEe(e=RNn*^J&)u zq9r%xzKosjAVemFG!r*9(4SQ7ryTls>XsC#Lq(z`KP(2H`n zE>5Weg}NXt+v>GNEP(a8BvO+8DOF_NP~oTmUyV~HVXL+Ng949CB^rTq<~?|J(;UXQ zV(!6cvGh|x9=1d&CX4kWm^gQYbto6Lp2E0beXr`Y^Q9T4^Ke&I&0DIu?AiSi?a1F7 zJv+rFT^EH9n$an8_ipVOUhLsAV{!;%9n>GqR_p zLBo;PAhx|dNY+`$_{rs-n8ce7`%n*=wjslT+a{9P+??ZyF~Z;Ht>n#6aY5P4; ENTRY POINT SYMBOL: "_start" address: 018a0000 @@ -43,15 +43,15 @@ section page origin length input sections 018b5b70 00000050 X11.obj (.cinit) 018b5bc0 00000050 rts.lib (.cinit) -.bss 0 01a00000 0001ec20 UNINITIALIZED +.bss 0 01a00000 0001ec00 UNINITIALIZED 01a00000 00008000 ENTRY.obj (.bss) 01a08000 00000400 MAIN.obj (.bss) 01a08400 00004a40 GAME.obj (.bss) 01a0ce40 00000b80 SCOREDB.obj (.bss) - 01a0d9c0 0000f220 X11.obj (.bss) - 01a1cbe0 00000020 COMPAT.obj (.bss) - 01a1cc00 00002000 XGFIX.obj (.bss) - 01a1ec00 00000020 rts.lib (.bss) + 01a0d9c0 0000f200 X11.obj (.bss) + 01a1cbc0 00000020 COMPAT.obj (.bss) + 01a1cbe0 00002000 XGFIX.obj (.bss) + 01a1ebe0 00000020 rts.lib (.bss) .data 0 018a0000 00000000 UNINITIALIZED @@ -92,23 +92,23 @@ address name address name 018b2c10 _sprintf 018b2390 _fflush 018b4330 _srand 018b2400 _putchar 018a0000 _start 018b2c10 _sprintf -01a1cbe0 _stdout 018b39d0 _vt_install_xg +01a1cbc0 _stdout 018b39d0 _vt_install_xg 018b4400 _strcmp 018b3be0 _memcpy 018b4550 _strcpy 018b3d80 _memmove 018b4670 _strlen 018b4030 _memset 018b4780 _strncpy 018b41d0 _rand -01a1cb00 _term_clearscr 018b4330 _srand -01a1cb40 _term_cursor 018b4400 _strcmp +01a1cae0 _term_clearscr 018b4330 _srand +01a1cb20 _term_cursor 018b4400 _strcmp 01a08a60 _term_height 018b4550 _strcpy -01a1cb80 _term_ibmchar 018b4670 _strlen +01a1cb60 _term_ibmchar 018b4670 _strlen 018b2320 _term_init 018b4780 _strncpy 018b0c30 _term_putstr 018b4940 cinit -01a1cae0 _term_reset 018b4940 etext -01a1cb60 _term_setcolor 01a00000 .bss -01a1cb20 _term_setcursor 01a08360 _dbg_loops -01a1cbc0 _term_type 01a08380 _dbg_events +01a1cac0 _term_reset 018b4940 etext +01a1cb40 _term_setcolor 01a00000 .bss +01a1cb00 _term_setcursor 01a08360 _dbg_loops +01a1cba0 _term_type 01a08380 _dbg_events 01a08a40 _term_width 01a083a0 _dbg_last -01a1cba0 _termenv 01a083c0 _dbg_focus +01a1cb80 _termenv 01a083c0 _dbg_focus 01a08960 _tick_interval 01a083e0 _dbg_rev 018a3130 _update 01a08940 _quit 01a08980 _use_bell 01a08960 _tick_interval @@ -121,21 +121,20 @@ address name address name 018a0530 _vt_poll_event 01a08a40 _term_width 018af950 _vt_root_window 01a08a60 _term_height 01a1ca60 _vt_win 01a08a80 _scr -01a1cac0 _vt_x11_drain_events 01a0ce40 _username -018b0dd0 _vt_x11_flush 01a1ca40 _vt_dpy -018afa30 _vt_x11_init 01a1ca60 _vt_win -01a1caa0 _vt_x11_progress 01a1ca80 _vt_gc -018b2000 _vt_x11_term_init 01a1caa0 _vt_x11_progress -018a1700 _wait_display 01a1cac0 _vt_x11_drain_events -018b01e0 _x11_clearscr 01a1cae0 _term_reset -018b03e0 _x11_cursor 01a1cb00 _term_clearscr -018b07b0 _x11_ibmchar 01a1cb20 _term_setcursor -018b03a0 _x11_reset 01a1cb40 _term_cursor -018b0440 _x11_setcolor 01a1cb60 _term_setcolor -018b0560 _x11_setcursor 01a1cb80 _term_ibmchar -018b4940 cinit 01a1cba0 _termenv -018a0000 edata 01a1cbc0 _term_type -01a1ec20 end 01a1cbe0 _stdout -018b4940 etext 01a1ec20 end +018b0dd0 _vt_x11_flush 01a0ce40 _username +018afa30 _vt_x11_init 01a1ca40 _vt_dpy +01a1caa0 _vt_x11_progress 01a1ca60 _vt_win +018b2000 _vt_x11_term_init 01a1ca80 _vt_gc +018a1700 _wait_display 01a1caa0 _vt_x11_progress +018b01e0 _x11_clearscr 01a1cac0 _term_reset +018b03e0 _x11_cursor 01a1cae0 _term_clearscr +018b07b0 _x11_ibmchar 01a1cb00 _term_setcursor +018b03a0 _x11_reset 01a1cb20 _term_cursor +018b0440 _x11_setcolor 01a1cb40 _term_setcolor +018b0560 _x11_setcursor 01a1cb60 _term_ibmchar +018b4940 cinit 01a1cb80 _termenv +018a0000 edata 01a1cba0 _term_type +01a1ec00 end 01a1cbc0 _stdout +018b4940 etext 01a1ec00 end -[77 symbols] +[76 symbols] diff --git a/apps/termtris/src/vt1200/x11.c b/apps/termtris/src/vt1200/x11.c index a79053e..9d89af5 100644 --- a/apps/termtris/src/vt1200/x11.c +++ b/apps/termtris/src/vt1200/x11.c @@ -1,7 +1,9 @@ /* * x11.c -- a small character-cell terminal that draws through the VT1200 ROM - * Xlib. The game's term_* callbacks feed an 80x24 grid; vt_x11_flush() paints - * only the dirty cells with XFillRectangle (background) + XDrawString (glyph). + * Xlib. The game's term_* callbacks feed an 80x24 grid; vt_x11_flush() repaints + * each dirty row with one XFillRectangle (background) and one XDrawString (the + * row's 80 characters), which keeps the request stream small enough for the + * in-machine X server to drain. * * Geometry is the default GC's font, measured on the real server: 6x13 cells, * baseline +10. Do not change the cell metrics without re-measuring. @@ -29,8 +31,7 @@ static int cur_fg, cur_bg; void *vt_dpy; vt_u32 vt_win; vt_u32 vt_gc; -volatile vt_u32 vt_x11_progress; -int vt_x11_drain_events; +volatile vt_u32 vt_x11_progress; /* debug: 0xFFFFFF when a flush finished */ /* term.h's callback objects (term.c is not compiled for the VT1200). */ void (*term_reset)(void); diff --git a/apps/termtris/src/vt1200/x11.h b/apps/termtris/src/vt1200/x11.h index 6ac8351..023512f 100644 --- a/apps/termtris/src/vt1200/x11.h +++ b/apps/termtris/src/vt1200/x11.h @@ -9,7 +9,6 @@ extern void *vt_dpy; extern vt_u32 vt_win; extern vt_u32 vt_gc; -extern int vt_x11_drain_events; int vt_x11_init(void); void vt_x11_flush(void); diff --git a/apps/termtris/tests/play_keys.sh b/apps/termtris/tests/play_keys.sh index 4d396b9..e39565e 100755 --- a/apps/termtris/tests/play_keys.sh +++ b/apps/termtris/tests/play_keys.sh @@ -29,6 +29,17 @@ for mode in ${MODES:-move panels quit}; do -autoboot_script "$DIR/play_keys.lua" -autoboot_delay 0 \ >"$LOG" 2>&1 || true grep -E 'PLAYKEYS:|LOADER: result|LOADER: snapshot' "$LOG" | tail -5 + DIAG=$(grep -a 'DIAG: loops' "$LOG" | sed -n 's/.*loops=\([0-9]*\) events=\([0-9]*\).*quit=\([0-9]*\)/\1 \2 \3/p' | head -1) + [ -n "$DIAG" ] || { echo "play_keys.sh: no DIAG line for $mode (see $LOG)" >&2; exit 1; } + set -- $DIAG + LOOPS=$1; EVENTS=$2; QUIT=$3 + echo "play_keys: $mode loops=$LOOPS events=$EVENTS quit=$QUIT" + [ "$LOOPS" -ge 100 ] || { echo "play_keys.sh: $mode main loop barely ran ($LOOPS)" >&2; exit 1; } + [ "$EVENTS" -ge 6 ] || { echo "play_keys.sh: $mode got $EVENTS events, expected the click and keys" >&2; exit 1; } + case $mode in + move|panels) [ "$QUIT" = 0 ] || { echo "play_keys.sh: $mode quit unexpectedly" >&2; exit 1; } ;; + quit) [ "$QUIT" = 1 ] || { echo "play_keys.sh: q did not reach the game (quit=$QUIT)" >&2; exit 1; } ;; + esac [ -s "$EV/loader_frame3200.png" ] || { echo "play_keys.sh: no $mode snapshot" >&2; exit 1; } cp "$EV/loader_frame3200.png" "$EV/termtris_${mode}_3200.png" done diff --git a/apps/termtris/tests/render.c b/apps/termtris/tests/render.c index ccb4e2c..1302a8c 100644 --- a/apps/termtris/tests/render.c +++ b/apps/termtris/tests/render.c @@ -48,7 +48,6 @@ int main(void) put(1, 20, word, 0x70); put(21, 20, "0123456789", 0x70); put(22, 20, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 0x70); - vt_x11_drain_events = 1; vt_x11_flush(); mark[4] = 0x464C5553UL; /* FLUS */ -- 2.47.3 From 7a2cef56add5db0adbb130194a7a4688e644dc38 Mon Sep 17 00:00:00 2001 From: "opencode.energia.arti.ee" Date: Wed, 11 Nov 2026 17:59:16 +0000 Subject: [PATCH 11/18] fix(termtris): review pass -- seed, focus/pause, expose, test assertions - seed the game from the raw 0x1E93C0 counter (now_ms() returns 0 on its first call, so the old seed was always zero) - track pause ourselves: FocusOut pauses once, FocusIn resumes only what we paused (game_input('p') is a toggle) - select ExposureMask so the Expose redraw path actually fires - play.sh/play_keys.sh rebuild the COFF (OUTNAME=termtris) and assert the run: status=0, tick>0, loops/events/quit per mode; render.sh asserts the renderer finished; map symbol lookup is column-order independent - run.sh passes -lightgun -lightgun_device mouse so the pointer drives the tablet - docs: link map addresses, monochrome/reverse wording, honest caption and limits text Verified here: play.sh and input.sh pass. Still open: the MAME driver in the tree is newer than the installed binary (Oct 30 build, VR150-era geometry), so the acceptance suite must be re-run after a rebuild. --- .gitignore | 1 - apps/termtris/VT1200.md | 17 +- apps/termtris/build.sh | 5 - apps/termtris/build/termtris.coff | Bin 15118 -> 15208 bytes apps/termtris/build/termtris.map | 213 +++++++++--------- apps/termtris/evidence/termtris_frame3300.png | Bin 19347 -> 19499 bytes apps/termtris/run.sh | 1 + apps/termtris/src/game.c | 4 +- apps/termtris/src/vt1200/main.c | 20 +- apps/termtris/src/vt1200/x11.c | 2 +- apps/termtris/tests/fmt.sh | 2 +- apps/termtris/tests/input.lua | 3 + apps/termtris/tests/input.sh | 4 +- apps/termtris/tests/play.lua | 5 + apps/termtris/tests/play.sh | 16 +- apps/termtris/tests/play_keys.sh | 3 + apps/termtris/tests/render.sh | 12 +- apps/termtris/tests/smoke.sh | 2 +- harness/mame/TERMTRIS_REPORT.md | 7 +- 19 files changed, 183 insertions(+), 134 deletions(-) diff --git a/.gitignore b/.gitignore index 7bc69e1..c7909c8 100644 --- a/.gitignore +++ b/.gitignore @@ -73,7 +73,6 @@ tools/tms34010-sdk/1991-07 TIGA Promo Kit and Art Software/ # termtris build scratch (TI tools + 2ine libs) apps/termtris/work/ apps/termtris/build/*.log -apps/termtris/build/*.log apps/termtris/build/smoke.* apps/termtris/build/fmt.* apps/termtris/build/render.* diff --git a/apps/termtris/VT1200.md b/apps/termtris/VT1200.md index 0add46b..8d418e9 100644 --- a/apps/termtris/VT1200.md +++ b/apps/termtris/VT1200.md @@ -40,7 +40,7 @@ Link map (relevant addresses are **bit** addresses, like everything the TMS34010 sees): ``` -.text 018A0000 .data 018A0000 .cinit 018B4730 +.text 018A0000 .data 018A0000 .cinit 018B4B10 .bss 01A00000 ENTRY POINT SYMBOL: "_start" address: 018a0000 ``` @@ -63,8 +63,9 @@ untouched. With that in place the smoke test receives a real 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 +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. @@ -123,8 +124,9 @@ 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`. +`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 @@ -136,8 +138,9 @@ delivered to it, and `quit=1` is the game's own global after `q`. 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. + 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. diff --git a/apps/termtris/build.sh b/apps/termtris/build.sh index 39c3b06..6b0c33e 100755 --- a/apps/termtris/build.sh +++ b/apps/termtris/build.sh @@ -103,11 +103,6 @@ compile_asm() { run "$base.obj" ./gspa.exe "$base.ASM" } -for base in $OBJS; do - case " $* " in - *" $base "*) ;; # handled below by extension scan - esac -done # Compile every collected object exactly once, in the order given. for arg in "$@"; do diff --git a/apps/termtris/build/termtris.coff b/apps/termtris/build/termtris.coff index 6c515d9717a183721e60d921df7d5405f808769b..cf4275a89a324a6c4360adfeba20763154b83f28 100644 GIT binary patch delta 7312 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 P-qxn3#&d=DPfGs>l;HRs delta 7132 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 diff --git a/apps/termtris/build/termtris.map b/apps/termtris/build/termtris.map index f8a9ea8..78e98d3 100644 --- a/apps/termtris/build/termtris.map +++ b/apps/termtris/build/termtris.map @@ -1,7 +1,7 @@ ***************************************************** TMS340 COFF Linker Version 5.01 ***************************************************** -Tue Nov 03 20:09:24 2026 +Wed Nov 11 17:58:44 2026 OUTPUT FILE NAME: ENTRY POINT SYMBOL: "_start" address: 018a0000 @@ -20,38 +20,38 @@ SECTION ALLOCATION MAP output attributes/ section page origin length input sections -------- ---- ---------- ---------- ---------------- -.text 0 018a0000 00014940 +.text 0 018a0000 00014b10 018a0000 00000150 ENTRY.obj (.text) - 018a0150 00001c90 MAIN.obj (.text) - 018a1de0 0000ce50 GAME.obj (.text) - 018aec30 00000d20 SCOREDB.obj (.text) - 018af950 00002a40 X11.obj (.text) - 018b2390 00001640 COMPAT.obj (.text) - 018b39d0 00000210 XGFIX.obj (.text) - 018b3be0 00000450 rts.lib (.text) - 018b4030 000001a0 rts.lib (.text) - 018b41d0 00000230 rts.lib (.text) - 018b4400 00000150 rts.lib (.text) - 018b4550 00000120 rts.lib (.text) - 018b4670 00000110 rts.lib (.text) - 018b4780 000001c0 rts.lib (.text) + 018a0150 00001e60 MAIN.obj (.text) + 018a1fb0 0000ce50 GAME.obj (.text) + 018aee00 00000d20 SCOREDB.obj (.text) + 018afb20 00002a40 X11.obj (.text) + 018b2560 00001640 COMPAT.obj (.text) + 018b3ba0 00000210 XGFIX.obj (.text) + 018b3db0 00000450 rts.lib (.text) + 018b4200 000001a0 rts.lib (.text) + 018b43a0 00000230 rts.lib (.text) + 018b45d0 00000150 rts.lib (.text) + 018b4720 00000120 rts.lib (.text) + 018b4840 00000110 rts.lib (.text) + 018b4950 000001c0 rts.lib (.text) -.cinit 0 018b4940 000012d0 - 018b4940 00000050 MAIN.obj (.cinit) - 018b4990 00001190 GAME.obj (.cinit) - 018b5b20 00000050 SCOREDB.obj (.cinit) - 018b5b70 00000050 X11.obj (.cinit) - 018b5bc0 00000050 rts.lib (.cinit) +.cinit 0 018b4b10 000012d0 + 018b4b10 00000050 MAIN.obj (.cinit) + 018b4b60 00001190 GAME.obj (.cinit) + 018b5cf0 00000050 SCOREDB.obj (.cinit) + 018b5d40 00000050 X11.obj (.cinit) + 018b5d90 00000050 rts.lib (.cinit) -.bss 0 01a00000 0001ec00 UNINITIALIZED +.bss 0 01a00000 0001ec20 UNINITIALIZED 01a00000 00008000 ENTRY.obj (.bss) - 01a08000 00000400 MAIN.obj (.bss) - 01a08400 00004a40 GAME.obj (.bss) - 01a0ce40 00000b80 SCOREDB.obj (.bss) - 01a0d9c0 0000f200 X11.obj (.bss) - 01a1cbc0 00000020 COMPAT.obj (.bss) - 01a1cbe0 00002000 XGFIX.obj (.bss) - 01a1ebe0 00000020 rts.lib (.bss) + 01a08000 00000420 MAIN.obj (.bss) + 01a08420 00004a40 GAME.obj (.bss) + 01a0ce60 00000b80 SCOREDB.obj (.bss) + 01a0d9e0 0000f200 X11.obj (.bss) + 01a1cbe0 00000020 COMPAT.obj (.bss) + 01a1cc00 00002000 XGFIX.obj (.bss) + 01a1ec00 00000020 rts.lib (.bss) .data 0 018a0000 00000000 UNINITIALIZED @@ -60,81 +60,82 @@ GLOBAL SYMBOLS address name address name -------- ---- -------- ---- -01a00000 .bss 018a0000 .text -018a0000 .data 018a0000 .data -018a0000 .text 018a0000 edata -01a08a00 _blackpf 018a0000 _start -018a2f40 _cleanup_game 018a0150 _now_ms -01a08380 _dbg_events 018a03d0 _sleep_ms -01a083c0 _dbg_focus 018a0530 _vt_poll_event -01a083a0 _dbg_last 018a0ea0 _vt_handle_event -01a08360 _dbg_loops 018a1700 _wait_display -01a083e0 _dbg_rev 018a1740 _main -018b2390 _fflush 018a1de0 _init_game -018a84e0 _game_input 018a2f40 _cleanup_game -018a1de0 _init_game 018a3130 _update -018a1740 _main 018a84e0 _game_input -018b3be0 _memcpy 018aef90 _read_scores -018b3d80 _memmove 018af310 _save_score -018b4030 _memset 018af8a0 _print_scores -01a089a0 _monochrome 018af950 _vt_root_window -018a0150 _now_ms 018afa30 _vt_x11_init -01a089e0 _onlyascii 018b01e0 _x11_clearscr -018af8a0 _print_scores 018b03a0 _x11_reset -018b2400 _putchar 018b03e0 _x11_cursor -01a08940 _quit 018b0440 _x11_setcolor -018b41d0 _rand 018b0560 _x11_setcursor -018aef90 _read_scores 018b07b0 _x11_ibmchar -01a08a20 _rotstep 018b0c30 _term_putstr -018af310 _save_score 018b0dd0 _vt_x11_flush -01a08a80 _scr 018b2000 _vt_x11_term_init -018a03d0 _sleep_ms 018b2320 _term_init -018b2c10 _sprintf 018b2390 _fflush -018b4330 _srand 018b2400 _putchar -018a0000 _start 018b2c10 _sprintf -01a1cbc0 _stdout 018b39d0 _vt_install_xg -018b4400 _strcmp 018b3be0 _memcpy -018b4550 _strcpy 018b3d80 _memmove -018b4670 _strlen 018b4030 _memset -018b4780 _strncpy 018b41d0 _rand -01a1cae0 _term_clearscr 018b4330 _srand -01a1cb20 _term_cursor 018b4400 _strcmp -01a08a60 _term_height 018b4550 _strcpy -01a1cb60 _term_ibmchar 018b4670 _strlen -018b2320 _term_init 018b4780 _strncpy -018b0c30 _term_putstr 018b4940 cinit -01a1cac0 _term_reset 018b4940 etext -01a1cb40 _term_setcolor 01a00000 .bss -01a1cb00 _term_setcursor 01a08360 _dbg_loops -01a1cba0 _term_type 01a08380 _dbg_events -01a08a40 _term_width 01a083a0 _dbg_last -01a1cb80 _termenv 01a083c0 _dbg_focus -01a08960 _tick_interval 01a083e0 _dbg_rev -018a3130 _update 01a08940 _quit -01a08980 _use_bell 01a08960 _tick_interval -01a089c0 _use_gfxchar 01a08980 _use_bell -01a0ce40 _username 01a089a0 _monochrome -01a1ca40 _vt_dpy 01a089c0 _use_gfxchar -01a1ca80 _vt_gc 01a089e0 _onlyascii -018a0ea0 _vt_handle_event 01a08a00 _blackpf -018b39d0 _vt_install_xg 01a08a20 _rotstep -018a0530 _vt_poll_event 01a08a40 _term_width -018af950 _vt_root_window 01a08a60 _term_height -01a1ca60 _vt_win 01a08a80 _scr -018b0dd0 _vt_x11_flush 01a0ce40 _username -018afa30 _vt_x11_init 01a1ca40 _vt_dpy -01a1caa0 _vt_x11_progress 01a1ca60 _vt_win -018b2000 _vt_x11_term_init 01a1ca80 _vt_gc -018a1700 _wait_display 01a1caa0 _vt_x11_progress -018b01e0 _x11_clearscr 01a1cac0 _term_reset -018b03e0 _x11_cursor 01a1cae0 _term_clearscr -018b07b0 _x11_ibmchar 01a1cb00 _term_setcursor -018b03a0 _x11_reset 01a1cb20 _term_cursor -018b0440 _x11_setcolor 01a1cb40 _term_setcolor -018b0560 _x11_setcursor 01a1cb60 _term_ibmchar -018b4940 cinit 01a1cb80 _termenv -018a0000 edata 01a1cba0 _term_type -01a1ec00 end 01a1cbc0 _stdout -018b4940 etext 01a1ec00 end +01a00000 .bss 018a0000 .data +018a0000 .data 018a0000 .text +018a0000 .text 018a0000 _start +01a08a20 _blackpf 018a0000 edata +018a3110 _cleanup_game 018a0150 _now_ms +01a083a0 _dbg_events 018a03d0 _vt_rand_seed +01a083e0 _dbg_focus 018a0420 _sleep_ms +01a083c0 _dbg_last 018a0580 _vt_poll_event +01a08380 _dbg_loops 018a0ef0 _vt_handle_event +01a08400 _dbg_rev 018a18d0 _wait_display +018b2560 _fflush 018a1910 _main +018a86b0 _game_input 018a1fb0 _init_game +018a1fb0 _init_game 018a3110 _cleanup_game +018a1910 _main 018a3300 _update +018b3db0 _memcpy 018a86b0 _game_input +018b3f50 _memmove 018af160 _read_scores +018b4200 _memset 018af4e0 _save_score +01a089c0 _monochrome 018afa70 _print_scores +018a0150 _now_ms 018afb20 _vt_root_window +01a08a00 _onlyascii 018afc00 _vt_x11_init +018afa70 _print_scores 018b03b0 _x11_clearscr +018b25d0 _putchar 018b0570 _x11_reset +01a08960 _quit 018b05b0 _x11_cursor +018b43a0 _rand 018b0610 _x11_setcolor +018af160 _read_scores 018b0730 _x11_setcursor +01a08a40 _rotstep 018b0980 _x11_ibmchar +018af4e0 _save_score 018b0e00 _term_putstr +01a08aa0 _scr 018b0fa0 _vt_x11_flush +018a0420 _sleep_ms 018b21d0 _vt_x11_term_init +018b2de0 _sprintf 018b24f0 _term_init +018b4500 _srand 018b2560 _fflush +018a0000 _start 018b25d0 _putchar +01a1cbe0 _stdout 018b2de0 _sprintf +018b45d0 _strcmp 018b3ba0 _vt_install_xg +018b4720 _strcpy 018b3db0 _memcpy +018b4840 _strlen 018b3f50 _memmove +018b4950 _strncpy 018b4200 _memset +01a1cb00 _term_clearscr 018b43a0 _rand +01a1cb40 _term_cursor 018b4500 _srand +01a08a80 _term_height 018b45d0 _strcmp +01a1cb80 _term_ibmchar 018b4720 _strcpy +018b24f0 _term_init 018b4840 _strlen +018b0e00 _term_putstr 018b4950 _strncpy +01a1cae0 _term_reset 018b4b10 cinit +01a1cb60 _term_setcolor 018b4b10 etext +01a1cb20 _term_setcursor 01a00000 .bss +01a1cbc0 _term_type 01a08380 _dbg_loops +01a08a60 _term_width 01a083a0 _dbg_events +01a1cba0 _termenv 01a083c0 _dbg_last +01a08980 _tick_interval 01a083e0 _dbg_focus +018a3300 _update 01a08400 _dbg_rev +01a089a0 _use_bell 01a08960 _quit +01a089e0 _use_gfxchar 01a08980 _tick_interval +01a0ce60 _username 01a089a0 _use_bell +01a1ca60 _vt_dpy 01a089c0 _monochrome +01a1caa0 _vt_gc 01a089e0 _use_gfxchar +018a0ef0 _vt_handle_event 01a08a00 _onlyascii +018b3ba0 _vt_install_xg 01a08a20 _blackpf +018a0580 _vt_poll_event 01a08a40 _rotstep +018a03d0 _vt_rand_seed 01a08a60 _term_width +018afb20 _vt_root_window 01a08a80 _term_height +01a1ca80 _vt_win 01a08aa0 _scr +018b0fa0 _vt_x11_flush 01a0ce60 _username +018afc00 _vt_x11_init 01a1ca60 _vt_dpy +01a1cac0 _vt_x11_progress 01a1ca80 _vt_win +018b21d0 _vt_x11_term_init 01a1caa0 _vt_gc +018a18d0 _wait_display 01a1cac0 _vt_x11_progress +018b03b0 _x11_clearscr 01a1cae0 _term_reset +018b05b0 _x11_cursor 01a1cb00 _term_clearscr +018b0980 _x11_ibmchar 01a1cb20 _term_setcursor +018b0570 _x11_reset 01a1cb40 _term_cursor +018b0610 _x11_setcolor 01a1cb60 _term_setcolor +018b0730 _x11_setcursor 01a1cb80 _term_ibmchar +018b4b10 cinit 01a1cba0 _termenv +018a0000 edata 01a1cbc0 _term_type +01a1ec20 end 01a1cbe0 _stdout +018b4b10 etext 01a1ec20 end -[76 symbols] +[77 symbols] diff --git a/apps/termtris/evidence/termtris_frame3300.png b/apps/termtris/evidence/termtris_frame3300.png index 47128cb0eb6bce2d62a79645d42ae1b074ac3e3a..54cbf26993349c5ac36bd8b266d7a82321ae8587 100644 GIT binary patch literal 19499 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 literal 19347 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

V=wgul?*b2O_` zDt4tTmfnvkDt$W)WBtKz!gEK4zA2lHFyX^hUm3c;0b;ZS67w8il z9_^??g>PaFx7&gd>l)jCtpc}hI%{})z_$oR%QnPB$UDl9Cf$9&XFrwu3RAwA(a%M$ zJJ?={n%eokRo*MftaU-q#_Us>nZq!k)s`%($k==3@bHeiXEf+WaYUuRAI;eBFF#o+ zCq^4&HgX*WiFydmV6!Jyd4Vjhqve9b49@nw#&Qoi-+GG?NxrFGb{JNsPY@R z+Z46JJV8)le{(o)Pzr=briyZ52}^!#HG%x;?Pkl6G-4$M*w8uCa%d={M$vSCtcCP3 zSANo)>swy6Am$0uoneksdLvp{$tk8(`!4Wt5h=%!$g2rl{oM4D8Mwh|{%nqfBxM(x z^CYF@D>VAn{_^OdAsiu`<3m(dE%s@6ygEmN+JU~dV<5jM(v&xLkmzeFUvP0vVHW$I;q3V8?*`00V@J+$J6NYY>(0 z1v!NWf(PHdRDUi9pz~&xufsmIGXhit&N#>1nQ=P|j{qZq>+O*9nLx(Ci?hHtP(vX< zBt-4iLe|VnQXK6}>DM2AU5^z=%75nYD(mhB_$oOOpY@gxc4h;i-i$ft&Mcf)aRCI1 z5gb12I_yjkANlka7k$%^;AN`Eaqb&UI#+j!h^4E^7mp}HR>*}8q$RB%T6sf?P7D{S8m(sCjqV7JG+2;8|M3K z!tvsTE{M z&kf4%w;Fs}cw5OMj)7#L)?yc}IAe#xc^x=jB>Zr+syRmq;@2R4%r0ob8ieF3yFj$w z`jf(o5mfXrtSfg;3R+qu*-2vZC%N9zYw^58zS<|hZrvW)w0}U6kM0~qWpI;u3nRJN z3<^t*$z z>DIFY<3kQ;!gn9xHev)m)#Ib_dm9+It|I$`VyXbc@7F`Da4Xci4{w~vlk=d93Asm) zb*OS8`@Cc+%;ubwRM~TLY}dOdKLI~NNyFXbcTl0+4|6tu;c(rb5$V&x&g_aT?O7aC zyS-F1i87g^%NfpHi46Nq4>VvEH|OLC7|07M<#3q`hRk%5io%jrb*h9$CdRVSOXGoe z)zstlShyNx$ZAZ0pP{lY4vLqqj7AJ*@zeN?Hh9Z3{!+eOflL~{680*L&W_(p!XgO2 zmI?!$@U&5}oK2}2xMr5pR|YMWa=nH!vr*pMLgauyJbUs$44Nb`$KX@WB5D?0903LKnIjVTrqb|$ z5-@IrVsDNQfX}K8m;SkPLLcGjb@_0`DwNj%^5=o?Ju6+^+;+Kij9{&k&YMBEa|z9b zwe+c!0GOC>Oo{>-iYis29L)<2dH51O7m?JaZdVd%wyFM*57hpU4-cZ%V$)3a{{XOxQl z0jj4E=W*;7Rrzo#sKFG&e+4O5F3#E+abE8qP|R*JJv()TyW}rrC58W`EV)DdFX~YLKa0VOWeesQ{=zlU8eI)RN%AOzhSgqP*xW(9H)w&QrZ$E#u-}>c~VG&^9M)|<- zpx_g}D!Z(HTIF. #include "term.h" #include "scoredb.h" -extern long now_ms(void); +extern unsigned vt_rand_seed(void); int quit; @@ -192,7 +192,7 @@ int init_game(void) scores = read_scores(0, 10); memset(&cur_score, 0, sizeof cur_score); - srand((unsigned)now_ms()); + srand(vt_rand_seed()); pause = 0; gameover = 0; diff --git a/apps/termtris/src/vt1200/main.c b/apps/termtris/src/vt1200/main.c index 650b978..f4500eb 100644 --- a/apps/termtris/src/vt1200/main.c +++ b/apps/termtris/src/vt1200/main.c @@ -18,6 +18,7 @@ static vt_u32 evbuf[24]; static long ms_base; static int ms_last = -1; static int focused; +static int vt_paused; volatile vt_u32 dbg_loops, dbg_events, dbg_last, dbg_focus, dbg_rev; long now_ms(void) @@ -34,6 +35,16 @@ long now_ms(void) return ms_base; } +/* + * Seed for the game's PRNG. now_ms() returns 0 on its first call (it only + * latches the counter there), so seeding from it gives a constant; read the + * free-running 16-bit tick directly instead. + */ +unsigned vt_rand_seed(void) +{ + return (unsigned)*(volatile unsigned short *)0x001E93C0UL; +} + void sleep_ms(int ms) { if(ms < 1) ms = 1; @@ -105,8 +116,15 @@ void vt_handle_event(void) game_input('`'); } else if(evbuf[0] == 9) { /* FocusIn */ focused = 1; + if(vt_paused) { /* resume only what we paused */ + game_input('p'); + vt_paused = 0; + } } else if(evbuf[0] == 10) { /* FocusOut */ - if(focused) game_input('p'); + if(focused && !vt_paused) { + game_input('p'); /* 'p' toggles pause in game_input */ + vt_paused = 1; + } focused = 0; } } diff --git a/apps/termtris/src/vt1200/x11.c b/apps/termtris/src/vt1200/x11.c index 9d89af5..54139f2 100644 --- a/apps/termtris/src/vt1200/x11.c +++ b/apps/termtris/src/vt1200/x11.c @@ -64,7 +64,7 @@ int vt_x11_init(void) if(!vt_win) return -1; vt_x11_progress = 14; - VT_XSELECT(vt_dpy, vt_win, 0x22000FUL); + VT_XSELECT(vt_dpy, vt_win, 0x22800FUL); /* + ExposureMask */ vt_gc = VT_XGC(vt_dpy, vt_win, 0, 0); if(!vt_gc) return -1; diff --git a/apps/termtris/tests/fmt.sh b/apps/termtris/tests/fmt.sh index acb354b..19ff689 100755 --- a/apps/termtris/tests/fmt.sh +++ b/apps/termtris/tests/fmt.sh @@ -12,7 +12,7 @@ LOG="$APP/build/fmt.log" mkdir -p "$APP/build" "$APP/build.sh" "$DIR/fmt.c" "$APP/src/vt1200/compat.c" >"$APP/build/fmt-build.log" 2>&1 -FMT=$(awk '/_out/ {print $1; exit}' "$APP/build/fmt.map") +FMT=$(awk '{for(i=2;i<=NF;i++) if($i=="_out") print $(i-1)}' "$APP/build/fmt.map" | head -1) VT1200_SKIP_LEVEL1=1 FMT_MARK="0x$FMT" \ LOADER_AT=2500 LOADER_END=2700 \ LOADER_BIN="$ROOT/dwt/loader/build/loader.bin" \ diff --git a/apps/termtris/tests/input.lua b/apps/termtris/tests/input.lua index ca3c703..fd89e54 100644 --- a/apps/termtris/tests/input.lua +++ b/apps/termtris/tests/input.lua @@ -36,6 +36,7 @@ local key_zero = ports[":lk401:K10"].fields["0 )"] local key_ret = ports[":lk401:K68"].fields["Return"] local key_del = ports[":lk401:K67"].fields["Delete"] local key_q = ports[":lk401:K13"].fields["Q"] +local key_p = ports[":lk401:K22"].fields["P"] local tabx = ports[":tablet:TABLET_X"].fields["Tablet X"] local taby = ports[":tablet:TABLET_Y"].fields["Tablet Y"] local tabb = ports[":tablet:TABLET_BTN"].fields["P1 Button 1"] @@ -154,6 +155,8 @@ local function key_step(frames, key) if frames == 3250 then key_del:set_value(0) end if frames == 3260 then key_q:set_value(1) end if frames == 3270 then key_q:set_value(0) end + if frames == 3280 then key_p:set_value(1) end + if frames == 3290 then key_p:set_value(0) end end local function r32(a) diff --git a/apps/termtris/tests/input.sh b/apps/termtris/tests/input.sh index 6453f57..c4dd04d 100755 --- a/apps/termtris/tests/input.sh +++ b/apps/termtris/tests/input.sh @@ -8,7 +8,7 @@ M=${MAME:-/home/arti/code/k-space/mame0289/vt1200} LOG="$APP/build/input.log" mkdir -p "$APP/build" "$APP/build.sh" "$DIR/input.c" "$APP/src/vt1200/main.c" "$APP/src/vt1200/x11.c" "$APP/src/vt1200/xgfix.c" >"$APP/build/input-build.log" 2>&1 -MARK=$(awk '/_mark/ {print $1; exit}' "$APP/build/input.map") +MARK=$(awk '{for(i=2;i<=NF;i++) if($i=="_mark") print $(i-1)}' "$APP/build/input.map" | head -1) VT1200_SKIP_LEVEL1=1 INPUT_MARK="0x$MARK" \ LOADER_AT=2500 LOADER_END=3400 \ LOADER_BIN="$ROOT/dwt/loader/build/loader.bin" \ @@ -20,7 +20,7 @@ LOADER_OUT="$APP/build" \ -autoboot_script "$DIR/input.lua" -autoboot_delay 0 \ >"$LOG" 2>&1 || true grep 'INPUT:' "$LOG" | tail -3 -grep -q 'INPUT: .*bytes=.*61 9B 41 9B 42 9B 44 9B 43 30 0A 7F 71' "$LOG" || { +grep -q 'INPUT: .*bytes=.*61 9B 41 9B 42 9B 44 9B 43 30 0A 7F 71 70' "$LOG" || { echo "input.sh: mapped key sequence missing (see $LOG)" >&2 exit 1 } diff --git a/apps/termtris/tests/play.lua b/apps/termtris/tests/play.lua index 5f35daf..9e91d32 100644 --- a/apps/termtris/tests/play.lua +++ b/apps/termtris/tests/play.lua @@ -25,6 +25,8 @@ local function hex(v, w) return string.format("%0" .. (w or 8) .. "X", v) end local machine = manager.machine +local PTICK = tonumber(os.getenv("DIAG_TICK") or "0") +local PQUIT = tonumber(os.getenv("DIAG_QUIT") or "0") local ports = machine.ioport.ports local tabx = ports[":tablet:TABLET_X"].fields["Tablet X"] local taby = ports[":tablet:TABLET_Y"].fields["Tablet Y"] @@ -274,6 +276,9 @@ local function inspect() r32(0x01890140), r32(0x01890160), hex(r32(0x01890100)), hex(r32(0x01890110)), hex(r32(0x01890120)), hex(r32(0x01890130)))) end + if PTICK ~= 0 or PQUIT ~= 0 then + print(string.format("DIAG: tick=%d quit=%d", PTICK ~= 0 and r32(PTICK) or -1, PQUIT ~= 0 and r32(PQUIT) or -1)) + end print(string.format("LOADER: PC at inspection = %s", hex(cpu.state['PC'].value))) end diff --git a/apps/termtris/tests/play.sh b/apps/termtris/tests/play.sh index 56f88fb..7afc6d9 100755 --- a/apps/termtris/tests/play.sh +++ b/apps/termtris/tests/play.sh @@ -8,8 +8,13 @@ M=${MAME:-/home/arti/code/k-space/mame0289/vt1200} EV="$APP/evidence" LOG="$APP/build/play.log" mkdir -p "$EV" "$APP/build" +OUTNAME=termtris "$APP/build.sh" "$APP/src/vt1200/main.c" "$APP/src/game.c" "$APP/src/vt1200/scoredb.c" \ + "$APP/src/vt1200/x11.c" "$APP/src/vt1200/compat.c" "$APP/src/vt1200/xgfix.c" \ + >"$APP/build/play-build.log" 2>&1 rm -f "$EV/loader_frame3300.png" -VT1200_SKIP_LEVEL1=1 LOADER_SNAP=1 \ +TICK=$(awk '{for(i=2;i<=NF;i++) if($i=="_tick_interval") print $(i-1)}' "$APP/build/termtris.map" | head -1) +QUIT=$(awk '{for(i=2;i<=NF;i++) if($i=="_quit") print $(i-1)}' "$APP/build/termtris.map" | head -1) +VT1200_SKIP_LEVEL1=1 LOADER_SNAP=1 DIAG_TICK="0x$TICK" DIAG_QUIT="0x$QUIT" \ LOADER_AT=2500 LOADER_END=3300 \ LOADER_BIN="$ROOT/dwt/loader/build/loader.bin" \ LOADER_COFF="$APP/build/termtris.coff" \ @@ -19,7 +24,14 @@ LOADER_OUT="$EV" \ -debug -debugger none \ -autoboot_script "$DIR/play.lua" -autoboot_delay 0 \ >"$LOG" 2>&1 || true -grep -E 'PLAY:|LOADER: result|LOADER: snapshot' "$LOG" | tail -6 +grep -E 'PLAY:|DIAG:|LOADER: result|LOADER: snapshot' "$LOG" | tail -6 +grep -q 'LOADER: result status=0' "$LOG" || { echo "play.sh: the game did not start (see $LOG)" >&2; exit 1; } +DIAG=$(grep -a 'DIAG: tick=' "$LOG" | head -1) +[ -n "$DIAG" ] || { echo "play.sh: no DIAG line (see $LOG)" >&2; exit 1; } +TICKVAL=$(printf '%s' "$DIAG" | sed -n 's/.*tick=\([-0-9]*\).*/\1/p') +QUITVAL=$(printf '%s' "$DIAG" | sed -n 's/.*quit=\([-0-9]*\).*/\1/p') +[ "$TICKVAL" -gt 0 ] || { echo "play.sh: init_game never ran (tick=$TICKVAL)" >&2; exit 1; } +[ "$QUITVAL" = 0 ] || { echo "play.sh: game quit by itself" >&2; exit 1; } [ -s "$EV/loader_frame3300.png" ] || { echo "play.sh: no snapshot (see $LOG)" >&2; exit 1; } cp "$EV/loader_frame3300.png" "$EV/termtris_frame3300.png" echo "play: $EV/termtris_frame3300.png" diff --git a/apps/termtris/tests/play_keys.sh b/apps/termtris/tests/play_keys.sh index e39565e..a7a1fc0 100755 --- a/apps/termtris/tests/play_keys.sh +++ b/apps/termtris/tests/play_keys.sh @@ -7,6 +7,9 @@ ROOT=$(cd "$APP/../.." && pwd) M=${MAME:-/home/arti/code/k-space/mame0289/vt1200} EV="$APP/evidence" mkdir -p "$EV" "$APP/build" +OUTNAME=termtris "$APP/build.sh" "$APP/src/vt1200/main.c" "$APP/src/game.c" "$APP/src/vt1200/scoredb.c" \ + "$APP/src/vt1200/x11.c" "$APP/src/vt1200/compat.c" "$APP/src/vt1200/xgfix.c" \ + >"$APP/build/play_keys-build.log" 2>&1 for mode in ${MODES:-move panels quit}; do LOG="$APP/build/play_$mode.log" rm -f "$EV/loader_frame3200.png" diff --git a/apps/termtris/tests/render.sh b/apps/termtris/tests/render.sh index a088feb..74cb5c8 100755 --- a/apps/termtris/tests/render.sh +++ b/apps/termtris/tests/render.sh @@ -11,8 +11,8 @@ LOG="$APP/build/render.log" mkdir -p "$EV" "$APP/build" "$APP/build.sh" "$DIR/render.c" "$APP/src/vt1200/x11.c" "$APP/src/vt1200/xgfix.c" >"$APP/build/render-build.log" 2>&1 rm -f "$EV/loader_frame3400.png" -MARK=$(awk '/_mark/ {print $1; exit}' "$APP/build/render.map") -PROG=$(awk '/_vt_x11_progress/ {print $1; exit}' "$APP/build/render.map") +MARK=$(awk '{for(i=2;i<=NF;i++) if($i=="_mark") print $(i-1)}' "$APP/build/render.map" | head -1) +PROG=$(awk '{for(i=2;i<=NF;i++) if($i=="_vt_x11_progress") print $(i-1)}' "$APP/build/render.map" | head -1) VT1200_SKIP_LEVEL1=1 LOADER_SNAP=1 RENDER_MARK="0x$MARK" X11_PROGRESS="0x$PROG" \ LOADER_AT=2500 LOADER_END=3400 \ LOADER_BIN="$ROOT/dwt/loader/build/loader.bin" \ @@ -24,6 +24,14 @@ LOADER_OUT="$EV" \ -autoboot_script "$DIR/render.lua" -autoboot_delay 0 \ >"$LOG" 2>&1 || true grep -E 'LOADER: snapshot|LOADER: result' "$LOG" | tail -4 +grep -q 'RENDER: magic=524E4452 .*flus=464C5553' "$LOG" || { + echo "render.sh: the renderer did not finish (see $LOG)" >&2 + exit 1 +} +grep -q 'RENDER: .*progress=00FFFFFF' "$LOG" || { + echo "render.sh: flush did not complete (see $LOG)" >&2 + exit 1 +} [ -s "$EV/loader_frame3400.png" ] || { echo "render.sh: no snapshot (see $LOG)" >&2; exit 1; } cp "$EV/loader_frame3400.png" "$EV/render_frame3400.png" echo "render: $EV/render_frame3400.png" diff --git a/apps/termtris/tests/smoke.sh b/apps/termtris/tests/smoke.sh index a35d03d..af7044e 100755 --- a/apps/termtris/tests/smoke.sh +++ b/apps/termtris/tests/smoke.sh @@ -13,7 +13,7 @@ LOG="$APP/build/smoke.log" mkdir -p "$APP/build" "$APP/build.sh" "$DIR/smoke.c" "$APP/src/vt1200/xgfix.c" >"$APP/build/smoke-build.log" 2>&1 -MARK=$(awk '/_mark/ {print $1; exit}' "$APP/build/smoke.map") +MARK=$(awk '{for(i=2;i<=NF;i++) if($i=="_mark") print $(i-1)}' "$APP/build/smoke.map" | head -1) VT1200_SKIP_LEVEL1=1 SMOKE_MARK="0x$MARK" \ LOADER_AT=2500 LOADER_END=3400 \ LOADER_BIN="$ROOT/dwt/loader/build/loader.bin" \ diff --git a/harness/mame/TERMTRIS_REPORT.md b/harness/mame/TERMTRIS_REPORT.md index 5d3f80a..aaa78d2 100644 --- a/harness/mame/TERMTRIS_REPORT.md +++ b/harness/mame/TERMTRIS_REPORT.md @@ -30,7 +30,7 @@ file as success and retries a missing one. Two quirks are handled there: Result (map): ``` -.text 018A0000 .cinit 018B4730 .bss 01A00000 +.text 018A0000 .cinit 018B4B10 .bss 01A00000 ENTRY POINT SYMBOL: "_start" address: 018a0000 ``` @@ -51,8 +51,9 @@ Smoke test evidence (`apps/termtris/tests/smoke.sh`): ## 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 +The raster is 1 bit deep, so the backend drops colour (like the game's `-m` +monochrome mode) and treats the bold bit (`0x80`) as reverse video. That +reversal is this backend's choice; upstream `ansi.c -m` only emits SGR bold. The playfield is black spaces; tetrominoes are the `[`/`]` tiles. `vt_x11_flush()` repaints **whole rows** — one fill for the row background, one -- 2.47.3 From be5b323f5d2cc4f7a7d25e7143c67bd9828ccbdf Mon Sep 17 00:00:00 2001 From: "opencode.energia.arti.ee" Date: Wed, 11 Nov 2026 17:59:30 +0000 Subject: [PATCH 12/18] docs(termtris): build the harness binary before reproducing evidence --- apps/termtris/VT1200.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/apps/termtris/VT1200.md b/apps/termtris/VT1200.md index 8d418e9..7ad4b72 100644 --- a/apps/termtris/VT1200.md +++ b/apps/termtris/VT1200.md @@ -94,6 +94,16 @@ codes (`0x9b` + `A`/`B`/`C`/`D`), which `game_input` translates back to ## 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: + +```sh +harness/mame/build.sh # rebuild the vt1200 subtarget (JOBS=4 on small hosts) +``` + +Then: + ```sh apps/termtris/tests/play.sh # load, click, snapshot apps/termtris/tests/play_keys.sh # move / panels / quit acceptance runs -- 2.47.3 From a12fb47d5fb0c850af0aeb71b098ed3b05d0abbd Mon Sep 17 00:00:00 2001 From: "opencode.energia.arti.ee" Date: Thu, 12 Nov 2026 05:14:33 +0000 Subject: [PATCH 13/18] chore(termtris): handoff script and notes for the next machine apps/termtris/verify.sh runs the whole acceptance: ROM md5 guard, 2ine check, MAME build (--skip-mame to reuse one), the six tests, the acceptance summary and the evidence list, all logged under build/. HANDOFF.md states what is done, what is still open (the docs' figures and screenshots predate the VR319 default because the installed MAME binary is the 30 Oct build), the expected outputs, and the failure modes already diagnosed. Refresh the acceptance numbers from the verify run: 348/19, 338/13, 292/11. --- apps/termtris/HANDOFF.md | 133 ++++++++++++++++++ apps/termtris/VT1200.md | 9 +- apps/termtris/build/termtris.coff | Bin 15208 -> 15208 bytes apps/termtris/build/termtris.map | 2 +- apps/termtris/evidence/termtris_move_3200.png | Bin 19458 -> 19524 bytes .../evidence/termtris_panels_3200.png | Bin 22396 -> 22474 bytes apps/termtris/evidence/termtris_quit_3200.png | Bin 19407 -> 19497 bytes apps/termtris/verify.sh | 107 ++++++++++++++ harness/mame/TERMTRIS_REPORT.md | 6 +- 9 files changed, 249 insertions(+), 8 deletions(-) create mode 100644 apps/termtris/HANDOFF.md create mode 100755 apps/termtris/verify.sh diff --git a/apps/termtris/HANDOFF.md b/apps/termtris/HANDOFF.md new file mode 100644 index 0000000..5a5ad87 --- /dev/null +++ b/apps/termtris/HANDOFF.md @@ -0,0 +1,133 @@ +# Handoff — termtris running inside the VT1200 + +Everything here is on branch **`termtris`** of +`https://git.wut.ee/opencode.energia.arti.ee/vt1200.git` (remote `origin`). +The branch was developed in a worktree at +`/home/arti/.local/share/opencode/worktree/26e92e/termtris`; any checkout of the +branch is equivalent. + +```sh +git fetch origin termtris +git checkout -b termtris origin/termtris +apps/termtris/verify.sh +``` + +`verify.sh` is the whole acceptance run: ROM checksum, toolchain check, MAME +build (skip with `--skip-mame`), the six tests, the acceptance summary and the +evidence list. It exits non-zero on the first failure and keeps every test log +under `apps/termtris/build/verify_.log`. `RUNTIME` is roughly 12 minutes +once MAME is built; the MAME build itself is the long part. + +## What is done + +The upstream game (John Tsiombikas' `termtris`, commit `7c5435d`) builds as a +native TMS34010 program with the 1990 TI r5.01 C toolchain under 2ine, loads +into VT1200 RAM through `dwt/loader`, opens a 480×312 window on the terminal's +own X server through the ROM Xlib, and reads real X11 key events. It is the +actual game: board, score/level/lines panels, piece preview, help and +high-score panels, moving/rotating/dropping pieces, and `q` to quit. + +| task | state | commit | +|---|---|---| +| vendor + toolchain + XG smoke test | done, `smoke.sh` PASS | `a861268` | +| `sprintf`/stdio compat | done, `fmt.sh` PASS | `a804181` | +| cell-grid X11 backend | done, `render.sh` PASS | `264b53a` | +| key map, clock, main loop | done, `input.sh` PASS | `c1945cf` | +| game integration, scores, COFF | done, `play.sh`/`play_keys.sh` PASS | `41d3cec` | +| docs + harness report | done | `3e2ddde` | +| review pass (seed, focus, expose, assertions) | done | `7a2cef5` | +| handoff script + this note | done | (this commit) | + +`VT1200.md` is the port documentation; `harness/mame/TERMTRIS_REPORT.md` is the +harness-side report; the design and plan are in +`docs/superpowers/specs/2026-11-03-termtris-vt1200-design.md` and +`docs/superpowers/plans/2026-11-03-termtris-vt1200.md`. + +## What is *not* done yet — read this first + +All screenshots and the acceptance numbers in the docs were produced with an +**older MAME binary** (`/home/arti/code/k-space/mame0289/vt1200d`, 30 Oct) that +predates the driver's VR319 default. That binary has no `VT1200_MONITOR`, and +its frames are 1024×864, i.e. the old VR150 geometry. The driver in this tree +defaults to **VR319** (1280×1024 active, 1696×1063 raster), which changes the +frame cadence and therefore the frame at which the loader and the tests act. + +So on the new box, after `harness/mame/build.sh`: + +1. Re-run `apps/termtris/verify.sh --skip-mame` and note the new + `DIAG: loops=... events=... quit=...` values. +2. Re-capture the screenshots (the test scripts write them into + `apps/termtris/evidence/`) and look at them: the board must still be a + bordered well with `[`/`]` tetrominoes, the score/level/lines boxes on the + right, and the preview box. The window is positioned at (80,80) inside a + larger raster, so the PNGs get bigger but the window content is unchanged. +3. Update the numbers in `VT1200.md` ("Acceptance values") and in + `harness/mame/TERMTRIS_REPORT.md`, and say plainly that the earlier figures + were VR150. + +If the behaviour itself changes at VR319 (pieces not visible, no events), that +is a real finding, not a formatting detail — the loader acts at frame 2500 and +the keys at 3100–3290, which may need re-timing if the new raster shifts the +frame budget. + +## Prerequisites on the new box + +* a MAME 0.289 checkout (`MAME_SRC`, default `/home/arti/code/k-space/mame0289`); +* the 2ine build with `lx_loader` (`NINE=`, default + `/home/arti/os2-work/2ine/build`); +* the r5.01 SDK zips, already in the repo under + `tools/tms34010-sdk/1990-11 TMS340 Family SDK/`; +* `vt1200_v22.bin` in the repo root (the harness ROM dir symlinks to it). + +`verify.sh` checks the ROM and the 2ine loader before doing anything, and +`harness/mame/build.sh` re-registers the driver in `mame.lst` and builds the +single-driver `vt1200` subtarget. + +## Expected results + +Last run here (older MAME, VR150 geometry): + +``` +smoke PASS type=2 keycode=194 lookup_n=1 ch=61 ks=0061 +fmt PASS six formats exact +render PASS flus=464C5553 progress=00FFFFFF +input PASS bytes 60 61 9B 41 9B 42 9B 44 9B 43 30 0A 7F 71 70 + (60 is the Expose redraw, then a/up/down/left/right/0/Return/Delete/q/p) +play PASS DIAG: tick=887 quit=0 +play_keys PASS move DIAG: loops=348 events=19 focus=00400001 quit=0 + panels DIAG: loops=338 events=13 focus=00400001 quit=0 + quit DIAG: loops=292 events=11 focus=00400001 quit=1 +``` + +`focus=00400001` is the window the process created; `quit=1` is the game's own +global after `q` — that is the proof the key reached the game and not just the +window. + +## Known failure modes (all seen, all diagnosed) + +| symptom | cause | handling | +|---|---|---| +| build stage "fails" with exit 139 but produced its file | 2ine-hosted TI tools crash during teardown | `build.sh` retries only when the output is missing; `gspac` needs `-i.` | +| linker points `cinit` into `.text` | empty `.cinit` section | `build.sh` relinks with the one-word `CINITZ.obj` terminator | +| `XNextEvent` never returns an event, keycode zero | child's per-process Xlib context is NULL | `src/vt1200/xgfix.c` writes a zeroed 1 KiB block before `XOpenDisplay` (RAM only) | +| the game exits immediately at startup, sometimes | `XOpenDisplay`/window creation while the display server settles | `main()` retries 40 × 50 ms, then returns 1 | +| window paints once, then hangs; a debug marker stops mid-flush | per-cell flushing overflowed the X socket / `XSync` per row deadlocked the client | flush repaints whole rows (fill + one 80-char string + inverse spans + one `XFlush`) | +| playfield is a white block with invisible pieces | the 1-bit raster cannot show colour | backend drops colour and uses bold (`0x80`) as reverse; pieces are `[`/`]` glyphs | + +## Layout + +``` +apps/termtris/ +├── build.sh TI r5.01 -> COFF (OUTNAME=termtris for the game) +├── run.sh interactive MAME session (lightgun/tablet input) +├── verify.sh one-command acceptance (see above) +├── VT1200.md port documentation +├── src/game.c upstream game; only srand(vt_rand_seed()) differs +├── src/vt1200/ main.c x11.c xgfix.c compat.c xlib.h scoredb.c *.asm +├── tests/ smoke fmt render input play play_keys (+ .lua drivers) +├── evidence/ screenshots +└── build/termtris.coff the built image (tracked for convenience) +``` + +The tests build from source every run (`play.sh`/`play_keys.sh` rebuild +`termtris.coff`), so an edit cannot silently pass by using a stale binary. diff --git a/apps/termtris/VT1200.md b/apps/termtris/VT1200.md index 7ad4b72..18fb0ab 100644 --- a/apps/termtris/VT1200.md +++ b/apps/termtris/VT1200.md @@ -89,7 +89,7 @@ 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 +`w`/`s`/`d`/`a`. `0x7F` is Backspace/Delete. `Expose` (selected with `0x22800F`) triggers a full redraw and `FocusOut` (only after a `FocusIn`) pauses the game. ## Running it in MAME @@ -105,6 +105,7 @@ harness/mame/build.sh # rebuild the vt1200 subtarget (JOBS=4 on small h Then: ```sh +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 @@ -129,9 +130,9 @@ which copies the sections to their linked addresses and calls 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 +move DIAG: loops=348 events=19 focus=00400001 quit=0 +panels DIAG: loops=338 events=13 focus=00400001 quit=0 +quit DIAG: loops=292 events=11 focus=00400001 quit=1 ``` `focus=00400001` is the window this process created (it is the only one in the diff --git a/apps/termtris/build/termtris.coff b/apps/termtris/build/termtris.coff index cf4275a89a324a6c4360adfeba20763154b83f28..0889510d5cd12f8d8a1e1059e9102341589b57a4 100644 GIT binary patch delta 15 WcmaD+_M(h!0s{*})b5RJN!9>01qHwW delta 15 WcmaD+_M(h!0s{*}i0nqTBx?XM%LN<& diff --git a/apps/termtris/build/termtris.map b/apps/termtris/build/termtris.map index 78e98d3..bd21a62 100644 --- a/apps/termtris/build/termtris.map +++ b/apps/termtris/build/termtris.map @@ -1,7 +1,7 @@ ***************************************************** TMS340 COFF Linker Version 5.01 ***************************************************** -Wed Nov 11 17:58:44 2026 +Thu Nov 12 05:12:58 2026 OUTPUT FILE NAME: ENTRY POINT SYMBOL: "_start" address: 018a0000 diff --git a/apps/termtris/evidence/termtris_move_3200.png b/apps/termtris/evidence/termtris_move_3200.png index 1da3486239f34f97e62228745559de38b31aecfe..49e895aacc3eacfa0a3bf4796b99b28054c2ab46 100644 GIT binary patch delta 10727 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 delta 10900 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 diff --git a/apps/termtris/evidence/termtris_panels_3200.png b/apps/termtris/evidence/termtris_panels_3200.png index 586a0a16979d2abdb0c946a0df98c2d600303425..87af787c4336b95c006a3b36f829c8485c31c2b5 100644 GIT binary patch literal 22474 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 literal 22396 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 diff --git a/apps/termtris/evidence/termtris_quit_3200.png b/apps/termtris/evidence/termtris_quit_3200.png index 098ab58ebd90278d98366160625b29c0b94e38d7..decc0bddbecac0b273b5d5762c37e9d0ac3e5307 100644 GIT binary patch literal 19497 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 PQ$SeQ94a_)`iK7uI6*fc literal 19407 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 YgV)+DzKH!1%u_*H+wCpbed7E70MbA&RsaA1 diff --git a/apps/termtris/verify.sh b/apps/termtris/verify.sh new file mode 100755 index 0000000..5d0420f --- /dev/null +++ b/apps/termtris/verify.sh @@ -0,0 +1,107 @@ +#!/bin/sh +# verify.sh -- one-command acceptance for the VT1200 termtris port. +# +# apps/termtris/verify.sh # build MAME, then run every test +# apps/termtris/verify.sh --skip-mame # use the emulator already installed +# +# Exits non-zero on the first failed check. Each test's full output is kept in +# apps/termtris/build/verify_.log. +set -eu + +DIR=$(cd "$(dirname "$0")" && pwd) +ROOT=$(cd "$DIR/../.." && pwd) +M=${MAME:-/home/arti/code/k-space/mame0289/vt1200} +ROM_MD5=b46a3e442ccb86134d2356bd70d22fd2 + +skip_mame=0 +for arg in "$@"; do + case $arg in + --skip-mame) skip_mame=1 ;; + -h|--help) + sed -n '2,8p' "$0" | sed 's/^# \{0,1\}//' + exit 0 + ;; + *) + echo "verify.sh: unknown argument: $arg" >&2 + exit 2 + ;; + esac +done + +mkdir -p "$DIR/build" + +echo "== ROM checksum" +got=$(md5sum "$ROOT/vt1200_v22.bin" | cut -d' ' -f1) +if [ "$got" != "$ROM_MD5" ]; then + echo "FAIL: vt1200_v22.bin md5 is $got, expected $ROM_MD5" >&2 + exit 1 +fi +echo "ok: $got" + +echo "== 2ine toolchain" +LX="${NINE:-/home/arti/os2-work/2ine/build}/lx_loader" +if [ ! -x "$LX" ]; then + echo "FAIL: no 2ine lx_loader at $LX (set NINE=/path/to/2ine/build)" >&2 + exit 1 +fi +echo "ok: $LX" + +echo "== emulator" +if [ "$skip_mame" -eq 0 ]; then + JOBS=${JOBS:-8} "$ROOT/harness/mame/build.sh" +fi +if [ ! -x "$M" ]; then + echo "FAIL: no vt1200 binary at $M (set MAME=... or drop --skip-mame)" >&2 + exit 1 +fi +echo "ok: $M" + +export MAME="$M" +fail=0 + +run() { + name=$1 + shift + printf '== %-9s ' "$name" + if "$@" >"$DIR/build/verify_$name.log" 2>&1; then + echo "PASS" + else + echo "FAIL (apps/termtris/build/verify_$name.log)" + tail -20 "$DIR/build/verify_$name.log" | sed 's/^/ /' + fail=1 + fi +} + +run smoke "$DIR/tests/smoke.sh" +run fmt "$DIR/tests/fmt.sh" +run render "$DIR/tests/render.sh" +run input "$DIR/tests/input.sh" +run play "$DIR/tests/play.sh" +run play_keys "$DIR/tests/play_keys.sh" + +echo +echo "== play acceptance" +if [ -f "$DIR/build/play_move.log" ]; then + for mode in move panels quit; do + grep -a -h 'DIAG: loops' "$DIR/build/play_$mode.log" 2>/dev/null | + head -1 | sed "s/^/ $mode /" + done +fi + +echo +echo "== evidence" +ls -1 "$DIR"/evidence/*.png 2>/dev/null | sed 's/^/ /' + +echo +echo "== ROM checksum (after the suite)" +got=$(md5sum "$ROOT/vt1200_v22.bin" | cut -d' ' -f1) +[ "$got" = "$ROM_MD5" ] || { echo "FAIL: ROM changed during the run" >&2; exit 1; } +echo "ok: $got" + +if [ "$fail" -ne 0 ]; then + echo + echo "verify: FAILED" >&2 + exit 1 +fi +echo +echo "verify: PASS" diff --git a/harness/mame/TERMTRIS_REPORT.md b/harness/mame/TERMTRIS_REPORT.md index aaa78d2..88a7688 100644 --- a/harness/mame/TERMTRIS_REPORT.md +++ b/harness/mame/TERMTRIS_REPORT.md @@ -94,9 +94,9 @@ The run is `VT1200_SKIP_LEVEL1=1 LOADER_AT=2500` with the loader payload at bit 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 +move DIAG: loops=348 events=19 focus=00400001 quit=0 +panels DIAG: loops=338 events=13 focus=00400001 quit=0 +quit DIAG: loops=292 events=11 focus=00400001 quit=1 ``` `focus=00400001` is the termtris window, and `quit=1` is the game's global after -- 2.47.3 From 5dd01629b9abdddc77f7102efa8e0c780ff5d67d Mon Sep 17 00:00:00 2001 From: "opencode.energia.arti.ee" Date: Thu, 12 Nov 2026 05:19:52 +0000 Subject: [PATCH 14/18] 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. --- apps/termtris/HANDOFF.md | 11 +- apps/termtris/VT1200.md | 16 +- apps/termtris/build/termtris.coff | Bin 15208 -> 15186 bytes apps/termtris/build/termtris.map | 146 +++++++++--------- apps/termtris/evidence/termtris_frame3300.png | Bin 19499 -> 19434 bytes apps/termtris/evidence/termtris_move_3200.png | Bin 19524 -> 19584 bytes .../evidence/termtris_panels_3200.png | Bin 22474 -> 22424 bytes apps/termtris/evidence/termtris_quit_3200.png | Bin 19497 -> 19441 bytes apps/termtris/src/vt1200/main.c | 2 - apps/termtris/src/vt1200/x11.c | 2 +- harness/mame/TERMTRIS_REPORT.md | 10 +- 11 files changed, 97 insertions(+), 90 deletions(-) diff --git a/apps/termtris/HANDOFF.md b/apps/termtris/HANDOFF.md index 5a5ad87..19f2d7c 100644 --- a/apps/termtris/HANDOFF.md +++ b/apps/termtris/HANDOFF.md @@ -91,12 +91,12 @@ Last run here (older MAME, VR150 geometry): smoke PASS type=2 keycode=194 lookup_n=1 ch=61 ks=0061 fmt PASS six formats exact render PASS flus=464C5553 progress=00FFFFFF -input PASS bytes 60 61 9B 41 9B 42 9B 44 9B 43 30 0A 7F 71 70 - (60 is the Expose redraw, then a/up/down/left/right/0/Return/Delete/q/p) +input PASS bytes 61 9B 41 9B 42 9B 44 9B 43 30 0A 7F 71 70 + (a/up/down/left/right/0/Return/Delete/q/p) play PASS DIAG: tick=887 quit=0 -play_keys PASS move DIAG: loops=348 events=19 focus=00400001 quit=0 - panels DIAG: loops=338 events=13 focus=00400001 quit=0 - quit DIAG: loops=292 events=11 focus=00400001 quit=1 +play_keys PASS 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 the process created; `quit=1` is the game's own @@ -113,6 +113,7 @@ window. | the game exits immediately at startup, sometimes | `XOpenDisplay`/window creation while the display server settles | `main()` retries 40 × 50 ms, then returns 1 | | window paints once, then hangs; a debug marker stops mid-flush | per-cell flushing overflowed the X socket / `XSync` per row deadlocked the client | flush repaints whole rows (fill + one 80-char string + inverse spans + one `XFlush`) | | playfield is a white block with invisible pieces | the 1-bit raster cannot show colour | backend drops colour and uses bold (`0x80`) as reverse; pieces are `[`/`]` glyphs | +| blanked well border with stray `X` tiles at the top | `ExposureMask` + map-time Expose runs the game's full redraw before it has a piece | do not select `ExposureMask` (current code); if repainting on expose is ever needed, guard `full_redraw` against `cur_piece < 0` | ## Layout diff --git a/apps/termtris/VT1200.md b/apps/termtris/VT1200.md index 18fb0ab..cea1d20 100644 --- a/apps/termtris/VT1200.md +++ b/apps/termtris/VT1200.md @@ -89,8 +89,14 @@ 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` (selected with `0x22800F`) triggers a full redraw and -`FocusOut` (only after a `FocusIn`) pauses the game. +`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 @@ -130,9 +136,9 @@ which copies the sections to their linked addresses and calls Acceptance values from the last full run (`play_keys.sh`): ``` -move DIAG: loops=348 events=19 focus=00400001 quit=0 -panels DIAG: loops=338 events=13 focus=00400001 quit=0 -quit DIAG: loops=292 events=11 focus=00400001 quit=1 +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 diff --git a/apps/termtris/build/termtris.coff b/apps/termtris/build/termtris.coff index 0889510d5cd12f8d8a1e1059e9102341589b57a4..5305e97fea9674c3e6e649345aca03011ec28e11 100644 GIT binary patch delta 2808 zcmaJDZERE5_1Gn2_fXm!JDdnY_L3NLQ6u7NzzoC zDlnbajOU4kT&Y{vAA2l{GS({d=1r42Hd$Q@%u$;(T`PWcS;nAo=!m=xs+CI3&UsGk zP_aGfew@#H&bjBD`_h9mgZ5#fCNE#Q+dWu8NP-Zup17!v-lby8d3#x<*h0xPU{`;F znh@Vp$HK=32oYOiz!{^{>p=2+?+FgXW-Xj(J^#tw#x^raL1IYDRDtnH5m=bn@YxSB_&H8&w zewaU5Qp0?AscSNr^je3sf$zw4Tr0wSFjdEFs;|&T*psR%1L}Q?kC2HGaF30tyrAiA zm4nL|*`lh93w_2c>hi6`A<<1HZnDX`BiUid7-O^$02Ft#^g{ymGhRDnGQIx=lB_M?<-dbcWE4q5 z_QjF0hKUh06s<|~OG`_qzn%QKHt+|T`cnGMt@=-w3NFK0zf;a)TAi1gSe4FZkGo^^ zD&I+(l4IKFS((1twDzrS7>;pvL|5;P#3iZ;&Zfq+@+FzdFMx?7#t#I=qj*B1={5r} z3yDeH8IBXLv3C8VR(}BF=DCcKn_w&4;sN%$etjXHH#MrYEslg)J7yo7*AJP{`*H0Y zSgT55MA)f-&RZs0*D43P?o)4aF>b&yZpl9F3wu?nmqZqoZZMHYSw)X^r(MIg_(gduLSY{0A<8=7yp`Tdn> z<;??@8>afwz(4R@A6Ovli&1^BW)d64Gt1Syx6o6Kjpj>m_9Wk*>i}yNxHAW975MP7 z)XRnMLzExl1`NAD`|I&*Ce{I)0FfC$J;!fdH2o0t8JV0bCOJ(ZD9W^VEeVijglG8 z|ne7-Js&4zXyAbrMkNSU(76{p998S>`*n&W~%Mn9O`jW zffd&DaL{{C)qw8(>=rC=|Br3yMvLCq;)LHD_R$s@7>Bk#S%l9ws^gS@q%$zXyM1S? z6aNCN+tB<+wzZjvL|%( z_Z<#*5)x|2V+Q)3>WvU=hFKm|4IYztX63|=-mc#Mt^or6e_rc6h|kfox~w*`5)qDc z^n<#{w+LqV%8>(o$GgG=Ux8)J(WA$o?SF<~tH@&T$Pw5kKV{EvcVe()Sm1Wv+}`NH ztj)7sM>h3p%xxsHsyf#}VdHAN0m6V;o|i2F4Aw&)Qw|u+{yb(oV6X(U81%gN#9)`a Qn@!Zgy|pA}>R#gh3j-XvL;wH) delta 2804 zcmaJ@eQaA-6~Fh{>C202=cP_E+iBxByN=s5ar4onp@YrKN9)XWFfiDK~M0!8(e=5|gnBq!CQVh{KjBrE4^GL*bm~ z*sf89rFYLgpZDH#f9Lqlo=lxIog-@U#4m3TJYPddln}C!SZN`>O-1*4Q&p|#rX&H< zyML0J5XZs8{=+8-5#2$s4AR5~LK@Bw9p%7nuACE%=U3*QiJXyXUuOTBaF9JT`;b!s$smnIyCjgnB6eSJD&~H3El@|Z`{fBs)uMB`+{owrfm@YcY=>B zh0=$_)vgJ{WDd1F^=dZ%xyBz&g!7G`?TRG__X?@gvF?F`raj%n1_qL>@K=k64=G&J-OQ8u~ zf|J^X{1s=$dE-*Z&$Wx2N6Q24KW->mX+?Sn;iiV{+#V_~g{19DZ{z&TxPSzF3Ud)PW%B?oD& z(mhz7R7^)bU)gNeka(CP^uXd9x^G!N%0>xNO%{nMVdVWagy@)|_<}hGER9tI!lpwh zA*m=QJiI2Lz&Od?D8BC*rz=RebQ*s%Gwy#=BA8B0O`snG73{T%li*1{Kn~Z(YzUsS zoziO#Sk}Q(f=|(1@)T$SYAl9yzA#qTntw)jw}0`qnepd5wvI`5P}JjrM?|Ta=z??; zg#9k#4FD<(sni$Z|I+Au`+Lu04GzQRbBBNgPK&LZmX%!~1`{2S5>LYvc9#CN@~4Hi;WTXHQADC6i%i6EFQ7QW4wRbBSe^N}?1U=t0S488!3g_dY2#{OW<#<8+Z61E0d z#vs#|1yY3V;Y7h39-W4)cN?HQ+t^&0s}$=`<0*ssLwIB2MK9Y`ZsL44_NDSeke2AH zcBe`NNEF@OdC2jjPq>@_)y#KkPaFPN%| zd^z@YU?Vwp#ng*F-4$I(UaF9QM5ileIWc)hrmaSR8MFbVq9T=_=ExFTs@%dzH(!be?`Sa!B2cUs^2IE0vkdMX3z%C`!+1P%mos&Lh zKbEXCz^+R_qLJv}{q>X!2+-LfiATm)bTjSjsQ%`&>Ld zGmh2gbVc8(I-~B2yd%@c|0+{5C({?<^Zrey85zny0v>6`CY+S1>+kvBGVm8~$kh2R zi){O{BlWIKhyNkd5%{F{WEzIg`A?Z9;2W8jshCRZ!$744=B%j%wDr^k;Vr>FsJU3+ zt0ZI>8>`#Jep_or;l=G%rg02%J~tb2Snx)^?--!VjNfj>1lMdI;G9pfRILROcNMts z18hs(03eCg4MIl=)q|*w9RYGqv7-t0as43n*#3qA4YoZB4;~d2*|CNLc=6800bKZ{ zMhiS`SfViu9>;d$zcjP1|o<2A ENTRY POINT SYMBOL: "_start" address: 018a0000 @@ -20,28 +20,28 @@ SECTION ALLOCATION MAP output attributes/ section page origin length input sections -------- ---- ---------- ---------- ---------------- -.text 0 018a0000 00014b10 +.text 0 018a0000 00014a60 018a0000 00000150 ENTRY.obj (.text) - 018a0150 00001e60 MAIN.obj (.text) - 018a1fb0 0000ce50 GAME.obj (.text) - 018aee00 00000d20 SCOREDB.obj (.text) - 018afb20 00002a40 X11.obj (.text) - 018b2560 00001640 COMPAT.obj (.text) - 018b3ba0 00000210 XGFIX.obj (.text) - 018b3db0 00000450 rts.lib (.text) - 018b4200 000001a0 rts.lib (.text) - 018b43a0 00000230 rts.lib (.text) - 018b45d0 00000150 rts.lib (.text) - 018b4720 00000120 rts.lib (.text) - 018b4840 00000110 rts.lib (.text) - 018b4950 000001c0 rts.lib (.text) + 018a0150 00001db0 MAIN.obj (.text) + 018a1f00 0000ce50 GAME.obj (.text) + 018aed50 00000d20 SCOREDB.obj (.text) + 018afa70 00002a40 X11.obj (.text) + 018b24b0 00001640 COMPAT.obj (.text) + 018b3af0 00000210 XGFIX.obj (.text) + 018b3d00 00000450 rts.lib (.text) + 018b4150 000001a0 rts.lib (.text) + 018b42f0 00000230 rts.lib (.text) + 018b4520 00000150 rts.lib (.text) + 018b4670 00000120 rts.lib (.text) + 018b4790 00000110 rts.lib (.text) + 018b48a0 000001c0 rts.lib (.text) -.cinit 0 018b4b10 000012d0 - 018b4b10 00000050 MAIN.obj (.cinit) - 018b4b60 00001190 GAME.obj (.cinit) - 018b5cf0 00000050 SCOREDB.obj (.cinit) - 018b5d40 00000050 X11.obj (.cinit) - 018b5d90 00000050 rts.lib (.cinit) +.cinit 0 018b4a60 000012d0 + 018b4a60 00000050 MAIN.obj (.cinit) + 018b4ab0 00001190 GAME.obj (.cinit) + 018b5c40 00000050 SCOREDB.obj (.cinit) + 018b5c90 00000050 X11.obj (.cinit) + 018b5ce0 00000050 rts.lib (.cinit) .bss 0 01a00000 0001ec20 UNINITIALIZED 01a00000 00008000 ENTRY.obj (.bss) @@ -64,78 +64,78 @@ address name address name 018a0000 .data 018a0000 .text 018a0000 .text 018a0000 _start 01a08a20 _blackpf 018a0000 edata -018a3110 _cleanup_game 018a0150 _now_ms +018a3060 _cleanup_game 018a0150 _now_ms 01a083a0 _dbg_events 018a03d0 _vt_rand_seed 01a083e0 _dbg_focus 018a0420 _sleep_ms 01a083c0 _dbg_last 018a0580 _vt_poll_event 01a08380 _dbg_loops 018a0ef0 _vt_handle_event -01a08400 _dbg_rev 018a18d0 _wait_display -018b2560 _fflush 018a1910 _main -018a86b0 _game_input 018a1fb0 _init_game -018a1fb0 _init_game 018a3110 _cleanup_game -018a1910 _main 018a3300 _update -018b3db0 _memcpy 018a86b0 _game_input -018b3f50 _memmove 018af160 _read_scores -018b4200 _memset 018af4e0 _save_score -01a089c0 _monochrome 018afa70 _print_scores -018a0150 _now_ms 018afb20 _vt_root_window -01a08a00 _onlyascii 018afc00 _vt_x11_init -018afa70 _print_scores 018b03b0 _x11_clearscr -018b25d0 _putchar 018b0570 _x11_reset -01a08960 _quit 018b05b0 _x11_cursor -018b43a0 _rand 018b0610 _x11_setcolor -018af160 _read_scores 018b0730 _x11_setcursor -01a08a40 _rotstep 018b0980 _x11_ibmchar -018af4e0 _save_score 018b0e00 _term_putstr -01a08aa0 _scr 018b0fa0 _vt_x11_flush -018a0420 _sleep_ms 018b21d0 _vt_x11_term_init -018b2de0 _sprintf 018b24f0 _term_init -018b4500 _srand 018b2560 _fflush -018a0000 _start 018b25d0 _putchar -01a1cbe0 _stdout 018b2de0 _sprintf -018b45d0 _strcmp 018b3ba0 _vt_install_xg -018b4720 _strcpy 018b3db0 _memcpy -018b4840 _strlen 018b3f50 _memmove -018b4950 _strncpy 018b4200 _memset -01a1cb00 _term_clearscr 018b43a0 _rand -01a1cb40 _term_cursor 018b4500 _srand -01a08a80 _term_height 018b45d0 _strcmp -01a1cb80 _term_ibmchar 018b4720 _strcpy -018b24f0 _term_init 018b4840 _strlen -018b0e00 _term_putstr 018b4950 _strncpy -01a1cae0 _term_reset 018b4b10 cinit -01a1cb60 _term_setcolor 018b4b10 etext +01a08400 _dbg_rev 018a1820 _wait_display +018b24b0 _fflush 018a1860 _main +018a8600 _game_input 018a1f00 _init_game +018a1f00 _init_game 018a3060 _cleanup_game +018a1860 _main 018a3250 _update +018b3d00 _memcpy 018a8600 _game_input +018b3ea0 _memmove 018af0b0 _read_scores +018b4150 _memset 018af430 _save_score +01a089c0 _monochrome 018af9c0 _print_scores +018a0150 _now_ms 018afa70 _vt_root_window +01a08a00 _onlyascii 018afb50 _vt_x11_init +018af9c0 _print_scores 018b0300 _x11_clearscr +018b2520 _putchar 018b04c0 _x11_reset +01a08960 _quit 018b0500 _x11_cursor +018b42f0 _rand 018b0560 _x11_setcolor +018af0b0 _read_scores 018b0680 _x11_setcursor +01a08a40 _rotstep 018b08d0 _x11_ibmchar +018af430 _save_score 018b0d50 _term_putstr +01a08aa0 _scr 018b0ef0 _vt_x11_flush +018a0420 _sleep_ms 018b2120 _vt_x11_term_init +018b2d30 _sprintf 018b2440 _term_init +018b4450 _srand 018b24b0 _fflush +018a0000 _start 018b2520 _putchar +01a1cbe0 _stdout 018b2d30 _sprintf +018b4520 _strcmp 018b3af0 _vt_install_xg +018b4670 _strcpy 018b3d00 _memcpy +018b4790 _strlen 018b3ea0 _memmove +018b48a0 _strncpy 018b4150 _memset +01a1cb00 _term_clearscr 018b42f0 _rand +01a1cb40 _term_cursor 018b4450 _srand +01a08a80 _term_height 018b4520 _strcmp +01a1cb80 _term_ibmchar 018b4670 _strcpy +018b2440 _term_init 018b4790 _strlen +018b0d50 _term_putstr 018b48a0 _strncpy +01a1cae0 _term_reset 018b4a60 cinit +01a1cb60 _term_setcolor 018b4a60 etext 01a1cb20 _term_setcursor 01a00000 .bss 01a1cbc0 _term_type 01a08380 _dbg_loops 01a08a60 _term_width 01a083a0 _dbg_events 01a1cba0 _termenv 01a083c0 _dbg_last 01a08980 _tick_interval 01a083e0 _dbg_focus -018a3300 _update 01a08400 _dbg_rev +018a3250 _update 01a08400 _dbg_rev 01a089a0 _use_bell 01a08960 _quit 01a089e0 _use_gfxchar 01a08980 _tick_interval 01a0ce60 _username 01a089a0 _use_bell 01a1ca60 _vt_dpy 01a089c0 _monochrome 01a1caa0 _vt_gc 01a089e0 _use_gfxchar 018a0ef0 _vt_handle_event 01a08a00 _onlyascii -018b3ba0 _vt_install_xg 01a08a20 _blackpf +018b3af0 _vt_install_xg 01a08a20 _blackpf 018a0580 _vt_poll_event 01a08a40 _rotstep 018a03d0 _vt_rand_seed 01a08a60 _term_width -018afb20 _vt_root_window 01a08a80 _term_height +018afa70 _vt_root_window 01a08a80 _term_height 01a1ca80 _vt_win 01a08aa0 _scr -018b0fa0 _vt_x11_flush 01a0ce60 _username -018afc00 _vt_x11_init 01a1ca60 _vt_dpy +018b0ef0 _vt_x11_flush 01a0ce60 _username +018afb50 _vt_x11_init 01a1ca60 _vt_dpy 01a1cac0 _vt_x11_progress 01a1ca80 _vt_win -018b21d0 _vt_x11_term_init 01a1caa0 _vt_gc -018a18d0 _wait_display 01a1cac0 _vt_x11_progress -018b03b0 _x11_clearscr 01a1cae0 _term_reset -018b05b0 _x11_cursor 01a1cb00 _term_clearscr -018b0980 _x11_ibmchar 01a1cb20 _term_setcursor -018b0570 _x11_reset 01a1cb40 _term_cursor -018b0610 _x11_setcolor 01a1cb60 _term_setcolor -018b0730 _x11_setcursor 01a1cb80 _term_ibmchar -018b4b10 cinit 01a1cba0 _termenv +018b2120 _vt_x11_term_init 01a1caa0 _vt_gc +018a1820 _wait_display 01a1cac0 _vt_x11_progress +018b0300 _x11_clearscr 01a1cae0 _term_reset +018b0500 _x11_cursor 01a1cb00 _term_clearscr +018b08d0 _x11_ibmchar 01a1cb20 _term_setcursor +018b04c0 _x11_reset 01a1cb40 _term_cursor +018b0560 _x11_setcolor 01a1cb60 _term_setcolor +018b0680 _x11_setcursor 01a1cb80 _term_ibmchar +018b4a60 cinit 01a1cba0 _termenv 018a0000 edata 01a1cbc0 _term_type 01a1ec20 end 01a1cbe0 _stdout -018b4b10 etext 01a1ec20 end +018b4a60 etext 01a1ec20 end [77 symbols] diff --git a/apps/termtris/evidence/termtris_frame3300.png b/apps/termtris/evidence/termtris_frame3300.png index 54cbf26993349c5ac36bd8b266d7a82321ae8587..92302fb5c19808d1849bccf7cd06093b8cf6ba85 100644 GIT binary patch delta 249 zcmV5kJZ)9fXjt(@+Q2)c{zZOL*?V83G&B?yd z?WL>q*3rn!%zb?S1hf44uSJncyQZ;4bK1EterMPG&O41$x=I(9Yi4Hd=0KwicKVtB zO1Jm<+v*n0Y5%_Xo&EAV?=(*N;g`1Lzh!3Ti;gnK1pm94pg5&n)7YXpeM{XJpTGUo z7R@Q&1^Gs1X71=fqYU*weEw@um_C17-J&_|-xt5LUw-GE#wkDimH?j}%*?O#{S(ac z=f4(3D(#xa7R_nrzWAM8^OL*>8Iynu8U&Ce1uU}y2(&{8rmmI7y{?PslfFxB6RC?x delta 357 zcmV-r0h<2mmjSDo0gxmCk+CJ427eE#-mR?zIz~^NS&C48xz0B-Gjm4=8fB>e;qzaM zB9(T{)|SSvmer#}OPa?Q-l-5h9?!A?K(U+MNfe_P$M{N0ILTcY*2nRo7sCGcD5-hc1B(>Uda zU)qxYmYJC^I?5aq{O@Lh;*@qxV@rK=bxX7!H}lth@%h_NZPA?aU65~NX6B9#G|Eu_ z!{@&ih3WIR)h$c5`|<}atA}!!9yjyOeX#_73*GyjcN(Ys@LK|Wb}%!)*7r{^%b)*R z6sfdpE^dj|i)W7f7W(!(??g0C=_*}Zu9=y+n*)t9*y(5fD@7>ZL91IDzbYK3C(gX= z*ca01(p9=~1z=|8D~>Y91pm94pa)g&*46X%lYL8V D?`Xdw diff --git a/apps/termtris/evidence/termtris_move_3200.png b/apps/termtris/evidence/termtris_move_3200.png index 49e895aacc3eacfa0a3bf4796b99b28054c2ab46..b05ec08048f8ecba398a27109a9a12ace133a0ce 100644 GIT binary patch delta 10729 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 delta 10855 zcmc(FX;_n2*R?|{ZK>E+G_5iyf&&Oq5fOrxf4*^r+t3B*LPjtAO3LebIzXDUVEJ! zzcTaB%QG*S&%9l1aeWT-;-{v{Sn=`nm*;HVzkY^nmj4_Wc;xN(H)CYzWOeBF+=imo zwh}4zknFI*i*vR}y|Gh`l`)sP(2y-n?HJ!g~yAIB6_2LI~enT4wRFi(tGldR4jl-!Q9FlD_cok!7+JSI?UH;-e zKOsNz**vzzgCIRyY3QWtlbb3z-6dm7Wq*I>;i_Z(wGlVM*;0?5i$x99eOIqsx#sX< zC7DdKDlOl|F^Fp`}ym)%Z_xSem?*f zc>M-R>S9?LSFS8}e6bQcIgg_8Gsi4-_Z?6LDHhn;+DoPL&q)OAg&42e|FE5yyDg6z z!}O_<8|@1uoVi)Q_JQWeayR0vOk9yPt-ikF#fhH>gwTYlIjVrmFWQ7r@5nOs-y3@1 zyl+@%$yuX3N_q4ev-cuvR3o=ve^?@1eQ{I*kqFt0>cIXBTVFpwn@toXK%3t8JU zqsOr2c$r5Ymcej2;CK%?7XLis=176^dU$VZ%vb^^TNZ>970h3Q%}cGb1GkU< zeY+!SvL1@Pacv%3?i9pNYWEfIV4W;0JN%2@c(uAHIClFdCJwi`U4pP>+*=H;ZJpB5 zpSdM}W_NP>8e8_`DUaFsI;o^=k8lTxlj|PEYx)>&l2j@ck`=1(*wiO6$++u|+pcv4 ziPS;j=rWSo=T4Ag+Q4hp1T)lUUY$4hj^h)K{t%njtQ@@@U2nG(!s+2cWm`&QRBvua z?t1Pd8ewZImdii?SuVxZrLmkf&DK;7O45MhX7W1{`$ zqII0?i&ocgL>+MeC6&WyhAeR|WJDJLR|JisOMobQ?B=N z-k9vNLzI(!1vg97#$Qo!R6%jHNXb zlGM(mPd_s=a3gNcENYN*N38%%^ph^kSxma2XSz3X%d~kaPLW(&(oy7ygMt004c2e* z)jS4;acV$X-ekS#)d+qBrR;+-Lh?A`^LMQe>s5YJSJR?RYtCAHCxXJBY+RUD(DJD~ z4jj}&nyY6EfjlMzHLEzNG;cx$$9|n{^p5~tsG2uYT&mfnxaXXOX-`=%dO$3AFKg@T z!&OsgKr}3gxukac!iSJ8=>Yc$%BRTb=9n8>e1n@qR(T90pJNpDpy0?UXDz{uNI(7l zi(MB=jqGE7-_C49%)q4JM);xTeNbQi)7N@7GkH-REnLqv&*U#xV^VyWTX4Gy+~`R1 z@*q#Da-Qn9-F1<(b)d&^W#{_#dws=-xx^}SB6eXNx5jZYx_#Z_D%zpfLTT^3fb2bW zkybF~Nly~C=cZ8Wg$vUL;>W7}`OW&4HqAsyLvTrSx*^-IB(xJ!&-SIPfR?bI!f2uT z9cNh3ZOLG|98hH7NBTnkaP`MNbW`|F9a8ti?`>^p)4Y1mo!MRRbq6l{To^<8$MI?j zLAWrih2ag_V{uKLW}~97A>&T86MTq%CwxZ!#!|+gZ$YN5 z2><3UTvp-|?uKYug}(E=oIM|mjJgfDwK$mgcv5F6SL{v9R-B;R3biFXgAqX$w%hQQ zqC_U!e9L2!!)mEqA{;0)VAu6|R%+TOlMHMc*Q9tmt{FQoR0NXrB-cO!7IDrGzCRM2 zh$}jscsFq=F@g@!-^e;s1iP!GJX;F>Y!jl&wwKpJVlVPb-_M@VO}0!}b~%*Kb>Yi1>S%Wb+U zJn>TXY;G>(|Q*tj?=)@DN`X6(sXjdR{{JwykmP50-4E(sK&j zlg4TjE}E2R9&~4e_^URZOYqW0bXih{`KiKP35KcJ=56>U}-i7rKPL!0!cz#=?i@UeKP1t=v_3oe7ch9izBP?hB~*)M$D`Hf>%m z`HWHWYau;FuRq_?Qw$rcQe~LCPg)CMCN4*O!pR)^(kd5g> zGpnFmstYU2#M7JnCd+vG%Hf&O?CYkjsphK=;(TpyJPXR4bJ3uJk-zh?o&*I)Q#~>3 ztJ1rnaiEWkoiagU^N@vUPqMhH=f0IJjn|xTKMv|_47AQD!g?z=UAT1XjAOm7r}WMD z6a%M}A!n>rs=kL?$U)r7&8!)2Gu;ITuE!elBQHA4X+a#n(_}j4tfo8z)i0m5)7}dz9OAg9P14Y7{UB-L_n}H(;$WMYr??~%esy}!3p$=L?SPK7#-OX-x zOT44{GhkL>-@Rg(0H)6NYdAILnX@dvg%?!MlMZ$W8l~5t_w7#(_8q3Xo6r5@-)>mU z4ox4t#~5#ALeysA$S$lEHqUdlm$_^edab_KyG9t2q2!tW*?{qJjqq0f0uu2m=&a%{XVqbhF9*?&Aj=C{d& z8cNFe$_UcZ8HbV{AN1bz`sgw(S66)$;$npWjGh{m87ndEhjW{k#= zP9l#$HijDW5W9VscoWwkp0j7P*%Xe5vOZU0C7evv?8`;QZ9ce*2wQgBOm;1CeDJ&qJJjjM}%v0 z`>#Gs*)~lhU3#&MSmS$|utna$!hk>ALU4rr3ttx)A4;DI<3z~^XV)t8hOTGu2mIUN z6bDY0AA*U*Uy{L0QY4Scs~my1+X1?8p<9UmUp+?qEF9}ISTJJTfp{3d^>NRN2^9X& zfF+(lGb@OcoxO78BRYUxsm3&hJem-X;sJqZT9BF>NKzt~LzO=6k+->92q)oG$IrZw zTib`5Vcsd`wC9$3yD(sl}i)c~|RaV2ig+R@io>J8+}pB@X*c7dD8pbHGv=KRz! z634mKCx%S0RX;jT*&{eJk>l3a3>Pd3K#h`lFeBzRjj@W)il)S9gHJk-SOf(_4rT*k z)FAJwNYRn%_xBd)jpwW}t}qY&<*$%{r8-|LAfw!?lF}H@_kh+e^F+cph6E$?>b+HG*~P2wUqfZR?tX$avka2f`zuz z2qUye%|$f@z2eDB6S>{*D@eL5X?rf1vPxYJz&NASH^c;yelTAEjW!s)xmq>(X-Y0* z`m31zy1Ib7e_}8daU?ir46%~P@u6(Rc|K5N4@3`P%^>?^ zHCc{_vr8a&U{=MXo6Z@hXe1G$rRSZvyiNmFX`Q$e1~bng(dqd+f@Hn$@RzMVND-j#MlA>>H!isbkI#-dLrXGAprQ1LCyypmsb68TtT?wO zNmN#}<23XL&d74s4B-+73sFr_!?JO7337}goY;esILW!8^jN{ls87-(KyAj*y2IMg zP=yecKc9r_*Yb^BQ<)KSLv@%=@Asd#8@|54fz^}l#8p4MqxhjTy8PrxVlr+}oYj*w^jbQ24nOwU03Pc6 zaC(Js9SX4`kohzk1&`vUJiy)Imc{fLu`i?#P{%b)cw8MZj(?XB7wDXHVU)!lCN2GL z5v0rD#@mXLJ*qf|J5#Gv31fQBYRjN`mU?d{e0-M@ZG7K{Xey`XiCh$KhAFd@+-=bH zLHrUF!H{F>SsBX!m~XnAuuMz9ZWMDI9A&IXQS_y z^fskAk>P`=dUwq(egxTLHgV(GTMv+sOBO(zx_B-t_&I9<@yHl=Z+BS6epOx;JAq;H zPQ3~#;JF7MC_W|7GOx(*tv2hJm)^J*qCbTp`c>pt$Kdu~VQfRb+VBJn;g z)q@2z1r`#({s+{Lq3P#0dC{_nzV?zB(6`sk0E<($O*d|U)vc8+ok<(g1_rmdG!PIT z)I2}rh!Spvdp7;P;0BzZ53h3j_*3AC=Uja0)10@b&VQ@#|8OX62~f!`dK$6 z7O-@=Q-pc6C1gI8hl3$NpVNwVWUu!01zO~FK#LC)rceW_3G zmg<{hMY@=Ij;-1j(K}!Snj5fCcFZ3^PS#2wDh@$AA=4gvHk9fd+&vC+L12&7W*rwa z7*6H4+QGtAOkAE>Fk9zb=fxBQ9S8!>^>rJwu*JKF0gyGw_uK#CoJHL+>p+NG@Zw{LXi|g`0y2pZ<&Yv?UTzysjUTLxzn@wGQ=V!2{vitsp0ZjLORnJm z;s^@@1*R7uSO5EdcZ*vdMJs`{Q?U``1FbFvxyg=oTG?$}DK-}GFX9yKFdvHlw-~_v zh4gT#JL#~A`xlvw1@YGpYt8U9`4;l|t<1)1tZ`xyYz7N9Q??KhPh}WoFK4>3FWN);MdrHi{|Id(ws6Lv|G_ zyGCsKg&(E61^vkTjGL~AJ#)d9aeR17GGLW$Vr_bUh)S+URnJfVVeN*6{>P4dz`_4o zeeMuhk(kp*SzlE?nuG}1Q>?xa!_Z}{@S0OOh<>XUskDQM_hbBsY^i^Ii(`hkLl6b1 z_eZzN`=_LO!=sRy58sJ3 z<5zI|j862xv=l4iv|gl46Fo>8y?0u_0`bO~mc*W^qBb=t12~}ZcSl(d1jci2O^SSHx~w;nKYlJpl~M1UJL&%eVG1ogZ+p@nKm^N%<$5-a;K)pQNH zOzZ2mcvFn^ZSF0aw=`6i{Sh~G9W%px(W;*1($Feo1dCLDRY4#4!ax2~N3jJT{f~4U zfmm;i`C?}k;HCL6bpXuauf02lPD{Oc)2gZ0b7X?wYGBM4TK?3~N3sO0)xyu8;gT(P zn>+48LRa@dg4X!A=Z28m%*o-hu-o=WI<3i4N?dLm!n4mcnXbcOc zy8}F{{e2*Q!ey{0Yw>}FZ&;TnJxA#9T3FmPSv~DAFxP#yTPtszGa3+$EgQd5RHoc7 zM7s!K=!XN@QwMq0TRZ@|&POfcnRil%D7M^yw(LAP@3eCN7`mDbL!u=%j|@&dhFul}jLlIP!$2#E&vf#l}zyxbmzD zOVS0K21u2qz0k+pj`m?p`5@Hv0HQs)6>bm{ z(bJLox5@ZBhlYZoFX;tCpg&PWuxCizuNOj@BT^&2`qtIKRE4z17!Vg4c1duu{A@+X zu9K15rZC)u&fgeXHpDv@0yf<(B(!6altIN!Fjw*uCC;U|z)o8<;=Z6J+$ z+W9*GTBoC@ef0Fyv*bSUxF}*=6QDfLbn4>lt)@Z&NuO6E`JGb(FPb&uC+nfBWNV#e zF6{`#!<0H%Y8_uZ!p}}@RFmn?!A#DL7%Ze%^E-6_06zMwTkCz)1Q9O0bDvs*Fn;PGd>uatMya#Do_DYI{O0mcC7D1!y8=MN-_GCzODkC>!_e&KXm9X_&R zQW(lGPCk36XSU+CTO!bRI$AF|k>H**5Tv%kc>Mw7k|#Nb(k0{mVf>26yqgmKkx5K^ zfRlhjZS*XW*%+`!S+X$so9)N*<3RhL=+QrJeu{oCfPGIj$!iy}p{MGbK9Ii(<2za4 zY&DL{BM_VN9VHE=%0wcw;3%Yv^`Hla zwBhc)0U)Fv{Yadw&7APhEdq%x7^1PpRp+;Xx6~48d($=74FvU`Pf~_(xZ-2T>TIlx zW1e`2Z2auKWC`TRmUYz&3qt=x7+gxF+m-#)WhOf^rC|mb6qZ8A)=lM@w!K>PgD(dx zCxI|EjT|3<<4I<%e{L7dY@TGj@0_}fS>%7K#VnR~FTfpE7ylxT>TaXk6Qp$^{-X)O z>krbB)utpAau1Xl^h}^pv`?06WIKhZ_)ZqOm-B>dzjJVSnbasb*lx9^4)I=WT7r3F zWz#(W5*JdHE6=x8L>o0n>=UW0jlr8@zT$qD{Nx|5a$L4{MwZopN1!io%q{$Yd*_f8 zwz`RD9A{Fh`DRn{SZR91DngP7-f?}@^hdfrj;&E$P1$_Ib2j<-0>PRenA zOQ5|7Vzpm(u&k;#;x?;-**8I5&u1}EW!XU@>%k5-p^+mM*H@sA zU%jXq`OsP}RJzuzE46Z1~ybMDA-}G#4;T8EVb@j1Sj#p^D8psFX`AGe@0Gx=qr5Q1l zxZNwFgDK^83Y%$^grLgNVaa2bbNn%A4=nzH%mzh2b(KHe<3{M>=po8VGs`OPGhC5U$8ez^r zXWt5gkMw_5%%vh^n=I#G=m8d9_MK0&y=!&_SXSNVs{UrLbxj)lLyG{T7up{-j8?_g zQ6xc`G50?Rza-1AztnTzt*$4gXOWzd_E5RQQgh6v=!#6Ui^v@3ec%oCf%e71n`r2i z6}oD(tRy6MnWq4}P8^k&Szu0t7AI!hLZXd@jGN^@mKUV|S|{lIg8PlDb!n*^wtp3j$H8X9Q? zV!!E#?77DQL-$mjmhx~eHq2;x!Tml{a&^8?AP$<$m%4^uFP&>2|NHiPXGPbRQER?M zgDXhO3T2-IY)m)EVjT9Z486LA3rxfY zAF{+W^!Z`a@^ZXgeIvi`*FK}1Qcg4`5QJ)_rv-)?K&2M%h!MVhHm_Ezls$xi*PUX) zwi>lJxOB5HWc32FBU3V)2;=V2`OWs{DT4fAwq^%0x2}}A$lWDBy-0OD;1MwG3WWlP zJqo_9W-^~>jxB0x+GXh&8?!*~AVDkA+&k|{{)f+XuX7zQs-HmK0(pT_jJ{+(5Jf9` z;`OrNrWJY#%;CSVkHkwS{^{lx^^Rw*fnT7&LH|D+K4mxb|I6?xyTAIo;UuRGXa8R( z2FShWSRsS8%j1*Xf}`N45g+FS@oJ;+l{^}}+i4*Ru7qqyt|v*~@z~EbT##1pzYcEH z3KBERql^2rDMd67F7m+h5Orslh>VWB2Ki!20iSl_i003m3b=~I;9^*@Az|%IzG4U} z%rS|fh=p?~8hrH1GLofJ0BId+K17VUu`zV}jZX=)ahWiI;LW+}S~H@YNbrMtaV z=6o2LAOKBooHNao+D`v40cj8Ls z)EAnr=Cqx@2ITXOKV@4nUJrz#xFPy@{6k;%-+*9k9kS7%6%7ppR!5oDI-Y_1ZbuC< zcVizyU;?hU5NYkB)ob>J^A{6r_sLtj%> z!-2pH=E0>?p}6R9??!4`GiR)A9xpa`az*mHyqbCd$Uc$04ILf86U&PtvO@$X+x@OaMFFoP(!$C}5J#`?@LLce13KhT_GKH3D_?Cw(>$=4Gn_Xe{$(ZZb^4sCSO{rDvrgtQv5?N=9F;O38MV4096m_2*@n z4-n)!g&($IPZ5DLsZ7T2#S4~2FN~1BKH#fat|vgNFZfKKP~7lGIV^L}IwII#*DBFF z;!w2>PuYK9EoM^_?^V}#1ESgG{rs)a0GE#zbKD2-izYKXs_+;hvrG}`12v2!1B@CU zIh9%H-7ZRV_yWSWDGG$me)uqweB88DvjZhg6eX0YH|lAABAVMp$bh#<2b@DmosK^` zuv|b>38eD%j_n7|K*230FgW<@ec_MCjSm?dISk%6Fkw4Z{9nTV7X+*x{Qv*} diff --git a/apps/termtris/evidence/termtris_panels_3200.png b/apps/termtris/evidence/termtris_panels_3200.png index 87af787c4336b95c006a3b36f829c8485c31c2b5..0e67bdef2ce76e0fe832a612762286c675334328 100644 GIT binary patch delta 13985 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 CrC7=U literal 22474 zcmeIacT`i`*DkyP6%+*(m2;3{Ie>I4(!m-91rY@477&moHMBtN6|hiL2&j~ZG$r)V zL`5MOq!S--9+=dV3} zjs8$kA6U_K_fE*rR{EJtS#UiRfcCVA35LI&+teF4GwFJdAPEmwR52qm;q^Tx?JCZS zUjsgm=53)|MJf$aLiQRw+wCp}A1hw&g>19I4A~Oy;`=AyBampvdw%p+6!ZHNo)HD8 zKbdvA&ax>;DX*1YRmzWkNm)s?T?wvP8wLI<54<)%A=X>tHKu1ync>*keh{3|QV`OF z%45d$TKrqNi>r{wO)LYtYBy`EuXonw22M>JfSgY5p?mNuYR$E%oz^$bd z36Fg$po?oufwhTkcBk`pyyVzr0eB?s7yM{1qs?~Ud)xYPWE(Pzt~1qP>J0vd`YR|` zt)|`JuLBqS;SWgj20#4KcKg^oO>2IBKlqMz*+w2<_^*YJPe}ZB*}y!Q6xhK#^?@9A z6GiJP&z*`+41w3+9qhqx-YkG<6t@+Fwm)AE4i#<|gSI-a0pFe;%4&C+f*LghHRk%G zJr9dAcXC2Rcb->k+8IAx>O0Q$VpbZlkGY3!$ebZfj?1K7l{13qj2~TAD}*AM<3-+| z=X^L-6*w9*sQ@A8j+AhNv7vz5jvoTQTR}bZYg&uo0}w~<~p~(PQOe)+817K zn?ufo0u*%8Sbp+dXJQ~n&NJ*g=J9@bc+FYk`OWzsDQQ=9`|S)yvYJ;2=SnYAoi z5-XW!o}bvt64<~YmhlDfu*{ObeVz*p1kJ3SgJ%g%Yz>&=I>!O!-@sD-f(F#%bb2f0 zGI3T`5IRY1wiH4$wwjp+IwThOW1m?0L2H91b?7$*I3Rr7{YmntSSg6EVi^q$TrvVP95Ai^q&7P#pyM>A*n}$RFC3W4*W_r+gpr~x)B65o%%|Djveu*c zv~04V zPUb2xWZzS?$AnaUEy5J?rMLO(GUvw<%EDV>%Wc7UE$y^$=J98-Ux7hfmX2s43n2O9 z0gkFXp1oc?-U0SP9{#ToJ#S_j63$5br(hT!b1zO~5bSFgF~(-L7o{Gsxh_V(i4U5Q zpqA$fE5s2fD1Yil@}cg#QSL;9_lq5e3PO((>$%a4j-hg&;g6pwB2n&hnREWv?iNPd za-b=l%)@&jTGX(H=#KJpdO8sFp0uf7iLs8TJkJ;M_~Nzs>0C~zM1V$quS~P*zWQqQ zXka=x@I=1JwRJlf?F<;rq;T#tC&W{hrEN>t2gY)S$D+(7+=if+jYi#0DZ#^{?NP&w zfgQ>c6GbtRZAU`MrklU*^1gU!3um_*W|JFHnbE}>d+zhKV~!T?hNqP?`hq*5{9VJ< z-vo>8vd-jbxGK@2D5EriilI@>O@}Y^K93Zyn)qJv$mOL%kvrBEJjpS)Gs?a7E7y^~ z`7_472c%J5HY1&MFPx@`-Zv`Cr`2$x$^s4&y(?S zGr4B#gBon+8U=f;0^TfhKZf5T_XCVs1;*@|x1J4Ie*Ww{0HuG(5z#q)eAJ!!^6nR{ zWUsosRE6ho>^5b68&g!hm6A`Xy@9fKKGyHsuk&i~)3fcAgH{1F-Hx{@)g-sC%C!7Q z?t8l%vx~a79^TUbg8aDrWktt8X@$-qD=3y^)Lp)+SeZ5xj40dnetx!PnZm2S`X+Sn z`^|f})*XxJ&3Nce^g-}%uz5ZI1V{tYI+k@tWfm{;9bSKo-cCZp{8< zLH}#`Cezfj#B_&-Ja_tahlBA{?ddQu*a%te`XxKX883HWD@9abD`WjGzFnMh#)%QN zv&6b)v%;9T?Q$V}?sk3m9k!1-@?wv=#lKhO>eeMHJCx`X`uk%n#HH)LFA$ zHE-8yVC46C18(d%G#isV|E^^VEO0T0OXt&Z(*1@)iGqrjy`5a|`G;I9G9J+6JvEXY z0ypRGoYgy7*5@XdLN*sLC8}J;7IK=OJ?{H1^Vyu7fh$90&5O!KRb_z|7XMb>l~E#z4bS2NzoN!{@?QvO0*eOHSOS) za}1gUghs~^JWamM&Qls1hYGQvp zZJQ5pzNX}8VIcJ$R!C%CCz8IG?>|B+e96diQCqY4kfYmS!2bof)1YgQhz4`c3j&7F zryP?qGKKDZP!#Lv>zxpRVR9OHyb0j;IM-V%d{*@Z{)G&j(nrZ@#y;qDwf*kNkK)Ca z!N@TfqCNI_R6~+Xkz;Q1Tfb4eJsHuGX7L-XZ`136?I>6G zj{%O8j**b}O0rrY8qsXEx<(djY-FW>2eEh7660*l?4 zK9sEM6*gq+lR|bBOUpEK%BJ>7;~$lmyb6?gTzKPxJQHKhB6OL7eHYI>7=e}6tsxc`Z>mhth_(#8h)tI9LStXhQMVVg8PwY@Hoh|YwtztyL7E30!lL+=lq z>`4hT=0Esc@|>F5K^0Vwrxz9kT(MGb{l5F|yg!MDZ7G@<9=i%JARBMf=`X8*g5@ld z={_>vk?9q4wD+OPp~)Y}X-y?%IGeA@kI3PbmKVlfArFdvYIVZKyZZla zuV4&z#Se3qPiO8zpvHSyN~s6I#Qr?>n|>r-G65SZ;5fcsu0iE8t&34*Vr!=HCh*?a zxxc8B#u9FNrCl?w*?l)XRj-#3 z{j@GQCTC8hbS55l+Ionw>JZ+PI+fV*%+BF^4~wlU?t7(U5Lw-cj#j2>9bKpV3~agj zEC)OKNAlJdqEl=MFV8B+4>IZDqV%cu)uiQ6;} z$5FN3L{)o#r@wSFkEhP0Fo`8%3=qWQ%$q!{9@kOG38AA6>e4A^Ns6|q~N5+w~ zXj|m=_2GN>?7SL&-0+!)h3*eZJIo|Ubu^*EHm|JI#L#nmQ{N@T>Pb&*ZRD&8#;BJB zzE9onLwvHDXL1DYX+8h@X)$#xa{gny)4T61>-E^Xod-d<7yUe>FsH0)%Dq3?7m*=c z5N<0h=L2}w!E@03mn^-RQFv)F!yIp?drapM;HHh*>dCC!GlOq|Yd8{EH4$IKAE z?|Yv>Gm01_289W_XH3$$AaAJyV{R;8c0CY^Gc=v+GEv2Ez7x{$yv(`t)XG3^RMman z%FDD*>(mKmmRf`l9;Vwq{kwc?|O^{H0y}cx(+OVFg4nB zf7;y72wyqbF_SWrT$B>?O1v^lyTMBwN;sukF=gJFHmIe)VtQ=9dM&2U6p@&I2vLR$ zC#*D49`EXrYj}P_2cC6H-0nbyC^H3;)1CnSfC^lSn+zITc8NY@-|(LDUEl4*faiB^ ztxoD;(|gX1DF%sw9`%{)A&W*7QjwUGUP90KYJ?-NoO?&LO{6L39e{>Om@MGZ0ag_1 zPC|cu*15=awt}+9F=-^li5D#zC*Z#FZA1rV7SJoT&EFKh$G_uf0|Xc4(9HRQoBFXO zUu^Mvfqw#7qgRvNjMJQa>|T>XyHsW9L)13kd?xiW=D-{@XrN)11Jt zqj~0CBvoxDpeSN6+5L@^%(f=-s$y_zz)ei=dFPl*!wl~aBKN| zr!V^=Z7k~GGdhw?POOW!-hX*EIb1F1nZnxz!b|Cna6tNwCCA#DWx3Z5)mw?_M8}I) zRt<3Nj0t%XQPcp(HocSl=mY0_)Lji}hppzEQv5loxnW0xdopY%juGT8@`+8q^4~wB zdwhD2nqN8ni9#SfT$cMuhU0)^+`5b($~~OWaG8YTPl=>9$c9{^CF~372^Cbotc-C% z1ckb%6P;DX7)ccd^||4;`_KC@EKEyHngDN^qINcFnE4=Y+AG+be-@2n&(7^aJ#BYj z5IatK88V8CCFPnB@SZ(E!WK!6KVFkmM!kB*>nv#!{rnuU_DsR40>QPE(#+Go z+z3yv1Jlp+@ioM{&V_H>Hs1DG4S#Gh~>5MX%{v z6;vnxEt~49%rR1;<^;AmjbQ?Lyo!3-toM_SZ4^qfIA>0P2E4M)ObvxE*HWmy`)p-k zCQ}WbfE)vQGLA4>V*<^HEn)=7v1s|H&s%b}9jLyWyR4BgNMt|qWro#j%i*rzQYuGJ zMo6PJhK#JxR}E9aHYpRR?s^#SjME|o*K1;^K@kzbisPEWG349OISuMZS=(KK@e8}5 z(!Z}48|a9&-0DDcdlQ+xCwQpPxm8NOrcp3xd)7q|Vily;&9)dwW+h+LY35a$bDVl= zi~oxG%7f0e-h;F^OY`=J0Wi&76S}N7Q*l%GWG=zjCp^VLdLi74?;zGIzoGP+qAJxh ziL$nvGH{5rl#LyBeKj7rD+acRNh$=U!Q8%~NDLyi5rY#prq}n__FHDhV}P#B#g;}~ zOfN<5$)zMl2%9Q#}2PWWGNP?`~vr;Tyjo%JL48xeEW zqrJr;HhWN+p=f4uQ=*a@lJSG&>%M$oFZE+`rOliWi(Vr?kJEGQ&-*x=oF7k)eavf| zw%7Y(mAhS9O+bo6pGI9WGfwp_$`~=dSZO3-I_}`9JZ?9q7=8BHsGN8uk-EurNO+AV zG%U3Q42oPrsS08UpGo)ZxOh{a6r3@ofG$7kfGOV-9D83kXYX#I=~u1$fx{bfZ2doRnv_bqL;TGan;J&)`|^%w}fvyZIZb zEj8-QT#I_|>$q0kO$pUQ&OXXVLgX=)NvE*^o^4|Nhf+1R3vmx*IA98VJMIQWq^3zy zEriL&1sI-;k?zwiThNoC0=TG8GhquErb6_(>|vV_9&cgyHyEd|!n}(d--0|YTq?2Z zhn)?~0*0@E}3Z|c6^qWB* zFMhQ`{AhEomr=n@?MOuX)v}Lbw+q%sJJEm;1HwGwn^4>g%hYIP)4;l6M`Q-4_j9)< z`X&~8@kh;bYemQ??jF9n5UXwTZtxJ+wB4Mm=_-AHv}#&ubOP6XF`79|QU#Ur1D@tS zP|3LY549@t?`-!EjC#|!eGA|fS=z?g;?Sjs zCmYjm6g9(C4ab2IC5zBUAD{N>4ed81O6#eqP+#ks%$K0{wfc2njNn`r9HAch5zrku z?8uAXL$5v4Ga2%noI3sPhsmOvT|&tZH-Lm9(~(YjJCjPBBgNAE$4_ z#*>DQXGNXipWO9V=eI_|NB9H{D|$x#+4&|9JAdLRH3=o|b1z)2d5`(8XdFEcedz># z%-I3qF5!?HFdDMw?W0(469P4;R!>daO|jKEI@a&AUDNb7x1y1+KG>TEG`emQ+5W5) z$>Mk__3y(v(Xs8^=QMtT0RY6&$G$#3WbN}HNYR`5qTd-3mL{&YneqDl)S+S}Auv>f z|JiQm48;+ar$`T0*MU6+5xEUYc(PQ07HK5Q)7oC2M)GXX4(@%g;{q}UO`p|PzKLor zNz^&FBM7xF-$5lf1{ai3*%I!MSwSJMdSqMkS*`hUF02E;IQvtkG`k$^O={)Py)YGo zxLm_!S!K@@U)52pt4Ecus6Y20KF7F(x8M9J7SW*c9PN<)yx)>OXR$gyn%1>~qXHY1 zg}*uD3G;ueBr&MtFH~+>Hdh;Gr@bo4Z4Hd`WknTCZypf<>IM5j{JlwU3m-N42c->G z?qN-k+wYG9{(|mqs~9HWK3zuXhr@9XK3R|iX`!S;#}X?OLJ--D-layrF#jQW<;pEW zf}QrcS5W@k#;Ip4RhnMuS3Z$zfSgZ1J4tx|{E5*HtlT^w>=&_z{C&2HO)Wrx*bXopZ-w_QMgeTBp#p#lLMyw+TQ-^2!$xr&6B zt;Cb2^7oYe`|gKlf65Nut5J-@3ad{J(cr8yQhH5v(B5kx#H>pEP_okPJ27JPBu{@{A`cs4Mg*M^rOYIkt?EAp>d24rkuc$JMc;rc51f z6Y6#>moPQiqxyWQ3eIjz{p9?t0HG&4t32{Da~U)D2vR+L7^6!Dgu7xf?gb@@q*)#~ zHt|JX=WLUuI;uRk!tcY~HHjh2?1Qlqgo{H5gD*6=V53SAdY6ui>c$g4%UHgkzJiI) zal*Y?H)owP=SXJA8ym#4D4r9x^y;kg!3lQEIdstLVuZm+tx30kYjmYPK`iYiPgm^c zTs36*DtwT25{XXL$eNKIRYUCOX%k}4gJ4f6`T(nC_ZQ2;J5&Asq_5w%<9YXsB9Ivd zloybaj&h`iZgF~Ny{-|PQj6W5ix@Q6e?KroVMI$SI7=2vb^Tpqi^oq}eZ1P{xIN4B zHs_0S3@KE%;>1LfIx}Z+b@{|)4SA~8*>tK<@yT>jtr9n`d}f-Nbs%1)VlpZ%XOzUO zR*W|n`Dd&qRTK3yOU>~93|qb2vDXAu^~;hD?&Alkp0RR5VxKGNhoCD#j*4}X#wN;` zESr^q*_tuu!fm;xjtG+*?u>C;yb9=^I(Quy>fqa90y6luo7Mbs%d1jKKtk7n{7eNtK{gQ&*lK^wLB(@BOg<~CHvpBT~o*3$AA0mGNqVpq+lBK_XPD`GSp7k;bo zb#{HLt5`EhTDZr&OxZ7&394*Djo+nAkQ70S82if7} z2VbA9h~a?_kuZVj*O*v?Tc@pjEt^aZ>=3ipnGG+TQ2Nm%5TIrn+>C3;+jCDo#;@mN z_>tt+zN$WFMy4{L_Cf(r>bnhTqXuEU3&8?B%`bNglcVbzi4|Cn4Sn(I8*h7LL?lgC zNrBY^P83eayrKLicly;ELxWi zOZ?UBwqSa1pNU*$Pp!B!Yo@MW@^a>mH3RaZ&`@eiwG$sBt=pY9N5z$Q=QisW#Gwi` zkFd>$0lnB4gB(sCO}WA5p>A3No+<^}QTWtC-b@)S$}pD(z0(b!(4V z2dwPZ%3RBrtO3auJZOS7E>R=5yqt7{EcYXA7?iKPRgxnF)2UWHHoLOmG?z)=?a{S6kQF7v8yhox>iVa%>o)yU+ zxqHFYdz@8ZSR)T+QS_&FWMTE~X?2yWHqwy}1viI0Pp8$pg~eV5boz1okW`Q{Mg5)Q zKbRu?xTT9Er(B1vl=Ir+eQag&SQ=YIxOs&1+j2a;OKQ+1h%VPsT@+Au%nX@{3jnB| z%=86ud9Rc6+W_^mr`7zi;zm&uoL~l94eChtq*%yKGVthIy}L9+8Ugv+dFaX$-kQtG zmRGzM;yKk9>#i;Y4TtH^n=T;JGF^>btiBON`=|HPYaUCQrE;|VD9JFrH@kjb1D-mA z&rXe4_38;oy!NBkd?l zS2B{j5+|87Oxcu!o?RbMe$z7nCjI!0Sb;BG`G5LSjY))@WFL=|q3WtzHgJMQ1}*yMET%T3@Q+N$_N4d%g zgP8Q2WW|O!n75Sgxx?ly%y}yQOpd6&&~ug3>aUu2%D2R|x-}hBxk?C+bG)+yn*oHZX*w?G9 z7nYH2pf|+Rzh7yOUO|35ar51J!L$zIgQUu?QpFB9X+xUhDn1GHZX|Sxn}uhE@5kb7 zvgh>Qqz}=plR||pMjTbYYPcePpnV(r3ru>1D_O`ZOFKS@vb)IS_ zx5(WuBsAm00|(j?O=j_oLe&?DheR=`Sc*tDglgHAw(-(p?#B+&+Og!6;oJ|#7@TAM z!!jZn3xOn3i>u+7aha<41ZO#)dh;i0$(h0jNV8TE{qf-k37}Z^C(?B(kK|prs5EG$JM)0O@)U zgdzFx>+9s5I#0vn-XWXqpS_}H5=%8a>zBjXv1LDYG*)eKR33Mo8avY1tagN8k&%3` z!1Oi$o1qp5pVBI&175cQX$NLaXr;f>VArO=sI-R`?^ZucXwptE!uOj;KePcA!g+5a zM6kR{5wT9@Ad)LgpPM8QOY@ zdX{sREdS7@Y;D|vxrE1BBM4qi3dpU)QQV#xU2)Q%W3O4gI~ zg8ZX80FhUq{e8G&8A${%CfD<(a-S5>rN~Roq_9IAFrqA%aG3RQ|3%fXAqS*7%|t{_ zX4vlyCj55bo9oxUy;ImmxuhzGtV*BTtLWyH@|u;0cfA$|GX}8qZYk2wJQ9PiOWD!* z#1rALP%5cA43~a+qswalo@7FXX1G!mYC?{76y9Pb(gzaif*k*Vorsb&J1zgDUlW2I zqPE^~Te@5RszD@NWAR67>BUAJvp@cKcSlfP^qH)Dvue?aN4BG@Orzb?3boGG7sXNW zbsAYlQc`Jk-lOEl>?A$Yg6Y4{(3HQGZ_(c_FcH-B zchxDnD*d|!oZMrxfF+=CbQv=y5LOxcg!G^wTR!3cD`_ow{)<-rh;h_si~Rdb_Ts1e z-Fb)c)Q;jR$oL<7;op!eV<}Yu9Y#y`;yZ-~XR^o~YapJb63r?}UQAN0Ik#3!(sU}} zZ;Rb@`EMAvLq24Mj>lHhr^!y95$Gns$3a-Lj2~x#LEmow|FkB05Azh7vVtUGNPMM{o z*5^JrpcDhLgIAs~FP>u!o7U(?9XNsBczb-3iZ<)6_ttI#-h zuB1ick_4bctnxEl&_ylDHe@d(pForkLWV?_Q&5yR<|OH`jKd{F&Eub~A&y#H&_R7? z6R2e?E21DubdEhKY6g6v4-Pk0ClC&+`mQ_~`6>MVWJaGplwm+jh@H$dbB|mo+ri~v zcJB-jl0_cl^&|U>AGSp4F(-t_CCaM3$vVld=jm~e!##y?1B}YpkqEl#W$UKKojf!H z(tRkWxXMwRAmf?!ON9gk2cJEAy3v}4DTQ6tNJP+y&3_$j^AElSJdoj~njs`I!)$Bh zVuc2zybk=cV4F)GFzgC~oDHL-k-;v-U3=Qo#+U(Qx!{fV4($<-VzE~)Rvz8>*rN$+ zR&T$ef%w2*ypLXy;BjF{0Vc@4D%{L2N0t)jc~JDX*OcA@THq8mHgHu*xIknA57#|g zxV5zWp^xvmiwy)zj!tnvZ6@V+b7$FH0@Q1lom*=m(Nv{l*Cd-|USZoy5s2IXT|6ea zu$NYFrN|r3lU$DzcoO&whE`*cBU}p-qJ_DxAYJEiL7oGYo^ho+E(;FdsJ<^I7&RzD9v49l%7aQ+x$|-@<6jqh zSYT7o&cy=D|=5&a@UT z*W&(F6w3hOW@%FzGoY?ROIHtp_Fa&=6$KJz!3exV=m9YEuR*-%>ECMVcZ#dhUtv3A z6$X?5q&O9EzS#b2UH#x6ZV7ba)|hRC3XU&Tu^Vyb4#7rZ4-#B}Q=%}P5;JYTq$ZpWfoEIiPu<`~UHx1Z-m4%{ zKpBh065kjXhRgLPj_z0KwLOlH{KH7@>iHH?U&gKq! zeNyiWm)aI%O93T0|8#p-@<>OH^r_?FeL*cXwWgNp<@pc3qfd!j8Ff7pkY_ z2n5^HnhxW3W5JYk+-Xkw=7Ig#m2yIaVf^zRzA2SjVvWlRB zx6OQ-lJB>o57hw5cg4XNP%6Zec_$#w`9&4&t z+EcPlJ?MiwevLcPV+X6&4c4Sj$~#&nqm6#~2b3HLgJ5f&{fR3>Mx3W*rr!VJZ%TZt;fF`&7Jc4ecG1# zoU1{DsxhPoD=K5b%Q90K0L%VU9g*2wITCb|7p)N|ut#9-GP4ZMl>M(|9Qfq~9#Ge# zwB$fJ0Kt|*&!P@NU0TY!7V>#NyY4qfhT9$+4pcKOnhi0QlHr$2yPdqj$XU9V4!T1k zjv}fhV}4VVfOYnBW3i(+>97uOA6~>LZk1TG)%|f#>eX}0x}VVgsGN2_=~UnsIrBQ| zOwmUk(8xXZxX;dS8ryUL2@q;;U*-e>uKR+v9M*aZPz0{HNtU+PDd_w9gyCAfQ$n%_ z2K6(tt$dWiu=gxO1@>&VXSIy|z7kt%*eZlAD*v$8g~`=2qcZ&&`z>>Ck~J*my&Rss zd~7|wK5VAz$kf13OJ?Dm!nUtg;Su9!HG4b6y$bvvVDsbSPE5a>JB^wQa10!m9)%xE z{>W^o>{%l43dyg5Y<*t8J)bIX>ijYwCH-@6zZKzuyfxV{d;mEOI^BH~;jjfDp5Jsh z_pxEIjp3eNm6vlrDpC4_EnK9O^WIxNlZhxsc_B)MMnH4y0K1855uDXN&H|UHYTIo%kk~zNV_e&oXsp-gBU;XVfjT4i6_BY?1rWP58cDU z-twaPesg&&mhjG=aDbNVmxt)UMt>us zfUt~@$w4Zh$*rFq4Z`nau)Jz%!g4LKpRG0vkote#O7MRP0yaUY|EoL?J@5xAFE|v= zHjVC&=A3<0Z(DXEowb856wVi1zcPyd{Z76AzbU>&YDCd}FM@8oC3msZVzqt&bs|`& zY{!#vSf~hawmN&}xw9h&ko%4i18)4g2af?y!Zc*a-qp|DNW&x=DI z^3DBeH`G_CuQf3UDkAL`*=g;7DnReZlfR?4TUB9acV))g(@qhxTL*aKK4|>n@Vea! zW-Kz&d~!p|R>^2K6otw`l#?p16Td=(Tb2BNal}pgNoQXCbYyrqVG~+GLS*tkw(ZP4 z%v${d^Irre%Ju}P=@<9@KL~xspRI+^kItrE<%x@Yy{}>s%>|*g!i0TmuezCKntidN z!Hq{VSsc*S?stA1mU=28<)mG{g>n7EtgtSv3^ZZvyy~&X0AT*#f?G;huS^UCM2DS_ zD4-ks4c^C9?dC`Td;Lp7vbx5{)KQ_RPM0r1|F?h-Z+CgCS25JVl& z{Ad;@EuQu<+P!$MtY7lH-={PIt4xyDwl5mDVV^beSIh}tGIlq37gNOArH)+Un%3mz z@Q$_If#B$kMBC%u|A2*24ovMPqmuYg&84K~0lMgpRgCXcJrG~utW|@4J`KE~d*!O5 z>N@L~4=^e8e*i$*{6&jtA1l$FA8UIP3?_I7m)%ZQKeeIt6CnotZCmCg#45g{$A^=_ z%j|8OOQ%@0vyuZU&UQP0@vYCpf;C0m`fl*lqMy^SzzLv{u00lFwk{v=pd&c{euQQ- zZ88x=K5Z6Jz74iZC+BuKI_3?aUag-Jk*F*TPkLp$h}x+Q8l`$)hYf(YD#j~~rYdA_ zV&x~f8C@pri9P{u0Datf!bsB(@BHIjPNU%}iV^?o~j66jW^En^LM(nphRacMK*{u%=dyfsOp_ zLc8mNOlE$AWb8nDn1I}~)oUN6(#|FbQhO@o~5SlPz z8g%u6L9EQ?huzHD^39OJKTnE0WOca1mh_+Yfyur8wE|eh(DEa`s%iT*pxw+A-J))? z4LxKD2K9qY0Gwk$UbmnZhdIh>cW;|jlTn8E7BrPIzSrV4nst|fjpG9v#kK)9$V8ak zXCiT(;5XhjvQVbH;& zJ5} z!j(AS3P!m97D{YwyB-n!1X#=V?|n|*NU$r&!6BNIw50q>JQW|*H|5+k<%#S-Jupnm zpP_cf=J+>It4f=>p>x6{oi{%>BZ}J9l5wR$)2K1U-w@~4J$4K`)R5+vl~}FW${-Oq zpq-(PI;RSW6K25gYm_Ry{m1&Ir%Q56b>C+-Y^s}LpiXq5WqXMf1RA$dF|0;!*vnf= zI9N*^BOohN{0d0q`t(&{?lR(XlfAc1>GWjdoGaj0aK?{Qm>EwdorsWXPHvd=zF?9o zZtE2pM5AjwDV^!Zjts35)CDT z9VzCo+!{uYVV%Vw^vk|NV)=d%xIPK^+Vb!l22`8#($4}NvmD>#*VA(?h8im0>_3y+ z8|xJ;s@t)h6MCVFS4d1}_R9RfWnaMZmoORd12t{3OJx!cY_!FC+z$=E%GAyUBZWkB z{kD?UTa@t~%HD`^@M0CDGBnY83puW(?=1-QwM!=oU=~QowSnIWP^Lxc0b&VWj)%Wr zSJ&o)4}o7~5C*EnJngRWAx01U2m*XT3_!XotP8lodt0pghm`@uvhMf41$Y7bE|^EL zF5m|h7xn|(0fA#bV0EAd2m7vTV4GME5bM)-V?Tff2r2ske*@>szH8afX*I_FV6UJO zyc5TMz#fq7V&Ap$=d|7eEMc#pV{rwx6T>UncM1HQ*13?!*V!uouYa>wP&q&oU{6c( z=d|97igUA95WKj8nD&1K`>$aC)!2WbK>1%NApVQ7!~a6TzfkZm6uAEj1yTP(0qtKX z_&`^Bh!?>9fa1nmPx)=N= zCUY>gE9_A}r>e0g(7~R(;G8CF)T``KHO9CoFE{NLyFF+QqFWJd*{wAXSaCR qxH;K7pLg7{!)v$v?k$(>-JIaDq^578Zh`xt<41H4rye|a?SBEi+EC~K diff --git a/apps/termtris/evidence/termtris_quit_3200.png b/apps/termtris/evidence/termtris_quit_3200.png index decc0bddbecac0b273b5d5762c37e9d0ac3e5307..24bcb9b5802a35f676b56651afa278dfedc83624 100644 GIT binary patch literal 19441 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 literal 19497 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 PQ$SeQ94a_)`iK7uI6*fc diff --git a/apps/termtris/src/vt1200/main.c b/apps/termtris/src/vt1200/main.c index f4500eb..a440aa7 100644 --- a/apps/termtris/src/vt1200/main.c +++ b/apps/termtris/src/vt1200/main.c @@ -112,8 +112,6 @@ void vt_handle_event(void) VT_XGETFOCUS(vt_dpy, &fw, &rv); dbg_focus = fw; dbg_rev = (vt_u32)rv; - } else if(evbuf[0] == 12) { /* Expose */ - game_input('`'); } else if(evbuf[0] == 9) { /* FocusIn */ focused = 1; if(vt_paused) { /* resume only what we paused */ diff --git a/apps/termtris/src/vt1200/x11.c b/apps/termtris/src/vt1200/x11.c index 54139f2..9d89af5 100644 --- a/apps/termtris/src/vt1200/x11.c +++ b/apps/termtris/src/vt1200/x11.c @@ -64,7 +64,7 @@ int vt_x11_init(void) if(!vt_win) return -1; vt_x11_progress = 14; - VT_XSELECT(vt_dpy, vt_win, 0x22800FUL); /* + ExposureMask */ + VT_XSELECT(vt_dpy, vt_win, 0x22000FUL); vt_gc = VT_XGC(vt_dpy, vt_win, 0, 0); if(!vt_gc) return -1; diff --git a/harness/mame/TERMTRIS_REPORT.md b/harness/mame/TERMTRIS_REPORT.md index 88a7688..ba7c056 100644 --- a/harness/mame/TERMTRIS_REPORT.md +++ b/harness/mame/TERMTRIS_REPORT.md @@ -75,7 +75,9 @@ 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. +`w`/`s`/`d`/`a`; `0x7F` is Backspace/Delete. `ExposureMask` is not selected: +a map-time `Expose` fires the game's full-redraw before it has a current piece, +which draws a bogus piece over the top row (blank border plus stray `X` tiles). ## Reproduce @@ -94,9 +96,9 @@ The run is `VT1200_SKIP_LEVEL1=1 LOADER_AT=2500` with the loader payload at bit Last full acceptance run: ``` -move DIAG: loops=348 events=19 focus=00400001 quit=0 -panels DIAG: loops=338 events=13 focus=00400001 quit=0 -quit DIAG: loops=292 events=11 focus=00400001 quit=1 +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 termtris window, and `quit=1` is the game's global after -- 2.47.3 From ce40582e9de912b77a671a6519a5c1beba73623b Mon Sep 17 00:00:00 2001 From: opencode Date: Thu, 12 Nov 2026 05:35:30 +0000 Subject: [PATCH 15/18] fix(termtris): run the 2ine loader under qemu-i386 when the kernel lacks 16-bit LDT The r5.01 tools are 16-bit OS/2 NE programs. A Linux kernel built without CONFIG_X86_16BIT (this box's cloud kernel) rejects modify_ldt for 16-bit segments, so lx_loader aborts at its first segment even though it is a 32-bit binary. Probe for that failure and use qemu-i386, whose linux-user modify_ldt supports 16-bit segments. LX_RUNNER overrides the choice. The toolchain output is unchanged: the build completes in ~10 s and the COFF differs only in its header timestamp. --- apps/termtris/build.sh | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/apps/termtris/build.sh b/apps/termtris/build.sh index 6b0c33e..9d0d407 100755 --- a/apps/termtris/build.sh +++ b/apps/termtris/build.sh @@ -32,6 +32,26 @@ if [ ! -x "$WORK/gspac.exe" ]; then unzip -oq "$Z1" -d "$WORK" unzip -oq "$Z2" -d "$WORK" fi + +# The r5.01 tools are 16-bit OS/2 NE programs, which 2ine runs through 16-bit +# LDT segments. A Linux kernel built without CONFIG_X86_16BIT rejects those +# (modify_ldt returns EINVAL), so the loader aborts at its first segment even +# though it is a 32-bit binary. Detect that with a probe run and, when needed, +# drive the same loader through qemu-i386, whose linux-user emulation does +# implement the 16-bit LDT. Set LX_RUNNER to a command prefix (for example +# "qemu-i386") to force one; leave it unset to auto-detect. +if [ -z "${LX_RUNNER:-}" ]; then + if ( cd "$WORK" && "$LX" ./gspac.exe ) 2>&1 | + grep -q 'failed to allocate LDT selector'; then + if command -v qemu-i386 >/dev/null 2>&1; then + LX_RUNNER=qemu-i386 + else + echo "build.sh: this kernel has no 16-bit LDT support and qemu-i386 is not installed" >&2 + echo "build.sh: install qemu-user, or set LX_RUNNER to a working runner" >&2 + exit 1 + fi + fi +fi for so in "$NINE"/lib*.so; do [ -e "$so" ] && ln -sf "$so" "$WORK/" done @@ -78,7 +98,7 @@ run() { shift i=0 while [ "$i" -lt 5 ]; do - ( cd "$WORK" && "$LX" "$@" ) >/dev/null 2>&1 || true + ( cd "$WORK" && ${LX_RUNNER} "$LX" "$@" ) >/dev/null 2>&1 || true if [ -f "$WORK/$_out" ]; then return 0 fi -- 2.47.3 From 8df2320d6dd5ed01d209775c854dfb5ecaaae2d5 Mon Sep 17 00:00:00 2001 From: opencode Date: Thu, 12 Nov 2026 05:56:56 +0000 Subject: [PATCH 16/18] 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. --- apps/termtris/HANDOFF.md | 59 +++++++++--------- apps/termtris/VT1200.md | 30 ++++++--- apps/termtris/evidence/render_frame3400.png | Bin 18967 -> 22440 bytes apps/termtris/evidence/termtris_frame3300.png | Bin 19434 -> 22850 bytes apps/termtris/evidence/termtris_move_3200.png | Bin 19584 -> 22948 bytes .../evidence/termtris_panels_3200.png | Bin 22424 -> 25726 bytes apps/termtris/evidence/termtris_quit_3200.png | Bin 19441 -> 22908 bytes harness/mame/TERMTRIS_REPORT.md | 31 +++++---- 8 files changed, 73 insertions(+), 47 deletions(-) diff --git a/apps/termtris/HANDOFF.md b/apps/termtris/HANDOFF.md index 19f2d7c..bf453a8 100644 --- a/apps/termtris/HANDOFF.md +++ b/apps/termtris/HANDOFF.md @@ -36,39 +36,32 @@ high-score panels, moving/rotating/dropping pieces, and `q` to quit. | game integration, scores, COFF | done, `play.sh`/`play_keys.sh` PASS | `41d3cec` | | docs + harness report | done | `3e2ddde` | | review pass (seed, focus, expose, assertions) | done | `7a2cef5` | -| handoff script + this note | done | (this commit) | +| handoff script + this note | done | `a12fb47` | +| qemu-i386 fallback for kernels without 16-bit LDT | done, `verify.sh` PASS | `ce40582` | +| VR319 re-run, regenerated evidence, updated numbers | done, `verify.sh` PASS | (this commit) | `VT1200.md` is the port documentation; `harness/mame/TERMTRIS_REPORT.md` is the harness-side report; the design and plan are in `docs/superpowers/specs/2026-11-03-termtris-vt1200-design.md` and `docs/superpowers/plans/2026-11-03-termtris-vt1200.md`. -## What is *not* done yet — read this first +## The VR319 re-run — done -All screenshots and the acceptance numbers in the docs were produced with an -**older MAME binary** (`/home/arti/code/k-space/mame0289/vt1200d`, 30 Oct) that -predates the driver's VR319 default. That binary has no `VT1200_MONITOR`, and -its frames are 1024×864, i.e. the old VR150 geometry. The driver in this tree -defaults to **VR319** (1280×1024 active, 1696×1063 raster), which changes the -frame cadence and therefore the frame at which the loader and the tests act. +The screenshots and the acceptance numbers used to come from an **older MAME +binary** (`/home/arti/code/k-space/mame0289/vt1200d`, 30 Oct) that predates the +driver's VR319 default. That binary has no `VT1200_MONITOR`, and its frames are +1024×864, i.e. the old VR150 geometry. The driver in this tree defaults to +**VR319** (1280×1024 active, 1696×1063 raster), which changes the frame cadence +and therefore the frame at which the loader and the tests act. -So on the new box, after `harness/mame/build.sh`: - -1. Re-run `apps/termtris/verify.sh --skip-mame` and note the new - `DIAG: loops=... events=... quit=...` values. -2. Re-capture the screenshots (the test scripts write them into - `apps/termtris/evidence/`) and look at them: the board must still be a - bordered well with `[`/`]` tetrominoes, the score/level/lines boxes on the - right, and the preview box. The window is positioned at (80,80) inside a - larger raster, so the PNGs get bigger but the window content is unchanged. -3. Update the numbers in `VT1200.md` ("Acceptance values") and in - `harness/mame/TERMTRIS_REPORT.md`, and say plainly that the earlier figures - were VR150. - -If the behaviour itself changes at VR319 (pieces not visible, no events), that -is a real finding, not a formatting detail — the loader acts at frame 2500 and -the keys at 3100–3290, which may need re-timing if the new raster shifts the -frame budget. +The branch has now been re-run against a fresh MAME 0.289 build of this tree's +driver at the VR319 default. `apps/termtris/verify.sh --skip-mame` passes all +six tests, the evidence PNGs were regenerated at **1280×1024**, and the new +`DIAG` values are in `VT1200.md` and `harness/mame/TERMTRIS_REPORT.md`. The +behaviour did not change: same events, same window (`focus=00400001`), same +`quit=1`, board/panels/preview all visible. Only the loop counts moved, because +the VR319 raster runs at a different emulated frame rate. The loader still acts +at frame 2500 and the keys at 3095–3190; no re-timing was needed. ## Prerequisites on the new box @@ -79,13 +72,21 @@ frame budget. `tools/tms34010-sdk/1990-11 TMS340 Family SDK/`; * `vt1200_v22.bin` in the repo root (the harness ROM dir symlinks to it). +The r5.01 tools are 16-bit OS/2 `NE` programs. If the host kernel was built +without `CONFIG_X86_16BIT`, `lx_loader` cannot create their 16-bit LDT +segments and aborts at its first one; `build.sh` detects that and runs the same +loader under `qemu-i386` (install `qemu-user`), or honours an explicit +`LX_RUNNER=`. This box's cloud kernel is one of those; the generated COFF is +unchanged. + `verify.sh` checks the ROM and the 2ine loader before doing anything, and `harness/mame/build.sh` re-registers the driver in `mame.lst` and builds the single-driver `vt1200` subtarget. ## Expected results -Last run here (older MAME, VR150 geometry): +Last full run (MAME 0.289, this tree's driver, **VR319 default**, 1280×1024 +frames; the earlier figures were VR150): ``` smoke PASS type=2 keycode=194 lookup_n=1 ch=61 ks=0061 @@ -94,9 +95,9 @@ render PASS flus=464C5553 progress=00FFFFFF input PASS bytes 61 9B 41 9B 42 9B 44 9B 43 30 0A 7F 71 70 (a/up/down/left/right/0/Return/Delete/q/p) play PASS DIAG: tick=887 quit=0 -play_keys PASS 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 +play_keys PASS 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 the process created; `quit=1` is the game's own diff --git a/apps/termtris/VT1200.md b/apps/termtris/VT1200.md index cea1d20..104a283 100644 --- a/apps/termtris/VT1200.md +++ b/apps/termtris/VT1200.md @@ -36,6 +36,16 @@ 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): @@ -76,8 +86,8 @@ 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). +few hundred bytes of requests and the loop runs (measured at the VR319 +default: 650 iterations between frames 2500 and 3200). ## Timing and input @@ -133,17 +143,23 @@ which copies the sections to their linked addresses and calls | `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`): +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=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 +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`. +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 diff --git a/apps/termtris/evidence/render_frame3400.png b/apps/termtris/evidence/render_frame3400.png index 05de4424d7c9a7f45ab2e5ee6964311a032c87ec..5ced90fec745a506e675df4cb25369253e1dcd34 100644 GIT binary patch literal 22440 zcmeHvX;_oj+Ag-cT190mwOA2oYbb&ODq3XTb;3XuMVW_46%Y}^Czi+SS;YFNR zhk6}S!CweW8Jqr-k%NGGw?A(zr*2U?{85WF6ZLOB%L)1t#`oOR`+X|*aOBDg{>pOe z!uTrY1w!Yyq6Oa~@#x{Y5-wlMT_{kz4{)TOa~#RvCV zpdzSI2jE_|UrQt91tt`han3@!JHE8l9w}W zZt!uw?VyyFL+WQg-V7h}_b|lfQ(`L#V}`WS(9te^|CI1!Dyde=F3i{HA0<1$KN8DF zj|eqhX^GM0B`2R)X2eq@kE4#(FF9SO*E(*rwRVwqsGDSU56P`j-`9CXH{NBh z7@s`Dqb3(p&419A&O4;4CDM2$9Q)P*d%T*zI9g0NojoL72$P#5Uh~#!Y;CDPNB#oJVKP(QlRyUvgo1%JL zYZ9tT`%Uk0P-50y z4&7_kPit$kRA;6VV)|6*PP5og_^A{-uqdw2J-U%CV`?@}-|Jw9?^?Tts+ex%kK^$KNBF~e#k(G2QwIr+%kLW%T?BgEcc`l39gVne#)Y4aPC+D;(6gLNfbtL~ zSBx=5{v&J@FA1CQu8p)Gw~LHExXA^F=yk#*Z^ba3^8_t3Hoj*}zZq&A>B{Oq!UfDJ z-QiIU1gf@nk%)ZvHW7Q~Q(M2N<2dUqc%q?dAWwZNu|Qdf5_O;kaYp)j&w0V~Mr?;< zXb}Sxa1{oaaH`gS#_W#@tbS=g;ScZvK=ye z6OsH7NRfQzAnHdbAX<;WG`Ojrb?6!IJ_veR=)m1H)v`Fd*$e%xLWrm=b~x#Ch=38b zx6~*DiG{;)uUUnaXYL@7j!}%n$9gx%nko;16M&szs@{J^ET+9?dgSRj#WUb6<#`IP zz&!&bGmST%uEm(rMB0x>n-G{P;^VFtzx#xLn^VOv>K1!%F)`s>%s!c#TXMT>@&azE zBA%W_whXPGMK3WiF(E&haR|v}4n*1<#^v;Jua?FAkVDpLZEY1S)?4R$i?RWi|HjbX zf=XZI=jRt+Q`Q>#lNhtp9@n11l#Xw`u4tT~EZNILZQGhZ5zX{}gmi3ZiS*Jw0bhP7omG5< zw`U`H2Fou6+KouXgE_Micu*!CQ=$n!a9GkR_4#&Y*(O=Sq zyf8iR(SsDtSaJ#3O+NfeA=tA|Bn7x13A9+<7zE;x$|` zY#=2pKwK}*1RwFhtX@?MSVE1h4yJ@7*4sXg9dOr64{mQcW~@VTK;j%LP7nFI zFnJaYU}A@;bTgxIvYN(z+-m0%_T-oFWqt7SA@M~pg;%?PF0#C1NvLgT^G{o%X^YS; z6;R{s#!2VEN~pK;6X~r8X6xPIc0DC8#W}7KH5!v5WD>9%NXhg=kcP+$KD}IJaP=Y zXKF@Suknt4UiUgbHg{1)Jdb7_`_Q;9e|v&>yfNK~>I224S0-xD@>NxBlJ4-!2)1Wx zYKLj7kbkLC`lqGJu#0YU-zi5NfI}3V?rQh(l5F%9keKPo+k#Ytz7r+g`%GjGc>|z` zAi_U$;3M&D$WI7;A4ZLl+BSHq*HZD4Pr+zr90BjP>b(;!EtwQJTtevkQqotluK?cg z{vjv)lm|C*QYR(fGC^xi3s zsU9T<>mb{G4LxE8L`QJJfo?|AdUz zX}r%r;?YbhDdRur2>kY&Vk(a#X!3O-2iUUXUyqIO0{V9LIrleb1X=bvzHm{wZ@-c4#ZYDSv-2e@~uX>J)>e<@5vhmkwsS?R0G9uQsJ!PumL7+0z3A zdbcnAO}PDzKPJlsKa^t$?DK_|GE&s^Y-ThLkrc`1W8azPPqUqlX73*;OSJDm9e($$ zjN8>@Kb%=6tck7NaW7}ATg0U4k;o-Nv%2D63+Slhexo4(Aw#O&uHWx44jo&CjIrx)Lwge}!^QsWf%7Hm zP3$vg^F=qK4#vN>#00C6nQ)JzylhdH>K^Kg@|_Qqpj>-#~v`MxTw(Ky>s|{?{JO z4pW{&pI-6a9VN6G_2IRmCmFl}RS<)SF$r?dhD9e#?wlhd5|?^MI^#}P>?!s;TBI|+ zD(3O@vHmQ+s;4&Xq=O^*0V{CFU>CprC9|~c*+hkDeT~rgsc6?U%BSz0+xDcWxFu6p zT2L24Y9~^&9Xy3ch4!vuziphaAjoE1^@WJc-M1y&XV~K#8-7jH8R?w zz>x=f+c;y;BvQqgk3HiB1!W~BB?5IjewdGKng?G$!I)XHs3{v?*%|j$j9o7|jBFEX z7h?QaU;weoyAoMv$2Vu25470i<{N9AIr^dEu6ZGoVfBw6~+6B`!?AqA`je*HO<&cz)2fR4d)t-2D z7m~k=@x$_CgTatVRNyl^@*4ndQk{+V`B^`O2Db}%&UW-W1#FLd{9{XzF>Vaj)p-=p zNN_C0)z48iLCYz4h<_|ZxCa??fN|~WGXVECeNs0sdZ&;!2cx(0S`58(1kZ3&SCkL^ zI_Xp(dHu*7GS}suDz^5efcIMJnp^eE0UNAbNY2k#rmv4FkvU?gD_x_Knv!ozs?d{J z&)~6)JVkaTPL_8J5HfK2g&NMjyg7ROdR_sdRUt3>b4smUJ#gMfUR-+CJJa4e29aSr zu3z&N|Bk7)8GBP&{wiSUX%&F~x4>$ffgf-~nPI-?9^?!kZ} zwLZ*?Zgmk+u<ao5MX{L58=uyg2ORW#8_v3ap^iX!h0TU5T-fYnaQ}4HYE+G~=d%MgKcl zSe_Ohtd#0xEI+#Bi03kG=_rgQLB6~EClkk+2P?gzLtucH9Vh$TeLzsQMWBZRJXYbjn2 zTC?R|{fe9M7h=KdN8Br?S{}dBIxyR((uryi&~0{GsL@>dCPi>NE#hl(ws8_*6pKlS z#Kb)!jVR`RV&1Cpi#{kF^K|D>xA1*Tox`eWu@=$~4OXI6ypCNdOscnyu)tV-XA#2XAF z{j4c+6o5j(NKel<{?7^q@$i-b<-FWkghW%j_{An#2Nl zTc_jKJ*B;@VM9Qb>*v~gt-a8F%_)uf*s_EE`Yz<~GPXO;+M{}{j6F;vQ-7Cwz z43T1I^j<$v2lQb<)-}I14e)MgNOlUa=xJK_=|FA}rqXiTJl+|r7I^Do7BX&% z6;xS4ccw1Cc-!-8YtIm1@|kM4hWjY@x`UW5&Eza@nqB*EQp;&o7jrdegw$a|u`)}n zb?_4Mcs3$Ow!ZOT5Se4&;{E|YnKI`_!k)MJ4;KBpJV^}q+VW}cHPH~M6 z;k4af_>C}}@#{Z7Uj1!m^)qNJ!-&6}BS$Fkq2unqkTECc_@IE+F~sMyjFWRH$v zx*pagij$kQ%u-P*R<9JbD;RU7$ry@W45dOURiv0~MblXkqp=aGW_Y@6I*j08Gy^nQ z>L@#3Pf-Bn%}A=T^cC;Vm0X*af3q^GB)*RlWnBND?DiW4q3z(Ub)@G`~s>r)~dR{4$2z>H6l76MsQ90Eipx#T$UK!@;@ zQ07LgR4ov^v!7!`L(;MtERaldnmaEw%;8`Z9R$9O(3tz=rs`ma6hILg+B~*vAmd1j z)nBdtX3*0#N{x9s6)Bg1lD@->q&N&=CD*zy$-SVzQY$nfv|{R0ft_G)(Dgm)a3n}J)0T9V1-%B);7DhB!=SzIxxKQBhN4(&TSTDsg8 zG4!3G=IQi&X6J3%byj>u{G(POG)kpLso%{7St19?LRW9jc0433$>jYBmE1)G`mb`@ z9El{iv@)UhS66cgI7xEizC7AnYP6(L&YoKfpV8DZtLadsIud|OSaQvn@RlL+)I>Po8=9b^x|X(?hgtL-XdYE)N2ugfNVon{~E@lVQr{ zJ@)s@jt}Yl*U%_Hi>RF-8&>7Hk*hr0OTdsEC&-XH#db39{JpKu3xHeirPjEgA<$)9 z)y;a+$%pC#1)HUBXj?_(S!OTN$L~<{VSD7 zIqP-Dxz@NxP<#ohzK6K>J+XU$>5^ctISn#l8>brLOsNrcOd5oEgjG#a#kWo~1j_x8 zUovMUsesK3pK)c{3YOguQkrr>Dyl(w)yDjpsRpxx(b8wY)t1lL8dm}nXa5Bu@64M> zQ+s6-nJ{aEjET?+B*xjf{=s;8V(=hzy(tNf##iRUm<|hrf5~M3PH~IOB@J=i$Y@3> z#+b6>OB8$?$4!E>z~pV8$9PGI2>0!5?dz7NXbRkvO@Tp=ILIkyGcur?OmoJsRBIi}E^tF4*Wyo5+YIZPRt;MvfdV(t3ksBra+c1nKX z(LS-E?YbKzzGr!)C$2=lxFXL%Kf@cdfLo~Qa+llBa1r(%fkDh8tn6=lF(B%WhE$WP zXuk-}H3~7i)7d)Q-kn&Msx4xrN(p~fM6>PJKxifTLo-~G}E8^`*Y5L-TDxd4#>?+bY zFdx3lt*HdW=WOZu1V`ok#Jo(ku;k&z*}(sR-(gPaa!h(}bMwjzc{-QgT>rTpaL|=< zXt(lUm=2+=r7(3aM*C0N8@6S=&tN=El&nu9ONSJEW$q0{F-)Zh7l@JEN$4(H&A!Bz#V%*+&P^NV5$B>JDJ1k7cNcr6Eo$u< zfp(+1rs4`qG3zW%30T#jD&1ZH@J)v>1%p7gBJfrhfN-G$_*7~x)GR`*nJktNds>l6 zHI>1ZUnA8{bsZ~1SdnGEZ{rW1+A`z8@XK86LVi#O?5ciH@27U{>I703E#AW_-0ctf zuENT`$K|IkeA*g!4eka^fI#YZ*Q`p-*K-aoyY-f3(&$gwpny!SP+>vRox2#+8J*<5 z>0I{6sIx`l8#J+HOM7RRbv6jp=eN-eajjNg|oQF5zb zs9M`}<#gdC@Quav`s@n#v{_i7peB>W}GwUS`s^$zrd`*X~PMV)d)QQz;f z3@nPXxT=0Q(YD9VuElczlspz@_-+fH|5iz(dCmDhXcwa&l$sre)@>TiSo)cj7D{}`?$w9N4X_66lnK|6padj27k0S=a~ zlA-K^E(q(rKetMQ*j8S)8*NnrWLseOhMRIg~YA|$3Rhc8k38ttB=`gBnR=iLV0hM zermq)(Rn_@K7#^d09GJR_5;W|!5C4>5JL}svgU4NOf2Jv|2lsbfaHgPaO%HIqrP|2 z_cC?U0J6u9mdJfZ2wTqNRZHgb0U%v2;cw6g@bmA(#74JrOu1(Kx-bx(73oCioVRR752&b6Cs?dbL!u|5t5{d?3* z!v8*>%JoX{aqf+$iKvzMg6|UF`6N)!h)-xbY!)}VB}LT!qJ4N?dVmxs2geJtB;g&zPC!o43?`mb(>?kszYaHTf(`f zC838RJ?I5ZrnM8r9n?{QK3aH!1|w#gHypd0}U{E9YDQbYM1 zCMaQAgS)VB;1moO@tCVHgJ`*2(ez{PDg#W>T&xVb7plJiFaxr|bLONUBYKT*h*n>(_d#ju}eLe=ha2EBF|p4?e$-Q z{H)wP3J*M_QYsJ@*;GXC8gLuQSlgdhS|Q&5ZdkVo`y#?;OV~$JJE6B>*!9|POfQO z^g3zTvn~z-*v+}#2w+=g!KND~nwEM{`5fH_^~TbOSwU0eG^%z&_qq6jVY6o0j&IyB z7n%g-1W+@n(6sckW}=90g(eOstlcRRUvo#Pu)mvAY=iotqhB{EhMLkLKJ}8h zq?4XF&2>RBsN(cUJ8trYOi9yCjNI#BvESHyjXW!<3AT$`=y>8et{H*)lCye4EtBwq zqg##&b{vF_B(V4)p<|c1E53|hY-PN)t)HCK#EVTjjrqy{ zaZmJ8VIK?KFkc^q+3~ruU{NqPsz^|9Q>R3`OKec6gBCDvMP?I?b3@|+L%Eval}n!J zr_lv+8I+s4q1Wrk>Fp2G$Un(^4Qb`d*IpcYd?ONF>ZzCxm3f*q7}Q2&X{q;I?k<++ z8gGY7z3FRj8oW$*NL8U$0lNiJD)`E{2|!W!2_;&zcSZ4<ri7mBp0Zl{I2?DYJC>*M)!Z&2)*j*I{4V*noW!VjYOBCqY7Ie4 zn$M2-Wr^YI)G{`3)k-GvJTcC(tC1ONM8`KvO5;F}QfB^YrH4X~pcZ4hBjEd3>?cuK zWrBANjU6^iJu--;WPbzXz)e!<-LC-q3S3wnLE6 z>Qm-(jnlPHXpsmn&vy<|d4i?uDy&~T9wsaSiN{WIXM6aOJu}YL)=Vl{eHO1f)Y5?c zV}TR_SM(U5Sm-a0>ucDJjnf4$ZCRTHyv?8(QL!EYi*x@Y%m&`*e>4b28wH;^cgCE} zxAqsI3AVtUrXC{4?5yHjJQMwXrrw#}*=OxfZMD&M(?YK6q+tg^d#X~s3otsRgU)=2 zjX%ZB3Ta@aG^M%V@ABI%P*wu(9p1v?4Y6^ffk9d+2lWN~G!J*sboJuWkuk4DCK)m% z*jFI9@m)n!)PU_c)66!xT=enDBvK@U-&wSk<7PXJjf_oNA9Gk-aMH7%WF+v{MdA(T z%99X(+b^%P`K{>Ph3z?2k!?>mUcR>%BGWRtzIsM=UT9S^D3s zKkh*#lK&*09~o=P+0cHWgG}IJSK_Ab19TB{qwtwC@+!ARb~kIr>=<`e%rr~fKnWb- z^}gWzKH{RrH1)=aBP-+e)P#eb=PJ~KF?FJkpFUw3u9EIcZdauVhJ=H}y1SY-Kig0P zbc*mDe6@d@^*!xeMm{!6{+(~0_|;4__vgfO1efkBd(3Y)VfRQHN#r&0ixxL0B|7-e zXD^eFQO&q%ByYk%=Kz&Gj3o*?yZ8yi?+jjg3muM$*W`bffO<53*8qtRnzOQ?jpWx# zuCP=t8~rVb?B|zG*YVpj*)DYR_8ew&CYvBU+t!vDSSosyADSl4MYd-w>^H%^>2&0i zK%)v>-L75FK;yWINt6Z5?n+=zgT{?AAPZ1!Uu<=#?8;9*`d;@52#0I)JGy?Yr;RZM z?`kNG;{$8Ad*F1+Q_I45-_Q5o%!g_HI}%cG-$w?=);C$9Vw-1}M7rZpv69TnYqZtn z8FgM?S9y;mGJ~`Kd8(Gyrombp#>@h}fYlX640tf{)^Bo|qhygYWA8fDtiH(% z<`mWCtGYYWc0Pl_04Q%TUzf%zH&DAJ6nZG)+{+e7`8#w2dh zZVgM!BOUTR2FMrh60R_t(X8ha`_{#teX-Vwzl)2-wZ{D}$(Lcr4!0!t{{wb}ud*FA z-zGkCHqOj3{!9nMmz3jC8Cg4VjoXO&b+b(cv)ZCm%(P5IH{dztahFKZ7@dE19RJdC=G#_d5X)Ugz2f($bsYD!Ifp<} zgWTElf$#~^4$JlC$Y?W34ncV@J=ZG+#lj1CW5E&4U_D$9a|=w*`^4uNm&Enn_4^Ys zq?JIvNm$Y>EFz)}ouwJ)bG#iZmz1InYJ63|)s(}$<_K;n>j}B?>}b$s)Zm`HE#!OL z-Qea`UV=rUERerUe+_y;5k+|+7AbG6&%;jQWgJa#TkH8@OyF%p?(I9E(`V?>}WMj`kL^ah1rWhLp`^M0K@aWQkremQv zFxD0z7@Vw%oW}&0P5dL@qA`kdhWOi3GXQ;9Pw4!R!oxa}9Q-1V$NHqa6pmh%y*~`!2uni=yu?2GQx3SnQ)WIzs;{qbwR+TgKFfC{=%ZZUGylYz zTDO8j$vXMjphi`ERoOfhtDec!_eY-LO55qEDZz9B0@<5gky!8~mtHciDr-fME!3TX zLqtZ=zD?hCKK^bC>XB4IFQW!RLa??}G|CS5yw|PUb(t1_!~px1q^W0)b|dykT#ZGb zrhV>ivKPbnP3j4 z^&{Sr2!aGj5^(OYBx7A9YqU6MbHYmDnKbgBN3H-CWq@l2P>N41oO~JiPlmKy3RL)S z=9V^bN^}N@iZZ<>HKeI+iQ2X$<5k+^)DsO*gKGp3c+I28sH2gB^^{k#;Zj?5@ zNw7;+vH}YGH*-r9A#=P{qE`!Qzob&_6aN$YyoE(fJ%AnO^CH@LC6$GTol_J=&8<@q zFJ%S(e^BQj)sf}}Wg>Ix5-}~~<9|rH!R`yzoj*4_PZ?%4$tOM&!p%pWYEftA;6N>E z9aox=+d_WW|42bo`_Fv+7iJ~wagzz9oRoap58N+JbilnIxhlw?DB(B|2ss=m10a-s znGRbk9wZJqzZP7b} zdeif3()0RPiN=`6%W;UHG({1!JUVdU$Wzut$Hn59$d}XXlB5~t{*gwOK-m z&0P*90}oy?(#%&568X6E9Yvu%-iNrk}aie=EB&0C*acSBN&Zd%s! zdn&s#yz1O{=1%l9rC4!Dz;`8Bu=Z&=q#%Z7TF93r*)oa7-zAiGf!u)}O>ZpIBreke z#L!kTIc1B58PKl2c7aMGDqaiDyWsfvV$sOGM0v4OIiFA#O0pB<#nW$j%O(1R+=P((YR=Sd4y z2WV`AqVrM?^;20i{tbUpbXVSY3MYYDw7g3oRMv*5-ewQ6&tCHNBsZU%r0bvADqKDe{s z3kqtsi3zqj{P3^vZ!)y1;PPTVWm_w_^e6BGG6XL79na~s+olPwfr6`C;5y$RI;L-9 z<7qJ881$~;eEUe$Ch$JE&j@bBa$Ky$q#-gR^AZ>fom%*lRt-iQ40SOFUTABp&J0;(D?Kllm= z-{4Kr`y8>|HZ`z>DsaufT|*yF@slakZH>x*ZLrOUyu zH-I$*(}Is>tJ@%;2K;D`NY;nLlDR^4c(WIL)IdWC*c|+v2hp{3RBN7u=5W;DZQz-~ zFW_sggbr{NXN!L(yekVnXUpdoqXF9>ox$=s0DS2%?ZM6Avc;0m%q`Z;2X2>ihVvkO zBU`so!W_wBPv;kNm9Rvz?xFefUY61h9`fbZfcmTj-HXKgU6j zob0Pq#w6>0ATDI;2jW5r`XISL7CyiN5QPtz3$gkGEIEG&PTUy+p^_L u*DoV{!-4{WeNKP5@ys@(ZD0BYgayGV^=A_>zk+cp$h`;mlR1{Ev- literal 18967 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

>kRa&J@12$Jwu-IArdyf83cw<$& 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 diff --git a/apps/termtris/evidence/termtris_frame3300.png b/apps/termtris/evidence/termtris_frame3300.png index 92302fb5c19808d1849bccf7cd06093b8cf6ba85..09b0683c0f8b86b0c994b308e197e71989a46cb5 100644 GIT binary patch literal 22850 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 I;n>gr1K2DZ#Q*>R diff --git a/apps/termtris/evidence/termtris_move_3200.png b/apps/termtris/evidence/termtris_move_3200.png index b05ec08048f8ecba398a27109a9a12ace133a0ce..3f6884b66ca290c4cb812da24e1ca36513285017 100644 GIT binary patch literal 22948 zcmeHvcUY6>->=$QYpHVliDFTRRTLb6iVDiA12Iy?g$#j+3Wz8XK?5Wtb=3-mXk^NA zfQXEUfXGPVC}3qof^0}Ekwl0gLP$dPxt}D=+V{NYJ->6V_qvYxL&Njj^E*D@b>F$R zcaOuY@AST#GG)pv_|EP7r%ag+zJ5LB+iBpxwFAd=rc81B3ch{I!O)bCoqrK>%FaL7 zM^|Q+I-|SpJy>@A%I`-mR*UWhBvuJSn9FL~;PLhZLpUMBr6ixzwR9)+eLY1$QFL0q z{iMGP4*sUJ2^c>$>MJn|Z6(|`-6PO5+8wg;oA4L<%C1*_53^Q()OrN^8TRf?X8gnn z?n|~obz&AFeo=L|2x02JJFfYoQELeCi}Y~B)Oe`9{^~cn??~(p*=e@!yKKfy4zWkI zmPhBXQv+p2t$1a@MqCQlD&I*l4fA|1G%@wj`dV=Ver)AuQ^pqN?(&|=`c(I)RT3^E$TeYF~TCAk)^)H+kGT$s1&)_Q6t z?kx0>rr*PudYCbDDaLU%c?vbuRrT;Nf4b!#j%q6955MRTOt*~iR1EWn-27^p>T%^; zuY%E(C3TGy*Ffyleq21sT+QPQ#EJK~>1FJN%sAzAOS{J+#IIJcL(m3>n!f>k`bo+@ zbPsN-f<@UWou*nEW$(Zh(KFoCK&6GNa=I(jR_7s6q4kM=qmEg#CqQ6=;lC>5PjAJk zrAQDmto5l(8S$7;nS#U?>MN#oCcf6Z%Op$8o*h&A2s+K4$q@*-nq-omNaQWB;!h3A z23jIVdSv{5jBAa4Pqc?mIyvPQaD%@{bkPCsu1#e`N=G z3d327n=XmH8mF4JxLb>;5epQHGUaNhziR9@{o!eg?!fsT5~BpDUDrN^{u|U*XmDD< zD)J|Zn&9Dnic#@U(|c&HSp74%kQeOdw! z--*H<-ATx>h((%U19O4|!rg@R?Oo{$09eeLcQ}gCe)4+?Evh7fpp(Z*=)7PB6lGpd zauU+Q0v{laL!FZTTJk$#DCSp}dSt&b5E`HI1oESYVl?|5Rs zQawjSU)`To^WW#q>v&4yx|9A2;L#0x1$n^=tuxXXus}pS(swY~uAu2dTXYl34JgVG zzu2X|5}HNl@lkWWUoc-XvObZV4%XK-w!W+GphHH3ZT{s-qumAX`%^JP(4fcUoy{-6 z2zr6Mnm)SnL9y3a3*J+4jtqL4bx=0rFA*E;aTfFsJUm2+3n(aw2=fuILO{dyH(QY* zKBu;(pRVJO`gv9DxO=i7!(NLVmwF%EkDdxF{&Z&$-$ksS1U}SrxA)yI+u`RxO~?>! zs5pxatM0I~N=!-;Um@czYLN_(H!>K3p*^y}81a-^@L_CzV|+6ecEfPGnMr$)tpn7w zH)dV+bOkCn&hUy^z26LTb8|}Vg&j$KIM-$HziM>3mg!4DPuag1cF&!?lmRykudL3a zRi3Q_4>1?NieA^&>!(-+-~WKNGPSu+(>TN-i8)PKp0pVLoh#XcJp)>-QxI6J1lFu{ z*U1j?*K-Kjoqr|JERTlf?eO*WWg#P!U%Q0KiS{>kCEO3%-tsog1k0w$>Q_Y{aLjWidG^yeNsK4QgI@$`19y8&lS zLeTzkdw6C4DM}c^e{{?VQJBjkRonabk@#14Rwk+EI{Z5#qaMJVg(4QA88lR+^!v zVPQ{oPOZem*ZrQl7l68Gs87FsN08 zQZcnXft8CoLM@kk*yK98m;E^B5 zx7#>KxvF$QJoCqa6aRvB#FRl}lOHHx*arBPn&80Q-|>=xbzY)?5B+ z?$XmZ{zo}pCO0%{=4Ul-0JJYaeQa>B9NH6T{mufJfn-(q?rufDS5(`T?OMObDzj)8 z(#r#4A##yVjFyb&`{E2NJ+Q9bindAktPo#nzItrq=E zm5O77ZF2)~u;cI2m3MH2%0_{8dqTigDoJRNMVj_~uAyI?fw|JnsS=E8-7htI`kI3? zX>7+-)iJ78uny9!!n<#sLH=E#>Q2ZJ(M#`qXO%b#)8X&_|h13HHRYDXv_H$W0<(oB)R1#l8HbC3zf(SS0RMWJy&Gfw7GF%puFtNLoDUhIxF-Oc6g-0=rygvGM+1+WMFew>-4EyxcQI^k~&Q62;8?K?x$taK&-| zIIm@JH}U2_Yng!4$FtJKL__$UT{%U??gT|#^Gi$VNfvwa>qWY)z?Ma(1DkzN=eSv& zOb;ONL&#DM&r|Yu!6OflfA+*wFMH;7OY96xPEHDOIU(ajrgIr-MQjGgvF2@q4_9iH z?=7U&;!(HvEw_?emq~Vp`@~;N%ybS%OIN@}aq-zzdd{+wf7%ANgLoxH7E{xO_Gh6i z!R(eM%X&C1UGs8e0Hhmv9#JLaeBFbqTL?N?ML%z7e$egWD0FKiGg-{{sN<|c2-f#F z{#g~(lBel{_lVNYxf5u@5JMJfU6bp*NQ8+Mx*?7Ike6r{S1XHyq$6vC{g!r*{wTm_ zM(KQX&gCUVBB7SEZJz}jgG*Sft&qqBL&TvyCn{i|d)tiN(wJoYfz`i@nQQpBM@*po z1J@o4{D)iE_wFC_=0)x$^E+1A6U8qgC6Cy^;^25>l6ABfv0VKqb*rfVMAYd zo6T#+F(mJWoIYG6Nq3#~dwOd!zLEbf+)eBpm~wz&&503o5Rzs<7JO6QipWO#GG~aV{Iq!d{Gc%&PyH=>*LO~w$H8=zG`wXB5 zz>0ZRlyf<Z45kbtD;-w&1UV#DA{&m(~JOR)Jg01)_IE-DBje_2Cogp4B`-#167`zA(vAU z5eM^Q&4GL-ew+{R+fD^9b&XATTa=3{MqzkC8$qK)?(@jtKHbE?<`Yi)?J8Np8(3{< ze=gsG{X-}>+NDLQ!_}oJzvTdtTLc@{`mmt~d96E?X_|`alsU_n=Q5INF|YQqo<4(k zL)Z$I{JY0mp~+2H1ihSPO=_w5A2yjo@m6KIsi6+O3OeLhf)L?>8Vc3E61^3@BQH%E zKb4gVd!p*_S4pP`3MZ3@~a2h3&yUH8~N%a%6 zXKN2DpM-61dGBt#RoubrcQahE-QVW-LZUA=A2JmDS@OF(o02^{80FBEz{B3Z?>cEc zKLElD;dD!14r3!wTn{b#6FjaFoN|b~#&jw`EaD)Q#i7;f| zq;;%p9C+&7kxk?3H*8QZ5P9mGWxWZnb*rY;T7NVS_IL+HF5e*vJRE7%c(dPYF?S); z0Yxb~)= zcMRoQ#V=fcQJaPeD2n`RReS(k(l&VIMv_wqkjiinnh{`iJt<{ z)kO?!!P$b_##_-fWvk9bs9kaKSrWSvbp+1K^I8yf4xi*O*}Ltl02a<)W#RH@;}5+2WG`(^Tf_tHgrQr5V8oGCS)NoIC|j>6Nud}8OjV@g zbyzNOr*sjBio`FTe*B0`3VS1RIujQ)-M++yC_v{>e1S)WWtXX9f&pQTcE7$^{f!LN z!HMMAe|gl`919>gCPyM3JuxKR$MmjxR{IhPdAuT~#<;7DJ7oM_fKeGL?9l>H`2n(` zP@s1OL?^I~zY>OogI9uPK1%S#s;q^sR;1yIgZ^=WJgsHDf=Pm)vZ5i!4j98Jd&z6& zA7Obzp%4+0x#7VciApxNK(YlzL+y}pMGV*xV>8lC5DOBJ;s!u&>$X4ZMhraA8y*&0J+7SQr}LOW5CSD zWiU(;3$)g#BO@2YFZbd{T!&q*S&_b z-YOi5MB<2{6izzlLt(&<-gS9v-O18v9u?Q?E(vBbEbG(v&=P={kms)*^A=PAb62a5 zR~t<<{V8@Phk?ISe@U>ESE~GBnN5)l5i%77E|3$dl0>3wJ`yRGK5r1Rx;S(DbUV&7 zW%YisLw^k7>i-TWUQL6dOZT|v0tUM;-@`wV$rmrg?ttT0O%^f2*0@8xLh`4_8;$;S z@Kj>&f4|71#P&&j_zpM~f`BU*#~=W%XRNThTe$pp^%TwI)R2Kh+@vKiZA6yj?fJWNECB=PL)^;6d1&t&J zBKKl12mzvOCfdJuEG(IgQJyHfU$l!z80zTJIyOeuGq3uJu}DG7M+s=iIoqFd1JQf9 z11*Kqh;idVdY|J+LShNxmiDSHUHtukT{%NlzIzGS?0=U1cL}UNL{6VvB(G^JJ$ciY z^=3WPtqQrBR6P>r{hgwOp`{a62p|YBep#5rw zqn=8;K7V93%r!TiY?=8H-f#JT7Qf*FEb-fB40k$-+)#tJsE1Etk^Kpz8!9uOI2M_J zkdzBdq4(*0U%_)yW8@g4_?15U5^b?lI&I!*H;ux2BLpaEuB2PsJY=%p_;tFCWMLYs z1NPxID(gW!Kwow}!{|`1FSjz!c9Y47SrtEiZ2U7flTlgrc+3b1=~x0#mZn_S4}IoQ z43@)ZtzarhCz;kk=_D0uvR#P2Xif(!sd5Z$fuCc%e=OC5UbqDwepGPsNeTIMTGISc zgOBlX1G$}!j&FC-=H|E*k_4yuwMA>z@vlK~HG5Uf68Lv*^c{OudyVjz!ZB}T>Nq5Z zbwUQu&eVpGv9P0K9q23d-H1gz}{Z zNAPzHF0UYmbh*I8v6UyAy*m4DHjer+DQ2spUcB{^c;>F+Q08+vG+a}-fst~P(1TGC zRq+)O3At!axvq8XQHLYWx-ZZmIp4=;--Ij`<%|u!5o>-bP)9H8-0bE}cYI^>->!sH zk=eEnXQ!sjukv|8PZVcqU_1mpA~6kQQ@KkXKS+;v`5y0j8=E6l%T8odF6n8Lk?xq^ z$F6yOcW00837{Mx7zdVAd4+>Xsj}<1ha^D#Xgyxbdd2f{+|WTePJ2d?=L|U)L}4RY zg$D9^xo*BPNLc?dEI+Nfx0INl4XJ#?Xg`fdat?GOfCLu)fx;4*-ppT(&Po+{*qDE~ zMT>JEKhH$i1a@X4>H=Zt&1csSi6P5$FTucll>Z|x3)tOf-i}D{xR>qgio~n?2?kk~ zFKU@k4#4`+H}I8_O8kf1nM?{zo0R__jhLGKrXHDSvz$0#lAIevnr*j~;4Ycg& zI6TA$k-P#}m9i8ozxOVZW;F}Hi7(D*}uznG1^st)%xu5lY`Qx_jQ*v(U59;BR4Qx}>fDKwWuAC-n9MDs-1rUJFn_`S5~gi!T6m3*lg;=>At zj4{|9a9mdkafG1{pP|yE0o@rIYipsa1LSM`D*jBKktDtj1zq5sy|F59ely@s2Nait zhkRAjzMt!oB=&$@%drOI66=);Cs1Wklk*&VoS!w0_?i$vZ7VdSmuTW6QYi>&)bI=v zZ9>}&-ev+dtS#98Bs$?ILYk)a2B}WI_B#HLkZX=KH7+BO?CtDS`MW#$HE%`m2QHGO zT$0?d_ZmbcK|@t78x&3ss0LTZEkIr#I375qw+HvB5q5w(o&=owEOT?bbYgs^O`0!g z-a^eA$Ce3HG&_O(>WzpA|oWpXP@Z|@9uh(3Ke%^8XyHC%l?x+d@vW-&K> zS1+NMm~CitS1n(?M}jVod8wrP;giN6uZkHhMWt8YFvs(MInv-bYa0|NL9#*0?nad1 zK04)+e}EYUMv?Bo5Z-emn=$K(~4<**fQ_GQq4J?GVYB?-xk-B;@-e-8dDFq-GNoFEa_o1Mn?`fL?7;d;bU+ zh^Z09xD4bD$`P9h%9V(!sAQWN%X(i&`fxvZ_W3-r9n+5wLg9S5?6>^hPS7hr)oVN( z|1PR?iIrBS3@fk7!haUk5knFT+U9pbZQG&XV6GZf$E&ZsCj2(?ib8piQ5BJX&g1ZV zf+ibpRRhR4A^JQV@9xuAzM&7S1WG^7ChD(_nZ*1tyc~ytAQktID}dfX$qo(BkI@Da zMXB8=OgBF7{&p0{cHVkhjXNPWl7C#v_qwvj%(EQ$eGD~DkDGS{dp8e&dRh&ACR%x& zvdPa9g#D|=*_O}hETA18)n;|4*gZPUJjLQKfm$tmPVn72uV^$jbQbNc(AH=WSa-mb zYQAXC@S}sXQq5CP?*wYR9`%|gdO@1Vga?Fbz!YutQ9f6}O8E;ous|=2;o5nwb(>qB zb-y3kNh)zm)Qcj;X}(&b24tkULdB1`;QBN43oRug;mr&qVckiP$(x=S)(T}mK`KuZ zC5Cht4~Hg@=DO!Jec1FeY_69o>vs?M@#AB-kqqbvonY|~?*#wK2S})jQ)PWkADPo! z(@Xm?o6Xg_j`IjNOr-%HfNaXmm|FC_&7=_SA>cr3Vh^`5@JS`EhDtpD=b;Jt);ulG z2;YGY*{cbmxGHx+D%}#B;peQ5zDiveG-~`0E5xDsz2!pNl#25GFr^NDc;T;lEQejZ z3s9i&&w<^I6WqG_VXQQu7ck!7Zxrtb-q;K%8Rbw?daG%MXpiGaYVI8Qsm6b9Xiqj7 zthE8Q#FpMO2lsOCOW98Uxcny<2}2daLxr+XW(tUBEV$`O2o97-2Ii+Dr$s_)vO9>r zbVdJZmP&|V0~X(JsKtYd4`6GvYh660ys>WX_)vA3?><%i+RzVx>@ zE0)%o6wFl9@4(7q!6B;*M84M;3*@!9Wzof=zutGnNrtmR|6r_nyT)#RxOVR36v%&& zV6Z#&{jy%}9(Q#;H3PhCV6zHec|el&umiB zCI<4XohqY5DoHn#yHCtvt1^mri!BbnPeKC?wU#>#5LKZ5w;Zu1g{0j?KjJ;3AF@6l z?CksWiQR50IBEge!U;+J$T#%I2I5_#qVA+TAmc#IL8_mwwTtHD{JC8AIH71%>L|mj zb3zY61>D{eP&618Np<5|*RCf9wQb+$b&cuLWwS6}MIFzq%*c$LQv4bXx5JT%V;s`YeZs#>A^ET(T*&F%| z4%;6FI$Z-mHD0vC11i@DF2i_D$_~7C&JX%!uH2FUsn<*9hHqA-`FvcVLy?`;9V;L> zn1E<%bS(Sw`I6_6J(B!L?~Pq#7bUVoM9c8?$n7q9>GC9~x*PFY?;R|P6^95MOB@P{ z#m~*pY^VLy%V>ZmPtNB(4tv4U>aO07bCrGXFmx$D-F`?$6Y>}R(GlW6i>StwW34x8dolg zw++`9rQNmk<2ip{F%a%`FMl--tusazP*+itA#y-i&%pfnGSF_&J)&yE?1zCo;@ja% z7X8KIj{2Mml#gl@{+%j%D4OVwibKpEql_x03iQIr<@?68EK6@5*`bU7v67J&wAVfv zceg6@RA&ym-^QhsZQ^35Sb^M#Oib)au$0$Mbj4LQAwSI+e6m366bxw{Ds8j!J49u0 znLQsU(rVN}_ThZ24DS^5HsBZBFOnA4vitx1l2Upc-BMsfWd*dMFNN{{AO>woQadC8!h!F1z)m{ln+8|>;S{PiqR=tdsaEj+7$%bk*G23mI z)CG=|Oh*z&pw}GQ( z_5scxgA~kIDM4*VGwTOMPLL*^Oj%SdaBQ`rSpLOi5}UBu12Ym5(Z%U^&&yXk zbcsD+h!@p%_DZIO!ZMoju>K(^-=Or&@!(L@#G>)R1PU&a89i6nFBn)6Y1Pd8t4bVQ z-HZEF<=85-C|1b*oT+aTkMmdP@j1DJcG%BuGNJ;DFXn*(HhNN`wM*GMZMfC^1Qv_U zWFl=HrNoDY56$Lk&LfShq(Zx;(w|FG>d*q~%I`%Lk!cBUu=x^%hjgIYw|T|9gzV2? zOl;)%Ue-uOaBB$l21z%xxi&iMjp#IaZhX0D^n4L;eMZD2JCIL`rDXRdCp$>edxn_N zxY=lYq3B=w~Ua0IVjIAwFD438KqCaNpwz1@09$KLKi)Z)K0?B7-lo~)D5p{h@r z&XSi^dkJ)}Nhc6M0uGe?xe<2^N#sc;QeBEZ_1&^w-TzJp4zw3;gNLI*A%8+rKvjt< z5rImSWKz*iecYh{oEk$IPpDNdTY-V{ymp7lBjpoyF`c$j?tRP{jx_s+qUK(L7h)1- zjn%=l{cfKi(h>L8woilf%=yLkfpaD|+`H)E3Iu_}SE3P_3jJ zHu|1GT&Y@$!Td;*zGldJ{jlTm$SR3(&*7hwqO)kv>lja{{=avbKl>En6!zCuFXjqr z2VX~!?gI{Mw=5y$);TXxw)|1MbixtSA=Zvh=oW2oE|Q-jE2^7e9rWsehYW6oVD|EJ zrMSL&%1K2B-pnEVfVcz8PhV-S4V?rHt+Nf+1*Ph~p3R673d(CB18s!CJZrov8=QVW zS&q9b(LUP`!|_7C1|;^x+7aGr9rA4mS+#SAi2tcTCwCgpYic1X1Fp7XW0+@me3YX z1Sj47)@1)^1Z%Ql?Ye3nD$_U-3~Y;wGCMQ7M|pkRQJnuhcw<~#3NJGUF;`D=>xNoz z=ypI&T0+JW~@M*>!W0Z&n#UnXf7RJeTR|&M-5@(aU|W8}^p=z)~x-`Xr)4Y^rSELOc!e=!6bS_bGV=oDfoUG;1Hs8@l)~m4Db{(G5em-h*ntC! z-q?HKy5TnuOD60OdK6h$;*Cv$C^D_Z0z8O0^l^RPQdOzr2sSEeDIoDz^E$3AmM&sd zgR^_J$Pio2#i_r^N4&Vd^I0qK=h;onZNXYOz_o2|Xp8l)YXZHH<_*x!} zj~_pX!Br_YeMzp_c|C!;Xt{<`S$RVdlV%3^({zU_nLv$x-e?cv#Pv$8!pUInLsxA% z%#WuN5g^)FLwP2ss~9)M4mjjDqW_M*|xR@euwrlxL$v5lk3XO zL;bHA*BuihX7e32YLfv3ssy%+u)fnRr{G}7hc4%eANlHoc55gNe_NMqmYd20p3Q1` zYT3&8`{1e^rAo@JtirLtt_(GT5z-7$7H+~TXjT%fInGyr81?J9< zXx5#mZDT!1#jWaU>gXPq!D)q?humP}0qOktq0y17q9y9;MJp$bf?#Nqvn^c^B-~hQ7JPf(e|;N}kis`-7TTt^TuK*+r>M&g z|NePK_W0k3TqB5yn+;KYM9jeb_*n-}8C?YW1{Ds0R6}6fG?4|+qsq39HPCfNZ|nt5 zGI-#yY66nZ0XKgyL~^%Z2in8b2PVP7onYvHdm^yyg&tV~z)si%P~7ZH!~uZHCo1=7 z@?q0hB~LAvfb05u3&x8ExWzf^;6z)ri?Fr$co1N=K=r?b?R)MR;`gyJXZ`H@!p2_CxI)F>i->e*ORZc*B z=*z2QZSl2zZyXF^;1=jn_c&X%8$7r-Z-QR_1};$lG=U;EZwMQN-?E>`PFFEew$e(c zh`R3yhNllrWQ2hUh56}HO2^w9?b@Nt0|@g=hH&%*QMr1uGw9EDYqEH8GCk#qyds$y zu_!xYaInNosq>OprZ~;6NdT4a%Bu&*@#_5vw5U!yP{N#Mvd##O zJ)w$2t0nK#Hbo`0g)!Ps#TR zf=|;$v0{)1vpdLi=GoNKVOe}9z^mTV*qu%4141gq|1Wt_MG(^k%||t-!x9n9?%U9# z@i2QIJZPA#NI;7xcuU~7FZ3SJ-A}1q<{A6|k3|4x^^C{+U=Z%QkGD7Vcz`_-Z4!i7 zUGGdpfZv12FmpT{^({b%EVWA~*8UklL{uPS6nwx=>rLu`;w~fOra?Cl!8Ofnpu^CO zS#V`}y+hI^=#pPjQrHs!3easma9grP8{F;F ze9Z!d7pNbdIlfsjTXWKv70oL`9Y4Y`V$O}Qfj^)08px#*FR<*bfp}8_@~Au%Lnd)X5NQm`>or@8kvkc zIvEvQh#Hv}Tz4PYBsk8dnXGVZveGZa1rqdyxIp&OzDVvDSkPe57gzwI@C6niY4`#Q zklFqJz+2!e15mT0r%XAr;9D-`T-I>F*RyAuXBj-yxDtL|VVZ9(qt8kdFwL_s?aa-; z(8c_Vya2cTiyVBB11Nm^A_rgO;ENnU6!;`x%fW;QrrP` literal 19584 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 UCCo42IW?5I^$*#5j{f|A0O*-0MF0Q* diff --git a/apps/termtris/evidence/termtris_panels_3200.png b/apps/termtris/evidence/termtris_panels_3200.png index 0e67bdef2ce76e0fe832a612762286c675334328..e6767843cbec9c8759344e601dcd5eac9a5db8a3 100644 GIT binary patch literal 25726 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 diff --git a/apps/termtris/evidence/termtris_quit_3200.png b/apps/termtris/evidence/termtris_quit_3200.png index 24bcb9b5802a35f676b56651afa278dfedc83624..98f98978d6dd4ef4e90ba1feb60c1771d1dafefd 100644 GIT binary patch literal 22908 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 A?EnA( literal 19441 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 diff --git a/harness/mame/TERMTRIS_REPORT.md b/harness/mame/TERMTRIS_REPORT.md index ba7c056..6bf2b52 100644 --- a/harness/mame/TERMTRIS_REPORT.md +++ b/harness/mame/TERMTRIS_REPORT.md @@ -12,15 +12,19 @@ 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 +OUTNAME=termtris 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: +file as success and retries a missing one. A host kernel without +`CONFIG_X86_16BIT` cannot give the 16-bit OS/2 tools their LDT segments; the +wrapper detects that and runs the same loader through `qemu-i386`, whose +linux-user emulation implements them. Two more 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 @@ -64,8 +68,8 @@ a single `XFlush`. This is not an optimisation for looks: * 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. + a full redraw a few hundred bytes and `loops` reached 650 between frames + 2500 and 3200 (VR319 default). ## Timing and keys @@ -82,7 +86,7 @@ which draws a bogus piece over the top row (blank border plus stray `X` tiles). ## Reproduce ```sh -cd /home/arti/.local/share/opencode/worktree/26e92e/termtris +apps/termtris/verify.sh # the whole acceptance run apps/termtris/tests/play.sh # load, click, snapshot apps/termtris/tests/play_keys.sh # move / panels / quit apps/termtris/run.sh # interactive MAME session @@ -93,16 +97,21 @@ The run is `VT1200_SKIP_LEVEL1=1 LOADER_AT=2500` with the loader payload at bit `dwt/loader/build/loader.bin`, which places the sections and calls `create_process`. -Last full acceptance run: +Last full acceptance run — MAME 0.289 built from this tree's driver at the +**VR319 default** (1280×1024 active), which is the real unit's monitor. The +earlier figures in this report were VR150 (1024×864): ``` -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 +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 termtris window, and `quit=1` is the game's global after -`q` — so the keys reached the game, not just the window. +`q` — so the keys reached the game, not just the window. The event counts and +the `tick=887` are unchanged from the VR150 run; only the loop counts moved, +because the VR319 raster changes the emulated frame rate. The evidence PNGs +were regenerated in this run and are now 1280×1024. | evidence | file | |---|---| -- 2.47.3 From 4c7d94ea3408b17db863db0805ec9babab2a18c5 Mon Sep 17 00:00:00 2001 From: opencode Date: Fri, 13 Nov 2026 06:15:16 +0000 Subject: [PATCH 17/18] fix(termtris): fix the build runner and the stale-image path Review findings on the port: - run() expanded ${LX_RUNNER} while it was unset on the normal path, which is a fatal unbound-variable error under `set -eu`; the probe only sets it on kernels without 16-bit LDT support. Default it to empty and expand with ${LX_RUNNER:-}. - Clear each stage's expected output before every retry and require a non-empty result, so a stale or partially written file cannot be mistaken for success. Print the loader's captured output when a stage really fails instead of discarding it. - Widen the probe to recognise "modify_ldt" as well as the exact abort message. - run.sh --build passed no OUTNAME, so it built build/main.coff and then ran the tracked build/termtris.coff. Build termtris.coff. - Stop tracking the generated build/termtris.coff and .map: they are rebuilt by every acceptance run, churn on the COFF timestamp, and fed the stale-binary trap above. Add them to .gitignore. - Correct verify.sh's header comment (it runs all tests, then fails). --- .gitignore | 1 + apps/termtris/build.sh | 21 +++-- apps/termtris/build/termtris.coff | Bin 15186 -> 0 bytes apps/termtris/build/termtris.map | 141 ------------------------------ apps/termtris/run.sh | 2 +- apps/termtris/verify.sh | 4 +- 6 files changed, 19 insertions(+), 150 deletions(-) delete mode 100644 apps/termtris/build/termtris.coff delete mode 100644 apps/termtris/build/termtris.map diff --git a/.gitignore b/.gitignore index c7909c8..f7f02ef 100644 --- a/.gitignore +++ b/.gitignore @@ -77,4 +77,5 @@ apps/termtris/build/smoke.* apps/termtris/build/fmt.* apps/termtris/build/render.* apps/termtris/build/input.* +apps/termtris/build/termtris.* apps/termtris/evidence/loader_frame*.png diff --git a/apps/termtris/build.sh b/apps/termtris/build.sh index 9d0d407..27abea8 100755 --- a/apps/termtris/build.sh +++ b/apps/termtris/build.sh @@ -40,9 +40,11 @@ fi # drive the same loader through qemu-i386, whose linux-user emulation does # implement the 16-bit LDT. Set LX_RUNNER to a command prefix (for example # "qemu-i386") to force one; leave it unset to auto-detect. -if [ -z "${LX_RUNNER:-}" ]; then - if ( cd "$WORK" && "$LX" ./gspac.exe ) 2>&1 | - grep -q 'failed to allocate LDT selector'; then +LX_RUNNER=${LX_RUNNER:-} +if [ -z "$LX_RUNNER" ]; then + probe=$( ( cd "$WORK" && "$LX" ./gspac.exe ) 2>&1 || true ) + case $probe in + *failed\ to\ allocate\ LDT\ selector*|*modify_ldt*) if command -v qemu-i386 >/dev/null 2>&1; then LX_RUNNER=qemu-i386 else @@ -50,7 +52,8 @@ if [ -z "${LX_RUNNER:-}" ]; then echo "build.sh: install qemu-user, or set LX_RUNNER to a working runner" >&2 exit 1 fi - fi + ;; + esac fi for so in "$NINE"/lib*.so; do [ -e "$so" ] && ln -sf "$so" "$WORK/" @@ -97,14 +100,20 @@ run() { _out=$1 shift i=0 + _log= while [ "$i" -lt 5 ]; do - ( cd "$WORK" && ${LX_RUNNER} "$LX" "$@" ) >/dev/null 2>&1 || true - if [ -f "$WORK/$_out" ]; then + # A stage that crashed after a partial write must not be mistaken + # for success, and an output left by an earlier build must not + # hide a failing stage now. + rm -f "$WORK/$_out" + _log=$( ( cd "$WORK" && ${LX_RUNNER:-} "$LX" "$@" ) 2>&1 || true ) + if [ -s "$WORK/$_out" ]; then return 0 fi i=$((i + 1)) done echo "build.sh: stage failed to produce $WORK/$_out: $*" >&2 + [ -n "$_log" ] && printf '%s\n' "$_log" | tail -10 >&2 return 1 } diff --git a/apps/termtris/build/termtris.coff b/apps/termtris/build/termtris.coff deleted file mode 100644 index 5305e97fea9674c3e6e649345aca03011ec28e11..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15186 zcmcJ04Rln;m1aFjP3!Rxv_RM`oS$b<)A|7+Gm{<6VAFEcL5<$Sn#9Taj7`)+Y9t{c zbtD1KE;DXpi$>in#%MW-6Ytr!WlbZ5fw9-4Jv-U>tt|<7Z5z)x@vgnI6OGr3BxH1) zP3*~^^Y*({{fkLvJaf`I^rvuDeO?J?YvKV{ANT6@x?Hx0X!!lo2cclI&== z$u>}~*W0RWDT)@yL&UAhJ;WE=Yx;5}#^z-fib=%LPsMe+;%AGeh?<(GzO!A?^Db3*C1d`?)#2_Tx882E+tBt+#!$Z`BKSf>ZUON?UR4ca~*k;)x;iF)T&LyS-itiT~fq~+jEq!|e zh|r3Finu)l+Gs`SeRX)YF2j$DAHBW3sVHQNNQHVKsQF+y^`f#}|9JxIfpK34++Twh z-V1UFyX+&tyyrVbn?4vIgwxf9Lghg0_3m$2r$^%XDOtLwlQ!c7(^kNO0lpnB3!LF zIVL@}CQs}1M%IMGZMAbJ=9Is)D$O$`>#@hzV)uUzZ_)s_nDr$q%`>+y$9t)H`PL

&N(ePT6I-G3r`SbgS9E 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 diff --git a/apps/termtris/build/termtris.map b/apps/termtris/build/termtris.map deleted file mode 100644 index 092b59e..0000000 --- a/apps/termtris/build/termtris.map +++ /dev/null @@ -1,141 +0,0 @@ -***************************************************** -TMS340 COFF Linker Version 5.01 -***************************************************** -Thu Nov 12 05:18:17 2026 - -OUTPUT FILE NAME: -ENTRY POINT SYMBOL: "_start" address: 018a0000 - - -MEMORY CONFIGURATION - - name origin length attributes - -------- -------- --------- ---------- - prog 018a0000 000020000 RWIX - ram 01a00000 000040000 RWIX - - -SECTION ALLOCATION MAP - - output attributes/ -section page origin length input sections --------- ---- ---------- ---------- ---------------- -.text 0 018a0000 00014a60 - 018a0000 00000150 ENTRY.obj (.text) - 018a0150 00001db0 MAIN.obj (.text) - 018a1f00 0000ce50 GAME.obj (.text) - 018aed50 00000d20 SCOREDB.obj (.text) - 018afa70 00002a40 X11.obj (.text) - 018b24b0 00001640 COMPAT.obj (.text) - 018b3af0 00000210 XGFIX.obj (.text) - 018b3d00 00000450 rts.lib (.text) - 018b4150 000001a0 rts.lib (.text) - 018b42f0 00000230 rts.lib (.text) - 018b4520 00000150 rts.lib (.text) - 018b4670 00000120 rts.lib (.text) - 018b4790 00000110 rts.lib (.text) - 018b48a0 000001c0 rts.lib (.text) - -.cinit 0 018b4a60 000012d0 - 018b4a60 00000050 MAIN.obj (.cinit) - 018b4ab0 00001190 GAME.obj (.cinit) - 018b5c40 00000050 SCOREDB.obj (.cinit) - 018b5c90 00000050 X11.obj (.cinit) - 018b5ce0 00000050 rts.lib (.cinit) - -.bss 0 01a00000 0001ec20 UNINITIALIZED - 01a00000 00008000 ENTRY.obj (.bss) - 01a08000 00000420 MAIN.obj (.bss) - 01a08420 00004a40 GAME.obj (.bss) - 01a0ce60 00000b80 SCOREDB.obj (.bss) - 01a0d9e0 0000f200 X11.obj (.bss) - 01a1cbe0 00000020 COMPAT.obj (.bss) - 01a1cc00 00002000 XGFIX.obj (.bss) - 01a1ec00 00000020 rts.lib (.bss) - -.data 0 018a0000 00000000 UNINITIALIZED - - -GLOBAL SYMBOLS - -address name address name --------- ---- -------- ---- -01a00000 .bss 018a0000 .data -018a0000 .data 018a0000 .text -018a0000 .text 018a0000 _start -01a08a20 _blackpf 018a0000 edata -018a3060 _cleanup_game 018a0150 _now_ms -01a083a0 _dbg_events 018a03d0 _vt_rand_seed -01a083e0 _dbg_focus 018a0420 _sleep_ms -01a083c0 _dbg_last 018a0580 _vt_poll_event -01a08380 _dbg_loops 018a0ef0 _vt_handle_event -01a08400 _dbg_rev 018a1820 _wait_display -018b24b0 _fflush 018a1860 _main -018a8600 _game_input 018a1f00 _init_game -018a1f00 _init_game 018a3060 _cleanup_game -018a1860 _main 018a3250 _update -018b3d00 _memcpy 018a8600 _game_input -018b3ea0 _memmove 018af0b0 _read_scores -018b4150 _memset 018af430 _save_score -01a089c0 _monochrome 018af9c0 _print_scores -018a0150 _now_ms 018afa70 _vt_root_window -01a08a00 _onlyascii 018afb50 _vt_x11_init -018af9c0 _print_scores 018b0300 _x11_clearscr -018b2520 _putchar 018b04c0 _x11_reset -01a08960 _quit 018b0500 _x11_cursor -018b42f0 _rand 018b0560 _x11_setcolor -018af0b0 _read_scores 018b0680 _x11_setcursor -01a08a40 _rotstep 018b08d0 _x11_ibmchar -018af430 _save_score 018b0d50 _term_putstr -01a08aa0 _scr 018b0ef0 _vt_x11_flush -018a0420 _sleep_ms 018b2120 _vt_x11_term_init -018b2d30 _sprintf 018b2440 _term_init -018b4450 _srand 018b24b0 _fflush -018a0000 _start 018b2520 _putchar -01a1cbe0 _stdout 018b2d30 _sprintf -018b4520 _strcmp 018b3af0 _vt_install_xg -018b4670 _strcpy 018b3d00 _memcpy -018b4790 _strlen 018b3ea0 _memmove -018b48a0 _strncpy 018b4150 _memset -01a1cb00 _term_clearscr 018b42f0 _rand -01a1cb40 _term_cursor 018b4450 _srand -01a08a80 _term_height 018b4520 _strcmp -01a1cb80 _term_ibmchar 018b4670 _strcpy -018b2440 _term_init 018b4790 _strlen -018b0d50 _term_putstr 018b48a0 _strncpy -01a1cae0 _term_reset 018b4a60 cinit -01a1cb60 _term_setcolor 018b4a60 etext -01a1cb20 _term_setcursor 01a00000 .bss -01a1cbc0 _term_type 01a08380 _dbg_loops -01a08a60 _term_width 01a083a0 _dbg_events -01a1cba0 _termenv 01a083c0 _dbg_last -01a08980 _tick_interval 01a083e0 _dbg_focus -018a3250 _update 01a08400 _dbg_rev -01a089a0 _use_bell 01a08960 _quit -01a089e0 _use_gfxchar 01a08980 _tick_interval -01a0ce60 _username 01a089a0 _use_bell -01a1ca60 _vt_dpy 01a089c0 _monochrome -01a1caa0 _vt_gc 01a089e0 _use_gfxchar -018a0ef0 _vt_handle_event 01a08a00 _onlyascii -018b3af0 _vt_install_xg 01a08a20 _blackpf -018a0580 _vt_poll_event 01a08a40 _rotstep -018a03d0 _vt_rand_seed 01a08a60 _term_width -018afa70 _vt_root_window 01a08a80 _term_height -01a1ca80 _vt_win 01a08aa0 _scr -018b0ef0 _vt_x11_flush 01a0ce60 _username -018afb50 _vt_x11_init 01a1ca60 _vt_dpy -01a1cac0 _vt_x11_progress 01a1ca80 _vt_win -018b2120 _vt_x11_term_init 01a1caa0 _vt_gc -018a1820 _wait_display 01a1cac0 _vt_x11_progress -018b0300 _x11_clearscr 01a1cae0 _term_reset -018b0500 _x11_cursor 01a1cb00 _term_clearscr -018b08d0 _x11_ibmchar 01a1cb20 _term_setcursor -018b04c0 _x11_reset 01a1cb40 _term_cursor -018b0560 _x11_setcolor 01a1cb60 _term_setcolor -018b0680 _x11_setcursor 01a1cb80 _term_ibmchar -018b4a60 cinit 01a1cba0 _termenv -018a0000 edata 01a1cbc0 _term_type -01a1ec20 end 01a1cbe0 _stdout -018b4a60 etext 01a1ec20 end - -[77 symbols] diff --git a/apps/termtris/run.sh b/apps/termtris/run.sh index 495d30d..b877824 100755 --- a/apps/termtris/run.sh +++ b/apps/termtris/run.sh @@ -16,7 +16,7 @@ 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" \ + OUTNAME=termtris "$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 diff --git a/apps/termtris/verify.sh b/apps/termtris/verify.sh index 5d0420f..b7bdf5a 100755 --- a/apps/termtris/verify.sh +++ b/apps/termtris/verify.sh @@ -4,8 +4,8 @@ # apps/termtris/verify.sh # build MAME, then run every test # apps/termtris/verify.sh --skip-mame # use the emulator already installed # -# Exits non-zero on the first failed check. Each test's full output is kept in -# apps/termtris/build/verify_.log. +# Runs every test even after a failure and exits non-zero if any failed. Each +# test's full output is kept in apps/termtris/build/verify_.log. set -eu DIR=$(cd "$(dirname "$0")" && pwd) -- 2.47.3 From 58f8f4491ce8c2566b081e880beacb4b5e1b0164 Mon Sep 17 00:00:00 2001 From: opencode Date: Fri, 13 Nov 2026 06:15:17 +0000 Subject: [PATCH 18/18] docs(termtris): correct the build commands and solved addresses Review findings on the documentation: - The build blocks did not cd, and used paths relative to the repo root while build.sh resolves them from the caller's cwd; VT1200.md also omitted OUTNAME, so it produced build/main.coff. Show `cd apps/termtris && OUTNAME=termtris ./build.sh ...`. - The link map quoted .cinit at 018B4B10; it moved to 018B4A60 when the Expose branch was removed, and the loader log confirms it. - VT1200.md still credited the seed portability edit to now_ms(); the code uses vt_rand_seed(). - Treat 0x7F as Delete and 0x08 as Backspace, and stop claiming FocusIn/FocusOut tracks the game's own manual pause (it does not). - Say that focus=00400001 is sampled on the click's ButtonRelease; quit=1 is the key-delivery proof. - Record the real ExposureMask consequence (an occluding window is not repainted) in VT1200.md's Limits and as a deviation in the design spec, instead of only in HANDOFF.md. --- apps/termtris/HANDOFF.md | 19 ++++---- apps/termtris/VT1200.md | 45 ++++++++++++------- .../2026-11-03-termtris-vt1200-design.md | 9 ++-- harness/mame/TERMTRIS_REPORT.md | 12 ++--- 4 files changed, 51 insertions(+), 34 deletions(-) diff --git a/apps/termtris/HANDOFF.md b/apps/termtris/HANDOFF.md index bf453a8..6b8fc65 100644 --- a/apps/termtris/HANDOFF.md +++ b/apps/termtris/HANDOFF.md @@ -1,10 +1,8 @@ # Handoff — termtris running inside the VT1200 Everything here is on branch **`termtris`** of -`https://git.wut.ee/opencode.energia.arti.ee/vt1200.git` (remote `origin`). -The branch was developed in a worktree at -`/home/arti/.local/share/opencode/worktree/26e92e/termtris`; any checkout of the -branch is equivalent. +`https://git.wut.ee/opencode.energia.arti.ee/vt1200.git` (remote `origin`); any +checkout of the branch is equivalent. ```sh git fetch origin termtris @@ -14,9 +12,10 @@ apps/termtris/verify.sh `verify.sh` is the whole acceptance run: ROM checksum, toolchain check, MAME build (skip with `--skip-mame`), the six tests, the acceptance summary and the -evidence list. It exits non-zero on the first failure and keeps every test log -under `apps/termtris/build/verify_.log`. `RUNTIME` is roughly 12 minutes -once MAME is built; the MAME build itself is the long part. +evidence list. It keeps every test log under +`apps/termtris/build/verify_.log`, runs all six tests even after a +failure, and exits non-zero at the end if any failed. `RUNTIME` is roughly 12 +minutes once MAME is built; the MAME build itself is the long part. ## What is done @@ -76,8 +75,8 @@ The r5.01 tools are 16-bit OS/2 `NE` programs. If the host kernel was built without `CONFIG_X86_16BIT`, `lx_loader` cannot create their 16-bit LDT segments and aborts at its first one; `build.sh` detects that and runs the same loader under `qemu-i386` (install `qemu-user`), or honours an explicit -`LX_RUNNER=`. This box's cloud kernel is one of those; the generated COFF is -unchanged. +`LX_RUNNER` command prefix. This box's cloud kernel is one of those; the +generated COFF is unchanged. `verify.sh` checks the ROM and the 2ine loader before doing anything, and `harness/mame/build.sh` re-registers the driver in `mame.lst` and builds the @@ -128,7 +127,7 @@ apps/termtris/ ├── src/vt1200/ main.c x11.c xgfix.c compat.c xlib.h scoredb.c *.asm ├── tests/ smoke fmt render input play play_keys (+ .lua drivers) ├── evidence/ screenshots -└── build/termtris.coff the built image (tracked for convenience) +└── build/termtris.coff the built image (ignored; rebuild with build.sh) ``` The tests build from source every run (`play.sh`/`play_keys.sh` rebuild diff --git a/apps/termtris/VT1200.md b/apps/termtris/VT1200.md index 104a283..a040ec4 100644 --- a/apps/termtris/VT1200.md +++ b/apps/termtris/VT1200.md @@ -13,7 +13,7 @@ loaded into VT1200 RAM and executed by the TMS34010. | piece | file | role | |---|---|---| -| game rules | `src/game.c` (upstream) | unchanged logic; one portability edit (`srand((now_ms()))` instead of `time(0)`) | +| game rules | `src/game.c` (upstream) | unchanged logic; one portability edit (`srand(vt_rand_seed())` 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) | @@ -25,16 +25,19 @@ loaded into VT1200 RAM and executed by the TMS34010. ## 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 +cd apps/termtris +OUTNAME=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/

.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. +`gsplnk` through `lx_loader`, and writes `build/termtris.coff` + `.map` (without +`OUTNAME` it uses the first source's name). 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 clears the +expected output, treats a non-empty 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 @@ -50,11 +53,14 @@ Link map (relevant addresses are **bit** addresses, like everything the TMS34010 sees): ``` -.text 018A0000 .data 018A0000 .cinit 018B4B10 +.text 018A0000 .data 018A0000 .cinit 018B4A60 .bss 01A00000 ENTRY POINT SYMBOL: "_start" address: 018a0000 ``` +The `.cinit` address moves when the code changes; this block matches the +current build's `build/termtris.map`. + 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. @@ -99,14 +105,16 @@ 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). +`w`/`s`/`d`/`a`. `0x7F` is Delete (Backspace is `0x08`). `FocusOut` sends `p` +and `FocusIn` sends `p` again, but the flag only tracks focus, not the game's +own pause state: pressing `p` and then moving focus away resumes the game while +it is unfocused. `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`. +`X` tiles. The acceptance run never obscures the window (the Terminal Manager +sits beside it), so the mask buys nothing there. See `HANDOFF.md`. ## Running it in MAME @@ -154,9 +162,11 @@ 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 +`focus=00400001` is the window this process created, sampled on the click's +`ButtonRelease` (it is the only window in the run). It shows the click reached +the right window, not that each later key did; the key-delivery proof is +`quit=1`, 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. @@ -176,6 +186,9 @@ loader/keys still act at frames 2500/3095–3190. 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. +* **An occluding window is not repainted.** Because `ExposureMask` is off (the + map-time `Expose` bug above), cells covered by another window stay stale until + the game next draws them. The acceptance run does not exercise this. The ROM (`vt1200_v22.bin`, md5 `b46a3e442ccb86134d2356bd70d22fd2`) is never modified; the port lives entirely in RAM. diff --git a/docs/superpowers/specs/2026-11-03-termtris-vt1200-design.md b/docs/superpowers/specs/2026-11-03-termtris-vt1200-design.md index bc4ef38..ec35c87 100644 --- a/docs/superpowers/specs/2026-11-03-termtris-vt1200-design.md +++ b/docs/superpowers/specs/2026-11-03-termtris-vt1200-design.md @@ -96,9 +96,12 @@ 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. +StructureNotifyMask|FocusChangeMask`; the event loop drains `XNextEvent`; +`XLookupString` gives the character/keysym. Arrow keys become `ESC [ A..D` for +`game_input`; `FocusOut` pauses. **Deviation (implemented):** `ExposureMask` +was dropped because the map-time `Expose` runs the game's full redraw before a +piece exists and paints a bogus piece; the consequence is that an occluding +window is not repainted. See `apps/termtris/HANDOFF.md`. 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. diff --git a/harness/mame/TERMTRIS_REPORT.md b/harness/mame/TERMTRIS_REPORT.md index 6bf2b52..75a5742 100644 --- a/harness/mame/TERMTRIS_REPORT.md +++ b/harness/mame/TERMTRIS_REPORT.md @@ -11,8 +11,9 @@ Full port documentation: `apps/termtris/VT1200.md`. ## Build -``` -OUTNAME=termtris apps/termtris/build.sh src/vt1200/main.c src/game.c \ +```sh +cd apps/termtris +OUTNAME=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 ``` @@ -34,7 +35,7 @@ linux-user emulation implements them. Two more quirks are handled there: Result (map): ``` -.text 018A0000 .cinit 018B4B10 .bss 01A00000 +.text 018A0000 .cinit 018B4A60 .bss 01A00000 ENTRY POINT SYMBOL: "_start" address: 018a0000 ``` @@ -107,8 +108,9 @@ 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 termtris window, and `quit=1` is the game's global after -`q` — so the keys reached the game, not just the window. The event counts and +`focus=00400001` is the termtris window, sampled on the click's `ButtonRelease`; +`quit=1` is the game's global after `q`, and that is the proof the key reached +the game, not just the window. The event counts and the `tick=887` are unchanged from the VR150 run; only the loop counts moved, because the VR319 raster changes the emulated frame rate. The evidence PNGs were regenerated in this run and are now 1280×1024. -- 2.47.3