Skip to content

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 — AccuracyCoin 141/141 (100.00%), 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) — advanced features (run-ahead / rewind / netplay / TAS / dual save-state) scoped out in dual mode per ADR 0032; wasm/mobile deferred
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) ≤1-CPU-cycle differential on 4 #[ignore]'d sub-tests; zero game impact mmc3_test_2/4 #3 + siblings; mmc3_r1r2_phase_probe A12-phase golden probe (v2.1.5, --features mmc3-a12-phase-probe) CLOSED for the shipping default; axis-B candidate deferred to maintainer (F5.0, ADR 0002). v2.1.5 direct instrumentation refined the closure: "no post-access qualifying rise" is ROM-specific (holds for the two scanline_timing #3 residuals, irq_post=0; false for mmc3_test_v1/5+/6 #2, irq_post=4 — post-access IRQ-clocking rises Session B never measured). Every tested lever stays non-curative (incl. the mmc3-m2-phase-irq deferral, byte-identical status on /5+/6); the four pins stay #[ignore]'d. One untested lever — an ares-style M2-edge-precise falling-edge low-time filter — is deferred to a maintainer decision (needs a sacred-gate-risking substrate change to prototype)
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)

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: four failing ROMs no gate has ever seen (surfaced v2.6.2, NOT yet investigated). Nineteen ROMs sit 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, so that probe cannot distinguish "passed" from "not present". It is the same false oracle that made the NTSC blargg_apu_2005 suite vacuous for five minor releases.

Re-read with the on-screen decoder: nine are genuinely audio-only (apu_dmc_pitch, apu_env, apu_lin_ctr, apu_noise_pitch, apu_phase_reset, apu_square_pitch, apu_sweep_cutoff, apu_sweep_sub, apu_triangle_pitch), and ten report a verdictapu_test_1 through apu_test_10, of which _1, _2, _5 and _6 report TEST FAILED. They settle in 5-6 frames, are plain NROM with CHR-ROM, render correctly, and their SHA-256s differ from every ROM in nes-test-roms/apu_test/rom_singles/, so they are a distinct corpus rather than duplicates of the suite that passes 8/8. They carry no title text, so what each tests is not yet known.

Recorded rather than acted on: identifying and fixing four APU defects is its own work item with its own risk to AccuracyCoin 141/141 and to byte-identity. What is not acceptable is leaving the previous "cannot help" claim standing, because it was reached with a reader that cannot tell a pass from an unmapped address.

Notes

  • 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'."

Ignored-test dispositions (all 20)

Every #[ignore]'d test in the workspace, with its disposition. None is an open accuracy gap — each is either a superseded historical pin, a by-design revision/fixture/network limitation, or the MMC3 residual (closed for the shipping default; one axis-B lever deferred to a maintainer decision).

Group Count Tests Disposition
Permanent historical pins 7 APU $4015-load / reload-arm / put_cycle (apu.rs), CPU interrupt-dispatch ×3 (opcodes.rs), PPU BG-shifter (ppu.rs) Pin superseded pre-master-clock unit assertions on mock buses; the master-clock core is the only scheduler, so these legitimately cannot be un-ignored. Real coverage supersedes them: AccuracyCoin 100%, cpu_interrupts_v2 5/5 strict, visual_regression 7/7
MMC3 R1/R2 scanline-IRQ 4 mmc3_test_2/4 #3, mmc3_test_v1/4 #3, /5 #2, /6 #2 CLOSED for the shipping default (ADR 0002 F5.0, 2026-07-09; refined 2026-07-11) — a ≤1-CPU-cycle differential; zero production-ROM impact; 21+ falsified levers, all tested levers non-curative. The v2.1.5 A12-phase probe (mmc3_r1r2_phase_probe) showed post-access IRQ-clocking rises DO exist on /5+/6 (irq_post=4) — one untested axis-B lever (M2-edge low-time filter) is deferred to a maintainer decision. Fail-loud *_currently_fails companions stay
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 16 in the last five rows are documented in docs/testing-strategy.md; the 4 MMC3 R1/R2 pins are dispositioned in ADR 0002's F5.0 decision-update (closed for the shipping default; the v2.1.5 A12-phase instrumentation study (mmc3_r1r2_phase_probe) refined the rationale and deferred one untested axis-B lever to a maintainer decision).