> ## Documentation index
> Fetch the complete documentation index at: https://rustynes-git-release-v3-6-0-aaf8d1.docs7.io/llms.txt
> Ask it with GET https://rustynes-git-release-v3-6-0-aaf8d1.docs7.io/ask?q=<keywords or question>. Short keywords rank best.
> Use these to discover all available pages before exploring further.

# Accuracy ledger

# Accuracy Ledger

The single at-a-glance record of where RustyNES is **cycle-exact**, where it uses
a **documented approximation**, and where a behavior is a **by-design non-goal**
or **out of scope**. This exists so an audit can tell those apart — "not modeled"
is very different from "no oracle exists to model against" or "deliberately out of
scope." `docs/STATUS.md` remains the authoritative per-suite pass-count record;
this ledger is the approximation map. The remediation line is **v2.1.0
"Fathom"** (accuracy work, shipping ahead of the v2.2.0 mobile store launch).

**Headline:** every suite on the oracle path is green, with **no named
exception** — AccuracyCoin **146/146 (100.00%, RAM decoder)** at upstream `f5f41dc2` since v3.1.0, whose re-sync showed that the 144/144 reported from v2.6.18 to v3.0.1 was overstated, because the older ROM's `Misaligned OAM behavior` fail path fell through to a pass and hid a real failure. Before that: **144/144** as of v2.6.18,
which closed `Advanced Sprite Evaluation :: Frozen OAM2 Increment`, the last
entry the 2026-09 upstream re-sync left open. `KNOWN_FAILING` is now empty; it
is retained, because it fails BOTH ways and is what caught this closure.
nestest 0-diff, `cpu_interrupts_v2` 5/5, `ppu_sprites` 19/19,
mmc3 A12-IRQ 18/18, `ppu_vbl_nmi` 10/10, the 60-ROM commercial byte-identity
oracle 60/60. The items below are the residual approximations, each with its
disposition under the v2.1.0 "Fathom" accuracy-remediation line
(`to-dos/plans/v2.1.0-fathom-accuracy-remediation-plan.md`).

## Legend

- **Remediated** — closed (or being closed) in the Fathom line.
- **No stricter oracle** — the reference test only defines a tolerance band /
  visual bar; there is nothing to tighten against. Not a defect.
- **Deferred** — genuine engineering, scheduled but not yet done.
- **Out of scope** — deliberately not modeled.

| Area | Nature | Oracle | Disposition |
|---|---|---|---|
| Palette backdrop-override ($3F00-$3FFF, render disabled) | Was not modeled; PPU outputs `palette[v & 0x1F]` not the backdrop | `full_palette` / `flowing_palette` visual corpus | **Remediated** (F1.1) |
| OAMADDR forced to 0 across dots 257-320 + `$2004` `$E3` attribute-byte mask | Already modeled; both correct | AccuracyCoin `$2004` / `Sprite0Hit` (141/141) | **Verified** — F1.2 added a fast unit regression guard |
| `OAMADDR & 0xF8` render-start OAM copy | Not modeled on the default revision (revision-dependent, unreliable) | none | **Out of scope** on the default `Rp2c02H` — Mesen2, ares, and TriCNES all omit it. The related OAMADDR `$2003` write corruption is an opt-in `Rp2c02G` model (row below) |
| OAMADDR (`$2003`) write-during-render OAM corruption (P5) | A `$2003` write while rendering is active copies OAM row 0 over the row `(value>>3)&0x1F` (reuses the `CorruptOAM` row-copy), gated behind the opt-in `PpuRevision::Rp2c02G`; default `Rp2c02H` never arms it | No test ROM in-suite; *Huge Insect* is the reference title but its per-title byte output is not independently oracle-verified here | **Landed (v2.1.7 P5) — default-off / byte-identical.** Honest **opt-in approximation**: the exact 2C02 letter-revision taxonomy of the glitch and its per-title bytes are not oracle-verified; offered as a single "earlier revision" model, not exact silicon fidelity. Config re-applied on load (not snapshotted); the corruption state it arms already round-trips via the v6 PPU snapshot tail |
| Power-up palette RAM (P5) | Palette RAM is uninitialized at power-on; optional `PaletteInit::Blargg` loads the canonical blargg power-up dump (6-bit masked) instead of the all-zero default | No pass/fail ROM (blargg dump is the de-facto reference) | **Landed (v2.1.7 P5) — default-off / byte-identical.** Default `Zeroed` keeps the established all-zero power-up palette; writes only `palette_ram` (already snapshotted) so no snapshot-format change |
| Power-on work-RAM model (P5) | Work RAM is uninitialized at power-on; optional `PowerOnRam::Seeded(u64)` (deterministic xorshift64) / `Filled(u8)` for software that reads it (FF RNG, RCR, Cybernoid) | No pass/fail ROM (hardware is itself non-deterministic; RustyNES stays deterministic per config) | **Landed (v2.1.7 P5) — default-off / byte-identical.** Default `Zeroed` = all-zero (what CI/oracle/save-state tests use); all fills deterministic (no wall-clock/OS RNG); stored so `power_cycle == fresh boot` |
| PPU open-bus decay | Refresh map matches the Blargg `ppu_open_bus` table exactly; ~600 ms is the spec decay value (per-group model) | `ppu_open_bus` (verifies the map, not a stopwatch) | **Verified** — F1.3 audit + regression test; per-bit timing has no oracle and isn't pursued |
| Optional OAM decay (F2.3) | Dynamic-RAM sprite decay when rendering stays off: per-8-byte-row 3000-CPU-cycle refresh model (Mesen2 `ReadSpriteRam`/`WriteSpriteRam`), rows refreshed by sprite-eval + `$2004`/DMA, decayed rows read `((a&3)==2)?a&0xE3:a` | No decay test ROM (Mesen2 code-level parity); NTSC/Dendy-only | **Landed on `main`** (Unreleased, next tag; F2.3) — **default-off** so the golden vectors / AccuracyCoin / commercial oracle (all decay-off) stay byte-identical; a real core feature (affects the framebuffer when ON), deterministic off `dot_counter`, save-state v7 tail (relative-age) |
| NTSC generated palette (F1.4) | Optional composite-model base palette synthesized in-core (`rustynes_ppu::generate_base_palette`) in place of the hand table | `full_palette` visual corpus + Mesen2/ares baselines (visual only) | **Shipped** (v2.1.2) — **default-off**, golden-locked cross-target; select via Settings → Palette → "Generated NTSC". Default build byte-identical |
| NTSC composite shader ladder (F2.2) | Display-only GPU post-passes: simplified blur (`Ntsc`) → LMP88959 composite → Bisqwit per-dot (`CompositeRt`) | No pass/fail ROM (visual only); `visual_regression` stays byte-identical with any filter active | **Shipped** (v2.1.2) — three-rung ladder verified end-to-end, live emulator-synced dot-crawl now on both composite passes (`Lmp88959` phase wired), palette↔pass split documented; no separable-kernel rung (LMP covers that tier). See `docs/frontend.md` |
| Vs. `DualSystem` second screen | Core modeled + `sub_framebuffer()` exposed; desktop frontend now presents both screens (side-by-side / stacked), routes P1-P4 + coin, plays the main console's audio | Synth harness + boot of the 4 DualSystem titles (real-cabinet boot stays fixture-limited — see below) | **Shipped** (v2.1.2 F2.1, desktop) — netplay / TAS scoped out in dual mode per ADR 0032; dual save-state since v2.9.7, rewind and run-ahead since v3.1.0 (the ADR's amendments). Since v2.9.7 the browser's wasm-winit build runs the cabinet and presents both screens (the `wasm-canvas` embed too since v3.5.0, side by side through the same compositor; the browser runs are a manual check), and the mobile bridge carries the cabinet since v2.9.7, with a screen switch rather than two screens; its on-device rows (run sheet T8-T10) are NOT RUN and the Swift half is uncompiled |
| NSF non-60 Hz playback + NSFe | **Done** (F4.1/F4.2): the play-speed divider (`$6E-$6F`/`$78-$79`) is parsed and a non-standard rate (PAL 50 Hz / custom µs) drives `play` via a mapper cycle-timer IRQ (frame-IRQ-disabled); standard 60 Hz keeps the byte-identical vblank-NMI path. `NSFE` chunked container parsed (INFO/DATA/BANK/auth). | `nsf` unit + core integration tests | **Done** (F4.1/F4.2) |
| FDS medium model (F4.3) | Byte-stream wire medium: gap / `$80` mark / block / **CRC-16-KERMIT** per block, with **per-block CRC re-emitted on write** (`resynth_block_crc`) and an opt-in **continuous belt-velocity head-seek** model (distance-proportional re-seek, default-off) replacing the fixed-cycle window | **CI-verifiable (synthetic):** `medium_write_verify` BIOS-free oracle — write via the register path, re-walk the wire, assert every block's CRC-16 + gap/mark framing round-trips (`fds::tests::synthetic_write_verify_*`). **Local-only:** the real-BIOS write-CRC path (BIOS recomputes CRC in its own RAM → `$4024`) needs a copyright `disksys.rom`, kept in gitignored `tests/roms/external/` and out of CI | **Shipped (v2.2.0 "Capstone")** — additive: default (model-off, non-writing) `.fds` run is **byte-identical**; new state round-trips the **v4** save-state tail. AccuracyCoin has no FDS ROM, so 141/141 is unaffected |
| Famicom microphone ($4016.2) | Not modeled | Built-in controller-2 mic bit surfaced on `$4016` D2 (`Nes::set_microphone`); `famicom_microphone_drives_4016_bit2` bus unit test | **Shipped (v2.2.0)** — additive / default-off (mic released ⇒ `$4016` byte-identical); a `$4016`-only signal (never touches `$4017`). No pass/fail mic ROM exists (real-cart local test only) |
| FDS DRAM-refresh-watchdog IRQ (`$4030.D1`) | Not modeled. The FDS BIOS/hardware raises periodic IRQs tied to DRAM-refresh-row-vs-access cycle accounting while `$4023.D0=0`; per upstream (`TakuikaNinja`'s `FDS-4030D1-Addr` research, `NESdev` Wiki) this is itself still under active hardware research and not modeled by most current FDS emulators either | `TakuikaNinja`'s `FDS-4030D1-Addr` probe (gitignored, `tests/roms/external/fds-takuikaninja/`, no permissive license — see `tests/roms/external/README.md`), consumed by the `RUSTYNES_FDS_BIOS`-gated `fds_4030d1_addr_with_real_bios` test | **Out of scope / honest residual, tracked not asserted** — the gated test only proves construction + a bounded real-BIOS run complete without panicking; it does not assert a specific watchdog timing value, since neither RustyNES nor the public research has pinned one yet. Revisit if/when upstream hardware research settles the exact behavior |
| Zapper light-timing | Single-pixel per-frame framebuffer sample | **Photodiode aperture** (3x3 field-of-view, >=2 bright pixels — `ZAPPER_APERTURE_*`) vs the PPU per-dot output, plus (v2.2.3 A3) the **beam-relative temporal model**; `zapper_light_detected_for_bright_region` / `zapper_aperture_rejects_lone_bright_pixel` / `zapper_temporal_light_follows_the_beam` / `zapper_frame_model_is_scanline_invariant_but_temporal_is_not` unit tests | **Hardened (v2.2.0), temporal model added opt-in (v2.2.3 A3), PROMOTED TO DEFAULT (v2.3.6).** The ~19-26-scanline photodiode hold is modelled: `ZapperState::light_at_scanline` makes light a function of where the CRT beam is at the moment of the read — dark before the beam paints the aim row, lit for `ZAPPER_LIGHT_HOLD_SCANLINES`, dark once drained — which the frame-granular model structurally cannot express (it returns one answer per frame, sampled at end-of-frame, so every read during frame N reports frame N-1). **v2.3.6 CORRECTION:** this row previously justified `Default OFF` with "the supported titles re-poll every frame and are satisfied by either model". **That was false.** *Duck Hunt* requires the gun to see nothing for one frame and then a bright spot in the next; under the frame model it received that probe inverted (bright on the blanked frame, dark on the target frame) and discarded every shot before hit-testing, so **no duck could ever be hit** — reported by the maintainer, reproduced headlessly from the game's own `$4017` traffic. The claimed absence of an oracle was also wrong: the game itself is one. Also fixed in v2.3.6: the beam-relative sampler read aperture rows the beam had **not finished painting**, so it reported light on an all-black screen (`aperture_is_bright_painted`). Measured A/B, same ROM/aim/inputs — frame model: score 000000, duck flying; beam-relative: score 000500, duck hit. Regression test `duck_hunt_zapper_shot_can_score` asserts the game's own scoreboard, mutation-checked. Deterministic and stateless either way — a pure fn of framebuffer + aim + scanline, so nothing new to serialize and no save-state or rollback impact |
| BestEffort mapper tier (26 families, was 112) | Register-decode + save-state round-trip only; off the oracle gate | `mapper_tier_honesty.rs` invariant | **Mostly remediated** (F3): 86 promoted to Curated with commercial-ROM oracle; the 26 left have no cleanly-booting dump (16 NES 2.0 high-id + 8 no-cart + 2 jam-at-boot) |
| MMC3 R1/R2 scanline-IRQ (ADR 0002) | **Closed (T-MMC3-BG-A12, in v3.0.0)**: both `4-scanline_timing` ROMs pass all 13 sub-tests | `mmc3_test_2/4` + `mmc3_test/4` strict tests (un-ignored); `background_a12_rises_at_the_mmc3_pages_dot_324` and `scanline_0_dot_0_drives_bg_chr_only_when_the_skip_did_not_replace_it` (PPU unit tests); `mmc3_r1r2_phase_probe` A12-phase probe (v2.1.5) | The background fetches report A12 at the MMC3 page's dot 324 (they reported at their read dots, two dots later), and a visible line's dot 0 drives "the same CHR address that is later used to fetch the low background tile byte" (NESdev PPU rendering), except scanline 0's on an odd frame, which the skip replaces with a nametable tick: the MMC3 page's "decrement the counter twice every other vertical redraw". **Moved v2.9.9 (T-ORACLE-001)** before that: the NESdev reload rule replaced the `$C001` non-zero-clear discriminator and the IRQ output was deferred to the next per-cycle hook (ADR 0002's 2026-10-05 correction): sub-test 3 → 9, `mmc3_test/5-MMC3` passes, `mmc3_test/6-MMC6` fails only at its alternate-revision assertion (by design). The v2.1.5 phase study and the v2.9.5 refutation of the M2-edge low-time filter stand as history in ADR 0002 |
| PPU register write placement — M2-low vs phi2 (v2.6.17) | A 6502 commits a write at **phi2**, the last of a CPU cycle's three PPU dots; this core applies PPU register writes at **M2-low**, the first, because `Cpu::start_cycle` catches the PPU up before the bus access. Measured **half a dot** early: the commit lands 6 master clocks into the CPU cycle (1.5 dots) where phi2 is 8 (2.0). **v2.6.17's "two dots early" figure is RETRACTED** — see the plan's *SETTLED* section; a 2-master-clock nudge is sufficient to flip `Frozen OAM2 Increment`, which a two-dot error could not be. Several PPU behaviours compensate for it, one of which — `mask_for_skip_check`, the two-stage delay pipeline consumed by the dot-339 odd-frame skip — documents itself as a compensation in its own comment | AccuracyCoin `Frozen OAM2 Increment` (the ROM names the dots it writes on: 242 and 256, where this core applies 240 and 254); `ppu_vbl_nmi/10-even_odd_timing` as the independent adjudicator; `terminus_control.rs` as the first-difference control | **Known divergence, measured and deliberately NOT corrected (v2.6.17 "Terminus", maintainer decision).** The diagnosis is confirmed and the *change* was refuted by its own pre-written gate: the write moved alone reads **141/144** and breaks `10-even_odd_timing` (code `09`), and the best combination found — phi2 + the dot-321 OAM2 increment + a one-stage `mask_for_skip_check` — reads **142/144** against the **143/144** that shipped at v2.6.17 (144/144 from v2.6.18, by a different mechanism entirely -- see the Frozen OAM2 row), plus a save-state epoch and six re-baselined framebuffer goldens. Replacing a documented compensation with an undocumented one is worse than keeping it. The knob survives as the default-off `phi2-write-sweep` feature so a future attempt re-measures rather than rebuilding the apparatus; the three conditions for reopening are listed in the plan's *CLOSED* section **Re-measured v2.9.5** on the 144/144 tree (`phi2sweep.rs`): write +2 or +4 alone reads **142/144** (loses `Arbitrary Sprite zero` and `Stale Sprite Shift Regs`), read and write both moved reads **138/144** (loses the six NMI entries), and read +2 / write +2 reads 142/144. It gains nothing at any placement. No test adjudicates in favour of moving it, so it stays as shipped. **Re-measured v3.3.0 (`T-PHI2-WRITE`)** on the re-synced 146-test battery (`phi2sweep.rs`): the write at phi2 alone reads **145/146**, losing only `Stale Sprite Shift Regs`; moving the read too reads 140/146 (the six NMI entries). With the odd-frame skip at the one stage v2.6.17 derived for phi2, sweeping the dot-339 sprite re-arm's gate depth (live, 1, 2, 3, and the shared gate) and the shared rendering gate (0, 1, 2): never 146. The re-arm depth moves nothing, a shallower shared gate trades the loss for `Misaligned OAM2 Address`, and a deeper one loses `Frozen OAM2 Increment` as well. Refuted against its gate a second time, on the current battery; the knob is kept as the instrument that measured it |
| Frozen OAM2 Increment (AccuracyCoin, `$0493`) | Secondary-OAM address behaviour while rendering is toggled mid-fetch, and the dot on which a `$2001` change reaches each consumer | AccuracyCoin `Advanced Sprite Evaluation :: Frozen OAM2 Increment`, and its `sub-tests/` standalone ROM | **CLOSED in v2.6.18.** The row's previous cause is RETRACTED and kept here because each wrong part cost a refuted fix: it blamed the write placement, and all four writes the ROM names take effect during dot N-1 and are in force from the START of dot N, exactly where the ROM says; it read the failure as test 2 or 3, and it was test **4**, whose `$0E` code is indistinguishable from test 3's because the ROM omits an `INC <ErrorCode` between them; and it implicated the OAM2 freeze, which per-dot instrumentation showed byte-identical to the passing case. The real rule is that a mask change during dot N must not act on dot N -- applied at the dot-256 vertical increment (a two-dot gate, hoisted out of the shared block) and the dot-339 OAM2 reset (which stopped conjoining the live mask) |
| Misaligned-OAM out-of-range `OAMADDR` advance (v2.6.17) | AccuracyCoin's README states two rules: secondary OAM **full** is `+5` (the buggy `n+m` increment, implemented for releases), **not full** is `+4` then `& $FC` — which clears the byte index. The second was not implemented; the FSM carried the misaligned index forward | No ROM in the corpus discriminates it. Instrumented, the not-full out-of-range branch fires 56,953,944 times per battery run, of which exactly **114** have a non-zero byte index — the only case the rule changes, and the battery reads 143/144 either way | **REMEDIATED (v2.6.17)** — adopted only once a stimulus existed that could fail without it: `misaligned_oam_out_of_range_advance_follows_both_rules` drives one y-test from `OAMADDR = $05` past an out-of-range Y and asserts `(n, m)` for both rules. Recorded here because "the battery did not move" is a measured quantity rather than an absence of evidence, and because the test — not the README — is what authorises the change |
| APU non-linear mixer | Lookup-table matches within the `apu_mixer` band | `apu_mixer` (analog-cancellation, tolerance) | **No stricter oracle** — the LUT already passes; ±4% is honest |
| Expansion-audio levels — VRC6 / MMC5 / N163 (v2.1.6; VRC6 recalibrated **v2.2.7**) | Full-volume expansion square vs 2A03 pulse: VRC6 ≈1.0× (was ≈1.51×), MMC5 ≈1.0× ("equivalent"), N163 1-ch ≈6.0×. MMC5/N163 calibrated to Mesen2 mixer weights (the accuracy bar), cross-checked vs nestopia/puNES/fceux/tetanes; VRC6 now calibrated to the NESdev/field consensus instead | bbbradsmith `db_vrc6a/b`, `db_mmc5`, `db_n163` via the `audio_expansion.rs` `level_db_*` oracle (measured peak ratio) | **Remediated** (v2.1.6); **VRC6 re-remediated (v2.2.7 "Timbre II")** — the v2.1.6 VRC6 target (`VRC6_MIX_SCALE=979`, ≈1.51×) reproduced Mesen2's `×5` mixer weight, but that convention is a Mesen2-only outlier: the NESdev wiki states the VRC6 pulse is loudness-equivalent to a 2A03 pulse, and rustico/tetanes/BizHawk each encode that equivalence exactly (ares/higan/nestopia reach it via a `sum/61` normalization). `VRC6_MIX_SCALE` moved 979 → 650 (≈1.0×); the `db_vrc6a/b` snapshots were re-blessed (audio-only, framebuffer/cycle-count byte-identical). MMC5 `650/40`, `NAMCO163_MIX_SCALE=261` unchanged since v2.1.6 (were 256, 256/16, 64 — N163 was ~12 dB too quiet). Base-APU byte-identity preserved (expansion is a separate additive `mix_audio` term = 0 for non-expansion mappers) |
| Expansion-audio level — Sunsoft 5B absolute (v2.1.6 → **v2.2.3**) | Log-volume DAC *shape* exact (`×1.4126`/step, `LUT[12]=668`/`LUT[15]=1882` vs Mesen2 `[63,177]`). The *absolute* level is now calibrated too: `SUNSOFT5B_MIX_SCALE_NUM/DEN = 2549/138 ≈ 18.471`, measured (`db_5b` read `0.0685×`, ~23 dB too quiet) against the Mesen2-derived target `LUT[12]=63 × weight 15 / 746.9 = 1.265×` (full scale `177×15/746.9 = 3.554×`) | `db_5b` via the new `level_db_5b` oracle (`1.265 ± 0.04`, measured **1.2651**); step law by `sunsoft5b_volume_dac_follows_logarithmic_step_law` | **REMEDIATED (v2.2.3, A1)** — the blocker was never the value but the type: `Mapper::mix_audio` returned `i16` and the corrected full-scale tone is `1882 × 18.471 = 34,761`, past `i16::MAX` for ONE channel (three tones ≈104 k, 3.2× over). Widening the trait return to **`i32`** unblocked it; shape and level are now separately pinned. Non-5B boards return the values they always did — the widening is representational only, and the other 24 audio-expansion tests plus AccuracyCoin 141/141 are unchanged. **v2.2.7 "Timbre II"** completed the DAC shape for envelope mode: the 5-bit hardware envelope level previously truncated to 4 bits (`>> 1`) before indexing the 16-entry `SUNSOFT5B_LOG_VOL` shape table (the wiki's documented 3 dB/step approximation), and now indexes a new 32-entry `SUNSOFT5B_LOG_VOL32` table (`×1.1885`/step = 1.5 dB, matching nestopia/rustico) with the full 5-bit level — `LOG_VOL32`'s odd entries equal `LOG_VOL` exactly (`log_vol32_odd_entries_match_4bit`). Fixed 4-bit volume (already correct 3 dB/step) and the absolute level (1.265×) are unchanged; committed 5B snapshots stayed byte-identical since the extant 5B test ROMs don't exercise odd envelope levels |
| Expansion-audio level — VRC7 FM (v2.1.6) | OPLL FM synth implemented (emu2413 MIT port); instrument patch ROM byte-for-byte canonical (Nuke.YKT); absolute FM level vs APU square is patch-/TL-/feedback-dependent pseudo-sine, ≈2.7× peak per Mesen2 emu2413 but not a clean square-vs-square oracle | `patch_vrc7` = `vrc7_all_15_melodic_patches_match_nuke_ykt_canonical` unit test (real); `db_vrc7`/`clip_vrc7` snapshot-guarded | **Patch verified; level snapshot-guarded** — the FM synthesis + instrument set are correct; the absolute output level is not oracle-pinned (no square-vs-square reference for a pseudo-sine) |
| PAL APU frame-counter step positions | **Modeled (v2.1.5):** `frame_counter.rs` now clocks the PAL (2A07) sequencer at 8313/16627/24939/33252-33254 (4-step) and 8313/16627/24939/41565-41566 (5-step), region-gated by `FrameCounter::pal` (true only for `Region::Pal`; NTSC/Dendy keep 7457/14913/22371/29828-30, 37281-82). PAL *scheduler* timing (3.2:1, 50 Hz, PAL DMC/noise tables) was already modeled | `pal_apu_tests` (10 sub-ROMs, forced PAL, on-screen verdict via `run_nes_screen`) | **10/10 pass** — the 3 region-independent checks + the 5 PAL frame-counter-timing checks (clock jitter, mode-0/1 length timing, IRQ-flag/IRQ timing) that flipped to PASS with the PAL step positions, plus `10.len_halt_timing` / `11.len_reload_timing` (see the next row). NTSC byte-identity preserved (AccuracyCoin APU 141/141, `apu_test` 8/8, NTSC `blargg_apu_2005` 11/11 all unchanged). |
| Length halt/reload write-vs-half-frame-clock ordering | **Modeled (v2.1.5):** the length counter (`length.rs`) now defers a halt change (`new_halt`) and a length reload (`reload_val` + `previous_count` snapshot); the APU promotes both once per CPU cycle in `tick_with_external`, AFTER the half-frame length clock and BEFORE the mixer sample — so a halt/reload write that coincides with the clock is applied *after* it (halt) or dropped when the counter was clocked non-zero (reload). Mirrors `TetaNES` `LengthCounter::reload` + Mesen2 `_newHaltValue`. | `pal_apu_tests` `10.len_halt_timing`, `11.len_reload_timing` (forced PAL, on-screen verdict); NTSC `blargg_apu_2005` 10 & 11 + `f2a_*` (`f2_accuracy_audit.rs`) | **CLOSED (v2.1.5)** — both PAL ROMs now report on-screen `PASSED` (was `FAILED: #3` / `#4`). Region-agnostic ordering fix: byte-identical on NTSC (the reload settles in-cycle on the common non-coincident write, and halt does not affect channel output directly), so AccuracyCoin 141/141, `blargg_apu_2005` 11/11 and `f2_accuracy_audit` 6/6 are all unchanged. |
| APU analog HPF/LPF chain | Fixed-coefficient 3-pole | No pass/fail ROM | **No stricter oracle** (optional measured-RC future work) |
| PlayChoice-10 Z80 second-screen menu | Not modeled | — | **Out of scope** |
| MMC1 software WRAM write-protect | **REMEDIATED (v2.2.3 A2).** MMC1 has two software PRG-RAM write-protect layers -- the `$E000` bit-4 disable common to every board, and SNROM's second layer where a CHR-**RAM** board's CHR register bit 4 is wired to the RAM's other enable. Neither was modelled: `$6000-$7FFF` was read and written unconditionally | Holy Mapperel `M1_*` WRAM nibble (`1000` SJROM = `$E000` layer; `5000` SNROM = both) | **Closed** -- both layers modelled; a disabled window reports `cpu_read_unmapped` so the read floats to open bus rather than returning stale RAM, and writes are discarded. The CHR-register layer is gated on `chr_is_ram` so a CHR-ROM board still treats those bits as CHR banking. Holy Mapperel's README calls this a game-compat hazard (FCEUX / PowerPak omit it), so it was validated before landing rather than assumed: commercial oracle **60/60** (incl. 7 battery-backed MMC1 saves -- Zelda, Metroid, Final Fantasy, Mega Man 2, Castlevania II, Ninja Gaiden, Kid Icarus) and the extended corpus **138/138**. Pinned by three new unit tests, incl. a negative control that a CHR-ROM board ignores the SNROM layer |
| FME-7 open bus on RAM-selected-but-disabled `$6000-$7FFF` | **REMEDIATED (v2.2.3 A2).** FME-7 models the command-`$8` RAM-enable (bit 7) / RAM-select (bit 6) bits; the one unmodelled state was **selected but disabled** (bit 6 = 1, bit 7 = 0), which drives neither the RAM nor the ROM chip, so the databus floats. RustyNES fell through to the PRG-ROM bank and returned its tag byte `1`, failing Holy Mapperel's "read open bus" sub-check (requires `>= 3`) and setting `MAPTEST_WRAMEN` | Holy Mapperel `M69_*` WRAM nibble | **Closed** — routed through `Mapper::cpu_read_unmapped`, the trait's existing "not wired to mapper-resident memory" contract, so the bus preserves whatever value the open-bus latch already holds rather than clobbering it with the ROM tag byte — the disabled window now returns open bus, not a fixed constant. (In the Holy Mapperel `M69_*` run that latch value is observed as `$7F`, which is the test's observation, not a universal result.) `M69_*` detail goes `1000` -> `0000`. Verified by negative control (reverting flips the on-screen digit back `0` -> `1`) and against the commercial oracle, where the FME-7 titles are unaffected |
| VRC7 OPLL synthesizer state carried by the mapper save state (v2.2.3 → **v2.3.7**) | **REMEDIATED (v2.3.7).** `Vrc7::save_state` wrote only the *shadow* register bytes (`audio.addr_latch`, `audio.data_latch`, `audio.silenced`, `audio.regs[0..64]`) and never the live `self.opll`, `opll_clock_counter`, or `last_opll_sample`; `load_state` never replayed them either, so after a rewind / netplay rollback / TAS restore the FM voice resumed from whatever envelope and phase state it happened to hold. `rustynes_apu::Opll` now exposes a `snapshot`/`restore` pair (`OPLL_SNAPSHOT_VERSION` 1, a fixed `OPLL_SNAPSHOT_LEN`) carrying the register shadow, the EG/LFO counters, the per-channel patch selection, all 18 operator slots, the user patch pair and the per-channel outputs. The lookup tables and the patch ROM are NOT carried — they are constants of construction — and `chip_type` rides only as a tag that rejects a cross-chip restore. The VRC7 mapper section is **v2**: the tail appends `opll_clock_counter`, `last_opll_sample` and that blob after the VRAM | `vrc7_save_state_carries_the_live_opll_so_audio_resumes_identically` (key a note, advance 20,000 cycles, save, then compare 4,000 mixed samples from the source against 4,000 from a **fresh** mapper restored from the blob — equal sample for sample), plus `Opll` round-trip / snapshot-idempotence / cross-chip-rejection / truncation / bad-tag tests, and `Opll` is now registered in `snapshot_schema_audit.rs` so a future field cannot be added without being classified | **CLOSED (v2.3.7)** — additive, so a v1 blob still loads and leaves the synthesizer exactly where the pre-v2.3.7 build left it (an old save is no worse than it was, not newly silent). A build without `mapper-audio` has no synthesizer to describe, so it still writes v1 and validates-then-ignores a v2 tail, preserving the cross-feature portability ADR 0004 promises. The rejected alternative is recorded because it is the one a reader will think of first: replaying `audio.regs` through `Opll::write_reg` needs no new format, but restarts every keyed-on channel at attack, so every rewind frame would produce an audible transient. Mutation-checked — making the tail carry a *reset* synthesizer reproduces the pre-fix failure |
| 2A03 die-revision "unexpected DMA" extra read (`Cpu2A03Revision`, ADR 0033) | The DMC-halt-overlaps-OAM-halt "double-halt" extra parked-address re-read is revision-gated: `Rp2A03G` (default) performs it, `Rp2A03H` omits it. On this engine the gate fires (~75× in a synthetic DMC+OAM+`$2007` probe) but is a no-op — the parked address during a DMC+OAM overlap is always the post-`$4014` instruction fetch, never a side-effect register — so `Rp2A03H` is byte-identical to `Rp2A03G` on every oracle | **None exists** — no public reference (Mesen2/ares/BizHawk/TriCNES/fceux/nestopia/GeraNES/higan) branches DMA behavior on 2A03 die stepping, and no test ROM captures it; the five `dmc_dma_during_read4` ROMs + both `sprdma_and_dmc_dma` ROMs all `Pass` on the default and are the verified floor | **Frontier — documented, not closed** (v2.1.7, ADR 0033). Config surface + mechanism-correct gate shipped **default-off / byte-identical**; the `Rp2A03H` direction is an unverified hypothesis; the H≡G equality is pinned by `cpu_2a03_revision::rp2a03h_matches_rp2a03g_documented_residual`. The reference-grounded **console-type** DMC-glitch axis (Mesen2 `isNesBehavior`) is a separate deferred knob (`T-PS-dmc-glitch-console-type`) |
| MMC5 scanline counter and in-frame flag (v3.5.0) | **Remediated (v3.5.0).** Until v3.5.0 the counter ticked at dot 0 of the pre-render line and of every visible line, rendering or not: visible line N read N + 1, a `$5203` = N IRQ rose at dot 0 of line N - 1 (a whole line early), and it counted with rendering off. The MiSTer sibling found it (its ledger 3.100). Now the page's read-watching detector: three consecutive reads of one `$2000-$2FFF` address, then the next read (line N's attribute fetch, dot 4) detects line N; in frame clears after three CPU cycles with no PPU read, on a `$2001` disable and on a `$FFFA`/`$FFFB` read. The page's prose and diagram detect at the read after the third match, its pseudocode at the third match (two dots earlier); the model takes the prose. Not modelled: the `$2002` / `$4014` counter resets, pin 92. The 8x16 CHR-set background window still follows the PPU's line hooks (`render_window`), unchanged | `nesdev_wiki/MMC5.xhtml` ("Scanline Detection and Scanline IRQ"); blargg's 2005 hardware timings (NESdev forum t658: in frame at visible line 0, `$5203`=1 at line 1, 239 at 239); `tests/mmc5_scanline_irq.rs` (synthetic cartridge) | **Remediated**; no public MMC5 test ROM here checks IRQ timing (the three in `tests/roms/mmc5/` are smoke tests) |

## Oracles / regression nets

- **Holy Mapperel bank-reachability + IRQ net** (v2.1.5, `crates/rustynes-test-harness/tests/holy_mapperel.rs`, `--features test-roms`): the 17 committed zlib-licensed ROMs (`tests/roms/holy_mapperel/`) each run to their settled result screen, pinned by an `insta` framebuffer-hash snapshot with settled + non-blank structural guards. Catches silent mapper-detection / bank-layout / RAM-sizing / IRQ regressions the `AccuracyCoin` / blargg suites don't cover; **all 17** detect + reach all banks with detailed code `0000` as of v2.2.3 A2, which closed the last two residuals (the MMC1 WRAM write-protect and the FME-7 open-bus-on-disabled-RAM rows above).

- **`tests/roms/extra/apu/apu_test_{1..10}`: the `$4017` clock that shares an
  APU cycle with the sequencer's step (surfaced v2.6.2, CLOSED v2.9.5).**
  Nineteen ROMs sat in that directory referenced by nothing in the workspace.
  They had been dismissed as audio-output-only on the strength of a `$6000`
  probe, but `$6000` is unmapped on this vintage and reads back `0`, which is
  blargg's *success* code. That probe could not tell "passed" from "not
  present", the same false oracle that had made the NTSC `blargg_apu_2005`
  suite vacuous for five minor releases. Re-read with the on-screen decoder:
  - nine are audio-only;
  - ten report a verdict, and four of those (`_1`, `_2`, `_5`, `_6`) failed.

  v2.9.5 read the ten ROMs' code, and they are one program with ten delays. Each
  loads pulse 1's length counter, spends some of it with `$80` writes to
  `$4017`, sets a mode, waits almost exactly one sequencer period, writes
  `$4017` again, and checks whether the length counter reached zero. ROMs 9 and
  10 show that the sequencer's last half-frame step happens before a reset
  landing at those deltas. ROMs 1, 2, 5 and 6 then require that a mode-1
  write's immediate clock, maturing one CPU cycle after that step, adds **no
  second decrement**; one cycle later (3, 4, 7, 8) it must. The quarter- and
  half-frame triggers are emitted on APU-cycle boundaries (nesdev wiki, *APU
  Frame Counter* and its Talk page), so the two are one pulse.

  `FrameCounter::prev_tick_step` suppresses the write's clock when the
  sequencer fired the same clock on the previous tick. It is derived from the
  sequencer position, so the save-state format is unchanged. **Pinned by
  `apu_frame_clock_coincidence.rs`, 10/10.** Before the fix, exactly 1, 2, 5
  and 6 failed. Each ROM's pass branch was decoded from its own
  `lda $4015 / and #$01 / beq|bne` rather than trusted from the screen.

  The quarter-frame half of the rule rides the same mechanism, but no ROM here
  observes envelopes, so it is by construction rather than pinned.

## Notes

- **PPU A12 stream, corrected (v2.9.7).** The PPU reported the A12 level of
  pattern fetches only, so the garbage nametable reads of the sprite window
  never pulled A12 low, and each rendered line delivered one rise where the
  hardware makes eight. MMC3 filters the extra seven, so every MMC3 test ROM
  and AccuracyCoin passed. Boards that count raw edges were starved eightfold:
  the MC-ACC (mapper 4.3), mapper 91 submapper 0 and the J.Y. ASIC's A12 mode.
  Found by real games, not a test ROM. Six Acclaim MC-ACC titles run with
  v2.9.6's model lost their status bars, and Alien 3 went black. Measured: 242
  falls per frame reached the mapper, against the 1,928 a correct stream
  gives. Fixed at the source: `Ppu::read_vram` reports every read's A12. Four
  MMC3-core boards that clocked on every rise gained MMC3's filter
  (`a12_filter.rs`). The filter clocks on `notify_cpu_cycle`, which the bus
  sends only to boards that declare `cpu_cycle_hook`. The first draft left it
  undeclared on those four, so their IRQ never fired on a real `Nes` while
  their unit tests, which drove the hook by hand, passed. *AV Jiu Ji Ma Jiang
  2* (mapper 115) showed it. The shared test helper now asserts the flag.
  Changed goldens: `mmc1_a12` (9 pixels; SNROM's RAM enable follows CHR A12).
  Commercial frames that moved, each checked against v2.9.6: the
  *Donkey Kong Country 4* world map (mapper 211), garbled before and correct
  now; *Time Diver* (mapper 250, its own IRQ fix). Mappers 115, 134 and 189
  match v2.9.6 frame for frame. Pinned by
  `a12_reports_the_hardware_stream_and_an_mmc3_filter_still_sees_241` and a
  per-board `irq_counts_scanlines_not_raw_a12_pulses`.
- **`T-COMMERCIAL-GARBLE`, v2.9.7.** *Time Diver: Avenger* (mapper 250) is
  **fixed**. The IRQ was an 8-bit M2 cycle counter, which
  `INES_Mapper_250.md` does not support ("a regular MMC3 chip connected in [a]
  different way"). It is now the MMC3 scanline counter, and the playfield,
  intro and second stage render correctly. *Uchuu Keibitai SDF* (MMC5) is
  **localised, not fixed**. Frames 120-400 of the intro are wrong in every
  frame, and later screens, including its vertical-split "ROUND WAVE" screen,
  are correct. The intro's MMC5 state:
  - `$5100=3`, `$5101=3` (1 KiB CHR), `$5104=0` (ExRAM nametable), `$5105=$10`;
  - BG set `$5128-$512B` = `$30-$33`, 8x16 sprites (`$2000=$B0`);
  - no IRQ register written; split off (`$5200=0`); `$5130=0`; 128 KiB CHR.

  The corrupted frames show recognisable letter tiles scattered in wrong
  places, so the nametable contents look displaced, not the CHR banks. The
  next step is a per-access comparison against a reference run of the intro.
  It stays open under the same ticket.
  **Fixed v2.9.9:** the game reads CHR data through `$2007` with 8x16 sprites
  selected and the A set written last, so the MMC5 serves it from the A set;
  the model always used the B set. The "displaced nametable" was correct
  nametable data read from the wrong CHR bank. Closed by the MMC5 page's
  `$2007` rule (T-MMC5-8X8-SET below), with no game-specific code.
- **MMC5 CHR bank sets, fixed v2.9.9 (`T-MMC5-8X8-SET`, found v2.9.7).**
  `MMC5.md`: "When using 8x8 sprites, only registers $5120-$5127 are used."
  The model had used the background set for both kinds of fetch in 8x8 mode,
  ignored `$5101` for sprites, indexed 1 KiB banks in the 8/4/2 KiB modes and
  served `$2007` from the background set always. It now follows the page's
  table and the hardware results the page cites (8x8: A set for everything;
  8x16: B for background while rendering, last-written for `$2007`, A for
  `$2007` reads under extended attributes), learning the sprite size from its
  own decode of `$2000` / `$2001`. Five of six local MMC5 dumps are
  byte-identical; *Uchuu Keibitai SDF* is fixed.
- **Terminator 2 (MC-ACC) pause screen, unexplained.** Pressing Start shows a
  "PAUSED" screen overlaid with CPU-register-like text (`ADC $0C15`, `TAX`,
  `CLI`). It looks the same under MC-ACC and standard-MMC3 timing, and the
  same before and after the A12 fix, so it is not an IRQ symptom. Whether it
  is the game's own pause display or an emulation fault is not established;
  it is recorded rather than guessed.

- **Determinism boundary.** Display-only work (the NTSC composite filter/shader
  ladder) stays in the frontend/shader and never feeds the framebuffer/audio
  golden-vector or save-state hash. Two accuracy features DO change deterministic
  core output when enabled — the generated NTSC palette (F1.4) and optional OAM
  decay (F2.3) — so both ship **default-off**: with the default the golden vectors
  / AccuracyCoin / commercial oracle / save-state hashes are byte-identical, and
  turning either on is a deliberate, deterministic opt-in (OAM decay is driven off
  the monotonic `dot_counter`, never wall-clock, and its per-row state round-trips
  the save-state via the v7 relative-age tail). The v2.1.7 P5 hardware-revision
  knobs (opt-in `PpuRevision::Rp2c02G` OAMADDR corruption, `PaletteInit::Blargg`
  power-up palette, `PowerOnRam::Seeded`/`Filled` work RAM) are likewise all
  **default-off / deterministic**: at their defaults the core is byte-identical,
  and every configured fill is seeded or uniform (no wall-clock / OS RNG). They
  need no snapshot-format change — the state they touch (`oam_corruption_pending`,
  `palette_ram`, work RAM) is already serialized, and the selections themselves
  are config re-applied on load like `region`.

- **NROM provides PRG-RAM at `$6000-$7FFF` where the board has none, and the
  MiSTer co-simulation DUT found it (2026-08-25, v2.6.3 rung 5).**
  `crates/rustynes-mappers/src/m000_nrom.rs` allocates 8 KiB of PRG-RAM
  unconditionally so accesses "don't fall off the edge". `cpu_read` returns
  `self.prg_ram[addr - 0x6000]` and `cpu_write` stores into it, so the window
  behaves as **real, writable RAM**: an *unwritten* location reads `$00`
  (the array is zero-filled at construction) and a written one reads back what
  was stored. On hardware an NROM board decodes nothing there and the read
  floats — it returns **open bus**, the last value the data bus held, whether
  or not the program has written to that address.

  This is the emulator default `nesdev_wiki/Open_bus_behavior.xhtml` names as a
  problem in its own words (public source:
  <https://www.nesdev.org/wiki/Open_bus_behavior>): *"A few games read the region $6000-7FFF but have no
  WRAM present here. This can be a problem for emulators, as the original iNES
  file format had no way to specify a lack of WRAM, leaving the emulator to
  provide WRAM behavior in that region by default. The NES 2.0 format corrects
  this with a WRAM size field."* The page then names titles that break on the
  WRAM answer — **Low G Man** (a crash in Chapter 1 Scene 3B, or boomerang
  graphical glitches) and **Battletoads & Double Dragon** (a `$00` read at the
  end of level 1 crashes it).

  **Not fixed here, and the reason is scope rather than doubt.** The fix is a
  behaviour change to the shipped emulator on every iNES-header NROM cartridge,
  so it needs the NES 2.0 WRAM-size field honoured where present, a decision
  about iNES-header defaults, the per-game database consulted, and the full
  accuracy battery plus the commercial-ROM oracle re-run — the same shape as the
  header-override hazard that froze *Wizards & Warriors* (ADR 0031). It is a
  correctness item with a citation and named symptoms, recorded rather than
  quietly carried.

  **How it was found is the point.** The v2.6.3 co-simulation step wrote a
  `cpu_bus` module from the wiki and ran it against this emulator; the DUT and
  the oracle agreed on `$4016`, `$4017`, `$5000` and `$5C34` and disagreed only
  in `$6000-$7FFF`. This is the first time the verification ladder has found the
  **oracle** wrong rather than the DUT, which the v2.5.0 plan listed in advance
  as risk 6: *"the oracle can be wrong. 141/141 is not 'matches silicon'."*

## AccuracyCoin's coded passes, triaged (v2.6.5)

Over a **4500-frame** window, where all 146 catalog entries execute, the vector
reads `total=146 pass=130 pass_with_code=16 fail=0 skipped=0 not_run=0`. The
sixteen are triaged here because `pass_with_code` is easy to read as "did not
pass cleanly", and for most of them that is simply wrong: AccuracyCoin uses the
code to say **which of several accepted outcomes occurred**, and for several
tests code 1 IS the canonical answer.

| entry | code | what the code means | disposition |
|---|---|---|---|
| `Controller Clocking` | 2 → **1** | Famicom → NES / AV Famicom | **FIXED v2.6.5** |
| `DMA + $4016 Read` | 2 → **1** | Famicom → NES / AV Famicom | **FIXED v2.6.5** |
| `Sprites On Scanline 0` | 2 | "RGB PPU Detected" | **open — see below** |
| `Implicit DMA Abort` | 2 | "pre-1990 CPU" | revision selection |
| `APU Register Activation` | 2 | the OAM-DMA bus conflict **clocked** the controller ports | **an accepted console variant, closed v2.9.5** |
| `PPU Read Buffer` | 16 | `$41` = ASCII **`G`** — revision-G PPU | the revision this core models |
| `Address $2004 behavior` | 16 | `$41` = ASCII **`G`** | the revision this core models |
| `$93`/`$9F`/`$9B` SHA/SHS | 1 | the test's FIRST success code | a clean pass |
| `DMA + $2002 Read` | 1 | the test's FIRST success code | a clean pass |
| `PPU Reset Flag`, `CPU RAM`, `CPU Registers`, `PPU RAM`, `Palette RAM` | 53 | — | **not tests** |

**`APU Register Activation` code 2 is a console variant, not a defect.** Its
finale reads `$4016` twice and branches on whether the OAM-DMA bus conflict
clocked the controller port. The routine's own comment says *"This used to be
an error code, but different consoles behave differently, so let's just print
if it did or not"*, and it returns code 1 ("OAM DMA Bus Conflict no Clock") or
code 2 ("OAM DMA Bus Conflict Clocks") as two success codes. Read from upstream
`AccuracyCoin.asm` (`TEST_APURegActivation_Finale`) in v2.9.5.

**The five "code 53" entries are not tests.** Each routine opens with
`JSR RTS_If_Running_All_Tests` and its own comment says so: *"This isn't actually
testing anything anyway."* They print the recorded power-on bytes on screen and
return early under `RunningAllTests`, so the byte left in the result slot is
whatever `A` held. There is no clean pass to earn.

**The two "code 16" entries are a revision selection, not a defect.** `$41` is
ASCII `G`, and the ROM writes it as *"Success code 'G', referring to revision G
PPU (or later) behavior"* — the revision this core models. Its counterpart is
`$39` = `E` for pre-revision-G.

**`Sprites On Scanline 0` was the one genuinely open item. v2.9.5 found that
the paragraph below had the wrong half open, and closed it.**

The ROM (upstream `AccuracyCoin.asm`, `TEST_Scanline0Sprites`) runs two sets
of two consecutive frames:

- **`$500`/`$501`:** is there a sprite-zero hit at X=`$80` on scanline 0?
- **`$502`/`$503`:** is there one at X=0?

The result then says:

- **code 2, "RGB PPU":** both `$500` and `$501` hit;
- **code 1, composite:** the two sets alternate, `40 00 00 40` or `00 40 40 00`.

So this core already produces the scanline-0 sprite from stale secondary OAM
(the pre-render line's in-range check reads `261 & 255 = 5`). **What it lacks
is the alternation.**

The mechanism is stated in the forum thread the ROM cites
(`forums.nesdev.org/viewtopic.php?t=26291`, from Visual2C02 analysis). The
sprite shifters are told to start counting on dot 339 and see that signal a dot
late. When an odd frame skips dot 340 of the pre-render line, every shifter
therefore starts scanline 0 still in the drawing state. It outputs its first
pixel at X=0, shifts, and only then begins counting, one dot late. The delayed
count cancels the extra shift, so pixels 1-7 land where they always do, and
only the first pixel moves, to X=0.

In this core that is the dot-339 re-arm (`spr_halted = false`) taking effect
after pixel 0 on a skipped frame. It is one piece of deferred state that lives
from pre-render dot 339 to scanline-0 dot 1. That span contains the frame
boundary, which is exactly where save states and run-ahead snapshots are taken,
so the state must be serialized. `PPU_SNAPSHOT_VERSION` 10 → 11 is a `.rns`
epoch, because the container compares the PPU section's version for equality
(ADR 0028). **Implemented in v2.9.5**, with the epoch taken now at the
maintainer's direction rather than deferred to v3.0.0's. The skip in
`advance_dot` sets `spr_rearm_deferred`, and `emit_pixel` releases the slots
after pixel 0. The PPU snapshot's v11 tail carries it, and
`snapshot_v11_carries_the_deferred_sprite_rearm` pins it. `Sprites On
Scanline 0` now reads code 1, the only AccuracyCoin entry that moved against
v2.9.4, and the per-entry sub-test ROM moved with it.

**Superseded, kept for the record (the text before v2.9.5):**

The ROM reports
"RGB PPU Detected" because this core produces no sprite-zero hit at x=0 on
scanline 0. A composite 2C02 does, and the ROM cites the mechanism
(`forums.nesdev.org/viewtopic.php?t=26291`): the pre-render line is treated as
scanline `261 & 255 = 5` for the in-range checks during the sprite-fetch phase,
so stale secondary-OAM slots whose pixel lands on row 5 load into the shifters
for scanline 0.

Half of that is **already implemented**: `Ppu::tick`'s fetch phase computes
`next_line = prerender_line() & 0xFF` and the shifter `load` gate filters on it,
both tagged for this very test. What suppresses it is `in_use = slot <
spr_count`: evaluation on the pre-render line runs with `next_line = -1`, finds
nothing, and sets `spr_count = 0`, so no slot reaches the `load` gate.

Relaxing `in_use` on the pre-render line alone was **measured and is not
sufficient** — AccuracyCoin held 141/141 and the entry stayed at code 2, because
the stale secondary-OAM content is not present either. The remaining question is
what secondary OAM holds across the pre-render line, which is sprite-evaluation
work and the item this programme's own plan names as its hardest. Deferred with
the evidence rather than attempted on a partial understanding; the experiment was
reverted, not left behind a flag.

## Ignored-test dispositions

Every `#[ignore]`'d test in the workspace at v2.1.0, with its disposition (20
then; the seven permanent historical pins were deleted at v2.9.8, see below).
**None is an
open accuracy gap** — each is either a superseded historical pin, a by-design
revision/fixture/network limitation. The MMC3 `4-scanline_timing` residual
(sub-test 9 from v2.9.9's T-ORACLE-001 fix, sub-test 12 after the dot-324
step) closed with T-MMC3-BG-A12 for v3.0.0.

| Group | Count | Tests | Disposition |
|---|---|---|---|
| Permanent historical pins | 7 → **0** | APU `$4015`-load / reload-arm / `put_cycle` (`apu.rs`), CPU interrupt-dispatch ×3 (`opcodes.rs`), PPU BG-shifter (`ppu.rs`) | Pinned **superseded pre-master-clock** unit assertions on mock buses that could never be un-ignored. **Deleted at v2.9.8** (ADR 0042): the CPU three overrode the removed `poll_irq` / `poll_nmi` hooks, and all seven were records of a scheduler that no longer exists. Real coverage supersedes them: AccuracyCoin 100%, `cpu_interrupts_v2` 5/5 strict, `visual_regression` |
| MMC3 R1/R2 scanline-IRQ | 4 → 3 → **1** | `mmc3_test_v1/6` (alternate-revision assertion, by design) | **Closed in v3.0.0 (T-MMC3-BG-A12)**: both `4-scanline_timing` ROMs pass all 13 sub-tests, so their strict tests run un-ignored and their `_currently_fails` probes are deleted. The background fetches report A12 at the MMC3 page's dot 324, and a visible line's dot 0 drives the background CHR address except where the odd-frame skip replaced scanline 0's (NESdev PPU rendering, "Cycle 0"). **Moved v2.9.9 (T-ORACLE-001)** before that: the NESdev reload rule plus the IRQ output deferred to the next per-cycle hook took both ROMs from sub-test 3 to 9 and made `mmc3_test_v1/5` pass. History, kept: closed for the shipping default under ADR 0002 F5.0 (2026-07-09, refined 2026-07-11); the M2-edge low-time filter tried and refuted at v2.9.5 |
| MMC3 NEC-rev-B | 1 | `mmc3_test_2/6-MMC3_alt` | By-design: only one of the two *opposite* silicon revisions can pass; the project defaults to Sharp rev A (sub-ROM 5 passes strictly) |
| Vs. DualSystem GVS boots | 5 | `vs_dualsystem` boot ×4 + 1 combined-dump diagnostic | Fixture-limited: the staged GVS dumps are the MAME maincpu half only (sub-CPU PRG absent), so boot cannot complete; needs a combined 64 KiB dual dump (see `docs/audit/vs-dualsystem-combined-dumps-2026-07-02.md`) |
| Live-network probes | 2 | `stun_probe`, `turn_probe` | Hit live public STUN / TURN servers; `#[ignore]`'d so CI/offline runs stay hermetic — run manually with `--ignored` |
| HD-pack local | 1 | `hdpack` | Needs a copyrighted local HD pack via `RUSTYNES_HDPACK_LOCAL` |

The 13 in the last five rows are documented in `docs/testing-strategy.md`; the
MMC3 R1/R2 pins (4 until v2.9.9, 3 since) are dispositioned in ADR 0002, whose
2026-10-03 update records the v2.9.9 move (T-ORACLE-001) on top of the F5.0
closure, the v2.1.5 A12-phase study and the v2.9.5 filter refutation.
