# VT1200 execution harness (MAME) A minimal MAME machine that loads the VT1200 firmware into a TMS34010 CPU so we can **run it under MAME's debugger**: instruction trace, breakpoints, single-stepping, and direct memory watch. It is a *visibility* tool, not an accurate VT1200 emulation. Everything in this directory is source-of-truth in this repo; the MAME source tree is disposable and lives outside the project (`/home/arti/code/k-space/mame0289`). ## Working conventions Five rules. Each of them has already cost this project real time, which is why they are written down rather than assumed. 1. **Never search from `/`.** A filesystem-wide `find /` or `grep -r` is slow, visible to whoever is at the keyboard, and never necessary. Use `rg` with an **explicit path** — this directory, or `/home/arti/code/k-space/mame0289/src` (within which `src/devices/machine`, `src/devices/bus`, `src/osd/modules/netdev` cover most questions). Never `/usr`, `/opt`, `/home` at large, `/proc` or `/sys`. Worth knowing why this keeps happening: `scn2681.cpp` **does not exist** (the SCN2681 lives in `mc68681.*`), so a search for it walks the entire disk before giving up. 2. **Stage only your own paths.** `git add […]` — never `git add -A`, never `git commit -a`. Several workers share this repository at once, and a `-A` sweeps someone else's in-flight files into your commit. That happened twice in one day. If it happens, unstage and `git commit --amend`, and say so. 3. **One sample is not evidence.** "It never paints", "the driver only models 256 KiB of RAM", and "it is stuck" were all *wrong*, and all three came from a single observation. Before concluding, either **compare across time** (several frames, several minutes, several runs) or **read the source**. A claim about behaviour needs more than one look; a claim about the code needs the code. 4. **Changing MAME requires a quote and a second reader.** Plausible-sounding sentences get assembled to justify a number that was really found by trial and error — one such edit was reverted after an independent reader found the datasheet said the opposite. So: quote the sentence or the source line, add a **revert note**, and have someone else read the citation before it is believed. 5. **Do not fake it.** No responder that feeds the firmware the answer a test wants. Where a real device is unknown, model what the documentation actually supports and **report the gap**; where a stand-in is genuinely unavoidable, make it **opt-in and name it for what it is** — see `VT1200_IO_PATH_STUB` and `VT1200_SKIP_LEVEL1`, both of which are documented divergences, not quiet assumptions. And the **ROM stays byte-identical** (`md5 b46a3e442ccb86134d2356bd70d22fd2`): patch at runtime if you must, never the file. ``` harness/mame/ ├── vt1200.cpp # the MAME driver (symlinked into the MAME tree) ├── build.sh # clone-safe build wrapper ├── *.lua # probe / capture / dump scripts (many; see the reports) └── roms/vt1200/ # MAME ROM dir (symlink to ../../vt1200_v22.bin) ``` Reports in this directory: | file | what it records | |---|---| | `GATE_CLEANUP_REPORT.md` | promoting the working config to the default; retiring the settled env gates | | `TABLET_REPORT.md` | the VSXXX-AB tablet: the self-test device-code ID, the 5-byte absolute report, and absolute host-pointer input | | `RENDER_REPORT.md` | the active-pixel scanline renderer, and the geometry it derived | | `DUART_REPORT.md` | the DUARTs: wiring, the ISR-driven transmit, the datasheet-verified model edits | | `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) | ## Address model (the important part) The TMS34010 is **bit-addressed**: a register/PC holds a bit address, and a pointer `P` refers to physical byte `P >> 3`. MAME models this with a program space of `memory_access<32, 1, 3>`, i.e. the **address-map unit is 1 bit** (a 16-bit word spans 16 address units). Source: `tms34010.h` — note the TMS34020 differs (`memory_access<32, 2, 3>`), so this is *not* interchangeable with the 34020. Therefore: * Address-map entries here are **bit addresses**, not byte addresses. * `read_word(0x3f200000)` returns ROM bytes 0..1 (physical byte 0x07e40000). * The reset vector is read from logical bit `0xffffffe0`, which maps to the last 4 bytes of the image (file offset `0x1bfffc`). The driver maps: | region | bit range | bytes | |---|---|---| | RAM | `0x00000000`–`0x001fffff` | 4 MiB at byte 0 | | ROM | `0x3f200000`–`0x3fffffff` | 1.75 MiB at byte 0x07e40000 | | ROM aliases | `0x7f…`, `0xbf…`, `0xff200000`–`0xffffffff` | same ROM (top 2 logical bits not decoded) | The `0xff…` alias is what makes the reset-vector page resolve. ### What is modelled by default This **changed** — see `GATE_CLEANUP_REPORT.md`. The working configuration used to be a six-variable incantation; it is now the default, and a bare `vt1200 vt1200` with **no environment** runs it. * Video RAM at bit `0x04000000` — required for the level-1 VIDEO MEMORY test. * The board-window handshake model (two-tick busy bit). * MAME's `SCN2681` DUART at **DUART1** (`0x0640000`, host/printer), with input port **IP4–IP6 held high** — that is how the driver reads the VR150 monitor switch (`0x70`). The level-1 host-port transmit is **ISR-driven**, so a real DUART is required here, not a convenience. * MAME's `SCN2681` at **DUART2** (`0x0620000`, keyboard/mouse). This one is a *faithfulness* fix, not a convenience — the real board has one. Without it the board write-back latch answers a read of DUART2's **write-only** IMR with its own written value, so the OS services a phantom interrupt that can never clear. * The **real input devices** on DUART2, driven by MAME's host input: a **DEC LK401 keyboard** (channel A, 4800 8N1) and a pointing device on channel B (4800 8O1) — a **DEC VSXXX-AB tablet** (**the default**, absolute) or, with `VT1200_MOUSE=1`, a **DEC VSXXX-AA mouse** (relative). All are modelled from `reference/keyboard-mouse/PROTOCOL.md` and wired to the SCN2681 at the bit level (`rx_a_w`/`rx_b_w` and `a_tx_cb`/`b_tx_cb`). They answer `FD`/`T` because that is the protocol, not to satisfy a test. See `INPUT_REPORT.md` and `TABLET_REPORT.md`. * A mono palette (pens 0/1 = black/white) and an active-pixel scanline renderer that reads VRAM. See `RENDER_REPORT.md`. A bare run gets through POST and then **fails level 0's I/O data path test**, so it lands in the interactive diagnostics menu (`VERSION 2.08 - SELECTED TEST TYPE:`) instead of reaching level 1. That test wants **10 bytes** on DUART2 channel A and nothing is attached, so this is an honest failure. **It used to "pass" by accident:** the write-back latch returned `0x0099` from DUART2's status register, and bit 0 of `0x99` is `RXRDY` — stuck asserted — so the firmware's drain loop counted ten phantom `0x8C` bytes. **It is not satisfiable by any documented device** — see `reference/level0-10byte-puzzle.md`. The routine writes exactly one byte (`0xE1`, "disable auto-repeat", which has no reply), sets `MR2A=0x07` so the channel is in **normal mode, not loopback** (confirmed statically *and* from a live access log), and checks **only the count**: the received byte is stored and never compared, and there is no expected-value table. The keyboard's `FD`/power-up reply is 4 bytes, the mouse's `T` reply is 4, the tablet's 5 — none is 10. A faithful LK401 would not help either: the init sequence's receiver-reset **flushes the FIFO**. The real terminal *does* pass this test, so the bytes exist on real hardware — they are simply not inferable from the ROM, and the only honest way to close the question is to observe a real keyboard port at power-up. **No faithful responder can be derived from any document.** So instead there is an **opt-in stand-in**, `VT1200_IO_PATH_STUB=1`. Read what it is carefully, because it is easy to misread: * It is **not a keyboard model** and it is not any documented device. It stands in for an **unknown test fixture on the keyboard port**. * Its bytes are **arbitrary** (`0x00`). Nothing compares them: the routine stores the received byte and never reads it back, and there is no expected-value table, so only the **count** matters. Feeding a pattern would imply a protocol that does not exist. * It triggers on the exact thing the test does: the one `0xE1` write to DUART2-A `THRA` (handler reg 3, window `0x0620000`, store at bit `3FDCE8B0`), then feeds **10 bytes** back on the same channel's receive path via the SCN2681's own `rx_fifo_push()`. It replies only while the channel is **polled** (`IMR = 0x00`, the level-0 condition), so it cannot inject into the interrupt-enabled OS phase or cause an interrupt there. * It is **off by default**. A bare run still fails level 0 exactly as before and says so. This is deliberate: the faithful configuration stays the default. With the stub on the boot clears level 0 and reaches level 1, where the DUART tests fail: `HOST PORT - 15`, then `PRINTER PORT - 16`. **It does not park at `HOST PORT - 15`.** Each failure is reported, the board LED blinks once (finite), the firmware prints `TYPE 'GO' TO CONTINUE` (which nothing answers and which does not wait), and the runner **continues**. It then enables DUART1's counter-ready interrupt and **wedges in a DUART1 interrupt storm** — that is the faithful ceiling, not `HOST PORT - 15`. It does **not** produce a booted OS. See `LEVEL1_CONTINUE_REPORT.md`. There is **no infinite board-LED loop** on this path. `3F208060`/`3F208650` are never executed here, and the boot loop's `3F204440 jane L3F208060` is never reached (the level-1 runner never returns). The blink that is seen is the finite `L3F208660` called by the error handler. A level-1 failure is fatal only through `L3FDF1010 → L3FDF1270`, the genuinely infinite blink, which is entered when the status word `*A13(0xFFA0)` is nonzero; the DUART failures leave it zero. ### How far the OS gets With `VT1200_SKIP_LEVEL1=1` (below) the OS boots to a **running X11 server** and on to the **Terminal Manager**: the root-window weave renders, the cursor is handed over to the terminal's own mouse cursor, and the V2.2 box appears — title bar, Messages, Ethernet/IP/LAT, a **Memory bar**, and "All Diagnostics Passed". Confirmed in the GUI and from frame dumps: `skip_frame3000.png` (the weave) and `duart2_default_tm_frame3000.png` (the Terminal Manager). The real terminal shows the same sequence — see `reference/vt1200-boot-video/`. The OS's keyboard and mouse drivers **initialise and speak the real protocol**: it programs DUART2 (`MR1A=0x13`, `MR2A=0x07`, `CSRA/CSRB=0x99`, `ACR=0x60`, `IMR=0x33`) and sends `FD` plus LED/click/bell/auto-repeat to the keyboard and `'T'` to the mouse. **Input now works**: the LK401 and VSXXX-AA device models answer those commands, the OS reads `01 00 00 00` / `A0 02 00 00`, switches the mouse to stream mode, and the X11 cursor moves with the mouse while keycodes reach the OS. See `INPUT_REPORT.md` (the cursor evidence is the `input_cursor_*.png` frames). The channel-B device is **by default** the **VSXXX-AB tablet**: it identifies as device code `0100`, so the OS switches to 5-byte absolute reports, and the emulated cursor follows the **host pointer's absolute position** (driven through `IPT_LIGHTGUN_X/Y`, i.e. MAME needs `-lightgun -lightgun_device mouse`). See `TABLET_REPORT.md` (evidence: `tablet_cursor_*.png`). **`VT1200_MOUSE=1`** puts the VSXXX-AA mouse back (device code `0010`, 3-byte relative reports). The mouse is the *faithful* part — the real terminal almost certainly has a mouse — so the tablet default is a **deliberate divergence** (below). Not modelled: **LANCE Ethernet**. It does not block the X11 bring-up. ### Getting past level 1 `VT1200_SKIP_LEVEL1=1` drives DUART1's **IP3** high, which the firmware takes as a signal to jump to the OS cold-start and skip **all 15** level-1 tests. Note what this is: a *modelled board input*, and the real machine reads IP3 **low**. So a level-1-skipped run is evidence about the **OS, graphics and crash behaviour**, and is **not** evidence about the real terminal's POST. The skip is deliberately **not** the default: the real machine runs level 1, so the faithful configuration stays the default and the switch is how we diverge. ### Deliberate divergences from the target machine Three harness behaviours are **not** what the real terminal does. They exist so the emulator can be driven and inspected, and they are collected here so a reader cannot mistake the harness for the machine: | divergence | default | what it does | how to restore the real machine | |---|---|---|---| | **VSXXX-AB tablet** | **on** | puts the tablet, not the mouse, on DUART2-B; absolute reports let the host pointer drive the cursor positionally | `VT1200_MOUSE=1` | | `VT1200_SKIP_LEVEL1` | off | drives DUART1 IP3 high, jumping past all 15 level-1 tests to the OS cold-start | leave it unset | | `VT1200_IO_PATH_STUB` | off | feeds 10 bytes on DUART2-A after the level-0 `0xE1` write, so the count-only I/O data path test passes | leave it unset | The tablet is the one that is **on by default** because the OS is only reachable through the skip gate, and there the tablet's absolute pointer is what makes the Terminal Manager usable. It is a **value-aware inverse switch**: the default lives in the code, not in a wrapper script setting `VT1200_TABLET`, so `VT1200_MOUSE=1` (or `VT1200_MOUSE=0` for the tablet) always flips it either way. ## Environment knobs (19; down from 33) Almost all are opt-in and off by default — there is no longer a "working incantation" to remember, because everything the firmware needs to run is now the default. The one exception is that the **tablet is on by default**; `VT1200_MOUSE` is the switch that restores the faithful mouse (see the divergences above). Instrumentation: `VT1200_DUARTLOG`, `VT1200_BOARD_LOG`, `VT1200_BOARD_MAP`, `VT1200_IRQLOG`, `VT1200_LOG_UNMAPPED`, `VT1200_DPYLOG` (+`_FRAMES`), `VT1200_DUMP_FRAME` (+`_FRAMES`), `VT1200_DUMP_RAM` (+`_FRAMES`), `VT1200_DUMP_VRAM` (+`_FRAMES`), `VT1200_DUMP_DIR`, `VT1200_NO_RENDER`, `VT1200_INPUT_LOG` (per-byte keyboard/mouse/tablet TX/RX trace, with emulated time). Behaviour switches: `VT1200_SKIP_LEVEL1`, `VT1200_MOUSE` (put the VSXXX-AA mouse (relative) back on DUART2-B; the VSXXX-AB tablet is the default — either pointing device needs MAME's `-lightgun -lightgun_device mouse` to follow the host pointer), `VT1200_IO_PATH_STUB` (the level-0 test-fixture stand-in described above — not a keyboard model, off by default). ## Regression baseline `progress.lua` at 200 frames, **no env**: ``` PC=3FDCE400 samples=198 (3 distinct PCs) ``` This has moved **twice**, both deliberately: 1. `samples=199` (1 PC) → `samples=53` (78 PCs): the working configuration became the default, so a bare run stopped wedging at the hardware-ready gate and began executing real POST work. 2. `samples=53` (78 PCs) → `samples=198` (3 PCs): DUART2 became a real `SCN2681`. Level 0's I/O data path test now **fails honestly** instead of being satisfied by the write-back latch's phantom `RXRDY`, so the run parks in the diagnostics menu. That looks like a regression and is not one — it is the removal of a false pass. **The tablet-default change did NOT move this baseline** (198 → 198), and that is expected, not an oversight. The tablet changes what DUART2-B carries on the wire — its `T` self-test reply is `A0 04 00 00` versus the mouse's `A0 02 00 00`, and it then frames 5-byte absolute reports instead of 3-byte relative ones — but at 200 frames a **no-env** run is still parked in the level-0 diagnostics menu and has never addressed DUART2-B at all: the `T`/`R` exchange happens only in the OS phase, which the no-env run does not reach. The only DUART2-B traffic in the window is the device's unsolicited power-up self-test, which nothing clocks in. The OS-phase render is unchanged too: with `VT1200_SKIP_LEVEL1=1` the frame-3000 dump is **pixel-identical** for the tablet and for `VT1200_MOUSE=1` (`magick compare -metric AE` = 0), and both reproduce the committed `tablet_cursor_*.png` frames exactly. **The no-env value is unchanged by `VT1200_IO_PATH_STUB`** — stub off is the default, so the `samples=198 (3 PCs)` line above still holds. The stub is a separate observed configuration: ``` # VT1200_IO_PATH_STUB=1, progress.lua at 200 frames PC=3FDCE400 samples=53 (75 distinct PCs) ``` That is a genuine move, not a false pass: with the count-only level-0 test satisfied, the run leaves the level-0 wait and executes level-0/level-1 work. It is **not** parked at `HOST PORT - 15`: the console transcript continues (`LEVEL1/2 TESTING **`, `LEVEL1/2 ERROR -> HOST PORT - 15`, `LEVEL1/2 ERROR -> PRINTER PORT - 16`, `TYPE 'GO' TO CONTINUE`), and the run ends in a DUART1 counter-ready **interrupt storm** (from frame ~2839; PC in the DUART1 INT1 ISR, polling ISR reg 5 = `0x08`). See `LEVEL1_CONTINUE_REPORT.md`. It is recorded as a *different* baseline because it is opt-in; the default must stay `samples=198`. It is a change detector, not a specification. If it moves without you meaning it to, find out why. ## Build MAME source is required. A shallow 0.289 checkout is large and slow to fetch. ```sh git clone --depth 1 --branch mame0289 \ https://github.com/mamedev/mame.git /home/arti/code/k-space/mame0289 harness/mame/build.sh # symlinks the driver in and runs make ``` `build.sh` runs `make -j$JOBS SUBTARGET=vt1200 SOURCES=src/mame/dec/vt1200.cpp`, which builds a single-driver binary `/home/arti/code/k-space/mame0289/vt1200`. The MAME tree is large and the build is **memory-hungry**: `build.sh` defaults to **`-j8`**, not all cores. Override with `JOBS=4 harness/mame/build.sh` on a small/low-memory machine. Progress is written to `harness/mame/build.log`. ## Run Headless execution and inspection use MAME's Lua debugger API from `-autoboot_script`. (`-debugscript` only works when a debugger UI drives the console, so it is not usable headless.) ```sh cd /home/arti/code/k-space/vt1200 M=/home/arti/code/k-space/mame0289/vt1200 # trace execution from the reset vector $M vt1200 -rompath harness/mame/roms -video none -sound none -nothrottle \ -debug -debugger none \ -autoboot_script harness/mame/trace.lua -autoboot_delay 0 # demonstrate the pcfReadBitmaps n*4 overflow (OVF_N overrides n, decimal) $M vt1200 -rompath harness/mame/roms -video none -sound none -nothrottle \ -debug -debugger none -debuglog -seconds_to_run 5 \ -autoboot_script harness/mame/overflow.lua -autoboot_delay 0 ``` The Lua API exposes `manager.machine.debugger`, `device.debug` (step, go, bpset, wpset), CPU registers (`cpu.state['A0'].value`), memory (`cpu.spaces['program']:read_u32/write_u32`), and frame hooks. ### Drive the keyboard and mouse (`input_inject.lua`) The LK401/VSXXX-AA models read MAME's host input, so they can be driven from Lua for a headless acceptance run: ```sh M=/home/arti/code/k-space/mame0289/vt1200 VT1200_SKIP_LEVEL1=1 \ VT1200_DUMP_FRAMES=4400,4600 VT1200_DUMP_DIR=/tmp \ $M vt1200 -rompath harness/mame/roms -video none -sound none -nothrottle \ -debug -debugger none \ -autoboot_script harness/mame/input_inject.lua -autoboot_delay 0 ``` `input_inject.lua` walks the mouse from frame 4400 and taps `A` at frame 4450. `input_probe.lua` prints every device port/field tag. See `INPUT_REPORT.md`. ### Trace from the reset vector (`trace.lua`) Prints the reset PC (expect `3F200040`), disassembles the power-on code to `reset.dasm`, and writes an instruction trace of the first two frames to `vt1200-reset.trace`. ### Exercise the `pcfReadBitmaps` overflow (`overflow.lua`) Instead of booting the parser we jump directly into its arithmetic block with a crafted in-memory `PCF_CharInfo`: ``` a9 = 0x00040000 ; fake pCS; pCS->nChars at a9+0x270 a13 = 0x00060000 ; fake arg blk; n (from pcfReadInt32) at a13+0x20 write_u32(0x00040270, 0) ; nChars == 0 write_u32(0x00060020, n) ; n pc = 3f984e70 break at 3f985060 ; just after 'sll 0x2,a11' ``` This is possible because the font parser's field moves use **field 1**, which `SETF 0,0,1` leaves at 32 bits, so `n` is a full 32-bit `pcfReadInt32` value. See `FINDINGS.md` for results and the rest of the analysis.