diff --git a/.gitignore b/.gitignore
index 08158dc..28d6eaf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -33,3 +33,6 @@ lat/code/latd/
harness/mame/vt1200-reset.trace
reference/x11/
tools/ghidra-headless-mcp/
+
+# Generated harness logs
+harness/mame/board-*.log
diff --git a/docs/superpowers/plans/2026-10-16-vt1200-board-model.md b/docs/superpowers/plans/2026-10-16-vt1200-board-model.md
index 78f4d1a..d206b2b 100644
--- a/docs/superpowers/plans/2026-10-16-vt1200-board-model.md
+++ b/docs/superpowers/plans/2026-10-16-vt1200-board-model.md
@@ -256,7 +256,13 @@ Expected: `all board tests passed`
- [ ] **Step 6: Wire the driver to the model and verify no regression**
-In `harness/mame/vt1200.cpp`: add `#include "vt1200_board.h"`, give `vt1200_state` a `vt1200_board m_board;` member, and in `board_read`/`board_write` delegate to `m_board.read(addr)` / `m_board.write(addr, data)` instead of the current inline logic. In `machine_start`, translate the existing env vars into the model's setters (`VT1200_BOARD_DEFAULT`, `VT1200_BOARD_MAP`, `VT1200_BOARD_MODEL`, `VT1200_BOARD_BUSY_TICKS`, `VT1200_BOARD_SWITCH`). Call `m_board.tick()` once per emulated frame — the simplest correct place is the existing `scanline` callback guarded so it only fires once per frame, or `screen_update` when `cliprect.min_y == 0`.
+In `harness/mame/vt1200.cpp`: add `#include "vt1200_board.h"`, give `vt1200_state` a `vt1200_board m_board;` member, and in `board_read`/`board_write` delegate to `m_board.read(addr)` / `m_board.write(addr, data)` instead of the current inline logic. In `machine_start`, translate the existing env vars into the model's setters (`VT1200_BOARD_DEFAULT`, `VT1200_BOARD_MAP`, `VT1200_BOARD_MODEL`, `VT1200_BOARD_BUSY_TICKS`, `VT1200_BOARD_SWITCH`). Call `m_board.tick()` once per emulated frame. Put it in the **scanline callback**, guarded so it fires exactly once per frame — the callback is invoked once per scanline (verified: lines 168, 169, 170 …), and `scanline == screen.visible_area().max_y` is the last line of the frame. Do **not** use `screen_update`: it is called with small clip regions and a guard there fired only once in an 80-frame run.
+
+```cpp
+// in scanline(), at the end:
+if (scanline == screen.visible_area().max_y)
+ m_board.tick();
+```
In `harness/mame/build.sh`: add the new source to the symlink list and to `SOURCES=` (comma-separated), and add `g++ -c vt1200_board.cpp` to the unit-test recipe.
diff --git a/ghidra/vt1200.lock b/ghidra/vt1200.lock
deleted file mode 100644
index 0ae2975..0000000
--- a/ghidra/vt1200.lock
+++ /dev/null
@@ -1,9 +0,0 @@
-#Ghidra Lock File
-#Fri Oct 16 01:36:23 EEST 2026
-\ Supports\ File\ Channel\ Locking=Channel Lock
-Hostname=sid
-OS\ Architecture=amd64
-OS\ Name=Linux
-OS\ Version=7.2.4-arch1-2
-Timestamp=10/16/26, 1\:36\u202FAM
-Username=arti
diff --git a/ghidra/vt1200.lock~ b/ghidra/vt1200.lock~
deleted file mode 100644
index e69de29..0000000
diff --git a/harness/mame/board-4w.log b/harness/mame/board-4w.log
deleted file mode 100644
index 1d889af..0000000
--- a/harness/mame/board-4w.log
+++ /dev/null
@@ -1,56 +0,0 @@
-# kind addr pc value (bit addresses; distinct (addr,pc) only)
-W 0640000D pc=3F2000B0 val=0000
-R 0640000F pc=3F200100 val=0000
-R 0640000E pc=3F200150 val=0000
-W 0620000D pc=3F200190 val=0000
-W 0620000F pc=3F2001E0 val=0004
-R 0640000F pc=3F200250 val=00FF
-R 0640000D pc=3F200380 val=0070
-R 0640000D pc=3F200A20 val=0070
-R 0640000D pc=3F202650 val=0070
-W 05900000 pc=3FDDCAE0 val=0000
-W 05800000 pc=3FDDCB20 val=0004
-R 0640000D pc=3F2045A0 val=0070
-W 0620000D pc=3F2052C0 val=0000
-W 0620000E pc=3F205310 val=00FF
-W 0620000F pc=3F205370 val=0004
-R 0640000E pc=3F2042D0 val=000F
-R 0640000D pc=3F204300 val=0070
-W 0640000D pc=3FDF1150 val=0000
-W 0640000F pc=3FDF1190 val=0001
-R 06200001 pc=3FDF3900 val=0001
-W 06200003 pc=3FDF3AD0 val=0011
-W 06200003 pc=3FDF3BE0 val=008C
-R 0640000D pc=3FDF7DE0 val=0070
-W 06200002 pc=3FA91260 val=00CA
-W 06200002 pc=3FA91300 val=002A
-W 06200002 pc=3FA913B0 val=003A
-W 06200002 pc=3FA91460 val=004A
-W 06200002 pc=3FA91500 val=005A
-W 06200002 pc=3FA915A0 val=001A
-W 0620000A pc=3FA91650 val=00CA
-W 0620000A pc=3FA916F0 val=002A
-W 0620000A pc=3FA917A0 val=003A
-W 0620000A pc=3FA91850 val=004A
-W 0620000A pc=3FA91900 val=005A
-W 0620000A pc=3FA919A0 val=001A
-W 06200000 pc=3FA91A30 val=0013
-W 06200000 pc=3FA91AC0 val=0007
-W 06200001 pc=3FA91B70 val=0099
-W 06200002 pc=3FA91C70 val=0005
-W 06200008 pc=3FA91D10 val=0007
-W 06200008 pc=3FA91DB0 val=0007
-W 06200009 pc=3FA91E60 val=0099
-W 0620000A pc=3FA91F40 val=0005
-W 06200005 pc=3FA91FE0 val=0000
-R 06200001 pc=3FDCE670 val=0099
-R 06200003 pc=3FDCE700 val=008C
-R 06200001 pc=3FDCE820 val=0099
-W 06200003 pc=3FDCE8D0 val=00E1
-R 06200001 pc=3FDCE910 val=0099
-R 06200003 pc=3FDCE970 val=00E1
-R 06200001 pc=3FDCEA90 val=0099
-W 0620000D pc=3FDF1B40 val=0000
-W 0620000E pc=3FDF1B80 val=000C
-R 06200001 pc=3FDCF420 val=0099
-R 06200003 pc=3FDCF560 val=00E1
diff --git a/reference/memory-space-map.md b/reference/memory-space-map.md
new file mode 100644
index 0000000..6429886
--- /dev/null
+++ b/reference/memory-space-map.md
@@ -0,0 +1,89 @@
+# VT1200 firmware — address-space map (spike, 2026-10-16)
+
+What external memory/register space the v2.2 firmware actually touches, derived by
+sweeping every scalar operand in the disassembled image plus the raw ROM images, and
+cross-checked against the MAME harness.
+
+## Conventions (read this first)
+
+* The TMS34010 is **bit-addressed**. The firmware stores **bit addresses**.
+ A physical **byte** address is `bit >> 3`; **Ghidra** byte address = `0x07E40000 + file_offset`.
+* This document gives **bit addresses** (the value the firmware actually stores) and,
+ in parentheses, the **byte** address (`bit>>3`).
+* "harness unit" (the MAME driver's `BOARD_BASE` and the access-log address) = `bit >> 4`
+ = 2 bytes per unit. The four known windows are written `0x5800000` (bit) here, which is
+ the driver's `0x5800000` too (leading-zero identical) — but the **log** prints them as
+ `0x0580000D` style (`bit>>4`). Do not mix the two.
+* Evidence codes: **CONST** = scalar-operand count from the Ghidra sweep (96,801
+ instructions / 1,177 distinct `@`-targets); **XREF** = string/reference cross-check;
+ **LOG** = `harness/mame/board-4w.log` / `board-access.log`; **SG** = service guide.
+* "Driver?" = does `harness/mame/vt1200.cpp` currently map it.
+
+## Region table
+
+| # | bit range | byte range | what | evidence | driver? | conf. |
+|---|---|---|---|---|---|---|
+| 1 | `0x0000000`–`0x2000000` | `0x000000`–`0x400000` | **Main RAM** 4 MiB | mem-test at `0x07EDFF22` walks SIMM regions at bit `0x800000`/`0x1000000`… (byte 1 MiB/2 MiB); ~5 000 absolute globals; POST clears RAM; SG: 1 MiB onboard + 3×1 MiB SIMM | yes (bit `0`–`0x1FFFFFF`) | **HIGH** |
+| 2 | `0xC0000000`–`0xC00001FF` | (on-chip) | **CPU on-chip registers** (DPYCTL/DPYSTRT/DPYADR/CONTROL/…) | datasheet + `MEMORY_MAP.md`; 72 absolute operands | internal | **HIGH** |
+| 3 | `0x5000000` | `0xA00000` | **Memory-config / SIMM-presence input port.** bits 0,1,2 = "SIMM 1/2/3 installed" | CONST 3 `@` reads; XREF: `0x07E58FB2`/`0x07E58FCE`/`0x07E58FEA` read bits 0/1/2 then print `Not Present`/`SIMM n installed` (strings @ file `0x0199BD`…); also read at `0x07EDFF22` | **no** | **HIGH** |
+| 4 | `0x5600000` | `0xAC0000` | **Unidentified board register block** — most-touched unmodelled region. Offsets `+0x00 +0x10 +0x20 +0x30 +0x80 +0x100 +0x200 +0x380` | CONST **110** `@` accesses (2nd only to RAM/the known windows); writer at `0x07E6CB62`, adjacent to X-server strings (`no screens found`) | **no** | **HIGH** region / **LOW** identity |
+| 5 | `0x5800000` | `0xB00000` | **DMA/board data port** | CONST 10 `@`; LOG; paired with #6; firmware `>>3`s a bit address into it (byte-addressed device) | yes | **HIGH** |
+| 6 | `0x5900000` | `0xB20000` | **DMA/board command port** | CONST 6 `@`; LOG (commands 0,1,2,4,0x42) | yes | **HIGH** |
+| 7 | `0x6000000` | `0xC00000` | **Candidate NVRAM.** ~2 KiB user-settings copy writes here; sits in a 256 KiB device slot between #6 and #8 | CONST 2 (`0x07E957F4`, `0x07E95974` push it as the destination of a settings copy/metadata routine `0x07E966D0`); XREF: adjacent `Old user settings layout.` / `User settings invalid.` | **no** | **MEDIUM-LOW** |
+| 8 | `0x6200000` | `0xC40000` | **Board window** (STATUS `+0x10`, CMD `+0x20`, config `+0xD0`) | CONST 26 `@`; LOG; `BRINGUP_LOG.md` | yes | **HIGH** |
+| 9 | `0x6400000` | `0xC80000` | **Board window** incl. monitor-selection switch (bits 4–6 of `+0xD0`) | CONST 24 `@`; LOG; `BRINGUP_LOG.md` | yes | **HIGH** |
+| 10 | `0x6800000` | `0xD00000` | **Ethernet MAC-address ROM** | CONST 2 (`0x07E46112`); the 6-iteration byte-read loop `0x07E4611E`–`0x07E4613A` copies 6 bytes (a MAC) from this base; SG level-1 "Ethernet address ROM test" | **no** | **HIGH** |
+| 11 | `0x4000000` | `0x800000` | **VRAM / framebuffer — candidate only** | `MOVI 0x4000000,B4` at `0x07E40AB0` in the reset-bank video init (`video_regs_init`); the `B` file holds bitmap bases | **no** | **LOW** |
+
+### Notes on rows 3–7 (the peripheral map is regular)
+
+The byte addresses `0xA00000`, `0xAC0000`, `0xB00000`, `0xB20000`, `0xC00000`,
+`0xC40000`, `0xC80000`, `0xD00000` look like a decoded device map with a ~256 KiB
+slot at the top end (`0xC00000`, `0xC40000`, `0xC80000`, `0xD00000` are exactly
+`0x40000` apart). That regularity is *circumstantial* evidence for rows 7 and 10.
+
+## What was ruled out (candidates that are NOT regions)
+
+These appear constantly as large 32-bit constants but are **not** addresses:
+
+| value (bit) | file offsets | why it is not an address |
+|---|---|---|
+| `0x112A880`, `0x2DC6C0`, `0x5B8D80`, `0x927C0`, `0x281F90`, `0x3600400`, `0x4000500` | reset/POST bank | `DSJS` delay-loop counters (gate 2 in `BRINGUP_LOG.md`) and per-monitor display-format constants selected by the monitor switch (`0x07E4089E`–`0x07E4090A`) |
+| `0x800000`, `0x1000000` | `0x07EDFF2C`… | **RAM region boundaries** used by the SIMM memory test (byte 1 MiB / 2 MiB), not devices |
+| `0x4000000` (all but one) | 49 aligned hits | bit-26 mask OR/AND, not an address |
+| byte `0x600000`–`0x7E40000` "buckets" | whole image | code bytes / data tables. A naive raw scan of every 32-bit word is ~25 % false positives. The `0x5000000`/`0x6000000` "cluster" in bank 6 is **unaligned byte noise** inside sparse tables — re-scanning 16-bit-aligned collapses it. |
+
+## What remains unknown (stated plainly)
+
+1. **Exact VRAM base and size.** The framebuffer is *not* accessed by any absolute operand
+ (the `@`-sweep finds it nowhere), so it is addressed indirectly — via the `B`-register
+ file and/or the `DPYADR` (`0xC00001E0`) register, both set at runtime from a RAM screen
+ descriptor. Row 11 is the only static hint and is **weak**. It is also possible the
+ video DRAM sits *behind* the `0x5800000`/`0x5900000` DMA pair rather than being CPU-visible.
+2. **Identity of row 4 (`0x5600000`).** Heavy, display-adjacent, but I could not name the device.
+3. **NVRAM extent / exact base** (row 7), and whether the MAC ROM (row 10) is 16-bit-wide
+ (the read loop advances 2 bytes per MAC byte).
+4. **Extent of each I/O block** — only point accesses are observed, so sizes are guessed from slot spacing.
+
+## How to reproduce
+
+```
+# operand sweep (Ghidra, session required):
+# iterate listing.getInstructions(True); for each Scalar operand >= 0x10000 and < 0x40000000,
+# bucket by (value>>3)>>16. Filter to "@0x" operands for true memory accesses.
+python3 tools/ptr_constants.py vt1200_v22.bin # ROM-pointer subset
+# raw aligned scan: 32-bit LE words whose (value>>3) lands in the region of interest
+```
+
+## Durable Ghidra annotations added (project `ghidra/vt1200`, program `vt1200_v22.bin`)
+
+* Memory blocks (with comments) created at each region byte base:
+ `VRAM_800000_CAND`, `IO_50_SIMM_MEMCFG`, `IO_56_BOARD_REG`, `IO_58_DMA_DATA`,
+ `IO_59_DMA_CMD`, `IO_60_NVRAM_CAND`, `IO_62_WIN2`, `IO_64_WIN3`, `IO_68_MACROM`.
+* Functions created/named: `video_regs_init`, `io5600000_screen_init`,
+ `nvr_user_settings_validate`, `io_settings_copy_expand`, `io_settings_copy_linear`,
+ `io_settings_restore`, `io5800_5900_init`, `io5800_status_read`,
+ `keyboard_mouse_irq_handler`, `led_control_1/2`.
+* PRE comments at the key access sites (MAC read, SIMM read, `0x5600000` write,
+ settings copy, SIMM mem-test, video init, `B4=0x4000000`).
+* Program saved. Function count 8 → 19; memory blocks 1 → 10.