The dispatch status is 0x100 (printed as '22' in the message). A driver-side transparent low-RAM write log (VT1200_LOWLOG) shows the slot at bit 0x16F0 is written by the store at 3FDF9090 from a13(0x10), which L3FA7C050 sets to the literal 0x100 when the memory test L3F2018B0 returns non-zero. L3F2018B0 is the level-1 video-memory test (base bit 0x04000000, count 0x20000). Its failing checks: the 16-bit walking-bit read-back at 0x04000000 (baseline, unmapped) and, with video RAM mapped, the REFCNT (0xC00001F0) 'must change' check.
14 KiB
CODE 22 — which level-1 test raises it, and why
Bounded investigation, 2026-10-16. Subagent task: identify the level-1 test that raises the "VIDEO MEMORY - 22" status and the condition that raises it. The task is identification only — level 1 is not made to pass.
Driver: harness/mame/vt1200.cpp (env-gated additions, see §7).
MAME: /home/arti/code/k-space/mame0289. ROM: harness/mame/roms/vt1200/vt1200_v22.bin.
0. Answer in one paragraph
The failing test is the level-1 video-memory self-test at bit 0x3F2018B0
(file 0x000316, Ghidra 0x07E40316), which the level-1 runner calls with the
screen-memory base bit 0x04000000 (arg3) and a count of 0x20000
(arg4). It does a 16-bit walking-bit data-bus check on the first word at
0x04000000, then a 32-bit pattern check across the region, then a DRAM
refresh-counter check. When any of these fails it returns a non-zero status
(0x19 = walking-bit / 0x0E = REFCNT in the runs below). The caller at
0x3FA7C050 turns any non-zero return into the literal constant 0x100
(at bit 0x3FA7DE30, file 0x10FBC6). That 0x100 is written into the frame
slot a13-0x20 (bit 0x16F0) by the store at bit 0x3FDF9090, and the error
dispatcher at bit 0x3FDF0630 maps 0x100 → handler 0x3FDE7F00
(Ghidra 0x07FBCFE0), which prints the string LEVEL1/2 ERROR -> VIDEO MEMORY - 22.
Note on the number: the dispatcher's status code for this error is
0x100; 22 is a constant in the message string, as the bring-up log already
recorded for 0x40000C. So "status code 22" = dispatcher code 0x100.
The condition that makes the test fail depends on which hardware is modelled:
- Default (committed) driver, no video RAM mapped — the walking-bit check
fails: writes of
1,2,4,…,0x8000to bit0x04000000read back0. - With video RAM mapped (
VT1200_VRAM=1) — the walking-bit and pattern checks now pass; the failure moves to the DRAM refresh-counter (REFCNT) check:REFCNT(0xC00001F0) returns the same value (0x0E74) on two consecutive reads, which the firmware treats as an error.
So the residual condition — the one that still holds after "video memory is mapped and round-trips", which is exactly the state the bring-up log describes — is the REFCNT check.
1. Method
The stack-walk approach in dispatch_walk.lua only sampled 8 stack words and
reached the reporting framework, so it was replaced with a write watch on the
status slot, done driver-side (no watchpoints — wpset is a dead end here).
1a. The dispatcher reads a 32-bit code from bit 0x16F0
harness/mame/code22_probe.lua, run with the level-1 config
(VT1200_BOARD_MODEL=handshake VT1200_BOARD_BUSY_TICKS=2 VT1200_BOARD_SWITCH=0640000D:70:70), breaks at the dispatcher read and at the
next instruction:
AT-READ pc=3FDF0630 a13=00001710 a14=00001770 a8=00000001 d@16F0=0000FFFF
AT-NEXT pc=3FDF0650 a8=0000FFFF d@16F0=0000FFFF
AT-READ pc=3FDF0630 a13=00001710 a14=00001770 a8=00000100 d@16F0=00000100
AT-NEXT pc=3FDF0650 a8=00000100 d@16F0=00000100
Two facts fall out:
- A
bpsetfires before its instruction executes (thea8atAT-READis stale; the loaded code is the value atAT-NEXT). - The code the dispatcher reads is the dword at bit
a13-0x20=0x16F0. Hit 1 =0xFFFF→ handler0x3FDEFF60printsLEVEL1/2 TESTING **; hit 2 =0x100→ handler0x3FDE7F00printsLEVEL1/2 ERROR -> VIDEO MEMORY - 22.
The two hits and their order match the captured console transcript exactly.
Confirming the switch arithmetic with dis34 (its subi operand is the amount
actually subtracted, unlike Ghidra's rendering):
3fdf07e0: subi 0x78, a8 ; jaeq L3FDE7A00 ; code 0x080
3fdf0820: subi 0x80, a8 ; jaeq L3FDE7F00 ; code 0x100 <- VIDEO MEMORY-22
3fdf0860: subi 0x100,a8 ; jaeq L3FDE8150 ; code 0x200
...
3fdf0a20: subi 0x7fff,a8; jaeq L3FDEFF60 ; code 0xFFFF <- LEVEL1/2 TESTING
1b. Driver-side write watch finds the writer
New env-gated knob VT1200_LOWLOG=<path>: a transparent logging proxy over
bit 0x0000-0x3FFF that forwards reads/writes to the real mainram share and
records every distinct (addr, pc, value, kind). It does not perturb execution
(regression verified, §6).
Command:
M=/home/arti/code/k-space/mame0289/vt1200
VT1200_BOARD_MODEL=handshake VT1200_BOARD_BUSY_TICKS=2 \
VT1200_BOARD_SWITCH="0640000D:70:70" VT1200_LOWLOG=/tmp/opencode/code22/low.log \
CP_FRAMES=300 timeout 300 $M vt1200 -rompath harness/mame/roms \
-video none -sound none -nothrottle -debug -debugger none -debuglog \
-autoboot_script harness/mame/code22_probe.lua -autoboot_delay 0
Relevant lines (the only writer of 0x100 to the status slot):
W 001590 pc=3FDF8BF0 val=0100
R 001590 pc=3FDF8D80 val=0100
R 001590 pc=3FDF8FB0 val=0100
R 001590 pc=3FDF90B0 val=0100
W 0016F0 pc=3FDF90B0 val=0100 <-- the status slot is loaded here
R 0016F0 pc=3FDF0630 val=0100 <-- the dispatcher reads it
R 0016F0 pc=3FDF0650 val=0100
(pc is reported as the next instruction, i.e. after the displacement word;
pc=3FDF90B0 is the store at 0x3FDF9090, pc=3FDF8BF0 is the store at
0x3FDF8BD0.)
2. The call chain, bottom to top
Bits and file offsets:
| bit | file | what |
|---|---|---|
3FDF0630 |
0x17E0C6 |
dispatcher reads code from a13-0x20 (Ghidra 07FBE0C6) |
3FDE7F00 |
0x17CFE0 |
VIDEO MEMORY-22 handler (Ghidra 07FBCFE0) |
3FDF9090 |
0x17F212 |
move *a13(0x10), *a14+, 1 — loads status into the dispatch frame |
3FDF8FB0 |
0x17F1F6 |
jaeq L3FDF86B0 — "if result == 0, retry" |
3FDF8BD0 |
0x17F17A |
move a8, *a13(0x10), 1 — stores the test's return value |
3FDF8BA0 |
0x17F174 |
calla L3FA7C050 — the sub-test |
3FA7DE30 |
0x10FBC6 |
movi 0x100, a7 — turns any failure into code 0x100 |
3FA7DD70 |
0x10FBAE |
calla L3F2018B0 — the memory test |
3F2018B0 |
0x000316 |
the level-1 video-memory test |
3F201C10 |
0x000382 |
walking-bit read-back |
3F201C20 |
0x000384 |
walking-bit compare (xor) |
3F201C80 |
0x000390 |
walking-bit failure exit |
3F201E90 |
0x0003D2 |
REFCNT read |
3F201ED0 |
0x0003DA |
REFCNT compare / error branch |
3F2070C0 |
0x000E18 |
REFCNT error handler (movi 0xe,a8) |
3F208060 |
0x00100C |
restart entry (DINT) the task referenced |
2a. The runner decides the code (bit 0x3FDF8A90 region)
3fdf8ba0: calla L3FA7C050
3fdf8bd0: move a8, *a13(0x10), 1 ; store the sub-test's return value
...
3fdf8f90: move *a13(0x10), a11, 1
3fdf8fb0: jaeq L3FDF86B0 ; 0 => no error, loop back
3fdf8fd0: sub a9, a9
...
3fdf9090: move *a13(0x10), *a14+, 1 ; pass it on as the error code
3fdf90b0: calla L3FDE55F0 ; -> report/dispatch
2b. L3FA7C050 maps any non-zero failure to 0x100
3fa7dd70: calla L3F2018B0
3fa7dda0: move a8, a8
3fa7ddb0: jreq L3FA7DEA0 ; 0 => continue
...
3fa7de30: movi 0x100, a7 ; <-- the literal status code
3fa7de50: move a7, *a11(0x40), 1
3fa7de70: move a7, a8
3fa7de80: jauc L3FA900A0 ; return 0x100
So the internal status code is set at bit 0x3FA7DE30 (file 0x10FBC6), and
the condition is simply "the memory test returned non-zero".
2c. L3F2018B0 is the level-1 video-memory test
Loaded arguments (breakpoint at 0x3F201990):
ARGS a13=00000000 a7=00000000 a3=04000000 a4=00020000
a3 = 0x04000000— the walking-bit / pattern target base (screen memory).a4 = 0x00020000— iteration count.
Walking-bit loop (16 iterations, single word at a5 = a3):
3f201b20: movi 0x1, a8
3f201b60: movi 0x10, a9
L3F201B80:
3f201b80: clr a11
3f201b90: move a11, *a5, 0 ; write 0
3f201ba0: move a8, *a5, 0 ; write walking bit
(nops)
3f201c10: move *a5, a11, 0 ; read back
3f201c20: xor a8, a11 ; a11 = read ^ expected
3f201c30: or a11, a12 ; accumulate failures
3f201c40: sll 0x1, a8
3f201c50: dsjs a9, L3F201B80
3f201c60: or a12, a12
3f201c70: jreq L3F201CF0
3f201c80: move a12, *a13(0x100), 1 ; <-- FAILURE exit
3f201ca0: movi 0x19, a8 ; status 25
3f201cd0: jauc L3F207200 ; return
DRAM refresh-counter check (later in the same routine):
3f201e30: movi 0xea60, a3
3f201e60: move @0xc00001f0, a0, 0 ; REFCNT
L3F201E90:
3f201e90: move @0xc00001f0, a6, 0 ; REFCNT again
3f201ec0: cmp a0, a6
3f201ed0: jaeq L3F2070C0 ; equal => FAILURE exit
3f201ef0: move a6, a0
3f201f00: dsjs a3, L3F201E90
...
3f2070c0: movi 0xe, a8 ; status 14
3f2070f0: move a2, a1
3f207100: jruc L3F207200 ; return
L3F207200 is the common error return: it stores a8 into a13(0xc0) /
a13(0x40), then at 3f2073d0 or a8,a8; jreq L3F207510 returns a8
non-zero. (When sp == 0 it instead jauc L3F208060, the DINT restart entry.)
3. Observed failures (raw evidence)
harness/mame/code22_final.lua, 300 frames.
Default driver (no video RAM mapped):
ARGS a13=00000000 a7=00000000 a3=04000000 a4=00020000
WALK-CMP read=00000000 expected=00000001
WALK-CMP read=00000000 expected=00000002
... (every bit reads 0) ...
WALK-FAIL a12=0000FFFF a8=00010000 a11=00008000
MEMTEST-RET a8=00000019
C050-RET-0x100 a8=00000019
With VT1200_VRAM=1 (video RAM mapped 0x04000000-0x1fffffff):
ARGS a13=00000000 a7=00000000 a3=04000000 a4=00020000
WALK-CMP read=00000001 expected=00000001
WALK-CMP read=00000002 expected=00000002
... (every walking bit reads back correctly) ...
REFCNT-CMP a0=00000E74 a6=00000E74
MEMTEST-RET a8=0000000E
C050-RET-0x100 a8=0000000E
(i.e. a0 == a6 — REFCNT did not change between the two reads.)
Additional checks with a targeted failure-branch probe
(harness/mame/code22_fail.lua, breakpoints on each error exit of
L3F2018B0):
- no-VRAM run: only
WALKING-BIT-MISMATCH→L3F207200(walkerr)→L3F2073D0→L3FA7DE30. VT1200_VRAM=1run: onlyHANDLER-3F2070C0-REFCNT-eq→L3F207200(a8=0x0e)→L3F2073D0→L3FA7DE30; the walking-bit branch never fires.
4. What the condition actually is
The video-memory test requires, in order:
- A readable/writable region at bit
0x04000000. The 32-bit pattern phase walksa4 = 0x20000dwords with post-increment — i.e. 512 KiB of video memory at bit0x04000000. With no mapping, reads return 0 and the very first walking-bit compare fails (a12=0xFFFF, all 16 bits wrong). - The DRAM refresh counter
REFCNT(0xC00001F0) to change between two consecutive reads. MAME returns(total_cycles()/16) & 0xfffc, which is unchanged across the ~2 instructions between0x3F201E60and0x3F201E90(both read0x0E74). The firmware treats "unchanged" as an error (jaeq L3F2070C0, status0x0E).
Both produce the same outcome: L3F2018B0 returns non-zero →
L3FA7C050 returns literal 0x100 → dispatcher code 0x100 →
LEVEL1/2 ERROR -> VIDEO MEMORY - 22.
Reconciling with the bring-up log's claim ("video memory is mapped and
round-trips, yet the test still fails"): reproduced. With
VT1200_VRAM=1 the walking-bit and pattern checks pass and the failure is
purely the REFCNT check. That is the remaining requirement.
Confidence:
- The chain (dispatcher code 0x100 → handler 0x3FDE7F00; writer of the slot at
0x3FDF9090; sourceL3FA7C050;movi 0x100at0x3FA7DE30; memory testL3F2018B0; branch that fires) — high, all measured, both directions. - The walking-bit condition (reads return 0 when the region is unmapped) — high.
- The REFCNT condition (two consecutive
0xC00001F0reads equal → error) — high as observed behaviour; the interpretation that this is the intended hardware requirement (vs. a MAME REFCNT granularity artifact) is medium.
5. What I could not determine
- The exact original hardware REFCNT tick rate, i.e. whether the firmware's
"must change between two consecutive reads" is satisfiable on real hardware
and merely too slow in MAME. MAME's
REFCNTis(total_cycles()/16) & 0xfffc(src/devices/cpu/tms34010/tms34010.cpp:1573). - Whether the status slot at bit
0x16F0is a true global or the frame slota13-0x20of theL3FDE55F0dispatch frame.a13is a constant0x1710at the dispatcher in every observed run, and*a13(0xffe0)is the standard first-argument access (7325 read sites), so the slot behaves as a fixed address either way. - Which level-1 test number (service-guide ordering)
L3FA7C050corresponds to beyond "video memory": it is the routine that calls the memory test on the0x04000000region and reportsVIDEO MEMORY.
6. Regression
With no VT1200_* env var set, progress.lua at 200 frames still prints:
PC=3FDCE400 samples=199
All new instrumentation is env-gated (VT1200_LOWLOG). The proxy is
transparent (forwards to the real mainram share), so even with VT1200_LOWLOG
set the boot path is unchanged.
7. Files added / changed
harness/mame/vt1200.cpp— addedVT1200_LOWLOG=<path>: a transparent, env-gated logging proxy over bit0x0000-0x3FFF(forwards tomainram).harness/mame/code22_walk.lua— dump registers + stack at the dispatcher.harness/mame/code22_probe.lua— resolve before/after semantics + slot value.harness/mame/code22_fail.lua— break on eachL3F2018B0error exit.harness/mame/code22_args.lua— print memory-test args + walking-bit/REFCNT values.harness/mame/code22_final.lua— compact evidence capture for this report.