Files
vt1200/harness/mame/OS_BRINGUP_REPORT.md
SDD 1ca4817745 harness: model DUART2 by default (drop the VT1200_DUART2 gate)
The real VT1200 board has a DUART2 (SCN2681) at 0x0620000 for the keyboard
(ch A) and mouse (ch B) with INT2 wired.  Map it unconditionally instead of
opting in via VT1200_DUART2; the OS's DUART interrupt dispatcher needs a real
read-only ISR distinct from the write-only IMR.

Verified with frame dumps: with ONLY VT1200_SKIP_LEVEL1=1 the Terminal Manager
V2.2 dialog now appears (frame 3000; VRAM md5 73c3c9a1..., identical to the old
skip+DUART2 run).

This does NOT keep the faithful no-env path: level 0's I/O data path test
(L3FDCE470) transmits 0xE1 to DUART2-A and drains the receiver until it has read
10 bytes (SRA RxRDY).  The board write-back latch used to alias CSRA/RHRA and
faked that (baseline first divergence: R 06200001 pc=3FDCE670 = 0x0099 vs the
real chip's 0x000C), so the firmware dropped into the interactive diagnostics
menu.  No faithful device we have (LK401 sends at most 4 bytes) supplies the 10
bytes; an external TX->RX loopback does not change the transmit count.  See
OS_BRINGUP_REPORT.md section 8 (DONE_WITH_CONCERNS).

The OS keyboard/mouse driver does initialise: skip-phase OS code programs
DUART2 (MR1A=0x13, CSRA=0x99, IMR=0x33, ...) and sends FD to the keyboard plus
the documented LED/click/bell/auto-repeat commands, and 'T' to the mouse.
2026-10-16 13:59:09 +03:00

23 KiB
Raw Permalink Blame History

VT1200 OS bring-up: is the skip run progressing or stuck?

Date: 2026-10-16. Scope: name the wall the VT1200_SKIP_LEVEL1=1 run hits after the X11 root weave, with evidence that spans emulated time. ROM untouched (md5 b46a3e442ccb86134d2356bd70d22fd2).

Verdict: STUCK, and the wall is named. It is not slow — the framebuffer is byte-identical across millions of emulated cycles, while a single RAM byte oscillates. The wall is the unmodelled DUART2 (keyboard/mouse, bit 0x0620000): the OS's DUART interrupt dispatcher reads DUART2's read-only interrupt-status register (ISR) and gets back the value the OS itself wrote to the write-only interrupt-mask register (IMR) — same chip register address, 0x06200005 — so it services a phantom interrupt that can never clear. Mapping MAME's already-present SCN2681 there clears it, and the GUI's Terminal Manager dialog box — the reference video's ~0:48 target — appears.

2026-10-16 update (§8): DUART2 is now modelled by default. The VT1200_DUART2 gate is gone; a bare VT1200_SKIP_LEVEL1=1 run now reaches the Terminal Manager (§8.1). This does break the faithful no-env path: level 0's I/O data path test needs received data on DUART2-A that the old board write-back latch faked (§8.2). Read §8 before relying on the "no-env → HOST PORT - 15" baseline elsewhere in this file.


1. Progressing or stuck — evidence across time

One skip run, dumps at increasing frames (72 Hz: 3000 ≈ 42 s, 10000 ≈ 2.3 min, 30000 ≈ 7 min, 60000 ≈ 14 min). A frame (rendered PPM) and the video RAM and main RAM were written at each sample.

frame ≈ emulated frame PPM md5 video RAM md5 main RAM md5
3000 42 s 511743ace0999c5fa60cfffcd8e7c130 5ce8ad80d78efc26146b95eeb214a785 95dccfd3ae6c43a9f61e8caa22c6f3ea
10000 2.3 m 511743ace0999c5fa60cfffcd8e7c130 5ce8ad80d78efc26146b95eeb214a785 600f29d7d74dc3cbd139708be71bd77d
30000 7 m 511743ace0999c5fa60cfffcd8e7c130 5ce8ad80d78efc26146b95eeb214a785 95dccfd3ae6c43a9f61e8caa22c6f3ea
45000 10.4 m 511743ace0999c5fa60cfffcd8e7c130 5ce8ad80d78efc26146b95eeb214a785 95dccfd3ae6c43a9f61e8caa22c6f3ea
60000 14 m 511743ace0999c5fa60cfffcd8e7c130 5ce8ad80d78efc26146b95eeb214a785 95dccfd3ae6c43a9f61e8caa22c6f3ea
  • The framebuffer is byte-identical at 3000, 10000, 30000, 45000 and 60000: 58,720,256 bytes of video RAM, same md5, five times, spanning ~14 minutes of emulated time. The rendered frame is identical too (19.3158% lit — the root weave).
  • The 4 MiB of main RAM differs by exactly one byte between 3000 and 10000, at offset 0x829e2: 0x50 → 0x52. That is the only moving thing in all of memory. At 30000 and again at 45000/60000 the whole 4 MiB is byte-identical to frame 3000 — the byte is back to 0x50. So the machine is in a limit cycle, not advancing: a counter toggling inside a dead loop.
  • Net: no drawing, no new window, no changed state over ~14 emulated minutes. This is a deadlock, not slow progress. (BRINGUP_LOG.md already retired one false "it never paints" conclusion from a single sample; this conclusion is from five samples, the last 14 minutes after the first.)

PC distribution over the whole run (progress.lua, 60000 frames)

143 distinct PCs sampled once per frame. The distribution is tight, and the loop begins at frame ~611 (≈ 8.7 s) and never leaves:

  PC=3FB30390  samples=10020  first_frame=614
  PC=3FB30110  samples=9777   first_frame=611
  PC=3FB30370  samples=1834   first_frame=629
  PC=3FB42E50  samples=1589   first_frame=628
  PC=3FB42EF0  samples=1223   first_frame=618
  PC=3FB301F0  samples=1223   first_frame=615
  PC=3FB42710  samples=1223   first_frame=622
  PC=3FB30180  samples=1223   first_frame=620
  PC=3FB42F30  samples=1223   first_frame=613
  PC=3FB30240  samples=1222   first_frame=634
  PC=3FB42610  samples=1222   first_frame=632
  PC=3FB42760  samples=1160   first_frame=670

All of 0x3FB30110–0x3FB30390 and 0x3FB42610–0x3FB42F30 is either the OS serial-channel interrupt handler or the queue routine it calls (see §3).


2. The wall, named

PC / routine. The hot code is the OS's DUART interrupt service:

bit address what
0x3FB43800 DUART interrupt dispatcher (ISR). Reads DUART1 and DUART2 status, masks 0x66, calls the channel handler for each pending channel.
0x3FB42410 the per-channel handler it calls (calla at 0x3FB439C0 and 0x3FB43AF0).
0x3FB300F0 the lock-protected byte-queue routine the handler calls (calla at 0x3FB42CD0, 0x3FB42E20); its tail rets at 0x3FB30390 is the single hottest sampled PC.

The address it polls and the value it gets. VT1200_BOARD_LOG over the run records the distinct accesses of the stuck phase:

W 06200005 pc=3F6F3C50 val=0033   ; OS writes DUART2 IMR  = 0x33 (interrupt mask)
R 06200005 pc=3FB43A20 val=0033   ; ISR read returns the SAME value
R 06200001 pc=3FB42610 val=0098   ; DUART2 status
W 06200002 pc=3FB426F0 val=0040   ; DUART2 control write
R 06400005 pc=3FB43900 val=0009   ; DUART1 ISR (real chip: legitimate)

On an SCN2681/MC68681, register 5 is the ISR when read and the IMR when written — two different registers at one address. DUART2 is not modelled, so the address falls through to the board-window write-back latch, which returns the value last written. The dispatcher therefore reads 0x0033 as the ISR on every dispatch; it masks it with 0x66:

0x33 & 0x66 = 0x22  != 0   ->  "DUART2 has a pending interrupt" -> handler runs

and the condition can never clear, because the "ISR" always reads back the IMR. That is the deadlock: the CPU spins in the channel handler and its queue routine, at 0x3FB300F0–0x3FB30390, forever.

What is missing. A real DUART2 — the SCN2681/MC68681 at bit 0x0620000 (channel A keyboard, channel B mouse) — whose read-only ISR is distinct from its write-only IMR, and whose interrupts are delivered on INT2. Without it, the OS enables DUART2 interrupts it can never receive (§4.1) and sees a permanent phantom pending bit (§2).

MAME already contains that part, and the driver already wires it:

VT1200_SKIP_LEVEL1=1 VT1200_DUART2=1 <mame> vt1200 ...

With VT1200_DUART2=1 the deadlock clears and the screen changes between 3000 and 10000 (39.99% → 39.90% lit; video-RAM md5 differs) — the machine is progressing.

Causal proof: it is that one register value

Mapping the whole chip could have fixed many things at once. It did not need to: forcing only the read of 0x06200005 to 0 — nothing else changed — reproduces the DUART2 run byte-for-byte:

env VT1200_SKIP_LEVEL1=1 VT1200_BOARD_MAP="0x06200005=0" <mame> vt1200 ...
  frame 3000  39.9879% lit, video-RAM md5 73c3c9a1f13378d3ccf1e092aa47fa60
  frame 10000 39.8986% lit, video-RAM md5 4ff38c8c782684c562369dc47fb8a781

identical to the VT1200_DUART2=1 run's md5s. So the wall is exactly this: the value returned by the read at bit 0x06200005 (DUART2 register 5, ISR), 0x0033 instead of a real ISR clear. The OS does not need DUART2 interrupts to reach the GUI — it only needs to stop seeing the phantom one.


3. The disassembly that pins it

dispatcher 0x3FB438E0..0x3FB43AF0
  3fb438e0: move *a11(0x50),a7,0     ; a11 = 0x0640000 (DUART1); offset 0x50 bits = reg 5
  3fb43910: andi 0x66,a7 / jreq       ; skip if no pending channel-A/B
  3fb439c0: calla 0x3fb42410          ; -> channel handler
  3fb43a00: move *a12(0x50),a7,0     ; a12 = 0x0620000 (DUART2); reg 5 again
  3fb43a40: andi 0x66,a7 / jreq       ; <- (0x33 & 0x66) != 0, never skips here
  3fb43af0: calla 0x3fb42410          ; -> channel handler, every dispatch

The channel handler 0x3FB42410 reads *a6(0x10) and *a6(0x30) (the DUART SRA/CSR) and calls the queue routine 0x3FB300F0, whose body opens with a test-and-set lock (move a12,*a7,0 on the global at @0x1b3900+0x1c1) and a ring-buffer head/tail compare — i.e. it is draining a serial channel's queue.


4. Prime suspects

4.1 Interrupts enabled but never received — this is the wall, via DUART2

The OS writes IMR = 0x33 to DUART2 (0x06200005), enabling channel-A and channel-B TxRDY/Rx interrupts. DUART2 is unmapped, so no DUART2 interrupt is ever delivered (the SCN2681 object exists in the machine config but is never addressed, so it never raises its INT2 line). The write-back latch makes it worse than "silent": the read-only ISR aliases the value just written to the write-only IMR, so the dispatcher sees a pending interrupt that never clears. Everything else (DUART1 on INT2, the display interrupt) is healthy — which is exactly the shape the brief predicted: a task that never runs because its interrupt never fires, with everything else looking fine.

4.2 LANCE Ethernet — not modelled, but not the wall

Not modelled; the window 0x0580000/0x0590000 serves it and reads return the board latch. The OS does drive it — a cluster of writes/reads at 0x3F9D2880–0x3F9D46D0 (the board log shows W 05900000 … val=0000/0001/…, R 05800000 … val=0001), and LANCE/Ethernet Port Error are in the ROM. But the OS gives up after a bounded number of retries and continues: in the DUART2 run the dialog reports

Could not determine IP address after 5 retries
TCP/IP is now down.

which is the emulated analogue of the real terminal's Ethernet Port Error - 24 banner. So LANCE degrades gracefully; it does not hang the boot.

4.3 DUART2 keyboard/mouse — the wall (see §2)

Note on the earlier, untrusted lead ("a queue scan reading DUART2 registers"): that part is real and reproducible — R 06200005 pc=3FB43A20, R 06200001 pc=3FB42610 — but the same investigation's "no framebuffer updates" claim was wrong, so its conclusion (that this queue scan was benign) is the part to discard. This report treats the registers as data and lets the mapped-vs-unmapped experiment settle it, which it does.

4.4 NVRAM — read and used, not a wall

The dialog itself proves NVRAM is reached: it renders Secure Reset Count: 0, User settings invalid., System defaults loaded.. No stall there.


5. Does the dialog box from the reference video appear?

Yes — with DUART2 modelled. This is an independent, checkable target and it is reached.

reference/vt1200-boot-video/real_t52.png shows the real machine's Terminal Manager V2.2 dialog (Ethernet Address / IP Address / LAT Host Name / Memory Remaining bar / Secure Reset Count / a banner at the bottom). The emulated VT1200_SKIP_LEVEL1=1 VT1200_DUART2=1 run renders the same box:

  • frame 3000 (≈ 42 s) — "Terminal Manager V2.2 Copyright © 1991 Digital Equipment Corporation", Icon Box / TM icons, Messages "Using local fonts. User settings invalid. Using local fonts. System defaults loaded.", Ethernet Address 00-00-00-00-00-00, LAT Host Name LAT_00000000000, Memory Remaining, Kilobytes bar (3104), Secure Reset Count: 0, plus the terminal-manager mouse cursor over the root weave. Bottom banner: "All Diagnostics Passed".
  • frame 10000 (≈ 2.3 min) — the same dialog, and the message area has advanced to "Could not determine IP address after 5 retries / TCP/IP is now down." — i.e. the machine is still running and reporting the (expected) absence of a LANCE.
  • frame 30000 (≈ 7 min) — identical to frame 10000 (same PPM and video-RAM md5). The machine is now idle at the GUI, which is the correct state: no keyboard/mouse device is attached to feed it events, so it just waits. Contrast the no-DUART2 case, which is stalled before the GUI and never changes at all.

So the boot path is healthy once DUART2 is present; the only difference from the real machine is the network (no LANCE) and the banner text (level 1 was skipped by the gate). Note the two "stable at 10000→30000" results are not the same thing: the DUART2 run made it to the interactive GUI and then idled, whereas the no-DUART2 run is byte-frozen at the root weave from frame ~611 onward.


6. Reproduce

cd /home/arti/code/k-space/vt1200
M=/home/arti/code/k-space/mame0289/vt1200
R="-rompath harness/mame/roms -sound none -skip_gameinfo -video none -nothrottle"

# A. stuck, with time-samples (the §1 table).  Do NOT attach progress.lua here:
#    its exit at frame N would race the frame-N dump.  60000 frames ≈ 860 s.
env VT1200_SKIP_LEVEL1=1 \
    VT1200_DUMP_FRAMES=3000,10000,30000,45000,60000 \
    VT1200_DUMP_VRAM_FRAMES=3000,10000,30000,45000,60000 \
    VT1200_DUMP_RAM_FRAMES=3000,10000,30000,45000,60000 \
    VT1200_DUMP_DIR=/tmp/vt1200 $M vt1200 $R -seconds_to_run 950

# B. progressing, dialog appears
env VT1200_SKIP_LEVEL1=1 VT1200_DUART2=1 \
    VT1200_DUMP_FRAMES=3000,10000 \
    VT1200_DUMP_DIR=/tmp/vt1200d2 $M vt1200 $R -seconds_to_run 160

# C. console transcript (firmware's own printer), resolved to strings
env VT1200_SKIP_LEVEL1=1 $M vt1200 $R -debug -debugger none \
    -autoboot_script harness/mame/cap.lua -autoboot_delay 0 -debuglog
python3 harness/mame/resolve_console.py debug.log vt1200_v22.bin

New instrumentation added to vt1200.cpp for A/B (all opt-in, default off, so neither the no-env nor the skip baseline changes): VT1200_DUMP_FRAMES, VT1200_DUMP_RAM_FRAMES, VT1200_DUMP_VRAM_FRAMES — comma-separated lists, dumped at exactly those frames in one run. resolve_console.py resolves cap.lua's a9 message pointers to strings.

Baselines (unchanged)

  • no env → POST + level 1 to LEVEL1/2 ERROR -> HOST PORT - 15 (verified this session with cap.lua; then TYPE 'GO' TO CONTINUE).
  • VT1200_SKIP_LEVEL1=1 → the X11 root weave (skip_frame3000.png), byte-identical at 3000/10000/30000.
  • progress.lua was not changed; its no-env 200-frame baseline is intact.

Not-changed on purpose

VT1200_DUART2 is not promoted to a default. A no-env run with VT1200_DUART2=1 leaves the faithful path entirely and drops into the level-0 interactive self-test menu (VERSION 2.08 - SELECTED TEST TYPE …), which would break "no-env runs POST + level 1 to HOST PORT - 15". The GUI is therefore the documented combination VT1200_SKIP_LEVEL1=1 VT1200_DUART2=1.


7. What I did NOT establish

  • The exact micro-dynamics of the loop (whether the handler is re-entered purely by a DUART1 TxRDY interrupt storm, or by the main loop re-polling). The mechanism — an ISR read that cannot clear — is proven by the board log and by the mapped/unmapped experiment; the cycle-accounting is not.
  • Whether MAME's SCN2681 reproduces the real VT1200 DUART2 exactly (it is the right part family and the OS accepts it, but no keyboard/mouse device is attached, so RX behaviour is untested).
  • What happens after the dialog on real hardware ("light screen ~1:08"). With DUART2 the emulated run reaches the interactive GUI and then idles (identical 10000→30000), so I did not chase a post-dialog sequence; the "light screen" in the reference is most likely the video camera's CRT auto-exposure, not content.
  • NVRAM contents/initialisation policy — only that the OS reads it and falls back to system defaults.

8. DUART2 promoted to a default — what it fixes and what it breaks

Date: 2026-10-16 (later). Change: harness/mame/vt1200.cpp maps MAME's SCN2681 at 0x0620000 (keyboard ch A / mouse ch B) unconditionally; the VT1200_DUART2 gate is deleted. ROM untouched (md5 b46a3e442ccb86134d2356bd70d22fd2).

8.1 The OS now reaches the GUI with only VT1200_SKIP_LEVEL1=1

One run, no VT1200_DUART2 anywhere:

env VT1200_SKIP_LEVEL1=1 VT1200_DUMP_FRAMES=3000,10000 \
    VT1200_DUMP_VRAM_FRAMES=3000,10000 VT1200_DUMP_DIR=<dir> <mame> vt1200 ...
frame lit video-RAM md5
3000 461172 (39.9879%) 73c3c9a1f13378d3ccf1e092aa47fa60
10000 460143 (39.8986%) 4ff38c8c782684c562369dc47fb8a781

Video-RAM md5s identical to the report's VT1200_SKIP_LEVEL1=1 VT1200_DUART2=1 run — a bare skip run now is that run. The dumped frame (I rendered vt1200-frame-3000.ppm to duart2_default_tm_frame3000.png) shows the Terminal Manager V2.2 box — title bar "Terminal Manager V2.2: Copyright © 1991 Digital Equipment Corporation", Session/Create/Customize/Print Screen, Messages ("Using local fonts. / User settings invalid. / System defaults loaded."), Ethernet Address 00-00-00-00-00-00, LAT Host Name LAT_00000000000, the Memory Remaining bar (3104), Secure Reset Count: 0, the mouse cursor, and the bottom banner "All Diagnostics Passed". This matches reference/vt1200-boot-video/real_t52.png. Frame 10000 differs, so the machine is still running (not frozen).

8.2 Level 0: the earlier claim is REAL — and the exact test is named

The claim "a real DUART2 diverts level 0 to the interactive diagnostics menu" holds. The inference that it "looks wrong" came from the skip run, but VT1200_SKIP_LEVEL1 skips level 0 entirely, so that run says nothing about it.

  • Console, no env at all now (cap.lua): the transcript is no longer LEVEL1/2 TESTING / LEVEL1/2 ERROR -> HOST PORT - 15; it is the interactive diagnostics menu — VERSION 2.08 - SELECTED TEST TYPE: and the A–Z test list (D - KEYBOARD TEST, H - DUART2, CHANNEL A (KEYBOARD) TEST, …). So the faithful path is broken by this change.
  • Exact test. Level-0's I/O data path test, routine L3FDCE470, called from 3F3DFA80 with window base 0x0620000 (DUART2 ch A). It runs at cyc≈1.494 M, before the baseline's LEVEL1/2 TESTING print at cyc=2.119 M (confirmed with a cycle-stamped printer breakpoint), i.e. it is a level-0 test, not level 1.
  • What it waits for. It initialises the channel (MR1A=0x13, MR2A=0x07, CSRA=0x99, CRA enable, normal mode — not local loopback), transmits 0xE1 to THRA (pc=3FDCE8D0), and then drains the receiver: loop 3FDCE6C0/3FDCE930 reads RHRA, delays 100, and repeats while SRA bit 0 (RXRDY) is set, up to 10 bytes; the pass branch is taken at 3FDCEAE0 only when the count is exactly 10, otherwise it falls through to the menu display. With a real chip and nothing on the wire, RXRDY never sets (SRA=0x000C = TXRDY|TXEMT, bit 0 clear) and the test fails.
  • Why the baseline passed. The first differing access is R 06200001 pc=3FDCE670: baseline (board model) returned 0x0099 — the value last written to CSRA, whose bit 0 is set — so the driver drained 10 copies of the latched RHRA 0x008C and hit count 10. A real chip returns 0x000C. The write-back latch was an accidental loopback that faked the test; modelling the real device removes the fake and exposes the gap.
  • The LK401 model would not fix it. The firmware transmits only three bytes before this test (0x11 0x8C = LEDs off, then 0xE1 = disable auto-repeat); none of these has a documented LK401 reply, and the keyboard's power-up / FD self-test report is 4 bytes (reference/keyboard-mouse/PROTOCOL.md §3.5; vt100net-lk401.txt §3.3.4.6). The test drains until it has read 10. I also wired an external DUART2-A TX→RX loopback (a_tx_cb→rx_a_w, the old VT1200_DUART_LB2) as a diagnostic: the firmware's transmit count is still 3 and the menu still appears. So no faithful device/protocol model we have supplies the 10 bytes; restoring the no-env path would require re-introducing a read-back fake, which is exactly what this change removes.

8.3 Does the OS keyboard/mouse driver initialise? — Yes

VT1200_SKIP_LEVEL1=1 with VT1200_DUARTLOG, first 40 s: OS code (pc in the OS range, e.g. 3F6F3990…3F6F3CA0, then 3FB43550) drives 0x0620xxx:

W 0620000D=0000 OP, W 0620000E=000C, W 0620000F=0004   ; output port setup
W 06200002=002A/003A/005A/0010  (CRA) , W 0620000A …    ; channel resets
W 06200000=0013 MR1A , W 06200008=0007 MR1B
W 06200004=0060 ACR
W 06200001=0099 CSRA , W 06200009=0099 CSRB
W 06200000=0007 MR2A , W 06200008=0007 MR2B
W 06200005=0033 IMR (both channels Rx/Tx)
W 06200002=0009 CRA , W 0620000A=0009 CRB           ; enable Tx+Rx+A
… then the driver's command traffic (pc=3FB43550):
W 06200003=00FD   FD  -> keyboard: jump to power-up / self-test
W 06200003=0013 0080, 001B 0086, 0023 0084, 00E3    ; LED / click / bell / auto-repeat
W 0620000B=0054   'T' -> mouse: self-test & identify

Every one of those keyboard bytes is a documented LK401 command and 'T' is the documented VSXXX mouse self-test (PROTOCOL.md §3.3, §4.2) — so the driver initialises and speaks the real protocol. Input still does not work (no keyboard/mouse is attached, and nothing is faked); the chip receives no reply, which is the honest state.

8.4 Baselines

  • no env → CHANGED: no longer POST + level 1 to HOST PORT - 15; now level-0 I/O data path fails and the machine sits in the diagnostics menu (§8.2). progress.lua 200 frames: 3 distinct PCs, hot PC=3FDCE400 samples=198 (the menu's key-wait delay loop). This replaces the PC=3FDCE400 samples=53 / 78 distinct PCs baseline recorded in GATE_CLEANUP_REPORT.md; the baseline is a change detector, and this change is deliberate.
  • VT1200_SKIP_LEVEL1=1 → intact and extended: the X11 weave still paints first and the run now progresses through it to the Terminal Manager (§8.1). Frame/VRAM md5s unchanged from the old skip+DUART2 run.
  • progress.lua unchanged.

8.5 Reproduce

M=/home/arti/code/k-space/mame0289/vt1200
R="-rompath harness/mame/roms -sound none -skip_gameinfo -video none -nothrottle"

# GUI with ONLY the skip gate (<dir> must exist):
env VT1200_SKIP_LEVEL1=1 VT1200_DUMP_FRAMES=3000 \
    VT1200_DUMP_DIR=<dir> $M vt1200 $R -seconds_to_run 60

# Level-0 divert, with no env at all:
env CAP_MAX=2500 $M vt1200 -rompath harness/mame/roms -sound none \
    -skip_gameinfo -video none -nothrottle -debug -debugger none -debuglog \
    -autoboot_script harness/mame/cap.lua -autoboot_delay 0 -seconds_to_run 60
python3 harness/mame/resolve_console.py debug.log harness/mame/roms/vt1200/vt1200_v22.bin

# OS keyboard/mouse driver init:
env VT1200_SKIP_LEVEL1=1 VT1200_DUARTLOG=harness/mame/scratch/duart_skip.log \
    $M vt1200 $R -seconds_to_run 40

Bottom line. The faithfulness fix works and the GUI is now reachable with one env var — but it invalidates the "no env reaches HOST PORT - 15" constraint, for a real, named reason (level-0 I/O data path test needs 10 received bytes on DUART2-A). The previous no-env baseline only reached level 1 because the board latch faked that test. I did not fake it back.