Files
vt1200/harness/mame/CODE22_REPORT.md
T
SDD 5ef8712960 harness: identify the level-1 video-memory test that raises code 0x100 (VIDEO MEMORY-22)
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.
2026-10-16 10:55:44 +03:00

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,…,0x8000 to bit 0x04000000 read back 0.
  • 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 bpset fires before its instruction executes (the a8 at AT-READ is stale; the loaded code is the value at AT-NEXT).
  • The code the dispatcher reads is the dword at bit a13-0x20 = 0x16F0. Hit 1 = 0xFFFF → handler 0x3FDEFF60 prints LEVEL1/2 TESTING **; hit 2 = 0x100 → handler 0x3FDE7F00 prints LEVEL1/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=1 run: only HANDLER-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:

  1. A readable/writable region at bit 0x04000000. The 32-bit pattern phase walks a4 = 0x20000 dwords with post-increment — i.e. 512 KiB of video memory at bit 0x04000000. With no mapping, reads return 0 and the very first walking-bit compare fails (a12=0xFFFF, all 16 bits wrong).
  2. 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 between 0x3F201E60 and 0x3F201E90 (both read 0x0E74). The firmware treats "unchanged" as an error (jaeq L3F2070C0, status 0x0E).

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; source L3FA7C050; movi 0x100 at 0x3FA7DE30; memory test L3F2018B0; 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 0xC00001F0 reads 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 REFCNT is (total_cycles()/16) & 0xfffc (src/devices/cpu/tms34010/tms34010.cpp:1573).
  • Whether the status slot at bit 0x16F0 is a true global or the frame slot a13-0x20 of the L3FDE55F0 dispatch frame. a13 is a constant 0x1710 at 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) L3FA7C050 corresponds to beyond "video memory": it is the routine that calls the memory test on the 0x04000000 region and reports VIDEO 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 — added VT1200_LOWLOG=<path>: a transparent, env-gated logging proxy over bit 0x0000-0x3FFF (forwards to mainram).
  • 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 each L3F2018B0 error 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.