From a823dc54254730fe038b84daeece005055332c1c Mon Sep 17 00:00:00 2001 From: DoubleGate Date: Sun, 2 Aug 2026 10:14:15 -0400 Subject: [PATCH 1/3] feat(accuracysnes): add A6.15, all 256 opcodes defined and only STP hangs Coverage 360 -> 361 of 443. The row executes each of the 241 straight-line 65C816 opcodes in a WRAM sandbox and counts three outcomes against the length Table 5-4 of the WDC W65C816S datasheet documents: returned where it should, returned late, or did not return. The expectation comes from the datasheet via a new cpu_opcodes.rs, built from the matrix's own columnar rules with a gate that fails the build if they do not tile it -- which caught XBA ($EB) missing on the first run. The terminator cannot be a return. TXS and TCS move the stack pointer, and with x=1 a TXS puts it in page zero, so the return address is no longer where an RTS would pop it from. Control comes back through a JMP, and because a JMP is three bytes the exit addresses are chosen so its OWN operand bytes are harmless one-byte instructions: $AAAA (TAX) clean, $B8B8 (CLV) overshoot. An opcode that consumes one byte too many therefore executes a register transfer and walks into a NOP fill rather than executing half an address. ($7E:EAEA was the first choice and is wrong -- it is inside the space runtime.inc reserves for widening the measurement channel.) The watchdog takes TWO strikes. NMI fires once per vblank and across 241 sandbox runs will eventually land inside a healthy one, so one hit cannot mean stuck. Four opcodes are dangerous even when correct and the preamble handles each rather than excluding it: MVN/MVP move A+1 bytes so A=0; XCE flips to emulation mode only if C is set so CLC first makes it a no-op; TXS/TCS are why the exits restore SP from WRAM; SED and PLP are why they re-establish m, x, d and c. Verified by injection twice. WDM made three bytes -> exactly one LATE, first-bad $42. TRB dp made to jam -> exactly one NO-RETURN, first-bad $14, watchdog rescuing the battery. Picking that second injection is not free: SED and CLC both hang the cart BEFORE A6.15 runs, because the runtime and earlier Group A rows execute them. Also fixes the bug that made this look like a reference defect. The NMI handler runs with the sandbox's DBR ($7E), so `lda $4210` read a WRAM byte and RDNMI was never acknowledged. Three hosts never landed an NMI where it showed; ares did, and the row reported PLA ($68) overshooting on ares alone. It was ours, not ares'. Long addressing fixes it, and the handler now preserves A as well -- RTI restores P and PC but not the accumulator. Battery 311/311. Three references agree; ARES_KNOWN_FAILURES stays 3. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 47 ++ .../tests/accuracysnes.rs | 9 +- docs/accuracysnes-coverage.md | 4 +- docs/accuracysnes-plan.md | 2 +- tests/roms/AccuracySNES/ERROR_CODES.md | 9 + tests/roms/AccuracySNES/SOURCE_CATALOG.tsv | 529 ++++++------ tests/roms/AccuracySNES/asm/tests_group_a.s | 797 +++++++++++++++--- .../AccuracySNES/build/accuracysnes-pal.sfc | Bin 262144 -> 262144 bytes .../roms/AccuracySNES/build/accuracysnes.sfc | Bin 262144 -> 262144 bytes .../roms/AccuracySNES/gen/src/cpu_opcodes.rs | 475 +++++++++++ tests/roms/AccuracySNES/gen/src/dossier.rs | 1 + tests/roms/AccuracySNES/gen/src/main.rs | 1 + .../AccuracySNES/gen/src/tests/cpu_defined.rs | 749 ++++++++++++++++ tests/roms/AccuracySNES/gen/src/tests/mod.rs | 2 + 14 files changed, 2259 insertions(+), 366 deletions(-) create mode 100644 tests/roms/AccuracySNES/gen/src/cpu_opcodes.rs create mode 100644 tests/roms/AccuracySNES/gen/src/tests/cpu_defined.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index a5bf5d75..8ca0f775 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,53 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **`A6.15` — every 65C816 opcode is defined, and only `STP` hangs. Coverage 361 of 443.** The row + executes each of the 241 straight-line opcodes in a WRAM sandbox and counts three outcomes against + the length **Table 5-4 of the WDC W65C816S datasheet** documents: returned where it should, + returned late, or did not return. + + **The sandbox terminator cannot be a return.** `TXS` and `TCS` move the stack pointer — with + `x = 1` a `TXS` puts it in page zero — so the return address is no longer where an `RTS` would pop + it from. Control comes back through a `JMP`, and because a `JMP` is three bytes, the addresses are + chosen so its **own operand bytes are harmless one-byte instructions**: `$AAAA` (`TAX`) for the + clean exit and `$B8B8` (`CLV`) for the overshoot one. An opcode that consumes one byte too many + therefore executes a register transfer and walks into a `NOP` fill, instead of executing half an + address. + + **The watchdog takes two strikes.** `runtime.s` already carried an NMI trampoline with a settable + vector; one strike would be wrong, because NMI fires once per vblank and across 241 sandbox runs + it will eventually land inside a *healthy* one. + + **Four opcodes are dangerous even when correct, and the preamble handles each rather than + excluding it:** `MVN`/`MVP` move `A + 1` bytes, so `A = 0`; `XCE` flips to emulation mode only if + `C` is set, so `CLC` first makes it a no-op; `TXS`/`TCS` are why the exits restore `SP` from WRAM; + `SED` and `PLP` are why they re-establish `m`, `x`, `d` and `c` rather than trusting what came back. + + **Verified by injection, twice.** `WDM` made a three-byte instruction produced exactly one LATE + with first-bad `$42`; `TRB dp` made to jam produced exactly one NO-RETURN with first-bad `$14`, + the watchdog rescuing the battery. Picking that second injection is not free — `SED` and `CLC` + both hang the cart *before* `A6.15` runs, because the runtime and earlier Group A rows execute + them. `TRB` is executed nowhere else on the cart. + +### Fixed + +- **The `A6.15` watchdog read `RDNMI` through `DBR`, and it cost a false accusation of a reference.** + The NMI handler runs with whatever data bank the sandbox left — `$7E` — so `lda $4210` read a WRAM + byte and the NMI was never acknowledged. Three hosts happened never to land an NMI where it + showed; **ares did**, and the row reported `PLA` (`$68`) overshooting on ares alone, stably, with + every other opcode agreeing. + + That reads exactly like a reference bug, and it was ours. The chain that settled it is worth + recording: a diagnostic that replaced the terminator with `INX` fill showed **both** ares and + RustySNES resuming at the correct offset, so `PLA`'s length was never in question; sweeping only + `$68` passed on ares, so the failure needed the full sweep's elapsed time; and disarming the + watchdog made all four hosts agree. Long addressing (`lda f:$004210`) is DBR-independent and fixes + it. The handler now also preserves `A` — `RTI` restores `P` and `PC` but not the accumulator, and + an interrupt that silently rewrites `A` is not transparent to what it interrupted. + + With the fix the injections are exact where they had been approximate: the jam injection reported + two stuck opcodes and 63 clean ones before, and reports one and 240 now. + - **`E2.10` — the full 256-opcode SPC700 cycle sweep, and the coverage number moves to 360 of 443.** The cart measures how long every opcode the SPC700 can execute in a straight line actually takes, and compares it on-cart against the cycle count **fullsnes** documents for it. The host supplies diff --git a/crates/rustysnes-test-harness/tests/accuracysnes.rs b/crates/rustysnes-test-harness/tests/accuracysnes.rs index fd128a2f..69bde5c6 100644 --- a/crates/rustysnes-test-harness/tests/accuracysnes.rs +++ b/crates/rustysnes-test-harness/tests/accuracysnes.rs @@ -599,7 +599,7 @@ const A5_08_SLOTS: [(u8, &str); 7] = [ /// /// Version-agnostic on purpose: this list grows whenever a Group E row records a slot, and naming /// one release in the doc made it wrong the first time it did. -const GROUP_E_BATCH_SLOTS: [(u16, &str); 25] = [ +const GROUP_E_BATCH_SLOTS: [(u16, &str); 29] = [ (256, "E8.01 KON sweep key-on mask (bit 7 = voice 0)"), ( 257, @@ -629,6 +629,13 @@ const GROUP_E_BATCH_SLOTS: [(u16, &str); 25] = [ "B2.07 APU ticks over 48 frames (~6398 NTSC, ~7689 PAL)", ), (276, "B2.07 measured frame height (261 NTSC, 311 PAL)"), + ( + 283, + "A6.15 opcodes returning at their documented length (expect 241)", + ), + (284, "A6.15 opcodes returning LATE (expect 0)"), + (285, "A6.15 opcodes that did not return (expect 0)"), + (286, "A6.15 first opcode that was not clean ($FF = none)"), (280, "E2.10 opcodes disagreeing with fullsnes (expect 0)"), (281, "E2.10 opcodes measured (expect 231)"), (282, "E2.10 first disagreeing opcode ($FF = none)"), diff --git a/docs/accuracysnes-coverage.md b/docs/accuracysnes-coverage.md index 8da9f4f0..5e842337 100644 --- a/docs/accuracysnes-coverage.md +++ b/docs/accuracysnes-coverage.md @@ -13,7 +13,7 @@ Every sub-group of Part V is enumerated, so this is a **complete** statement of | `A3` | 10 | 10 | 0 | 0 | — | | `A4` | 10 | 10 | 0 | 0 | — | | `A5` | 15 | 14 | 0 | 0 | A5.20 | -| `A6` | 15 | 13 | 0 | 0 | A6.13, A6.15 | +| `A6` | 15 | 14 | 0 | 0 | A6.13 | | `A7` | 5 | 5 | 0 | 0 | — | | `A8` | 6 | 6 | 0 | 0 | — | | `A9` | 3 | 3 | 0 | 0 | — | @@ -52,7 +52,7 @@ Every sub-group of Part V is enumerated, so this is a **complete** statement of | `F1` | 22 | 13 | 0 | 0 | F1.13, F1.15, F1.16, F1.17, F1.18, F1.19, F1.20, F1.21, F1.22 | | `G1` | 18 | 15 | 0 | 2 | G1.13 | -**303 of 443** enumerated assertion rows covered by an on-cart test, plus **55** covered only by a rendered scene (`docs/adr/0013`) and **2** covered only by a host-side test — **360 of 443** in total. +**304 of 443** enumerated assertion rows covered by an on-cart test, plus **55** covered only by a rendered scene (`docs/adr/0013`) and **2** covered only by a host-side test — **361 of 443** in total. The three columns are kept apart on purpose, in descending order of what the evidence is worth. An on-cart result means the same thing on any emulator and on real hardware; a rendered scene needs a host holding the golden; a **host-side** cover is this project testing its own code, which is the one thing AccuracySNES exists to stop being the only evidence. The host tier is admitted only where the cart *physically cannot* observe the assertion — the stimulus comes from outside the cartridge, or the subject is the loader rather than the machine — and every entry names the test and the reason (`dossier.rs::HOST_COVERED`). Adding the columns into one figure would quietly change what the number claims. diff --git a/docs/accuracysnes-plan.md b/docs/accuracysnes-plan.md index aa39f1e6..56ff1eb9 100644 --- a/docs/accuracysnes-plan.md +++ b/docs/accuracysnes-plan.md @@ -14,7 +14,7 @@ AccuracySNES closed ticket **T-04**. The follow-on tickets minted here are **T-0 | | | |---|---| | Tests | **343** (scoring + golden vectors + region SKIP per image) — *tests, not assertions; see the note below the table* | -| Assertion coverage | **360 of 443** dossier assertions — **303 on-cart** + **55 rendered scenes** + **2 host-side**, kept as separate columns in descending order of what the evidence is worth (`docs/accuracysnes-coverage.md`) | +| Assertion coverage | **361 of 443** dossier assertions — **304 on-cart** + **55 rendered scenes** + **2 host-side**, kept as separate columns in descending order of what the evidence is worth (`docs/accuracysnes-coverage.md`) | | Rendered scenes | **55** declared, all blessed and matching on both scene hosts (`docs/adr/0013`); **55** dossier rows have a scene as their only cover. One uses the `hires-even` extraction (`C5.15`, Mode 5) | | Pass rate | **100.00%** on-cart, floor enforced at 1.00 by `tests/accuracysnes.rs` | | Cross-validated | **Three references** as of `v1.29.0`. Mesen2 agrees on every test but `F1.03`, which clocks both ports out of one latch and so needs the port-2 input its Lua runner cannot drive; snes9x has 14 recorded divergences and **ares** 3, each with a citation in `scripts/accuracysnes/crossval.sh`. A headless **MesenCE** is separately the per-dot compositor's blueprint + exact-frame oracle. All images. | diff --git a/tests/roms/AccuracySNES/ERROR_CODES.md b/tests/roms/AccuracySNES/ERROR_CODES.md index 12aa5eb2..b0ede007 100644 --- a/tests/roms/AccuracySNES/ERROR_CODES.md +++ b/tests/roms/AccuracySNES/ERROR_CODES.md @@ -676,6 +676,15 @@ Provenance: **Contested** (superfamicom.org describes the wrap as happening "the No failure codes — this is a **golden vector**. It cannot fail: it records what it observed and is excluded from the pass rate. Where the observation fits in a byte it goes in the verdict as a variant code (`(variant << 1) | 1`); where it does not — a dot count, say — the verdict is a plain pass and the value goes to the measurement channel at `$7E:E200`, which the host harness reads and prints. See the test's entry in `SOURCE_CATALOG.tsv` for its provenance tier and the reason it records rather than asserts. +### A6.15 — all 256 opcodes defined + +Provenance: **Documented** (WDC W65C816S datasheet, Table 5-4 opcode matrix). Kind: scored. + +| Code | Byte | Meaning | +|---|---|---| +| 1 | `$02` | the sweep did not execute 241 opcodes cleanly — it stopped early, or it covered a different set than the 15 documented control-transfer exclusions. Slots 283-286 hold the three counts and the first opcode that was not clean | +| 2 | `$04` | at least one opcode either failed to return or advanced PC by a different number of bytes than Table 5-4 documents for it. Slot 284 counts the late ones, 285 the ones that never came back, and 286 names the first | + ### A5.S01 — Sweep: CLC Provenance: **Documented** (WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md). Kind: scored. diff --git a/tests/roms/AccuracySNES/SOURCE_CATALOG.tsv b/tests/roms/AccuracySNES/SOURCE_CATALOG.tsv index dd586920..127d681c 100644 --- a/tests/roms/AccuracySNES/SOURCE_CATALOG.tsv +++ b/tests/roms/AccuracySNES/SOURCE_CATALOG.tsv @@ -81,267 +81,268 @@ 78 A6.13 A IRQ handler PBR = $00 Documented WDC datasheet: the vector is 16-bit, so the handler runs in bank 0 Scored 0x7EF06E A6.09 79 A6.14 A RTI pull matches mode Documented WDC datasheet: RTI pulls PBR in native mode only Scored 0x7EF06F A6.10 80 A2.13 A 16-bit dp page cross Contested superfamicom.org describes the wrap as happening "theoretically"; the dossier records the row as UNVERIFIED, so it is observed rather than asserted Golden 0x7EF070 A2.05 -81 C1.01 C OAM word write/read Documented SNESdev Wiki, OAM; fullsnes Scored 0x7EF071 C1.02 -82 C1.02 C OAM odd write latched Documented SNESdev Wiki, OAM; anomie Scored 0x7EF072 C1.02 -83 C1.03 C OAMADDR reload clears Documented SNESdev Wiki, OAM; anomie Scored 0x7EF073 C1.01 -84 C1.04 C OAM rd/wr one counter Documented SNESdev Wiki, OAM Scored 0x7EF074 C1.05 -85 C1.05 C OAM high table mirror Documented SNESdev Wiki, OAM; fullsnes Scored 0x7EF075 C1.04 -86 C1.03b C High table commits bytes Documented SNESdev Wiki, OAM; fullsnes Scored 0x7EF076 C1.03 -87 C1.07 C Blank edge reloads OAM Documented SNESdev Wiki, OAM; fullsnes Scored 0x7EF077 C1.07 -88 C7.09 C Overflow flags clear Documented SNESdev Wiki, sprites; fullsnes Scored 0x7EF078 C7.09 -89 C2.01 C VMAIN step 1 word Documented SNESdev Wiki, PPU registers; fullsnes Scored 0x7EF079 C2.01 -90 C2.02 C VMAIN low-byte trigger Documented SNESdev Wiki, PPU registers; fullsnes Scored 0x7EF07A C2.02 -91 C2.03 C VRAM read prefetch Documented SNESdev Wiki; docs/ppu.md edge case 4 Scored 0x7EF07B C2.08 -92 C2.04 C VRAM bit 15 unconnected Documented SNESdev Wiki, PPU registers; fullsnes Scored 0x7EF07C C2.07 -93 C2.05 C VMAIN step 32 words Documented SNESdev Wiki, PPU registers; fullsnes Scored 0x7EF07D C2.01 -94 C2.06 C VMAIN remap hits bus Documented SNESdev Wiki, PPU registers; fullsnes; anomie Scored 0x7EF07E C2.03-05,C2.06 -95 C2.12 C Blank off closes VRAM Documented SNESdev Wiki, VRAM access; fullsnes Scored 0x7EF07F C2.12 -96 C3.03b C CGRAM read bit 7 is bus Documented SNESdev Wiki, CGRAM; fullsnes Scored 0x7EF080 C3.03 -97 C3.09 C $213F is a field flag Documented SNESdev Wiki, PPU registers; fullsnes Scored 0x7EF081 C3.09 -98 C3.01 C CGRAM two-write commit Documented SNESdev Wiki, PPU registers; fullsnes Scored 0x7EF082 C3.01 -99 C3.02 C CGADD resets flipflop Documented SNESdev Wiki, PPU registers Scored 0x7EF083 C3.02 -100 C3.03 C OPHCT is a 9-bit pair Documented SNESdev Wiki, PPU registers; fullsnes Scored 0x7EF084 C3.06 -101 C3.04 C H counter advances Documented SNESdev Wiki, PPU registers Scored 0x7EF085 - -102 C3.05 C $213F resets flipflop Documented SNESdev Wiki, PPU registers; fullsnes Scored 0x7EF086 C3.08 -103 C3.07 C Counter flipflops differ Documented SNESdev Wiki, PPU registers; fullsnes Scored 0x7EF087 C3.07 -104 C3.12 C CGRAM taken in render Documented fullsnes and the SNESdev Wiki: a CGRAM access during active display uses the colour the PPU is drawing, not the CPU CGADD. Mesen2 models it (writes use InternalCgramAddress when !CanAccessCgram); the batch compositor and snes9x use the programmed CGADD and fail Scored 0x7EF088 C3.04 -105 C13.01 C PPU1 open bus in $213E Documented SNESdev Wiki, PPU registers; fullsnes Scored 0x7EF089 C13.07,C13.10 -106 C13.02 C PPU2 open bus in $213F Documented SNESdev Wiki, PPU registers; fullsnes Scored 0x7EF08A C13.08,C13.10 -107 C13.03 C PPU1/PPU2 bus separate Corroborated the bsnes/ares lineage and Mesen2 model two distinct latches Scored 0x7EF08B C13.09 -108 C14.01 C PPU1 version (golden) Documented SNESdev Wiki, PPU registers; fullsnes Golden 0x7EF08C C14.01 -109 C14.02 C PPU2 version (golden) Documented SNESdev Wiki, PPU registers; fullsnes Golden 0x7EF08D C14.02 -110 C14.03 C PPU1 mstr/slv (golden) Contested the bit reports a board wiring input, so no software-visible value is correct or incorrect; recorded rather than asserted Golden 0x7EF08E C14.03 -111 C11.06 C MPY is 16x8 signed Documented SNESdev Wiki, Mode 7; fullsnes Scored 0x7EF08F C11.06 -112 C11.06b C MPY sign handling Documented SNESdev Wiki, Mode 7; fullsnes Scored 0x7EF090 C11.06 -113 C11.07 C MPY latch is shared Documented fullsnes and the SNESdev Wiki: PPU1's write-twice registers share one byte latch, so $210D/$210E and $211B-$211E interfere with each other Scored 0x7EF091 C11.07 -114 C11.08 C MPY busy during render Contested fullsnes and the SNESdev Wiki agree the Mode 7 multiplier is the renderer's and is busy during active display, but neither states which intermediate it holds at a given moment, and that is the only thing a cart can observe Golden 0x7EF092 C11.08 -115 C7.01 C Range Over at 32 sprites Documented SNESdev Wiki, Sprites; fullsnes Scored 0x7EF093 C7.01 -116 C7.04 C Offscreen X takes a slot Documented fullsnes and the SNESdev Wiki: sprite range evaluation selects on Y alone, so a sprite at X = $100 occupies a range slot despite being entirely off-screen Scored 0x7EF094 C7.04 -117 C7.05 C RangeOver dot = idx*2 Documented fullsnes and the SNESdev Wiki: range evaluation walks OAM two cycles per sprite, so the 33rd in-range sprite trips Range Over at H = OAM.INDEX * 2 on V = OBJ.YLOC Scored 0x7EF095 C7.05 -118 C7.06 C TimeOver by YLOC+1 Documented fullsnes and the SNESdev Wiki: the sprite tile budget is 34 per line and Time Over is raised by the fetch phase, so it reads set by the start of the line the sprites paint on Scored 0x7EF096 C7.06 -119 C7.02 C Time Over is slivers Documented SNESdev Wiki, Sprites; fullsnes; anomie Scored 0x7EF097 C7.02 -120 C7.08 C Flags ignore $212C Documented SNESdev Wiki, Sprites; fullsnes Scored 0x7EF098 C7.08 -121 C7.10 C OAM write to high table Documented fullsnes and the SNESdev Wiki (the Uniracers case): an OAM write during active display is driven to the evaluator's address and lands in the high table, not the CPU OAMADDR. Mesen2 models it (oamAddr = 0x200 | ((oamAddr & 0x1F0) >> 4)); the batch compositor and snes9x write the programmed OAMADDR and fail Scored 0x7EF099 C7.16 -122 C2.11 C VRAM locked in render Documented SNESdev Wiki, PPU registers; fullsnes Scored 0x7EF09A C2.11 -123 C1.08 C OAM addr in render Documented fullsnes and the SNESdev Wiki: during active display the renderer drives the OAM address, so a $2138 read returns a render-time address, not the CPU-programmed one. MesenCE models it (GetOamAddress -> _oamEvaluationIndex<<2); the batch compositor and snes9x return the programmed address and fail Scored 0x7EF09B C1.08 -124 C2.10 C Dropped write still incs Documented SNESdev Wiki, PPU registers; fullsnes; anomie Scored 0x7EF09C C2.10 -125 C1.06 C OAM addr reloads Documented SNESdev Wiki, OAM; anomie Scored 0x7EF09D C1.06 -126 C9.04 C Overscan moves vblank Documented SNESdev Wiki, Timing; fullsnes Scored 0x7EF09E C9.04,B4.02 -127 C9.05 C Mid-frame overscan lock Contested RustySNES and snes9x re-close the VRAM window on a mid-frame overscan enable and Mesen2 does not, so the references split one each way; the dossier's repro cannot break the tie because it read-modify-writes the write-only $2133 Golden 0x7EF09F C9.05 -128 C2.09 C VRAM read latch order Documented SNESdev Wiki, PPU registers: return latch, refill latch, then increment, with VMAIN bit 7 selecting which of $2139/$213A triggers it; bsnes and ares both refill before the step in sfc/ppu/io.cpp, and snes9x and Mesen2 agree Scored 0x7EF0A0 C2.09 -129 C3.10 C $2137 latch is gated Documented superfamicom.org registers: $2137 latches the H/V counter only if $4201 bit 7 is set, and no latching can occur while it is 0; snes9x and Mesen2 both gate it. What the read returns is a separate question, split out into C3.11 Scored 0x7EF0A1 C3.05 -130 C3.11 C $2137 open bus source Contested the sources say only that $2137 reads back as open bus; snes9x and RustySNES present PPU1's latch while Mesen2 presents the CPU's, and nothing available decides between two physically reasonable readings Golden 0x7EF0A2 C3.05 -131 B1.01 B MEMSEL selects FastROM Documented SNESdev Wiki, Memory map / timing; fullsnes Scored 0x7EF0A3 B1.01 -132 B1.02 B JOYSER is 12 clocks Documented SNESdev Wiki, Memory map / timing; fullsnes Scored 0x7EF0A4 B1.02 -133 B2.04 B NTSC frame is 262 lines Documented SNESdev Wiki, Timing; fullsnes Scored 0x7EF0A5 B2.04 -134 B4.03 B RDNMI sets at vblank Documented SNESdev Wiki, Timing; fullsnes Scored 0x7EF0A6 B4.03 -135 B2.01 B No dot above 339 Corroborated fullsnes' PPU H-Counter-Latch Quantities histogram, a direct hardware measurement: dots 323 and 327 latch six times, dot 340 never. bsnes, ares and Mesen2 all implement it; snes9x uses 322/326 and is the outlier Scored 0x7EF0A7 B2.01 -136 B4.16 B H-IRQ position (golden) Contested no source pins the fired dot at single-dot precision; recorded as the before/after guard for T-06-A's clock-domain comparator change Golden 0x7EF0A8 - -137 B4.17 B NMI enable is a level Documented SNESdev Wiki NMITIMEN/RDNMI [ERRATA]; fullsnes $4200/$4210 Scored 0x7EF0A9 B4.06 -138 B4.04 B RDNMI is read-to-clear Documented SNESdev Wiki, Timing; fullsnes Scored 0x7EF0AA B4.04 -139 B4.05 B RDNMI auto-clears Documented SNESdev Wiki, Timing; fullsnes Scored 0x7EF0AB B4.05 -140 B4.08 B V-IRQ fires at VTIME Documented SNESdev Wiki, Timing; fullsnes Scored 0x7EF0AC B4.08 -141 B4.12 B $4211 read releases IRQ Documented SNESdev Wiki, Timing; fullsnes Scored 0x7EF0AD B4.12 -142 B4.15 B CPU revision (golden) Documented SNESdev Wiki, Timing; fullsnes Golden 0x7EF0AE G1.09 -143 B5.01 B 8x8 unsigned multiply Documented SNESdev Wiki, CPU registers; fullsnes Scored 0x7EF0AF B5.01 -144 B5.02 B 16/8 unsigned divide Documented SNESdev Wiki, CPU registers; fullsnes Scored 0x7EF0B0 B5.02 -145 B5.03 B Divide by zero Documented SNESdev Wiki, CPU registers; fullsnes Scored 0x7EF0B1 - -146 B5.04 B Mul/div overlap (golden) Contested SNESdev Errata states overlapping $4203/$4206 operation is undefined Golden 0x7EF0B2 B5.03 -147 B5.05 B Mul/div power-on state Documented anomie regs.txt r1157 and nocash fullsnes, independently; implemented by bsnes/ares/Mesen2. No known hardware test ROM Scored 0x7EF0B3 B5.04 -148 B1.03 B Internal cycles are 6 Documented SNES Development Manual Bk I 21.1; SNESdev Wiki; fullsnes Scored 0x7EF0B4 B1.03 -149 B1.04 B DMA speed is uniform Documented SNES Development Manual Bk I 21.1 (DMA at 2.68MHz regardless of address) Scored 0x7EF0B5 B1.04 -150 B2.06 B Interlace line count Contested the dossier conditions the extra line on $213F.7 (the field), which this test does not control — sampling V across an uncontrolled field cannot assert a line count Golden 0x7EF0B6 B2.06 -151 B2.05 B PAL frame is 312 lines Documented SNESdev Wiki, Timing; fullsnes Scored 0x7EF0B7 B2.05 -152 B4.14 B IRQ dispatch latency Documented SNESdev Wiki, Timing; fullsnes — the sub-cycle poll point is not CPU-observable, so its consequence is measured instead Golden 0x7EF0B8 B4.14 -153 B2.10 B Region bit (golden) Contested SNESdev PPU registers places the 50/60Hz bit at bit 3, which overlaps the PPU2 version field; fullsnes places it at bit 4 Golden 0x7EF0B9 B2.10 -154 B4.07 B H-IRQ position (golden) Contested the $4211 poll loop is coarser than the dot the comparator fires on, so the exact H position is not resolvable from software by polling Golden 0x7EF0BA B4.07 -155 B4.09 B HV-IRQ needs both Documented SNESdev Wiki, Timing; fullsnes Scored 0x7EF0BB B4.09 -156 B3.01 B DRAM refresh pause Contested fullsnes and anomie put the pause at 40 clocks near line-clock 536, and RustySNES now models it there (docs/dram-refresh.md), so this loop records variant 2 (one interval ~10 dots longer); ares' own source still calls its refresh *pattern* technically wrong and only right on average, so the exact per-line position stays host-cross-validated rather than self-asserted Golden 0x7EF0BC B3.01,B3.02,B3.03 -157 B4.13 B Timer range is 9-bit Documented fullsnes $4207-$420A: HTIME is 0-339 and VTIME 0-261 (NTSC) / 0-311 (PAL), both held in nine bits Scored 0x7EF0BD B4.13 -158 B4.11 B Dot 153, last line Contested superfamicom.org's timing page states the exception and gives no mechanism; its timing text derives from fullsnes, so the two are one source, and no test ROM verifies it. ares, bsnes, Mesen2 and snes9x were each searched and none implements it Golden 0x7EF0BE B4.11 -159 D1.01 D DMA mode 0 Documented SNESdev Wiki, DMA; fullsnes Scored 0x7EF0BF D1.01 -160 D1.01b D DMA mode 1 Documented SNESdev Wiki, DMA; fullsnes Scored 0x7EF0C0 D1.01 -161 D1.06 D DMA count hits zero Documented SNESdev Wiki, DMA registers; fullsnes Scored 0x7EF0C1 D1.06 -162 D1.07 D DMA fixed A-bus Documented SNESdev Wiki, DMA registers; fullsnes Scored 0x7EF0C2 D1.07 -163 D1.07b D DMA decrementing A-bus Documented SNESdev Wiki, DMA registers; fullsnes Scored 0x7EF0C3 D1.07 -164 D1.10 D DMA $43xB scratch latch Corroborated ares and bsnes both model the latch and serialize it Scored 0x7EF0C4 D1.10 -165 D1.02 D DMA 8 clocks/byte Documented SNESdev Wiki, DMA timing; fullsnes Scored 0x7EF0C5 D1.02 -166 D1.05 D DMA count 0 = 65536 Documented SNESdev Wiki, DMA registers; fullsnes Scored 0x7EF0C6 D1.05 -167 D1.09 D WRAM->$2180 no-write Documented fullsnes: "does not cause a write to occur" Scored 0x7EF0C7 D1.09,D1.15 -168 D2.03 D HDMA line-count byte Documented SNESdev Wiki, HDMA; fullsnes Scored 0x7EF0C8 D2.03 -169 D2.04 D HDMA repeat flag Documented SNESdev Wiki, HDMA; fullsnes Scored 0x7EF0C9 D2.04 -170 D2.07 D HDMA preempts GP-DMA Documented SNESdev Wiki, HDMA; anomie's timing doc; fullsnes Scored 0x7EF0CA D2.07 -171 D2.09 D HDMA armed mid-frame Contested fullsnes and the SNESdev Wiki record that enabling HDMA outside vblank produces erroneous writes from uninitialised A2An/NLTRn, but what those writes contain is a function of the previous frame's leftover state and is specified nowhere Golden 0x7EF0CB D2.09 -172 D1.14 D $2180 B->A does write Documented fullsnes: $2180->WRAM writes, but the value written is invalid Scored 0x7EF0CC D1.14 -173 D1.13 D DMA count hits zero Documented fullsnes and ares: the DAS $43x5/6 byte-count register decrements as GP-DMA transfers and reads $0000 when the transfer completes Scored 0x7EF0CD D1.13 -174 D1.11 D DMA power-on state Corroborated fullsnes register table and the SNESdev DMA-registers page agree independently; ares and bsnes default every channel field to match Scored 0x7EF0CE D1.11 -175 D1.08 D Invalid A-bus (golden) Contested the errata names the ranges invalid but does not specify what is read instead; the substitute is open bus, whose content is core-specific and time-dependent Golden 0x7EF0CF D1.08 -176 D1.03 D DMA startup overhead Documented SNESdev Wiki, DMA timing; fullsnes Golden 0x7EF0D0 D1.03 -177 D1.04 D DMA channel priority Documented SNESdev Wiki, DMA; fullsnes Scored 0x7EF0D1 D1.04 -178 D2.05 D HDMA indirect mode Documented SNESdev Wiki, HDMA; fullsnes Scored 0x7EF0D2 D2.05 -179 D2.06 D HDMA $4308/$430A state Documented SNESdev Wiki, HDMA registers; fullsnes Scored 0x7EF0D3 D2.06 -180 E4.11 E ARAM power-on pattern Contested the dossier records a repeating 32x$00 / 32x$FF fill and marks it chip-dependent and informational; RustySNES, snes9x and Mesen2 all boot APU RAM uniformly zero instead Golden 0x7EF0D4 E4.11 -181 E1.01 E MUL YA flags from Y Documented SNESdev Wiki, SPC700 reference; fullsnes — flagged as errata Scored 0x7EF0D5 E1.01 -182 E1.02 E DIV YA,X normal branch Documented SNESdev Wiki, SPC700 reference; fullsnes Scored 0x7EF0D6 E1.02 -183 E1.07 E DIV valid to Q<=511 Documented SNESdev Wiki SPC700 reference and fullsnes, both flagging DIV as valid only for quotients up to 511; the values past it follow E1.03's overflow formula Scored 0x7EF0D7 E1.07 -184 E1.04 E DIV H = nibble compare Documented SNESdev Wiki, SPC700 reference; fullsnes — flagged as errata Scored 0x7EF0D8 E1.04 -185 E1.05 E DIV V is quotient bit 8 Documented SNESdev Wiki, SPC700 reference; fullsnes Scored 0x7EF0D9 E1.05 -186 E1.06 E DIV flags from quotient Documented SNESdev Wiki, SPC700 reference; fullsnes — flagged as errata Scored 0x7EF0DA E1.06 -187 E1.13 E ADDW H = bit-11 carry Documented SNESdev Wiki, SPC700 reference; fullsnes Scored 0x7EF0DB E1.13 -188 E1.15 E MOVW YA sets 16-bit N/Z Documented SNESdev Wiki, SPC700 reference; fullsnes Scored 0x7EF0DC E1.15 -189 E3.01 E Timer read clears it Documented SNESdev Wiki, SPC700 I/O; fullsnes Scored 0x7EF0DD E3.01 -190 E3.02 E Timer enable 0->1 resets Documented SNESdev Wiki SPC700 timers and fullsnes: a 0->1 on a $F1 timer-enable bit resets that timer's stage-2 divider and stage-3 output counter Scored 0x7EF0DE E3.02 -191 E3.11 E $F2 bit 7 blocks writes Documented SNESdev Wiki, S-DSP; fullsnes Scored 0x7EF0DF E3.11 -192 E3.11b E DSP register addressing Documented SNESdev Wiki, S-DSP registers; fullsnes Scored 0x7EF0E0 - -193 E2.01 E Store dummy-reads target Documented SNESdev Wiki, SPC700; fullsnes — flagged as errata Scored 0x7EF0E1 E2.01 -194 E2.05 E DP index wraps in page Documented SNESdev Wiki, SPC700 addressing; fullsnes Scored 0x7EF0E2 E2.05 -195 E6.09 E Gaussian sum wraps Documented fullsnes and anomie's DSP doc: of the four gaussian additions the first cannot overflow, the second wraps in 16 bits and the third saturates Scored 0x7EF0E3 E6.09 -196 E6.11 E BRR waveform vectors Contested the dossier names four nibble patterns and asks what a decoder makes of each without stating an expected value for any of them; the row's content is the measurement Golden 0x7EF0E4 E6.11 -197 E10.01 E 32 SPC cycles per sample Documented fullsnes and anomie: the SPC700's 1.024 MHz clock and the DSP's 32 kHz output rate fix the ratio at 32 CPU cycles per output sample Scored 0x7EF0E5 E10.01 -198 E10.05 E Soft reset acts as $E0 Contested both sources agree FLG bit 7 makes the DSP behave as $E0 and force every voice into release, and contradict each other on what ENDX then reads: nocash says $FF, anomie says 0. The dossier marks the row [CONFLICT] and asks for a golden vector Golden 0x7EF0E6 E10.05 -199 E1.14 E XCN costs five cycles Documented the SNESdev Wiki SPC700 reference and fullsnes both give XCN as 5 cycles, against 2 for NOP Scored 0x7EF0E7 E1.14 -200 E2.04 E DBNZ dp is an RMW Documented SNESdev Wiki SPC700 reference and fullsnes: DBNZ dp,rel reads its operand, decrements it and writes it back, and $FD-$FF are read-to-clear Scored 0x7EF0E8 E2.04 -201 E3.14 E $F8/$F9 are plain RAM Documented SNESdev Wiki, SPC700 I/O; fullsnes Scored 0x7EF0E9 E3.14 -202 E3.11c E DSP global registers Documented SNESdev Wiki, S-DSP registers; fullsnes Scored 0x7EF0EA - -203 E9.19 E ENDX write clears it Documented SNESdev Wiki, S-DSP registers; fullsnes Scored 0x7EF0EB E9.19 -204 E9.03 E Pitch not noise rate Documented fullsnes and anomie's DSP doc: the noise generator's step rate is set by FLG bits 0-4, and a voice's pitch register does not participate Scored 0x7EF0EC E9.03 -205 E9.01 E Noise LFSR seed Documented fullsnes and anomie's DSP doc: the noise shift register resets to $4000, with taps bit0 XOR bit1 feeding bit 14 Scored 0x7EF0ED E9.01 -206 E5.07 E End+mute zeroes env Documented fullsnes, S-DSP BRR; anomie's DSP doc Scored 0x7EF0EE E5.07 -207 E5.08 E Loop flag without end Documented fullsnes, S-DSP BRR; anomie's DSP doc Scored 0x7EF0EF E5.08 -208 E5.09 E ENDX sets on end block Documented fullsnes, S-DSP BRR; anomie's DSP doc Scored 0x7EF0F0 E5.09 -209 E5.01 E BRR header layout Documented fullsnes and anomie's DSP doc: the BRR header is ssssffle, shift in bits 7-4, and a nibble decodes as (nibble << shift) >> 1 Scored 0x7EF0F1 E5.01 -210 E5.10 E Released voice decodes Documented fullsnes and anomie's DSP doc: key-off begins the release ramp and does not halt BRR decoding, which continues to follow loop points and set ENDX Scored 0x7EF0F2 E5.10 -211 E5.11 E Directory entry address Documented fullsnes, S-DSP BRR; anomie's DSP doc Scored 0x7EF0F3 E5.11 -212 E7.10 E Direct GAIN is envelope Documented SNESdev Wiki, S-DSP envelopes; fullsnes Scored 0x7EF0F4 E7.10 -213 E1.03 E DIV overflow branch Documented SNESdev Wiki, SPC700 reference; fullsnes — flagged as errata Scored 0x7EF0F5 E1.03 -214 E1.08 E DAA adjustments Documented SNESdev Wiki, SPC700 reference; fullsnes Scored 0x7EF0F6 E1.08 -215 E2.08 E TCALL vector table Documented SNESdev Wiki, SPC700 reference; fullsnes Scored 0x7EF0F7 E2.08 -216 E2.09 E BRK shares TCALL 0 Documented SNESdev Wiki, SPC700 reference; fullsnes Scored 0x7EF0F8 E2.09 -217 E3.03 E $F1 bit 5 clears port 3 Documented SNESdev Wiki, SPC700 I/O; fullsnes Scored 0x7EF0F9 E3.03 -218 E3.04 E Writes pass under IPL Documented SNESdev Wiki, SPC700 I/O; fullsnes Scored 0x7EF0FA E3.04 -219 E3.05 E TnDIV $00 means 256 Documented SNESdev Wiki, SPC700 timers; fullsnes Scored 0x7EF0FB E3.05 -220 E3.10 E TEST gates RAM writes Documented SNESdev Wiki, SPC700 I/O; fullsnes Scored 0x7EF0FC E3.10 -221 E1.09 E DAS mirrors DAA Documented SNESdev Wiki, SPC700 reference; fullsnes Scored 0x7EF0FD E1.09 -222 E1.10 E TSET1 is a compare Documented SNESdev Wiki, SPC700 reference; fullsnes — flagged as errata Scored 0x7EF0FE E1.10 -223 E1.12 E CLRV clears H too Documented SNESdev Wiki, SPC700 reference; fullsnes — flagged as errata Scored 0x7EF0FF E1.12 -224 E2.02 E MOV dp,dp is exempt Documented SNESdev Wiki, SPC700; fullsnes — flagged as errata Scored 0x7EF100 E2.02 -225 E2.03 E MOVW reads the low byte Documented SNESdev Wiki, SPC700; fullsnes — flagged as errata Scored 0x7EF101 E2.03 -226 E2.06 E PSW.P selects the page Documented SNESdev Wiki, SPC700 addressing; fullsnes Scored 0x7EF102 E2.06 -227 E2.07 E CALL pushes exact addr Documented SNESdev Wiki, SPC700 reference; fullsnes Scored 0x7EF103 E2.07 -228 E4.01 E IPL ROM contents Documented the canonical 64-byte IPL listing; fullsnes, SNESdev Wiki Scored 0x7EF104 E4.01 -229 E4.02 E IPL handoff state Documented fullsnes, SNESdev Wiki, APU boot handshake Scored 0x7EF105 E4.02 -230 E4.03 E IPL zerofills $00-$EF Documented the canonical 64-byte IPL listing zero-fills $0000-$00EF as the first thing it does at $FFC0, before entering its transfer loop; fullsnes and the SNESdev Wiki both carry it Scored 0x7EF106 E4.03 -231 E4.04 E IPL ready announcement Documented fullsnes, SNESdev Wiki, APU boot handshake Scored 0x7EF107 E4.04 -232 E4.06 E IPL multi-block continue Documented fullsnes, SNESdev Wiki; canonical IPL boot ROM $FFEF-$FFFB Scored 0x7EF108 E4.06 -233 E4.08 E IPL DSP-poke via $00F2 Documented fullsnes (APU / S-DSP); canonical IPL boot ROM store loop Scored 0x7EF109 E4.08 -234 E5.12 E SRCN change source Documented fullsnes and anomie's DSP doc: a mid-playback SRCN change takes the new entry's start address, or its loop address if the voice has already passed a loop point Scored 0x7EF10A E5.12 -235 E5.02 E BRR nibbles are signed Documented fullsnes, S-DSP BRR; anomie's DSP doc Scored 0x7EF10B E5.02 -236 E7.16 E OUTX is pre-volume Documented fullsnes, S-DSP envelopes; anomie's DSP doc Scored 0x7EF10C E7.16 -237 E8.04 E KOFF outranks KON Documented fullsnes, S-DSP key on/off; anomie's DSP doc Scored 0x7EF10D E8.04 -238 E8.05 E KON is edge-triggered Documented fullsnes and anomie's DSP doc: KON is write-triggered and self-clears ~63 clocks later; only KOFF and FLG bit 7 are level-sensitive Scored 0x7EF10E E8.05 -239 E9.04 E Noise voices decode BRR Documented fullsnes, S-DSP noise; anomie's DSP doc — flagged as errata Scored 0x7EF10F E9.04 -240 E9.06 E EDL 0 is a 4-byte buffer Documented fullsnes, S-DSP echo — flagged as errata; anomie's DSP doc Scored 0x7EF110 E9.06 -241 E9.12 E Echo writes are masked Documented fullsnes, S-DSP echo; anomie's DSP doc Scored 0x7EF111 E9.12 -242 E9.15 E Voice mix saturates Documented fullsnes and anomie's DSP doc: the per-voice mix clamps to 16 bits after each addition rather than accumulating and clamping once Scored 0x7EF112 E9.15 -243 E9.05 E Echo entry is 4 bytes Documented fullsnes and anomie's DSP doc: the echo buffer holds four bytes per sample, a 16-bit left sample followed by a 16-bit right one Scored 0x7EF113 E9.05 -244 E9.09 E Echo wraps into page 0 Documented fullsnes and anomie's DSP doc [ERRATA]: the echo buffer address is 16-bit and wraps, so a buffer that runs past $FFFF continues at $0000 over page zero and the IPL shadow Scored 0x7EF114 E9.09 -245 E9.13 E L/R FIR independent Documented fullsnes and anomie's DSP doc: the echo FIR keeps a separate eight-sample history and accumulator per channel; only the coefficients are shared Scored 0x7EF115 E9.13 -246 E9.10 E FLG.5 stops echo writes Documented fullsnes, S-DSP echo; anomie's DSP doc Scored 0x7EF116 E9.10 -247 E9.17 E Mute is after OUTX Documented fullsnes, S-DSP FLG; anomie's DSP doc Scored 0x7EF117 E9.17 -248 E9.18 E FLG reset kills voices Documented SNESdev Wiki, S-DSP; fullsnes Scored 0x7EF118 E9.18 -249 E5.03 E BRR sample arithmetic Documented fullsnes, S-DSP BRR; anomie's DSP doc Scored 0x7EF119 E5.03 -250 E5.04 E Invalid shift collapses Documented fullsnes, S-DSP BRR; anomie's DSP doc Scored 0x7EF11A E5.04 -251 E5.05 E BRR filter 1 Documented fullsnes, S-DSP BRR filters; anomie's DSP doc Scored 0x7EF11B E5.05 -252 E5.06 E BRR wraps at 15 bits Documented fullsnes and anomie's DSP doc: the decoded sample is clamped to 16 bits and then stored as 15, so +4000h..+7FFFh becomes -4000h..-1 and the sign is lost Scored 0x7EF11C E5.06 -253 E5.13 E Released voices decode Documented fullsnes and anomie's DSP doc: the BRR decoder advances on the pitch clock regardless of the envelope; voices never actually stop decoding Scored 0x7EF11D E5.13 -254 E7.01 E Rate 0 never fires Documented SNESdev Wiki, S-DSP envelopes; fullsnes; anomie's DSP doc Scored 0x7EF11E E7.01 -255 E7.13 E GAIN bent-increase Documented fullsnes and anomie's DSP doc: GAIN mode 7 increases +32 per sample below $600 and +8 above, comparing the internal envelope latch unsigned Scored 0x7EF11F E7.13 -256 E7.17 E Lin-decrease clamps 0 Documented fullsnes and anomie's DSP doc: GAIN linear-decrease subtracts $20 per tick and clamps the envelope to zero on underflow, comparing the internal envelope unsigned Scored 0x7EF120 E7.17 -257 E7.18 E ENVX is E>>4 Documented fullsnes and anomie's DSP doc: VxENVX = envelope >> 4, a seven-bit value with bit 7 always clear Scored 0x7EF121 E7.18 -258 E7.04 E Attack $F is instant Documented fullsnes and anomie's DSP doc: attack rate $F fires every sample with a step of +1024, rather than the +32 on a counter tick every other rate uses Scored 0x7EF122 E7.04 -259 E7.09 E Release rate is fixed Documented fullsnes and anomie's DSP doc: release steps -8 per sample regardless of ADSR, which is why a custom release has to be built from GAIN Scored 0x7EF123 E7.09 -260 E7.05 E Decay index d*2+16 Documented fullsnes and anomie's DSP doc: the decay phase indexes the counter table at d*2+16, stepping E -= 1 then E -= E>>8 Scored 0x7EF124 E7.05 -261 E7.06 E Sustain index verbatim Documented fullsnes and anomie's DSP doc: the sustain phase indexes the counter table with the ADSR2 rate field directly, unlike decay's d*2+16 Scored 0x7EF125 E7.06 -262 E7.07 E Sustain boundary Documented fullsnes and anomie's DSP doc: the decay phase ends when (E >> 8) equals the ADSR2 sustain level, giving a boundary of $100 per level Scored 0x7EF126 E7.07 -263 E7.03 E Attack index a*2+1 Documented fullsnes and anomie's DSP doc: the attack phase indexes the counter table at a*2+1 and steps the envelope by +32 Scored 0x7EF127 E7.03 -264 E7.12 E GAIN sustain boundary Contested the dossier records the GAIN-sourced boundary as [ERRATA]; RustySNES takes it from ADSR2 instead, which E7.07 asserts, so which behaviour is right is not settled here Golden 0x7EF128 E7.12 -265 E7.08 E Key-off releases to zero Documented SNESdev Wiki, S-DSP envelopes; fullsnes; anomie's DSP doc Scored 0x7EF129 E7.08 -266 E8.01 E KON examined at 16 kHz Documented fullsnes and anomie's DSP doc [ERRATA]: the key-on/key-off registers are examined once every two output samples, giving a 16 kHz effective poll rate Scored 0x7EF12A E8.01 -267 E8.02 E Key-on takes 5 samples Documented fullsnes and anomie's DSP doc: KON is held for five output samples while the directory and the first BRR block are fetched, before the envelope starts Scored 0x7EF12B E8.02 -268 E8.03 E KON restarts a voice Documented fullsnes and anomie's DSP doc: KON re-enters the key-on sequence unconditionally, resetting the BRR pointer and zeroing the envelope, whether or not the voice was already playing Scored 0x7EF12C E8.03 -269 E8.07 E KOFF pulse collapses Contested KON/KOFF are sampled every second output sample, so whether a short pulse is seen depends on where the poll falls inside it -- which makes the outcome phase-dependent rather than fixed, the same hedge E8.05 and E8.06 carry as "usually" Golden 0x7EF12D E8.07 -270 E8.10 E KOFF+KON cuts faster Documented fullsnes and anomie's DSP doc: KON zeroes the envelope before its attack, and KOFF outranks KON, so the pair silences immediately where KOFF alone ramps Scored 0x7EF12E E8.10 -271 E7.11 E GAIN linear increase Documented SNESdev Wiki, S-DSP envelopes; fullsnes; anomie's DSP doc Scored 0x7EF12F E7.11 -272 E7.14 E GAIN decrease clamps Documented SNESdev Wiki, S-DSP envelopes; fullsnes; anomie's DSP doc Scored 0x7EF130 E7.14 -273 E7.15 E ENVX is E >> 4 Documented SNESdev Wiki, S-DSP envelopes; fullsnes; anomie's DSP doc Scored 0x7EF131 E7.15 -274 E6.02 E Pitch $1000 is 1:1 Documented fullsnes, S-DSP pitch; anomie's DSP doc Scored 0x7EF132 E6.02 -275 E6.02b E Pitch $1000 does finish Documented fullsnes, S-DSP pitch; anomie's DSP doc Scored 0x7EF133 E6.02 -276 E6.02c E Pitch $2000 is +1 octave Documented fullsnes, S-DSP pitch; anomie's DSP doc Scored 0x7EF134 E6.02 -277 E6.02d E Pitch $2000 upper bound Documented fullsnes, S-DSP pitch; anomie's DSP doc Scored 0x7EF135 E6.02 -278 E3.06 E T2 is eight times T0 Documented SNESdev Wiki, SPC700 timers; fullsnes Scored 0x7EF136 E3.06 -279 E3.08 E TEST bit 0 halts timers Documented fullsnes, SPC700 TEST register; ares and bsnes smp/timing Scored 0x7EF137 E3.08 -280 E3.09 E Waits: CPU 10, timer 8 Documented ares and bsnes sfc/smp/timing.cpp, identically: cycleWaitStates {2,4,10,20} against timerWaitStates {2,4,8,16}, with the comment that the timers are not affected by the 8/16 divider glitch Scored 0x7EF138 E3.09 -281 B2.07 B Frame rate vs APU clock Documented fullsnes and the SNESdev Wiki: NTSC 60.0988 Hz, PAL 50.00698 Hz; the APU's 24.576 MHz crystal is region-independent (ares apuFrequency, snes9x's two APU ratios) Scored 0x7EF139 B2.07,B2.08 -282 E3.13 E Regs shadow into RAM Documented fullsnes and the SNESdev Wiki: writes to $00F0-$00FF reach the underlying APU RAM as well as the register, which the S-DSP can then read as sample data Scored 0x7EF13A E3.13 -283 E2.10i E SPC cycle instrument Documented the apparatus for E2.10's 256-opcode sweep, validated against NOP = 2, XCN = 5 (also E1.14) and MUL YA = 9 -- each independently derivable from spc700_exec.rs's read/idle sequence Golden 0x7EF13B - -284 E2.10 E 256-opcode cycle sweep Documented fullsnes, SNES APU SPC700 CPU instruction set Scored 0x7EF13C E2.10 -285 E9.02 E Noise output is bipolar Documented fullsnes and anomie's DSP doc [ERRATA]: the noise output is highpass-filtered as a consequence of the 15-bit shift register being interpreted as the top bits of a signed 16-bit sample Scored 0x7EF13D E9.02 -286 F1.01 F Manual pad read order Documented fullsnes and the SNESdev Wiki controller protocol: the shift register presents B, Y, Select, Start, Up, Down, Left, Right, A, X, L, R and then four zero bits Scored 0x7EF13E F1.01 -287 F1.02 F Pad reads 17+ are 1 Documented SNESdev Wiki, controller protocol; fullsnes Scored 0x7EF13F F1.02 -288 F1.03 F Shared $4016 latch Documented fullsnes and the SNESdev Wiki controller protocol: bit 0 of $4016 is the shared latch line that parallel-loads both controller ports' shift registers Scored 0x7EF140 F1.03 -289 F1.04 F $4016 bits 7-2 open bus Corroborated RustySNES, snes9x and Mesen2 all return $41 for the absolute read and $01 for the long one -- identical bytes, so bits 7-2 follow the CPU bus in all three Scored 0x7EF141 F1.04 -290 F1.07 F Auto-read needs $4200.0 Documented fullsnes and the SNESdev Wiki: bit 0 of $4200 arms the automatic joypad read, and with it clear $4218-$421F are not written Scored 0x7EF142 F1.07 -291 F1.05 F Pad signature is 0000 Documented fullsnes and the SNESdev Wiki: bits 3-0 of the auto-read result identify the device, and a standard controller reports 0000 Scored 0x7EF143 F1.05 -292 F1.06 F First bit clocked is B Documented fullsnes and the SNESdev Wiki: the auto-read result holds the sixteen shifted bits in clock order, most significant first, so B is bit 15 Scored 0x7EF144 F1.06 -293 F1.08 F Auto-read start dot Documented fullsnes: the automatic joypad read begins ~dot 32.5-95.5 of the first vblank line (RustySNES delays it 256 master clocks ~ dot 64), not at the vblank edge Golden 0x7EF145 F1.08 -294 F1.09 F Auto-read duration Documented fullsnes: the automatic joypad read is busy for exactly 4224 master cycles (~3.097 scanlines); the poll count is a CPU-cycle-proportional proxy for it Golden 0x7EF146 F1.09 -295 F1.10 F Auto-read start race Documented fullsnes: the automatic joypad read begins ~dot 32.5-95.5 of the first vblank line, not at the vblank edge, so $4212 bit 0 reads not-busy for that window and a $4212 poll at NMI entry sees the read not yet started Scored 0x7EF147 F1.10 -296 F1.11 F Latch corrupts auto-read Documented fullsnes and the SNESdev Wiki: while $4016 bit 0 is high the shift registers reload continuously rather than shifting, so an automatic read taken across it returns the same bit in every position Scored 0x7EF148 F1.11 -297 F1.12 F Auto-read result timing Contested F1.12 says results are valid by V = $E3, which does not reconcile with F1.09's 4224-cycle duration and F1.08's start window; no source says which to believe, and the cores split on whether the result appears at once or progressively Golden 0x7EF149 F1.12 -298 F1.14 F $4213 reads $4201 back Documented fullsnes: RDIO reads the WRIO output pins, which are open-collector, so with nothing driving them low the value read is the value written Scored 0x7EF14A F1.14 -299 G1.02 G Reset: $4210/$4211 clear Documented SNESdev Wiki, power-on state; fullsnes Scored 0x7EF14B G1.02 -300 G1.04 G Reset: emulation mode Documented SNESdev Wiki, power-on state; WDC 65C816 datasheet, XCE Scored 0x7EF14C G1.04 -301 G1.05 G Power-on PPU registers Documented the dossier marks the PPU power-on state indeterminate ('no boot ROM; most PPU registers start unknown') and says to report it, never assert; the readable registers ($2134-$2136, $213E/$213F) are reported and the only scored check is the self-guard that the power-on capture actually ran Scored 0x7EF14D G1.05 -302 G1.08 G Write-only read: openbus Documented SNESdev Wiki, open bus; fullsnes, memory map notes Scored 0x7EF14E G1.08 -303 G1.10 G Checksum XOR complement Documented SNESdev Wiki, cartridge header; fullsnes Scored 0x7EF14F G1.10 -304 G1.11 G Checksum over the image Documented SNESdev Wiki, cartridge header checksum; fullsnes Scored 0x7EF150 G1.11 -305 G1.12 G LoROM header location Documented SNESdev Wiki, cartridge header; fullsnes Scored 0x7EF151 G1.12 -306 G1.14 G LoROM bank decode Documented SNESdev Wiki, memory map; fullsnes Scored 0x7EF152 G1.14 -307 G1.19 G Power-on $4201/timers Documented fullsnes and the SNESdev Wiki power-on table: $4201 = $FF, HTIME and VTIME = $1FF; the other registers in the row are observed by B5.05 and B1.01 Scored 0x7EF153 G1.01 -308 G1.20 G Power-on indeterminate Contested the dossier marks the whole row [UNDEFINED] and says to report it and never assert it; half the registers it names are write-only and cannot be reported at all Golden 0x7EF154 G1.03 -309 G1.07 G WRAM power-on fill Contested the dossier marks the row [UNDEFINED] and asks for a golden vector by name: no canonical WRAM fill exists and real consoles disagree Golden 0x7EF155 G1.07 -310 A5.S01 A Sweep: CLC Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF156 A5.01-08 -311 A5.S02 A Sweep: SEC Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF157 A5.01-08 -312 A5.S03 A Sweep: CLV Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF158 A5.01-08 -313 A5.S04 A Sweep: INX Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF159 A5.01-08 -314 A5.S05 A Sweep: DEX Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF15A A5.01-08 -315 A5.S06 A Sweep: TAX Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF15B A5.01-08 -316 A5.S07 A Sweep: TXY Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF15C A5.01-08 -317 A5.S08 A Sweep: ASL A Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF15D A5.01-08 -318 A5.S09 A Sweep: XBA Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF15E A5.01-08 -319 A5.S10 A Sweep: TCD Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF15F A5.01-08 -320 A5.S11 A Sweep: LDA #imm Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF160 A5.01-08 -321 A5.S12 A Sweep: LDX #imm Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF161 A5.01-08 -322 A5.S13 A Sweep: CMP #imm Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF162 A5.01-08 -323 A5.S14 A Sweep: BIT #imm Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF163 A5.01-08 -324 A5.S15 A Sweep: REP #imm Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF164 A5.01-08 -325 A5.S16 A Sweep: SEP #imm Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF165 A5.01-08 -326 A5.S17 A Sweep: WDM Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF166 A5.01-08 -327 A5.S18 A Sweep: PHA+PLA Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF167 A5.01-08 -328 A5.S19 A Sweep: PHP+PLP Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF168 A5.01-08 -329 A5.S20 A Sweep: PHB+PLB Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF169 A5.01-08 -330 A5.S21 A Sweep: PHD+PLD Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF16A A5.01-08 -331 A5.S22 A Sweep: PHX+PLX Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF16B A5.01-08 -332 A5.S23 A Sweep: LDA dp Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF16C A5.01-08 -333 A5.S24 A Sweep: LDA abs Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF16D A5.01-08 -334 A5.S25 A Sweep: LDA long Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF16E A5.01-08 -335 A5.S26 A Sweep: STA dp Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF16F A5.01-08 -336 A5.S27 A Sweep: STA abs Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF170 A5.01-08 -337 A5.S28 A Sweep: LDA dp,X Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF171 A5.01-08 -338 A5.S29 A Sweep: LDA abs,X Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF172 A5.01-08 -339 A5.S30 A Sweep: INC dp Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF173 A5.01-08 -340 A5.S31 A Sweep: INC abs Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF174 A5.01-08 -341 A5.S32 A Sweep: ADC dp Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF175 A5.01-08 -342 A5.S33 A Sweep: CMP abs Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF176 A5.01-08 -343 A5.S34 A Sweep: BVS untaken Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF177 A5.01-08 -344 A5.S35 A Sweep: BRL flat 4 Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF178 A5.16 +81 A6.15 A all 256 opcodes defined Documented WDC W65C816S datasheet, Table 5-4 opcode matrix Scored 0x7EF071 A6.15 +82 C1.01 C OAM word write/read Documented SNESdev Wiki, OAM; fullsnes Scored 0x7EF072 C1.02 +83 C1.02 C OAM odd write latched Documented SNESdev Wiki, OAM; anomie Scored 0x7EF073 C1.02 +84 C1.03 C OAMADDR reload clears Documented SNESdev Wiki, OAM; anomie Scored 0x7EF074 C1.01 +85 C1.04 C OAM rd/wr one counter Documented SNESdev Wiki, OAM Scored 0x7EF075 C1.05 +86 C1.05 C OAM high table mirror Documented SNESdev Wiki, OAM; fullsnes Scored 0x7EF076 C1.04 +87 C1.03b C High table commits bytes Documented SNESdev Wiki, OAM; fullsnes Scored 0x7EF077 C1.03 +88 C1.07 C Blank edge reloads OAM Documented SNESdev Wiki, OAM; fullsnes Scored 0x7EF078 C1.07 +89 C7.09 C Overflow flags clear Documented SNESdev Wiki, sprites; fullsnes Scored 0x7EF079 C7.09 +90 C2.01 C VMAIN step 1 word Documented SNESdev Wiki, PPU registers; fullsnes Scored 0x7EF07A C2.01 +91 C2.02 C VMAIN low-byte trigger Documented SNESdev Wiki, PPU registers; fullsnes Scored 0x7EF07B C2.02 +92 C2.03 C VRAM read prefetch Documented SNESdev Wiki; docs/ppu.md edge case 4 Scored 0x7EF07C C2.08 +93 C2.04 C VRAM bit 15 unconnected Documented SNESdev Wiki, PPU registers; fullsnes Scored 0x7EF07D C2.07 +94 C2.05 C VMAIN step 32 words Documented SNESdev Wiki, PPU registers; fullsnes Scored 0x7EF07E C2.01 +95 C2.06 C VMAIN remap hits bus Documented SNESdev Wiki, PPU registers; fullsnes; anomie Scored 0x7EF07F C2.03-05,C2.06 +96 C2.12 C Blank off closes VRAM Documented SNESdev Wiki, VRAM access; fullsnes Scored 0x7EF080 C2.12 +97 C3.03b C CGRAM read bit 7 is bus Documented SNESdev Wiki, CGRAM; fullsnes Scored 0x7EF081 C3.03 +98 C3.09 C $213F is a field flag Documented SNESdev Wiki, PPU registers; fullsnes Scored 0x7EF082 C3.09 +99 C3.01 C CGRAM two-write commit Documented SNESdev Wiki, PPU registers; fullsnes Scored 0x7EF083 C3.01 +100 C3.02 C CGADD resets flipflop Documented SNESdev Wiki, PPU registers Scored 0x7EF084 C3.02 +101 C3.03 C OPHCT is a 9-bit pair Documented SNESdev Wiki, PPU registers; fullsnes Scored 0x7EF085 C3.06 +102 C3.04 C H counter advances Documented SNESdev Wiki, PPU registers Scored 0x7EF086 - +103 C3.05 C $213F resets flipflop Documented SNESdev Wiki, PPU registers; fullsnes Scored 0x7EF087 C3.08 +104 C3.07 C Counter flipflops differ Documented SNESdev Wiki, PPU registers; fullsnes Scored 0x7EF088 C3.07 +105 C3.12 C CGRAM taken in render Documented fullsnes and the SNESdev Wiki: a CGRAM access during active display uses the colour the PPU is drawing, not the CPU CGADD. Mesen2 models it (writes use InternalCgramAddress when !CanAccessCgram); the batch compositor and snes9x use the programmed CGADD and fail Scored 0x7EF089 C3.04 +106 C13.01 C PPU1 open bus in $213E Documented SNESdev Wiki, PPU registers; fullsnes Scored 0x7EF08A C13.07,C13.10 +107 C13.02 C PPU2 open bus in $213F Documented SNESdev Wiki, PPU registers; fullsnes Scored 0x7EF08B C13.08,C13.10 +108 C13.03 C PPU1/PPU2 bus separate Corroborated the bsnes/ares lineage and Mesen2 model two distinct latches Scored 0x7EF08C C13.09 +109 C14.01 C PPU1 version (golden) Documented SNESdev Wiki, PPU registers; fullsnes Golden 0x7EF08D C14.01 +110 C14.02 C PPU2 version (golden) Documented SNESdev Wiki, PPU registers; fullsnes Golden 0x7EF08E C14.02 +111 C14.03 C PPU1 mstr/slv (golden) Contested the bit reports a board wiring input, so no software-visible value is correct or incorrect; recorded rather than asserted Golden 0x7EF08F C14.03 +112 C11.06 C MPY is 16x8 signed Documented SNESdev Wiki, Mode 7; fullsnes Scored 0x7EF090 C11.06 +113 C11.06b C MPY sign handling Documented SNESdev Wiki, Mode 7; fullsnes Scored 0x7EF091 C11.06 +114 C11.07 C MPY latch is shared Documented fullsnes and the SNESdev Wiki: PPU1's write-twice registers share one byte latch, so $210D/$210E and $211B-$211E interfere with each other Scored 0x7EF092 C11.07 +115 C11.08 C MPY busy during render Contested fullsnes and the SNESdev Wiki agree the Mode 7 multiplier is the renderer's and is busy during active display, but neither states which intermediate it holds at a given moment, and that is the only thing a cart can observe Golden 0x7EF093 C11.08 +116 C7.01 C Range Over at 32 sprites Documented SNESdev Wiki, Sprites; fullsnes Scored 0x7EF094 C7.01 +117 C7.04 C Offscreen X takes a slot Documented fullsnes and the SNESdev Wiki: sprite range evaluation selects on Y alone, so a sprite at X = $100 occupies a range slot despite being entirely off-screen Scored 0x7EF095 C7.04 +118 C7.05 C RangeOver dot = idx*2 Documented fullsnes and the SNESdev Wiki: range evaluation walks OAM two cycles per sprite, so the 33rd in-range sprite trips Range Over at H = OAM.INDEX * 2 on V = OBJ.YLOC Scored 0x7EF096 C7.05 +119 C7.06 C TimeOver by YLOC+1 Documented fullsnes and the SNESdev Wiki: the sprite tile budget is 34 per line and Time Over is raised by the fetch phase, so it reads set by the start of the line the sprites paint on Scored 0x7EF097 C7.06 +120 C7.02 C Time Over is slivers Documented SNESdev Wiki, Sprites; fullsnes; anomie Scored 0x7EF098 C7.02 +121 C7.08 C Flags ignore $212C Documented SNESdev Wiki, Sprites; fullsnes Scored 0x7EF099 C7.08 +122 C7.10 C OAM write to high table Documented fullsnes and the SNESdev Wiki (the Uniracers case): an OAM write during active display is driven to the evaluator's address and lands in the high table, not the CPU OAMADDR. Mesen2 models it (oamAddr = 0x200 | ((oamAddr & 0x1F0) >> 4)); the batch compositor and snes9x write the programmed OAMADDR and fail Scored 0x7EF09A C7.16 +123 C2.11 C VRAM locked in render Documented SNESdev Wiki, PPU registers; fullsnes Scored 0x7EF09B C2.11 +124 C1.08 C OAM addr in render Documented fullsnes and the SNESdev Wiki: during active display the renderer drives the OAM address, so a $2138 read returns a render-time address, not the CPU-programmed one. MesenCE models it (GetOamAddress -> _oamEvaluationIndex<<2); the batch compositor and snes9x return the programmed address and fail Scored 0x7EF09C C1.08 +125 C2.10 C Dropped write still incs Documented SNESdev Wiki, PPU registers; fullsnes; anomie Scored 0x7EF09D C2.10 +126 C1.06 C OAM addr reloads Documented SNESdev Wiki, OAM; anomie Scored 0x7EF09E C1.06 +127 C9.04 C Overscan moves vblank Documented SNESdev Wiki, Timing; fullsnes Scored 0x7EF09F C9.04,B4.02 +128 C9.05 C Mid-frame overscan lock Contested RustySNES and snes9x re-close the VRAM window on a mid-frame overscan enable and Mesen2 does not, so the references split one each way; the dossier's repro cannot break the tie because it read-modify-writes the write-only $2133 Golden 0x7EF0A0 C9.05 +129 C2.09 C VRAM read latch order Documented SNESdev Wiki, PPU registers: return latch, refill latch, then increment, with VMAIN bit 7 selecting which of $2139/$213A triggers it; bsnes and ares both refill before the step in sfc/ppu/io.cpp, and snes9x and Mesen2 agree Scored 0x7EF0A1 C2.09 +130 C3.10 C $2137 latch is gated Documented superfamicom.org registers: $2137 latches the H/V counter only if $4201 bit 7 is set, and no latching can occur while it is 0; snes9x and Mesen2 both gate it. What the read returns is a separate question, split out into C3.11 Scored 0x7EF0A2 C3.05 +131 C3.11 C $2137 open bus source Contested the sources say only that $2137 reads back as open bus; snes9x and RustySNES present PPU1's latch while Mesen2 presents the CPU's, and nothing available decides between two physically reasonable readings Golden 0x7EF0A3 C3.05 +132 B1.01 B MEMSEL selects FastROM Documented SNESdev Wiki, Memory map / timing; fullsnes Scored 0x7EF0A4 B1.01 +133 B1.02 B JOYSER is 12 clocks Documented SNESdev Wiki, Memory map / timing; fullsnes Scored 0x7EF0A5 B1.02 +134 B2.04 B NTSC frame is 262 lines Documented SNESdev Wiki, Timing; fullsnes Scored 0x7EF0A6 B2.04 +135 B4.03 B RDNMI sets at vblank Documented SNESdev Wiki, Timing; fullsnes Scored 0x7EF0A7 B4.03 +136 B2.01 B No dot above 339 Corroborated fullsnes' PPU H-Counter-Latch Quantities histogram, a direct hardware measurement: dots 323 and 327 latch six times, dot 340 never. bsnes, ares and Mesen2 all implement it; snes9x uses 322/326 and is the outlier Scored 0x7EF0A8 B2.01 +137 B4.16 B H-IRQ position (golden) Contested no source pins the fired dot at single-dot precision; recorded as the before/after guard for T-06-A's clock-domain comparator change Golden 0x7EF0A9 - +138 B4.17 B NMI enable is a level Documented SNESdev Wiki NMITIMEN/RDNMI [ERRATA]; fullsnes $4200/$4210 Scored 0x7EF0AA B4.06 +139 B4.04 B RDNMI is read-to-clear Documented SNESdev Wiki, Timing; fullsnes Scored 0x7EF0AB B4.04 +140 B4.05 B RDNMI auto-clears Documented SNESdev Wiki, Timing; fullsnes Scored 0x7EF0AC B4.05 +141 B4.08 B V-IRQ fires at VTIME Documented SNESdev Wiki, Timing; fullsnes Scored 0x7EF0AD B4.08 +142 B4.12 B $4211 read releases IRQ Documented SNESdev Wiki, Timing; fullsnes Scored 0x7EF0AE B4.12 +143 B4.15 B CPU revision (golden) Documented SNESdev Wiki, Timing; fullsnes Golden 0x7EF0AF G1.09 +144 B5.01 B 8x8 unsigned multiply Documented SNESdev Wiki, CPU registers; fullsnes Scored 0x7EF0B0 B5.01 +145 B5.02 B 16/8 unsigned divide Documented SNESdev Wiki, CPU registers; fullsnes Scored 0x7EF0B1 B5.02 +146 B5.03 B Divide by zero Documented SNESdev Wiki, CPU registers; fullsnes Scored 0x7EF0B2 - +147 B5.04 B Mul/div overlap (golden) Contested SNESdev Errata states overlapping $4203/$4206 operation is undefined Golden 0x7EF0B3 B5.03 +148 B5.05 B Mul/div power-on state Documented anomie regs.txt r1157 and nocash fullsnes, independently; implemented by bsnes/ares/Mesen2. No known hardware test ROM Scored 0x7EF0B4 B5.04 +149 B1.03 B Internal cycles are 6 Documented SNES Development Manual Bk I 21.1; SNESdev Wiki; fullsnes Scored 0x7EF0B5 B1.03 +150 B1.04 B DMA speed is uniform Documented SNES Development Manual Bk I 21.1 (DMA at 2.68MHz regardless of address) Scored 0x7EF0B6 B1.04 +151 B2.06 B Interlace line count Contested the dossier conditions the extra line on $213F.7 (the field), which this test does not control — sampling V across an uncontrolled field cannot assert a line count Golden 0x7EF0B7 B2.06 +152 B2.05 B PAL frame is 312 lines Documented SNESdev Wiki, Timing; fullsnes Scored 0x7EF0B8 B2.05 +153 B4.14 B IRQ dispatch latency Documented SNESdev Wiki, Timing; fullsnes — the sub-cycle poll point is not CPU-observable, so its consequence is measured instead Golden 0x7EF0B9 B4.14 +154 B2.10 B Region bit (golden) Contested SNESdev PPU registers places the 50/60Hz bit at bit 3, which overlaps the PPU2 version field; fullsnes places it at bit 4 Golden 0x7EF0BA B2.10 +155 B4.07 B H-IRQ position (golden) Contested the $4211 poll loop is coarser than the dot the comparator fires on, so the exact H position is not resolvable from software by polling Golden 0x7EF0BB B4.07 +156 B4.09 B HV-IRQ needs both Documented SNESdev Wiki, Timing; fullsnes Scored 0x7EF0BC B4.09 +157 B3.01 B DRAM refresh pause Contested fullsnes and anomie put the pause at 40 clocks near line-clock 536, and RustySNES now models it there (docs/dram-refresh.md), so this loop records variant 2 (one interval ~10 dots longer); ares' own source still calls its refresh *pattern* technically wrong and only right on average, so the exact per-line position stays host-cross-validated rather than self-asserted Golden 0x7EF0BD B3.01,B3.02,B3.03 +158 B4.13 B Timer range is 9-bit Documented fullsnes $4207-$420A: HTIME is 0-339 and VTIME 0-261 (NTSC) / 0-311 (PAL), both held in nine bits Scored 0x7EF0BE B4.13 +159 B4.11 B Dot 153, last line Contested superfamicom.org's timing page states the exception and gives no mechanism; its timing text derives from fullsnes, so the two are one source, and no test ROM verifies it. ares, bsnes, Mesen2 and snes9x were each searched and none implements it Golden 0x7EF0BF B4.11 +160 D1.01 D DMA mode 0 Documented SNESdev Wiki, DMA; fullsnes Scored 0x7EF0C0 D1.01 +161 D1.01b D DMA mode 1 Documented SNESdev Wiki, DMA; fullsnes Scored 0x7EF0C1 D1.01 +162 D1.06 D DMA count hits zero Documented SNESdev Wiki, DMA registers; fullsnes Scored 0x7EF0C2 D1.06 +163 D1.07 D DMA fixed A-bus Documented SNESdev Wiki, DMA registers; fullsnes Scored 0x7EF0C3 D1.07 +164 D1.07b D DMA decrementing A-bus Documented SNESdev Wiki, DMA registers; fullsnes Scored 0x7EF0C4 D1.07 +165 D1.10 D DMA $43xB scratch latch Corroborated ares and bsnes both model the latch and serialize it Scored 0x7EF0C5 D1.10 +166 D1.02 D DMA 8 clocks/byte Documented SNESdev Wiki, DMA timing; fullsnes Scored 0x7EF0C6 D1.02 +167 D1.05 D DMA count 0 = 65536 Documented SNESdev Wiki, DMA registers; fullsnes Scored 0x7EF0C7 D1.05 +168 D1.09 D WRAM->$2180 no-write Documented fullsnes: "does not cause a write to occur" Scored 0x7EF0C8 D1.09,D1.15 +169 D2.03 D HDMA line-count byte Documented SNESdev Wiki, HDMA; fullsnes Scored 0x7EF0C9 D2.03 +170 D2.04 D HDMA repeat flag Documented SNESdev Wiki, HDMA; fullsnes Scored 0x7EF0CA D2.04 +171 D2.07 D HDMA preempts GP-DMA Documented SNESdev Wiki, HDMA; anomie's timing doc; fullsnes Scored 0x7EF0CB D2.07 +172 D2.09 D HDMA armed mid-frame Contested fullsnes and the SNESdev Wiki record that enabling HDMA outside vblank produces erroneous writes from uninitialised A2An/NLTRn, but what those writes contain is a function of the previous frame's leftover state and is specified nowhere Golden 0x7EF0CC D2.09 +173 D1.14 D $2180 B->A does write Documented fullsnes: $2180->WRAM writes, but the value written is invalid Scored 0x7EF0CD D1.14 +174 D1.13 D DMA count hits zero Documented fullsnes and ares: the DAS $43x5/6 byte-count register decrements as GP-DMA transfers and reads $0000 when the transfer completes Scored 0x7EF0CE D1.13 +175 D1.11 D DMA power-on state Corroborated fullsnes register table and the SNESdev DMA-registers page agree independently; ares and bsnes default every channel field to match Scored 0x7EF0CF D1.11 +176 D1.08 D Invalid A-bus (golden) Contested the errata names the ranges invalid but does not specify what is read instead; the substitute is open bus, whose content is core-specific and time-dependent Golden 0x7EF0D0 D1.08 +177 D1.03 D DMA startup overhead Documented SNESdev Wiki, DMA timing; fullsnes Golden 0x7EF0D1 D1.03 +178 D1.04 D DMA channel priority Documented SNESdev Wiki, DMA; fullsnes Scored 0x7EF0D2 D1.04 +179 D2.05 D HDMA indirect mode Documented SNESdev Wiki, HDMA; fullsnes Scored 0x7EF0D3 D2.05 +180 D2.06 D HDMA $4308/$430A state Documented SNESdev Wiki, HDMA registers; fullsnes Scored 0x7EF0D4 D2.06 +181 E4.11 E ARAM power-on pattern Contested the dossier records a repeating 32x$00 / 32x$FF fill and marks it chip-dependent and informational; RustySNES, snes9x and Mesen2 all boot APU RAM uniformly zero instead Golden 0x7EF0D5 E4.11 +182 E1.01 E MUL YA flags from Y Documented SNESdev Wiki, SPC700 reference; fullsnes — flagged as errata Scored 0x7EF0D6 E1.01 +183 E1.02 E DIV YA,X normal branch Documented SNESdev Wiki, SPC700 reference; fullsnes Scored 0x7EF0D7 E1.02 +184 E1.07 E DIV valid to Q<=511 Documented SNESdev Wiki SPC700 reference and fullsnes, both flagging DIV as valid only for quotients up to 511; the values past it follow E1.03's overflow formula Scored 0x7EF0D8 E1.07 +185 E1.04 E DIV H = nibble compare Documented SNESdev Wiki, SPC700 reference; fullsnes — flagged as errata Scored 0x7EF0D9 E1.04 +186 E1.05 E DIV V is quotient bit 8 Documented SNESdev Wiki, SPC700 reference; fullsnes Scored 0x7EF0DA E1.05 +187 E1.06 E DIV flags from quotient Documented SNESdev Wiki, SPC700 reference; fullsnes — flagged as errata Scored 0x7EF0DB E1.06 +188 E1.13 E ADDW H = bit-11 carry Documented SNESdev Wiki, SPC700 reference; fullsnes Scored 0x7EF0DC E1.13 +189 E1.15 E MOVW YA sets 16-bit N/Z Documented SNESdev Wiki, SPC700 reference; fullsnes Scored 0x7EF0DD E1.15 +190 E3.01 E Timer read clears it Documented SNESdev Wiki, SPC700 I/O; fullsnes Scored 0x7EF0DE E3.01 +191 E3.02 E Timer enable 0->1 resets Documented SNESdev Wiki SPC700 timers and fullsnes: a 0->1 on a $F1 timer-enable bit resets that timer's stage-2 divider and stage-3 output counter Scored 0x7EF0DF E3.02 +192 E3.11 E $F2 bit 7 blocks writes Documented SNESdev Wiki, S-DSP; fullsnes Scored 0x7EF0E0 E3.11 +193 E3.11b E DSP register addressing Documented SNESdev Wiki, S-DSP registers; fullsnes Scored 0x7EF0E1 - +194 E2.01 E Store dummy-reads target Documented SNESdev Wiki, SPC700; fullsnes — flagged as errata Scored 0x7EF0E2 E2.01 +195 E2.05 E DP index wraps in page Documented SNESdev Wiki, SPC700 addressing; fullsnes Scored 0x7EF0E3 E2.05 +196 E6.09 E Gaussian sum wraps Documented fullsnes and anomie's DSP doc: of the four gaussian additions the first cannot overflow, the second wraps in 16 bits and the third saturates Scored 0x7EF0E4 E6.09 +197 E6.11 E BRR waveform vectors Contested the dossier names four nibble patterns and asks what a decoder makes of each without stating an expected value for any of them; the row's content is the measurement Golden 0x7EF0E5 E6.11 +198 E10.01 E 32 SPC cycles per sample Documented fullsnes and anomie: the SPC700's 1.024 MHz clock and the DSP's 32 kHz output rate fix the ratio at 32 CPU cycles per output sample Scored 0x7EF0E6 E10.01 +199 E10.05 E Soft reset acts as $E0 Contested both sources agree FLG bit 7 makes the DSP behave as $E0 and force every voice into release, and contradict each other on what ENDX then reads: nocash says $FF, anomie says 0. The dossier marks the row [CONFLICT] and asks for a golden vector Golden 0x7EF0E7 E10.05 +200 E1.14 E XCN costs five cycles Documented the SNESdev Wiki SPC700 reference and fullsnes both give XCN as 5 cycles, against 2 for NOP Scored 0x7EF0E8 E1.14 +201 E2.04 E DBNZ dp is an RMW Documented SNESdev Wiki SPC700 reference and fullsnes: DBNZ dp,rel reads its operand, decrements it and writes it back, and $FD-$FF are read-to-clear Scored 0x7EF0E9 E2.04 +202 E3.14 E $F8/$F9 are plain RAM Documented SNESdev Wiki, SPC700 I/O; fullsnes Scored 0x7EF0EA E3.14 +203 E3.11c E DSP global registers Documented SNESdev Wiki, S-DSP registers; fullsnes Scored 0x7EF0EB - +204 E9.19 E ENDX write clears it Documented SNESdev Wiki, S-DSP registers; fullsnes Scored 0x7EF0EC E9.19 +205 E9.03 E Pitch not noise rate Documented fullsnes and anomie's DSP doc: the noise generator's step rate is set by FLG bits 0-4, and a voice's pitch register does not participate Scored 0x7EF0ED E9.03 +206 E9.01 E Noise LFSR seed Documented fullsnes and anomie's DSP doc: the noise shift register resets to $4000, with taps bit0 XOR bit1 feeding bit 14 Scored 0x7EF0EE E9.01 +207 E5.07 E End+mute zeroes env Documented fullsnes, S-DSP BRR; anomie's DSP doc Scored 0x7EF0EF E5.07 +208 E5.08 E Loop flag without end Documented fullsnes, S-DSP BRR; anomie's DSP doc Scored 0x7EF0F0 E5.08 +209 E5.09 E ENDX sets on end block Documented fullsnes, S-DSP BRR; anomie's DSP doc Scored 0x7EF0F1 E5.09 +210 E5.01 E BRR header layout Documented fullsnes and anomie's DSP doc: the BRR header is ssssffle, shift in bits 7-4, and a nibble decodes as (nibble << shift) >> 1 Scored 0x7EF0F2 E5.01 +211 E5.10 E Released voice decodes Documented fullsnes and anomie's DSP doc: key-off begins the release ramp and does not halt BRR decoding, which continues to follow loop points and set ENDX Scored 0x7EF0F3 E5.10 +212 E5.11 E Directory entry address Documented fullsnes, S-DSP BRR; anomie's DSP doc Scored 0x7EF0F4 E5.11 +213 E7.10 E Direct GAIN is envelope Documented SNESdev Wiki, S-DSP envelopes; fullsnes Scored 0x7EF0F5 E7.10 +214 E1.03 E DIV overflow branch Documented SNESdev Wiki, SPC700 reference; fullsnes — flagged as errata Scored 0x7EF0F6 E1.03 +215 E1.08 E DAA adjustments Documented SNESdev Wiki, SPC700 reference; fullsnes Scored 0x7EF0F7 E1.08 +216 E2.08 E TCALL vector table Documented SNESdev Wiki, SPC700 reference; fullsnes Scored 0x7EF0F8 E2.08 +217 E2.09 E BRK shares TCALL 0 Documented SNESdev Wiki, SPC700 reference; fullsnes Scored 0x7EF0F9 E2.09 +218 E3.03 E $F1 bit 5 clears port 3 Documented SNESdev Wiki, SPC700 I/O; fullsnes Scored 0x7EF0FA E3.03 +219 E3.04 E Writes pass under IPL Documented SNESdev Wiki, SPC700 I/O; fullsnes Scored 0x7EF0FB E3.04 +220 E3.05 E TnDIV $00 means 256 Documented SNESdev Wiki, SPC700 timers; fullsnes Scored 0x7EF0FC E3.05 +221 E3.10 E TEST gates RAM writes Documented SNESdev Wiki, SPC700 I/O; fullsnes Scored 0x7EF0FD E3.10 +222 E1.09 E DAS mirrors DAA Documented SNESdev Wiki, SPC700 reference; fullsnes Scored 0x7EF0FE E1.09 +223 E1.10 E TSET1 is a compare Documented SNESdev Wiki, SPC700 reference; fullsnes — flagged as errata Scored 0x7EF0FF E1.10 +224 E1.12 E CLRV clears H too Documented SNESdev Wiki, SPC700 reference; fullsnes — flagged as errata Scored 0x7EF100 E1.12 +225 E2.02 E MOV dp,dp is exempt Documented SNESdev Wiki, SPC700; fullsnes — flagged as errata Scored 0x7EF101 E2.02 +226 E2.03 E MOVW reads the low byte Documented SNESdev Wiki, SPC700; fullsnes — flagged as errata Scored 0x7EF102 E2.03 +227 E2.06 E PSW.P selects the page Documented SNESdev Wiki, SPC700 addressing; fullsnes Scored 0x7EF103 E2.06 +228 E2.07 E CALL pushes exact addr Documented SNESdev Wiki, SPC700 reference; fullsnes Scored 0x7EF104 E2.07 +229 E4.01 E IPL ROM contents Documented the canonical 64-byte IPL listing; fullsnes, SNESdev Wiki Scored 0x7EF105 E4.01 +230 E4.02 E IPL handoff state Documented fullsnes, SNESdev Wiki, APU boot handshake Scored 0x7EF106 E4.02 +231 E4.03 E IPL zerofills $00-$EF Documented the canonical 64-byte IPL listing zero-fills $0000-$00EF as the first thing it does at $FFC0, before entering its transfer loop; fullsnes and the SNESdev Wiki both carry it Scored 0x7EF107 E4.03 +232 E4.04 E IPL ready announcement Documented fullsnes, SNESdev Wiki, APU boot handshake Scored 0x7EF108 E4.04 +233 E4.06 E IPL multi-block continue Documented fullsnes, SNESdev Wiki; canonical IPL boot ROM $FFEF-$FFFB Scored 0x7EF109 E4.06 +234 E4.08 E IPL DSP-poke via $00F2 Documented fullsnes (APU / S-DSP); canonical IPL boot ROM store loop Scored 0x7EF10A E4.08 +235 E5.12 E SRCN change source Documented fullsnes and anomie's DSP doc: a mid-playback SRCN change takes the new entry's start address, or its loop address if the voice has already passed a loop point Scored 0x7EF10B E5.12 +236 E5.02 E BRR nibbles are signed Documented fullsnes, S-DSP BRR; anomie's DSP doc Scored 0x7EF10C E5.02 +237 E7.16 E OUTX is pre-volume Documented fullsnes, S-DSP envelopes; anomie's DSP doc Scored 0x7EF10D E7.16 +238 E8.04 E KOFF outranks KON Documented fullsnes, S-DSP key on/off; anomie's DSP doc Scored 0x7EF10E E8.04 +239 E8.05 E KON is edge-triggered Documented fullsnes and anomie's DSP doc: KON is write-triggered and self-clears ~63 clocks later; only KOFF and FLG bit 7 are level-sensitive Scored 0x7EF10F E8.05 +240 E9.04 E Noise voices decode BRR Documented fullsnes, S-DSP noise; anomie's DSP doc — flagged as errata Scored 0x7EF110 E9.04 +241 E9.06 E EDL 0 is a 4-byte buffer Documented fullsnes, S-DSP echo — flagged as errata; anomie's DSP doc Scored 0x7EF111 E9.06 +242 E9.12 E Echo writes are masked Documented fullsnes, S-DSP echo; anomie's DSP doc Scored 0x7EF112 E9.12 +243 E9.15 E Voice mix saturates Documented fullsnes and anomie's DSP doc: the per-voice mix clamps to 16 bits after each addition rather than accumulating and clamping once Scored 0x7EF113 E9.15 +244 E9.05 E Echo entry is 4 bytes Documented fullsnes and anomie's DSP doc: the echo buffer holds four bytes per sample, a 16-bit left sample followed by a 16-bit right one Scored 0x7EF114 E9.05 +245 E9.09 E Echo wraps into page 0 Documented fullsnes and anomie's DSP doc [ERRATA]: the echo buffer address is 16-bit and wraps, so a buffer that runs past $FFFF continues at $0000 over page zero and the IPL shadow Scored 0x7EF115 E9.09 +246 E9.13 E L/R FIR independent Documented fullsnes and anomie's DSP doc: the echo FIR keeps a separate eight-sample history and accumulator per channel; only the coefficients are shared Scored 0x7EF116 E9.13 +247 E9.10 E FLG.5 stops echo writes Documented fullsnes, S-DSP echo; anomie's DSP doc Scored 0x7EF117 E9.10 +248 E9.17 E Mute is after OUTX Documented fullsnes, S-DSP FLG; anomie's DSP doc Scored 0x7EF118 E9.17 +249 E9.18 E FLG reset kills voices Documented SNESdev Wiki, S-DSP; fullsnes Scored 0x7EF119 E9.18 +250 E5.03 E BRR sample arithmetic Documented fullsnes, S-DSP BRR; anomie's DSP doc Scored 0x7EF11A E5.03 +251 E5.04 E Invalid shift collapses Documented fullsnes, S-DSP BRR; anomie's DSP doc Scored 0x7EF11B E5.04 +252 E5.05 E BRR filter 1 Documented fullsnes, S-DSP BRR filters; anomie's DSP doc Scored 0x7EF11C E5.05 +253 E5.06 E BRR wraps at 15 bits Documented fullsnes and anomie's DSP doc: the decoded sample is clamped to 16 bits and then stored as 15, so +4000h..+7FFFh becomes -4000h..-1 and the sign is lost Scored 0x7EF11D E5.06 +254 E5.13 E Released voices decode Documented fullsnes and anomie's DSP doc: the BRR decoder advances on the pitch clock regardless of the envelope; voices never actually stop decoding Scored 0x7EF11E E5.13 +255 E7.01 E Rate 0 never fires Documented SNESdev Wiki, S-DSP envelopes; fullsnes; anomie's DSP doc Scored 0x7EF11F E7.01 +256 E7.13 E GAIN bent-increase Documented fullsnes and anomie's DSP doc: GAIN mode 7 increases +32 per sample below $600 and +8 above, comparing the internal envelope latch unsigned Scored 0x7EF120 E7.13 +257 E7.17 E Lin-decrease clamps 0 Documented fullsnes and anomie's DSP doc: GAIN linear-decrease subtracts $20 per tick and clamps the envelope to zero on underflow, comparing the internal envelope unsigned Scored 0x7EF121 E7.17 +258 E7.18 E ENVX is E>>4 Documented fullsnes and anomie's DSP doc: VxENVX = envelope >> 4, a seven-bit value with bit 7 always clear Scored 0x7EF122 E7.18 +259 E7.04 E Attack $F is instant Documented fullsnes and anomie's DSP doc: attack rate $F fires every sample with a step of +1024, rather than the +32 on a counter tick every other rate uses Scored 0x7EF123 E7.04 +260 E7.09 E Release rate is fixed Documented fullsnes and anomie's DSP doc: release steps -8 per sample regardless of ADSR, which is why a custom release has to be built from GAIN Scored 0x7EF124 E7.09 +261 E7.05 E Decay index d*2+16 Documented fullsnes and anomie's DSP doc: the decay phase indexes the counter table at d*2+16, stepping E -= 1 then E -= E>>8 Scored 0x7EF125 E7.05 +262 E7.06 E Sustain index verbatim Documented fullsnes and anomie's DSP doc: the sustain phase indexes the counter table with the ADSR2 rate field directly, unlike decay's d*2+16 Scored 0x7EF126 E7.06 +263 E7.07 E Sustain boundary Documented fullsnes and anomie's DSP doc: the decay phase ends when (E >> 8) equals the ADSR2 sustain level, giving a boundary of $100 per level Scored 0x7EF127 E7.07 +264 E7.03 E Attack index a*2+1 Documented fullsnes and anomie's DSP doc: the attack phase indexes the counter table at a*2+1 and steps the envelope by +32 Scored 0x7EF128 E7.03 +265 E7.12 E GAIN sustain boundary Contested the dossier records the GAIN-sourced boundary as [ERRATA]; RustySNES takes it from ADSR2 instead, which E7.07 asserts, so which behaviour is right is not settled here Golden 0x7EF129 E7.12 +266 E7.08 E Key-off releases to zero Documented SNESdev Wiki, S-DSP envelopes; fullsnes; anomie's DSP doc Scored 0x7EF12A E7.08 +267 E8.01 E KON examined at 16 kHz Documented fullsnes and anomie's DSP doc [ERRATA]: the key-on/key-off registers are examined once every two output samples, giving a 16 kHz effective poll rate Scored 0x7EF12B E8.01 +268 E8.02 E Key-on takes 5 samples Documented fullsnes and anomie's DSP doc: KON is held for five output samples while the directory and the first BRR block are fetched, before the envelope starts Scored 0x7EF12C E8.02 +269 E8.03 E KON restarts a voice Documented fullsnes and anomie's DSP doc: KON re-enters the key-on sequence unconditionally, resetting the BRR pointer and zeroing the envelope, whether or not the voice was already playing Scored 0x7EF12D E8.03 +270 E8.07 E KOFF pulse collapses Contested KON/KOFF are sampled every second output sample, so whether a short pulse is seen depends on where the poll falls inside it -- which makes the outcome phase-dependent rather than fixed, the same hedge E8.05 and E8.06 carry as "usually" Golden 0x7EF12E E8.07 +271 E8.10 E KOFF+KON cuts faster Documented fullsnes and anomie's DSP doc: KON zeroes the envelope before its attack, and KOFF outranks KON, so the pair silences immediately where KOFF alone ramps Scored 0x7EF12F E8.10 +272 E7.11 E GAIN linear increase Documented SNESdev Wiki, S-DSP envelopes; fullsnes; anomie's DSP doc Scored 0x7EF130 E7.11 +273 E7.14 E GAIN decrease clamps Documented SNESdev Wiki, S-DSP envelopes; fullsnes; anomie's DSP doc Scored 0x7EF131 E7.14 +274 E7.15 E ENVX is E >> 4 Documented SNESdev Wiki, S-DSP envelopes; fullsnes; anomie's DSP doc Scored 0x7EF132 E7.15 +275 E6.02 E Pitch $1000 is 1:1 Documented fullsnes, S-DSP pitch; anomie's DSP doc Scored 0x7EF133 E6.02 +276 E6.02b E Pitch $1000 does finish Documented fullsnes, S-DSP pitch; anomie's DSP doc Scored 0x7EF134 E6.02 +277 E6.02c E Pitch $2000 is +1 octave Documented fullsnes, S-DSP pitch; anomie's DSP doc Scored 0x7EF135 E6.02 +278 E6.02d E Pitch $2000 upper bound Documented fullsnes, S-DSP pitch; anomie's DSP doc Scored 0x7EF136 E6.02 +279 E3.06 E T2 is eight times T0 Documented SNESdev Wiki, SPC700 timers; fullsnes Scored 0x7EF137 E3.06 +280 E3.08 E TEST bit 0 halts timers Documented fullsnes, SPC700 TEST register; ares and bsnes smp/timing Scored 0x7EF138 E3.08 +281 E3.09 E Waits: CPU 10, timer 8 Documented ares and bsnes sfc/smp/timing.cpp, identically: cycleWaitStates {2,4,10,20} against timerWaitStates {2,4,8,16}, with the comment that the timers are not affected by the 8/16 divider glitch Scored 0x7EF139 E3.09 +282 B2.07 B Frame rate vs APU clock Documented fullsnes and the SNESdev Wiki: NTSC 60.0988 Hz, PAL 50.00698 Hz; the APU's 24.576 MHz crystal is region-independent (ares apuFrequency, snes9x's two APU ratios) Scored 0x7EF13A B2.07,B2.08 +283 E3.13 E Regs shadow into RAM Documented fullsnes and the SNESdev Wiki: writes to $00F0-$00FF reach the underlying APU RAM as well as the register, which the S-DSP can then read as sample data Scored 0x7EF13B E3.13 +284 E2.10i E SPC cycle instrument Documented the apparatus for E2.10's 256-opcode sweep, validated against NOP = 2, XCN = 5 (also E1.14) and MUL YA = 9 -- each independently derivable from spc700_exec.rs's read/idle sequence Golden 0x7EF13C - +285 E2.10 E 256-opcode cycle sweep Documented fullsnes, SNES APU SPC700 CPU instruction set Scored 0x7EF13D E2.10 +286 E9.02 E Noise output is bipolar Documented fullsnes and anomie's DSP doc [ERRATA]: the noise output is highpass-filtered as a consequence of the 15-bit shift register being interpreted as the top bits of a signed 16-bit sample Scored 0x7EF13E E9.02 +287 F1.01 F Manual pad read order Documented fullsnes and the SNESdev Wiki controller protocol: the shift register presents B, Y, Select, Start, Up, Down, Left, Right, A, X, L, R and then four zero bits Scored 0x7EF13F F1.01 +288 F1.02 F Pad reads 17+ are 1 Documented SNESdev Wiki, controller protocol; fullsnes Scored 0x7EF140 F1.02 +289 F1.03 F Shared $4016 latch Documented fullsnes and the SNESdev Wiki controller protocol: bit 0 of $4016 is the shared latch line that parallel-loads both controller ports' shift registers Scored 0x7EF141 F1.03 +290 F1.04 F $4016 bits 7-2 open bus Corroborated RustySNES, snes9x and Mesen2 all return $41 for the absolute read and $01 for the long one -- identical bytes, so bits 7-2 follow the CPU bus in all three Scored 0x7EF142 F1.04 +291 F1.07 F Auto-read needs $4200.0 Documented fullsnes and the SNESdev Wiki: bit 0 of $4200 arms the automatic joypad read, and with it clear $4218-$421F are not written Scored 0x7EF143 F1.07 +292 F1.05 F Pad signature is 0000 Documented fullsnes and the SNESdev Wiki: bits 3-0 of the auto-read result identify the device, and a standard controller reports 0000 Scored 0x7EF144 F1.05 +293 F1.06 F First bit clocked is B Documented fullsnes and the SNESdev Wiki: the auto-read result holds the sixteen shifted bits in clock order, most significant first, so B is bit 15 Scored 0x7EF145 F1.06 +294 F1.08 F Auto-read start dot Documented fullsnes: the automatic joypad read begins ~dot 32.5-95.5 of the first vblank line (RustySNES delays it 256 master clocks ~ dot 64), not at the vblank edge Golden 0x7EF146 F1.08 +295 F1.09 F Auto-read duration Documented fullsnes: the automatic joypad read is busy for exactly 4224 master cycles (~3.097 scanlines); the poll count is a CPU-cycle-proportional proxy for it Golden 0x7EF147 F1.09 +296 F1.10 F Auto-read start race Documented fullsnes: the automatic joypad read begins ~dot 32.5-95.5 of the first vblank line, not at the vblank edge, so $4212 bit 0 reads not-busy for that window and a $4212 poll at NMI entry sees the read not yet started Scored 0x7EF148 F1.10 +297 F1.11 F Latch corrupts auto-read Documented fullsnes and the SNESdev Wiki: while $4016 bit 0 is high the shift registers reload continuously rather than shifting, so an automatic read taken across it returns the same bit in every position Scored 0x7EF149 F1.11 +298 F1.12 F Auto-read result timing Contested F1.12 says results are valid by V = $E3, which does not reconcile with F1.09's 4224-cycle duration and F1.08's start window; no source says which to believe, and the cores split on whether the result appears at once or progressively Golden 0x7EF14A F1.12 +299 F1.14 F $4213 reads $4201 back Documented fullsnes: RDIO reads the WRIO output pins, which are open-collector, so with nothing driving them low the value read is the value written Scored 0x7EF14B F1.14 +300 G1.02 G Reset: $4210/$4211 clear Documented SNESdev Wiki, power-on state; fullsnes Scored 0x7EF14C G1.02 +301 G1.04 G Reset: emulation mode Documented SNESdev Wiki, power-on state; WDC 65C816 datasheet, XCE Scored 0x7EF14D G1.04 +302 G1.05 G Power-on PPU registers Documented the dossier marks the PPU power-on state indeterminate ('no boot ROM; most PPU registers start unknown') and says to report it, never assert; the readable registers ($2134-$2136, $213E/$213F) are reported and the only scored check is the self-guard that the power-on capture actually ran Scored 0x7EF14E G1.05 +303 G1.08 G Write-only read: openbus Documented SNESdev Wiki, open bus; fullsnes, memory map notes Scored 0x7EF14F G1.08 +304 G1.10 G Checksum XOR complement Documented SNESdev Wiki, cartridge header; fullsnes Scored 0x7EF150 G1.10 +305 G1.11 G Checksum over the image Documented SNESdev Wiki, cartridge header checksum; fullsnes Scored 0x7EF151 G1.11 +306 G1.12 G LoROM header location Documented SNESdev Wiki, cartridge header; fullsnes Scored 0x7EF152 G1.12 +307 G1.14 G LoROM bank decode Documented SNESdev Wiki, memory map; fullsnes Scored 0x7EF153 G1.14 +308 G1.19 G Power-on $4201/timers Documented fullsnes and the SNESdev Wiki power-on table: $4201 = $FF, HTIME and VTIME = $1FF; the other registers in the row are observed by B5.05 and B1.01 Scored 0x7EF154 G1.01 +309 G1.20 G Power-on indeterminate Contested the dossier marks the whole row [UNDEFINED] and says to report it and never assert it; half the registers it names are write-only and cannot be reported at all Golden 0x7EF155 G1.03 +310 G1.07 G WRAM power-on fill Contested the dossier marks the row [UNDEFINED] and asks for a golden vector by name: no canonical WRAM fill exists and real consoles disagree Golden 0x7EF156 G1.07 +311 A5.S01 A Sweep: CLC Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF157 A5.01-08 +312 A5.S02 A Sweep: SEC Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF158 A5.01-08 +313 A5.S03 A Sweep: CLV Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF159 A5.01-08 +314 A5.S04 A Sweep: INX Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF15A A5.01-08 +315 A5.S05 A Sweep: DEX Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF15B A5.01-08 +316 A5.S06 A Sweep: TAX Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF15C A5.01-08 +317 A5.S07 A Sweep: TXY Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF15D A5.01-08 +318 A5.S08 A Sweep: ASL A Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF15E A5.01-08 +319 A5.S09 A Sweep: XBA Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF15F A5.01-08 +320 A5.S10 A Sweep: TCD Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF160 A5.01-08 +321 A5.S11 A Sweep: LDA #imm Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF161 A5.01-08 +322 A5.S12 A Sweep: LDX #imm Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF162 A5.01-08 +323 A5.S13 A Sweep: CMP #imm Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF163 A5.01-08 +324 A5.S14 A Sweep: BIT #imm Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF164 A5.01-08 +325 A5.S15 A Sweep: REP #imm Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF165 A5.01-08 +326 A5.S16 A Sweep: SEP #imm Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF166 A5.01-08 +327 A5.S17 A Sweep: WDM Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF167 A5.01-08 +328 A5.S18 A Sweep: PHA+PLA Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF168 A5.01-08 +329 A5.S19 A Sweep: PHP+PLP Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF169 A5.01-08 +330 A5.S20 A Sweep: PHB+PLB Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF16A A5.01-08 +331 A5.S21 A Sweep: PHD+PLD Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF16B A5.01-08 +332 A5.S22 A Sweep: PHX+PLX Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF16C A5.01-08 +333 A5.S23 A Sweep: LDA dp Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF16D A5.01-08 +334 A5.S24 A Sweep: LDA abs Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF16E A5.01-08 +335 A5.S25 A Sweep: LDA long Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF16F A5.01-08 +336 A5.S26 A Sweep: STA dp Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF170 A5.01-08 +337 A5.S27 A Sweep: STA abs Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF171 A5.01-08 +338 A5.S28 A Sweep: LDA dp,X Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF172 A5.01-08 +339 A5.S29 A Sweep: LDA abs,X Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF173 A5.01-08 +340 A5.S30 A Sweep: INC dp Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF174 A5.01-08 +341 A5.S31 A Sweep: INC abs Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF175 A5.01-08 +342 A5.S32 A Sweep: ADC dp Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF176 A5.01-08 +343 A5.S33 A Sweep: CMP abs Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF177 A5.01-08 +344 A5.S34 A Sweep: BVS untaken Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF178 A5.01-08 +345 A5.S35 A Sweep: BRL flat 4 Documented WDC/GTE/VLSI instruction-operation tables agree; docs/accuracysnes-timing-oracle.md Scored 0x7EF179 A5.16 diff --git a/tests/roms/AccuracySNES/asm/tests_group_a.s b/tests/roms/AccuracySNES/asm/tests_group_a.s index 94b32192..de08f527 100644 --- a/tests/roms/AccuracySNES/asm/tests_group_a.s +++ b/tests/roms/AccuracySNES/asm/tests_group_a.s @@ -5154,6 +5154,494 @@ CATALOG_IMPL = 1 jml test_restore .endproc +; A6.15 — all 256 opcodes defined +; provenance: Documented (WDC W65C816S datasheet, Table 5-4 opcode matrix) +.proc test_a6_15 + .a16 + .i16 + ; The two exit stubs and the NMI watchdog are written into WRAM before the sweep starts. + bra @body +@nmi: + ; Long addressing throughout: the sandbox may have left DBR and D anywhere, and this + ; handler runs before anything has put them back. + ; A is preserved. RTI restores P and PC but not the accumulator, and an interrupt that + ; silently rewrites A is not transparent to the thing it interrupted. + rep #$30 + .a16 + .i16 + pha + sep #$20 + .a8 + .a8 + ; LONG addressing, and that is the whole bug this handler shipped with. It runs with the + ; SANDBOX's DBR, which is $7E — so `lda $4210` reads $7E:4210, a WRAM byte, and RDNMI is + ; never acknowledged. Every other host happened not to land an NMI where it showed; ares + ; did, and the row read as a PLA divergence that was nothing of the kind. + lda f:$004210 ; acknowledge RDNMI, DBR or no DBR + lda f:$7E6104 + beq @nmi_out ; no sandbox in flight — nothing to rescue + lda f:$7E6105 + inc a + sta f:$7E6105 + cmp #$02 + bcc @nmi_out ; first hit: a healthy sandbox can be caught once by chance + ; Second hit on the same opcode. It is not coming back — abandon the interrupt frame + ; entirely and re-enter the driver at the stuck exit. + jml @stuck_entry +@nmi_out: + rep #$30 + .a16 + .i16 + .a16 + .i16 + pla + rti +@body: + rep #$30 + .a16 + .i16 + phk + plb + ; Install the NMI handler and arm VBlank NMI. NMI ignores the I flag, so an opcode inside + ; the sandbox that runs SEI cannot disarm the thing that rescues it. + rep #$20 + .a16 + lda #@nmi + sta a:V_NMI_VEC + ; Counters, and a poisoned first-bad so 'nothing failed' is distinguishable from 'the + ; sweep never ran'. + ; STZ has no long-addressing form, so every clear here is an explicit LDA #$00 + STA. + sep #$20 + .a8 + lda #$00 + sta f:$7E6106 + sta f:$7E6107 + sta f:$7E6108 + sta f:$7E6104 + lda #$FF + sta f:$7E6109 + ; Copy both stubs into the fixed WRAM addresses the terminators jump to. + rep #$30 + .a16 + .i16 + ldx #$0000 +@copy_ok: + sep #$20 + .a8 + lda f:a6_15_stub_ok,x + sta f:$7EAAAA,x + rep #$30 + .a16 + .i16 + inx + cpx #$0013 + bne @copy_ok + rep #$30 + .a16 + .i16 + ldx #$0000 +@copy_over: + sep #$20 + .a8 + lda f:a6_15_stub_over,x + sta f:$7EB8B8,x + rep #$30 + .a16 + .i16 + inx + cpx #$0013 + bne @copy_over + ; Patch each JML's target. It cannot be assembled into the .byte table: that table lives in + ; the data segment, where this proc's cheap-local labels are out of scope. + sep #$20 + .a8 + lda #.lobyte(@ok_entry) + sta f:$7EAABA + lda #.hibyte(@ok_entry) + sta f:$7EAABB + sep #$20 + .a8 + lda #.lobyte(@over_entry) + sta f:$7EB8C8 + lda #.hibyte(@over_entry) + sta f:$7EB8C9 + sep #$20 + .a8 + lda #$80 + sta $4200 ; VBlank NMI on — the watchdog's clock + rep #$30 + .a16 + .i16 + ldx #$0000 +@oploop: + ; len = a6_15_len[X]. Zero means the opcode leaves the sandbox and is not executed. + sep #$20 + .a8 + lda f:a6_15_len,x + sta f:$7E6101 + ; Inverted over a JMP: the body between here and @next is far beyond a branch's reach. + bne :+ + jmp @next + : + rep #$20 + .a16 + txa + sep #$20 + .a8 + sta f:$7E6100 + ; Copy the four encoded bytes; only the first `len` of them are reached, and the rest are + ; overwritten by the terminator. + rep #$30 + .a16 + .i16 + phx + rep #$30 + .a16 + .i16 + plx + phx + sep #$20 + .a8 + lda f:a6_15_b0,x + rep #$30 + .a16 + .i16 + ldx #$6000 + sep #$20 + .a8 + sta f:$7E0000,x + rep #$30 + .a16 + .i16 + plx + phx + sep #$20 + .a8 + lda f:a6_15_b1,x + rep #$30 + .a16 + .i16 + ldx #$6001 + sep #$20 + .a8 + sta f:$7E0000,x + rep #$30 + .a16 + .i16 + plx + phx + sep #$20 + .a8 + lda f:a6_15_b2,x + rep #$30 + .a16 + .i16 + ldx #$6002 + sep #$20 + .a8 + sta f:$7E0000,x + rep #$30 + .a16 + .i16 + plx + phx + sep #$20 + .a8 + lda f:a6_15_b3,x + rep #$30 + .a16 + .i16 + ldx #$6003 + sep #$20 + .a8 + sta f:$7E0000,x + rep #$30 + .a16 + .i16 + plx + ; The clean terminator at BUF+len, then NOP fill, then the overshoot terminator. + rep #$30 + .a16 + .i16 + phx + sep #$20 + .a8 + lda f:$7E6101 + rep #$30 + .a16 + .i16 + and #$00FF + clc + adc #$6000 + tax + sep #$20 + .a8 + lda #$4C + sta f:$7E0000,x + rep #$30 + .a16 + .i16 + inx + sep #$20 + .a8 + lda #$AA + sta f:$7E0000,x + rep #$30 + .a16 + .i16 + inx + sep #$20 + .a8 + lda #$AA + sta f:$7E0000,x + ; Fill from there to the overshoot terminator with NOP, then write it. + rep #$30 + .a16 + .i16 + inx +@fill: + cpx #$6010 + bcs @filled + sep #$20 + .a8 + lda #$EA + sta f:$7E0000,x + rep #$30 + .a16 + .i16 + inx + bra @fill +@filled: + sep #$20 + .a8 + lda #$4C + sta f:$7E6010 + lda #$B8 + sta f:$7E6011 + lda #$B8 + sta f:$7E6012 + rep #$30 + .a16 + .i16 + plx + ; Seed the operand window: the indirect pointers at D+$10 (16-bit) and D+$12 (24-bit) both + ; point back into the window, so an indirect load cannot reach MMIO or the sandbox. + rep #$30 + .a16 + .i16 + lda #$5000 + sta f:$7E0210 + sta f:$7E0212 + sep #$20 + .a8 + lda #$7E + sta f:$7E0214 + ; Save X across the run — a great many opcodes clobber it — and mark the sandbox active. + ; Through A: only the accumulator has long addressing, so X cannot be saved directly. + rep #$30 + .a16 + .i16 + txa + sta f:$7E610A + sep #$20 + .a8 + lda #$01 + sta f:$7E6104 + lda #$00 + sta f:$7E6105 + rep #$30 + .a16 + .i16 + tsc + sta f:$7E6102 + ; The preamble is the whole of the danger handling. A = 0 makes MVN/MVP a one-byte move; + ; CLC makes XCE a no-op in native mode; DBR = $7E keeps every absolute operand in WRAM; + ; D = $0200 puts direct-page operands in the low-WRAM mirror, clear of the runtime's own. + lda #$0200 + tcd + sep #$20 + .a8 + lda #$7E + pha + plb + rep #$30 + .a16 + .i16 + lda #$0000 + ldx #$0000 + ldy #$0000 + sep #$30 + .a8 + .i8 + .a8 + .i8 + cld + clc + jml $7E6000 +@ok_entry: + rep #$30 + .a16 + .i16 + .a16 + .i16 + phk + plb + sep #$20 + .a8 + lda #$00 + sta f:$7E6104 + lda f:$7E6106 + inc a + sta f:$7E6106 + rep #$30 + .a16 + .i16 + lda f:$7E610A + tax + jmp @next +@over_entry: + rep #$30 + .a16 + .i16 + .a16 + .i16 + phk + plb + sep #$20 + .a8 + lda #$00 + sta f:$7E6104 + lda f:$7E6107 + inc a + sta f:$7E6107 + jsr @note_bad + rep #$30 + .a16 + .i16 + lda f:$7E610A + tax + jmp @next +@stuck_entry: + ; Entered from the NMI handler, so the stack still holds an interrupt frame. Rebuilding + ; the machine from saved state rather than returning is the point. + rep #$30 + .a16 + .i16 + .a16 + .i16 + lda f:$7E6102 + tcs + lda #$0000 + tcd + phk + plb + sep #$20 + .a8 + cld + lda #$00 + sta f:$7E6104 + lda f:$7E6108 + inc a + sta f:$7E6108 + jsr @note_bad + rep #$30 + .a16 + .i16 + lda f:$7E610A + tax + jmp @next +@note_bad: + sep #$20 + .a8 + lda f:$7E6109 + cmp #$FF + bne :+ + lda f:$7E6100 + sta f:$7E6109 + : + rts +@next: + rep #$30 + .a16 + .i16 + inx + cpx #$0100 + beq :+ + jmp @oploop + : + ; Disarm before asserting: a failure exits immediately and must not leave NMI armed for + ; whatever runs next. + sep #$20 + .a8 + stz $4200 + lda $4210 ; clear any pending RDNMI latch + rep #$30 + .a16 + .i16 + lda f:$7E6106 + and #$00FF + ; record slot 283: A6.15 opcodes that returned at their documented length + sta f:$7EE436 + lda f:$7E6107 + and #$00FF + ; record slot 284: A6.15 opcodes that returned LATE (expect 0) + sta f:$7EE438 + lda f:$7E6108 + and #$00FF + ; record slot 285: A6.15 opcodes that did not return (expect 0) + sta f:$7EE43A + lda f:$7E6109 + and #$00FF + ; record slot 286: A6.15 first opcode that was not clean ($FF = none) + sta f:$7EE43C + ; Liveness first, and for the same reason E2.10 checks it first: a driver that fell over + ; early would report no late and no stuck opcodes, and two zeros would read as a pass. + lda f:$7E6106 + and #$00FF + cmp #$00F1 + bcs :+ + jmp @fail1 + : + cmp #$00F2 + bcc :+ + jmp @fail1 + : + ; The row: nothing hung, and nothing consumed a different number of bytes than the WDC + ; table documents. STP is excluded by name and is the only opcode this cart cannot run. + lda f:$7E6107 + and #$00FF + clc + adc f:$7E6108 + and #$00FF + cmp #$0000 + bcs :+ + jmp @fail2 + : + cmp #$0001 + bcc :+ + jmp @fail2 + : + sep #$20 + .a8 + lda #$01 + sta f:$7EE010 + jml test_restore +@fail1: + ; the sweep did not execute 241 opcodes cleanly — it stopped early, or it covered a different set than the 15 documented control-transfer exclusions. Slots 283-286 hold the three counts and the first opcode that was not clean + sep #$20 + .a8 + lda #$02 + sta f:$7EE010 + jml test_restore +@fail2: + ; at least one opcode either failed to return or advanced PC by a different number of bytes than Table 5-4 documents for it. Slot 284 counts the late ones, 285 the ones that never came back, and 286 names the first + sep #$20 + .a8 + lda #$04 + sta f:$7EE010 + jml test_restore +.endproc + ; B1.01 — MEMSEL selects FastROM ; provenance: Documented (SNESdev Wiki, Memory map / timing; fullsnes) .proc test_b1_01 @@ -32998,6 +33486,111 @@ CATALOG_IMPL = 1 .endproc .segment "APUDATA" +a6_15_len: + .byte $00,$02,$00,$02,$02,$02,$02,$02,$01,$02,$01,$01,$03,$03,$03,$04 + .byte $02,$02,$02,$02,$02,$02,$02,$02,$01,$03,$01,$01,$03,$03,$03,$04 + .byte $00,$02,$00,$02,$02,$02,$02,$02,$01,$02,$01,$01,$03,$03,$03,$04 + .byte $02,$02,$02,$02,$02,$02,$02,$02,$01,$03,$01,$01,$03,$03,$03,$04 + .byte $00,$02,$02,$02,$03,$02,$02,$02,$01,$02,$01,$01,$00,$03,$03,$04 + .byte $02,$02,$02,$02,$03,$02,$02,$02,$01,$03,$01,$01,$00,$03,$03,$04 + .byte $00,$02,$03,$02,$02,$02,$02,$02,$01,$02,$01,$00,$00,$03,$03,$04 + .byte $02,$02,$02,$02,$02,$02,$02,$02,$01,$03,$01,$01,$00,$03,$03,$04 + .byte $02,$02,$03,$02,$02,$02,$02,$02,$01,$02,$01,$01,$03,$03,$03,$04 + .byte $02,$02,$02,$02,$02,$02,$02,$02,$01,$03,$01,$01,$03,$03,$03,$04 + .byte $02,$02,$02,$02,$02,$02,$02,$02,$01,$02,$01,$01,$03,$03,$03,$04 + .byte $02,$02,$02,$02,$02,$02,$02,$02,$01,$03,$01,$01,$03,$03,$03,$04 + .byte $02,$02,$02,$02,$02,$02,$02,$02,$01,$02,$01,$00,$03,$03,$03,$04 + .byte $02,$02,$02,$02,$02,$02,$02,$02,$01,$03,$01,$00,$00,$03,$03,$04 + .byte $02,$02,$02,$02,$02,$02,$02,$02,$01,$02,$01,$01,$03,$03,$03,$04 + .byte $02,$02,$02,$02,$03,$02,$02,$02,$01,$03,$01,$01,$00,$03,$03,$04 +a6_15_b0: + .byte $00,$01,$02,$03,$04,$05,$06,$07,$08,$09,$0A,$0B,$0C,$0D,$0E,$0F + .byte $10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$1A,$1B,$1C,$1D,$1E,$1F + .byte $20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$2A,$2B,$2C,$2D,$2E,$2F + .byte $30,$31,$32,$33,$34,$35,$36,$37,$38,$39,$3A,$3B,$3C,$3D,$3E,$3F + .byte $40,$41,$42,$43,$44,$45,$46,$47,$48,$49,$4A,$4B,$4C,$4D,$4E,$4F + .byte $50,$51,$52,$53,$54,$55,$56,$57,$58,$59,$5A,$5B,$5C,$5D,$5E,$5F + .byte $60,$61,$62,$63,$64,$65,$66,$67,$68,$69,$6A,$6B,$6C,$6D,$6E,$6F + .byte $70,$71,$72,$73,$74,$75,$76,$77,$78,$79,$7A,$7B,$7C,$7D,$7E,$7F + .byte $80,$81,$82,$83,$84,$85,$86,$87,$88,$89,$8A,$8B,$8C,$8D,$8E,$8F + .byte $90,$91,$92,$93,$94,$95,$96,$97,$98,$99,$9A,$9B,$9C,$9D,$9E,$9F + .byte $A0,$A1,$A2,$A3,$A4,$A5,$A6,$A7,$A8,$A9,$AA,$AB,$AC,$AD,$AE,$AF + .byte $B0,$B1,$B2,$B3,$B4,$B5,$B6,$B7,$B8,$B9,$BA,$BB,$BC,$BD,$BE,$BF + .byte $C0,$C1,$C2,$C3,$C4,$C5,$C6,$C7,$C8,$C9,$CA,$CB,$CC,$CD,$CE,$CF + .byte $D0,$D1,$D2,$D3,$D4,$D5,$D6,$D7,$D8,$D9,$DA,$DB,$DC,$DD,$DE,$DF + .byte $E0,$E1,$E2,$E3,$E4,$E5,$E6,$E7,$E8,$E9,$EA,$EB,$EC,$ED,$EE,$EF + .byte $F0,$F1,$F2,$F3,$F4,$F5,$F6,$F7,$F8,$F9,$FA,$FB,$FC,$FD,$FE,$FF +a6_15_b1: + .byte $00,$10,$00,$10,$10,$10,$10,$10,$EA,$00,$EA,$EA,$00,$00,$00,$00 + .byte $00,$10,$10,$10,$10,$10,$10,$10,$EA,$00,$EA,$EA,$00,$00,$00,$00 + .byte $00,$10,$00,$10,$10,$10,$10,$10,$EA,$00,$EA,$EA,$00,$00,$00,$00 + .byte $00,$10,$10,$10,$10,$10,$10,$10,$EA,$00,$EA,$EA,$00,$00,$00,$00 + .byte $EA,$10,$00,$10,$7E,$10,$10,$10,$EA,$00,$EA,$EA,$00,$00,$00,$00 + .byte $00,$10,$10,$10,$7E,$10,$10,$10,$EA,$00,$EA,$EA,$00,$00,$00,$00 + .byte $EA,$10,$00,$10,$10,$10,$10,$10,$EA,$00,$EA,$EA,$00,$00,$00,$00 + .byte $00,$10,$10,$10,$10,$10,$10,$10,$EA,$00,$EA,$EA,$00,$00,$00,$00 + .byte $00,$10,$00,$10,$10,$10,$10,$10,$EA,$00,$EA,$EA,$00,$00,$00,$00 + .byte $00,$10,$10,$10,$10,$10,$10,$10,$EA,$00,$EA,$EA,$00,$00,$00,$00 + .byte $00,$10,$00,$10,$10,$10,$10,$10,$EA,$00,$EA,$EA,$00,$00,$00,$00 + .byte $00,$10,$10,$10,$10,$10,$10,$10,$EA,$00,$EA,$EA,$00,$00,$00,$00 + .byte $00,$10,$00,$10,$10,$10,$10,$10,$EA,$00,$EA,$EA,$00,$00,$00,$00 + .byte $00,$10,$10,$10,$10,$10,$10,$10,$EA,$00,$EA,$EA,$00,$00,$00,$00 + .byte $00,$10,$00,$10,$10,$10,$10,$10,$EA,$00,$EA,$EA,$00,$00,$00,$00 + .byte $00,$10,$10,$10,$00,$10,$10,$10,$EA,$00,$EA,$EA,$00,$00,$00,$00 +a6_15_b2: + .byte $EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$50,$50,$50,$50 + .byte $EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$50,$EA,$EA,$50,$50,$50,$50 + .byte $50,$EA,$50,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$50,$50,$50,$50 + .byte $EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$50,$EA,$EA,$50,$50,$50,$50 + .byte $EA,$EA,$EA,$EA,$7E,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$50,$50,$50,$50 + .byte $EA,$EA,$EA,$EA,$7E,$EA,$EA,$EA,$EA,$50,$EA,$EA,$50,$50,$50,$50 + .byte $EA,$EA,$00,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$50,$50,$50,$50 + .byte $EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$50,$EA,$EA,$50,$50,$50,$50 + .byte $EA,$EA,$00,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$50,$50,$50,$50 + .byte $EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$50,$EA,$EA,$50,$50,$50,$50 + .byte $EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$50,$50,$50,$50 + .byte $EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$50,$EA,$EA,$50,$50,$50,$50 + .byte $EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$50,$50,$50,$50 + .byte $EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$50,$EA,$EA,$50,$50,$50,$50 + .byte $EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$50,$50,$50,$50 + .byte $EA,$EA,$EA,$EA,$50,$EA,$EA,$EA,$EA,$50,$EA,$EA,$50,$50,$50,$50 +a6_15_b3: + .byte $EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$7E + .byte $EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$7E + .byte $EA,$EA,$7E,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$7E + .byte $EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$7E + .byte $EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$7E + .byte $EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$7E,$EA,$EA,$7E + .byte $EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$7E + .byte $EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$7E + .byte $EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$7E + .byte $EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$7E + .byte $EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$7E + .byte $EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$7E + .byte $EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$7E + .byte $EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$7E + .byte $EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$7E + .byte $EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$EA,$7E +a6_15_stub_ok: + .byte $E2,$30 ; SEP #$30 + .byte $D8 ; CLD + .byte $18 ; CLC + .byte $C2,$30 ; REP #$30 + .byte $AF,$02,$61,$7E ; LDA $7E6102 — the saved stack pointer + .byte $1B ; TCS + .byte $A9,$00,$00 ; LDA #$0000 + .byte $5B ; TCD — the runtime's variables live at D = 0 + .byte $5C,$00,$00,$00 ; JML — the target is patched in below +a6_15_stub_over: + .byte $E2,$30 ; SEP #$30 + .byte $D8 ; CLD + .byte $18 ; CLC + .byte $C2,$30 ; REP #$30 + .byte $AF,$02,$61,$7E ; LDA $7E6102 — the saved stack pointer + .byte $1B ; TCS + .byte $A9,$00,$00 ; LDA #$0000 + .byte $5B ; TCD — the runtime's variables live at D = 0 + .byte $5C,$00,$00,$00 ; JML — the target is patched in below apu_prog_0: .byte $CD, $EF, $BD, $E5, $00, $80, $C4, $F5, $E5, $20, $80, $C4 .byte $F6, $E5, $40, $80, $C4, $F7, $E8, $5A, $C4, $F4, $E4, $F4 @@ -35430,6 +36023,7 @@ apu_prog_120: .byte $C5, $8F, $80, $F1, $E4, $08, $C4, $F5, $E4, $07, $C4, $F6 .byte $E4, $09, $C4, $F7, $E8, $5A, $C4, $F4, $E4, $F4, $68, $A5 .byte $D0, $FA, $E8, $80, $C4, $F1, $5F, $C0, $FF +.segment "APUDATA2" apu_prog_121: .byte $5F, $0C, $02, $83, $79, $79, $79, $79, $79, $79, $79, $79 .byte $CD, $EF, $BD, $E8, $03, $C5, $00, $01, $E8, $02, $C5, $01 @@ -35464,7 +36058,7 @@ apu_prog_121: .export _test_flags _test_count: - .word 345 + .word 346 ; Entry points, 24-bit: test bodies no longer all live in bank $00. _test_entries: @@ -35549,6 +36143,7 @@ _test_entries: .faraddr test_a6_13 .faraddr test_a6_14 .faraddr test_a2_13 + .faraddr test_a6_15 .faraddr test_c1_01 .faraddr test_c1_02 .faraddr test_c1_03 @@ -35897,6 +36492,7 @@ _test_flags: .byte $01 ; A6.13 .byte $01 ; A6.14 .byte $02 ; A2.13 + .byte $01 ; A6.15 .byte $01 ; C1.01 .byte $01 ; C1.02 .byte $01 ; C1.03 @@ -36245,6 +36841,7 @@ _test_names: .addr @n_a6_13 .addr @n_a6_14 .addr @n_a2_13 + .addr @n_a6_15 .addr @n_c1_01 .addr @n_c1_02 .addr @n_c1_03 @@ -36752,6 +37349,9 @@ _test_names: @n_a2_13: .byte 20 .byte "16-bit dp page cross" +@n_a6_15: + .byte 23 + .byte "all 256 opcodes defined" @n_c1_01: .byte 19 .byte "OAM word write/read" @@ -37776,7 +38376,7 @@ _page_len: .byte 10 .byte 7 .byte 10 - .byte 4 + .byte 5 .byte 5 .byte 7 .byte 4 @@ -37831,47 +38431,47 @@ _page_off: .word 79 .word 86 .word 96 - .word 100 - .word 105 - .word 112 - .word 116 - .word 125 - .word 133 - .word 143 - .word 153 + .word 101 + .word 106 + .word 113 + .word 117 + .word 126 + .word 134 + .word 144 .word 154 - .word 157 - .word 160 - .word 164 - .word 166 - .word 170 - .word 176 - .word 186 - .word 189 - .word 194 + .word 155 + .word 158 + .word 161 + .word 165 + .word 167 + .word 171 + .word 177 + .word 187 + .word 190 .word 195 - .word 205 - .word 210 - .word 216 - .word 223 - .word 233 - .word 237 - .word 247 - .word 251 - .word 261 + .word 196 + .word 206 + .word 211 + .word 217 + .word 224 + .word 234 + .word 238 + .word 248 + .word 252 .word 262 - .word 268 - .word 270 - .word 280 - .word 284 - .word 294 - .word 297 - .word 307 - .word 314 - .word 321 - .word 331 - .word 334 - .word 344 + .word 263 + .word 269 + .word 271 + .word 281 + .word 285 + .word 295 + .word 298 + .word 308 + .word 315 + .word 322 + .word 332 + .word 335 + .word 345 _page_tests: .word 0 @@ -37925,7 +38525,6 @@ _page_tests: .word 70 .word 71 .word 72 - .word 310 .word 311 .word 312 .word 313 @@ -37960,6 +38559,7 @@ _page_tests: .word 342 .word 343 .word 344 + .word 345 .word 30 .word 31 .word 32 @@ -37974,6 +38574,7 @@ _page_tests: .word 75 .word 78 .word 79 + .word 81 .word 38 .word 39 .word 40 @@ -37990,34 +38591,33 @@ _page_tests: .word 46 .word 51 .word 53 - .word 81 .word 82 .word 83 .word 84 .word 85 .word 86 .word 87 - .word 123 - .word 125 .word 88 - .word 115 + .word 124 + .word 126 + .word 89 .word 116 .word 117 .word 118 .word 119 .word 120 .word 121 - .word 89 + .word 122 .word 90 .word 91 .word 92 .word 93 .word 94 .word 95 - .word 122 - .word 124 - .word 128 .word 96 + .word 123 + .word 125 + .word 129 .word 97 .word 98 .word 99 @@ -38026,9 +38626,9 @@ _page_tests: .word 102 .word 103 .word 104 - .word 129 - .word 130 .word 105 + .word 130 + .word 131 .word 106 .word 107 .word 108 @@ -38038,38 +38638,38 @@ _page_tests: .word 112 .word 113 .word 114 - .word 126 + .word 115 .word 127 - .word 131 + .word 128 .word 132 - .word 148 - .word 149 .word 133 - .word 135 + .word 149 .word 150 - .word 151 - .word 153 - .word 281 .word 134 .word 136 + .word 151 + .word 152 + .word 154 + .word 282 + .word 135 .word 137 .word 138 .word 139 .word 140 .word 141 .word 142 - .word 152 - .word 154 + .word 143 + .word 153 .word 155 - .word 157 + .word 156 .word 158 - .word 143 + .word 159 .word 144 .word 145 .word 146 .word 147 - .word 156 - .word 159 + .word 148 + .word 157 .word 160 .word 161 .word 162 @@ -38078,26 +38678,26 @@ _page_tests: .word 165 .word 166 .word 167 - .word 172 + .word 168 .word 173 .word 174 .word 175 .word 176 .word 177 - .word 168 + .word 178 .word 169 .word 170 .word 171 - .word 178 + .word 172 .word 179 .word 180 - .word 228 + .word 181 .word 229 .word 230 .word 231 .word 232 .word 233 - .word 181 + .word 234 .word 182 .word 183 .word 184 @@ -38105,49 +38705,49 @@ _page_tests: .word 186 .word 187 .word 188 - .word 199 - .word 213 + .word 189 + .word 200 .word 214 - .word 221 + .word 215 .word 222 .word 223 - .word 189 + .word 224 .word 190 .word 191 .word 192 - .word 201 + .word 193 .word 202 - .word 217 + .word 203 .word 218 .word 219 .word 220 - .word 278 + .word 221 .word 279 .word 280 - .word 282 - .word 193 + .word 281 + .word 283 .word 194 - .word 200 - .word 215 + .word 195 + .word 201 .word 216 - .word 224 + .word 217 .word 225 .word 226 .word 227 - .word 283 + .word 228 .word 284 - .word 195 + .word 285 .word 196 - .word 274 + .word 197 .word 275 .word 276 .word 277 - .word 197 + .word 278 .word 198 - .word 203 + .word 199 .word 204 .word 205 - .word 239 + .word 206 .word 240 .word 241 .word 242 @@ -38157,23 +38757,23 @@ _page_tests: .word 246 .word 247 .word 248 - .word 285 - .word 206 + .word 249 + .word 286 .word 207 .word 208 .word 209 .word 210 .word 211 - .word 234 + .word 212 .word 235 - .word 249 + .word 236 .word 250 .word 251 .word 252 .word 253 - .word 212 - .word 236 .word 254 + .word 213 + .word 237 .word 255 .word 256 .word 257 @@ -38185,17 +38785,17 @@ _page_tests: .word 263 .word 264 .word 265 - .word 271 + .word 266 .word 272 .word 273 - .word 237 + .word 274 .word 238 - .word 266 + .word 239 .word 267 .word 268 .word 269 .word 270 - .word 286 + .word 271 .word 287 .word 288 .word 289 @@ -38219,3 +38819,4 @@ _page_tests: .word 307 .word 308 .word 309 + .word 310 diff --git a/tests/roms/AccuracySNES/build/accuracysnes-pal.sfc b/tests/roms/AccuracySNES/build/accuracysnes-pal.sfc index bd9621a30851b62ae52f1913eab25948348831e2..baae12b73bc37181c60d26e098aaecbdf9228000 100644 GIT binary patch delta 6956 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 delta 5177 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 a#zgv7N5g{7HPNV5K8@ATi(%fO=Klp=D=lXL diff --git a/tests/roms/AccuracySNES/gen/src/cpu_opcodes.rs b/tests/roms/AccuracySNES/gen/src/cpu_opcodes.rs new file mode 100644 index 00000000..3d2c3737 --- /dev/null +++ b/tests/roms/AccuracySNES/gen/src/cpu_opcodes.rs @@ -0,0 +1,475 @@ +//! The documented 65C816 opcode map — addressing mode, length, and whether the opcode continues +//! the straight line. +//! +//! # Where the numbers come from +//! +//! **Table 5-4** of the WDC W65C816S datasheet (`ref-docs/2026-07-20-wdc-w65c816s-citation.md`) is +//! the opcode matrix: mnemonic, addressing-mode symbol, cycles and bytes for all 256 opcodes. +//! Lengths here are that table's byte counts, and the addressing modes are its symbols. Nothing is +//! read out of `crates/rustysnes-cpu/` — `A6.15` is a scored row, so its expectation has to come +//! from outside the thing being tested. +//! +//! # Why the map is built from rules +//! +//! The 65C816 matrix is columnar, and the datasheet presents it that way: the eight ALU operations +//! share one fifteen-entry addressing-mode column, the read-modify-writes share another, the +//! conditional branches occupy `$10` apart in the `$x0` column. Typing 256 rows out by hand +//! introduces transcription errors the rules cannot have. [`table`] asserts that the rules filled +//! all 256 slots **exactly once**, so a rule that overlaps another or misses a slot fails the build +//! rather than shipping a hole — which for this row would be a hole in the very claim it makes. +//! +//! # Lengths are stated at `m = 1, x = 1` +//! +//! Immediate operands are one byte or two depending on the `m` and `x` flags, so a length table is +//! meaningless without pinning them. Everything here is the 8-bit-accumulator, 8-bit-index case, +//! which is what `sweep.rs`'s sandbox establishes with `sep #$30` — and which it had to learn the +//! hard way, having first set only `sep #$20` and measured `LDX #imm` as a three-byte fetch against +//! a two-byte expectation. + +/// Whether an opcode continues the straight line, and if not, why not. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Flow { + /// Executes and falls through to the next instruction. + Straight, + /// A relative branch. With a displacement of zero the taken path lands on the following + /// instruction, which is where a not-taken branch would have gone — so either way the sandbox + /// resumes at the same address and the length claim is still what is being tested. + Branch, + /// Leaves the sandbox, with the reason. These are the opcodes for which "the following + /// instruction" is not a thing that exists. + Leaves(&'static str), +} + +/// The addressing mode, which is what decides the length. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Mode { + /// No operand: implied, accumulator, or stack. + Implied, + /// One immediate byte (at `m = 1` / `x = 1`). + Immediate, + /// A direct-page offset, indexed or not, direct or indirect. + Direct, + /// A 16-bit absolute address, indexed or not. + Absolute, + /// A 24-bit absolute long address. + Long, + /// An 8-bit stack-relative offset. + StackRelative, + /// An 8-bit signed displacement. + Relative, + /// A 16-bit signed displacement (`BRL`, `PER`). + RelativeLong, + /// Two direct-page-ish bytes: `MVN`/`MVP`'s source and destination banks. + BlockMove, +} + +impl Mode { + /// Instruction length in bytes, opcode included, at `m = 1` and `x = 1`. + #[must_use] + pub const fn len(self) -> u8 { + match self { + Self::Implied => 1, + Self::Immediate | Self::Direct | Self::StackRelative | Self::Relative => 2, + Self::Absolute | Self::RelativeLong | Self::BlockMove => 3, + Self::Long => 4, + } + } +} + +/// One opcode's documented shape. +#[derive(Clone, Copy, Debug)] +pub struct Op { + /// The opcode byte. + pub code: u8, + /// Its addressing mode, which decides [`Op::len`]. + pub mode: Mode, + /// Whether it continues the straight line. + pub flow: Flow, + /// Mnemonic plus mode, for the generated failure text and the catalog. + pub name: &'static str, +} + +impl Op { + /// Instruction length in bytes at `m = 1`, `x = 1`. + #[must_use] + pub const fn len(self) -> u8 { + self.mode.len() + } +} + +/// The eight operations sharing the ALU addressing-mode column, by the high nibble they base at. +const ALU: [(u8, &str); 8] = [ + (0x00, "ORA"), + (0x20, "AND"), + (0x40, "EOR"), + (0x60, "ADC"), + (0x80, "STA"), + (0xA0, "LDA"), + (0xC0, "CMP"), + (0xE0, "SBC"), +]; + +/// The ALU column: `(offset from the base, mode, suffix)`. +/// +/// `$09` — the immediate — is deliberately absent: `STA` has no immediate form and `$89` is +/// `BIT #`, so the column does not actually tile there. Handled in [`SINGLES`], one row each. +const ALU_MODES: [(u8, Mode, &str); 14] = [ + (0x01, Mode::Direct, "(dp,X)"), + (0x03, Mode::StackRelative, "sr,S"), + (0x05, Mode::Direct, "dp"), + (0x07, Mode::Direct, "[dp]"), + (0x0D, Mode::Absolute, "abs"), + (0x0F, Mode::Long, "long"), + (0x11, Mode::Direct, "(dp),Y"), + (0x12, Mode::Direct, "(dp)"), + (0x13, Mode::StackRelative, "(sr,S),Y"), + (0x15, Mode::Direct, "dp,X"), + (0x17, Mode::Direct, "[dp],Y"), + (0x19, Mode::Absolute, "abs,Y"), + (0x1D, Mode::Absolute, "abs,X"), + (0x1F, Mode::Long, "long,X"), +]; + +/// The read-modify-write group, by the high nibble it bases at. `DEC`/`INC` are the two whose +/// accumulator form sits at `$3A`/`$1A` rather than in this column, so they carry only four entries +/// here and their odd ones out are in [`SINGLES`]. +const RMW: [(u8, &str); 6] = [ + (0x00, "ASL"), + (0x20, "ROL"), + (0x40, "LSR"), + (0x60, "ROR"), + (0xC0, "DEC"), + (0xE0, "INC"), +]; + +/// The read-modify-write column: `(offset, mode, suffix)`. +const RMW_MODES: [(u8, Mode, &str); 4] = [ + (0x06, Mode::Direct, "dp"), + (0x0E, Mode::Absolute, "abs"), + (0x16, Mode::Direct, "dp,X"), + (0x1E, Mode::Absolute, "abs,X"), +]; + +/// The eight conditional branches, `$10` apart in the `$x0` column. +const BRANCHES: [(u8, &str); 8] = [ + (0x10, "BPL"), + (0x30, "BMI"), + (0x50, "BVC"), + (0x70, "BVS"), + (0x90, "BCC"), + (0xB0, "BCS"), + (0xD0, "BNE"), + (0xF0, "BEQ"), +]; + +/// Everything the columns do not cover. +/// +/// `(code, mode, flow, name)`, grouped the way the datasheet groups them so a run can be checked +/// against one block of Table 5-4 at a time. +const SINGLES: &[(u8, Mode, Flow, &str)] = &[ + // The ALU immediates. `$89` is `BIT #`, not a store — the one place the ALU column does not + // tile, and the reason `$09` is not in `ALU_MODES`. + (0x09, Mode::Immediate, Flow::Straight, "ORA #"), + (0x29, Mode::Immediate, Flow::Straight, "AND #"), + (0x49, Mode::Immediate, Flow::Straight, "EOR #"), + (0x69, Mode::Immediate, Flow::Straight, "ADC #"), + (0x89, Mode::Immediate, Flow::Straight, "BIT #"), + (0xA9, Mode::Immediate, Flow::Straight, "LDA #"), + (0xC9, Mode::Immediate, Flow::Straight, "CMP #"), + (0xE9, Mode::Immediate, Flow::Straight, "SBC #"), + // The accumulator forms of the shift and increment group. + (0x0A, Mode::Implied, Flow::Straight, "ASL A"), + (0x2A, Mode::Implied, Flow::Straight, "ROL A"), + (0x4A, Mode::Implied, Flow::Straight, "LSR A"), + (0x6A, Mode::Implied, Flow::Straight, "ROR A"), + (0x1A, Mode::Implied, Flow::Straight, "INC A"), + (0x3A, Mode::Implied, Flow::Straight, "DEC A"), + // `STZ`, which has no eight-way base of its own. + (0x64, Mode::Direct, Flow::Straight, "STZ dp"), + (0x74, Mode::Direct, Flow::Straight, "STZ dp,X"), + (0x9C, Mode::Absolute, Flow::Straight, "STZ abs"), + (0x9E, Mode::Absolute, Flow::Straight, "STZ abs,X"), + // `BIT`, `TRB`, `TSB`. + (0x24, Mode::Direct, Flow::Straight, "BIT dp"), + (0x2C, Mode::Absolute, Flow::Straight, "BIT abs"), + (0x34, Mode::Direct, Flow::Straight, "BIT dp,X"), + (0x3C, Mode::Absolute, Flow::Straight, "BIT abs,X"), + (0x14, Mode::Direct, Flow::Straight, "TRB dp"), + (0x1C, Mode::Absolute, Flow::Straight, "TRB abs"), + (0x04, Mode::Direct, Flow::Straight, "TSB dp"), + (0x0C, Mode::Absolute, Flow::Straight, "TSB abs"), + // Index-register loads, stores and compares. + (0xA0, Mode::Immediate, Flow::Straight, "LDY #"), + (0xA4, Mode::Direct, Flow::Straight, "LDY dp"), + (0xAC, Mode::Absolute, Flow::Straight, "LDY abs"), + (0xB4, Mode::Direct, Flow::Straight, "LDY dp,X"), + (0xBC, Mode::Absolute, Flow::Straight, "LDY abs,X"), + (0xA2, Mode::Immediate, Flow::Straight, "LDX #"), + (0xA6, Mode::Direct, Flow::Straight, "LDX dp"), + (0xAE, Mode::Absolute, Flow::Straight, "LDX abs"), + (0xB6, Mode::Direct, Flow::Straight, "LDX dp,Y"), + (0xBE, Mode::Absolute, Flow::Straight, "LDX abs,Y"), + (0x84, Mode::Direct, Flow::Straight, "STY dp"), + (0x8C, Mode::Absolute, Flow::Straight, "STY abs"), + (0x94, Mode::Direct, Flow::Straight, "STY dp,X"), + (0x86, Mode::Direct, Flow::Straight, "STX dp"), + (0x8E, Mode::Absolute, Flow::Straight, "STX abs"), + (0x96, Mode::Direct, Flow::Straight, "STX dp,Y"), + (0xC0, Mode::Immediate, Flow::Straight, "CPY #"), + (0xC4, Mode::Direct, Flow::Straight, "CPY dp"), + (0xCC, Mode::Absolute, Flow::Straight, "CPY abs"), + (0xE0, Mode::Immediate, Flow::Straight, "CPX #"), + (0xE4, Mode::Direct, Flow::Straight, "CPX dp"), + (0xEC, Mode::Absolute, Flow::Straight, "CPX abs"), + // Register transfers, increments and decrements. + (0xAA, Mode::Implied, Flow::Straight, "TAX"), + (0xA8, Mode::Implied, Flow::Straight, "TAY"), + (0x8A, Mode::Implied, Flow::Straight, "TXA"), + (0x98, Mode::Implied, Flow::Straight, "TYA"), + (0xBA, Mode::Implied, Flow::Straight, "TSX"), + (0x9A, Mode::Implied, Flow::Straight, "TXS"), + (0x9B, Mode::Implied, Flow::Straight, "TXY"), + (0xBB, Mode::Implied, Flow::Straight, "TYX"), + (0x5B, Mode::Implied, Flow::Straight, "TCD"), + (0x7B, Mode::Implied, Flow::Straight, "TDC"), + (0x1B, Mode::Implied, Flow::Straight, "TCS"), + (0x3B, Mode::Implied, Flow::Straight, "TSC"), + (0xE8, Mode::Implied, Flow::Straight, "INX"), + (0xC8, Mode::Implied, Flow::Straight, "INY"), + (0xCA, Mode::Implied, Flow::Straight, "DEX"), + (0x88, Mode::Implied, Flow::Straight, "DEY"), + // Flags. + (0x18, Mode::Implied, Flow::Straight, "CLC"), + (0x38, Mode::Implied, Flow::Straight, "SEC"), + (0x58, Mode::Implied, Flow::Straight, "CLI"), + (0x78, Mode::Implied, Flow::Straight, "SEI"), + (0xB8, Mode::Implied, Flow::Straight, "CLV"), + (0xD8, Mode::Implied, Flow::Straight, "CLD"), + (0xF8, Mode::Implied, Flow::Straight, "SED"), + (0xC2, Mode::Immediate, Flow::Straight, "REP #"), + (0xE2, Mode::Immediate, Flow::Straight, "SEP #"), + (0xFB, Mode::Implied, Flow::Straight, "XCE"), + // Stack. + (0x48, Mode::Implied, Flow::Straight, "PHA"), + (0x68, Mode::Implied, Flow::Straight, "PLA"), + (0xDA, Mode::Implied, Flow::Straight, "PHX"), + (0xFA, Mode::Implied, Flow::Straight, "PLX"), + (0x5A, Mode::Implied, Flow::Straight, "PHY"), + (0x7A, Mode::Implied, Flow::Straight, "PLY"), + (0x08, Mode::Implied, Flow::Straight, "PHP"), + (0x28, Mode::Implied, Flow::Straight, "PLP"), + (0x8B, Mode::Implied, Flow::Straight, "PHB"), + (0xAB, Mode::Implied, Flow::Straight, "PLB"), + (0x0B, Mode::Implied, Flow::Straight, "PHD"), + (0x2B, Mode::Implied, Flow::Straight, "PLD"), + (0x4B, Mode::Implied, Flow::Straight, "PHK"), + (0xF4, Mode::Absolute, Flow::Straight, "PEA abs"), + (0xD4, Mode::Direct, Flow::Straight, "PEI dp"), + (0x62, Mode::RelativeLong, Flow::Straight, "PER rel16"), + // No-ops and the reserved two-byte no-op. + (0xEA, Mode::Implied, Flow::Straight, "NOP"), + (0xEB, Mode::Implied, Flow::Straight, "XBA"), + (0x42, Mode::Immediate, Flow::Straight, "WDM"), + // Block moves. Two operand bytes, and they repeat until the counter runs out — but they do + // return to the following instruction, so the length claim is testable. + (0x54, Mode::BlockMove, Flow::Straight, "MVN"), + (0x44, Mode::BlockMove, Flow::Straight, "MVP"), + // The conditional branches' unconditional relatives. + (0x80, Mode::Relative, Flow::Branch, "BRA"), + (0x82, Mode::RelativeLong, Flow::Branch, "BRL"), + // The opcodes that leave the sandbox. + (0x4C, Mode::Absolute, JUMP, "JMP abs"), + (0x6C, Mode::Absolute, JUMP, "JMP (abs)"), + (0x7C, Mode::Absolute, JUMP, "JMP (abs,X)"), + (0x5C, Mode::Long, JUMP, "JML long"), + (0xDC, Mode::Absolute, JUMP, "JML [abs]"), + (0x20, Mode::Absolute, CALL, "JSR abs"), + (0xFC, Mode::Absolute, CALL, "JSR (abs,X)"), + (0x22, Mode::Long, CALL, "JSL long"), + (0x60, Mode::Implied, RETURN, "RTS"), + (0x6B, Mode::Implied, RETURN, "RTL"), + (0x40, Mode::Implied, RETURN, "RTI"), + (0x00, Mode::Immediate, VECTOR, "BRK"), + (0x02, Mode::Immediate, VECTOR, "COP"), + (0xDB, Mode::Implied, STOPS, "STP"), + (0xCB, Mode::Implied, WAITS, "WAI"), +]; + +/// A jump's target is a fixed address the sandbox does not control per copy. +const JUMP: Flow = Flow::Leaves( + "a jump sets PC outright, so there is no `following instruction` for a length claim to be about", +); + +/// A call jumps and pushes. +const CALL: Flow = Flow::Leaves( + "a call jumps and pushes a return address; where it resumes is decided by the RTS that matches \ + it, not by this opcode's length", +); + +/// A return pops an address the sandbox did not push. +const RETURN: Flow = Flow::Leaves( + "a return pops an address the sandbox never pushed, and resumes there rather than after itself", +); + +/// The software interrupts vector away. +const VECTOR: Flow = Flow::Leaves( + "vectors through the cartridge's own handler, which is a different measurement — `A6.07` owns \ + the one about BRK's skipped signature byte", +); + +/// `STP` is the row's own named exception. +const STOPS: Flow = Flow::Leaves( + "halts the processor until RESET, which is exactly what this row asserts is the only opcode \ + that does so — a self-scoring battery that executed it would never report anything again", +); + +/// `WAI` needs an interrupt to come back. +const WAITS: Flow = Flow::Leaves( + "waits for an interrupt. It does resume at the following instruction once one arrives, so \ + unlike STP it is recoverable — but the wait is the subject and the length is not", +); + +/// The whole 256-entry map. +/// +/// # Panics +/// +/// If the rules leave a slot empty or fill one twice. The columns and [`SINGLES`] are two +/// independent descriptions of the same matrix, and a clash between them means one of the two is +/// wrong about the map's shape — which for this row would be an error inside the claim itself. +#[must_use] +pub fn table() -> Vec { + let mut slots: Vec> = vec![None; 256]; + + let mut put = |code: u8, mode: Mode, flow: Flow, name: String| { + let name: &'static str = Box::leak(name.into_boxed_str()); + assert!( + slots[code as usize].is_none(), + "opcode ${code:02X} is described twice — as {} and as {name}", + slots[code as usize].expect("checked").name + ); + slots[code as usize] = Some(Op { + code, + mode, + flow, + name, + }); + }; + + for (base, op) in ALU { + for (offset, mode, form) in ALU_MODES { + put(base + offset, mode, Flow::Straight, format!("{op} {form}")); + } + } + for (base, op) in RMW { + for (offset, mode, form) in RMW_MODES { + put(base + offset, mode, Flow::Straight, format!("{op} {form}")); + } + } + for (code, op) in BRANCHES { + put(code, Mode::Relative, Flow::Branch, format!("{op} rel")); + } + for &(code, mode, flow, name) in SINGLES { + put(code, mode, flow, name.to_owned()); + } + + let holes: Vec = slots + .iter() + .enumerate() + .filter(|(_, op)| op.is_none()) + .map(|(code, _)| format!("${code:02X}")) + .collect(); + assert!( + holes.is_empty(), + "{} opcode(s) have no description: {}", + holes.len(), + holes.join(" ") + ); + slots.into_iter().flatten().collect() +} + +#[cfg(test)] +mod tests { + use super::{Flow, Mode, table}; + + /// Every slot filled exactly once. The 65C816 has no undefined opcodes, which is the very + /// thing `A6.15` asserts — so a hole here would be a hole inside the claim. + #[test] + fn the_matrix_is_complete_and_has_no_overlaps() { + let t = table(); + assert_eq!(t.len(), 256); + for (i, op) in t.iter().enumerate() { + assert_eq!(usize::from(op.code), i); + assert!( + (1..=4).contains(&op.len()), + "${i:02X} has length {}", + op.len() + ); + } + } + + /// Fifteen opcodes leave the sandbox, and they are a named list rather than a count that can + /// drift. If this moves, the row's account of what it does not execute has to move with it. + #[test] + fn fifteen_opcodes_leave_the_sandbox() { + let leaves: Vec<_> = table() + .into_iter() + .filter(|op| matches!(op.flow, Flow::Leaves(_))) + .map(|op| op.name) + .collect(); + assert_eq!(leaves.len(), 15, "the list is {leaves:?}"); + for expected in [ + "JMP abs", + "JMP (abs)", + "JMP (abs,X)", + "JML long", + "JML [abs]", + "JSR abs", + "JSR (abs,X)", + "JSL long", + "RTS", + "RTL", + "RTI", + "BRK", + "COP", + "STP", + "WAI", + ] { + assert!( + leaves.contains(&expected), + "{expected} is not in {leaves:?}" + ); + } + } + + /// Spot-checks against Table 5-4, chosen where a columnar rule could plausibly be wrong: the + /// place the ALU column does *not* tile, the two long-relative operands, the block moves, and + /// the four-byte long modes. + #[test] + fn the_awkward_corners_match_the_datasheet() { + let t = table(); + assert_eq!(t[0x89].name, "BIT #", "$89 is BIT #, not a store"); + assert_eq!(t[0x82].mode, Mode::RelativeLong, "BRL is three bytes"); + assert_eq!(t[0x62].mode, Mode::RelativeLong, "PER is three bytes"); + assert_eq!(t[0x54].mode, Mode::BlockMove, "MVN carries two bank bytes"); + assert_eq!(t[0x44].mode, Mode::BlockMove, "MVP carries two bank bytes"); + assert_eq!(t[0x0F].len(), 4, "ORA long is four bytes"); + assert_eq!(t[0x1F].len(), 4, "ORA long,X is four bytes"); + assert_eq!(t[0x22].len(), 4, "JSL is four bytes"); + assert_eq!(t[0x5C].len(), 4, "JML is four bytes"); + assert_eq!(t[0xEA].name, "NOP"); + assert_eq!(t[0x42].len(), 2, "WDM is a reserved TWO-byte no-op"); + } + + /// Nine opcodes are four bytes: the eight ALU long forms' two columns less the ones that are + /// jumps, plus `JSL` and `JML`. Counting them is a cheap check that the long columns tiled. + #[test] + fn the_long_modes_are_where_they_should_be() { + let four: Vec<_> = table() + .into_iter() + .filter(|op| op.len() == 4) + .map(|op| op.code) + .collect(); + // Eight `op long` at $x F, eight `op long,X` at $1F, plus JSL ($22) and JML ($5C). + assert_eq!(four.len(), 18, "the four-byte set is {four:02X?}"); + } +} diff --git a/tests/roms/AccuracySNES/gen/src/dossier.rs b/tests/roms/AccuracySNES/gen/src/dossier.rs index 86234b77..d146665e 100644 --- a/tests/roms/AccuracySNES/gen/src/dossier.rs +++ b/tests/roms/AccuracySNES/gen/src/dossier.rs @@ -102,6 +102,7 @@ pub const MAP: &[(&str, &[&str])] = &[ ("D1.08", &["D1.08"]), ("E2.10i", &[]), ("E2.10", &["E2.10"]), + ("A6.15", &["A6.15"]), ("B4.16", &[]), // --- T-04-I opcode sweep: many tests, one enumerated assertion (declared in SPLITS) --- ("A5.S01", &["A5.01-08"]), diff --git a/tests/roms/AccuracySNES/gen/src/main.rs b/tests/roms/AccuracySNES/gen/src/main.rs index 8dd988e6..52c6d66a 100644 --- a/tests/roms/AccuracySNES/gen/src/main.rs +++ b/tests/roms/AccuracySNES/gen/src/main.rs @@ -11,6 +11,7 @@ //! //! Requires `ca65` and `ld65` (cc65 2.19+) on `PATH`. +mod cpu_opcodes; mod dossier; mod dsl; mod emit; diff --git a/tests/roms/AccuracySNES/gen/src/tests/cpu_defined.rs b/tests/roms/AccuracySNES/gen/src/tests/cpu_defined.rs new file mode 100644 index 00000000..d2ccced6 --- /dev/null +++ b/tests/roms/AccuracySNES/gen/src/tests/cpu_defined.rs @@ -0,0 +1,749 @@ +//! `A6.15` — every 65C816 opcode is defined, and only `STP` hangs. +//! +//! # What the row claims +//! +//! The 65C816 has no undefined opcodes. Unlike the NMOS 6502, whose map is full of jams and +//! unintended combinations, every one of the 256 bytes is a documented instruction with a +//! documented length — and exactly one of them, `STP`, stops the processor until `RESET`. +//! +//! Testing that means **executing** each opcode and proving control comes back at the right place. +//! The cart builds a one-opcode sandbox in WRAM, jumps into it, and counts three outcomes: the +//! opcode returned where its documented length says it should, it returned late, or it did not +//! return at all. The expectation — the length — comes from Table 5-4 of the WDC datasheet via +//! [`crate::cpu_opcodes`], not from `rustysnes-cpu`. +//! +//! # The sandbox +//! +//! ```text +//! BUF+0: ; L bytes, L from the table +//! BUF+L: JMP $AAAA ; $4C $AA $AA -> the CLEAN exit +//! BUF+L+3..15: $EA fill ; NOP +//! BUF+16: JMP $B8B8 ; $4C $B8 $B8 -> the OVERSHOOT exit +//! ``` +//! +//! **The exit addresses are chosen so the `JMP`'s own operand bytes are harmless instructions.** +//! `$AA` is `TAX` and `$B8` is `CLV`; both are one byte and touch nothing the sandbox needs. So an +//! opcode that consumes one or two bytes too many lands *inside* the terminator's operand, executes +//! a register transfer, and walks into the `NOP` fill — reaching the overshoot exit rather than +//! doing something unpredictable. Without that property a one-byte overshoot would execute half an +//! address as an instruction. +//! +//! | PC advance | lands on | outcome | +//! |---|---|---| +//! | exactly `L` | `JMP $AAAA` | **OK** | +//! | `L+1` .. `L+15` | `TAX`, or a `NOP` in the fill | **OVERSHOOT**, with the opcode recorded | +//! | `< L`, or `> L+15` | the opcode's own operand bytes, or anywhere | **NO RETURN** — the watchdog | +//! +//! # Why the terminator cannot be `RTS` or `RTL` +//! +//! A one-byte return would be tidier, and it does not work: `TXS` and `TCS` move the stack pointer, +//! and with `x = 1` a `TXS` puts it in page zero — so the return address is no longer where a +//! return would pop it from. Control has to come back without the stack, which means a `JMP`, which +//! means three bytes, which is what makes the operand-byte choice above load-bearing. +//! +//! # The watchdog takes two strikes, not one +//! +//! A battery whose entire value is that it always reports cannot execute arbitrary opcodes with no +//! way back. `runtime.s` carries an NMI trampoline with a settable vector, put there for the +//! `WAI`/`STP` and mid-block-move rows; the ordinary battery keeps `NMITIMEN` clear and polls +//! `$4212`, so it is free. +//! +//! One strike would be wrong. NMI fires once per vblank, and across 241 sandbox runs vblank will +//! eventually land inside a **healthy** one — so "an NMI arrived while a sandbox was active" does +//! not mean "stuck". The handler counts hits per opcode and abandons only on the second: a healthy +//! sandbox is microseconds and can be caught at most once; a jammed one is caught every frame. +//! +//! # Operand safety +//! +//! `DBR = $7E` for the whole sweep, so every absolute operand is WRAM rather than a `DBR = $00` +//! address that could land in MMIO — the hazard `sweep.rs` already documents. `D = $0200` puts +//! direct-page operands in the low-WRAM mirror, clear of the runtime's own variables at `$00`-`$5F`. +//! The window is `$5000`, well away from the sandbox at `$6000`. +//! +//! Four opcodes are dangerous even when they execute correctly, and each is handled in the preamble +//! rather than excluded: +//! +//! - **`MVN`/`MVP`** move `A + 1` bytes. `A = 0` makes that one byte, from `$7E:0000` to itself. +//! - **`XCE`** flips to emulation mode only if `C` is set. `CLC` first makes it a no-op in native +//! mode — it swaps a clear `C` with a clear `E`. +//! - **`TXS`/`TCS`** move the stack pointer, which is why the exits restore it from WRAM. +//! - **`SED`** leaves decimal mode set, and **`PLP`** can set anything at all; the exits re-establish +//! `m`, `x`, `d` and `c` explicitly rather than trusting what came back. + +use crate::cpu_opcodes::{Flow, table}; +use crate::dsl::{Asm, Kind, Provenance, Test}; + +/// Where the sandbox is assembled. Bank `$7E`, clear of `A8.07`'s block-move source which ends at +/// `$7E:3FFF`. +const BUF: u16 = 0x6000; + +/// Where every memory operand points. +const WINDOW: u16 = 0x5000; + +/// The clean exit. `$AA` is `TAX` — see the module docs on why the address's own bytes matter. +const EXIT_OK: u16 = 0xAAAA; + +/// The overshoot exit. `$B8` is `CLV`. +const EXIT_OVER: u16 = 0xB8B8; + +/// Bytes from the start of the sandbox to the overshoot terminator. Four for the longest opcode +/// plus three for the clean terminator leaves nine bytes of fill, which is slack rather than a +/// bound — nothing is expected to land in it. +const OVERSHOOT_AT: u16 = 16; + +/// Direct page during a sandbox run: the low-WRAM mirror, clear of the runtime's variables. +const SANDBOX_DP: u16 = 0x0200; + +/// The operand window must not overlap the sandbox — an absolute store landing there would rewrite +/// the very bytes under test. A compile-time assertion rather than a unit test, because both sides +/// are constants and there is no reason to let a bad pair get as far as a test run. +const _: () = assert!( + WINDOW + 0x100 <= BUF || BUF + OVERSHOOT_AT + 3 <= WINDOW, + "the operand window overlaps the sandbox" +); + +/// Scratch for the driver, immediately above the sandbox. +/// +/// **Not in low WRAM**, and that cost a debugging cycle twice over. `$7E:0170` upward looked free +/// and is used by a `bus.rs` row — WRAM scratch has no allocator and no collision gate, unlike the +/// measurement channel. And any address in `$7E:0200`-`$02FF` would have been inside the sandbox's +/// own **direct page**, so the first opcode with a direct-page operand would overwrite the driver's +/// state with its own test data. `the_scratch_is_clear_of_everything_the_sandbox_touches` +/// is the gate that would have caught both. +mod var { + /// The opcode under test. + pub const OP: &str = "$7E6100"; + /// Its documented length. + pub const LEN: &str = "$7E6101"; + /// The stack pointer to put back on the way out. + pub const SAVED_SP: &str = "$7E6102"; + /// Non-zero while a sandbox is in flight, so the NMI handler can tell. + pub const ACTIVE: &str = "$7E6104"; + /// NMI hits against the current opcode. The watchdog abandons on the second. + pub const HITS: &str = "$7E6105"; + /// Opcodes that returned exactly where their length says. + pub const OK: &str = "$7E6106"; + /// Opcodes that returned late. + pub const OVER: &str = "$7E6107"; + /// Opcodes that did not return. + pub const STUCK: &str = "$7E6108"; + /// The first opcode that was not OK, or `$FF`. + pub const FIRST_BAD: &str = "$7E6109"; + /// The opcode index, saved across a run because most opcodes clobber `X`. + pub const SAVED_X: &str = "$7E610A"; +} + +/// The first byte of [`var`], for the range checks. +#[cfg(test)] +const SCRATCH: u16 = 0x6100; + +/// How many bytes [`var`] spans. +#[cfg(test)] +const SCRATCH_LEN: u16 = 0x10; + +/// Opcodes the sweep executes: 256 less the fifteen that leave the sandbox. +const EXECUTED: u16 = 241; + +/// Build the four ROM tables the driver indexes with the opcode in `X`. +/// +/// A length of zero means "not executed", which is how the fifteen control-transfer opcodes are +/// skipped without a second table. +fn tables(a: &mut Asm) { + let ops = table(); + let mut len = Vec::with_capacity(256); + let mut bytes: [Vec; 4] = [const { Vec::new() }; 4]; + for op in &ops { + let skipped = matches!(op.flow, Flow::Leaves(_)); + len.push(if skipped { 0 } else { op.len() }); + let enc = operand_bytes(op.code, op.mode); + for (i, b) in enc.iter().enumerate() { + bytes[i].push(*b); + } + } + + a.d("a6_15_len:"); + emit_bytes(a, &len); + for (i, table) in bytes.iter().enumerate() { + a.d(&format!("a6_15_b{i}:")); + emit_bytes(a, table); + } +} + +/// Emit a 256-byte table as `.byte` lines. +fn emit_bytes(a: &mut Asm, data: &[u8]) { + for chunk in data.chunks(16) { + let list = chunk + .iter() + .map(|b| format!("${b:02X}")) + .collect::>() + .join(","); + a.d(&format!(" .byte {list}")); + } +} + +/// The four encoded bytes of one opcode, operands filled in safely. +/// +/// Unused trailing bytes are `$EA`, so a table read past the instruction's real length lands on a +/// `NOP` rather than on whatever happened to be there. +const fn operand_bytes(code: u8, mode: crate::cpu_opcodes::Mode) -> [u8; 4] { + use crate::cpu_opcodes::Mode; + let [wlo, whi] = WINDOW.to_le_bytes(); + let mut out = [0xEA; 4]; + out[0] = code; + match mode { + Mode::Implied => {} + // A direct-page offset into the seeded window page. `$10`/`$12` hold the indirect pointers. + Mode::Direct | Mode::StackRelative => out[1] = 0x10, + // An immediate value of zero, and — for a branch — a zero DISPLACEMENT, which is what makes + // the taken path land on the following instruction, exactly where the not-taken path goes. + // A branch cannot escape the sandbox either way. + Mode::Relative | Mode::Immediate => out[1] = 0x00, + Mode::RelativeLong => { + out[1] = 0x00; + out[2] = 0x00; + } + Mode::Absolute => { + out[1] = wlo; + out[2] = whi; + } + Mode::Long => { + out[1] = wlo; + out[2] = whi; + out[3] = 0x7E; + } + // `MVN`/`MVP` encode destination then source. Both banks are `$7E`, and `A = 0` in the + // preamble makes the move exactly one byte. + Mode::BlockMove => { + out[1] = 0x7E; + out[2] = 0x7E; + } + } + out +} + +/// `A6.15`. +#[allow(clippy::too_many_lines)] +pub fn a6_15() -> Test { + let mut a = Asm::new(); + tables(&mut a); + + a.c("The two exit stubs and the NMI watchdog are written into WRAM before the sweep starts."); + a.l("bra @body"); + watchdog(&mut a); + a.label("body"); + a.l("rep #$30"); + a.l("phk"); + a.l("plb"); + + a.c("Install the NMI handler and arm VBlank NMI. NMI ignores the I flag, so an opcode inside"); + a.c("the sandbox that runs SEI cannot disarm the thing that rescues it."); + a.l("rep #$20"); + a.l("lda #@nmi"); + a.l("sta a:V_NMI_VEC"); + + a.c("Counters, and a poisoned first-bad so 'nothing failed' is distinguishable from 'the"); + a.c("sweep never ran'."); + a.c("STZ has no long-addressing form, so every clear here is an explicit LDA #$00 + STA."); + a.l("sep #$20"); + a.l("lda #$00"); + a.l(&format!("sta f:{}", var::OK)); + a.l(&format!("sta f:{}", var::OVER)); + a.l(&format!("sta f:{}", var::STUCK)); + a.l(&format!("sta f:{}", var::ACTIVE)); + a.l("lda #$FF"); + a.l(&format!("sta f:{}", var::FIRST_BAD)); + + emit_exit_stubs(&mut a); + + a.l("sep #$20"); + a.l("lda #$80"); + a.l("sta $4200 ; VBlank NMI on — the watchdog's clock"); + + a.l("rep #$30"); + a.l("ldx #$0000"); + a.label("oploop"); + a.c("len = a6_15_len[X]. Zero means the opcode leaves the sandbox and is not executed."); + a.l("sep #$20"); + a.l("lda f:a6_15_len,x"); + a.l(&format!("sta f:{}", var::LEN)); + a.c("Inverted over a JMP: the body between here and @next is far beyond a branch's reach."); + a.l("bne :+"); + a.l("jmp @next"); + a.l(":"); + a.l("rep #$20"); + a.l("txa"); + a.l("sep #$20"); + a.l(&format!("sta f:{}", var::OP)); + + build_sandbox(&mut a); + run_sandbox(&mut a); + + a.label("next"); + a.l("rep #$30"); + a.l("inx"); + a.l("cpx #$0100"); + a.l("beq :+"); + a.l("jmp @oploop"); + a.l(":"); + + a.c("Disarm before asserting: a failure exits immediately and must not leave NMI armed for"); + a.c("whatever runs next."); + a.l("sep #$20"); + a.l("stz $4200"); + a.l("lda $4210 ; clear any pending RDNMI latch"); + + report(a) +} + +/// The NMI handler: the watchdog that gets control back when an opcode does not return. +fn watchdog(a: &mut Asm) { + a.label("nmi"); + a.c("Long addressing throughout: the sandbox may have left DBR and D anywhere, and this"); + a.c("handler runs before anything has put them back."); + a.c("A is preserved. RTI restores P and PC but not the accumulator, and an interrupt that"); + a.c("silently rewrites A is not transparent to the thing it interrupted."); + a.l("rep #$30"); + a.l("pha"); + a.l("sep #$20"); + a.l(".a8"); + a.c("LONG addressing, and that is the whole bug this handler shipped with. It runs with the"); + a.c("SANDBOX's DBR, which is $7E — so `lda $4210` reads $7E:4210, a WRAM byte, and RDNMI is"); + a.c("never acknowledged. Every other host happened not to land an NMI where it showed; ares"); + a.c("did, and the row read as a PLA divergence that was nothing of the kind."); + a.l("lda f:$004210 ; acknowledge RDNMI, DBR or no DBR"); + a.l(&format!("lda f:{}", var::ACTIVE)); + a.l("beq @nmi_out ; no sandbox in flight — nothing to rescue"); + a.l(&format!("lda f:{}", var::HITS)); + a.l("inc a"); + a.l(&format!("sta f:{}", var::HITS)); + a.l("cmp #$02"); + a.l("bcc @nmi_out ; first hit: a healthy sandbox can be caught once by chance"); + a.c("Second hit on the same opcode. It is not coming back — abandon the interrupt frame"); + a.c("entirely and re-enter the driver at the stuck exit."); + a.l("jml @stuck_entry"); + a.label("nmi_out"); + a.l("rep #$30"); + a.l(".a16"); + a.l(".i16"); + a.l("pla"); + a.l("rti"); +} + +/// Emit the two exit stubs as a ROM table, then copy them into WRAM. +/// +/// They have to live at fixed addresses whose *own bytes* are benign instructions (see the module +/// docs), which rules out assembling them in place — so they are sixteen bytes of `.byte` each, +/// with ca65 filling in the return address, copied into `$7E:AAAA` and `$7E:B8B8` before the sweep. +/// +/// Each restores rather than assumes, because the sandbox may have run `PLP`, `PLD`, `PLB` or +/// `TXS`: `SEP #$30`, `CLD`, `CLC`, put the saved stack pointer back, put the direct page back, +/// then `JML` into the driver. `DBR` is left alone — the stub runs in bank `$7E` so `PHK`/`PLB` +/// there would set the wrong one; the driver does it on arrival instead. +fn emit_exit_stubs(a: &mut Asm) { + for (name, target) in [("ok", "@ok_entry"), ("over", "@over_entry")] { + a.d(&format!("a6_15_stub_{name}:")); + a.d(" .byte $E2,$30 ; SEP #$30"); + a.d(" .byte $D8 ; CLD"); + a.d(" .byte $18 ; CLC"); + a.d(" .byte $C2,$30 ; REP #$30"); + a.d(&format!( + " .byte $AF,${:02X},${:02X},${:02X} ; LDA {} — the saved stack pointer", + SAVED_SP_ADDR & 0xFF, + (SAVED_SP_ADDR >> 8) & 0xFF, + (SAVED_SP_ADDR >> 16) & 0xFF, + var::SAVED_SP + )); + a.d(" .byte $1B ; TCS"); + a.d(" .byte $A9,$00,$00 ; LDA #$0000"); + a.d(" .byte $5B ; TCD — the runtime's variables live at D = 0"); + let _ = target; + a.d(" .byte $5C,$00,$00,$00 ; JML — the target is patched in below"); + } + + a.c("Copy both stubs into the fixed WRAM addresses the terminators jump to."); + for (name, addr) in [("ok", EXIT_OK), ("over", EXIT_OVER)] { + a.l("rep #$30"); + a.l("ldx #$0000"); + a.label(&format!("copy_{name}")); + a.l("sep #$20"); + a.l(&format!("lda f:a6_15_stub_{name},x")); + a.l(&format!("sta f:$7E{addr:04X},x")); + a.l("rep #$30"); + a.l("inx"); + a.l(&format!("cpx #${STUB_LEN:04X}")); + a.l(&format!("bne @copy_{name}")); + } + + a.c( + "Patch each JML's target. It cannot be assembled into the .byte table: that table lives in", + ); + a.c("the data segment, where this proc's cheap-local labels are out of scope."); + for (addr, target) in [(EXIT_OK, "@ok_entry"), (EXIT_OVER, "@over_entry")] { + a.l("sep #$20"); + a.l(&format!("lda #.lobyte({target})")); + a.l(&format!("sta f:$7E{:04X}", addr + JML_TARGET_AT)); + a.l(&format!("lda #.hibyte({target})")); + a.l(&format!("sta f:$7E{:04X}", addr + JML_TARGET_AT + 1)); + } +} + +/// Offset of the `JML`'s 16-bit target within a stub. +const JML_TARGET_AT: u16 = 16; + +/// Bytes in one exit stub. Pinned as a constant because the copy loop counts them, and asserted +/// against the emitted table by a unit test — a miscount would copy a truncated stub into WRAM and +/// the first opcode to reach it would run off the end. +const STUB_LEN: u16 = 19; + +/// [`var::SAVED_SP`] as a number, for the `LDA long` the stub carries as raw bytes. +const SAVED_SP_ADDR: u32 = 0x007E_6102; + +/// Assemble the one-opcode sandbox for the opcode in `X`. +fn build_sandbox(a: &mut Asm) { + a.c("Copy the four encoded bytes; only the first `len` of them are reached, and the rest are"); + a.c("overwritten by the terminator."); + a.l("rep #$30"); + a.l("phx"); + for i in 0..4u16 { + a.l("rep #$30"); + a.l("plx"); + a.l("phx"); + a.l("sep #$20"); + a.l(&format!("lda f:a6_15_b{i},x")); + a.l("rep #$30"); + a.l(&format!("ldx #${:04X}", BUF + i)); + a.l("sep #$20"); + a.l("sta f:$7E0000,x"); + } + a.l("rep #$30"); + a.l("plx"); + + a.c("The clean terminator at BUF+len, then NOP fill, then the overshoot terminator."); + a.l("rep #$30"); + a.l("phx"); + a.l("sep #$20"); + a.l(&format!("lda f:{}", var::LEN)); + a.l("rep #$30"); + a.l("and #$00FF"); + a.l("clc"); + a.l(&format!("adc #${BUF:04X}")); + a.l("tax"); + a.l("sep #$20"); + a.l("lda #$4C"); + a.l("sta f:$7E0000,x"); + a.l("rep #$30"); + a.l("inx"); + a.l("sep #$20"); + a.l(&format!("lda #${:02X}", EXIT_OK & 0xFF)); + a.l("sta f:$7E0000,x"); + a.l("rep #$30"); + a.l("inx"); + a.l("sep #$20"); + a.l(&format!("lda #${:02X}", EXIT_OK >> 8)); + a.l("sta f:$7E0000,x"); + + a.c("Fill from there to the overshoot terminator with NOP, then write it."); + a.l("rep #$30"); + a.l("inx"); + a.label("fill"); + a.l(&format!("cpx #${:04X}", BUF + OVERSHOOT_AT)); + a.l("bcs @filled"); + a.l("sep #$20"); + a.l("lda #$EA"); + a.l("sta f:$7E0000,x"); + a.l("rep #$30"); + a.l("inx"); + a.l("bra @fill"); + a.label("filled"); + a.l("sep #$20"); + a.l("lda #$4C"); + a.l(&format!("sta f:$7E{:04X}", BUF + OVERSHOOT_AT)); + a.l(&format!("lda #${:02X}", EXIT_OVER & 0xFF)); + a.l(&format!("sta f:$7E{:04X}", BUF + OVERSHOOT_AT + 1)); + a.l(&format!("lda #${:02X}", EXIT_OVER >> 8)); + a.l(&format!("sta f:$7E{:04X}", BUF + OVERSHOOT_AT + 2)); + a.l("rep #$30"); + a.l("plx"); +} + +/// Seed the window, take the machine into its known state, and jump into the sandbox. +fn run_sandbox(a: &mut Asm) { + a.c("Seed the operand window: the indirect pointers at D+$10 (16-bit) and D+$12 (24-bit) both"); + a.c("point back into the window, so an indirect load cannot reach MMIO or the sandbox."); + a.l("rep #$30"); + a.l(&format!("lda #${WINDOW:04X}")); + a.l(&format!("sta f:$7E{:04X}", SANDBOX_DP + 0x10)); + a.l(&format!("sta f:$7E{:04X}", SANDBOX_DP + 0x12)); + a.l("sep #$20"); + a.l("lda #$7E"); + a.l(&format!("sta f:$7E{:04X}", SANDBOX_DP + 0x14)); + + a.c("Save X across the run — a great many opcodes clobber it — and mark the sandbox active."); + a.c("Through A: only the accumulator has long addressing, so X cannot be saved directly."); + a.l("rep #$30"); + a.l("txa"); + a.l(&format!("sta f:{}", var::SAVED_X)); + a.l("sep #$20"); + a.l("lda #$01"); + a.l(&format!("sta f:{}", var::ACTIVE)); + a.l("lda #$00"); + a.l(&format!("sta f:{}", var::HITS)); + a.l("rep #$30"); + a.l("tsc"); + a.l(&format!("sta f:{}", var::SAVED_SP)); + + a.c("The preamble is the whole of the danger handling. A = 0 makes MVN/MVP a one-byte move;"); + a.c("CLC makes XCE a no-op in native mode; DBR = $7E keeps every absolute operand in WRAM;"); + a.c("D = $0200 puts direct-page operands in the low-WRAM mirror, clear of the runtime's own."); + a.l(&format!("lda #${SANDBOX_DP:04X}")); + a.l("tcd"); + a.l("sep #$20"); + a.l("lda #$7E"); + a.l("pha"); + a.l("plb"); + a.l("rep #$30"); + a.l("lda #$0000"); + a.l("ldx #$0000"); + a.l("ldy #$0000"); + a.l("sep #$30"); + a.l(".a8"); + a.l(".i8"); + a.l("cld"); + a.l("clc"); + a.l(&format!("jml $7E{BUF:04X}")); + + exits(a); +} + +/// The three ways back out of a sandbox. +/// +/// Split out of [`run_sandbox`] because the entry sequences are the bulk of it and none of them is +/// about getting *into* the sandbox. +fn exits(a: &mut Asm) { + a.label("ok_entry"); + a.l("rep #$30"); + a.l(".a16"); + a.l(".i16"); + a.l("phk"); + a.l("plb"); + a.l("sep #$20"); + a.l("lda #$00"); + a.l(&format!("sta f:{}", var::ACTIVE)); + a.l(&format!("lda f:{}", var::OK)); + a.l("inc a"); + a.l(&format!("sta f:{}", var::OK)); + a.l("rep #$30"); + a.l(&format!("lda f:{}", var::SAVED_X)); + a.l("tax"); + a.l("jmp @next"); + + a.label("over_entry"); + a.l("rep #$30"); + a.l(".a16"); + a.l(".i16"); + a.l("phk"); + a.l("plb"); + a.l("sep #$20"); + a.l("lda #$00"); + a.l(&format!("sta f:{}", var::ACTIVE)); + a.l(&format!("lda f:{}", var::OVER)); + a.l("inc a"); + a.l(&format!("sta f:{}", var::OVER)); + a.l("jsr @note_bad"); + a.l("rep #$30"); + a.l(&format!("lda f:{}", var::SAVED_X)); + a.l("tax"); + a.l("jmp @next"); + + a.label("stuck_entry"); + a.c("Entered from the NMI handler, so the stack still holds an interrupt frame. Rebuilding"); + a.c("the machine from saved state rather than returning is the point."); + a.l("rep #$30"); + a.l(".a16"); + a.l(".i16"); + a.l(&format!("lda f:{}", var::SAVED_SP)); + a.l("tcs"); + a.l("lda #$0000"); + a.l("tcd"); + a.l("phk"); + a.l("plb"); + a.l("sep #$20"); + a.l("cld"); + a.l("lda #$00"); + a.l(&format!("sta f:{}", var::ACTIVE)); + a.l(&format!("lda f:{}", var::STUCK)); + a.l("inc a"); + a.l(&format!("sta f:{}", var::STUCK)); + a.l("jsr @note_bad"); + a.l("rep #$30"); + a.l(&format!("lda f:{}", var::SAVED_X)); + a.l("tax"); + a.l("jmp @next"); + + a.label("note_bad"); + a.l("sep #$20"); + a.l(&format!("lda f:{}", var::FIRST_BAD)); + a.l("cmp #$FF"); + a.l("bne :+"); + a.l(&format!("lda f:{}", var::OP)); + a.l(&format!("sta f:{}", var::FIRST_BAD)); + a.l(":"); + a.l("rts"); +} + +/// Record the three counts and assert the row. +fn report(mut a: Asm) -> Test { + a.l("rep #$30"); + a.l(&format!("lda f:{}", var::OK)); + a.l("and #$00FF"); + a.record( + 283, + "A6.15 opcodes that returned at their documented length", + ); + a.l(&format!("lda f:{}", var::OVER)); + a.l("and #$00FF"); + a.record(284, "A6.15 opcodes that returned LATE (expect 0)"); + a.l(&format!("lda f:{}", var::STUCK)); + a.l("and #$00FF"); + a.record(285, "A6.15 opcodes that did not return (expect 0)"); + a.l(&format!("lda f:{}", var::FIRST_BAD)); + a.l("and #$00FF"); + a.record(286, "A6.15 first opcode that was not clean ($FF = none)"); + + a.c("Liveness first, and for the same reason E2.10 checks it first: a driver that fell over"); + a.c("early would report no late and no stuck opcodes, and two zeros would read as a pass."); + a.l(&format!("lda f:{}", var::OK)); + a.l("and #$00FF"); + a.assert_a16_range( + EXECUTED, + EXECUTED, + "the sweep did not execute 241 opcodes cleanly — it stopped early, or it covered a \ + different set than the 15 documented control-transfer exclusions. Slots 283-286 hold the \ + three counts and the first opcode that was not clean", + ); + + a.c("The row: nothing hung, and nothing consumed a different number of bytes than the WDC"); + a.c("table documents. STP is excluded by name and is the only opcode this cart cannot run."); + a.l(&format!("lda f:{}", var::OVER)); + a.l("and #$00FF"); + a.l("clc"); + a.l(&format!("adc f:{}", var::STUCK)); + a.l("and #$00FF"); + a.assert_a16_range( + 0, + 0, + "at least one opcode either failed to return or advanced PC by a different number of \ + bytes than Table 5-4 documents for it. Slot 284 counts the late ones, 285 the ones that \ + never came back, and 286 names the first", + ); + + a.finish( + "A6.15", + 'A', + "all 256 opcodes defined", + Provenance::Documented("WDC W65C816S datasheet, Table 5-4 opcode matrix"), + Kind::Scored, + None, + ) +} + +#[cfg(test)] +mod tests { + use super::{ + BUF, EXECUTED, EXIT_OK, EXIT_OVER, OVERSHOOT_AT, SANDBOX_DP, SCRATCH, SCRATCH_LEN, WINDOW, + operand_bytes, + }; + use crate::cpu_opcodes::{Flow, table}; + + /// The exits' own address bytes are executed when an opcode overshoots by one or two, so both + /// halves of both addresses have to be single-byte instructions that touch nothing the sandbox + /// needs. `$AA` is `TAX` and `$B8` is `CLV`. + #[test] + fn the_exit_addresses_are_made_of_harmless_opcodes() { + for addr in [EXIT_OK, EXIT_OVER] { + let [lo, hi] = addr.to_le_bytes(); + assert_eq!(lo, hi, "${addr:04X} — keep both halves the same byte"); + assert!( + matches!(lo, 0xAA | 0xB8), + "${lo:02X} is not one of the vetted one-byte opcodes" + ); + } + } + + /// The longest opcode plus its terminator has to fit before the overshoot terminator, or a + /// four-byte instruction would write its own clean exit on top of the overshoot one. + #[test] + fn the_longest_opcode_and_its_terminator_fit() { + let longest = table() + .iter() + .map(|op| u16::from(op.len())) + .max() + .expect("256 entries"); + assert!( + longest + 3 <= OVERSHOOT_AT, + "the longest opcode is {longest} bytes and the overshoot terminator is at +{OVERSHOOT_AT}" + ); + } + + /// The count the cart asserts has to be the one the table produces. + #[test] + fn the_asserted_count_matches_the_table() { + let n = table() + .iter() + .filter(|op| !matches!(op.flow, Flow::Leaves(_))) + .count(); + assert_eq!(n, usize::from(EXECUTED)); + } + + /// The driver's scratch must be clear of everything the sandbox can write: the sandbox itself, + /// the operand window, and — the one that is easy to miss — the sandbox's own **direct page**, + /// which is `$0200`-`$02FF` and reachable by every direct-page operand in the table. + /// + /// The first draft put the scratch at `$7E:0170`, which is inside neither of the first two and + /// is used by a `bus.rs` row; the second would have put it inside the third. WRAM scratch has + /// no allocator and no collision gate the way the measurement channel does, so this is it. + #[test] + fn the_scratch_is_clear_of_everything_the_sandbox_touches() { + let scratch = SCRATCH..SCRATCH + SCRATCH_LEN; + for (lo, hi, what) in [ + (BUF, BUF + OVERSHOOT_AT + 3, "the sandbox"), + (WINDOW, WINDOW + 0x100, "the operand window"), + (SANDBOX_DP, SANDBOX_DP + 0x100, "the sandbox's direct page"), + ] { + assert!( + scratch.end <= lo || hi <= scratch.start, + "the scratch at ${SCRATCH:04X} overlaps {what} (${lo:04X}-${hi:04X})" + ); + } + } + + /// No operand may name the sandbox, and no direct-page operand may reach the runtime's own + /// variables in the first `$60` bytes of the mirror. + #[test] + fn no_operand_reaches_the_sandbox_or_the_runtime_variables() { + use crate::cpu_opcodes::Mode; + for op in table() { + if matches!(op.flow, Flow::Leaves(_)) { + continue; + } + let b = operand_bytes(op.code, op.mode); + match op.mode { + Mode::Direct | Mode::StackRelative => assert!( + b[1] >= 0x10, + "{} uses direct page ${:02X}, inside the runtime's variables", + op.name, + b[1] + ), + Mode::Absolute | Mode::Long => { + let addr = u16::from_le_bytes([b[1], b[2]]); + assert!( + !(BUF..BUF + OVERSHOOT_AT + 3).contains(&addr), + "{} names ${addr:04X}, which is inside the sandbox", + op.name + ); + } + _ => {} + } + } + } +} diff --git a/tests/roms/AccuracySNES/gen/src/tests/mod.rs b/tests/roms/AccuracySNES/gen/src/tests/mod.rs index 06a00778..f66dc352 100644 --- a/tests/roms/AccuracySNES/gen/src/tests/mod.rs +++ b/tests/roms/AccuracySNES/gen/src/tests/mod.rs @@ -12,6 +12,7 @@ pub mod apu_sweep; pub mod bus; pub mod cart; pub mod cpu; +pub mod cpu_defined; pub mod dma; pub mod hirom; pub mod input; @@ -24,6 +25,7 @@ use crate::dsl::Test; #[must_use] pub fn all() -> Vec { let mut v = cpu::all(); + v.push(cpu_defined::a6_15()); v.extend(ppu::all()); v.extend(bus::all()); v.extend(dma::all()); From 317bdf66c19d2c9301074515b9798fc47173b9cd Mon Sep 17 00:00:00 2001 From: DoubleGate Date: Sun, 2 Aug 2026 11:57:29 -0400 Subject: [PATCH 2/3] fix(accuracysnes): resolve the A6.15 review findings Nine findings, all real. Two were functional and both are operand-safety violations the row's own docs claimed did not exist. - STACK-RELATIVE operands reached MMIO. The cart's stack sits at $1FFF, so `ORA $10,S` addressed $00:200F and a PLA read $00:2000 -- both outside WRAM, in the unmapped/MMIO region. The preamble now moves SP into page 1 for the run; the exits already restored the cart's own from SAVED_SP. - The LONG indirects read a pointer with a bank byte of $00. `[dp]` and `[dp],Y` take all three bytes at the direct-page pointer, and the first draft seeded two separate pointers, giving the long one bank $00 -- so it reached $00:5000, unmapped, rather than $7E:5000. There is now ONE pointer at D+$10, seeded 24-bit; the 16-bit indirects read its first two bytes and take the bank from DBR, which the preamble sets to $7E. - $FF was the "no failure" sentinel AND a real opcode: $FF is SBC long,X, which the row executes, so a genuine failure there would have been reported as `none`. ($DB is STP, not $FF -- I had that wrong in the first version of the test that was supposed to prove the sentinel safe, and the test caught me.) The sentinel is now $00 = BRK, which is in the not-executed set and therefore can never be a real answer. A unit test pins both halves of that. - STUB_LEN's doc claimed a unit test that did not exist. It exists now, and it counts the bytes the table actually emits. - cpu_opcodes.rs named sweep.rs as the sandbox that establishes sep #$30; the consumer of that table is tests/cpu_defined.rs. - The four-byte-opcode test's doc said nine where the assertion says eighteen. - emit_exit_stubs bound `target` and discarded it, and its doc omitted the REP #$30 the stub emits. - The CHANGELOG's E2.10 entry restated a coverage total that A6.15's entry then superseded in the same cumulative section. Re-verified after the SP and pointer changes, which move the image: battery 311/311, three references agree, 55 scenes match. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 2 +- .../tests/accuracysnes.rs | 2 +- tests/roms/AccuracySNES/ERROR_CODES.md | 2 +- tests/roms/AccuracySNES/asm/tests_group_a.s | 28 +++-- .../AccuracySNES/build/accuracysnes-pal.sfc | Bin 262144 -> 262144 bytes .../roms/AccuracySNES/build/accuracysnes.sfc | Bin 262144 -> 262144 bytes .../roms/AccuracySNES/gen/src/cpu_opcodes.rs | 11 +- tests/roms/AccuracySNES/gen/src/dsl.rs | 10 ++ .../AccuracySNES/gen/src/tests/cpu_defined.rs | 98 ++++++++++++++---- 9 files changed, 116 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ca0f775..594142ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,7 +58,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 With the fix the injections are exact where they had been approximate: the jam injection reported two stuck opcodes and 63 clean ones before, and reports one and 240 now. -- **`E2.10` — the full 256-opcode SPC700 cycle sweep, and the coverage number moves to 360 of 443.** +- **`E2.10` — the full 256-opcode SPC700 cycle sweep.** The cart measures how long every opcode the SPC700 can execute in a straight line actually takes, and compares it on-cart against the cycle count **fullsnes** documents for it. The host supplies no expected values; it reads back three bytes — opcodes measured, opcodes disagreeing, and the diff --git a/crates/rustysnes-test-harness/tests/accuracysnes.rs b/crates/rustysnes-test-harness/tests/accuracysnes.rs index 69bde5c6..455c2bc3 100644 --- a/crates/rustysnes-test-harness/tests/accuracysnes.rs +++ b/crates/rustysnes-test-harness/tests/accuracysnes.rs @@ -635,7 +635,7 @@ const GROUP_E_BATCH_SLOTS: [(u16, &str); 29] = [ ), (284, "A6.15 opcodes returning LATE (expect 0)"), (285, "A6.15 opcodes that did not return (expect 0)"), - (286, "A6.15 first opcode that was not clean ($FF = none)"), + (286, "A6.15 first opcode that was not clean ($00 = none)"), (280, "E2.10 opcodes disagreeing with fullsnes (expect 0)"), (281, "E2.10 opcodes measured (expect 231)"), (282, "E2.10 first disagreeing opcode ($FF = none)"), diff --git a/tests/roms/AccuracySNES/ERROR_CODES.md b/tests/roms/AccuracySNES/ERROR_CODES.md index b0ede007..39dad19e 100644 --- a/tests/roms/AccuracySNES/ERROR_CODES.md +++ b/tests/roms/AccuracySNES/ERROR_CODES.md @@ -683,7 +683,7 @@ Provenance: **Documented** (WDC W65C816S datasheet, Table 5-4 opcode matrix). Ki | Code | Byte | Meaning | |---|---|---| | 1 | `$02` | the sweep did not execute 241 opcodes cleanly — it stopped early, or it covered a different set than the 15 documented control-transfer exclusions. Slots 283-286 hold the three counts and the first opcode that was not clean | -| 2 | `$04` | at least one opcode either failed to return or advanced PC by a different number of bytes than Table 5-4 documents for it. Slot 284 counts the late ones, 285 the ones that never came back, and 286 names the first | +| 2 | `$04` | at least one opcode either failed to return or advanced PC by a different number of bytes than Table 5-4 documents for it. Slot 284 counts the late ones, 285 the ones that never came back, and 286 names the first ($00 there means none) | ### A5.S01 — Sweep: CLC diff --git a/tests/roms/AccuracySNES/asm/tests_group_a.s b/tests/roms/AccuracySNES/asm/tests_group_a.s index de08f527..f4667b11 100644 --- a/tests/roms/AccuracySNES/asm/tests_group_a.s +++ b/tests/roms/AccuracySNES/asm/tests_group_a.s @@ -5208,8 +5208,10 @@ CATALOG_IMPL = 1 .a16 lda #@nmi sta a:V_NMI_VEC - ; Counters, and a poisoned first-bad so 'nothing failed' is distinguishable from 'the - ; sweep never ran'. + ; Counters, and a first-bad of $00 meaning `none`. $00 is BRK, which is in the set this row + ; does NOT execute, so it can never be a real answer — where $FF, the obvious poison, is + ; SBC long,X and very much can be. `The sweep never ran` is caught by the liveness assertion + ; below, not by this slot. ; STZ has no long-addressing form, so every clear here is an explicit LDA #$00 + STA. sep #$20 .a8 @@ -5218,7 +5220,6 @@ CATALOG_IMPL = 1 sta f:$7E6107 sta f:$7E6108 sta f:$7E6104 - lda #$FF sta f:$7E6109 ; Copy both stubs into the fixed WRAM addresses the terminators jump to. rep #$30 @@ -5424,18 +5425,19 @@ CATALOG_IMPL = 1 .a16 .i16 plx - ; Seed the operand window: the indirect pointers at D+$10 (16-bit) and D+$12 (24-bit) both - ; point back into the window, so an indirect load cannot reach MMIO or the sandbox. + ; ONE pointer at D+$10, seeded as a 24-BIT $7E:5000. The 16-bit indirects `(dp)`/`(dp),Y` + ; read its first two bytes and take the bank from DBR, which the preamble sets to $7E; the + ; long indirects `[dp]`/`[dp],Y` read all three. The first draft seeded two pointers and + ; gave the long one a bank byte of $00, so `[dp]` reached $00:5000 — unmapped, not WRAM. rep #$30 .a16 .i16 lda #$5000 sta f:$7E0210 - sta f:$7E0212 sep #$20 .a8 lda #$7E - sta f:$7E0214 + sta f:$7E0212 ; the pointer's BANK byte ; Save X across the run — a great many opcodes clobber it — and mark the sandbox active. ; Through A: only the accumulator has long addressing, so X cannot be saved directly. rep #$30 @@ -5457,6 +5459,12 @@ CATALOG_IMPL = 1 ; The preamble is the whole of the danger handling. A = 0 makes MVN/MVP a one-byte move; ; CLC makes XCE a no-op in native mode; DBR = $7E keeps every absolute operand in WRAM; ; D = $0200 puts direct-page operands in the low-WRAM mirror, clear of the runtime's own. + ; SP moves into page 1 as well, and that is not cosmetic: the cart's own stack sits at + ; $1FFF, so a stack-relative operand of $10 would address $00:200F and a PLA would read + ; $00:2000 — both outside WRAM and into the unmapped/MMIO region. The exits restore the + ; cart's stack pointer from SAVED_SP, which was captured before this. + lda #$01F0 + tcs lda #$0200 tcd sep #$20 @@ -5553,8 +5561,8 @@ CATALOG_IMPL = 1 @note_bad: sep #$20 .a8 + ; Record only the FIRST one; $00 means none has been recorded yet. lda f:$7E6109 - cmp #$FF bne :+ lda f:$7E6100 sta f:$7E6109 @@ -5592,7 +5600,7 @@ CATALOG_IMPL = 1 sta f:$7EE43A lda f:$7E6109 and #$00FF - ; record slot 286: A6.15 first opcode that was not clean ($FF = none) + ; record slot 286: A6.15 first opcode that was not clean ($00 = none; $00 is BRK, never run) sta f:$7EE43C ; Liveness first, and for the same reason E2.10 checks it first: a driver that fell over ; early would report no late and no stuck opcodes, and two zeros would read as a pass. @@ -5634,7 +5642,7 @@ CATALOG_IMPL = 1 sta f:$7EE010 jml test_restore @fail2: - ; at least one opcode either failed to return or advanced PC by a different number of bytes than Table 5-4 documents for it. Slot 284 counts the late ones, 285 the ones that never came back, and 286 names the first + ; at least one opcode either failed to return or advanced PC by a different number of bytes than Table 5-4 documents for it. Slot 284 counts the late ones, 285 the ones that never came back, and 286 names the first ($00 there means none) sep #$20 .a8 lda #$04 diff --git a/tests/roms/AccuracySNES/build/accuracysnes-pal.sfc b/tests/roms/AccuracySNES/build/accuracysnes-pal.sfc index baae12b73bc37181c60d26e098aaecbdf9228000..a737009c7610de4b37a303d7cf50b7113d3a402a 100644 GIT binary patch delta 2203 zcmY+Ddr%wK6~@n95zH=cp#dx&7Upe-dYuT&I}|8>K*P(grV=0-*Trp(?P=1atryWI zag!9Q^-ZdA<7sHxDT6(ZoXFuJfdaS=EFeI<#2bOZ@ucI1cG{+HozT0xvYq~O=APg8 zediwSY7K@UgCXep@RpGKcT|bzRsT6}rrexTwfz^(n|CbBTdGlr|0hIPRkivUpK^13 ztU=!#hRf-2BqKF6l5LiIbU#UN-P|+=xH&^uvybI6o#%zpEsOqBqtEeHbprmnIyJD(XuZOW5%}%Y%vg0>A6tBt2w?Qmapo;LPH7}X6-&68(Q#Kw z)cD`2F^OO4+g4R(TE~s~m_f%G$*HD~z3T?FAuG9r^V16H_BQ_S@GDtKJ2s|;GB?|J zBy>01OwL!)4$Ev>9%6QO@F4=+*@0t&!w2A0eOx33WY_i+5#&Ql*`uBP&Zm z{jAe;hDA}>lu;Bdv+_U@x6Eo*Q5+9&T~-ok69DB)@>}YoYl(CCpRenw;G@EhpNqPK0VXfzK0oO1aT(&UbVCW&yRaNJS&kOopwKn!zc+={ zti1wzSua!?9Z4vN8-39@^T zT|%E7NTQR6Bv&$473=9YhxkBYr8h~JiEkAr(VHH9QzUE&%bXqNw+NoI!{XdO5VY<= z>kq)a_t zsm#R9mFe`S6H??;LQXQ;Nj_GHXeY(Ug@ftL%%o(Pkq~`|Ws0Zxrv+E>l<4~Dp>*cs zDZW5JA4}--LoA&!E!~xY*~4d<8`IL58`C5sJ*9hx)0sUk>EZ2hiKD)IB%S%dCGGHm z3kQdXTl$XVQC@aes|=sfM_6K>+fE@nF4V+Qb{GDuCYovTiD~s|qGMF#+tP%2`_7Or zLWc9SDq552=*SC=B(C z9B^O+x<`N=g{z~GFb2OLgGeWsoWP93E92lZ0Y905e@#H$B&uv9~V{1KY^m@hm+w1(g;f|&JM0L2O=0w?}Qsn!pf%=m{X80c>BWybW delta 2225 zcmY+Edr(tX9>;&@CT_?Tml7qqUOrFPe8w+|b_ zTD1O=ruNK~TJ4ruyX~Ok1}9?(1cGFl373aFLZCc^fLdkiy1I`}x3gXL+g;;+c zFnQGGy4DTYAdN>7qOlv*t+By;sE}`4UA3+Yn9Omlq4o<>ofzfXZ-#9YZBcMG(*F-J zV#Qxix2l3in>JudUWMR9ERR$~r9Dp34^6>E$|DChqkrS|8C$8gRa}sf8Sb=pT{5*@ zv$`&s$Zpg4WJV&=;IA?UX=@b4A(J%*|2A_I{xmZ?QRDEa_gp3rtYXGwlQ1Tkm55>s z;WnBvUD9jtcbUrMpPU`b3Y(NMv8!VtHegIv64!dv_X2<(iuC7kepXSk%fbE~J{0L) z4z44)MEtQ3{>uWii}YRxi@3Dc(K3DtrP%#hIz*>C*=P=?JMp6(a-63*LkFDf1`Y)T z^gy$Z+SkcvH967LsZM+~Hv!}9V!5P?1&SCYT{Z<}5{5)~vHKBW(OsC4J&QMJvyc*3 zXn&6;@FQ&!mEwBbXx&lQm59P*GYbzz`qy3jO88`sjIwc-P1v1NOv}63BittP?jVZ8 zMY-|RtK3nJPvmY#eYidMh0v&;E3t(v#dVTnlvftoz=Mn^0^TBE8@A--$^O}6D-4ha zY$O!iwjvTEt|Zir&u-1c>+>tpo6d;F z>>uc=5RE+SIc`hju|L}JD})+w^-cqN3%3((##E7kKHI;nxB!J$A`8i*q!UJYmTwKhfM6Tgm#hM?`6M`6YBT2FciwdmeK_L-x3wA8(>A; zJlz2Pb!j#}x=Tf04x-Bfn%t$5#4sQbRi>sdj2pDR?kOHDS4(5Y z*)pz441Q!tC9R94)^VZUN{ALa4QiU2VB{gNj8xyWN`WZ-_Y}uf?c98vlHtBK3RxOh}9=W?Z zLB@|nmQ-b1xCN9RbuHOIVp*e9Fu1AOKt_|j$hdE-H%Kp&?Jk5V+YoNAiwkXg$0Fg1 z)(Y0#wL%ql12Ya|Qt-u^L{jdpQ6UB^>#}ijZ48O>Yj+Se*X|_R&ua+7#d@YVgyD*j zMU9u30)l3lVxr2ruZT8Pr{VAFHXa~2dhiKR#BhgoQtvb9JuN>M+ z&Jl-qA)meKa5_Sa{mjdW>v4Nys<>RyWzub80m(T9%$}?>pc+fhO=(i-3uMPka8U)eURA)AN4_)2gHAd z&pHB#dJUo1TMQ!Vz|kcy=m(%<05-E=Vc~lg>IdP*AY>207ef#}48I$Oe+|PgM__IQ z_TGTL8;~&ywoynKgFlXeVjRwo!@3DLGXbGKX!OAkK4_SPdz0`BKTP?dbPD>Xpkx|a zmc&$YOX?bGZAX!qk|w>nZf4s|<;?M!t22L_iJX0V_T$;!*?Y5*a|Lq^bLZ#0bK?2! z^Of^&&UeoLIKSuS$2afZ)GVA>a4)PbBrYCX>{1pf?~|Dp~qq$-lI5`NVST za{qF~t@>N;TlDQCxBG90-8pdQ^E)GV-uuRGI8qaDuRT)!M2gp*GE-lgMdtqj$r^$z diff --git a/tests/roms/AccuracySNES/build/accuracysnes.sfc b/tests/roms/AccuracySNES/build/accuracysnes.sfc index e861318492d201e28940624d6cfb033d1c56381c..8b863c84fa0ab56f9d99faf1e7c5de2961b4e9ea 100644 GIT binary patch delta 2203 zcmY+Dc~BeY8OGmlMKHVELIYSF7Us4?y-oz?4h4!2Xt;be6^CS87dJ7sr%98xUPPP3 zO;W7ZUs8=5Pea;H8SHW7L=J}p3g9}hfBjSP1Z3?=3OBH`s^`EmQ%Ec*FTYuNIam&24sR{-8enf;7Rg0J5F&Ed% z>UB*axRee>GE#%X*(SML_mT9LjSYjJi_?cRd6_TLIUXq0yxF$qC;PV=ESI>J_YKxd99e7< zzod>vYTTk8@FXo~kmEDSTV(4f48NJ29<6TeWeYD80Sq2m&a_F(DGj8kq6s%KTJBPj z8vi>vD*iit>x#-mYq?P`6KFXDIaT+vw_SkNWknZoZc0Ag*2;ele~=ZmVM9tVbG?;E zLU+B@=y(}zGtZ>tAZB|zA0)u-?KrA10_UW@%FMU(VFH?$Q0Eibct=_=Roo#uGBOm@ zZ#s;pSQLScX$6ro3l9`=%Pb}p#qj`FWkvoLem^42--5Nu7`!Dtg_sYf{{;=>FVo|x zIP3jMen+iW0&)}eJgm!#PFcmhaApQ0mlHU8_n~%Kigah}p_QHdF=0I=4$Le@$FVlE z40Vxn1a@R5`1o~Q3C`syVV)9KXYIr(S&=@qB2>!>e3rly%JnW&u8ZS0>Iu2mWy&XK zBt*Py_>XbPI3l}@X|PF8p}{6D`={(bqjbFUi6unt&51^KoSdUUL+0~2KSInmJ^Te> zrEhu!uUVd3j8HAs<&}Cr`%m&lYZ~d?7Qg}d3OttgIBLM2`K74Qd^rCag|1-V-ASZo z?B1`s(Z|T$L3$`uM*`66)wPYAKY0v&d_r_9QTw{k%-LG_#*v znuebtsM(F0B{WTtoR?g3UUcQ{O<>Nr(HRN7L(pwEx-Fs2`x2P)0V!BMAO>F~$mT{i z34OLNfleHhT!~m!sH0yWx?DV5!ZHC4zF3F_I37&fTgo!8 zIi%QY0#3u71byq^Lj>oy653IgO2071#|!8Me6sA9%)K#wm(blC6EjKWEL}G)W$N&9 zc{*+?Po+N|mm(h%a-6A{;G=~|#e^6+w?CDco{$XF5~2^VOyMN|xZo#~^LkWF-fTh!>q`T5Ed+;=KZAuz*ZHi>1r*!9FDznQeJ-l5`anw%_r84h1r5)aL z;=s^QbMK)X%ERVtk>OLiFmu#XTPbA2`I>0T=ER?BBAG_7s1~nAI!Z;pElr50_cZw; zWH_fnMQkIQ2;`0LXkvYAPJ&g9eo+_wqVT9DT*Abnt%woNTfQ^se5DdEX?!2fQvRP9 zPXY;*u_XLtr7-{L%1~5+?Ybl!p!3HWl|I;BNfVWY7Ql>-M4%9CDW}Z##1W=A zx<}N5%qNeABUE8-{7Lj?Tz$*~U#)-CqtfyA_kQcDZ*$e7cN)q@T47f^+-`>-cfh$0 z2(!SD1s=1)87s6|A+8gi>xAET0^bR7UC_`4on4@?L8A={J^=Fv5Z4W|9@yCf=X!wd z1-K3u+FXb_iO?xysf;>UyU+)F`oY`}kvzP@!xubM4Zy$vBo9KmXcERI;RjRDJq3AA zFfMygiN;4Cp}gDjJt#W<1T&K~Q$ABSb7kg}8QX+^o83x#_ujWVL&hUMpLBbB$lq-!hjTs|qz&A1irK3cO#|Q~%J*^#21QC2TtY delta 2225 zcmY+Edr(tX9>;&@1~=pi@?5|uF_9{P!Vp)WA-p28Aest6E5kGb7qqUOrFPe8w+|b_ zT6Fy*P3@T}wc3`c-FBkm1}9?(1cIc@gv&!7A+S7zfLdkiy1I`}x3gXL+}(*N_= z5cywEw<&{1nsl|78K%MLMs8MO@nJXdOR=Qtke1Euu4=Yzzl8ocPfW8O~Rqp#x5K1BU_v zdZ6A%?d#;T>f9LWR42ZgmxysriA>zZ0!57CE}NV(3PWPL*!_sGm@dpnpTQe6*+_va zHJ_ph{792brMezBT6ffSC9){R#KJ?7?ll*`5JBZ?O zab5!T3U`#@6M5TFA8yb4Wmt62mAE37;yTGN$}bOV017 z_V={ah(;dv9JeL%*dOiqHA0QJW~Uy#iQ9>`V5(S8pY30kUx1=3QAOlY3hd{L8!2=Z zOG?v;K3BS5&U)FV^`ryqHO1QAMMm~mV!KG^_p)Ee2=#kg846`%b6Fz(Z>f^j4zMC_ zo^}BLrYr{^-KC^22hn8#P3}@k;#k2IhhHtz;KDMcq&Y~M38|Od51#Jx`JeArN~#8h ziYk0{w+5RC@dSy7ka{|ONXVxT@pBLCQPLj`u@T%V9|@?BAknbk5)JdNG@XimeV7$< zuGa}L~t2K_`()~MuTWnK;J*Fk`&z#ey>G%+uoNY*Mg^ObK)EPkXc98ymHu-VJRwETZ9=W?F zQOb`*<}`&h!VC(Js*Y?RHm_008Qff>C!@(;WZbtk8zh&>b{E1GR)pK@1J#KGG3vtaBZ#fxg>IgKUKQ&kS9FXgT`%b9sgbzAlQx}YN zLADE;UEp#-YB#*q4exiuNH?VRKuZr??|}q2oOQ$QUg+qB)axMbgRDOIpbx@55b`yA z(h)$^s|dZ;suxiQjxKpYHvk<2u$cuj3*WQQFbFpWA!i7_7=nml`28^aYZ!ht0&^p< z_XhOcfXq>_jY8@e{BaEA<8Xc))=j{f2?+B+lMjCILE|Lcn}iqrFy)7`Dd?Yq(rIX2 z3ZYV3)7DUHJBmXnN%AY}X12{#%^aV(I`g-gsM)t>Kb-BIy*C>*S2))=cYe-07c#$n zzH0uB`Of(t=l9(F@aEl{>V*>v?uFHbq{U;4J&WHi<^pIB~N y?q80))o{ywi@ts2cK_}0I|uH3c4y?yyWiOLM`{!7bw?_mNXgn$ChAL*$n-zS9D*zW diff --git a/tests/roms/AccuracySNES/gen/src/cpu_opcodes.rs b/tests/roms/AccuracySNES/gen/src/cpu_opcodes.rs index 3d2c3737..40d2d2b3 100644 --- a/tests/roms/AccuracySNES/gen/src/cpu_opcodes.rs +++ b/tests/roms/AccuracySNES/gen/src/cpu_opcodes.rs @@ -22,9 +22,9 @@ //! //! Immediate operands are one byte or two depending on the `m` and `x` flags, so a length table is //! meaningless without pinning them. Everything here is the 8-bit-accumulator, 8-bit-index case, -//! which is what `sweep.rs`'s sandbox establishes with `sep #$30` — and which it had to learn the -//! hard way, having first set only `sep #$20` and measured `LDX #imm` as a three-byte fetch against -//! a two-byte expectation. +//! which is what the sandbox in `tests/cpu_defined.rs` — the consumer of this table — establishes +//! with `sep #$30`. `sweep.rs` learned the same lesson the hard way, having first set only +//! `sep #$20` and measured `LDX #imm` as a three-byte fetch against a two-byte expectation. /// Whether an opcode continues the straight line, and if not, why not. #[derive(Clone, Copy, PartialEq, Eq, Debug)] @@ -460,8 +460,9 @@ mod tests { assert_eq!(t[0x42].len(), 2, "WDM is a reserved TWO-byte no-op"); } - /// Nine opcodes are four bytes: the eight ALU long forms' two columns less the ones that are - /// jumps, plus `JSL` and `JML`. Counting them is a cheap check that the long columns tiled. + /// Eighteen opcodes are four bytes: the eight `op long` at `$xF`, the eight `op long,X` at + /// `$1F`, plus `JSL` (`$22`) and `JML` (`$5C`). Counting them is a cheap check that the two long + /// columns tiled. #[test] fn the_long_modes_are_where_they_should_be() { let four: Vec<_> = table() diff --git a/tests/roms/AccuracySNES/gen/src/dsl.rs b/tests/roms/AccuracySNES/gen/src/dsl.rs index 789ed346..8b4a0b35 100644 --- a/tests/roms/AccuracySNES/gen/src/dsl.rs +++ b/tests/roms/AccuracySNES/gen/src/dsl.rs @@ -193,6 +193,16 @@ impl Asm { /// For read-only blobs that the test refers to by address instead of executing in place. Bank /// $00 holds the runtime, the font, every test body and the catalog, and it is finite; a /// several-hundred-byte SPC700 image per test is the one thing here big enough to matter. + /// The lines emitted into the out-of-bank data segment so far. + /// + /// Exists so a test can check what a data emitter actually produced — `A6.15`'s exit stubs are + /// raw `.byte` tables whose length a copy loop counts, and the two must not drift. + #[cfg(test)] + #[must_use] + pub fn data_lines(&self) -> &[String] { + &self.data + } + pub fn d(&mut self, line: &str) -> &mut Self { self.data.push(line.to_string()); self diff --git a/tests/roms/AccuracySNES/gen/src/tests/cpu_defined.rs b/tests/roms/AccuracySNES/gen/src/tests/cpu_defined.rs index d2ccced6..c092da76 100644 --- a/tests/roms/AccuracySNES/gen/src/tests/cpu_defined.rs +++ b/tests/roms/AccuracySNES/gen/src/tests/cpu_defined.rs @@ -94,6 +94,11 @@ const OVERSHOOT_AT: u16 = 16; /// Direct page during a sandbox run: the low-WRAM mirror, clear of the runtime's variables. const SANDBOX_DP: u16 = 0x0200; +/// Stack pointer during a sandbox run. In page 1, so a stack-relative operand and anything the +/// sandbox pulls stay inside WRAM — see the preamble in [`run_sandbox`] for why the cart's own +/// `$1FFF` is not safe here. +const SANDBOX_SP: u16 = 0x01F0; + /// The operand window must not overlap the sandbox — an absolute store landing there would rewrite /// the very bytes under test. A compile-time assertion rather than a unit test, because both sides /// are constants and there is no reason to let a bad pair get as far as a test run. @@ -241,8 +246,12 @@ pub fn a6_15() -> Test { a.l("lda #@nmi"); a.l("sta a:V_NMI_VEC"); - a.c("Counters, and a poisoned first-bad so 'nothing failed' is distinguishable from 'the"); - a.c("sweep never ran'."); + a.c( + "Counters, and a first-bad of $00 meaning `none`. $00 is BRK, which is in the set this row", + ); + a.c("does NOT execute, so it can never be a real answer — where $FF, the obvious poison, is"); + a.c("SBC long,X and very much can be. `The sweep never ran` is caught by the liveness assertion"); + a.c("below, not by this slot."); a.c("STZ has no long-addressing form, so every clear here is an explicit LDA #$00 + STA."); a.l("sep #$20"); a.l("lda #$00"); @@ -250,7 +259,6 @@ pub fn a6_15() -> Test { a.l(&format!("sta f:{}", var::OVER)); a.l(&format!("sta f:{}", var::STUCK)); a.l(&format!("sta f:{}", var::ACTIVE)); - a.l("lda #$FF"); a.l(&format!("sta f:{}", var::FIRST_BAD)); emit_exit_stubs(&mut a); @@ -336,11 +344,11 @@ fn watchdog(a: &mut Asm) { /// with ca65 filling in the return address, copied into `$7E:AAAA` and `$7E:B8B8` before the sweep. /// /// Each restores rather than assumes, because the sandbox may have run `PLP`, `PLD`, `PLB` or -/// `TXS`: `SEP #$30`, `CLD`, `CLC`, put the saved stack pointer back, put the direct page back, -/// then `JML` into the driver. `DBR` is left alone — the stub runs in bank `$7E` so `PHK`/`PLB` +/// `TXS`: `SEP #$30`, `CLD`, `CLC`, `REP #$30`, put the saved stack pointer back, put the direct +/// page back, then `JML` into the driver. `DBR` is left alone — the stub runs in bank `$7E` so `PHK`/`PLB` /// there would set the wrong one; the driver does it on arrival instead. fn emit_exit_stubs(a: &mut Asm) { - for (name, target) in [("ok", "@ok_entry"), ("over", "@over_entry")] { + for name in ["ok", "over"] { a.d(&format!("a6_15_stub_{name}:")); a.d(" .byte $E2,$30 ; SEP #$30"); a.d(" .byte $D8 ; CLD"); @@ -356,7 +364,6 @@ fn emit_exit_stubs(a: &mut Asm) { a.d(" .byte $1B ; TCS"); a.d(" .byte $A9,$00,$00 ; LDA #$0000"); a.d(" .byte $5B ; TCD — the runtime's variables live at D = 0"); - let _ = target; a.d(" .byte $5C,$00,$00,$00 ; JML — the target is patched in below"); } @@ -390,9 +397,9 @@ fn emit_exit_stubs(a: &mut Asm) { /// Offset of the `JML`'s 16-bit target within a stub. const JML_TARGET_AT: u16 = 16; -/// Bytes in one exit stub. Pinned as a constant because the copy loop counts them, and asserted -/// against the emitted table by a unit test — a miscount would copy a truncated stub into WRAM and -/// the first opcode to reach it would run off the end. +/// Bytes in one exit stub. Pinned as a constant because the copy loop counts them, and checked +/// against the emitted table by `the_stub_length_matches_what_is_emitted` — a miscount +/// would copy a truncated stub into WRAM and the first opcode to reach it would run off the end. const STUB_LEN: u16 = 19; /// [`var::SAVED_SP`] as a number, for the `LDA long` the stub carries as raw bytes. @@ -468,15 +475,19 @@ fn build_sandbox(a: &mut Asm) { /// Seed the window, take the machine into its known state, and jump into the sandbox. fn run_sandbox(a: &mut Asm) { - a.c("Seed the operand window: the indirect pointers at D+$10 (16-bit) and D+$12 (24-bit) both"); - a.c("point back into the window, so an indirect load cannot reach MMIO or the sandbox."); + a.c("ONE pointer at D+$10, seeded as a 24-BIT $7E:5000. The 16-bit indirects `(dp)`/`(dp),Y`"); + a.c("read its first two bytes and take the bank from DBR, which the preamble sets to $7E; the"); + a.c("long indirects `[dp]`/`[dp],Y` read all three. The first draft seeded two pointers and"); + a.c("gave the long one a bank byte of $00, so `[dp]` reached $00:5000 — unmapped, not WRAM."); a.l("rep #$30"); a.l(&format!("lda #${WINDOW:04X}")); a.l(&format!("sta f:$7E{:04X}", SANDBOX_DP + 0x10)); - a.l(&format!("sta f:$7E{:04X}", SANDBOX_DP + 0x12)); a.l("sep #$20"); a.l("lda #$7E"); - a.l(&format!("sta f:$7E{:04X}", SANDBOX_DP + 0x14)); + a.l(&format!( + "sta f:$7E{:04X} ; the pointer's BANK byte", + SANDBOX_DP + 0x12 + )); a.c("Save X across the run — a great many opcodes clobber it — and mark the sandbox active."); a.c("Through A: only the accumulator has long addressing, so X cannot be saved directly."); @@ -495,6 +506,12 @@ fn run_sandbox(a: &mut Asm) { a.c("The preamble is the whole of the danger handling. A = 0 makes MVN/MVP a one-byte move;"); a.c("CLC makes XCE a no-op in native mode; DBR = $7E keeps every absolute operand in WRAM;"); a.c("D = $0200 puts direct-page operands in the low-WRAM mirror, clear of the runtime's own."); + a.c("SP moves into page 1 as well, and that is not cosmetic: the cart's own stack sits at"); + a.c("$1FFF, so a stack-relative operand of $10 would address $00:200F and a PLA would read"); + a.c("$00:2000 — both outside WRAM and into the unmapped/MMIO region. The exits restore the"); + a.c("cart's stack pointer from SAVED_SP, which was captured before this."); + a.l(&format!("lda #${SANDBOX_SP:04X}")); + a.l("tcs"); a.l(&format!("lda #${SANDBOX_DP:04X}")); a.l("tcd"); a.l("sep #$20"); @@ -582,8 +599,8 @@ fn exits(a: &mut Asm) { a.label("note_bad"); a.l("sep #$20"); + a.c("Record only the FIRST one; $00 means none has been recorded yet."); a.l(&format!("lda f:{}", var::FIRST_BAD)); - a.l("cmp #$FF"); a.l("bne :+"); a.l(&format!("lda f:{}", var::OP)); a.l(&format!("sta f:{}", var::FIRST_BAD)); @@ -608,7 +625,10 @@ fn report(mut a: Asm) -> Test { a.record(285, "A6.15 opcodes that did not return (expect 0)"); a.l(&format!("lda f:{}", var::FIRST_BAD)); a.l("and #$00FF"); - a.record(286, "A6.15 first opcode that was not clean ($FF = none)"); + a.record( + 286, + "A6.15 first opcode that was not clean ($00 = none; $00 is BRK, never run)", + ); a.c("Liveness first, and for the same reason E2.10 checks it first: a driver that fell over"); a.c("early would report no late and no stuck opcodes, and two zeros would read as a pass."); @@ -634,7 +654,7 @@ fn report(mut a: Asm) -> Test { 0, "at least one opcode either failed to return or advanced PC by a different number of \ bytes than Table 5-4 documents for it. Slot 284 counts the late ones, 285 the ones that \ - never came back, and 286 names the first", + never came back, and 286 names the first ($00 there means none)", ); a.finish( @@ -650,10 +670,11 @@ fn report(mut a: Asm) -> Test { #[cfg(test)] mod tests { use super::{ - BUF, EXECUTED, EXIT_OK, EXIT_OVER, OVERSHOOT_AT, SANDBOX_DP, SCRATCH, SCRATCH_LEN, WINDOW, - operand_bytes, + BUF, EXECUTED, EXIT_OK, EXIT_OVER, OVERSHOOT_AT, SANDBOX_DP, SCRATCH, SCRATCH_LEN, + STUB_LEN, WINDOW, emit_exit_stubs, operand_bytes, }; use crate::cpu_opcodes::{Flow, table}; + use crate::dsl::Asm; /// The exits' own address bytes are executed when an opcode overshoots by one or two, so both /// halves of both addresses have to be single-byte instructions that touch nothing the sandbox @@ -685,6 +706,45 @@ mod tests { ); } + /// [`super::STUB_LEN`] is what the copy loop counts, so it has to be what the table emits. A + /// miscount copies a truncated stub into WRAM and the first opcode to reach it runs off the end. + #[test] + fn the_stub_length_matches_what_is_emitted() { + let mut a = Asm::new(); + emit_exit_stubs(&mut a); + // Two stubs, and every `.byte` line in the data segment belongs to one of them. + let emitted: usize = a + .data_lines() + .iter() + .filter(|l| l.trim_start().starts_with(".byte")) + .map(|l| l.split(';').next().unwrap_or(l).matches('$').count()) + .sum(); + assert_eq!( + emitted, + 2 * usize::from(STUB_LEN), + "the table emits {emitted} bytes for two stubs of {STUB_LEN}" + ); + } + + /// The "nothing failed" sentinel is an opcode index like any other, so it has to be one the row + /// never executes. `$00` is `BRK`, which is in the not-executed set. + /// + /// `$FF` was the first choice and is **wrong**: `$FF` is `SBC long,X`, which the row does + /// execute, so a genuine failure there would have been reported as `none`. + #[test] + fn the_no_failure_sentinel_can_never_be_a_real_answer() { + let t = table(); + assert!( + matches!(t[0x00].flow, Flow::Leaves(_)), + "$00 ({}) is executed, so it cannot double as the `none` sentinel", + t[0x00].name + ); + assert!( + !matches!(t[0xFF].flow, Flow::Leaves(_)), + "$FF is no longer executed — the comment explaining why it is a bad sentinel is stale" + ); + } + /// The count the cart asserts has to be the one the table produces. #[test] fn the_asserted_count_matches_the_table() { From 77880bbb3bcb30547ea381e8ba4f5dc84cefe1d8 Mon Sep 17 00:00:00 2001 From: DoubleGate Date: Sun, 2 Aug 2026 12:08:58 -0400 Subject: [PATCH 3/3] docs(ppu): inidisp_forgot_to_force_blank is a reference disagreement, not our gap It was recorded as "the last per-dot framebuffer gap" -- a Phase 4d item, to be closed by tracking the CGRAM redirect target in the fetch stage per layer, then re-blessing. Reading the third reference overturns that. The recorded account of MesenCE was incomplete. It was described as updating InternalCgramAddress only inside GetRgbColor, "so a transparent pixel leaves the previous opaque column's value standing". It does not: SnesPpu::RenderBgColor() runs AFTER every layer render in a span and sets InternalCgramAddress = 0 for each backdrop column it fills. What survives a span is therefore decided by pass ordering across the whole span -- an artefact of a span-based renderer, not a per-dot physical process. ares settles it. PPU::DAC::paletteColor sets latch.cgramAddress = palette UNCONDITIONALLY, called during per-dot priority resolution in the DAC, including paletteColor(0) for the transparent case (dac.cpp:71). That is RustySNES's model exactly: one assignment per dot, from the composited pixel, backdrop giving zero. So RustySNES and ares implement the same per-dot model and MesenCE differs for architectural reasons; the 7fc6 in the golden encodes MesenCE's renderer rather than hardware. The engine is NOT changed to match a pass-ordering artefact, and the "when 4d lands, remove the entry and re-bless" instruction is withdrawn. Two corroborations. C3.12 -- the scored cart row that asserts the redirect target directly -- passes on BOTH Mesen2 and RustySNES, so the two agree on the assertion and differ only on this homebrew framebuffer hash. And C3.12's own provenance is a per-dot statement, "a CGRAM access during active display uses the colour the PPU IS DRAWING", which the per-dot model implements directly and a span-ordering model only approximates. Same shape as the Mode-5 first-pixel claim, with the lesson from that retraction applied: the third reference was read BEFORE publishing, not after. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 48 ++++++++++++------- .../tests/undisbeliever_golden.rs | 44 ++++++++++++----- 2 files changed, 63 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 594142ee..33ceb6a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -147,24 +147,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 It belongs with `C13.*` and `F1.22`: enumerated, uncoverable, scored as such rather than chased. -- **`inidisp_forgot_to_force_blank`: the model difference is now identified exactly, and a recorded - claim about it is corrected.** The last per-dot framebuffer gap was documented only as "an - `internal_cgram_address` draw-ordering detail". The concrete difference: MesenCE updates - `InternalCgramAddress` inside `GetRgbColor`, called **only when `color > 0`** and **per layer - during tilemap render** — so a transparent pixel leaves the previous opaque column's value standing, - and several layers may update it within one column. RustySNES assigns it **once per column, - unconditionally, from the composited pixel** at the draw cursor. Structurally different models, not - an off-by-one. - - **Gate-on-opaque is not the fix**, measured rather than assumed: wrapping the assignment in - `if ap.opaque` moves the ROM's hash from `0xaeb678a4165b28c5` to `0xa55bd66a1e6dd125` — still not - the MesenCE-agreeing golden — because gating the *composite* is not gating each *layer fetch*. - - **Correction:** the AccuracySNES row that breaks under gate-on-opaque is **`C3.12`** ("CGRAM taken - in render"), not `C3.04` ("H counter advances") as previously recorded — `C3.04` passes. `C3.12` - is the row that asserts the redirect target directly, so its failure means the cart row and - MesenCE's model disagree about a backdrop column, and that needs adjudicating before either - changes. The real fix tracks the target in the fetch stage, per layer, on non-zero colour indices. +- **`inidisp_forgot_to_force_blank` is NOT a RustySNES defect, and the previous entry's account of + MesenCE's model was incomplete.** It was recorded as "the last per-dot framebuffer gap", to be + closed by tracking the CGRAM redirect target "in the fetch stage, per layer, on non-zero colour + indices". Reading the third reference changes the conclusion. + + **The incomplete claim.** MesenCE was described as updating `InternalCgramAddress` only inside + `GetRgbColor`, "so a transparent pixel leaves the previous opaque column's value standing". It does + not. `SnesPpu::RenderBgColor()` runs **after** every layer render in a span and sets + `InternalCgramAddress = 0` for every backdrop column it fills, ascending. So MesenCE zeroes on + backdrop too — and what survives a span is decided by **pass ordering across the whole span**, not + by the last column drawn. That is an artefact of a span-based renderer, not a per-dot process. + + **ares settles it.** `PPU::DAC::paletteColor` sets `latch.cgramAddress = palette` unconditionally, + called during per-dot priority resolution in the DAC — including `paletteColor(0)` for the + transparent case (`dac.cpp:71`). That is RustySNES's model exactly: one assignment per dot, from + the composited pixel, backdrop giving zero. + + So RustySNES and ares implement the same per-dot model and MesenCE differs for architectural + reasons. The `7fc6` in the golden encodes **MesenCE's renderer**, not hardware. The row is + reclassified from a per-dot gap to a reference disagreement, and the engine is not changed to match + a pass-ordering artefact. + + Two corroborations: `C3.12` ("CGRAM taken in render"), the scored cart row that asserts the + redirect target directly, **passes on Mesen2 and on RustySNES** — the two agree on the assertion + and differ only on this homebrew framebuffer hash. And `C3.12`'s own provenance is a per-dot + statement — "a CGRAM access during active display uses the colour the PPU **is drawing**" — which + the per-dot model implements directly and a span-ordering model only approximates. + + This is the shape the Mode-5 first-pixel claim had, and the lesson from that retraction applied: + the third reference was read **before** publishing, not after. - **Interlace scenes: the recorded Mesen2 nondeterminism is GONE, but interlace is blocked for a different reason.** Probed and withdrawn. Three consecutive Mesen2 runs and two snes9x runs of an diff --git a/crates/rustysnes-test-harness/tests/undisbeliever_golden.rs b/crates/rustysnes-test-harness/tests/undisbeliever_golden.rs index 1da83116..04c8a215 100644 --- a/crates/rustysnes-test-harness/tests/undisbeliever_golden.rs +++ b/crates/rustysnes-test-harness/tests/undisbeliever_golden.rs @@ -22,20 +22,42 @@ use rustysnes_core::{System, cart::Cart}; /// Frames to run before hashing (enough for the ROMs to reach their stable rendered pattern). const FRAMES: u32 = 60; -/// ROMs that render differently — and, so far, *less* correctly — under the per-dot compositor than -/// the batch model, so their golden keeps the batch (MesenCE-agreeing) hash and the per-dot mismatch -/// is accepted here as a documented, pinned gap. Each entry pins the exact per-dot hash so a *change* -/// in the wrong output still trips the gate. Currently one: `inidisp_forgot_to_force_blank` does a -/// PPU access during active display without force-blank; per-dot returns `7fff` where MesenCE returns -/// `7fc6` — a Phase 4d (PPU access-during-render) gap. When 4d lands, remove the entry and re-bless. +/// ROMs whose per-dot output differs from the MesenCE-agreeing golden, with the difference +/// **adjudicated** rather than assumed to be ours. Each entry pins the exact per-dot hash, so a +/// *change* in this output still trips the gate. /// -/// The pinned hash was refreshed for the DRAM-refresh stall (`docs/dram-refresh.md`): the 4d gap is -/// unchanged, but the per-line refresh shifts the whole frame's timing, so the wrong output lands at -/// a different instant and hashes differently. The golden TSV entry stays the pre-refresh batch value -/// (it only needs to differ from the pinned value to route through this known-gap branch). +/// Currently one: `inidisp_forgot_to_force_blank` does a PPU access during active display without +/// force-blank; per-dot returns `7fff` where MesenCE returns `7fc6`. +/// +/// # This is a reference disagreement, not a RustySNES gap +/// +/// It was recorded as "a Phase 4d (PPU access-during-render) gap — when 4d lands, remove the entry +/// and re-bless". Reading the third reference (2026-08-02) overturned that: +/// +/// - **ares** (`sfc/ppu/dac.cpp:158`) sets `latch.cgramAddress = palette` **unconditionally**, in +/// `paletteColor`, called during per-dot priority resolution — including `paletteColor(0)` for the +/// transparent case at `dac.cpp:71`. That is RustySNES's model exactly: one assignment per dot, +/// from the composited pixel, backdrop giving zero. +/// - **MesenCE** writes it per *layer fetch* and then runs `RenderBgColor()` — which zeroes backdrop +/// columns — **after** every layer render in a span. What survives a span is therefore decided by +/// pass ordering across the whole span, which is an artefact of a span-based renderer rather than +/// a per-dot physical process. +/// +/// So the `7fc6` encodes MesenCE's renderer architecture. Two corroborations: `C3.12` — the scored +/// AccuracySNES row that asserts the redirect target directly — **passes on both Mesen2 and +/// RustySNES**, so they agree on the assertion and differ only on this homebrew hash; and `C3.12`'s +/// provenance is a per-dot statement ("uses the colour the PPU *is drawing*"), which the per-dot +/// model implements directly. +/// +/// **Do not "fix" this by matching MesenCE.** The golden TSV entry stays the batch value; it only +/// needs to differ from the pinned value to route through this branch. +/// +/// The pinned hash was refreshed once for the DRAM-refresh stall (`docs/dram-refresh.md`): the +/// difference is unchanged, but the per-line refresh shifts the whole frame's timing, so the output +/// lands at a different instant and hashes differently. /// /// Unconditional: the per-dot PPU is the only compositor (the batch path was removed), so this crate -/// always exercises it and the gap list is not gated on any feature. +/// always exercises it and the list is not gated on any feature. const PERDOT_KNOWN_GAPS: &[(&str, u64)] = &[("inidisp_forgot_to_force_blank", 0xaeb6_78a4_165b_28c5)];