Closed cross-check (single run): the catch-all and VT1200_CAL proxies are directly comparable (both base 0) and resolve identically; last-wins holds for same- and mismatched-width overlaps. The apparent conflict was a units error: a .rw(FUNC(offs_t)) handler receives (bitaddr-base)>>4 (tms34010 Width=1, AddrShift=3), so the catch-all logs bitaddr>>4. Its 0x400000..0x40D7FF is bit 0x04000000..0x040D7FF0 (bytes 0x800000..0x81AFFE), outside the declared 4 MiB RAM -- genuinely unmapped. The old 0x00400000 proxy was 16x too low. Catch-all reproduces the committed log byte-for-byte and does not perturb coverage (56 / PC=3F208950 with and without). Regression holds: PC=3FDCE400 samples=199. Report: harness/mame/XCROSS_REPORT.md
13 KiB
XCROSS: which address instrument is trustworthy?
Closed instrumentation experiment, 2026-10-16. Driver: harness/mame/vt1200.cpp
(modified, env-gated). MAME source: /home/arti/code/k-space/mame0289.
The question
VT1200_LOG_UNMAPPED (the catch-all) logged 123,411 accesses at
0x00400000-0x0040D7FF and called them unmapped, but the main-RAM map
declares 0x00000000-0x01FFFFFF, which (taken as the same units) contains
those addresses. Meanwhile a VT1200_CAL proxy registered LAST over
0x00400000-0x00400FFF saw zero hits, while one over
0x00000000-0x0000FFFF fired. Which instrument is right, and why do they
disagree?
1. What MAME's source says
Precedence: LAST REGISTERED WINS (no ties, no first-wins)
address_mapentries are kept in insertion order (src/emu/addrmap.h:597,simple_list<address_map_entry> m_entrylist), andprogram_map()adds them in source order viaaddress_map::operator()(src/emu/addrmap.cpp:874-878).- They are installed in that same order:
src/emu/emumem_aspace.cpp:783-786loopsmap->m_entrylistand callspopulate_map_entry(entry, READ)then(entry, WRITE). populate_map_entryinstalls withm_root_read->populate(...)(src/emu/emumem_aspace.cpp:455,:488).populateunconditionally replaces the existing dispatch slot, dropping the previous handler:src/emu/emumem_hedr.ipp:268-272There is no first-wins case and no error: overlapping entries are silently permitted.for(offs_t ent = start_entry; ent <= end_entry; ent++) { m_u_dispatch[ent]->unref(); m_u_dispatch[ent] = handler; m_u_ranges[ent].set(ostart, oend); }DETECT_OVERLAPPING_MEMORYis0(src/emu/addrmap.cpp:20), andmap_validity_checkprints at most a warning (:1088). So "no overlap warning" proves nothing.- Narrower-than-native handlers follow the same rule. The mismatched path
calls
mismatched_patch, which also doestarget->unref(); target = replacement;(src/emu/emumem_hedr.ipp:380-381). Verified empirically in §4.
Conclusion: last-wins, for both same-width and different-width overlap.
Why the two instruments report different numbers: the handler offset is shifted
The handler does not receive the absolute space address. A front-end delegate receives
src/emu/emumem_hedp.cpp:15
return m_delegate(*this->m_space,
((offset - this->m_address_base) & this->m_address_mask) >> (Width + AddrShift),
mem_mask);
where m_address_base = entry start and m_address_mask comes from
check_optimize_all (src/emu/emumem.cpp:595: nmask = addrmask ? addrmask : changing_bits | addrselect).
For the TMS34010:
src/devices/cpu/tms34010/tms34010.cpp:66—m_program_config("program", ENDIANNESS_LITTLE, 16, 32, 3, ...), i.e. data_width 16, addr_width 32, addr_shift 3. (Same insrc/devices/cpu/tms34010/tms34010.h:991:memory_access<32, 1, 3, ...>— the second template parameter isWidth(log2 bytes), not a 1-bit data width; Width=1 means 16-bit.)Width = 1,AddrShift = 3⇒ the shift is 4.- Space (bit) address → bytes:
address >> addr_shift=>> 3(src/emu/emumem.h:2078). The0x2000000-unit RAM map is0x2000000 >> 3 = 0x400000bytes = 4 MiB, matching the 4194304-byte RAM dump.
Therefore a .rw(FUNC(offs_t)) handler over an entry starting at base
receives
off = (bit_address - base) >> 4
For the catch-all and RAM (base 0): off = bit_address >> 4; i.e. the
catch-all's logged address is not the map address, it is the map address
divided by 16. The driver's own log header already said this
(# ... firmware operand = addr << 4), but the address was compared to the RAM
range without applying it.
This dissolves the contradiction. The catch-all's 0x400000..0x40D7FF
are bit addresses 0x04000000..0x040D7FF0 (bytes 0x800000..0x81AFFE,
~8 MiB), which is outside the declared 4 MiB RAM. The catch-all was right.
The VT1200_CAL proxy over 0x00400000 was simply placed 16× too low.
Independent anchor: the board switch. The disassembly operand is 0x064000D0
(bit address); the driver's board_read reconstructs BOARD_BASE | (off & 0xffff) and logged 0x0640000D, so the delegate off was 0xD0 >> 4 = 0x0D.
Same shift.
2. The closed cross-check (one run, one configuration)
All runs headless (-video none -sound none -nothrottle -debug -debugger none),
progress.lua, 200 frames, ROM path harness/mame/roms. Binary:
/home/arti/code/k-space/mame0289/vt1200.
S1 — catch-all (first) + CAL proxy over 0x00400000 (last) + control, same run
cd /home/arti/code/k-space/vt1200
M=/home/arti/code/k-space/mame0289/vt1200
VT1200_LOG_UNMAPPED=/tmp/opencode/xcross/S1_A.log \
VT1200_CAL=/tmp/opencode/xcross/S1_B.log \
PROG_FRAMES=200 timeout 300 $M vt1200 -rompath harness/mame/roms \
-video none -sound none -nothrottle -debug -debugger none \
-autoboot_script harness/mame/progress.lua -autoboot_delay 0
Raw result:
A (VT1200_LOG_UNMAPPED, registered FIRST, range 0..0xffffffff):
183595 lines, addresses 00400000..0064000F <-- logs the region
B (VT1200_CAL, registered LAST):
2833 CTRL (0x00000000-0x0000ffff) <-- positive control FIRES
0 VID-A-0x00400000 (0x00400000-0x004fffff) <-- zero hits
0 RAM-0x00800000 / RAM-0x01000000 / RAM-0x01800000 / RAM-0x02000000
So in one run: A reports the 0x00400000-style region; B's proxy over
0x00400000 reports nothing; the positive control fires. That is exactly the
reported disagreement — and it is not a difference in overlap resolution.
S2 — same, plus a proxy over 0x04000000 (= 0x400000 << 4)
VT1200_LOG_UNMAPPED=/tmp/opencode/xcross/S2_A.log \
VT1200_CAL=/tmp/opencode/xcross/S2_B.log VT1200_CAL_VID4M=1 \
PROG_FRAMES=200 timeout 300 $M vt1200 -rompath harness/mame/roms \
-video none -sound none -nothrottle -debug -debugger none \
-autoboot_script harness/mame/progress.lua -autoboot_delay 0
Raw result:
A: 59 lines, 00580000..0064000F <-- the 0x400000 region is GONE from A
B: 2833 CTRL
183536 VID-4M-0x04000000 <-- the region moved here
A and B now overlap at 0x04000000; the last-registered probe (B) wins and
A is silent there. Containment is exact: 183595 - 59 = 183536, i.e. every
access A lost is an access B gained. Because the 0x04000000 probe caught
the traffic that S1's catch-all logged as 0x400000, the shift-16 relation is
proven by construction (a shift-0 reading would put the traffic at
0x00400000, where the probe range 0x04000000 would miss it).
Delegate-offset measurement (positive control, base 0, entry 0..0xffff):
CTRL W off=00000000 space4=00000000 space0=00000000 pc=3F2009E0
CTRL W off=00000001 space4=00000010 space0=00000001 pc=3F2009E0
...
max off = 0x53D -> off is bit_address>>4, not bit_address
off never exceeds 0xFFF in a 64K-bit entry, so off = address >> 4.
VID-4M sample (PC 3F204A20 is the blitter path the old analysis identified):
VID-4M-0x04000000 W off=00000000 space4=04000000 space0=04000000 pc=3F204A20
...
VID-4M-0x04000000 W off=0000D8E1 space4=040D8E10 space0=0400D8E1 pc=3FDF9A00
Reproduction and perturbation check (step 4)
VT1200_LOG_UNMAPPED alone (handshake config) reproduces the committed
harness/mame/unmapped.log byte for byte (123413 lines = 2 header + 123411
accesses, range 0x00400000..0x0040D7FF).
Handshake config, coverage:
# without catch-all
VT1200_BOARD_MODEL=handshake VT1200_BOARD_BUSY_TICKS=2 \
VT1200_BOARD_SWITCH="0640000D:70:70" COV_FRAMES=600 COV_WINDOW=300 \
timeout 400 $M vt1200 ... -autoboot_script harness/mame/coverage.lua
# frame 600 new=2 total_distinct=56 PC=3F208950
# with catch-all (VT1200_LOG_UNMAPPED=/tmp/.../perturb.log)
# frame 600 new=2 total_distinct=56 PC=3F208950
Identical coverage and parked PC, and the log is byte-identical to the
committed one. The catch-all does not perturb the machine (it is shadowed by
RAM over 0..0x1FFFFFF and only handles the genuinely unmapped >=0x04000000).
Mismatched-width check (step 4 of the parallel brief)
An 8-bit (narrower-than-native) probe registered last over the same
0x00000000-0x0000FFFF slice as the 16-bit CTRL (VT1200_CAL +
VT1200_CAL_W8=1):
B tags: 11195 W8-MISMATCH (off 0x0..0x1FFF)
0 CTRL <-- the later 8-bit handler replaced the 16-bit one
Last-wins holds for the width-mismatched path too; width is not the explanation for the disagreement. (This run's probe replaced live low RAM with zeros and changed the boot path, which is itself a reminder that any last-registered probe over live RAM perturbs.)
3. Where does main RAM actually end? (step 3)
Probes at 0x00800000, 0x01000000, 0x01800000 (inside the declared range)
and 0x02000000 (just outside) were registered last. In every unperturbed
run (default wedge and the progressive handshake config, both 600 frames) all
four logged zero hits. The firmware never touches bit address >= 0x800000
(byte >= 1 MiB), so its behaviour cannot localise RAM's end. The declared
4 MiB is not contradicted, but it is also not confirmed by execution.
(Only the deliberately perturbing 8-bit run, which no longer wedges, touched
all four — including 0x02000000, outside the declared range — but that run's
control flow is altered by the probe, so it does not establish the map.)
4. Verdict
The catch-all (VT1200_LOG_UNMAPPED) is the trustworthy instrument. Its
accesses are real and its PC/kind are correct; only its printed address is in
shifted units.
The VT1200_CAL proxy mechanism was also trustworthy, but the proxy at
0x00400000 was misplaced by a factor of 16. The "zero hits" was an
addressing error, not a resolution disagreement. There was never a conflict
between the two handlers: under last-wins the catch-all is shadowed by every
later entry, so it only ever reports genuinely unmapped accesses, and the
0x400000 traffic was simply outside RAM.
Correct address convention
- Address-map / space address = TMS34010 bit address. Bytes =
bit >> 3. - A
.rw(FUNC(offs_t))handler on an entry starting atbasereceivesoff = (bit_address - base) >> 4(TMS34010 Width=1, AddrShift=3). - The catch-all (
base 0) logsbit_address >> 4. Convert:- bit address =
logged << 4 - byte address =
logged << 1
- bit address =
- The board windows are the exception:
board_readbuildsBOARD_BASE | (off & 0xffff), which yieldsbit_base + (operand >> 4), a hybrid that is nevertheless consistent with the log.
Corrected locations (previously mis-stated)
| item | old claim | corrected |
|---|---|---|
| blitter traffic, catch-all logged | 0x00400000-0x0040D7FF (a map address) |
bit 0x04000000-0x040D7FF0; bytes 0x800000-0x81AFFE (~8 MiB) |
VT1200_CAL proxy should be at |
0x00400000 |
0x04000000 (bit) |
| catch-all logged addr vs bit addr | same | logged = bit >> 4 |
4b. Reconciliation with the parallel doc commit
While this experiment ran, a parallel session committed
99ca146fe304 ("ADDRESS UNITS RESOLVED ...") into this shared worktree. It is
right that the map is in BITS and bytes = bit >> 3, but it contains two
errors this experiment corrects (I did not edit that file — it is not mine):
- "The catch-all's
0x400000means bytes0x80000-0x81AFF." No: the delegate offset isbit >> 4, so0x400000is bit0x04000000, i.e. bytes0x800000-0x81AFFE(~110 KiB at 8 MiB), not a 64 KiB region at 512 KiB. Proven by S1 vs S2: the0x04000000probe catches the exact accesses the catch-all logged at0x400000(183595-59 = 183536). The doc's reading ofmemory_access<32, 1, 3>is also off by one field — the second parameter isWidth(log2 bytes) = 1, i.e. 16-bit, not a 1-bit data width;tms34010.cpp:66statesdata_width = 16. - "The precedence rule must be subtler than later-wins; maybe
populate_mismatched." No: precedence is plain last-wins for both same-width and mismatched-width handlers (emumem_hedr.ipp:268-272and:380-381), verified in §4. The0x400000traffic was outside RAM (bit0x04000000>0x01FFFFFF), so there was never a RAM/catch-all conflict to explain.
5. What I could not determine
- The true end of main RAM. Firmware execution never reaches bit
>= 0x800000, so the RAM-end probes cannot fire; a transparent (non-shadowing) probe would be needed. - Whether the ~8 MiB region is really video memory, and its exact size and layout. The region is established as genuinely unmapped and as the blitter's target; modelling it is explicitly out of scope here.
- The
0x3600400blitter target quoted from POST does not match0x04000000; it may be an internal/TMS register or a different pointer, and I did not resolve it.
6. Regression
With no VT1200_* env var set, the driver still prints:
PC=3FDCE400 samples=199
All new instrumentation is env-gated (VT1200_CAL, VT1200_CAL_VID4M,
VT1200_CAL_W8; VT1200_LOG_UNMAPPED unchanged).