diff --git a/harness/mame/XCROSS_REPORT.md b/harness/mame/XCROSS_REPORT.md new file mode 100644 index 0000000..52b9053 --- /dev/null +++ b/harness/mame/XCROSS_REPORT.md @@ -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 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). diff --git a/harness/mame/vt1200.cpp b/harness/mame/vt1200.cpp index 6fbfb1d..522aea8 100644 --- a/harness/mame/vt1200.cpp +++ b/harness/mame/vt1200.cpp @@ -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= // 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