Files
vt1200/harness/mame/README.md
T
SDD 6d8097360c harness: the VSXXX-AB tablet is the default pointing device; VT1200_MOUSE=1 forces the faithful mouse back
The tablet was opt-in (VT1200_TABLET=1) with the mouse as the default.  Invert
it: the tablet is selected with no environment, and VT1200_MOUSE=1 (a
value-aware inverse switch, so VT1200_MOUSE=0 selects the tablet) restores the
VSXXX-AA mouse.

The real terminal almost certainly has a mouse, so this is a DELIBERATE
divergence from the target machine, documented in README.md alongside
VT1200_SKIP_LEVEL1 and VT1200_IO_PATH_STUB.  It is on by default because the OS
is only reachable through the skip gate and there the tablet's absolute reports
are what make the Terminal Manager usable.

Baselines re-measured, both unchanged:
  * no-env progress.lua at 200 frames: PC=3FDCE400 samples=198 (3 PCs) -> same.
    DUART2-B is never addressed in that window (the T/R exchange is OS-phase
    only), so the reply bytes A0 04 00 00 vs A0 02 00 00 and the 5- vs 3-byte
    framing do not reach the CPU.
  * VT1200_SKIP_LEVEL1=1 frame 3000: pixel-identical for tablet and
    VT1200_MOUSE=1 (magick AE=0); both reproduce the committed
    tablet_cursor_*.png frames exactly.

VT1200_TABLET is now a no-op; run.sh's tablet mode is reported to the owner for
rewording (its comments are his).
2026-10-16 16:19:46 +03:00

393 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 <path> [<path>…]` — 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.