14 KiB
How much work is a "full boot" of the VT1200 in MAME?
Investigated 2026-10-15/16. Companion to FINDINGS.md (which covers the harness that
exists today and the font questions). This file answers: what would it take to make
the MAME driver boot this machine as opposed to merely executing its firmware?
Short answer: a multi-week project, dominated by one unknown — decoding the board's custom peripheral windows. Everything else has a device model in MAME already. Below is the evidence and the breakdown.
1. What "full boot" means here
The terminal boots to an X server + the VTE terminal emulator + a LAT client + a 4.3BSD TCP/IP stack. "Full boot" = the firmware completes POST, initialises video, the DUARTs, the LANCE and the NVRAM, and reaches the point where it is listening on the network and driving the screen. That is exactly the state in which the crash we have been chasing happens — i.e. it is not just a debugging convenience. It is the condition needed to reproduce the LAT/announcement crash offline.
A weaker goal — "run specific firmware routines" (e.g. the font parser) — is far cheaper and is covered in §6.
2. Hardware inventory (derived, not guessed)
The ROM ships a self-test menu that enumerates the board, which gives a clean device list (each entry is the test's bit in the selection mask, not an address):
C - LANCE TEST (00008000h)
D - KEYBOARD TEST (00004000h)
E - DUART1, CHANNEL A (HOST) TEST (00000400h)
F - DUART1, CHANNEL B (PRINT) TEST (00000800h)
G - DUART1 TIMER TEST (00080000h)
H - DUART2, CHANNEL A (KEYBOARD) TEST (00001000h)
I - DUART2, CHANNEL B (MOUSE) TEST (00002000h)
J - DUART2 TIMER TEST (00100000h)
K - MOUSE/TABLET TEST (00010000h)
L - VIDEO TEST (00020000h)
N - VIDEO MEMORY TEST (00000100h)
M - ETHERNET ROM TEST (00040000h)
A[x]/Y - ROM TEST (x = bank select)/NVM
So the board is:
| block | notes |
|---|---|
| LANCE Ethernet | AM79C90-class; 4.3BSD-derived stack above it (lance_inet_interface.c, lpd_lance_devdriver.c) |
| 2× DUART | DUART1 = host + printer; DUART2 = keyboard + mouse; each has 2 timers. MC68681 / SCN2681 class |
| Ethernet ROM | MAC address storage |
| NVM | non-volatile setup memory (battery-backed) |
| Video | TMS34010 on-chip display controller + VRAM + an output stage |
| Keyboard, mouse/tablet | serial, behind DUART2 |
| Custom ASIC(s) | the undecoded windows in §4 |
Interrupt sources documented in the service guide: Display, Host Interface, Interrupt 1, Interrupt 2, NMI, Trap, Window Violation, plus LANCE, DUART (2 ch + 2 timers) and video clock.
3. What MAME already gives us (this is the good news)
Everything needed exists as a device model, and there are two near-perfect templates:
| need | MAME device | template driver |
|---|---|---|
| CPU | tms34010 (incl. display controller) |
already used here |
| LANCE Ethernet | am79c90 |
dec/kn01.cpp (DEC PMAX) — shows the DMA callback wiring |
| DUART ×2 | scn2681 / mc68681 |
ncd/ncd68k.cpp |
| NVRAM | nvram, eepromser, ds1215 (phantom clock) |
many |
| Video RAMDAC | bt478_device, ramdac family |
ncd/ncd68k.cpp |
| Keyboard | pc_kbdc, or a plain serial model |
ncd/ncd68k.cpp |
ncd/ncd68k.cpp is the closest analogue in MAME: it is an NCD X terminal with
LANCE + SCN2681 + VRAM + RAMDAC + EEPROM + keyboard. Anyone doing this work should
start from that file and from dec/kn01.cpp.
So the chipset is not the hard part. Nothing here needs a new device model.
4. The one real unknown: the board's custom windows
MEMORY_MAP.md already flags this, and it is the dominant cost:
Board peripherals appear around byte
0x00B00000,0x00C40000,0x00C80000(bit0x5800000,0x6200000,0x6400000). POST writes@0x62000d0/f0and@0x64000d0/e0/f0; the two identical windows at0xC40000/0xC80000are presumed to be the two DUARTs. Device decoding not confirmed.
The reset vector (file offset 0x08) is literally:
3F200080: MOVE A0,@64000D0h,0 ; before anything else
3F2000D0: MOVB A0,@64000F0h
3F200120: MOVB A0,@64000E0h
3F200160: MOVE A0,@62000D0h,0
3F2001B0: MOVE A0,@62000F0h,0
3F200260: MOVE A0,@C00000B0h,0 ; CONTROL
3F200290: MOVE A0,@C0000080h,0 ; DPYCTL
So two opaque windows (0x6200000, 0x6400000) are programmed before the CPU's own
display registers, i.e. they are essential. They are presumably the system-logic
ASIC (interrupt controller, video timing, NVM chip-select, LAN chip-select, maybe a
watchdog). Nobody has decoded them yet.
Work required: reverse-engineer those register sets from POST + the diagnostics code. This is ordinary RE with good anchors (the diagnostic tests name each block), but it is open-ended: the register count is unknown until it is done.
Risk: high, and it is the item that decides the schedule. Everything else can be estimated; this cannot.
4b. Can we just stub the unknown windows? (tested)
Yes — and it is cheap — but it does not produce a boot. Tested 2026-10-16.
The driver now takes VT1200_BOARD_STUB:
| value | behaviour of the 0x5800000 / 0x6200000 / 0x6400000 windows |
|---|---|
| unset | windows stay unmapped — identical to the pre-experiment driver |
0 |
windows mapped, every read returns 0 (same visible result as unset) |
0xffff |
every read returns all-ones, i.e. every polled "ready" bit asserted |
ram |
windows behave as read/write RAM — writes persist and read back |
Results, 1200 frames each — three different places to get stuck, none of them a boot:
| stub | parked at | what that code is |
|---|---|---|
0 |
PC=3FDCE400 (file 0x179C80) |
the 0x6200000 ready-poll (see below) |
0xffff |
PC=3F2082C0 (file 0x1058) |
POST video/refresh delay loop, JR 8060h |
ram |
PC=3F2088B0 (file 0x1116) |
a third, different gate |
What the ready-poll actually is
stackwalk.lua dumps the stack while parked in the delay loop, which is how the
first gate was identified. The delay routine is fine (it gets count 5968 ×
multiplier 100 ≈ 600k iterations ≈ 45 ms); it is simply called in a poll loop:
bcf210: MOVI 64h,A11 ; 100
bcf240: CALLA 3FDCE1C0h ; delay(100)
bcf270: MOVE *A13(FF80h),A9,1 ; A9 = 0x06200000
bcf2a0: MOVE *A9(10h),A11,0 ; read byte @ 0x06200010
bcf2c0: BTST 0h,A11 ; test bit 0
bcf2d0: JREQ BCF200h ; not ready -> delay + retry
i.e. while (!(read8(0x06200010) & 1)) delay(100); — a hardware-ready poll on the
undecoded window. Note the CPU's own registers (CONTROL, HSTCTLH) are modelled
by MAME's tms34010, so those reads were already real; the gate is purely the board ASIC.
The conclusion that matters
Stubbing is a good diagnostic and a poor shortcut:
- It is cheap (≈20 lines in the driver) and it does move the firmware — from the ready-poll into the POST video phase, and onwards under a different policy.
- But each policy lands somewhere different and still looping. Blindly asserting "ready" does not walk the boot forward; it selects a different wrong path.
- Passing a gate correctly still requires knowing what the register means.
The useful part: iterating targeted per-gate stubs enumerates the gates. In one afternoon, one known gate became three distinct stuck points. If that enumeration is done systematically, item 1 above stops being "decode an unknown ASIC" and becomes "decode the specific registers these N gates touch" — a bounded, much smaller job. That is the recommended way to attack item 1.
The experimental stub is committed in vt1200.cpp and defaults to the old behaviour,
so nothing regresses if VT1200_BOARD_STUB is unset.
5. Where the boot stops today (measured, not assumed)
harness/mame/progress.lua (new) samples the PC once per emulated frame and reports
the hottest PCs. Two runs:
- 400 frames, and
- 3000 frames (≈ 10 minutes of emulated time at this driver's ~4.9 Hz frame rate).
Result — the CPU sits in one instruction:
PC=3FDCE400 samples=2976 of 3000 first_frame=1
PC=3FDCE1C0 samples=1 first_frame=2848 <- routine entry
PC=3FDCE460 samples=1 first_frame=2736 <- its RETS
Disassembled (PC 0x3FDCE400 -> byte 0x07FB9C80 -> file 0x179C80), the routine is:
bce1c0: MMTM SP,A9,A11,A13 ; entry
bce210: MOVE *A13(FFE0h),A9,1 ; A9 = caller's argument
bce230: MOVI C00000B0h,A11 ; CONTROL
bce2d0: MOVE *A13(20h),A11,1
bce300: MOVE *A11,A11,0 ; read CONTROL
bce310: BTST Fh,A11 ; test bit 15
bce330: MOVE *A13(40h),A11,1 ; HSTCTLH (C0000100)
bce350: MOVE *A11,A11,0 ; read HSTCTLH
bce360: BTST Eh,A11 ; test bit 14
bce380: MOVK 1Eh,A11 ; multiplier = 30
bce3b0: MOVI 64h,A11 ; ... or 100
bce3e0: MPYS A11,A9 ; A9 = argument * multiplier
bce400: DSJS A9,BCE400h ; <-- THE SPIN: ~2^32 iterations
bce460: RETS 2h
Reading: this is a timed-wait / calibration helper. One call to it takes ~2,700
frames ≈ 9 minutes of emulated CPU time, because the multiplier/count inputs come
from peripheral status that the current driver stubs to 0xFFFF — so the loop runs
on garbage. The real machine boots in seconds, so this is not "slow firmware", it is
"missing hardware".
This is a useful, concrete marker: the first thing that must be modelled is whatever
feeds this routine, and we cannot get past early POST without it. It also confirms
the driver's own note that unmapped reads returning 0xffff are being acted upon.
6. Work breakdown and estimate
Ordered by dependency. Effort is for one engineer already familiar with this codebase and MAME, working part-time, and excludes the §6b shortcut.
| # | item | effort | uncertainty |
|---|---|---|---|
| 1 | Decode the 0x6200000 / 0x6400000 windows (interrupt controller, chip selects, video timing, NVM) |
1–3 weeks | high — this is the schedule |
| 2 | Wire LANCE: address, DMA, IRQ (dec/kn01.cpp pattern) |
2–4 days | low |
| 3 | Wire 2× DUART + their 4 timers + IRQs | 2–3 days | low |
| 4 | NVRAM + Ethernet ROM (MAC) | 1–2 days | low (needs correct contents) |
| 5 | Video: display params, VRAM mapping, palette/output stage; get a non-blank screen | 3–7 days | medium (TMS34010 display is modelled; the board video path is not) |
| 6 | Interrupt plumbing: TMS34010 INTENB/INTPEND + the external controller | 2–4 days | medium — depends on #1 |
| 7 | Keyboard + mouse so the terminal is usable | 2–4 days | low–medium |
| 8 | Boot debugging to "network up + screen up" | 1–3 weeks | high (integration) |
| 9 | Bonus: make the LAN path work end-to-end against latd | 3–7 days | medium |
Total: roughly 6–12 weeks part-time, with the spread driven almost entirely by items 1 and 8. Nothing in it is research-grade; it is a large, ordinary bring-up task with one genuinely unknown block.
What would make it collapse to something much smaller
- If the custom windows turn out to be a few dozen registers and the diagnostics code documents them (quite likely — the self-test knows every register it pokes), item 1 could be days rather than weeks.
- If the boot turns out not to need the LANCE before reaching video, items 2 and 9 can be deferred, and the terminal becomes usable on-screen much sooner.
What would blow it up
- The custom ASIC turning out to contain a DMA engine or a video path with
undocumented timings (plausible for the
0x6200000writes at reset). - POST validating device responses in ways that require behavioural accuracy, not just register presence.
6b. The much cheaper alternative (and what I'd actually do)
For this project's real questions — the font parser, the VTE, the LAT client — a full boot is not the cheapest route, and probably not needed at all.
A call harness gets you inside a specific routine in days:
- put a writable overlay at
fosRead(0x3f96eee0) /fosAlloc(0x3f703d90), - have Lua write a small TMS34010 stub that serves bytes from a RAM copy of the file
under test (a
.pcf, an SNF, a LAT session buffer), - set
PCto the routine of interest with a crafted argument block.
That is how the pcfReadBitmaps overflow was already demonstrated in-emulator
(overflow.lua does exactly this pattern, jumping straight into the arithmetic with
fabricated state). Extending it to pcfReadFont is the cheapest way to answer the
outstanding font question.
Recommendation: do the call harness now; treat the full boot as an optional parallel project, and only start it if you want offline reproduction of the LAT crash — in which case item 1 (the custom-window decode) is the thing to schedule.
7. Reproduce
cd /home/arti/code/k-space/vt1200
M=/home/arti/code/k-space/mame0289/vt1200
# where does the boot get, and where does it spin?
PROG_FRAMES=3000 $M vt1200 -rompath harness/mame/roms -video none -sound none -nothrottle \
-debug -debugger none -autoboot_script harness/mame/progress.lua -autoboot_delay 0
# hardware inventory (the self-test menu)
strings -n 6 vt1200_v22.bin | grep -E "TEST ?\(|Selftest"
# disassemble the spin site (byte offsets; printed address = byte*8)
python3 lat/notes/ua.py 0x179c30 0x179cd0
Note: Lua print() goes to stdout; debugger printf/-debuglog output goes to
./debug.log. Two different streams — easy to lose an afternoon to that.