harness: close the instrument disagreement -- catch-all trustworthy, off = bit>>4

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
This commit is contained in:
SDD committed 2026-10-16 10:25:17 +03:00
1 parent 99ca146fe3
commit f49f9984bb
2 files changed
+320 -2

No files matched your search

+305
View File
@@ -0,0 +1,305 @@
# 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_map` entries are kept in insertion order
(`src/emu/addrmap.h:597`, `simple_list<address_map_entry> m_entrylist`), and
`program_map()` adds them in source order via `address_map::operator()`
(`src/emu/addrmap.cpp:874-878`).
* They are installed in that same order:
`src/emu/emumem_aspace.cpp:783-786` loops `map->m_entrylist` and calls
`populate_map_entry(entry, READ)` then `(entry, WRITE)`.
* `populate_map_entry` installs with `m_root_read->populate(...)`
(`src/emu/emumem_aspace.cpp:455`, `:488`).
* `populate` **unconditionally replaces** the existing dispatch slot, dropping
the previous handler: `src/emu/emumem_hedr.ipp:268-272`
```
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);
}
```
There is no first-wins case and no error: overlapping entries are silently
permitted. `DETECT_OVERLAPPING_MEMORY` is `0`
(`src/emu/addrmap.cpp:20`), and `map_validity_check` prints 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 does `target->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`
```cpp
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 in
`src/devices/cpu/tms34010/tms34010.h:991`: `memory_access<32, 1, 3, ...>` —
the second template parameter is `Width` (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`). The `0x2000000`-unit RAM map is `0x2000000 >> 3 =
0x400000` bytes = 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
```sh
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`)
```sh
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:
```sh
# 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 at `base` receives
`off = (bit_address - base) >> 4` (TMS34010 Width=1, AddrShift=3).
* The catch-all (`base 0`) logs **`bit_address >> 4`**. Convert:
* bit address = `logged << 4`
* **byte address = `logged << 1`**
* The board windows are the exception: `board_read` builds
`BOARD_BASE | (off & 0xffff)`, which yields `bit_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):
1. **"The catch-all's `0x400000` means bytes `0x80000`-`0x81AFF`."** No:
the delegate offset is `bit >> 4`, so `0x400000` is bit `0x04000000`, i.e.
**bytes `0x800000`-`0x81AFFE` (~110 KiB at 8 MiB)**, not a 64 KiB region at
512 KiB. Proven by S1 vs S2: the `0x04000000` probe catches the exact
accesses the catch-all logged at `0x400000` (`183595-59 = 183536`).
The doc's reading of `memory_access<32, 1, 3>` is also off by one field —
the second parameter is `Width` (log2 bytes) = 1, i.e. 16-bit, not a 1-bit
data width; `tms34010.cpp:66` states `data_width = 16`.
2. **"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-272` and
`:380-381`), verified in §4. The `0x400000` traffic was **outside RAM**
(bit `0x04000000` > `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 `0x3600400` blitter target quoted from POST does not match `0x04000000`;
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).
+15 -2
View File
@@ -183,6 +183,11 @@ private:
u16 calram100_r(offs_t off); void calram100_w(offs_t off, u16 data); // 0x01000000
u16 calram180_r(offs_t off); void calram180_w(offs_t off, u16 data); // 0x01800000
u16 calram200_r(offs_t off); void calram200_w(offs_t off, u16 data); // 0x02000000 (outside RAM)
// Mismatched-width overlap check (gated on VT1200_CAL_W8, needs VT1200_CAL
// too): an 8-bit handler over the same bottom slice as CTRL. It is
// registered even later, so if the mismatched populate path also follows
// last-wins it fires and CTRL does not.
u8 calw8_r(offs_t off); void calw8_w(offs_t off, u8 data);
// VT1200_LOG_UNMAPPED=<path>
// A catch-all mapping over the whole address space, registered FIRST so every
@@ -343,6 +348,9 @@ void vt1200_state::calram180_w(offs_t off, u16 data) { log_probe("RAM-0x01800000
u16 vt1200_state::calram200_r(offs_t off) { log_probe("RAM-0x02000000", 'R', (u32)off, 0x02000000); return 0; }
void vt1200_state::calram200_w(offs_t off, u16 data) { log_probe("RAM-0x02000000", 'W', (u32)off, 0x02000000); }
u8 vt1200_state::calw8_r(offs_t off) { log_probe("W8-MISMATCH", 'R', (u32)off, 0x00000000); return 0; }
void vt1200_state::calw8_w(offs_t off, u8 data) { log_probe("W8-MISMATCH", 'W', (u32)off, 0x00000000); }
void vt1200_state::machine_reset()
{
if (m_log)
@@ -428,14 +436,19 @@ void vt1200_state::program_map(address_map &map)
// The original disputed "video" range (a space address inside RAM).
map(0x00400000, 0x004fffff).rw(FUNC(vt1200_state::calvida_r), FUNC(vt1200_state::calvida_w));
// The catch-all's logged 0x400000 read as a space address, times 16:
// 0x400000 << 4 = 0x04000000.
map(0x04000000, 0x040fffff).rw(FUNC(vt1200_state::calvid4m_r), FUNC(vt1200_state::calvid4m_w));
// 0x400000 << 4 = 0x04000000. Gated separately so a plain VT1200_CAL run
// leaves the region to the catch-all (the strict A-vs-B cross-check).
if (getenv("VT1200_CAL_VID4M"))
map(0x04000000, 0x040fffff).rw(FUNC(vt1200_state::calvid4m_r), FUNC(vt1200_state::calvid4m_w));
// Where does RAM actually end? Probes at three MiB marks of the
// declared 4 MiB range, plus one OUTSIDE it (0x02000000).
map(0x00800000, 0x00800fff).rw(FUNC(vt1200_state::calram80_r), FUNC(vt1200_state::calram80_w));
map(0x01000000, 0x01000fff).rw(FUNC(vt1200_state::calram100_r), FUNC(vt1200_state::calram100_w));
map(0x01800000, 0x01800fff).rw(FUNC(vt1200_state::calram180_r), FUNC(vt1200_state::calram180_w));
map(0x02000000, 0x02000fff).rw(FUNC(vt1200_state::calram200_r), FUNC(vt1200_state::calram200_w));
// Mismatched-width check: same slice as CTRL, but an 8-bit handler.
if (getenv("VT1200_CAL_W8"))
map(0x00000000, 0x0000ffff).rw(FUNC(vt1200_state::calw8_r), FUNC(vt1200_state::calw8_w));
}
// Undecoded board windows. Left unmapped unless an experiment asks for