diff --git a/harness/mame/OS_BRINGUP_REPORT.md b/harness/mame/OS_BRINGUP_REPORT.md
index dc7fdd1..d5e3ffa 100644
--- a/harness/mame/OS_BRINGUP_REPORT.md
+++ b/harness/mame/OS_BRINGUP_REPORT.md
@@ -14,6 +14,13 @@ the write-only interrupt-mask register (IMR) — same chip register address,
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
@@ -305,3 +312,147 @@ documented combination `VT1200_SKIP_LEVEL1=1 VT1200_DUART2=1`.
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:
+
+```sh
+env VT1200_SKIP_LEVEL1=1 VT1200_DUMP_FRAMES=3000,10000 \
+ VT1200_DUMP_VRAM_FRAMES=3000,10000 VT1200_DUMP_DIR=
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
+
+```sh
+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 ( must exist):
+env VT1200_SKIP_LEVEL1=1 VT1200_DUMP_FRAMES=3000 \
+ VT1200_DUMP_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.
diff --git a/harness/mame/duart2_default_tm_frame3000.png b/harness/mame/duart2_default_tm_frame3000.png
new file mode 100644
index 0000000..7b568bd
Binary files /dev/null and b/harness/mame/duart2_default_tm_frame3000.png differ
diff --git a/harness/mame/vt1200.cpp b/harness/mame/vt1200.cpp
index 0532ab6..97a3c96 100644
--- a/harness/mame/vt1200.cpp
+++ b/harness/mame/vt1200.cpp
@@ -32,8 +32,16 @@
the reset-vector page at logical bit 0xffffffe0 resolves to the last
four bytes of the image (file offset 0x1bfffc).
- What is NOT modelled: LANCE Ethernet, DUART2 (0x0620000 = keyboard/mouse;
- opt in with VT1200_DUART2=1), and the board system logic beyond the
+ * MAME's SCN2681 at DUART2 (0x0620000 = keyboard (ch A) / mouse (ch B)),
+ wired to INT2. This is modelled unconditionally because the real
+ board has it and the OS's DUART interrupt dispatcher needs a real
+ chip. NOTE: with a real DUART2 and nothing attached to the keyboard
+ port, the *faithful* (no-env) boot now fails the level-0 I/O data path
+ test and drops into the interactive diagnostics menu; before DUART2
+ was modelled the board write-back latch accidentally faked that test
+ (see OS_BRINGUP_REPORT.md).
+
+ What is NOT modelled: LANCE Ethernet and the board system logic beyond the
register model. The firmware therefore still stops at the level-1 HOST
PORT test and wedges in the system-board LED loop; that is expected.
@@ -96,11 +104,8 @@
Video: the driver paints the active scanlines itself, reading the
framebuffer out of video RAM (see render_active_line).
- VT1200_DUART2=1 additionally maps MAME's SCN2681 at 0x0620000
- (keyboard/mouse). It is off by default because mapping both DUARTs
- diverts the level-0 keyboard path.
-
- See FULL_BOOT_ESTIMATE.md section 4b for measured results.
+ See FULL_BOOT_ESTIMATE.md section 4b for measured results and
+ OS_BRINGUP_REPORT.md for the DUART2 / level-0 analysis.
***************************************************************************/
@@ -186,8 +191,8 @@ public:
u16 board3_r(offs_t off); void board3_w(offs_t off, u16 data);
// The two DUARTs. DUART1 = host (ch A) + printer (ch B) at window
- // 0x0640000 and is always mapped; DUART2 = keyboard (ch A) + mouse (ch B)
- // at window 0x0620000 is opt-in via VT1200_DUART2.
+ // 0x0640000; DUART2 = keyboard (ch A) + mouse (ch B) at window 0x0620000.
+ // Both are mapped unconditionally.
u16 duart1_r(offs_t off); void duart1_w(offs_t off, u16 data);
u16 duart2_r(offs_t off); void duart2_w(offs_t off, u16 data);
@@ -417,8 +422,7 @@ void vt1200_state::machine_start()
logerror("vt1200: board model=handshake busy_ticks=2 switch=duart-ip4/5/6 log=%s render=%d dump=%ld\n",
m_log ? "on" : "off", m_render ? 1 : 0, m_dump_frame);
- logerror("vt1200: duart1=on (SCN2681 @0640000 host/printer), duart2=%s skip_level1=%s\n",
- getenv("VT1200_DUART2") ? "on (kbd/mouse)" : "off",
+ logerror("vt1200: duart1=on (SCN2681 @0640000 host/printer), duart2=on (SCN2681 @0620000 kbd/mouse), skip_level1=%s\n",
getenv("VT1200_SKIP_LEVEL1") ? "on (IP3 high, IRQ->INT2)" : "off (IP3 low, IRQ->INT1)");
}
@@ -505,11 +509,14 @@ void vt1200_state::program_map(address_map &map)
// board-window model so it wins the register slice (map precedence is
// last-wins); the model still serves any other register in the same window.
// Base in map units is the bit address; 16 registers at 2 bytes each = 32
- // bytes = 0x100 bit units. DUART2 (0x0620000 = keyboard/mouse) is opt-in
- // via VT1200_DUART2: mapping both diverts the level-0 keyboard path.
+ // bytes = 0x100 bit units. DUART2 (0x0620000 = keyboard/mouse) is mapped
+ // the same way, unconditionally.
map(VT1200_WIN3, VT1200_WIN3 + 0xff).rw(FUNC(vt1200_state::duart1_r), FUNC(vt1200_state::duart1_w));
- if (getenv("VT1200_DUART2"))
- map(VT1200_WIN2, VT1200_WIN2 + 0xff).rw(FUNC(vt1200_state::duart2_r), FUNC(vt1200_state::duart2_w));
+ // DUART2 (0x0620000 = keyboard/mouse) is mapped unconditionally: the real
+ // board has it, and the OS's DUART dispatcher requires a real read-only ISR
+ // distinct from the write-only IMR (the board write-back latch aliased them,
+ // producing a phantom interrupt that wedged the OS at the X11 weave).
+ map(VT1200_WIN2, VT1200_WIN2 + 0xff).rw(FUNC(vt1200_state::duart2_r), FUNC(vt1200_state::duart2_w));
}
// MAME may hand us either an entry-relative offset (0..0xffff) or the absolute
// address; BOARD_BASE is 0x10000-aligned, so ORing the low 16 bits is correct