diff --git a/.github/release-notes/v2.6.5.md b/.github/release-notes/v2.6.5.md
new file mode 100644
index 00000000..1a14ae1c
--- /dev/null
+++ b/.github/release-notes/v2.6.5.md
@@ -0,0 +1,38 @@
+# RustyNES v2.6.5 "Muster"
+
+Rung 5 closes: the AccuracyCoin status vector is identical entry for entry across all 146 entries, with 146 of 146 executed on both sides and none `NotRun`.
+
+A muster is a roll call where every name is called **and answered**. That is this release's acceptance exactly, in two clauses: the vector agrees entry for entry, *and* no entry is unrun on both sides. The second clause is v2.6.4's addition — without it, an identical vector over entries that never executed is a pass, and was one.
+
+## The gate
+
+```text
+coverage: 146 of 146 entries executed on both sides (0 on neither, 0 on one side only)
+status vectors are IDENTICAL entry for entry across all 146 entries.
+```
+
+Measured over the 4500-frame golden, 134,012,761 cycles. At the version's start the same gate read **5 of 146 executed** and 22 differing.
+
+## Five defects, and the shape they share
+
+Four of the five were invisible to every gate that existed when the version opened, and the recurring shape is **a gate agreeing about a question it was never asked**.
+
+**The background shift registers' reload and their shift clock need separate gates.** With one shared gate `BG Serial In` was not merely failing, it was *arithmetically unreachable*: reload dots are absolute, so on a render re-enable the next reload is at most seven dots away, and the reload discards the low seven bits — a serial-in one can never reach bit 7, on any alignment, for any stimulus. Modelling both structures reproduces **both** measured shifter values, the oracle's `F807` falling out of the split model without being fitted to it.
+
+**That fix alone left the gate red.** The sprite X counters are **not** gated on rendering, and AccuracyCoin's `Stale Sprite Shift Registers` test 2 states it outright — "Rendering was disabled for 18 ppu cycles, but the sprite counters were NOT halted during that time". This core froze them, so a disable/enable pair pushed every sprite right by the width of the window. **The ROM that states the rule passes either way**: it expects no hit at X=254, and a sprite shoved 18 dots further right is also off the end of the line.
+
+**The PPUADDR second-write `v <- t` copy is delayed**, and the wiki says so inside the write sequence itself — "wait 1 to 1.5 dots after the write completes". This core committed it in the write's own edge. Swept 1 to 4 dots (all close `Hybrid Addresses`) against a control at 8 and 12 (both fail, which is what proves the parameter reached the compiler); the documented minimum ships.
+
+**The pre-render line clears secondary OAM.** The whole evaluation block — *including* the clear — was gated on `scanline < 240`, so the pre-render line kept what scanline 239 had left and the next frame's scanline 0 drew it. **No sprite can ever render on scanline 0**, because OAM Y is stored one less than the display row. A sprite-0 probe over the full battery named it in one run: 24 hits in 134 M cycles, four of them at scanline 0, one per frame.
+
+The fifth, the octal latch holding across the read dot, is verified by exactly one gate and was unverifiable until the fourth landed — the two compose the hybrid address together and neither produces it alone.
+
+## A diagnosis retracted
+
+The residual was read as a **two-dot CPU/PPU alignment error**, from comparing per-dot record spans across two instruments. Three configurations refute it: at the committed alignment the two consoles execute identical `pc`, `bus_addr` and `bus_access` for **1,695,131 cycles**, while a two-dot power-on shift moves the first divergence back to 593,228 and takes the differing share from 5.13% to 66.80%. The "two dots" was two instruments stamping their records at different points in the cycle — the v2.5.7 lesson, third occurrence.
+
+## Also
+
+`rustynes-cosim`'s `state_trace_records_carry_their_cpu_cycle` was gated on a feature no CI step enabled, so it **ran nowhere** — a regression test the gate could not reach, which is the shape the surrounding CI steps exist to prevent. It now runs, and the test that is genuinely inapplicable under that feature is gated out with its reason rather than left failing.
+
+**The oracle changes on the default path** (a `Controller::write_strobe` owed-shift fix), so **AccuracyCoin 141/141 (RAM decoder)** and nestest 0-diff are **verified, not asserted**.
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b3333e97..f6733bba 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -289,6 +289,22 @@ jobs:
--all-targets -- -D warnings
- name: "test (excluded crate: rustynes-cosim)"
run: cargo test --manifest-path crates/rustynes-cosim/Cargo.toml
+ # ... and again WITH `ppu-state-trace`, because that crate's
+ # `state_trace_records_carry_their_cpu_cycle` test is itself gated on the
+ # feature and so ran NOWHERE in CI. It exists to catch a field that is
+ # present but constant -- the exact defect that shipped when the bus
+ # stamped only one of its two call sites -- and a regression test the gate
+ # cannot reach is the shape this whole block of steps exists to prevent.
+ #
+ # `fast_path_does_not_bypass_the_fetch_trace` is `#![cfg(not(...))]` out
+ # under this feature, which compiles the fast dot path away entirely: with
+ # no fast path to compare against the general one, that test refuses
+ # rather than passing vacuously, and a red gate for a property the build
+ # does not have is noise rather than signal.
+ - name: "test (excluded crate: rustynes-cosim, ppu-state-trace)"
+ run: >
+ cargo test --manifest-path crates/rustynes-cosim/Cargo.toml
+ --features ppu-state-trace
# The four TRACE features, linted explicitly. `--workspace --all-targets`
# covers each crate's DEFAULT feature set only, and the step above lints
# the `rustynes-cosim` package while compiling `rustynes-core` and
diff --git a/.gitignore b/.gitignore
index 8debab74..52ef680a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -47,6 +47,12 @@ Cargo.lock
*.ram_init.bin
*.fetch.bin
*.irq.csv
+# A FIFTH, found the same way in v2.6.5. The sibling's `cpu-bus-gate` writes
+# `.dut.bin` -- the DUT's own capture, compared against `.obs.bin` --
+# and its output path is a variable, so a run pointed at this tree offers a file
+# this list did not name. A 134 M-cycle window is 2.0 GB. Verified to match
+# nothing tracked before being added.
+*.dut.bin
# A FOURTH, found the same way and for the same reason: `apu.bin` arrived with
# rung 4 (the 2A03's per-cycle channel levels) at v2.5.9 and was never added, so
# every APU golden the sibling's gates consume has been offered for commit since.
diff --git a/AGENTS.md b/AGENTS.md
index dd7f5db9..d427cbcd 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -43,7 +43,7 @@ Enforcement lives alongside the prose: `/ref-proj/` is gitignored/`.dockerignore
RustyNES is a cycle-accurate Nintendo Entertainment System emulator written in pure Rust. The accuracy bar is Mesen2 / higan / ares: tight lockstep scheduling at PPU-dot resolution on a master-clock-precise timebase, sub-instruction PPU events visible to subsequent CPU code, and a lookup-table non-linear audio mixer with band-limited synthesis. The frontend is pure Rust (`winit` + `wgpu` + `cpal` + `egui`).
-**Current release: v2.6.4 "Rubric"** (2026-08-26) — OAM DMA lands and all nine AccuracyCoin disagreements close, every rule that closed the last three stated by the test ROM and by neither nesdev page — and then the gate that certified them is measured to cover 88 of 146 entries. The emulation core is unchanged. Built on **v2.6.3 "Mainspring"** (2026-08-25) — the DUT runs on one master clock, and four enables that were never enabling — plus AccuracyCoin end to end and a status vector that names its disagreements by test. The emulation core is unchanged. Built on **v2.6.2 "Witness"** (2026-08-24) — rung 4 closes: blargg APU battery 11/11 on the co-simulation DUT, six defects no self-written gate could see, and a suite that had been asserting nothing for five minor releases. The emulation core is unchanged. Built on **v2.6.1 "Interleave"** (2026-08-24) — the DMC and its DMA cycle steal in the MiSTer co-simulation DUT, cycle-exact on the bus. The emulation core is unchanged. Built on **v2.6.0 "Assay"** (2026-08-24) — the triangle, the noise channel and the sweep unit **in the MiSTer co-simulation DUT** — and an audit of how much of the APU was fitted to the oracle rather than derived from documentation. The emulation core is unchanged. Built on **v2.5.9 "Overture"** (2026-08-24) — rung 4 opens: the two pulse channels, the frame counter, and four ROM defects the stimulus measurement found first. Built on **v2.5.8 "Blanking"** (2026-08-24) — VBlank, NMI and the PPUSTATUS race close rung 3 — and both fixes were deletions. Built on **v2.5.7 "Collimation"** (2026-08-24) — sprite rendering closes exact — the phase was wrong by two dots, and every window was compensating. Built on **v2.5.6 "Vestige"** (2026-08-23) — Sprite evaluation closes: all 59,993 overlapping cycles match, nine of nine behavioural mutants caught and two proved inert (announced as seven of eight at the cut), and the fix is a byte index that outlives the walk that set it. Built on **v2.5.5 "Raster"** (2026-08-23) — the first full frame, and three blind spots in the stimulus that fed it. Built on **v2.5.4 "Escapement"** (2026-08-23) — the background fetch pipeline, and an access two dots early that five gates could not see. Built on **v2.5.3 "Hysteresis"** (2026-08-23) — toggling rendering takes effect three dots after the write, and four instruments to prove it. Built on **v2.5.2 "Dormant"** (2026-08-23) — the 2C02 register file, and a gate that passed while testing nothing. Built on **v2.5.1 "Retrace"** (2026-08-23) — the interrupt sweep closes rung 2, and a gate reported a pass it could not have earned. Built on **v2.5.0 "Rungwork"** (2026-08-23) — the 6502 rung, and the two gates it cannot reach. Built on **v2.4.9 "Plumbline II"** (2026-08-23) — the bus half of rung 2, and what it found the day it existed. Built on **v2.4.8 "Palimpsest"** (2026-08-23) — read-modify-write, and a gate that cannot see its own subject. Built on **v2.4.7 "Keystone"** (2026-08-23) — the stack closes, and a dead line proves itself dead. Built on **v2.4.6 "Abacus"** (2026-08-22) — the core learns arithmetic. Built on **v2.4.5 "Compass"** (2026-08-22) — the core reaches memory, and chooses. Built on **v2.4.4 "Ignition"** (2026-08-22) — the first real RTL. The 6502's eight-cycle reset and the seventeen single-byte implied opcodes, in SystemVerilog in the sibling repository (`RustyNES_MiSTer@7f092bd`), matching the oracle on all seven CPU fields -- 29 records, and the gate demonstrated to fail on four mutations. The DUT is the **third writer** of the oracle's `CpuBootTrace` format, so `cpu_boot_trace_diff` reads it with no modification and the rung needed no oracle-side change at all. **The oracle settled a question our own prose could not**: reset is EIGHT cycles, and `docs/cpu-6502.md` said both seven and eight -- corrected here. A mutation the test ROM was built to catch came back NOT CAUGHT because `TSX` leaves exactly the flags a wrongly-flagging `TXS` would compute, and a harness bug made every mutation report a catch including the baseline. The emulation core is untouched. Built on **v2.4.3 "Touchstone"** (2026-08-22) — what the synthesiser accepts, and what the licence requires. A touchstone is a stone you rub gold against; the streak tells you what the metal actually is. This release settles the **two Fabric-plan risks that had to be answered before any RTL exists**, and both were answered by evidence that contradicted what the plan assumed. **Risk 4, the Quartus subset, is FITTED**: Quartus Prime Lite 17.0.2 Build 602 on a 5CSEBA6U23I7 produced a placed-and-routed netlist with **0 synthesis warnings**, and the 2 KiB array inferred as **2 M10K blocks with 29 total registers** — not 16,413 — from the source style alone, no `ramstyle` attribute. The `initial` block became a real MIF (so a boot ROM lands inside the block) and the `enum` was one-hot encoded. Nine constructs are promoted to *fitted*; plain `case`, `priority case` and `$bits` are deliberately left *documented* because the kitchen sink does not exercise them. **Risk 1, the `sys/` licence, inverts the plan's own hedge**: 57 files, **zero GPL-2.0-only**, and `hps_io.sv` — GPL-3.0-or-later and not optional, since it is how a core receives a ROM and reaches the OSD — forces the combined bitstream **up** to GPL-3.0-or-later, already RustyNES's licence. The emulation core is untouched. Built on **v2.4.2 "Cairn"** (2026-08-22) — the **rung-0 compare surface**. A cairn is a marker set along a route so you can tell you are still on it, which is what a rolling per-cycle hash checkpoint is. The constraint nobody budgets for in co-simulation is trace *volume*, not simulation time, and it is now **measured**: 3 frames of AccuracyCoin is 89,343 CPU cycles, **5,372,427 bytes** of `irq.csv` against **352 bytes** of `ckpt.bin` — a factor of **15,263** — so both sides chain a hash and compare every 4096 cycles, and only the divergent window is re-run with full capture. **What is hashed is a decision about hardware, not about convenience**: `CycleRecord` carries 29 fields and most are RustyNES's *model*, so `Observable` is the subset a device can genuinely produce, the IRQ pair is OR'd before hashing because hardware has one wire-OR'd /IRQ pin, and `pc` is marked DUT-observable rather than pin-observable. The emulation core is untouched. Built on **v2.4.1 "Fabric"** (2026-08-20) — the **oracle** release, opening the **v2.4.1 → v2.5.0 "Fabric"** line: a new NES core written in SystemVerilog from public hardware documentation, in a sibling repository, with this emulator as its **verification oracle**. **RustyNES is not being ported to FPGA and cannot be** — a MiSTer core is SystemVerilog compiled by Quartus 17.0.2 into a Cyclone V bitstream, and high-level synthesis of a cycle-accurate emulator's control flow does not produce usable hardware; what is buildable is a NEW implementation verified against this one. `crates/rustynes-cosim` is the boundary — a narrow C ABI a Verilator testbench links, plus a `nes_golden_export` CLI emitting five golden formats. **The provenance firewall extends to HDL** (ADR 0037): `NES_MiSTer` and `fpganes` `rtl/` are strict black boxes — instantiating one as an opaque testbench module to compare OUTPUTS is permitted, reading its source is not; anything unimplementable from documentation escalates to an ADR BEFORE any source is opened. Three design decisions are locked and each has a reason: **replay, not lockstep** (`Nes` exposes `run_frame()` and `step_instruction()` and nothing finer, and the determinism contract already makes a pre-recorded trace exactly the trace a lockstep run produces), **no DPI-C** (it puts `` `ifdef SIMULATION `` guards into RTL that must also pass Quartus — the construct that lets a simulated netlist drift from the synthesised one), and **hash first, capture on divergence** (a 4200-frame AccuracyCoin run is ~125 M cycles, ~7.5 GB as per-cycle CSV against ~480 KB of 4096-cycle checkpoints). The golden framebuffer is exported **pre-palette** so a palette difference cannot masquerade as a rendering one. **v2.5.0 is scoped to "the 6502 rung closes"**, not a finished core (7–13 months FTE for a full one), and two risks are accepted in writing: `NES_MiSTer` scores 121/125 on AccuracyCoin where real Famicom AV hardware also scores ~121/125, so there is no published accuracy headroom and **the core may be declined as a duplicate**; and **the oracle can be wrong**, since 141/141 is not "matches silicon" — every rung is labelled by whether it has an INDEPENDENT oracle. **The exclusion of `rustynes-cosim` from the workspace is the load-bearing detail, and it exposed a defect in the accuracy gate itself.** The crate enables `cpu-boot-trace` and `irq-timing-trace` on `rustynes-core`, and cargo unifies features across a workspace build, so as a MEMBER it made `cargo build --workspace` compile the core ONCE with the union — measured through `--message-format=json`, not inferred. `irq-timing-trace` is not an inert branch: it selects a **different** `for sub_dot in 0..3` loop in `Bus::tick_one_cpu_cycle`, so CI's `cargo test --workspace --release --features test-roms` — the accuracy battery — was validating a scheduler no user runs, the same shape as the v2.3.4 defect where the coverage harness tested a load path no user runs. The measured cost was **+1.24% / +1.39% / +1.89%** across the three `full_frame` benches, *below* this project's own 3% adoption bar, and it never touched the shipped binary or the perf gate — published precisely because it shows performance was never the argument. Exclusion has a price (an excluded package cannot use `field.workspace = true`, and `--workspace` no longer reaches it), and both halves are closed mechanically: `cosim_manifest_audit.rs` asserts every duplicated field and lint still equals the workspace's AND that the crate is still excluded (four mutations, all caught), and CI gains explicit `fmt`, `clippy` and `test` steps — the clippy step earning its place on its first run with a `must_use_candidate` `--workspace` had never surfaced. Two more findings the crate was not looking for: **the first `run_frame()` after power-on advances ZERO cycles** (the PPU is constructed at dot 340 of the pre-render line, so the seven-cycle reset ticks past the frame wrap and leaves `frame_complete` latched — gate on `Nes::frame()`, never the call count, or a `--frames 60` loop emits a 59-frame golden under a manifest claiming 60), and **no CI invocation had ever enabled `cpu-boot-trace` or `irq-timing-trace` for clippy**, so those two core modules had never passed the lint gate (six pre-existing findings; `--workspace --all-targets` covers each crate's DEFAULT feature set only). **It also carries v2.4.0 "Concordance", which merged to `main` and was never tagged**: the seven-property atomic-write sequence v2.3.9 built for `Config::save_to` is extracted into `crate::atomic_write` and adopted everywhere — the plan named three call sites and there were FOUR, the fourth being `save_state.rs`, where a truncated write is a user's game progress, while `per_game.rs` was not in the plan at all because it LOOKS correct (it renames a sibling temp file) and held two of seven: no `fsync`, and a FIXED scratch name shared across every process. Review then found **four more places the module reported success it had not earned**, each an error discarded under a comment explaining the rest of the operation: `set_permissions` swallowed (the mode applied is the one the target ALREADY had, so a failure widens a 0600 file to the umask default), the parent-directory `fsync` swallowed together with its `File::open` (so the whole barrier could be a no-op while the module's table claimed "yes", and `EIO` passed as success), a ONE-attempt occupied-scratch retry (justified by "the counter cannot repeat a name within a process", which is true and beside the point — the collision comes from a previous process whose pid was reused), and an exhaustion cleanup that deleted a file this process had not created. Plus **a `const fn` that only failed on Windows** — `is_transient_rename_error` was `const` and called `io::Error::kind`, which is not, behind `#[cfg(windows)]`, so it compiled clean on Linux and would have turned `main` red AFTER merge; the fix moved the predicate into an always-compiled function reached through `cfg!(windows) && …`, so restoring the `const` now fails on Linux. Also v2.4.0: `Nes::timeline_generation()`, a session-local counter deliberately NOT in the save state (serializing it would make a second load of the same slot restore the same generation, so a consumer would miss it — and because it lives outside the snapshot, `snapshot_schema_audit` cannot see it); the cheat save reporting its failure in the panel instead of a `stderr` nobody reads on a windowed build; and `release_anchor_audit.rs`, pinning 15 release anchors across 10 documents. It is **not** in the v2.3.9 tag — v2.3.9 corrected the eight drifted documents BY HAND, which is what its notes describe and all they claim; the standing gate merged afterwards in #427. (v2.4.1's notes as first published asserted that v2.3.9's body described the audit. It does not; that claim is retracted.) `rustynes-core` changes in both halves, so **AccuracyCoin 141/141 (100.00%, RAM decoder) and nestest 0-diff are VERIFIED, not asserted.** Built on **v2.3.9 "Crucible"** (2026-08-20) — the **gates** release. A crucible tests something to destruction rather than inspecting it, and that is what this release does to the project's own checks: what they cover, what they only *appear* to cover, and where a regression could still reach `main` unchallenged. The v2.3.x line added five tools in four releases and the recurring finding across all of them was never that the emulation was wrong — it was that **a check reported a pass it had not earned**. **The docs-only CI skip had never worked**: `dorny/paths-filter`'s `predicate-quantifier` defaults to `some`, which includes a file if it matches ANY pattern, so the `code` filter's leading `'**'` matched everything and all seven `!` exclusions under it were DEAD from the day they were written — proven from a run rather than the docs (a one-file markdown PR logged `Filter code = true` / `Matching files: AGENTS.md`). Every documentation PR in the project's history had been running the full matrix, and that stopped being merely wasteful the day two docs-only PRs were *blocked* by an ARM cross-compile failure on jobs that should never have been scheduled. Fixed with **two** filter steps because the quantifier is step-level and the two filters need OPPOSITE settings: `code` needs `every`, while `accuracy` is a list of **alternatives** and becomes unsatisfiable under it — the naive one-line fix would have silently disabled the accuracy battery while repairing a different gate. Both directions are now observed on real PRs. **The accuracy battery now runs at review time** — `test-roms` was full-run-only, so a regression landed on `main` rather than on the PR that caused it; it is now also path-filtered over the chip crates, the core, `rustynes-gamedb` (it rewrites the iNES header on load, so it changes what the emulator *is* before a cycle runs), the harness and `tests/`, measured first at 11 of the last 40 merged PRs so ~72% still pay nothing. **Bounds were calibrated against a measurement rather than a claim**: the ARM provisioning step failed on three consecutive PRs with NO apt error in the log at all, and the real number was `Fetched 4201 kB in 4min 45s (14.7 kB/s)` — three orders of magnitude below normal, which made the previous ~40 MB package set *hopeless* rather than unlucky (~45 minutes; no timeout could have saved it); it was also installing a whole cross toolchain to obtain `libc6-dev--cross`, which the comment above it had already named, because bindgen runs the **host** clang against `--sysroot` and never invokes the cross compiler. **A freeze from one cartridge kept writing into the next** — not a stale label but an active per-frame write into the wrong game, because both memory panels' freezes feed the raw-cheat overlay applied after every frame and neither was registered with the ROM-transition hook; the sweep that closed it now covers every panel under ONE rule: **derived output is discarded, user-authored input is kept, and only input that actively *writes* is neutralised** (so RAM Search baselines and reconstructed call stacks clear, while watch lists and breakpoints survive and breakpoints stay ARMED — a breakpoint halts, visible and recoverable, where a freeze writes, silent and continuous). Two negatives are recorded because they cost time to establish: the header editor LOOKS ROM-bound and is not (it is a standalone file tool), and the event panel / trace status / HD-pixel coordinates are per-frame state or preferences. **The config file is written atomically and durably** — `fs::write` truncates then writes, and saves became automatic (closing a ROM, moving a mixer slider, finishing a Latency Oracle measurement), so an interruption left the user holding a truncated `config.toml`; seven properties, and **five came from review rather than the first draft** (sibling scratch file, `fsync` before rename, parent-directory sync, `create_new(true)` for CWE-377, mode applied at creation, symlink resolution including a **broken** link, and a pid + per-call counter — the last is what makes exclusive creation adoptable at all). **Two shipped features told the truth for the first time**: movies record TWO ports (`FrameInput` models P1 and P2) while the Replay panel printed "Four Score (P1..P4)" at the moment a user decides to press Record — widening the format is a `.rnm` epoch change, so it is disclosed at three levels with the caveat printed directly under the claim it qualifies; and a failed Latency Oracle save now says so instead of being swallowed (remembering is still NOT applying — nothing touches `run_ahead`, and an inconclusive result is not remembered at all). Also: **257 lines of dead code removed** — an APU pair (34), a closed `LockstepBus` DMA-service island (183), and `drain_dma` (40), a function called on every CPU read, every CPU write and every bus cycle whose entire body was `let _ = read_addr;` and whose comments claimed the legacy service below it "stays active for the default build" — alongside **25 of 29 `#[allow(dead_code)]` attributes suppressing nothing**, established by stripping them and re-running clippy across all EIGHT gated combinations (an item can be live by default and dead on wasm, which is precisely the case that would have earned the attribute); the **SAFETY-comment rule is now a gate** (`clippy::undocumented_unsafe_blocks` — all 91 unsafe sites already carried a justification, two had it where a human reads correctly and a checker cannot, and the lint is demonstrated to fail); and two `cargo deny` advisory ignores retired on their own stated condition (their entry said to remove them once the resolve moved past quick-xml 0.40, and it had). `rustynes-apu` and `rustynes-core` both change, so **AccuracyCoin 141/141 (100.00%, RAM decoder) and nestest 0-diff are VERIFIED, not asserted** — and re-run AGAIN after the second round of deletions rather than only after the first. Built on **v2.3.8 "Parallax"** (2026-08-20) — **which pixels differ, not just which frame**. Parallax is the apparent shift of an object seen from two positions, and the displacement is the measurement. `Probe` could already say whether two configurations of the same ROM diverge and AT WHICH FRAME, and could say nothing about where or why: a trial reduces each frame to one `u64`, the right shape for *detecting* a difference and the wrong shape for *explaining* one — a hash says frame 412 differs and has nothing to hand to Pixel Provenance, which is where an answer actually lives. `divergence::localise` re-runs both configurations to the detected frame, keeps the full output instead of its hash, and reports the **shape** of the difference — population count, first pixel in raster order, and the inclusive bounding box — which separates kinds of bug from each other (one pixel is a sprite or a palette entry, 256 in a row is a scanline, tens of thousands is a scroll or a mode change); `is_single_scanline` is offered rather than left to call sites because the inclusive comparison is easy to get wrong. It localises on the **index** framebuffer (256x240 `u16`s of `(emphasis << 6) | colour`, the PPU's own per-pixel output before the palette lookup) — half the bytes and at least as sensitive, since the RGBA buffer is a pure function of it given the same palette. Three answers, and the third is the point: `Identical`, `Differs`, and **`Inconclusive`** for an exhausted budget or two trials that cannot be compared — the Latency Oracle precedent applies directly, "I stopped looking" must not arrive wearing the same shape as "they agree" — and the budget is checked UP FRONT for all four trials, so spending two on detection and then finding the localisation pair unaffordable cannot consume the budget that would have answered the question. Beyond locating a difference the Lens **explains** it: trial-scoped provenance capture hands a located pixel to the machinery that already answers "what wrote this, and from which instruction", and an audio lens resolves a divergence to the CPU cycle. One defect was found and fixed inside the same work — the Lens left the emulator **thirty frames ahead** of where it started, because a trial restores the anchor on the way IN and not on the way OUT (deliberate — it is what lets the Lens read the trial's final frame off `nes` directly) and the outermost caller has to put the timeline back, and did not. Cut from its own boundary commit (#407's merge) rather than from `main`, so its artifacts contain exactly the Divergence Lens. Built on **v2.3.7 "Overtone"** (2026-08-19) — the **audio-provenance** release. The APU counterpart of Pixel Provenance: a per-register write attribution answering *what wrote this, and from which instruction*, and a per-CPU-cycle mix trace answering *what were the channels actually doing* — per CPU cycle rather than per output sample, because that is the cadence at which the mix is genuinely computed, and carrying **raw** pre-mix channel values so a record describes the chip rather than the user's mixer sliders. Surfaced at **Tools → Audio → Audio Provenance**; output-only, runtime-default-off, not serialized. **Its subject is the trap it inherited.** Pixel Provenance shipped non-functional for four releases because run-ahead's per-frame rollback cleared its store after the visible frame was harvested and before the frontend released the emulator lock, so the carry landed **in the same change as the feature** here rather than after a bug report. That enumeration was then found to be incomplete: `rustynes-probe` has **three more** same-timeline restores — `Probe::run_uncounted` (once per trial, and a latency measurement runs up to **21**), `latency::measure_in_place` (the final restore, outside every per-trial guard), and the RAM Atlas panel's `TimelineGuard` — none of which used the stash, so **running the Latency Oracle or the RAM Atlas emptied both provenance panels**. Both stores are cumulative, so the records were not rebuilt by the next frame; they were gone for the session. The test named for the contract, `measure_in_place_restores_the_live_timeline`, compares `nes.snapshot()` and provenance is deliberately **not** in the snapshot — it asserted something strictly weaker than its own name and passed throughout. Closed by moving the stash into a shared `TrialGuard`, pinned by four independent mutations. **`$4014` and `$4016` were documented as attributed and were not** — the bus handles both without routing through `Apu::write_register`. **Two defects were caught by measurement rather than reading:** `apu_throughput`, built for this release, reshaped the plumbing **three times** on regressions invisible in the diff (the bench itself had to be corrected first — it omitted an end-of-cycle pair worth ~23% of true per-cycle cost), and a randomized sweep of the save-state parse boundary found **four** panics in VRC7's OPLL where hand-tracing found one, because the maximally-hostile all-`0xFF` payload set `update_requests` to all-ones and **concealed** an `eg_shift` panic. Also fixed: the **browser demo applied no per-game header corrections**, *Rad Racer*'s roadside artifact (a hybrid address spliced from a stale `v`), VRC7 save states dropping the live FM synthesizer so rewind garbled the music, and **no CI job carried a timeout** — one hung job silently skipped a release for five hours. `rustynes-apu` and `rustynes-core` both change, so **AccuracyCoin 141/141 (100.00%, RAM decoder) and nestest 0-diff are VERIFIED, not asserted.** Built on **v2.3.6 "Sounding"** (2026-08-17) — about **measuring, and what a measurement is allowed to claim**. **Two shipped features are found never to have worked.** Pixel Provenance (the v2.3.2 marquee) returned an empty report for every user on the default `run_ahead = 1`: run-ahead's per-frame rollback is the LAST thing before the frontend releases the emulator lock, so the panel's first look was always *after* the wipe — and "click any pixel" was **never implemented** (two `DragValue` spinboxes; the only `Sense` in the file was `hover()` on a colour swatch). **Two source comments and four doc claims asserted the opposite of their own code**, which is why four releases passed unchecked. And **Duck Hunt could never score**: its protocol is "see NOTHING for one frame, then a bright spot in the next", and the light bit was sampled at end-of-frame, so a read during frame N returned frame N-1 — the probe **exactly inverted** (000000 -> 000500). Two new tools, both built to **decline rather than guess**: the **Latency Oracle** (replays one moment with a button held and without it; `None` and `Some(0)` are different answers never collapsed; `START` excluded because pausing is a reaction to a *menu*; **recommends a run-ahead depth and never applies one**) and the **RAM Atlas** (classifies all 2 KiB of work RAM, then VERIFIES a candidate by perturbing it — observation returns all 2048 labels as `Untested` so it is *structurally* incapable of claiming an effect; liveness is relative to its lens and every verdict names it; `Inert` is documented as NOT meaning unused). **APU Workstream D is CLOSED** — the 18.7%-of-frame figure stands, but it is not recoverable by gating per-cycle bookkeeping: one adoption, three measured rejections, one declined on inspection, two left unmeasured deliberately. Tools and Debug are regrouped by task (Tools had reached twenty flat entries). The core gains one `const fn` getter and nothing else, so **AccuracyCoin 141/141 and nestest 0-diff are VERIFIED, not asserted**. **NOT fixed here:** `libretro/docs#1180` (the licence on the libretro docs site) is still open upstream. Built on **v2.3.5 "Manifest"** (2026-08-16) — about **what the core declares about itself**. A user reported RetroArch still showing the pre-relicense MIT/Apache-2.0 terms. It does: RetroArch reads `dist/info/rustynes_libretro.info` from **`libretro/libretro-super`**, a SEPARATE copy from this repo's that nothing syncs and nothing compared, so the v2.2.9 GPL relicense never reached it (both upstream PRs merged 2026-07-21, exactly two weeks BEFORE the 2026-08-04 relicense). The repo-side half is corrected here — `GPLv3+`, since libretro uses short tokens and marks "or later" with a trailing `+` (tallied across all 316 upstream cores) — plus a standing `libretro_info_audit.rs` that pins the local file against the workspace manifest AND the core's own `retro_get_system_info`, making the upstream sync a **copy** rather than a re-derivation. **A licence change is now a mandatory upstream-sync trigger.** Auditing the wrapper then found **five further defects, every one with correct emulation behind it**: a hardcoded 60.0988 fps for every cartridge plus `retro_get_region` unimplemented (**PAL/Dendy ran 20.2% fast**), `retro_reset` unimplemented so **RetroArch's Reset did nothing, ever** (the library default is a literal no-op), `retro_unload_game` unimplemented (Game Genie *indices* leaked across cartridges), `aspect_ratio = 0.0` (square pixels, against the desktop frontend's 8:7), and no controller info so the **Zapper was unreachable** despite `Nes::set_zapper` being fully implemented. Review caught a **use-after-free**: RetroArch shallow-`memcpy`s the outer `retro_controller_info` array but RETAINS each `types` pointer, so the description tables must be `'static` (`SET_INPUT_DESCRIPTORS` is different and safe — never generalize between environment calls). The crate went from **zero tests to eight**. Separately the APU (**18.7% of frame time**, invisible to a symbol profile because fat LTO inlines it into `cpu_clock`) gained its first throughput bench and a default-configuration mix specialization, **−3.3% to −4.2%** on `nes_run_frame_nestest`, byte-identical by construction. Declared values are now DERIVED from `rustynes_core` constants (`FRAME_DURATION_*`, `DEFAULT_SAMPLE_RATE`) rather than transcribed. Audio stays **44,100 Hz** — a matched-normalized-frequency SFDR comparison shows 44.1k and 48k are equivalent (81.6 vs 82.2 dB), so nothing is gained, and 44,100 is the only rate this project's audio is verified at. Shipped OUTPUT byte-identical, but the APU *implementation* did change (the mix specialization is a strict specialization, not a no-op), so **AccuracyCoin 141/141 and nestest 0-diff were VERIFIED, not asserted**. **NOT fixed by that release, and since RESOLVED upstream:** RetroArch showed the wrong licence until `libretro-super#2069` merged (2026-08-16 — it now reads `GPLv3+`), and RustyNES did not appear on iOS/iPadOS/tvOS until `RetroArch#19416` merged (2026-08-16, `76f60626984a` — `rustynes` is now line 268 of `pkg/apple/update-cores.sh`, between `reminiscence` and `sameboy`). Being in the build list is not the same as being installable: it arrives with the next App Store RetroArch build, on libretro's cadence. Only `libretro/docs#1180` remains open.
+**Current release: v2.6.5 "Muster"** (2026-08-29) — rung 5 closes — the AccuracyCoin status vector is identical entry for entry across all 146 entries, with 146 of 146 executed on both sides and none NotRun, where the same gate read 5 of 146 at the version's start. A muster is a roll call where every name is called AND answered, which is the two-clause acceptance exactly. Five PPU defects close the last six differing entries and four were invisible to every gate that existed when the version opened: the background shift registers' RELOAD and their shift clock need SEPARATE gates (with one shared gate the serial-in test was not merely failing but ARITHMETICALLY UNREACHABLE, since reload dots are absolute and the reload discards the low seven bits, so a serial-in one can never reach bit 7 on any alignment — and modelling both structures reproduces BOTH measured shifter values); the sprite X counters are NOT gated on rendering, which AccuracyCoin states outright and the ROM that states it passes either way, because it expects no hit at X=254 and a sprite shoved 18 dots right is also off the line; the PPUADDR second-write v-copy is DELAYED, as the wiki says inside the write sequence itself, swept 1 to 4 dots against a control at 8 and 12 that fails; and the pre-render line CLEARS secondary OAM, without which scanline 0 draws what scanline 239 left — no sprite can ever render on scanline 0, because OAM Y is one less than the display row, and a sprite-0 probe over the full 134 M-cycle battery found 24 hits with four of them there; and the octal latch holding across the read dot, which is verified by exactly ONE gate and was unverifiable until the v-copy delay landed, the two composing the hybrid address together and neither producing it alone. A DIAGNOSIS IS RETRACTED: the residual was read as a two-dot CPU/PPU alignment error from comparing dot spans across two instruments, and at the committed alignment the two consoles execute identical pc, bus_addr and bus_access for 1,695,131 cycles while a two-dot shift moves the first fork back to 593,228 and takes the differing share from 5.13% to 66.80%. The oracle changes on the default path, so AccuracyCoin 141/141 (RAM decoder) and nestest 0-diff are VERIFIED, not asserted. Built on **v2.6.4 "Rubric"** (2026-08-26) — OAM DMA lands and all nine AccuracyCoin disagreements close, every rule that closed the last three stated by the test ROM and by neither nesdev page — and then the gate that certified them is measured to cover 88 of 146 entries. The emulation core is unchanged. Built on **v2.6.3 "Mainspring"** (2026-08-25) — the DUT runs on one master clock, and four enables that were never enabling — plus AccuracyCoin end to end and a status vector that names its disagreements by test. The emulation core is unchanged. Built on **v2.6.2 "Witness"** (2026-08-24) — rung 4 closes: blargg APU battery 11/11 on the co-simulation DUT, six defects no self-written gate could see, and a suite that had been asserting nothing for five minor releases. The emulation core is unchanged. Built on **v2.6.1 "Interleave"** (2026-08-24) — the DMC and its DMA cycle steal in the MiSTer co-simulation DUT, cycle-exact on the bus. The emulation core is unchanged. Built on **v2.6.0 "Assay"** (2026-08-24) — the triangle, the noise channel and the sweep unit **in the MiSTer co-simulation DUT** — and an audit of how much of the APU was fitted to the oracle rather than derived from documentation. The emulation core is unchanged. Built on **v2.5.9 "Overture"** (2026-08-24) — rung 4 opens: the two pulse channels, the frame counter, and four ROM defects the stimulus measurement found first. Built on **v2.5.8 "Blanking"** (2026-08-24) — VBlank, NMI and the PPUSTATUS race close rung 3 — and both fixes were deletions. Built on **v2.5.7 "Collimation"** (2026-08-24) — sprite rendering closes exact — the phase was wrong by two dots, and every window was compensating. Built on **v2.5.6 "Vestige"** (2026-08-23) — Sprite evaluation closes: all 59,993 overlapping cycles match, nine of nine behavioural mutants caught and two proved inert (announced as seven of eight at the cut), and the fix is a byte index that outlives the walk that set it. Built on **v2.5.5 "Raster"** (2026-08-23) — the first full frame, and three blind spots in the stimulus that fed it. Built on **v2.5.4 "Escapement"** (2026-08-23) — the background fetch pipeline, and an access two dots early that five gates could not see. Built on **v2.5.3 "Hysteresis"** (2026-08-23) — toggling rendering takes effect three dots after the write, and four instruments to prove it. Built on **v2.5.2 "Dormant"** (2026-08-23) — the 2C02 register file, and a gate that passed while testing nothing. Built on **v2.5.1 "Retrace"** (2026-08-23) — the interrupt sweep closes rung 2, and a gate reported a pass it could not have earned. Built on **v2.5.0 "Rungwork"** (2026-08-23) — the 6502 rung, and the two gates it cannot reach. Built on **v2.4.9 "Plumbline II"** (2026-08-23) — the bus half of rung 2, and what it found the day it existed. Built on **v2.4.8 "Palimpsest"** (2026-08-23) — read-modify-write, and a gate that cannot see its own subject. Built on **v2.4.7 "Keystone"** (2026-08-23) — the stack closes, and a dead line proves itself dead. Built on **v2.4.6 "Abacus"** (2026-08-22) — the core learns arithmetic. Built on **v2.4.5 "Compass"** (2026-08-22) — the core reaches memory, and chooses. Built on **v2.4.4 "Ignition"** (2026-08-22) — the first real RTL. The 6502's eight-cycle reset and the seventeen single-byte implied opcodes, in SystemVerilog in the sibling repository (`RustyNES_MiSTer@7f092bd`), matching the oracle on all seven CPU fields -- 29 records, and the gate demonstrated to fail on four mutations. The DUT is the **third writer** of the oracle's `CpuBootTrace` format, so `cpu_boot_trace_diff` reads it with no modification and the rung needed no oracle-side change at all. **The oracle settled a question our own prose could not**: reset is EIGHT cycles, and `docs/cpu-6502.md` said both seven and eight -- corrected here. A mutation the test ROM was built to catch came back NOT CAUGHT because `TSX` leaves exactly the flags a wrongly-flagging `TXS` would compute, and a harness bug made every mutation report a catch including the baseline. The emulation core is untouched. Built on **v2.4.3 "Touchstone"** (2026-08-22) — what the synthesiser accepts, and what the licence requires. A touchstone is a stone you rub gold against; the streak tells you what the metal actually is. This release settles the **two Fabric-plan risks that had to be answered before any RTL exists**, and both were answered by evidence that contradicted what the plan assumed. **Risk 4, the Quartus subset, is FITTED**: Quartus Prime Lite 17.0.2 Build 602 on a 5CSEBA6U23I7 produced a placed-and-routed netlist with **0 synthesis warnings**, and the 2 KiB array inferred as **2 M10K blocks with 29 total registers** — not 16,413 — from the source style alone, no `ramstyle` attribute. The `initial` block became a real MIF (so a boot ROM lands inside the block) and the `enum` was one-hot encoded. Nine constructs are promoted to *fitted*; plain `case`, `priority case` and `$bits` are deliberately left *documented* because the kitchen sink does not exercise them. **Risk 1, the `sys/` licence, inverts the plan's own hedge**: 57 files, **zero GPL-2.0-only**, and `hps_io.sv` — GPL-3.0-or-later and not optional, since it is how a core receives a ROM and reaches the OSD — forces the combined bitstream **up** to GPL-3.0-or-later, already RustyNES's licence. The emulation core is untouched. Built on **v2.4.2 "Cairn"** (2026-08-22) — the **rung-0 compare surface**. A cairn is a marker set along a route so you can tell you are still on it, which is what a rolling per-cycle hash checkpoint is. The constraint nobody budgets for in co-simulation is trace *volume*, not simulation time, and it is now **measured**: 3 frames of AccuracyCoin is 89,343 CPU cycles, **5,372,427 bytes** of `irq.csv` against **352 bytes** of `ckpt.bin` — a factor of **15,263** — so both sides chain a hash and compare every 4096 cycles, and only the divergent window is re-run with full capture. **What is hashed is a decision about hardware, not about convenience**: `CycleRecord` carries 29 fields and most are RustyNES's *model*, so `Observable` is the subset a device can genuinely produce, the IRQ pair is OR'd before hashing because hardware has one wire-OR'd /IRQ pin, and `pc` is marked DUT-observable rather than pin-observable. The emulation core is untouched. Built on **v2.4.1 "Fabric"** (2026-08-20) — the **oracle** release, opening the **v2.4.1 → v2.5.0 "Fabric"** line: a new NES core written in SystemVerilog from public hardware documentation, in a sibling repository, with this emulator as its **verification oracle**. **RustyNES is not being ported to FPGA and cannot be** — a MiSTer core is SystemVerilog compiled by Quartus 17.0.2 into a Cyclone V bitstream, and high-level synthesis of a cycle-accurate emulator's control flow does not produce usable hardware; what is buildable is a NEW implementation verified against this one. `crates/rustynes-cosim` is the boundary — a narrow C ABI a Verilator testbench links, plus a `nes_golden_export` CLI emitting five golden formats. **The provenance firewall extends to HDL** (ADR 0037): `NES_MiSTer` and `fpganes` `rtl/` are strict black boxes — instantiating one as an opaque testbench module to compare OUTPUTS is permitted, reading its source is not; anything unimplementable from documentation escalates to an ADR BEFORE any source is opened. Three design decisions are locked and each has a reason: **replay, not lockstep** (`Nes` exposes `run_frame()` and `step_instruction()` and nothing finer, and the determinism contract already makes a pre-recorded trace exactly the trace a lockstep run produces), **no DPI-C** (it puts `` `ifdef SIMULATION `` guards into RTL that must also pass Quartus — the construct that lets a simulated netlist drift from the synthesised one), and **hash first, capture on divergence** (a 4200-frame AccuracyCoin run is ~125 M cycles, ~7.5 GB as per-cycle CSV against ~480 KB of 4096-cycle checkpoints). The golden framebuffer is exported **pre-palette** so a palette difference cannot masquerade as a rendering one. **v2.5.0 is scoped to "the 6502 rung closes"**, not a finished core (7–13 months FTE for a full one), and two risks are accepted in writing: `NES_MiSTer` scores 121/125 on AccuracyCoin where real Famicom AV hardware also scores ~121/125, so there is no published accuracy headroom and **the core may be declined as a duplicate**; and **the oracle can be wrong**, since 141/141 is not "matches silicon" — every rung is labelled by whether it has an INDEPENDENT oracle. **The exclusion of `rustynes-cosim` from the workspace is the load-bearing detail, and it exposed a defect in the accuracy gate itself.** The crate enables `cpu-boot-trace` and `irq-timing-trace` on `rustynes-core`, and cargo unifies features across a workspace build, so as a MEMBER it made `cargo build --workspace` compile the core ONCE with the union — measured through `--message-format=json`, not inferred. `irq-timing-trace` is not an inert branch: it selects a **different** `for sub_dot in 0..3` loop in `Bus::tick_one_cpu_cycle`, so CI's `cargo test --workspace --release --features test-roms` — the accuracy battery — was validating a scheduler no user runs, the same shape as the v2.3.4 defect where the coverage harness tested a load path no user runs. The measured cost was **+1.24% / +1.39% / +1.89%** across the three `full_frame` benches, *below* this project's own 3% adoption bar, and it never touched the shipped binary or the perf gate — published precisely because it shows performance was never the argument. Exclusion has a price (an excluded package cannot use `field.workspace = true`, and `--workspace` no longer reaches it), and both halves are closed mechanically: `cosim_manifest_audit.rs` asserts every duplicated field and lint still equals the workspace's AND that the crate is still excluded (four mutations, all caught), and CI gains explicit `fmt`, `clippy` and `test` steps — the clippy step earning its place on its first run with a `must_use_candidate` `--workspace` had never surfaced. Two more findings the crate was not looking for: **the first `run_frame()` after power-on advances ZERO cycles** (the PPU is constructed at dot 340 of the pre-render line, so the seven-cycle reset ticks past the frame wrap and leaves `frame_complete` latched — gate on `Nes::frame()`, never the call count, or a `--frames 60` loop emits a 59-frame golden under a manifest claiming 60), and **no CI invocation had ever enabled `cpu-boot-trace` or `irq-timing-trace` for clippy**, so those two core modules had never passed the lint gate (six pre-existing findings; `--workspace --all-targets` covers each crate's DEFAULT feature set only). **It also carries v2.4.0 "Concordance", which merged to `main` and was never tagged**: the seven-property atomic-write sequence v2.3.9 built for `Config::save_to` is extracted into `crate::atomic_write` and adopted everywhere — the plan named three call sites and there were FOUR, the fourth being `save_state.rs`, where a truncated write is a user's game progress, while `per_game.rs` was not in the plan at all because it LOOKS correct (it renames a sibling temp file) and held two of seven: no `fsync`, and a FIXED scratch name shared across every process. Review then found **four more places the module reported success it had not earned**, each an error discarded under a comment explaining the rest of the operation: `set_permissions` swallowed (the mode applied is the one the target ALREADY had, so a failure widens a 0600 file to the umask default), the parent-directory `fsync` swallowed together with its `File::open` (so the whole barrier could be a no-op while the module's table claimed "yes", and `EIO` passed as success), a ONE-attempt occupied-scratch retry (justified by "the counter cannot repeat a name within a process", which is true and beside the point — the collision comes from a previous process whose pid was reused), and an exhaustion cleanup that deleted a file this process had not created. Plus **a `const fn` that only failed on Windows** — `is_transient_rename_error` was `const` and called `io::Error::kind`, which is not, behind `#[cfg(windows)]`, so it compiled clean on Linux and would have turned `main` red AFTER merge; the fix moved the predicate into an always-compiled function reached through `cfg!(windows) && …`, so restoring the `const` now fails on Linux. Also v2.4.0: `Nes::timeline_generation()`, a session-local counter deliberately NOT in the save state (serializing it would make a second load of the same slot restore the same generation, so a consumer would miss it — and because it lives outside the snapshot, `snapshot_schema_audit` cannot see it); the cheat save reporting its failure in the panel instead of a `stderr` nobody reads on a windowed build; and `release_anchor_audit.rs`, pinning 15 release anchors across 10 documents. It is **not** in the v2.3.9 tag — v2.3.9 corrected the eight drifted documents BY HAND, which is what its notes describe and all they claim; the standing gate merged afterwards in #427. (v2.4.1's notes as first published asserted that v2.3.9's body described the audit. It does not; that claim is retracted.) `rustynes-core` changes in both halves, so **AccuracyCoin 141/141 (100.00%, RAM decoder) and nestest 0-diff are VERIFIED, not asserted.** Built on **v2.3.9 "Crucible"** (2026-08-20) — the **gates** release. A crucible tests something to destruction rather than inspecting it, and that is what this release does to the project's own checks: what they cover, what they only *appear* to cover, and where a regression could still reach `main` unchallenged. The v2.3.x line added five tools in four releases and the recurring finding across all of them was never that the emulation was wrong — it was that **a check reported a pass it had not earned**. **The docs-only CI skip had never worked**: `dorny/paths-filter`'s `predicate-quantifier` defaults to `some`, which includes a file if it matches ANY pattern, so the `code` filter's leading `'**'` matched everything and all seven `!` exclusions under it were DEAD from the day they were written — proven from a run rather than the docs (a one-file markdown PR logged `Filter code = true` / `Matching files: AGENTS.md`). Every documentation PR in the project's history had been running the full matrix, and that stopped being merely wasteful the day two docs-only PRs were *blocked* by an ARM cross-compile failure on jobs that should never have been scheduled. Fixed with **two** filter steps because the quantifier is step-level and the two filters need OPPOSITE settings: `code` needs `every`, while `accuracy` is a list of **alternatives** and becomes unsatisfiable under it — the naive one-line fix would have silently disabled the accuracy battery while repairing a different gate. Both directions are now observed on real PRs. **The accuracy battery now runs at review time** — `test-roms` was full-run-only, so a regression landed on `main` rather than on the PR that caused it; it is now also path-filtered over the chip crates, the core, `rustynes-gamedb` (it rewrites the iNES header on load, so it changes what the emulator *is* before a cycle runs), the harness and `tests/`, measured first at 11 of the last 40 merged PRs so ~72% still pay nothing. **Bounds were calibrated against a measurement rather than a claim**: the ARM provisioning step failed on three consecutive PRs with NO apt error in the log at all, and the real number was `Fetched 4201 kB in 4min 45s (14.7 kB/s)` — three orders of magnitude below normal, which made the previous ~40 MB package set *hopeless* rather than unlucky (~45 minutes; no timeout could have saved it); it was also installing a whole cross toolchain to obtain `libc6-dev--cross`, which the comment above it had already named, because bindgen runs the **host** clang against `--sysroot` and never invokes the cross compiler. **A freeze from one cartridge kept writing into the next** — not a stale label but an active per-frame write into the wrong game, because both memory panels' freezes feed the raw-cheat overlay applied after every frame and neither was registered with the ROM-transition hook; the sweep that closed it now covers every panel under ONE rule: **derived output is discarded, user-authored input is kept, and only input that actively *writes* is neutralised** (so RAM Search baselines and reconstructed call stacks clear, while watch lists and breakpoints survive and breakpoints stay ARMED — a breakpoint halts, visible and recoverable, where a freeze writes, silent and continuous). Two negatives are recorded because they cost time to establish: the header editor LOOKS ROM-bound and is not (it is a standalone file tool), and the event panel / trace status / HD-pixel coordinates are per-frame state or preferences. **The config file is written atomically and durably** — `fs::write` truncates then writes, and saves became automatic (closing a ROM, moving a mixer slider, finishing a Latency Oracle measurement), so an interruption left the user holding a truncated `config.toml`; seven properties, and **five came from review rather than the first draft** (sibling scratch file, `fsync` before rename, parent-directory sync, `create_new(true)` for CWE-377, mode applied at creation, symlink resolution including a **broken** link, and a pid + per-call counter — the last is what makes exclusive creation adoptable at all). **Two shipped features told the truth for the first time**: movies record TWO ports (`FrameInput` models P1 and P2) while the Replay panel printed "Four Score (P1..P4)" at the moment a user decides to press Record — widening the format is a `.rnm` epoch change, so it is disclosed at three levels with the caveat printed directly under the claim it qualifies; and a failed Latency Oracle save now says so instead of being swallowed (remembering is still NOT applying — nothing touches `run_ahead`, and an inconclusive result is not remembered at all). Also: **257 lines of dead code removed** — an APU pair (34), a closed `LockstepBus` DMA-service island (183), and `drain_dma` (40), a function called on every CPU read, every CPU write and every bus cycle whose entire body was `let _ = read_addr;` and whose comments claimed the legacy service below it "stays active for the default build" — alongside **25 of 29 `#[allow(dead_code)]` attributes suppressing nothing**, established by stripping them and re-running clippy across all EIGHT gated combinations (an item can be live by default and dead on wasm, which is precisely the case that would have earned the attribute); the **SAFETY-comment rule is now a gate** (`clippy::undocumented_unsafe_blocks` — all 91 unsafe sites already carried a justification, two had it where a human reads correctly and a checker cannot, and the lint is demonstrated to fail); and two `cargo deny` advisory ignores retired on their own stated condition (their entry said to remove them once the resolve moved past quick-xml 0.40, and it had). `rustynes-apu` and `rustynes-core` both change, so **AccuracyCoin 141/141 (100.00%, RAM decoder) and nestest 0-diff are VERIFIED, not asserted** — and re-run AGAIN after the second round of deletions rather than only after the first. Built on **v2.3.8 "Parallax"** (2026-08-20) — **which pixels differ, not just which frame**. Parallax is the apparent shift of an object seen from two positions, and the displacement is the measurement. `Probe` could already say whether two configurations of the same ROM diverge and AT WHICH FRAME, and could say nothing about where or why: a trial reduces each frame to one `u64`, the right shape for *detecting* a difference and the wrong shape for *explaining* one — a hash says frame 412 differs and has nothing to hand to Pixel Provenance, which is where an answer actually lives. `divergence::localise` re-runs both configurations to the detected frame, keeps the full output instead of its hash, and reports the **shape** of the difference — population count, first pixel in raster order, and the inclusive bounding box — which separates kinds of bug from each other (one pixel is a sprite or a palette entry, 256 in a row is a scanline, tens of thousands is a scroll or a mode change); `is_single_scanline` is offered rather than left to call sites because the inclusive comparison is easy to get wrong. It localises on the **index** framebuffer (256x240 `u16`s of `(emphasis << 6) | colour`, the PPU's own per-pixel output before the palette lookup) — half the bytes and at least as sensitive, since the RGBA buffer is a pure function of it given the same palette. Three answers, and the third is the point: `Identical`, `Differs`, and **`Inconclusive`** for an exhausted budget or two trials that cannot be compared — the Latency Oracle precedent applies directly, "I stopped looking" must not arrive wearing the same shape as "they agree" — and the budget is checked UP FRONT for all four trials, so spending two on detection and then finding the localisation pair unaffordable cannot consume the budget that would have answered the question. Beyond locating a difference the Lens **explains** it: trial-scoped provenance capture hands a located pixel to the machinery that already answers "what wrote this, and from which instruction", and an audio lens resolves a divergence to the CPU cycle. One defect was found and fixed inside the same work — the Lens left the emulator **thirty frames ahead** of where it started, because a trial restores the anchor on the way IN and not on the way OUT (deliberate — it is what lets the Lens read the trial's final frame off `nes` directly) and the outermost caller has to put the timeline back, and did not. Cut from its own boundary commit (#407's merge) rather than from `main`, so its artifacts contain exactly the Divergence Lens. Built on **v2.3.7 "Overtone"** (2026-08-19) — the **audio-provenance** release. The APU counterpart of Pixel Provenance: a per-register write attribution answering *what wrote this, and from which instruction*, and a per-CPU-cycle mix trace answering *what were the channels actually doing* — per CPU cycle rather than per output sample, because that is the cadence at which the mix is genuinely computed, and carrying **raw** pre-mix channel values so a record describes the chip rather than the user's mixer sliders. Surfaced at **Tools → Audio → Audio Provenance**; output-only, runtime-default-off, not serialized. **Its subject is the trap it inherited.** Pixel Provenance shipped non-functional for four releases because run-ahead's per-frame rollback cleared its store after the visible frame was harvested and before the frontend released the emulator lock, so the carry landed **in the same change as the feature** here rather than after a bug report. That enumeration was then found to be incomplete: `rustynes-probe` has **three more** same-timeline restores — `Probe::run_uncounted` (once per trial, and a latency measurement runs up to **21**), `latency::measure_in_place` (the final restore, outside every per-trial guard), and the RAM Atlas panel's `TimelineGuard` — none of which used the stash, so **running the Latency Oracle or the RAM Atlas emptied both provenance panels**. Both stores are cumulative, so the records were not rebuilt by the next frame; they were gone for the session. The test named for the contract, `measure_in_place_restores_the_live_timeline`, compares `nes.snapshot()` and provenance is deliberately **not** in the snapshot — it asserted something strictly weaker than its own name and passed throughout. Closed by moving the stash into a shared `TrialGuard`, pinned by four independent mutations. **`$4014` and `$4016` were documented as attributed and were not** — the bus handles both without routing through `Apu::write_register`. **Two defects were caught by measurement rather than reading:** `apu_throughput`, built for this release, reshaped the plumbing **three times** on regressions invisible in the diff (the bench itself had to be corrected first — it omitted an end-of-cycle pair worth ~23% of true per-cycle cost), and a randomized sweep of the save-state parse boundary found **four** panics in VRC7's OPLL where hand-tracing found one, because the maximally-hostile all-`0xFF` payload set `update_requests` to all-ones and **concealed** an `eg_shift` panic. Also fixed: the **browser demo applied no per-game header corrections**, *Rad Racer*'s roadside artifact (a hybrid address spliced from a stale `v`), VRC7 save states dropping the live FM synthesizer so rewind garbled the music, and **no CI job carried a timeout** — one hung job silently skipped a release for five hours. `rustynes-apu` and `rustynes-core` both change, so **AccuracyCoin 141/141 (100.00%, RAM decoder) and nestest 0-diff are VERIFIED, not asserted.** Built on **v2.3.6 "Sounding"** (2026-08-17) — about **measuring, and what a measurement is allowed to claim**. **Two shipped features are found never to have worked.** Pixel Provenance (the v2.3.2 marquee) returned an empty report for every user on the default `run_ahead = 1`: run-ahead's per-frame rollback is the LAST thing before the frontend releases the emulator lock, so the panel's first look was always *after* the wipe — and "click any pixel" was **never implemented** (two `DragValue` spinboxes; the only `Sense` in the file was `hover()` on a colour swatch). **Two source comments and four doc claims asserted the opposite of their own code**, which is why four releases passed unchecked. And **Duck Hunt could never score**: its protocol is "see NOTHING for one frame, then a bright spot in the next", and the light bit was sampled at end-of-frame, so a read during frame N returned frame N-1 — the probe **exactly inverted** (000000 -> 000500). Two new tools, both built to **decline rather than guess**: the **Latency Oracle** (replays one moment with a button held and without it; `None` and `Some(0)` are different answers never collapsed; `START` excluded because pausing is a reaction to a *menu*; **recommends a run-ahead depth and never applies one**) and the **RAM Atlas** (classifies all 2 KiB of work RAM, then VERIFIES a candidate by perturbing it — observation returns all 2048 labels as `Untested` so it is *structurally* incapable of claiming an effect; liveness is relative to its lens and every verdict names it; `Inert` is documented as NOT meaning unused). **APU Workstream D is CLOSED** — the 18.7%-of-frame figure stands, but it is not recoverable by gating per-cycle bookkeeping: one adoption, three measured rejections, one declined on inspection, two left unmeasured deliberately. Tools and Debug are regrouped by task (Tools had reached twenty flat entries). The core gains one `const fn` getter and nothing else, so **AccuracyCoin 141/141 and nestest 0-diff are VERIFIED, not asserted**. **NOT fixed here:** `libretro/docs#1180` (the licence on the libretro docs site) is still open upstream. Built on **v2.3.5 "Manifest"** (2026-08-16) — about **what the core declares about itself**. A user reported RetroArch still showing the pre-relicense MIT/Apache-2.0 terms. It does: RetroArch reads `dist/info/rustynes_libretro.info` from **`libretro/libretro-super`**, a SEPARATE copy from this repo's that nothing syncs and nothing compared, so the v2.2.9 GPL relicense never reached it (both upstream PRs merged 2026-07-21, exactly two weeks BEFORE the 2026-08-04 relicense). The repo-side half is corrected here — `GPLv3+`, since libretro uses short tokens and marks "or later" with a trailing `+` (tallied across all 316 upstream cores) — plus a standing `libretro_info_audit.rs` that pins the local file against the workspace manifest AND the core's own `retro_get_system_info`, making the upstream sync a **copy** rather than a re-derivation. **A licence change is now a mandatory upstream-sync trigger.** Auditing the wrapper then found **five further defects, every one with correct emulation behind it**: a hardcoded 60.0988 fps for every cartridge plus `retro_get_region` unimplemented (**PAL/Dendy ran 20.2% fast**), `retro_reset` unimplemented so **RetroArch's Reset did nothing, ever** (the library default is a literal no-op), `retro_unload_game` unimplemented (Game Genie *indices* leaked across cartridges), `aspect_ratio = 0.0` (square pixels, against the desktop frontend's 8:7), and no controller info so the **Zapper was unreachable** despite `Nes::set_zapper` being fully implemented. Review caught a **use-after-free**: RetroArch shallow-`memcpy`s the outer `retro_controller_info` array but RETAINS each `types` pointer, so the description tables must be `'static` (`SET_INPUT_DESCRIPTORS` is different and safe — never generalize between environment calls). The crate went from **zero tests to eight**. Separately the APU (**18.7% of frame time**, invisible to a symbol profile because fat LTO inlines it into `cpu_clock`) gained its first throughput bench and a default-configuration mix specialization, **−3.3% to −4.2%** on `nes_run_frame_nestest`, byte-identical by construction. Declared values are now DERIVED from `rustynes_core` constants (`FRAME_DURATION_*`, `DEFAULT_SAMPLE_RATE`) rather than transcribed. Audio stays **44,100 Hz** — a matched-normalized-frequency SFDR comparison shows 44.1k and 48k are equivalent (81.6 vs 82.2 dB), so nothing is gained, and 44,100 is the only rate this project's audio is verified at. Shipped OUTPUT byte-identical, but the APU *implementation* did change (the mix specialization is a strict specialization, not a no-op), so **AccuracyCoin 141/141 and nestest 0-diff were VERIFIED, not asserted**. **NOT fixed by that release, and since RESOLVED upstream:** RetroArch showed the wrong licence until `libretro-super#2069` merged (2026-08-16 — it now reads `GPLv3+`), and RustyNES did not appear on iOS/iPadOS/tvOS until `RetroArch#19416` merged (2026-08-16, `76f60626984a` — `rustynes` is now line 268 of `pkg/apple/update-cores.sh`, between `reminiscence` and `sameboy`). Being in the build list is not the same as being installable: it arrives with the next App Store RetroArch build, on libretro's cadence. Only `libretro/docs#1180` remains open.
The prior release, **v2.3.4 "Ledger"** (2026-08-15), was the **coverage** release. Three boards land: **mapper 176 submapper 2** (WAIXING-FS005 — the `$A001` RAM Configuration Register with 32 KiB banked WRAM, the `$5000-$5FFF` register-window disable the Waixing copy-protection is built on, a mapper-195-like mixed CHR-ROM/CHR-RAM mode, two-bit `$A000` mirroring, the `$46`/`$47` bank-select swap that does NOT apply to `$06`/`$07`, PRG A21-A25, and the board's documented `$E003` decode mask), **154** (NAMCOT-3453 — mapper 88 plus a one-screen nametable bit decoded across the WHOLE `$8000-$FFFF` range, not just the bank-select window) and **243** (Sachen SA-020A — mapper 150's ASIC on its own PCB, same three registers at INVERTED significance, which is why they need separate numbers). Breadth **172 → 174 families** (51 Core + 95 Curated + 28 BestEffort). All three implemented from the NESdev wiki with **no reference-emulator source consulted**, unlike the FK23C transforms beside them which stay a disclosed Mesen2 derivation.
@@ -205,7 +205,7 @@ These cross-cutting decisions span multiple files. Reading individual chip docs
- `ref-docs/` is immutable. Research updates go in dated supplemental files.
- ADRs go in `docs/adr/` (Michael Nygard format).
- `rustynes-core` re-exports the public types from the chip crates; downstream consumers (`rustynes-frontend`, `rustynes-test-harness`) should depend on `rustynes-core` rather than the chip crates directly.
-- When relabeling old engine "v2.x" narrative for users, present it as upstream lineage/history — **never as a current RustyNES release version.** The current release is **v2.6.4 "Rubric"** (OAM DMA lands and all nine AccuracyCoin disagreements close, every rule that closed the last three stated by the test ROM and by neither nesdev page — and then the gate that certified them is measured to cover 88 of 146 entries. The emulation core is unchanged, on **v2.6.3 "Mainspring"** — the DUT runs on one master clock, and four enables that were never enabling — plus AccuracyCoin end to end and a status vector that names its disagreements by test. The emulation core is unchanged, on **v2.6.2 "Witness"** — rung 4 closes: blargg APU battery 11/11 on the co-simulation DUT, six defects no self-written gate could see, and a suite that had been asserting nothing for five minor releases. The emulation core is unchanged, on **v2.6.1 "Interleave"** — the DMC and its DMA cycle steal in the MiSTer co-simulation DUT, cycle-exact on the bus. The emulation core is unchanged, on **v2.6.0 "Assay"** — the triangle, the noise channel and the sweep unit **in the MiSTer co-simulation DUT** — and an audit of how much of the APU was fitted to the oracle rather than derived from documentation. The emulation core is unchanged, on **v2.5.9 "Overture"** — rung 4 opens: the two pulse channels, the frame counter, and four ROM defects the stimulus measurement found first, on **v2.5.8 "Blanking"** — VBlank, NMI and the PPUSTATUS race close rung 3 — and both fixes were deletions, on **v2.5.7 "Collimation"** — sprite rendering closes exact — the phase was wrong by two dots, and every window was compensating, on **v2.5.6 "Vestige"** — Sprite evaluation closes: all 59,993 overlapping cycles match, nine of nine behavioural mutants caught and two proved inert (announced as seven of eight at the cut), and the fix is a byte index that outlives the walk that set it, on **v2.5.5 "Raster"** — the first full frame, and three blind spots in the stimulus that fed it, on **v2.5.4 "Escapement"** — the background fetch pipeline, and an access two dots early that five gates could not see, on **v2.5.3 "Hysteresis"** — toggling rendering takes effect three dots after the write, and four instruments to prove it, on **v2.5.2 "Dormant"** — the 2C02 register file, and a gate that passed while testing nothing, on **v2.5.1 "Retrace"** — the interrupt sweep closes rung 2, and a gate reported a pass it could not have earned, on **v2.5.0 "Rungwork"** — the 6502 rung, and the two gates it cannot reach, on **v2.4.9 "Plumbline II"** — the bus half of rung 2, and what it found the day it existed, on **v2.4.8 "Palimpsest"** — read-modify-write, and a gate that cannot see its own subject, on **v2.4.7 "Keystone"** — the stack closes, and a dead line proves itself dead, on **v2.4.6 "Abacus"** — the core learns arithmetic, on **v2.4.5 "Compass"** — the core reaches memory, and chooses, on **v2.4.4 "Ignition"** — 2026-08-22, the first real RTL: the 6502's eight-cycle reset and the implied opcode group matching the oracle on all seven CPU fields, and the oracle settling a reset length our own prose gave two different answers for), on top of **v2.4.3 "Touchstone"** (2026-08-22, the two Fabric risks settled before any RTL: the Quartus 17.0.2 subset FITTED at 2 M10K blocks and 29 registers with zero synthesis warnings, and the sys/ licence audit finding ZERO GPL-2.0-only files, which inverts the plan's hedge and confirms GPL-3.0-or-later), on top of **v2.4.2 "Cairn"** (2026-08-22, the rung-0 compare surface: rolling per-cycle hash checkpoints measured at 15,263x smaller than the equivalent CSV, the acceptance gate made executable, and the partition between what RustyNES MODELS and what a device can OBSERVE), on top of **v2.4.1 "Fabric"** (2026-08-20, the oracle release opening the v2.4.1 → v2.5.0 "Fabric" line — a new NES core in SystemVerilog written from public hardware documentation in a sibling repository, with RustyNES as its VERIFICATION ORACLE; RustyNES is not being ported to FPGA and cannot be. `crates/rustynes-cosim` is the boundary (a narrow C ABI a Verilator testbench links, plus `nes_golden_export`), the firewall extends to HDL per ADR 0037 (`NES_MiSTer` and `fpganes` `rtl/` are strict black boxes), and v2.5.0 is scoped to "the 6502 rung closes" rather than a finished core. Excluding the crate from the workspace is the load-bearing detail: cargo unifies features, `irq-timing-trace` selects a DIFFERENT per-dot loop in `Bus::tick_one_cpu_cycle`, and the accuracy battery was therefore validating a scheduler no user runs. Also found: the first `run_frame()` after power-on advances ZERO cycles, and two trace-gated core modules had never been linted. It CARRIES v2.4.0 "Concordance", which merged to `main` and was never tagged — atomic durable writes on every path that persists user data (four call sites, four further silent successes found in review), `Nes::timeline_generation()`, and the 15-anchor release audit. AccuracyCoin 141/141 and nestest 0-diff VERIFIED), on top of **v2.3.9 "Crucible"** (2026-08-20, the gates release — a crucible tests to destruction rather than inspects, and this one does that to the project's own checks. The docs-only CI skip HAD NEVER WORKED [`predicate-quantifier` defaults to `some`, so the `code` filter's `'**'` matched everything and all seven `!` exclusions were dead from the day they were written]; fixed with TWO filter steps because the quantifier is step-level and `accuracy` is a list of alternatives that becomes unsatisfiable under `every` — the one-line fix would have silently disabled the accuracy battery. `test-roms` now runs at review time, path-filtered over the chip crates, the core, `rustynes-gamedb`, the harness and `tests/` [11 of the last 40 merged PRs]. A freeze from one cartridge kept writing into the next — an active per-frame write into the wrong game — closed by a ROM-transition sweep under one rule: derived output discarded, user-authored input kept, and only input that actively WRITES neutralised. The config file is written atomically and durably [seven properties, five from review]. Movies record two ports while the Replay panel advertised "Four Score (P1..P4)", now disclosed at three levels. 257 lines of dead code removed, 25 of 29 `#[allow(dead_code)]` attributes suppressing nothing, `undocumented_unsafe_blocks` made a gate, and two `cargo deny` ignores retired on their own stated condition. `rustynes-apu` and `rustynes-core` both change, so AccuracyCoin 141/141 and nestest 0-diff are VERIFIED), on top of **v2.3.8 "Parallax"** (2026-08-20, the Divergence Lens — `Probe` could say two configurations diverge and AT WHICH FRAME and nothing about where or why, because a trial reduces each frame to one `u64`; `divergence::localise` keeps the full output and reports the SHAPE of the difference [population count, first pixel in raster order, inclusive bounding box], localises on the INDEX framebuffer so a palette difference cannot masquerade as a rendering one, hands the located pixel to Pixel Provenance, and answers `Inconclusive` rather than collapsing "I stopped looking" into "they agree". Cut from its own boundary commit, so its artifacts contain exactly the Lens), on top of **v2.3.7 "Overtone"** (2026-08-19, the audio-provenance release — the APU counterpart of Pixel Provenance: a per-register write attribution [*what wrote this, and from which instruction*] plus a per-CPU-cycle mix trace [*what were the channels actually doing*], at Tools → Audio → Audio Provenance, output-only and runtime-default-off. Its real subject is the trap it inherited: Pixel Provenance shipped non-functional for four releases because run-ahead's rollback cleared its store before any UI could read it, so the carry landed in the SAME change as the feature — and then the same defect turned up in THREE more places, every restore in `rustynes-probe`, so running the Latency Oracle or the RAM Atlas silently emptied both provenance panels [the v2.3.6 fix had enumerated one caller rather than the mechanism, and `measure_in_place_restores_the_live_timeline` could not see the breach because provenance is deliberately not in the snapshot]. Two defects found by measurement not reading: the new `apu_throughput` bench reshaped the plumbing three times on regressions invisible in the diff, and a randomized sweep of the save-state parse boundary found FOUR panics in VRC7's OPLL where hand-tracing found one — the all-`0xFF` payload CONCEALED one. Also fixed: `$4014`/`$4016` documented as attributed and were not, the browser demo applied no per-game header corrections, Rad Racer's roadside artifact, VRC7 save states dropping the live FM synthesizer, and unbounded CI jobs. `rustynes-apu` and `rustynes-core` both change, so AccuracyCoin 141/141 and nestest 0-diff are VERIFIED), on top of **v2.3.6 "Sounding"** (2026-08-17, the measurement release — two shipped features found never to have worked [Pixel Provenance's record wiped by run-ahead before any UI could read it, its click never implemented; the Duck Hunt Zapper probe exactly inverted], the Latency Oracle and RAM Atlas both built to decline rather than guess, APU Workstream D closed on three measured rejections, and the Tools/Debug menus regrouped by task; core gains one `const fn` getter so AccuracyCoin 141/141 is VERIFIED), on top of **v2.3.5 "Manifest"** (2026-08-16, the declaration release — the libretro `.info` RetroArch reads is a SEPARATE upstream copy the GPL relicense never reached, corrected to `GPLv3+` with a standing audit; five wrapper defects each with correct emulation behind them [PAL 20.2% fast, Reset inert, unload leaked cheat indices, square-pixel aspect, Zapper unreachable]; a use-after-free in the controller tables found in review; the APU's first throughput bench + a −3.3%/−4.2% default-mix specialization; AccuracyCoin 141/141 VERIFIED. The RetroArch licence display and iOS/iPadOS/tvOS availability both remain blocked on upstream PRs), on top of **v2.3.4 "Ledger"** (2026-08-15, the coverage release — mappers 176/2 (WAIXING-FS005), 154 (NAMCOT-3453) and 243 (Sachen SA-020A) taking breadth to 174 families; the coverage harness moved onto the frontend's real load path, exposing a per-game-database defect that had made every Sachen cartridge unloadable since v1.2.0; this one TOUCHES the core, so AccuracyCoin 141/141 is verified, not by construction; Workstream C — the APU at 18.7% — was NOT delivered and is carried to v2.3.5), on top of **v2.3.3 "Cadence"** (2026-08-14, the display-pacing release — the run-ahead throttle oscillation traced to a stale median, a predictive engage arm, and the `wp_presentation` apparatus; frontend-only, AccuracyCoin 141/141), on top of **v2.3.2 "Lucid"** (2026-08-11, the pixel-provenance release — per-byte write attribution + the per-pixel causal record + the Tools → Pixel Provenance panel + deterministic replay attestation via `rustynes verify`; all `debug-hooks`-gated and output-only, so AccuracyCoin holds 141/141 and nestest is 0-diff), on top of **v2.3.1 "Plumb Line"** (2026-08-06, the measurement release — ten hot-path candidates measured and all ten rejected), itself on **v2.3.0 "Datum II"** (2026-08-05, the capstone closing the v2.2.6 → v2.3.0 NESdev-remediation line — **true multi-viewport OS-window detach** for every tool panel (v2.2.9's affordance only *embedded* them, so the Windows-10 trapped-window report is now genuinely fixed); a **frame-pacing fix** predating that work (the render path held the emulator lock across the blocking swapchain acquire + present, stalling frame production whenever a debugger panel was open — now split so the lock covers only the egui UI build, plus `pace_frames` reading a lock-free `has_rom` atomic instead of locking every `about_to_wait`); a **−5.13% / −3.51%** byte-identical PPU optimization (`v2.3.0 P1`: `#[inline]` on the per-dot sprite eval + hoisting the `tick_oam_bus` early-out); both remaining forum-reported accuracy items (SMB left edge, Rad Racer hybrid-address) **verified already-correct**; and the AccuracyCoin gate pinned to an **exact 141/141**), on top of **v2.2.9 "Studio II"** (2026-08-04, a frontend quality-of-life release — TAStudio piano-roll edits wired to the emulator, `.bk2` playback honoring the movie's `LogKey` column order, and a detach/pop-out affordance for tool windows (the shared `detachable_window` helper across 18 panels) [native-only; it **embedded** the panel on the single-viewport `egui_winit` integration rather than opening a separate OS window — **resolved in v2.3.0** by the real multi-viewport implementation]; frontend-only so the deterministic core is untouched and AccuracyCoin holds 141/141, nestest 0-diff), on top of **v2.2.8 "Aperture II"** (2026-08-04, a presentation-fidelity release — gamma-correct scanlines + a WebGL2 gamma fix + a sharper scanline profile; presentation-only so the pre-shader framebuffer + AccuracyCoin 141/141 are byte-identical, native default unchanged; visual verification pending), on top of **v2.2.7 "Timbre II"** (2026-08-04, an expansion-audio fidelity release — VRC6 recalibrated to ~1.0× a 2A03 pulse per the NESdev/field consensus [`VRC6_MIX_SCALE` 979→650; Mesen2's ~1.5× was the loud outlier], and the Sunsoft 5B envelope moved to the exact 5-bit 1.5 dB/step DAC; expansion-only, so the base 2A03 is byte-identical and AccuracyCoin holds 141/141), on top of **v2.2.6 "Almanac"** (2026-08-04, a de-monetization + provenance release — RustyNES is permanently open-source and income-free per ADR 0035; all planned monetization removed, native apps kept as free FOSS apps, and the TriCNES hybrid-address timing-calibration caveat disclosed per ADR 0030 for a v2.3.0 rework; zero emulation-core behavior changes so AccuracyCoin holds 141/141 by construction), on top of **v2.2.5 "Colophon"** (2026-08-03, a provenance/licensing/documentation-integrity release — zero emulation-core behavior changes so AccuracyCoin holds 141/141 by construction; `NOTICE` rewritten for full attribution + GPL-oracle disclosure + GeraNES, in-source "port" comments reworded to the oracle framing, the CRT-shader/NTSC provenance reworded to independent reimplementations, `docs/originality-and-provenance.md` added, README AI-assistance disclosure), on top of **v2.2.4 "Cartridge"** (2026-07-24, a libretro/RetroArch distribution cut — zero emulation-core changes so AccuracyCoin holds 141/141 by construction; the libretro core is confirmed up-to-date with all recent changes and builds for the buildbot ABIs [`x86_64-pc-windows-gnu`, `aarch64-linux-android`], and `rustynes_libretro.info` is corrected: `disk_control` false→true [the FDS Disk Control interface was wired but advertised absent], `display_version` v1.0.0→v2.2.4, mapper count 168→172; core options remain a documented future enhancement; the Antigravity reviewer standardization rides along), on top of **v2.2.3 "Datum"** (2026-07-23, a performance and accuracy-closure patch — the fast PPU dot path promoted to default and exposed, PGO binaries shipped on the release path, a same-runner relative frame-time CI gate, the last two Holy Mapperel residuals closed [MMC1 WRAM write-protect + FME-7 open bus, all 17 ROMs now `detail=0000`], the Sunsoft 5B level calibrated with `Mapper::mix_audio` widened to i32, a save-state schema gap fixed at `PPU_SNAPSHOT_VERSION` 8 + an APU v4 tail, an opt-in Zapper beam-relative light model, and the eleven `sprintN.rs` mapper modules renamed to `mNNN_.rs`; two optimizations measured and REJECTED and documented as such; AccuracyCoin 141/141 — on top of **v2.2.2 "Conduit"** [2026-07-21, a build/distribution/CI-integrity patch — the libretro buildbot recipe taken from 1 of 10 jobs green to all ten building, a GitHub Actions supply-chain hardening pass, and the toolchain collapsed to one pinned source of truth with no `nightly` on any build path; zero emulation-core changes], itself on **v2.2.1** [2026-07-15, a housekeeping patch: dev-tooling archival, a zero-source-change dependency consolidation, and a gitignored FDS test-corpus addition], itself on **v2.2.0 "Capstone"** [2026-07-12], the milestone cut that closes the v2.1.5 → v2.2.0 "deepen the existing project" run — its two remaining marquees the netplay matchmaking / lobby stack and the FDS medium model, atop a peripherals + quality/security pass (Famicom `$4016`-bit-2 microphone + 3×3-aperture Zapper; cargo-fuzz targets 3 → 8 finding + fixing two `Movie::deserialize` OOM-DoS paths; a read-only Tools → ROM Info browser); every change additive or default-off, AccuracyCoin 141/141) on the v2.0.0 "Timebase" one-clock / every-cycle-bus-access scheduler rewrite + Vs. `DualSystem` dual-console support. The v2.0.x "Harbor" mobile-finalization train (v2.0.1→v2.0.9) and the entire v2.1.x "Fathom" line (v2.1.0→v2.1.10) plus the v2.2.0 "Capstone" milestone have all shipped — the run's steps being v2.1.5 "Vernier" (regression-net & residual) → v2.1.6 "Timbre" (expansion-audio fidelity) → v2.1.7 "Stepping" (opt-in PPU/2A03 die-revisions + power-on RAM/palette models; the DMA "unexpected read" frontier a documented no-op on every oracle, ADR 0033) → v2.1.8 "Tempo" (a default-OFF fast PPU dot path + SIMD blitter + wasm size pass) → v2.1.9 "Aperture" (a marquee CRT shader stack + raw NTSC composite signal-decode + GIF/WAV capture + palette editor) → v2.1.10 "Loom" (TAStudio greenzone + Lua API breadth + browser-RA auth-proxy deploy stack + Vs. `DualSystem` libretro presentation) → v2.2.0 "Capstone" (the milestone cut closing the run) → v2.2.1 (housekeeping) → **v2.2.2 "Conduit"** the build/distribution/CI-integrity patch — preceded by v1.10.0 "Arcade" the native Libretro / RetroArch core, the v1.9.0→v1.9.9 iOS TestFlight train, the v1.8.0→v1.8.9 "Android" train, and the desktop-feature lineage v1.1.0→v1.7.1, all on the v1.0.0 production core (see the top "Current release" block + `docs/STATUS.md`). **Never claim any version *later* than v2.6.4 is released** — the **v2.2.6 → v2.3.0** line (de-monetization + NESdev remediation: audio [v2.2.7, shipped], video/gamma [v2.2.8, shipped], TAS/UX [v2.2.9, shipped], and the PPU left-edge + hybrid-address accuracy capstone at **v2.3.0** "Datum II" [shipped]) is now **complete**. The freed **v2.3.0** slot is repurposed as that accuracy capstone (NOT a store launch — RustyNES is now income-free per ADR 0035; any free mobile-app store listing is a later, unversioned step with no monetization — see `to-dos/ROADMAP.md`). Two distinct "v2.0"s exist and must not be conflated, **both now shipped, at different times, for different reasons**: the **engine-lineage v2.0** master-clock work shipped as the **v1.0.0** production core (2026-06-13) — it was the *only* scheduler through v1.10.0. RustyNES's own **v2.0.0 "Timebase"** release (2026-07-03) is a *different* milestone that *replaces* that same dot-lockstep scheduler outright: the **one-clock + every-cycle-bus-access collapse** (a single canonical cycle counter + a split-around-the-access `start_cycle`/`end_cycle` PPU catch-up, mirroring Mesen2's structure), full Vs. `DualSystem` dual-console emulation (core-and-harness-only; frontend wiring deferred), and the breaking save-state / cross-version changes it entailed (ADR 0002 / ADR 0028 / ADR 0029) — the one release that broke byte-identity / save-state compatibility, by design. The R1/R2 hard-tier MMC3 IRQ-timing residual was investigated under a bounded-effort campaign and is by-design-deferred beyond v2.0.0, not closed — see ADR 0002's decision-update section for the mechanism-level finding.
+- When relabeling old engine "v2.x" narrative for users, present it as upstream lineage/history — **never as a current RustyNES release version.** The current release is **v2.6.5 "Muster"** (rung 5 closes — the AccuracyCoin status vector is identical entry for entry across all 146 entries, with 146 of 146 executed on both sides and none NotRun, where the same gate read 5 of 146 at the version's start. A muster is a roll call where every name is called AND answered, which is the two-clause acceptance exactly. Five PPU defects close the last six differing entries and four were invisible to every gate that existed when the version opened: the background shift registers' RELOAD and their shift clock need SEPARATE gates (with one shared gate the serial-in test was not merely failing but ARITHMETICALLY UNREACHABLE, since reload dots are absolute and the reload discards the low seven bits, so a serial-in one can never reach bit 7 on any alignment — and modelling both structures reproduces BOTH measured shifter values); the sprite X counters are NOT gated on rendering, which AccuracyCoin states outright and the ROM that states it passes either way, because it expects no hit at X=254 and a sprite shoved 18 dots right is also off the line; the PPUADDR second-write v-copy is DELAYED, as the wiki says inside the write sequence itself, swept 1 to 4 dots against a control at 8 and 12 that fails; and the pre-render line CLEARS secondary OAM, without which scanline 0 draws what scanline 239 left — no sprite can ever render on scanline 0, because OAM Y is one less than the display row, and a sprite-0 probe over the full 134 M-cycle battery found 24 hits with four of them there; and the octal latch holding across the read dot, which is verified by exactly ONE gate and was unverifiable until the v-copy delay landed, the two composing the hybrid address together and neither producing it alone. A DIAGNOSIS IS RETRACTED: the residual was read as a two-dot CPU/PPU alignment error from comparing dot spans across two instruments, and at the committed alignment the two consoles execute identical pc, bus_addr and bus_access for 1,695,131 cycles while a two-dot shift moves the first fork back to 593,228 and takes the differing share from 5.13% to 66.80%. The oracle changes on the default path, so AccuracyCoin 141/141 (RAM decoder) and nestest 0-diff are VERIFIED, not asserted, on **v2.6.4 "Rubric"** — OAM DMA lands and all nine AccuracyCoin disagreements close, every rule that closed the last three stated by the test ROM and by neither nesdev page — and then the gate that certified them is measured to cover 88 of 146 entries. The emulation core is unchanged, on **v2.6.3 "Mainspring"** — the DUT runs on one master clock, and four enables that were never enabling — plus AccuracyCoin end to end and a status vector that names its disagreements by test. The emulation core is unchanged, on **v2.6.2 "Witness"** — rung 4 closes: blargg APU battery 11/11 on the co-simulation DUT, six defects no self-written gate could see, and a suite that had been asserting nothing for five minor releases. The emulation core is unchanged, on **v2.6.1 "Interleave"** — the DMC and its DMA cycle steal in the MiSTer co-simulation DUT, cycle-exact on the bus. The emulation core is unchanged, on **v2.6.0 "Assay"** — the triangle, the noise channel and the sweep unit **in the MiSTer co-simulation DUT** — and an audit of how much of the APU was fitted to the oracle rather than derived from documentation. The emulation core is unchanged, on **v2.5.9 "Overture"** — rung 4 opens: the two pulse channels, the frame counter, and four ROM defects the stimulus measurement found first, on **v2.5.8 "Blanking"** — VBlank, NMI and the PPUSTATUS race close rung 3 — and both fixes were deletions, on **v2.5.7 "Collimation"** — sprite rendering closes exact — the phase was wrong by two dots, and every window was compensating, on **v2.5.6 "Vestige"** — Sprite evaluation closes: all 59,993 overlapping cycles match, nine of nine behavioural mutants caught and two proved inert (announced as seven of eight at the cut), and the fix is a byte index that outlives the walk that set it, on **v2.5.5 "Raster"** — the first full frame, and three blind spots in the stimulus that fed it, on **v2.5.4 "Escapement"** — the background fetch pipeline, and an access two dots early that five gates could not see, on **v2.5.3 "Hysteresis"** — toggling rendering takes effect three dots after the write, and four instruments to prove it, on **v2.5.2 "Dormant"** — the 2C02 register file, and a gate that passed while testing nothing, on **v2.5.1 "Retrace"** — the interrupt sweep closes rung 2, and a gate reported a pass it could not have earned, on **v2.5.0 "Rungwork"** — the 6502 rung, and the two gates it cannot reach, on **v2.4.9 "Plumbline II"** — the bus half of rung 2, and what it found the day it existed, on **v2.4.8 "Palimpsest"** — read-modify-write, and a gate that cannot see its own subject, on **v2.4.7 "Keystone"** — the stack closes, and a dead line proves itself dead, on **v2.4.6 "Abacus"** — the core learns arithmetic, on **v2.4.5 "Compass"** — the core reaches memory, and chooses, on **v2.4.4 "Ignition"** — 2026-08-22, the first real RTL: the 6502's eight-cycle reset and the implied opcode group matching the oracle on all seven CPU fields, and the oracle settling a reset length our own prose gave two different answers for), on top of **v2.4.3 "Touchstone"** (2026-08-22, the two Fabric risks settled before any RTL: the Quartus 17.0.2 subset FITTED at 2 M10K blocks and 29 registers with zero synthesis warnings, and the sys/ licence audit finding ZERO GPL-2.0-only files, which inverts the plan's hedge and confirms GPL-3.0-or-later), on top of **v2.4.2 "Cairn"** (2026-08-22, the rung-0 compare surface: rolling per-cycle hash checkpoints measured at 15,263x smaller than the equivalent CSV, the acceptance gate made executable, and the partition between what RustyNES MODELS and what a device can OBSERVE), on top of **v2.4.1 "Fabric"** (2026-08-20, the oracle release opening the v2.4.1 → v2.5.0 "Fabric" line — a new NES core in SystemVerilog written from public hardware documentation in a sibling repository, with RustyNES as its VERIFICATION ORACLE; RustyNES is not being ported to FPGA and cannot be. `crates/rustynes-cosim` is the boundary (a narrow C ABI a Verilator testbench links, plus `nes_golden_export`), the firewall extends to HDL per ADR 0037 (`NES_MiSTer` and `fpganes` `rtl/` are strict black boxes), and v2.5.0 is scoped to "the 6502 rung closes" rather than a finished core. Excluding the crate from the workspace is the load-bearing detail: cargo unifies features, `irq-timing-trace` selects a DIFFERENT per-dot loop in `Bus::tick_one_cpu_cycle`, and the accuracy battery was therefore validating a scheduler no user runs. Also found: the first `run_frame()` after power-on advances ZERO cycles, and two trace-gated core modules had never been linted. It CARRIES v2.4.0 "Concordance", which merged to `main` and was never tagged — atomic durable writes on every path that persists user data (four call sites, four further silent successes found in review), `Nes::timeline_generation()`, and the 15-anchor release audit. AccuracyCoin 141/141 and nestest 0-diff VERIFIED), on top of **v2.3.9 "Crucible"** (2026-08-20, the gates release — a crucible tests to destruction rather than inspects, and this one does that to the project's own checks. The docs-only CI skip HAD NEVER WORKED [`predicate-quantifier` defaults to `some`, so the `code` filter's `'**'` matched everything and all seven `!` exclusions were dead from the day they were written]; fixed with TWO filter steps because the quantifier is step-level and `accuracy` is a list of alternatives that becomes unsatisfiable under `every` — the one-line fix would have silently disabled the accuracy battery. `test-roms` now runs at review time, path-filtered over the chip crates, the core, `rustynes-gamedb`, the harness and `tests/` [11 of the last 40 merged PRs]. A freeze from one cartridge kept writing into the next — an active per-frame write into the wrong game — closed by a ROM-transition sweep under one rule: derived output discarded, user-authored input kept, and only input that actively WRITES neutralised. The config file is written atomically and durably [seven properties, five from review]. Movies record two ports while the Replay panel advertised "Four Score (P1..P4)", now disclosed at three levels. 257 lines of dead code removed, 25 of 29 `#[allow(dead_code)]` attributes suppressing nothing, `undocumented_unsafe_blocks` made a gate, and two `cargo deny` ignores retired on their own stated condition. `rustynes-apu` and `rustynes-core` both change, so AccuracyCoin 141/141 and nestest 0-diff are VERIFIED), on top of **v2.3.8 "Parallax"** (2026-08-20, the Divergence Lens — `Probe` could say two configurations diverge and AT WHICH FRAME and nothing about where or why, because a trial reduces each frame to one `u64`; `divergence::localise` keeps the full output and reports the SHAPE of the difference [population count, first pixel in raster order, inclusive bounding box], localises on the INDEX framebuffer so a palette difference cannot masquerade as a rendering one, hands the located pixel to Pixel Provenance, and answers `Inconclusive` rather than collapsing "I stopped looking" into "they agree". Cut from its own boundary commit, so its artifacts contain exactly the Lens), on top of **v2.3.7 "Overtone"** (2026-08-19, the audio-provenance release — the APU counterpart of Pixel Provenance: a per-register write attribution [*what wrote this, and from which instruction*] plus a per-CPU-cycle mix trace [*what were the channels actually doing*], at Tools → Audio → Audio Provenance, output-only and runtime-default-off. Its real subject is the trap it inherited: Pixel Provenance shipped non-functional for four releases because run-ahead's rollback cleared its store before any UI could read it, so the carry landed in the SAME change as the feature — and then the same defect turned up in THREE more places, every restore in `rustynes-probe`, so running the Latency Oracle or the RAM Atlas silently emptied both provenance panels [the v2.3.6 fix had enumerated one caller rather than the mechanism, and `measure_in_place_restores_the_live_timeline` could not see the breach because provenance is deliberately not in the snapshot]. Two defects found by measurement not reading: the new `apu_throughput` bench reshaped the plumbing three times on regressions invisible in the diff, and a randomized sweep of the save-state parse boundary found FOUR panics in VRC7's OPLL where hand-tracing found one — the all-`0xFF` payload CONCEALED one. Also fixed: `$4014`/`$4016` documented as attributed and were not, the browser demo applied no per-game header corrections, Rad Racer's roadside artifact, VRC7 save states dropping the live FM synthesizer, and unbounded CI jobs. `rustynes-apu` and `rustynes-core` both change, so AccuracyCoin 141/141 and nestest 0-diff are VERIFIED), on top of **v2.3.6 "Sounding"** (2026-08-17, the measurement release — two shipped features found never to have worked [Pixel Provenance's record wiped by run-ahead before any UI could read it, its click never implemented; the Duck Hunt Zapper probe exactly inverted], the Latency Oracle and RAM Atlas both built to decline rather than guess, APU Workstream D closed on three measured rejections, and the Tools/Debug menus regrouped by task; core gains one `const fn` getter so AccuracyCoin 141/141 is VERIFIED), on top of **v2.3.5 "Manifest"** (2026-08-16, the declaration release — the libretro `.info` RetroArch reads is a SEPARATE upstream copy the GPL relicense never reached, corrected to `GPLv3+` with a standing audit; five wrapper defects each with correct emulation behind them [PAL 20.2% fast, Reset inert, unload leaked cheat indices, square-pixel aspect, Zapper unreachable]; a use-after-free in the controller tables found in review; the APU's first throughput bench + a −3.3%/−4.2% default-mix specialization; AccuracyCoin 141/141 VERIFIED. The RetroArch licence display and iOS/iPadOS/tvOS availability both remain blocked on upstream PRs), on top of **v2.3.4 "Ledger"** (2026-08-15, the coverage release — mappers 176/2 (WAIXING-FS005), 154 (NAMCOT-3453) and 243 (Sachen SA-020A) taking breadth to 174 families; the coverage harness moved onto the frontend's real load path, exposing a per-game-database defect that had made every Sachen cartridge unloadable since v1.2.0; this one TOUCHES the core, so AccuracyCoin 141/141 is verified, not by construction; Workstream C — the APU at 18.7% — was NOT delivered and is carried to v2.3.5), on top of **v2.3.3 "Cadence"** (2026-08-14, the display-pacing release — the run-ahead throttle oscillation traced to a stale median, a predictive engage arm, and the `wp_presentation` apparatus; frontend-only, AccuracyCoin 141/141), on top of **v2.3.2 "Lucid"** (2026-08-11, the pixel-provenance release — per-byte write attribution + the per-pixel causal record + the Tools → Pixel Provenance panel + deterministic replay attestation via `rustynes verify`; all `debug-hooks`-gated and output-only, so AccuracyCoin holds 141/141 and nestest is 0-diff), on top of **v2.3.1 "Plumb Line"** (2026-08-06, the measurement release — ten hot-path candidates measured and all ten rejected), itself on **v2.3.0 "Datum II"** (2026-08-05, the capstone closing the v2.2.6 → v2.3.0 NESdev-remediation line — **true multi-viewport OS-window detach** for every tool panel (v2.2.9's affordance only *embedded* them, so the Windows-10 trapped-window report is now genuinely fixed); a **frame-pacing fix** predating that work (the render path held the emulator lock across the blocking swapchain acquire + present, stalling frame production whenever a debugger panel was open — now split so the lock covers only the egui UI build, plus `pace_frames` reading a lock-free `has_rom` atomic instead of locking every `about_to_wait`); a **−5.13% / −3.51%** byte-identical PPU optimization (`v2.3.0 P1`: `#[inline]` on the per-dot sprite eval + hoisting the `tick_oam_bus` early-out); both remaining forum-reported accuracy items (SMB left edge, Rad Racer hybrid-address) **verified already-correct**; and the AccuracyCoin gate pinned to an **exact 141/141**), on top of **v2.2.9 "Studio II"** (2026-08-04, a frontend quality-of-life release — TAStudio piano-roll edits wired to the emulator, `.bk2` playback honoring the movie's `LogKey` column order, and a detach/pop-out affordance for tool windows (the shared `detachable_window` helper across 18 panels) [native-only; it **embedded** the panel on the single-viewport `egui_winit` integration rather than opening a separate OS window — **resolved in v2.3.0** by the real multi-viewport implementation]; frontend-only so the deterministic core is untouched and AccuracyCoin holds 141/141, nestest 0-diff), on top of **v2.2.8 "Aperture II"** (2026-08-04, a presentation-fidelity release — gamma-correct scanlines + a WebGL2 gamma fix + a sharper scanline profile; presentation-only so the pre-shader framebuffer + AccuracyCoin 141/141 are byte-identical, native default unchanged; visual verification pending), on top of **v2.2.7 "Timbre II"** (2026-08-04, an expansion-audio fidelity release — VRC6 recalibrated to ~1.0× a 2A03 pulse per the NESdev/field consensus [`VRC6_MIX_SCALE` 979→650; Mesen2's ~1.5× was the loud outlier], and the Sunsoft 5B envelope moved to the exact 5-bit 1.5 dB/step DAC; expansion-only, so the base 2A03 is byte-identical and AccuracyCoin holds 141/141), on top of **v2.2.6 "Almanac"** (2026-08-04, a de-monetization + provenance release — RustyNES is permanently open-source and income-free per ADR 0035; all planned monetization removed, native apps kept as free FOSS apps, and the TriCNES hybrid-address timing-calibration caveat disclosed per ADR 0030 for a v2.3.0 rework; zero emulation-core behavior changes so AccuracyCoin holds 141/141 by construction), on top of **v2.2.5 "Colophon"** (2026-08-03, a provenance/licensing/documentation-integrity release — zero emulation-core behavior changes so AccuracyCoin holds 141/141 by construction; `NOTICE` rewritten for full attribution + GPL-oracle disclosure + GeraNES, in-source "port" comments reworded to the oracle framing, the CRT-shader/NTSC provenance reworded to independent reimplementations, `docs/originality-and-provenance.md` added, README AI-assistance disclosure), on top of **v2.2.4 "Cartridge"** (2026-07-24, a libretro/RetroArch distribution cut — zero emulation-core changes so AccuracyCoin holds 141/141 by construction; the libretro core is confirmed up-to-date with all recent changes and builds for the buildbot ABIs [`x86_64-pc-windows-gnu`, `aarch64-linux-android`], and `rustynes_libretro.info` is corrected: `disk_control` false→true [the FDS Disk Control interface was wired but advertised absent], `display_version` v1.0.0→v2.2.4, mapper count 168→172; core options remain a documented future enhancement; the Antigravity reviewer standardization rides along), on top of **v2.2.3 "Datum"** (2026-07-23, a performance and accuracy-closure patch — the fast PPU dot path promoted to default and exposed, PGO binaries shipped on the release path, a same-runner relative frame-time CI gate, the last two Holy Mapperel residuals closed [MMC1 WRAM write-protect + FME-7 open bus, all 17 ROMs now `detail=0000`], the Sunsoft 5B level calibrated with `Mapper::mix_audio` widened to i32, a save-state schema gap fixed at `PPU_SNAPSHOT_VERSION` 8 + an APU v4 tail, an opt-in Zapper beam-relative light model, and the eleven `sprintN.rs` mapper modules renamed to `mNNN_.rs`; two optimizations measured and REJECTED and documented as such; AccuracyCoin 141/141 — on top of **v2.2.2 "Conduit"** [2026-07-21, a build/distribution/CI-integrity patch — the libretro buildbot recipe taken from 1 of 10 jobs green to all ten building, a GitHub Actions supply-chain hardening pass, and the toolchain collapsed to one pinned source of truth with no `nightly` on any build path; zero emulation-core changes], itself on **v2.2.1** [2026-07-15, a housekeeping patch: dev-tooling archival, a zero-source-change dependency consolidation, and a gitignored FDS test-corpus addition], itself on **v2.2.0 "Capstone"** [2026-07-12], the milestone cut that closes the v2.1.5 → v2.2.0 "deepen the existing project" run — its two remaining marquees the netplay matchmaking / lobby stack and the FDS medium model, atop a peripherals + quality/security pass (Famicom `$4016`-bit-2 microphone + 3×3-aperture Zapper; cargo-fuzz targets 3 → 8 finding + fixing two `Movie::deserialize` OOM-DoS paths; a read-only Tools → ROM Info browser); every change additive or default-off, AccuracyCoin 141/141) on the v2.0.0 "Timebase" one-clock / every-cycle-bus-access scheduler rewrite + Vs. `DualSystem` dual-console support. The v2.0.x "Harbor" mobile-finalization train (v2.0.1→v2.0.9) and the entire v2.1.x "Fathom" line (v2.1.0→v2.1.10) plus the v2.2.0 "Capstone" milestone have all shipped — the run's steps being v2.1.5 "Vernier" (regression-net & residual) → v2.1.6 "Timbre" (expansion-audio fidelity) → v2.1.7 "Stepping" (opt-in PPU/2A03 die-revisions + power-on RAM/palette models; the DMA "unexpected read" frontier a documented no-op on every oracle, ADR 0033) → v2.1.8 "Tempo" (a default-OFF fast PPU dot path + SIMD blitter + wasm size pass) → v2.1.9 "Aperture" (a marquee CRT shader stack + raw NTSC composite signal-decode + GIF/WAV capture + palette editor) → v2.1.10 "Loom" (TAStudio greenzone + Lua API breadth + browser-RA auth-proxy deploy stack + Vs. `DualSystem` libretro presentation) → v2.2.0 "Capstone" (the milestone cut closing the run) → v2.2.1 (housekeeping) → **v2.2.2 "Conduit"** the build/distribution/CI-integrity patch — preceded by v1.10.0 "Arcade" the native Libretro / RetroArch core, the v1.9.0→v1.9.9 iOS TestFlight train, the v1.8.0→v1.8.9 "Android" train, and the desktop-feature lineage v1.1.0→v1.7.1, all on the v1.0.0 production core (see the top "Current release" block + `docs/STATUS.md`). **Never claim any version *later* than v2.6.5 is released** — the **v2.2.6 → v2.3.0** line (de-monetization + NESdev remediation: audio [v2.2.7, shipped], video/gamma [v2.2.8, shipped], TAS/UX [v2.2.9, shipped], and the PPU left-edge + hybrid-address accuracy capstone at **v2.3.0** "Datum II" [shipped]) is now **complete**. The freed **v2.3.0** slot is repurposed as that accuracy capstone (NOT a store launch — RustyNES is now income-free per ADR 0035; any free mobile-app store listing is a later, unversioned step with no monetization — see `to-dos/ROADMAP.md`). Two distinct "v2.0"s exist and must not be conflated, **both now shipped, at different times, for different reasons**: the **engine-lineage v2.0** master-clock work shipped as the **v1.0.0** production core (2026-06-13) — it was the *only* scheduler through v1.10.0. RustyNES's own **v2.0.0 "Timebase"** release (2026-07-03) is a *different* milestone that *replaces* that same dot-lockstep scheduler outright: the **one-clock + every-cycle-bus-access collapse** (a single canonical cycle counter + a split-around-the-access `start_cycle`/`end_cycle` PPU catch-up, mirroring Mesen2's structure), full Vs. `DualSystem` dual-console emulation (core-and-harness-only; frontend wiring deferred), and the breaking save-state / cross-version changes it entailed (ADR 0002 / ADR 0028 / ADR 0029) — the one release that broke byte-identity / save-state compatibility, by design. The R1/R2 hard-tier MMC3 IRQ-timing residual was investigated under a bounded-effort campaign and is by-design-deferred beyond v2.0.0, not closed — see ADR 0002's decision-update section for the mechanism-level finding.
- **Forward plans + roadmap live in `to-dos/`.** `to-dos/ROADMAP.md` (updated in #129) is the planning entry point and frames the release line + "the path to v2.0.0 and beyond"; `to-dos/plans/` holds the per-release plan docs (through `v1.7.0-forge-plan.md` on `main`, plus the staged-forward `v1.8.0-android-plan.md` / `v1.9.0-ios-plan.md` / `v2.0.0-master-clock-plan.md`) + the `to-dos/plans/engine-lineage/` history archive + a `to-dos/plans/research/` reference-mining archive.
- The v1.0.0 release + GitHub Pages/CI + post-release record is in `docs/v1.0.0-synthesis-handoff-2026-06-13.md` — read it before touching CI, Pages, or release tooling. Full per-release history is in `CHANGELOG.md`.
- **Markdownlint is a CI gate** (pre-commit, pinned `markdownlint-cli v0.49.1`). The pin was v0.39.0 until the v2.6.3 dependency refresh, held because the newer local binary reported rules the pin lacked — chiefly **MD060** (`table-column-style`), which was therefore NOT gated. That is now measured and resolved: MD060's inferred default reads this corpus as style `compact` and reports **1,936 findings across 122 files** and nothing else, so `.markdownlint.json` pins `MD060` to the style actually in use (`leading_and_trailing`), which measures **zero** and rewrites no document. It IS a gate now. Still verify with `pre-commit run markdownlint --all-files` rather than the bare binary — the pin and the local build can drift apart again. `.markdownlint.json` also keeps `MD013`/`MD033`/`MD041` disabled by design (long technical tables, the README HTML banner/`
`, the HTML-led README). `.markdownlintignore` exempts `ref-docs/`, `ref-proj/` (the reference-emulator clone, now removed from disk but kept in the ignore lists as a firewall guard so it can never re-enter the tree — see the MOST IMPORTANT RULE section above), the vendored `tricnes/` + upstream READMEs, and the frozen `docs/archive/` + `to-dos/archive/` trees — don't lint or reformat those.
@@ -240,7 +240,7 @@ These cross-cutting decisions span multiple files. Reading individual chip docs
- **"Inert on almost every cycle" predicts an optimization win only if the work is actually EXECUTED.** Under `lto = "fat"` with `codegen-units = 1` the guarded code is already inlined into its caller, its repeated loads already merged by common-subexpression elimination, and always-not-taken branches are perfectly predicted — so swapping predictable not-taken branches for an equivalent count of loads plus a predicate is arithmetically a wash. This is why APU Workstream D produced three nulls (D1, D3, D6) and why D2 and D4 are left unmeasured: their prior is a null, not an unknown. Full numbers and the three conditions that would justify reopening: `docs/performance.md`.
-- **The Antigravity reviewer USED to delete and replace its comment each round, destroying unread findings. Fixed in v2.4.0; the fix is INERT until it reaches each repo's `main`.** Observed on PR #428: round 1 posted at `13:37:57Z` and round 2 at `14:21:35Z`, after which `gh api repos/O/R/issues/N/comments` returned exactly ONE bot comment with `created_at == updated_at == 14:21:35Z`. The first was **gone** — not edited (the timestamps would differ), not appended to. CodeRabbit and Copilot threads persist and can be resolved, so an unread finding stays visible; an Antigravity finding did not, and nothing on the PR indicated a round had happened. **Both destroyed rounds on #428 were blocking and correct, one a data-loss defect**, so the cost was not hypothetical. `scripts/agy-review.sh` now **edits one comment per PR**, folding earlier rounds into a collapsed `` archive bounded by `MAX_BODY_BYTES` (oldest dropped first, and the count dropped is *announced*), and issues no `DELETE` at all — `scripts/agy-review-selftest.sh` asserts the absence of a `DELETE`, and the body format lives in the sourceable `scripts/_agy_comment_body.sh` so the selftest exercises the real implementation rather than a copy (its first version inlined the pipeline and a mutation came back NOT CAUGHT). **Rounds are delimited by an HTML-comment sentinel, not by the `` tag**: a review body legitimately contains `` blocks, and matching the tag cut *inside* a round — found in review, one commit after adopting markers for exactly that reason elsewhere in the same file. **The workflow runs the script from a DEFAULT-BRANCH checkout, so the fix does nothing on any PR until it merges to `main` there** — and note the corollary that bit once: the workflow YAML itself comes from the PR branch, so a change adding a script file breaks its own PR unless the workflow half tolerates both script sets. The four sibling repos (`Rusty2600`, `RustyN64`, `RustySNES`, `SLAC`) keep the old destructive behaviour until their own PRs land. Until then: read the comment **before every push**, and quote its findings into your reply, since the reply persists and the original may not.
+- **The Antigravity reviewer USED to delete and replace its comment each round, destroying unread findings. Fixed in v2.4.0; the fix is INERT until it reaches each repo's `main`.** Observed on PR #428: round 1 posted at `13:37:57Z` and round 2 at `14:21:35Z`, after which `gh api repos/O/R/issues/N/comments` returned exactly ONE bot comment with `created_at == updated_at == 14:21:35Z`. The first was **gone** — not edited (the timestamps would differ), not appended to. CodeRabbit and Copilot threads persist and can be resolved, so an unread finding stays visible; an Antigravity finding did not, and nothing on the PR indicated a round had happened. **Both destroyed rounds on #428 were blocking and correct, one a data-loss defect**, so the cost was not hypothetical. `scripts/agy-review.sh` now **edits one comment per PR**, folding earlier rounds into a collapsed `` archive bounded by `MAX_BODY_BYTES` (oldest dropped first, and the count dropped is *announced*), and issues no `DELETE` at all — `scripts/agy-review-selftest.sh` asserts the absence of a `DELETE`, and the body format lives in the sourceable `scripts/_agy_comment_body.sh` so the selftest exercises the real implementation rather than a copy (its first version inlined the pipeline and a mutation came back NOT CAUGHT). **Rounds are delimited by an HTML-comment sentinel, not by the `` tag**: a review body legitimately contains `` blocks, and matching the tag cut *inside* a round — found in review, one commit after adopting markers for exactly that reason elsewhere in the same file. **The workflow runs the script from a DEFAULT-BRANCH checkout, so the fix does nothing on any PR until it merges to `main` there** — and note the corollary that bit once: the workflow YAML itself comes from the PR branch, so a change adding a script file breaks its own PR unless the workflow half tolerates both script sets. **RESOLVED FLEET-WIDE, verified 2026-08-26.** The sentence that stood here said the four sibling repos (`Rusty2600`, `RustyN64`, `RustySNES`, `SLAC`) "keep the old destructive behaviour until their own PRs land". Those PRs landed — Rusty2600 #34, RustyN64 #260, RustySNES #334, SLAC #22, all merged — and `origin/main` in every one now carries `scripts/agy-review.sh` **byte-identical to the template** with zero `-X DELETE` calls and `scripts/_agy_comment_body.sh` present. `RustyNES_MiSTer` was installed on 2026-08-26 and gets the fixed version from the start. **The claim was repeated to a user from this file before being re-checked, which is the exact failure this document warns about two bullets down** — a stale note here launders into a stated fact. Re-verify with a one-liner over the fleet (`git show origin/main:scripts/agy-review.sh | grep -c '\-X DELETE'` plus a `cat-file -e` for the helper) rather than trusting this paragraph. The reading discipline still stands on its own merits: read the comment **before every push** and quote its findings into your reply, because a reply persists and an edited comment's archive is bounded.
- **The bot ceremony has a THIRD hiding place: plain issue comments.** `AGENTS.md` already warned that CodeRabbit and Copilot post suppressed findings in review BODIES, invisible to a resolve-every-thread sweep. The **Antigravity reviewer posts its entire review as an ordinary PR comment** — not a review, not a thread — so `gh pr view --json reviews` misses it too. On PR #385 its blocking finding (an emoji in code, against a hard project rule) was caught only because the comment list was read. Fetch all three: `reviewThreads`, `reviews[].body`, AND `comments[]`. Also note Antigravity re-reviews on every push, so a green build plus zero unresolved threads can still sit under an unread blocking finding — check the newest comment's timestamp against your last push.
@@ -259,6 +259,14 @@ These cross-cutting decisions span multiple files. Reading individual chip docs
- **The MiSTer / SuperStation One programme is a co-simulation ORACLE role, not a port** (ADR 0037; `docs/mister.md`; `to-dos/plans/v2.5.0-fabric-plan.md`). RustyNES cannot become a bitstream — a MiSTer core is SystemVerilog compiled by **Quartus 17.0.2** into a Cyclone V bitstream — so "Fabric" writes a **new** core from public documentation in a sibling `RustyNES_MiSTer` repo and verifies it against this emulator. Facts worth not re-deriving: **v2.5.0 is scoped to "the 6502 rung closes"** (7–13 months FTE for a full core against a 2–4 week window, so PPU/APU/MiSTer are **v2.6–v2.9**); the design is **replay, not lockstep** (`Nes` has no per-cycle step, and the determinism contract makes a pre-recorded trace *exactly* the lockstep trace); **no DPI-C**, because it puts `` `ifdef SIMULATION `` guards into RTL that must also pass Quartus; **hash first, capture on divergence** (4200 frames ≈ 7.5 GB of per-cycle CSV versus ~480 KB of 4096-cycle checkpoints — the plan's 244 KB assumed an 8-byte record, and `ENCODED_LEN` is 16: cycle AND hash); **`index_framebuffer` pre-palette is the PPU gate** so a palette difference cannot masquerade as a rendering difference; and **`ppu-state-trace` plus mixed `f32` audio are DIAGNOSTIC, never gates** — they encode RustyNES's modelling choices, not hardware facts. Two risks are accepted **in writing**: `NES_MiSTer` scores 121/125 on AccuracyCoin and *real Famicom AV hardware also scores ~121/125*, so there is no published accuracy headroom and the core **may be declined as a duplicate** (Retro Remake is the planned fallback home, not a contingency); and **the oracle can be wrong**, since 141/141 is not "matches silicon", so every rung is labelled by whether it has an *independent* oracle. **The `sys/` licence audit is DONE (v2.4.3) and it INVERTED the hedge.** The plan feared a GPL-2.0-**only** file would force the RTL down to GPL-2.0-or-later; across 57 files there are **zero**, and four are GPL-3.0-or-later (`ddr_svc.sv`, `hps_io.sv`, `scandoubler.v`, `sd_card.sv`). `hps_io.sv` is **not optional** — it is how a core receives a ROM from the HPS and how the OSD reaches it — so the combined bitstream **must** be GPL-3.0-or-later, which is already RustyNES's licence. No relicensing needed. **The Quartus 17.0.2 subset is likewise FITTED, not documented** (v2.4.3): a kitchen-sink module reached a placed-and-routed netlist on a 5CSEBA6U23I7 with **0 errors and 0 synthesis warnings**, and its 2 KiB array inferred as **2 M10K blocks with 29 total registers** — not 16,413 — from the source style alone, no `ramstyle` attribute; the `initial` block produced a real MIF and the `enum` was one-hot encoded. Nine constructs are *fitted*; plain `case`, `priority case` and `$bits` stay *documented* because the module does not exercise them, and extending the subset means extending that module and re-fitting, never adding a row on the strength of documentation. **Quartus 17.0.2 specifically**: Lite 25.1 still supports Cyclone V, so device support is not the constraint — `Template_MiSTer` pins v17.0.x in writing, `sys/` carries Platform Designer IP that a newer Quartus compiles only after a **one-way IP upgrade**, and a newer Quartus is the *more permissive* tool, so a pass under it would report success for a property never tested. Its installer tarball is a **two-stage bundle** whose own `setup.sh` runs only the 17.0.0 base, so a naive install silently lands on 17.0.0 — assert the exact version, never a `17.0*` pattern. **Rung 1 has three opcode groups closed** (v2.4.4 reset + implied, v2.4.5 addressing modes + loads/stores + branches, v2.4.6 indexed + ADC/SBC + compares): **573 records across three ROMs**, seven mutations caught at each of the last two steps, and the entire rung has needed **no oracle-side change** because the DUT is the third writer of `CpuBootTrace` and `--skip-fields` already existed. Detail: `docs/mister.md` and `RustyNES_MiSTer/docs/rung1-6502.md`. **Three tests read correctly and verified nothing**, each found by mutation rather than by reading — a `TXS` whose wrong answer coincided with `TSX`'s leftover flags, a store/load pair in the SAME addressing mode (self-consistent under any address mutation), and a read of RAM the program had not written (the oracle seeds work RAM; a flat-memory testbench zeroes it). **The oracle also corrected our own spec**: `docs/cpu-6502.md` said reset was 7 cycles in one section and 8 in another, and an independent implementation written FROM that document implemented seven and diverged on its first record — it is eight. **The accuracy gate is NOT automated**: the sibling's `cpu-smoke` step says in its own name that it is not the gate, because the goldens are not vendored there; `make -C tb cpu-gate GOLDEN=…` is the real comparison and golden fetching from a pinned commit is not built. Also: **`misterfpga.org` returns HTTP 403 to automated fetching**, so its Development-forum threads need reading by hand, and a **DE10-Nano requires the SDRAM add-on** for any NES core (the NES reads cartridge ROM directly; the onboard DDR3 is too slow) while the SuperStation One has 128 MB integrated.
- **A test ROM's own source is the SPECIFICATION, and checking whether it is reachable costs one command.** Three AccuracyCoin entries resisted a full working session of tracing and hypothesis in v2.6.4. AccuracyCoin is **MIT-licensed and its assembly source is one `curl` away**; it carries a prose explanation of every assertion, written by the author who chose the stimulus, and it settled all three in minutes — naming a rule the nesdev pages do not state at all (`$4015` reads are internal to the 2A03, so the data bus is not driven), giving the exact failing stimulus (`LDX #$16 / LDA $40FF,X`, which matched a trace divergence found independently), and distinguishing the neighbouring assertions a broad "fix" would break. The v2.6.4 plan's own note that the source "is not vendored" is true of this repository and had been read as *unavailable*; they are not the same statement. **A test ROM is stimulus, not a reference implementation, so reading it raises no firewall question** — check its licence, then read it before theorising about its verdict. The same holds for blargg's `readme.txt`, which states two APU rules the wiki does not. **And decode its failure codes from the macro, never by inference**: `TEST_Fail` reports `(ErrorCode << 2) | 2` and the runner sets `ErrorCode` to **1** before every routine, so `Fail(N)` names test N one-based — read as a zero-based index it is off by one, and in v2.6.4 that made a **regression** (test 7 -> test 5) read as *progress*, a description that reached a code comment before the macro was read. It also retires v2.6.3's reading that six entries "sharing one failure code" implied one shared cause: the code indexes within one routine, so two entries sharing it share nothing.
+- **The AccuracyCoin SUB-TEST corpus is the rung-5 instrument, and it was sitting unused.** `tests/roms/AccuracyCoin/sub-tests/` holds **28 ROMs that boot straight into ONE catalog entry**; through v2.6.4 exactly three were wired as gates. `sprite-eval-oam-corruption` reproduces a full-battery failure in **6.0 M cycles instead of 134 M**, a factor of twenty-two, so an investigation that cost eighty minutes per iteration costs one. Wire the sub-test before debugging the entry. Two rules for using them: the **result address is read from the golden RAM** (scan `$0400-$04FF` for the byte the ROM actually wrote) and never from the catalog, because they differ; and `subtest_verdict.py` **refuses** when the oracle side is not a pass, so a sub-test the oracle itself fails cannot adjudicate anything — `sprite-eval-arbitrary-sprite-zero` is in that state and is deliberately unregistered. **Running a `NotRun` entry standalone is also the only way to separate "the behaviour is wrong" from "the battery never got there"**, which the status vector cannot do: it re-read `Sprites On Scanline 0` (the coded-pass item open since v2.6.4) and `Implied Dummy Reads` as CORRECT, both hidden behind a stall fifteen entries upstream.
+- **A NOT CAUGHT can mean the MUTANT IS RIGHT — a sixth meaning, and the most dangerous.** v2.6.5 mutated `$4015`'s DMA-conflict path back to the external data bus, measured it byte-identical against `internal-data-bus`, and filed it as *stimulus blind*. It was not: the 134 M battery then reported `APU Register Activation` as a **regression**, and AccuracyCoin's own test 7 states the rule — "`$24` is the triangle channel from reading `$4015`, **+ bit 5 is set from the byte on page 2**". The mutation was the correct code and the baseline it was measured against was the defect. **When a mutation is NOT CAUGHT, ask whether the mutant might be right before filing it as blind** — and prefer a source that states the rule over a gate that fails to distinguish it.
+- **Run a mutation against the gate that can SEE it, not only the gate the fix was written for.** The dot-339 sprite-X quirk's "zero regardless of `rendering`" mutant came back NOT CAUGHT against `ppu-misc-stale-bg-shift-regs` — because **every expectation in that ROM is consistent with X = 0**: three of its four sub-tests place sprite 0 at X = `$00` and the fourth expects X = `$80` to *behave* as 0, so it is structurally incapable of catching a mutant that zeroes too often. Against the whole suite it fails `sprite-render`, `sprite-mask` and four sub-tests. A NOT CAUGHT scoped to one ROM is a statement about that ROM.
+- **A difference can hide in the DATA for millions of cycles, and an address-only diff will not see it.** The v2.6.5 VBlank defect put a wrong `$2002` bit through `ASL`/`ROL` into `$0053` — *the same address on both sides* — so `pc`/`bus_addr`/`bus_access` stayed identical for **3.04 M cycles** until the ROM compared the byte and a `BNE` forked at 44,343,242. The address diff reports the fork; the cause is three million cycles upstream. When the first divergence is a branch on a stored value, **search backwards for who wrote the byte the branch read** rather than reading the branch.
+- **When one shared window serves several flags, check each flag's wording separately.** `$2002`'s three flags were cleared through one `dot <= 1` window. The wiki states each differently — VBlank "cleared **on** dot 1", sprite 0 "stays set **until** dot 1", overflow "cleared at the **start of**" — and the difference is a dot. Narrowing the window fixed AccuracyCoin's VBlank sweep and broke `blargg07`/`blargg11` on **bit 5**; splitting it satisfied both. *"It fixes a ROM but breaks a gate" is a bug report about the change*, and the suite said so in one run.
+- **Disassemble the PC to find where a run stops; never infer it from catalog order.** v2.6.4 published such an inference and retracted it. v2.6.5 took a PC histogram over the tail of the 134 M trace — `$E819`, pinned for the last **2,000,000 records** — and the three bytes there are `EE 14 40`, `INC $4014`: a read-modify-write **on the OAM DMA register**. The defect was that a *failed* OAM-DMA halt still stalled the CPU, and a stalled CPU holds `cpu_we`, so a pending write stayed a write forever. **Fifteen catalog entries sat behind one line**, and fixing it took the battery from 131 of 146 entries executed to **146 of 146**.
+- **`/tmp` is tmpfs — it is RAM.** A 134 M-cycle `obs.bin` is **2.0 GB per side** and a 4500-frame `irq.csv` is **8.4 GB**, which `nes_golden_export` builds as one `String` in memory before writing (peak RSS ~12 GB). Big traces belong in `~/.cache/rustynes-cosim/`, and the CSV is a diagnostic no gate reads — delete it.
+- **Verilator exempts signals whose name begins with `unused`.** That prefix is why `ppu2c02.sv`'s `unused_until_rendering` passes UNUSEDSIGNAL and a differently-named equivalent does not. Relevant whenever a modelled register is real hardware state with no reader yet.
- **A pass count is a claim about what RAN; measure that separately.** `accuracycoin_status` reported "IDENTICAL entry for entry across all 146 entries" while **58 of those entries were `NotRun` on BOTH sides** — the 600-frame window reaches the CPU catalog and stops partway through `CPU Interrupts`, asking nothing about the APU, PPU, sprite-evaluation or PPU-misc suites, i.e. the chips rungs 3 and 4 exist for. The comparator was correct and the stimulus window was short. **4500 frames executes all 146** (134,012,761 cycles) and is now the golden. Three standing rules fall out. First, **break a result down by group and look for a group at zero** — a whole subsystem missing is far easier to see than 58 rows scattered through a table. Second, **write acceptance criteria a vacuous result cannot satisfy**: this one said "including `Skipped` and `NotRun`" so a DUT could not pass by skipping, and needed one more clause — *and no entry is `NotRun` on both sides*. Third, **coverage work is not bookkeeping** — the first run of the widened window found a real RTL defect at cycle 20,636,325, 2.8 M cycles past where every previous run had stopped.
- **Comparing two consoles at a fixed cycle count assumes both reach the same PLACE, and that stops holding exactly when they disagree.** At 17.9 M cycles the DUT and oracle agreed on 88 AccuracyCoin entries; at 134 M the oracle had all 146 and the DUT had **five**. Nothing was wrong with either dump. **Measure the reference over the same axis or the subject's numbers mean nothing**: the oracle climbs 88 -> 95 -> 117 -> 120 -> 146 while the DUT goes 88 -> 5 -> 5 -> 5, and only that pairing makes the shape the DUT's rather than the ROM's or the instrument's. **Two points support any story** — from 88-then-5 alone the conclusion was "completed a pass and restarted", and three more run lengths showed a flat line, which is a different defect. Then: **catalog order is a plausible suspect, not evidence.** The suite after the last one the DUT completed had never executed, so "it hangs there" was published in a commit body, a CHANGELOG, release notes and a PR. One PC probe refuted it — a three-cycle self-loop at `$80DF`, which disassembles to `INC $EC` / `JMP $80DF`, AccuracyCoin's **menu idle loop**, meaning the DUT had **reset** and returned to the menu. Reset and hang are different defects with different searches. **When the question is "where did it stop", the answer is the program counter.**
- **`cargo test` prints `error:` when a TEST FAILS, not only when the build breaks.** A mutation classifier keyed on `^error` reported BUILD-FAILED for three mutations that were all CAUGHT — the inverse of the usual trap, discarding evidence rather than manufacturing it. Classify from the runner's own vocabulary in order: `could not compile`/`error[E` -> BUILD-FAILED, `test result: FAILED` -> CAUGHT, `test result: ok` -> NOT CAUGHT, anything else -> investigate. And **run the baseline through the same classifier**: a baseline that does not land on the "tests pass" branch means the classifier is broken before any mutant has run.
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index b4dfac5b..c0eb888b 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -2,7 +2,7 @@
**Document Version:** 2.1.0
**Last Updated:** 2026-08-20
-**Applies to:** RustyNES v2.6.4 (the scheduling model is v2.0.0 "Timebase" onward)
+**Applies to:** RustyNES v2.6.5 (the scheduling model is v2.0.0 "Timebase" onward)
This document fixes the high-level architecture of RustyNES. The per-subsystem specs under `docs/` (`cpu-6502.md`, `ppu-2c02.md`, `apu-2a03.md`, `mappers.md`, `scheduler.md`) take these decisions as given and elaborate one chip each. After reading this you should know the workspace shape, the scheduling model, the public boundary, and the load-bearing invariants. The canonical, always-current architecture spec is [`docs/architecture.md`](docs/architecture.md); this file is the top-level companion.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 684c8578..90c3684c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -26,6 +26,85 @@ cycle-accurate core later replaced.
## [Unreleased]
+## [2.6.5] - 2026-08-29 - "Muster" (rung 5 closes — the AccuracyCoin status vector is identical entry for entry across all 146 entries, with 146 of 146 executed on both sides and none NotRun. Five PPU defects close the last six differing entries, and one of the release's own diagnoses is retracted)
+
+A muster is a roll call where every name is called **and answered**. That is this
+release's acceptance exactly, in two clauses: the vector agrees entry for entry,
+*and* no entry is unrun on both sides. The second clause is v2.6.4's addition —
+without it, an identical vector over entries that never executed is a pass, and
+was one.
+
+### The gate
+
+```text
+coverage: 146 of 146 entries executed on both sides (0 on neither, 0 on one side only)
+status vectors are IDENTICAL entry for entry across all 146 entries.
+```
+
+Measured over the 4500-frame golden, 134,012,761 cycles. At the version's start
+the same gate read **5 of 146 executed** and 22 differing.
+
+### Five defects, and the shape they share
+
+Four of the five were invisible to every gate that existed when the version
+opened, and the recurring shape is **a gate agreeing about a question it was
+never asked**.
+
+**The background shift registers' reload and their shift clock need separate
+gates.** With one shared gate `BG Serial In` was not merely failing, it was
+*arithmetically unreachable*: reload dots are absolute, so on a render re-enable
+the next reload is at most seven dots away, and the reload discards the low seven
+bits — a serial-in one can never reach bit 7, on any alignment, for any stimulus.
+Modelling both structures reproduces **both** measured shifter values, the
+oracle's `F807` falling out of the split model without being fitted to it.
+
+**That fix alone left the gate red.** The sprite X counters are **not** gated on
+rendering, and AccuracyCoin's `Stale Sprite Shift Registers` test 2 states it
+outright — "Rendering was disabled for 18 ppu cycles, but the sprite counters
+were NOT halted during that time". This core froze them, so a disable/enable pair
+pushed every sprite right by the width of the window. **The ROM that states the
+rule passes either way**: it expects no hit at X=254, and a sprite shoved 18 dots
+further right is also off the end of the line.
+
+**The PPUADDR second-write `v <- t` copy is delayed**, and the wiki says so inside
+the write sequence itself — "wait 1 to 1.5 dots after the write completes". This
+core committed it in the write's own edge. Swept 1 to 4 dots (all close
+`Hybrid Addresses`) against a control at 8 and 12 (both fail, which is what
+proves the parameter reached the compiler); the documented minimum ships.
+
+**The pre-render line clears secondary OAM.** The whole evaluation block —
+*including* the clear — was gated on `scanline < 240`, so the pre-render line kept
+what scanline 239 had left and the next frame's scanline 0 drew it. **No sprite
+can ever render on scanline 0**, because OAM Y is stored one less than the display
+row. A sprite-0 probe over the full battery named it in one run: 24 hits in
+134 M cycles, four of them at scanline 0, one per frame.
+
+The fifth, the octal latch holding across the read dot, is verified by exactly one
+gate and was unverifiable until the fourth landed — the two compose the hybrid
+address together and neither produces it alone.
+
+### A diagnosis retracted
+
+The residual was read as a **two-dot CPU/PPU alignment error**, from comparing
+per-dot record spans across two instruments. Three configurations refute it: at
+the committed alignment the two consoles execute identical `pc`, `bus_addr` and
+`bus_access` for **1,695,131 cycles**, while a two-dot power-on shift moves the
+first divergence back to 593,228 and takes the differing share from 5.13% to
+66.80%. The "two dots" was two instruments stamping their records at different
+points in the cycle — the v2.5.7 lesson, third occurrence.
+
+### Also
+
+`rustynes-cosim`'s `state_trace_records_carry_their_cpu_cycle` was gated on a
+feature no CI step enabled, so it **ran nowhere** — a regression test the gate
+could not reach, which is the shape the surrounding CI steps exist to prevent. It
+now runs, and the test that is genuinely inapplicable under that feature is gated
+out with its reason rather than left failing.
+
+**The oracle changes on the default path** (a `Controller::write_strobe` owed-shift
+fix), so **AccuracyCoin 141/141 (RAM decoder)** and nestest 0-diff are **verified,
+not asserted**.
+
## [2.6.4] - 2026-08-26 - "Rubric" (OAM DMA lands and all nine AccuracyCoin disagreements close, every rule that closed the last three stated by the test ROM and by neither nesdev page — and then the gate that certified them is found to have covered 88 of 146 entries. The emulation core is unchanged)
A rubric is the authoritative statement of the rules, written in the margin by
diff --git a/Cargo.lock b/Cargo.lock
index 5e860420..e0dd0add 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -641,9 +641,9 @@ checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
[[package]]
name = "chacha20"
-version = "0.10.1"
+version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
+checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
@@ -4290,7 +4290,7 @@ dependencies = [
[[package]]
name = "rustynes-android"
-version = "2.6.4"
+version = "2.6.5"
dependencies = [
"android-activity",
"android_logger",
@@ -4308,7 +4308,7 @@ dependencies = [
[[package]]
name = "rustynes-apu"
-version = "2.6.4"
+version = "2.6.5"
dependencies = [
"bitflags 2.13.1",
"criterion",
@@ -4321,7 +4321,7 @@ dependencies = [
[[package]]
name = "rustynes-cheevos"
-version = "2.6.4"
+version = "2.6.5"
dependencies = [
"cc",
"ureq",
@@ -4329,7 +4329,7 @@ dependencies = [
[[package]]
name = "rustynes-core"
-version = "2.6.4"
+version = "2.6.5"
dependencies = [
"bitflags 2.13.1",
"criterion",
@@ -4346,7 +4346,7 @@ dependencies = [
[[package]]
name = "rustynes-cpu"
-version = "2.6.4"
+version = "2.6.5"
dependencies = [
"bitflags 2.13.1",
"criterion",
@@ -4357,7 +4357,7 @@ dependencies = [
[[package]]
name = "rustynes-frontend"
-version = "2.6.4"
+version = "2.6.5"
dependencies = [
"anstyle",
"arboard",
@@ -4416,18 +4416,18 @@ dependencies = [
[[package]]
name = "rustynes-gamedb"
-version = "2.6.4"
+version = "2.6.5"
dependencies = [
"rustynes-core",
]
[[package]]
name = "rustynes-gfx-shaders"
-version = "2.6.4"
+version = "2.6.5"
[[package]]
name = "rustynes-hdpack"
-version = "2.6.4"
+version = "2.6.5"
dependencies = [
"lewton",
"png",
@@ -4438,7 +4438,7 @@ dependencies = [
[[package]]
name = "rustynes-ios"
-version = "2.6.4"
+version = "2.6.5"
dependencies = [
"bytemuck",
"cpal",
@@ -4452,7 +4452,7 @@ dependencies = [
[[package]]
name = "rustynes-libretro"
-version = "2.6.4"
+version = "2.6.5"
dependencies = [
"libc",
"rust-libretro",
@@ -4461,7 +4461,7 @@ dependencies = [
[[package]]
name = "rustynes-mappers"
-version = "2.6.4"
+version = "2.6.5"
dependencies = [
"bitflags 2.13.1",
"criterion",
@@ -4473,7 +4473,7 @@ dependencies = [
[[package]]
name = "rustynes-mobile"
-version = "2.6.4"
+version = "2.6.5"
dependencies = [
"rustynes-core",
"rustynes-hdpack",
@@ -4488,7 +4488,7 @@ dependencies = [
[[package]]
name = "rustynes-netplay"
-version = "2.6.4"
+version = "2.6.5"
dependencies = [
"futures-util",
"js-sys",
@@ -4504,7 +4504,7 @@ dependencies = [
[[package]]
name = "rustynes-ppu"
-version = "2.6.4"
+version = "2.6.5"
dependencies = [
"bitflags 2.13.1",
"criterion",
@@ -4516,21 +4516,21 @@ dependencies = [
[[package]]
name = "rustynes-probe"
-version = "2.6.4"
+version = "2.6.5"
dependencies = [
"rustynes-core",
]
[[package]]
name = "rustynes-ra"
-version = "2.6.4"
+version = "2.6.5"
dependencies = [
"rustynes-cheevos",
]
[[package]]
name = "rustynes-script"
-version = "2.6.4"
+version = "2.6.5"
dependencies = [
"mlua",
"piccolo",
@@ -4541,7 +4541,7 @@ dependencies = [
[[package]]
name = "rustynes-test-harness"
-version = "2.6.4"
+version = "2.6.5"
dependencies = [
"insta",
"png",
diff --git a/Cargo.toml b/Cargo.toml
index fcbe97ab..3813b988 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -77,7 +77,7 @@ default-members = ["crates/rustynes-libretro"]
# `release-auto.yml` reads the `## [X.Y.Z]` line for BOTH the release body
# fallback and the title codename — so the date and quoted codename are load-
# bearing, not decoration.
-version = "2.6.4"
+version = "2.6.5"
edition = "2024"
rust-version = "1.96"
license = "GPL-3.0-or-later"
diff --git a/OVERVIEW.md b/OVERVIEW.md
index 846776ce..61443019 100644
--- a/OVERVIEW.md
+++ b/OVERVIEW.md
@@ -2,7 +2,7 @@
**Document Version:** 2.1.0
**Last Updated:** 2026-08-24
-**Applies to:** RustyNES v2.6.4
+**Applies to:** RustyNES v2.6.5
---
@@ -22,9 +22,9 @@
RustyNES is the **definitive NES emulator for the modern era** — combining cycle-perfect accuracy with a complete contemporary feature set and the safety guarantees of Rust. It is more than an emulator: it is a platform for NES preservation, competitive online play, tool-assisted speedrunning, and homebrew development.
-As of **v1.0.0**, that vision was realized: RustyNES clears the Mesen2 / higan / ares accuracy bar, ships a polished desktop application and a browser build, and supports the full platform surface — netplay, achievements, TAS movies, a debugger, FDS, and arcade (Vs. / PlayChoice-10) hardware. Since then the additive v1.x line added three more platforms (native Android, iOS / iPadOS, and a Libretro / RetroArch core), **v2.0.0 "Timebase"** replaced the scheduler substrate with the one-clock / every-cycle-bus-access model (ADR 0029 — the one deliberate breaking release), and the v2.1.x → v2.3.x lines deepened accuracy, presentation, and analysis tooling. The current release is **v2.6.4 "Rubric"**. The never-tagged v2.4.0 "Concordance" shipped inside **v2.4.1 "Fabric"** — this sentence had attached that fact to whichever release was current, carried forward by three mechanical version bumps, and said it of v2.4.2, v2.4.3 and v2.4.4 in turn.
+As of **v1.0.0**, that vision was realized: RustyNES clears the Mesen2 / higan / ares accuracy bar, ships a polished desktop application and a browser build, and supports the full platform surface — netplay, achievements, TAS movies, a debugger, FDS, and arcade (Vs. / PlayChoice-10) hardware. Since then the additive v1.x line added three more platforms (native Android, iOS / iPadOS, and a Libretro / RetroArch core), **v2.0.0 "Timebase"** replaced the scheduler substrate with the one-clock / every-cycle-bus-access model (ADR 0029 — the one deliberate breaking release), and the v2.1.x → v2.3.x lines deepened accuracy, presentation, and analysis tooling. The current release is **v2.6.5 "Muster"**. The never-tagged v2.4.0 "Concordance" shipped inside **v2.4.1 "Fabric"** — this sentence had attached that fact to whichever release was current, carried forward by three mechanical version bumps, and said it of v2.4.2, v2.4.3 and v2.4.4 in turn.
-> RustyNES's emulation core descends from an extensively-documented accuracy program. Where this and related docs reference deep "v1.x"/"v2.x" engine narrative, read it as upstream engine lineage (engineering history), not as RustyNES release versions. Two distinct "v2.0"s exist and must not be conflated: the engine-lineage v2.0 master-clock work shipped as RustyNES **v1.0.0**, while RustyNES's own **v2.0.0 "Timebase"** (2026-07-03) is the later release that *replaced* that same scheduler. The current release is **v2.6.4**.
+> RustyNES's emulation core descends from an extensively-documented accuracy program. Where this and related docs reference deep "v1.x"/"v2.x" engine narrative, read it as upstream engine lineage (engineering history), not as RustyNES release versions. Two distinct "v2.0"s exist and must not be conflated: the engine-lineage v2.0 master-clock work shipped as RustyNES **v1.0.0**, while RustyNES's own **v2.0.0 "Timebase"** (2026-07-03) is the later release that *replaced* that same scheduler. The current release is **v2.6.5**.
---
diff --git a/README.md b/README.md
index 885c6b5a..1cc07223 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
-

+


@@ -674,7 +674,7 @@ and the Material-for-MkDocs documentation handbook at
## Current Release
-RustyNES's current release is **v2.6.4 "Rubric"** — OAM DMA lands and all nine AccuracyCoin disagreements close, every rule that closed the last three stated by the test ROM and by neither nesdev page — and then the gate that certified them is measured to cover 88 of 146 entries. The emulation core is unchanged. Built on **v2.6.3 "Mainspring"** — the DUT runs on one master clock, and four enables that were never enabling — plus AccuracyCoin end to end and a status vector that names its disagreements by test. The emulation core is unchanged. Built on **v2.6.2 "Witness"** — rung 4 closes: blargg APU battery 11/11 on the co-simulation DUT, six defects no self-written gate could see, and a suite that had been asserting nothing for five minor releases. The emulation core is unchanged. Built on **v2.6.1 "Interleave"** — the DMC and its DMA cycle steal in the MiSTer co-simulation DUT, cycle-exact on the bus. The emulation core is unchanged. Built on **v2.6.0 "Assay"** — the triangle, the noise channel and the sweep unit **in the MiSTer co-simulation DUT** — and an audit of how much of the APU was fitted to the oracle rather than derived from documentation. The emulation core is unchanged. Built on **v2.5.9 "Overture"** — rung 4 opens: the two pulse channels, the frame counter, and four ROM defects the stimulus measurement found first. Built on **v2.5.8 "Blanking"** — VBlank, NMI and the PPUSTATUS race close rung 3 — and both fixes were deletions. Built on **v2.5.7 "Collimation"** — sprite rendering closes exact — the phase was wrong by two dots, and every window was compensating. Built on **v2.5.6 "Vestige"** — Sprite evaluation closes: all 59,993 overlapping cycles match, nine of nine behavioural mutants caught and two proved inert (announced as seven of eight at the cut), and the fix is a byte index that outlives the walk that set it. Built on **v2.5.5 "Raster"** — the first full frame, and three blind spots in the stimulus that fed it. Built on **v2.5.4 "Escapement"** — the background fetch pipeline, and an access two dots early that five gates could not see. Built on **v2.5.3 "Hysteresis"** — toggling rendering takes effect three dots after the write, and four instruments to prove it. Built on **v2.5.2 "Dormant"** — the 2C02 register file, and a gate that passed while testing nothing. Built on **v2.5.1 "Retrace"** — the interrupt sweep closes rung 2, and a gate reported a pass it could not have earned. Built on **v2.5.0 "Rungwork"** — the 6502 rung, and the two gates it cannot reach. Built on **v2.4.9 "Plumbline II"** — the bus half of rung 2, and what it found the day it existed. Built on **v2.4.8 "Palimpsest"** — read-modify-write, and a gate that cannot see its own subject. Built on **v2.4.7 "Keystone"** — the stack closes, and a dead line proves itself dead. Built on **v2.4.6 "Abacus"** — the core learns arithmetic. Built on **v2.4.5 "Compass"** — the core reaches memory, and chooses. Built on **v2.4.4 "Ignition"** — the first real RTL. The 6502's eight-cycle reset and the seventeen single-byte implied opcodes, in SystemVerilog in the sibling repository (`RustyNES_MiSTer@7f092bd`), matching the oracle on all seven CPU fields -- 29 records, and the gate demonstrated to fail on four mutations. The DUT is the **third writer** of the oracle's `CpuBootTrace` format, so `cpu_boot_trace_diff` reads it with no modification and the rung needed no oracle-side change at all. **The oracle settled a question our own prose could not**: reset is EIGHT cycles, and `docs/cpu-6502.md` said both seven and eight -- corrected here. A mutation the test ROM was built to catch came back NOT CAUGHT because `TSX` leaves exactly the flags a wrongly-flagging `TXS` would compute, and a harness bug made every mutation report a catch including the baseline. The emulation core is untouched. It builds on **v2.4.3 "Touchstone"** — what the synthesiser accepts, and what the licence requires. A touchstone is a stone you rub gold against; the streak tells you what the metal actually is. This release settles the **two Fabric-plan risks that had to be answered before any RTL exists**, and both were answered by evidence that contradicted what the plan assumed. **Risk 4, the Quartus subset, is FITTED**: Quartus Prime Lite 17.0.2 Build 602 on a 5CSEBA6U23I7 produced a placed-and-routed netlist with **0 synthesis warnings**, and the 2 KiB array inferred as **2 M10K blocks with 29 total registers** — not 16,413 — from the source style alone, no `ramstyle` attribute. The `initial` block became a real MIF (so a boot ROM lands inside the block) and the `enum` was one-hot encoded. Nine constructs are promoted to *fitted*; plain `case`, `priority case` and `$bits` are deliberately left *documented* because the kitchen sink does not exercise them. **Risk 1, the `sys/` licence, inverts the plan's own hedge**: 57 files, **zero GPL-2.0-only**, and `hps_io.sv` — GPL-3.0-or-later and not optional, since it is how a core receives a ROM and reaches the OSD — forces the combined bitstream **up** to GPL-3.0-or-later, already RustyNES's licence. The emulation core is untouched.
+RustyNES's current release is **v2.6.5 "Muster"** — rung 5 closes — the AccuracyCoin status vector is identical entry for entry across all 146 entries, with 146 of 146 executed on both sides and none NotRun, where the same gate read 5 of 146 at the version's start. A muster is a roll call where every name is called AND answered, which is the two-clause acceptance exactly. Five PPU defects close the last six differing entries and four were invisible to every gate that existed when the version opened: the background shift registers' RELOAD and their shift clock need SEPARATE gates (with one shared gate the serial-in test was not merely failing but ARITHMETICALLY UNREACHABLE, since reload dots are absolute and the reload discards the low seven bits, so a serial-in one can never reach bit 7 on any alignment — and modelling both structures reproduces BOTH measured shifter values); the sprite X counters are NOT gated on rendering, which AccuracyCoin states outright and the ROM that states it passes either way, because it expects no hit at X=254 and a sprite shoved 18 dots right is also off the line; the PPUADDR second-write v-copy is DELAYED, as the wiki says inside the write sequence itself, swept 1 to 4 dots against a control at 8 and 12 that fails; and the pre-render line CLEARS secondary OAM, without which scanline 0 draws what scanline 239 left — no sprite can ever render on scanline 0, because OAM Y is one less than the display row, and a sprite-0 probe over the full 134 M-cycle battery found 24 hits with four of them there; and the octal latch holding across the read dot, which is verified by exactly ONE gate and was unverifiable until the v-copy delay landed, the two composing the hybrid address together and neither producing it alone. A DIAGNOSIS IS RETRACTED: the residual was read as a two-dot CPU/PPU alignment error from comparing dot spans across two instruments, and at the committed alignment the two consoles execute identical pc, bus_addr and bus_access for 1,695,131 cycles while a two-dot shift moves the first fork back to 593,228 and takes the differing share from 5.13% to 66.80%. The oracle changes on the default path, so AccuracyCoin 141/141 (RAM decoder) and nestest 0-diff are VERIFIED, not asserted. Built on **v2.6.4 "Rubric"** — OAM DMA lands and all nine AccuracyCoin disagreements close, every rule that closed the last three stated by the test ROM and by neither nesdev page — and then the gate that certified them is measured to cover 88 of 146 entries. The emulation core is unchanged. Built on **v2.6.3 "Mainspring"** — the DUT runs on one master clock, and four enables that were never enabling — plus AccuracyCoin end to end and a status vector that names its disagreements by test. The emulation core is unchanged. Built on **v2.6.2 "Witness"** — rung 4 closes: blargg APU battery 11/11 on the co-simulation DUT, six defects no self-written gate could see, and a suite that had been asserting nothing for five minor releases. The emulation core is unchanged. Built on **v2.6.1 "Interleave"** — the DMC and its DMA cycle steal in the MiSTer co-simulation DUT, cycle-exact on the bus. The emulation core is unchanged. Built on **v2.6.0 "Assay"** — the triangle, the noise channel and the sweep unit **in the MiSTer co-simulation DUT** — and an audit of how much of the APU was fitted to the oracle rather than derived from documentation. The emulation core is unchanged. Built on **v2.5.9 "Overture"** — rung 4 opens: the two pulse channels, the frame counter, and four ROM defects the stimulus measurement found first. Built on **v2.5.8 "Blanking"** — VBlank, NMI and the PPUSTATUS race close rung 3 — and both fixes were deletions. Built on **v2.5.7 "Collimation"** — sprite rendering closes exact — the phase was wrong by two dots, and every window was compensating. Built on **v2.5.6 "Vestige"** — Sprite evaluation closes: all 59,993 overlapping cycles match, nine of nine behavioural mutants caught and two proved inert (announced as seven of eight at the cut), and the fix is a byte index that outlives the walk that set it. Built on **v2.5.5 "Raster"** — the first full frame, and three blind spots in the stimulus that fed it. Built on **v2.5.4 "Escapement"** — the background fetch pipeline, and an access two dots early that five gates could not see. Built on **v2.5.3 "Hysteresis"** — toggling rendering takes effect three dots after the write, and four instruments to prove it. Built on **v2.5.2 "Dormant"** — the 2C02 register file, and a gate that passed while testing nothing. Built on **v2.5.1 "Retrace"** — the interrupt sweep closes rung 2, and a gate reported a pass it could not have earned. Built on **v2.5.0 "Rungwork"** — the 6502 rung, and the two gates it cannot reach. Built on **v2.4.9 "Plumbline II"** — the bus half of rung 2, and what it found the day it existed. Built on **v2.4.8 "Palimpsest"** — read-modify-write, and a gate that cannot see its own subject. Built on **v2.4.7 "Keystone"** — the stack closes, and a dead line proves itself dead. Built on **v2.4.6 "Abacus"** — the core learns arithmetic. Built on **v2.4.5 "Compass"** — the core reaches memory, and chooses. Built on **v2.4.4 "Ignition"** — the first real RTL. The 6502's eight-cycle reset and the seventeen single-byte implied opcodes, in SystemVerilog in the sibling repository (`RustyNES_MiSTer@7f092bd`), matching the oracle on all seven CPU fields -- 29 records, and the gate demonstrated to fail on four mutations. The DUT is the **third writer** of the oracle's `CpuBootTrace` format, so `cpu_boot_trace_diff` reads it with no modification and the rung needed no oracle-side change at all. **The oracle settled a question our own prose could not**: reset is EIGHT cycles, and `docs/cpu-6502.md` said both seven and eight -- corrected here. A mutation the test ROM was built to catch came back NOT CAUGHT because `TSX` leaves exactly the flags a wrongly-flagging `TXS` would compute, and a harness bug made every mutation report a catch including the baseline. The emulation core is untouched. It builds on **v2.4.3 "Touchstone"** — what the synthesiser accepts, and what the licence requires. A touchstone is a stone you rub gold against; the streak tells you what the metal actually is. This release settles the **two Fabric-plan risks that had to be answered before any RTL exists**, and both were answered by evidence that contradicted what the plan assumed. **Risk 4, the Quartus subset, is FITTED**: Quartus Prime Lite 17.0.2 Build 602 on a 5CSEBA6U23I7 produced a placed-and-routed netlist with **0 synthesis warnings**, and the 2 KiB array inferred as **2 M10K blocks with 29 total registers** — not 16,413 — from the source style alone, no `ramstyle` attribute. The `initial` block became a real MIF (so a boot ROM lands inside the block) and the `enum` was one-hot encoded. Nine constructs are promoted to *fitted*; plain `case`, `priority case` and `$bits` are deliberately left *documented* because the kitchen sink does not exercise them. **Risk 1, the `sys/` licence, inverts the plan's own hedge**: 57 files, **zero GPL-2.0-only**, and `hps_io.sv` — GPL-3.0-or-later and not optional, since it is how a core receives a ROM and reaches the OSD — forces the combined bitstream **up** to GPL-3.0-or-later, already RustyNES's licence. The emulation core is untouched.
It builds on **v2.4.2 "Cairn"** — the **rung-0 compare surface**. A cairn is a marker set along a route so you can tell you are still on it, which is what a rolling per-cycle hash checkpoint is. The constraint nobody budgets for in co-simulation is trace *volume*, not simulation time, and it is now **measured**: 3 frames of AccuracyCoin is 89,343 CPU cycles, **5,372,427 bytes** of `irq.csv` against **352 bytes** of `ckpt.bin` — a factor of **15,263** — so both sides chain a hash and compare every 4096 cycles, and only the divergent window is re-run with full capture. **What is hashed is a decision about hardware, not about convenience**: `CycleRecord` carries 29 fields and most are RustyNES's *model*, so `Observable` is the subset a device can genuinely produce, the IRQ pair is OR'd before hashing because hardware has one wire-OR'd /IRQ pin, and `pc` is marked DUT-observable rather than pin-observable. The emulation core is untouched.
diff --git a/ROADMAP.md b/ROADMAP.md
index bbd9c1b1..bffb630a 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -2,13 +2,13 @@
**Document Version:** 2.0.4
**Last Updated:** 2026-08-24
-**Project Status:** v2.6.4 "Rubric" released — the current head of the line, on **v2.6.3 "Mainspring"** and **v2.6.2 "Witness"** and **v2.6.1 "Interleave"** and **v2.6.0 "Assay"** and **v2.5.9 "Overture"** and **v2.5.8 "Blanking"** and **v2.5.7 "Collimation"** and **v2.5.6 "Vestige"** and **v2.5.5 "Raster"** and **v2.5.4 "Escapement"** and **v2.5.3 "Hysteresis"** and **v2.5.2 "Dormant"** and **v2.5.1 "Retrace"** and **v2.5.0 "Rungwork"** and **v2.4.9 "Plumbline II"** and **v2.4.8 "Palimpsest"** and **v2.4.7 "Keystone"** and **v2.4.6 "Abacus"** and **v2.4.5 "Compass"** and **v2.4.4 "Ignition"** and v2.4.3 "Touchstone" and v2.4.2 "Cairn" and v2.4.1 "Fabric", on the v2.0.0 "Timebase" MAJOR cut. **This file is a historical snapshot of the v1.0.0 cut**; see [`to-dos/ROADMAP.md`](to-dos/ROADMAP.md) for the authoritative forward roadmap and [`docs/STATUS.md`](docs/STATUS.md) for current state.
+**Project Status:** v2.6.5 "Muster" released — the current head of the line, on **v2.6.4 "Rubric"** and **v2.6.3 "Mainspring"** and **v2.6.2 "Witness"** and **v2.6.1 "Interleave"** and **v2.6.0 "Assay"** and **v2.5.9 "Overture"** and **v2.5.8 "Blanking"** and **v2.5.7 "Collimation"** and **v2.5.6 "Vestige"** and **v2.5.5 "Raster"** and **v2.5.4 "Escapement"** and **v2.5.3 "Hysteresis"** and **v2.5.2 "Dormant"** and **v2.5.1 "Retrace"** and **v2.5.0 "Rungwork"** and **v2.4.9 "Plumbline II"** and **v2.4.8 "Palimpsest"** and **v2.4.7 "Keystone"** and **v2.4.6 "Abacus"** and **v2.4.5 "Compass"** and **v2.4.4 "Ignition"** and v2.4.3 "Touchstone" and v2.4.2 "Cairn" and v2.4.1 "Fabric", on the v2.0.0 "Timebase" MAJOR cut. **This file is a historical snapshot of the v1.0.0 cut**; see [`to-dos/ROADMAP.md`](to-dos/ROADMAP.md) for the authoritative forward roadmap and [`docs/STATUS.md`](docs/STATUS.md) for current state.
---
## Where we are
-RustyNES is well past v1.0.0. The current release is **v2.6.4 "Rubric"** (2026-08-26) — OAM DMA lands and all nine AccuracyCoin disagreements close, every rule that closed the last three stated by the test ROM and by neither nesdev page — and then the gate that certified them is measured to cover 88 of 146 entries. The emulation core is unchanged. Built on **v2.6.3 "Mainspring"** (2026-08-25) — the DUT runs on one master clock, and four enables that were never enabling — plus AccuracyCoin end to end and a status vector that names its disagreements by test. The emulation core is unchanged. Built on **v2.6.2 "Witness"** (2026-08-24) — rung 4 closes: blargg APU battery 11/11 on the co-simulation DUT, six defects no self-written gate could see, and a suite that had been asserting nothing for five minor releases. The emulation core is unchanged. Built on **v2.6.1 "Interleave"** (2026-08-24) — the DMC and its DMA cycle steal in the MiSTer co-simulation DUT, cycle-exact on the bus. The emulation core is unchanged. Built on **v2.6.0 "Assay"** (2026-08-24) — the triangle, the noise channel and the sweep unit **in the MiSTer co-simulation DUT** — and an audit of how much of the APU was fitted to the oracle rather than derived from documentation. The emulation core is unchanged. Built on **v2.5.9 "Overture"** (2026-08-24) — rung 4 opens: the two pulse channels, the frame counter, and four ROM defects the stimulus measurement found first. Built on **v2.5.8 "Blanking"** (2026-08-24) — VBlank, NMI and the PPUSTATUS race close rung 3 — and both fixes were deletions. Built on **v2.5.7 "Collimation"** (2026-08-24) — sprite rendering closes exact — the phase was wrong by two dots, and every window was compensating. Built on **v2.5.6 "Vestige"** (2026-08-23) — Sprite evaluation closes: all 59,993 overlapping cycles match, nine of nine behavioural mutants caught and two proved inert (announced as seven of eight at the cut), and the fix is a byte index that outlives the walk that set it. Built on **v2.5.5 "Raster"** (2026-08-23) — the first full frame, and three blind spots in the stimulus that fed it. Built on **v2.5.4 "Escapement"** (2026-08-23) — the background fetch pipeline, and an access two dots early that five gates could not see. Built on **v2.5.3 "Hysteresis"** (2026-08-23) — toggling rendering takes effect three dots after the write, and four instruments to prove it. Built on **v2.5.2 "Dormant"** (2026-08-23) — the 2C02 register file, and a gate that passed while testing nothing. Built on **v2.5.1 "Retrace"** (2026-08-23) — the interrupt sweep closes rung 2, and a gate reported a pass it could not have earned. Built on **v2.5.0 "Rungwork"** (2026-08-23) — the 6502 rung, and the two gates it cannot reach. Built on **v2.4.9 "Plumbline II"** (2026-08-23) — the bus half of rung 2, and what it found the day it existed. Built on **v2.4.8 "Palimpsest"** (2026-08-23) — read-modify-write, and a gate that cannot see its own subject. Built on **v2.4.7 "Keystone"** (2026-08-23) — the stack closes, and a dead line proves itself dead. Built on **v2.4.6 "Abacus"** (2026-08-22) — the core learns arithmetic. Built on **v2.4.5 "Compass"** (2026-08-22) — the core reaches memory, and chooses. Built on **v2.4.4 "Ignition"** (2026-08-22) — the first real RTL of the co-simulation programme, on **v2.4.3 "Touchstone"** (2026-08-22), the two Fabric risks settled before any RTL, on **v2.4.2 "Cairn"** (2026-08-22), the rung-0 compare surface, on **v2.4.1 "Fabric"** (2026-08-20), the oracle release opening the v2.4.1 → v2.5.0 "Fabric" line, and carrying the never-tagged v2.4.0 "Concordance", sitting atop **v2.0.0 "Timebase"** (2026-07-03), the designated MAJOR cut that replaced the PPU-dot lockstep scheduler with the one-clock / every-cycle-bus-access model. Since then the **v2.0.x "Harbor"** mobile-finalization train, the **v2.1.x "Fathom"** accuracy line, the **v2.2.0 "Capstone"** milestone, the **v2.2.6 → v2.3.0** de-monetization + NESdev-remediation line, and the **v2.3.1 → v2.3.9** measurement / tooling / gates line have all shipped. Between v1.0.0 and v2.0.0 the additive v1.x line delivered deep desktop tooling and three whole new platforms (native Android, iOS / iPadOS, and a Libretro / RetroArch core); the v2.0.x train then re-ported mobile onto the new core and, at **v2.0.3**, promoted the 2-cycle-ALE PPU fetch model to the default to reach **AccuracyCoin 100% (141/141)**.
+RustyNES is well past v1.0.0. The current release is **v2.6.5 "Muster"** (2026-08-29) — rung 5 closes — the AccuracyCoin status vector is identical entry for entry across all 146 entries, with 146 of 146 executed on both sides and none NotRun, where the same gate read 5 of 146 at the version's start. A muster is a roll call where every name is called AND answered, which is the two-clause acceptance exactly. Five PPU defects close the last six differing entries and four were invisible to every gate that existed when the version opened: the background shift registers' RELOAD and their shift clock need SEPARATE gates (with one shared gate the serial-in test was not merely failing but ARITHMETICALLY UNREACHABLE, since reload dots are absolute and the reload discards the low seven bits, so a serial-in one can never reach bit 7 on any alignment — and modelling both structures reproduces BOTH measured shifter values); the sprite X counters are NOT gated on rendering, which AccuracyCoin states outright and the ROM that states it passes either way, because it expects no hit at X=254 and a sprite shoved 18 dots right is also off the line; the PPUADDR second-write v-copy is DELAYED, as the wiki says inside the write sequence itself, swept 1 to 4 dots against a control at 8 and 12 that fails; and the pre-render line CLEARS secondary OAM, without which scanline 0 draws what scanline 239 left — no sprite can ever render on scanline 0, because OAM Y is one less than the display row, and a sprite-0 probe over the full 134 M-cycle battery found 24 hits with four of them there; and the octal latch holding across the read dot, which is verified by exactly ONE gate and was unverifiable until the v-copy delay landed, the two composing the hybrid address together and neither producing it alone. A DIAGNOSIS IS RETRACTED: the residual was read as a two-dot CPU/PPU alignment error from comparing dot spans across two instruments, and at the committed alignment the two consoles execute identical pc, bus_addr and bus_access for 1,695,131 cycles while a two-dot shift moves the first fork back to 593,228 and takes the differing share from 5.13% to 66.80%. The oracle changes on the default path, so AccuracyCoin 141/141 (RAM decoder) and nestest 0-diff are VERIFIED, not asserted. Built on **v2.6.4 "Rubric"** (2026-08-26) — OAM DMA lands and all nine AccuracyCoin disagreements close, every rule that closed the last three stated by the test ROM and by neither nesdev page — and then the gate that certified them is measured to cover 88 of 146 entries. The emulation core is unchanged. Built on **v2.6.3 "Mainspring"** (2026-08-25) — the DUT runs on one master clock, and four enables that were never enabling — plus AccuracyCoin end to end and a status vector that names its disagreements by test. The emulation core is unchanged. Built on **v2.6.2 "Witness"** (2026-08-24) — rung 4 closes: blargg APU battery 11/11 on the co-simulation DUT, six defects no self-written gate could see, and a suite that had been asserting nothing for five minor releases. The emulation core is unchanged. Built on **v2.6.1 "Interleave"** (2026-08-24) — the DMC and its DMA cycle steal in the MiSTer co-simulation DUT, cycle-exact on the bus. The emulation core is unchanged. Built on **v2.6.0 "Assay"** (2026-08-24) — the triangle, the noise channel and the sweep unit **in the MiSTer co-simulation DUT** — and an audit of how much of the APU was fitted to the oracle rather than derived from documentation. The emulation core is unchanged. Built on **v2.5.9 "Overture"** (2026-08-24) — rung 4 opens: the two pulse channels, the frame counter, and four ROM defects the stimulus measurement found first. Built on **v2.5.8 "Blanking"** (2026-08-24) — VBlank, NMI and the PPUSTATUS race close rung 3 — and both fixes were deletions. Built on **v2.5.7 "Collimation"** (2026-08-24) — sprite rendering closes exact — the phase was wrong by two dots, and every window was compensating. Built on **v2.5.6 "Vestige"** (2026-08-23) — Sprite evaluation closes: all 59,993 overlapping cycles match, nine of nine behavioural mutants caught and two proved inert (announced as seven of eight at the cut), and the fix is a byte index that outlives the walk that set it. Built on **v2.5.5 "Raster"** (2026-08-23) — the first full frame, and three blind spots in the stimulus that fed it. Built on **v2.5.4 "Escapement"** (2026-08-23) — the background fetch pipeline, and an access two dots early that five gates could not see. Built on **v2.5.3 "Hysteresis"** (2026-08-23) — toggling rendering takes effect three dots after the write, and four instruments to prove it. Built on **v2.5.2 "Dormant"** (2026-08-23) — the 2C02 register file, and a gate that passed while testing nothing. Built on **v2.5.1 "Retrace"** (2026-08-23) — the interrupt sweep closes rung 2, and a gate reported a pass it could not have earned. Built on **v2.5.0 "Rungwork"** (2026-08-23) — the 6502 rung, and the two gates it cannot reach. Built on **v2.4.9 "Plumbline II"** (2026-08-23) — the bus half of rung 2, and what it found the day it existed. Built on **v2.4.8 "Palimpsest"** (2026-08-23) — read-modify-write, and a gate that cannot see its own subject. Built on **v2.4.7 "Keystone"** (2026-08-23) — the stack closes, and a dead line proves itself dead. Built on **v2.4.6 "Abacus"** (2026-08-22) — the core learns arithmetic. Built on **v2.4.5 "Compass"** (2026-08-22) — the core reaches memory, and chooses. Built on **v2.4.4 "Ignition"** (2026-08-22) — the first real RTL of the co-simulation programme, on **v2.4.3 "Touchstone"** (2026-08-22), the two Fabric risks settled before any RTL, on **v2.4.2 "Cairn"** (2026-08-22), the rung-0 compare surface, on **v2.4.1 "Fabric"** (2026-08-20), the oracle release opening the v2.4.1 → v2.5.0 "Fabric" line, and carrying the never-tagged v2.4.0 "Concordance", sitting atop **v2.0.0 "Timebase"** (2026-07-03), the designated MAJOR cut that replaced the PPU-dot lockstep scheduler with the one-clock / every-cycle-bus-access model. Since then the **v2.0.x "Harbor"** mobile-finalization train, the **v2.1.x "Fathom"** accuracy line, the **v2.2.0 "Capstone"** milestone, the **v2.2.6 → v2.3.0** de-monetization + NESdev-remediation line, and the **v2.3.1 → v2.3.9** measurement / tooling / gates line have all shipped. Between v1.0.0 and v2.0.0 the additive v1.x line delivered deep desktop tooling and three whole new platforms (native Android, iOS / iPadOS, and a Libretro / RetroArch core); the v2.0.x train then re-ported mobile onto the new core and, at **v2.0.3**, promoted the 2-cycle-ALE PPU fetch model to the default to reach **AccuracyCoin 100% (141/141)**.
**This root ROADMAP is a historical snapshot of the v1.0.0 cut.** For the authoritative, current forward roadmap see **[`to-dos/ROADMAP.md`](to-dos/ROADMAP.md)**; for the authoritative current-state pass counts and platform matrix see **[`docs/STATUS.md`](docs/STATUS.md)**; for the full per-release history see **[`CHANGELOG.md`](CHANGELOG.md)**. Many of the "post-1.0 directions" listed further down (mobile, Lua scripting, TAS editor, Vs. DualSystem, HD packs, hosted netplay) have since shipped — the tables below record what was **done at v1.0.0**, not the current feature set.
diff --git a/SECURITY.md b/SECURITY.md
index 61e4259f..e2478b98 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -2,7 +2,7 @@
## Supported Versions
-The current release is **v2.6.4 "Rubric"**, on **v2.6.3 "Mainspring"** and **v2.6.2 "Witness"** and **v2.6.1 "Interleave"** and **v2.6.0 "Assay"** and **v2.5.9 "Overture"** and **v2.5.8 "Blanking"** and **v2.5.7 "Collimation"** and **v2.5.6 "Vestige"** and **v2.5.5 "Raster"** and **v2.5.4 "Escapement"** and **v2.5.3 "Hysteresis"** and **v2.5.2 "Dormant"** and **v2.5.1 "Retrace"** and **v2.5.0 "Rungwork"** and **v2.4.9 "Plumbline II"** and **v2.4.8 "Palimpsest"** and **v2.4.7 "Keystone"** and **v2.4.6 "Abacus"** and **v2.4.5 "Compass"** and **v2.4.4 "Ignition"** and **v2.4.3 "Touchstone"** and **v2.4.2 "Cairn"** and **v2.4.1 "Fabric"**, which also carries the never-tagged v2.4.0 "Concordance". RustyNES ships from `main` on a
+The current release is **v2.6.5 "Muster"**, on **v2.6.4 "Rubric"** and **v2.6.3 "Mainspring"** and **v2.6.2 "Witness"** and **v2.6.1 "Interleave"** and **v2.6.0 "Assay"** and **v2.5.9 "Overture"** and **v2.5.8 "Blanking"** and **v2.5.7 "Collimation"** and **v2.5.6 "Vestige"** and **v2.5.5 "Raster"** and **v2.5.4 "Escapement"** and **v2.5.3 "Hysteresis"** and **v2.5.2 "Dormant"** and **v2.5.1 "Retrace"** and **v2.5.0 "Rungwork"** and **v2.4.9 "Plumbline II"** and **v2.4.8 "Palimpsest"** and **v2.4.7 "Keystone"** and **v2.4.6 "Abacus"** and **v2.4.5 "Compass"** and **v2.4.4 "Ignition"** and **v2.4.3 "Touchstone"** and **v2.4.2 "Cairn"** and **v2.4.1 "Fabric"**, which also carries the never-tagged v2.4.0 "Concordance". RustyNES ships from `main` on a
rolling patch cadence rather than maintaining long-lived release branches, so
security fixes land in the next patch release rather than being backported.
Report against the latest release or `main`.
diff --git a/SUPPORT.md b/SUPPORT.md
index 6d39b83e..63b02d48 100644
--- a/SUPPORT.md
+++ b/SUPPORT.md
@@ -94,7 +94,7 @@ A: RustyNES is a cycle-accurate NES emulator written in pure Rust, clearing the
**Q: Can I use RustyNES now?**
-A: Yes. RustyNES is well past its first stable release — the current release is **v2.6.4 "Rubric"** (OAM DMA lands and all nine AccuracyCoin disagreements close, every rule that closed the last three stated by the test ROM and by neither nesdev page — and then the gate that certified them is measured to cover 88 of 146 entries. The emulation core is unchanged, on **v2.6.3 "Mainspring"** — the DUT runs on one master clock, and four enables that were never enabling — plus AccuracyCoin end to end and a status vector that names its disagreements by test. The emulation core is unchanged, on **v2.6.2 "Witness"** — rung 4 closes: blargg APU battery 11/11 on the co-simulation DUT, six defects no self-written gate could see, and a suite that had been asserting nothing for five minor releases. The emulation core is unchanged, on **v2.6.1 "Interleave"** — the DMC and its DMA cycle steal in the MiSTer co-simulation DUT, cycle-exact on the bus. The emulation core is unchanged, on **v2.6.0 "Assay"** — the triangle, the noise channel and the sweep unit **in the MiSTer co-simulation DUT** — and an audit of how much of the APU was fitted to the oracle rather than derived from documentation. The emulation core is unchanged, on **v2.5.9 "Overture"** — rung 4 opens: the two pulse channels, the frame counter, and four ROM defects the stimulus measurement found first, on **v2.5.8 "Blanking"** — VBlank, NMI and the PPUSTATUS race close rung 3 — and both fixes were deletions, on **v2.5.7 "Collimation"** — sprite rendering closes exact — the phase was wrong by two dots, and every window was compensating, on **v2.5.6 "Vestige"** — Sprite evaluation closes: all 59,993 overlapping cycles match, nine of nine behavioural mutants caught and two proved inert (announced as seven of eight at the cut), and the fix is a byte index that outlives the walk that set it, on **v2.5.5 "Raster"** — the first full frame, and three blind spots in the stimulus that fed it, on **v2.5.4 "Escapement"** — the background fetch pipeline, and an access two dots early that five gates could not see, on **v2.5.3 "Hysteresis"** — toggling rendering takes effect three dots after the write, and four instruments to prove it, on **v2.5.2 "Dormant"** — the 2C02 register file, and a gate that passed while testing nothing, on **v2.5.1 "Retrace"** — the interrupt sweep closes rung 2, and a gate reported a pass it could not have earned, on **v2.5.0 "Rungwork"** — the 6502 rung, and the two gates it cannot reach, on **v2.4.9 "Plumbline II"** — the bus half of rung 2, and what it found the day it existed, on **v2.4.8 "Palimpsest"** — read-modify-write, and a gate that cannot see its own subject, on **v2.4.7 "Keystone"** — the stack closes, and a dead line proves itself dead, on **v2.4.6 "Abacus"** — the core learns arithmetic, on **v2.4.5 "Compass"** — the core reaches memory, and chooses, on **v2.4.4 "Ignition"** — the first real RTL of the co-simulation programme, on v2.4.3 "Touchstone", the two Fabric risks settled before any RTL, on v2.4.2 "Cairn", the rung-0 compare surface of the v2.4.1 → v2.5.0 "Fabric" line, on v2.4.1 "Fabric" and the never-tagged v2.4.0 "Concordance", atop the v2.0.0 "Timebase" one-clock scheduler base), a complete, playable desktop application plus native Android / iOS / Libretro builds and a browser build. See [`to-dos/ROADMAP.md`](to-dos/ROADMAP.md) for what shipped and the forward directions.
+A: Yes. RustyNES is well past its first stable release — the current release is **v2.6.5 "Muster"** (rung 5 closes — the AccuracyCoin status vector is identical entry for entry across all 146 entries, with 146 of 146 executed on both sides and none NotRun, where the same gate read 5 of 146 at the version's start. A muster is a roll call where every name is called AND answered, which is the two-clause acceptance exactly. Five PPU defects close the last six differing entries and four were invisible to every gate that existed when the version opened: the background shift registers' RELOAD and their shift clock need SEPARATE gates (with one shared gate the serial-in test was not merely failing but ARITHMETICALLY UNREACHABLE, since reload dots are absolute and the reload discards the low seven bits, so a serial-in one can never reach bit 7 on any alignment — and modelling both structures reproduces BOTH measured shifter values); the sprite X counters are NOT gated on rendering, which AccuracyCoin states outright and the ROM that states it passes either way, because it expects no hit at X=254 and a sprite shoved 18 dots right is also off the line; the PPUADDR second-write v-copy is DELAYED, as the wiki says inside the write sequence itself, swept 1 to 4 dots against a control at 8 and 12 that fails; and the pre-render line CLEARS secondary OAM, without which scanline 0 draws what scanline 239 left — no sprite can ever render on scanline 0, because OAM Y is one less than the display row, and a sprite-0 probe over the full 134 M-cycle battery found 24 hits with four of them there; and the octal latch holding across the read dot, which is verified by exactly ONE gate and was unverifiable until the v-copy delay landed, the two composing the hybrid address together and neither producing it alone. A DIAGNOSIS IS RETRACTED: the residual was read as a two-dot CPU/PPU alignment error from comparing dot spans across two instruments, and at the committed alignment the two consoles execute identical pc, bus_addr and bus_access for 1,695,131 cycles while a two-dot shift moves the first fork back to 593,228 and takes the differing share from 5.13% to 66.80%. The oracle changes on the default path, so AccuracyCoin 141/141 (RAM decoder) and nestest 0-diff are VERIFIED, not asserted, on **v2.6.4 "Rubric"** — OAM DMA lands and all nine AccuracyCoin disagreements close, every rule that closed the last three stated by the test ROM and by neither nesdev page — and then the gate that certified them is measured to cover 88 of 146 entries. The emulation core is unchanged, on **v2.6.3 "Mainspring"** — the DUT runs on one master clock, and four enables that were never enabling — plus AccuracyCoin end to end and a status vector that names its disagreements by test. The emulation core is unchanged, on **v2.6.2 "Witness"** — rung 4 closes: blargg APU battery 11/11 on the co-simulation DUT, six defects no self-written gate could see, and a suite that had been asserting nothing for five minor releases. The emulation core is unchanged, on **v2.6.1 "Interleave"** — the DMC and its DMA cycle steal in the MiSTer co-simulation DUT, cycle-exact on the bus. The emulation core is unchanged, on **v2.6.0 "Assay"** — the triangle, the noise channel and the sweep unit **in the MiSTer co-simulation DUT** — and an audit of how much of the APU was fitted to the oracle rather than derived from documentation. The emulation core is unchanged, on **v2.5.9 "Overture"** — rung 4 opens: the two pulse channels, the frame counter, and four ROM defects the stimulus measurement found first, on **v2.5.8 "Blanking"** — VBlank, NMI and the PPUSTATUS race close rung 3 — and both fixes were deletions, on **v2.5.7 "Collimation"** — sprite rendering closes exact — the phase was wrong by two dots, and every window was compensating, on **v2.5.6 "Vestige"** — Sprite evaluation closes: all 59,993 overlapping cycles match, nine of nine behavioural mutants caught and two proved inert (announced as seven of eight at the cut), and the fix is a byte index that outlives the walk that set it, on **v2.5.5 "Raster"** — the first full frame, and three blind spots in the stimulus that fed it, on **v2.5.4 "Escapement"** — the background fetch pipeline, and an access two dots early that five gates could not see, on **v2.5.3 "Hysteresis"** — toggling rendering takes effect three dots after the write, and four instruments to prove it, on **v2.5.2 "Dormant"** — the 2C02 register file, and a gate that passed while testing nothing, on **v2.5.1 "Retrace"** — the interrupt sweep closes rung 2, and a gate reported a pass it could not have earned, on **v2.5.0 "Rungwork"** — the 6502 rung, and the two gates it cannot reach, on **v2.4.9 "Plumbline II"** — the bus half of rung 2, and what it found the day it existed, on **v2.4.8 "Palimpsest"** — read-modify-write, and a gate that cannot see its own subject, on **v2.4.7 "Keystone"** — the stack closes, and a dead line proves itself dead, on **v2.4.6 "Abacus"** — the core learns arithmetic, on **v2.4.5 "Compass"** — the core reaches memory, and chooses, on **v2.4.4 "Ignition"** — the first real RTL of the co-simulation programme, on v2.4.3 "Touchstone", the two Fabric risks settled before any RTL, on v2.4.2 "Cairn", the rung-0 compare surface of the v2.4.1 → v2.5.0 "Fabric" line, on v2.4.1 "Fabric" and the never-tagged v2.4.0 "Concordance", atop the v2.0.0 "Timebase" one-clock scheduler base), a complete, playable desktop application plus native Android / iOS / Libretro builds and a browser build. See [`to-dos/ROADMAP.md`](to-dos/ROADMAP.md) for what shipped and the forward directions.
**Q: How accurate is RustyNES?**
diff --git a/VERSION-PLAN.md b/VERSION-PLAN.md
index d806c832..79c7e25a 100644
--- a/VERSION-PLAN.md
+++ b/VERSION-PLAN.md
@@ -1,6 +1,6 @@
# RustyNES Version Plan
-**Current release: v2.6.4 "Rubric"** — OAM DMA lands and all nine AccuracyCoin disagreements close, every rule that closed the last three stated by the test ROM and by neither nesdev page — and then the gate that certified them is measured to cover 88 of 146 entries. The emulation core is unchanged. Built on **v2.6.3 "Mainspring"** — the DUT runs on one master clock, and four enables that were never enabling — plus AccuracyCoin end to end and a status vector that names its disagreements by test. The emulation core is unchanged. Built on **v2.6.2 "Witness"** — rung 4 closes: blargg APU battery 11/11 on the co-simulation DUT, six defects no self-written gate could see, and a suite that had been asserting nothing for five minor releases. The emulation core is unchanged. Built on **v2.6.1 "Interleave"** — the DMC and its DMA cycle steal in the MiSTer co-simulation DUT, cycle-exact on the bus. The emulation core is unchanged. Built on **v2.6.0 "Assay"** — the triangle, the noise channel and the sweep unit **in the MiSTer co-simulation DUT** — and an audit of how much of the APU was fitted to the oracle rather than derived from documentation. The emulation core is unchanged. Built on **v2.5.9 "Overture"** — rung 4 opens: the two pulse channels, the frame counter, and four ROM defects the stimulus measurement found first. Built on **v2.5.8 "Blanking"** — VBlank, NMI and the PPUSTATUS race close rung 3 — and both fixes were deletions. Built on **v2.5.7 "Collimation"** — sprite rendering closes exact — the phase was wrong by two dots, and every window was compensating. Built on **v2.5.6 "Vestige"** — Sprite evaluation closes: all 59,993 overlapping cycles match, nine of nine behavioural mutants caught and two proved inert (announced as seven of eight at the cut), and the fix is a byte index that outlives the walk that set it. Built on **v2.5.5 "Raster"** — the first full frame, and three blind spots in the stimulus that fed it. Built on **v2.5.4 "Escapement"** — the background fetch pipeline, and an access two dots early that five gates could not see. Built on **v2.5.3 "Hysteresis"** — toggling rendering takes effect three dots after the write, and four instruments to prove it. Built on **v2.5.2 "Dormant"** — the 2C02 register file, and a gate that passed while testing nothing. Built on **v2.5.1 "Retrace"** — the interrupt sweep closes rung 2, and a gate reported a pass it could not have earned. Built on **v2.5.0 "Rungwork"** — the 6502 rung, and the two gates it cannot reach. Built on **v2.4.9 "Plumbline II"** — the bus half of rung 2, and what it found the day it existed. Built on **v2.4.8 "Palimpsest"** — read-modify-write, and a gate that cannot see its own subject. Built on **v2.4.7 "Keystone"** — the stack closes, and a dead line proves itself dead. Built on **v2.4.6 "Abacus"** — the core learns arithmetic. Built on **v2.4.5 "Compass"** — the core reaches memory, and chooses. Built on **v2.4.4 "Ignition"** — the first real RTL. The 6502's eight-cycle reset and the seventeen single-byte implied opcodes, in SystemVerilog in the sibling repository (`RustyNES_MiSTer@7f092bd`), matching the oracle on all seven CPU fields -- 29 records, and the gate demonstrated to fail on four mutations. The DUT is the **third writer** of the oracle's `CpuBootTrace` format, so `cpu_boot_trace_diff` reads it with no modification and the rung needed no oracle-side change at all. **The oracle settled a question our own prose could not**: reset is EIGHT cycles, and `docs/cpu-6502.md` said both seven and eight -- corrected here. A mutation the test ROM was built to catch came back NOT CAUGHT because `TSX` leaves exactly the flags a wrongly-flagging `TXS` would compute, and a harness bug made every mutation report a catch including the baseline. The emulation core is untouched. Built on **v2.4.3 "Touchstone"** — what the synthesiser accepts, and what the licence requires. A touchstone is a stone you rub gold against; the streak tells you what the metal actually is. This release settles the **two Fabric-plan risks that had to be answered before any RTL exists**, and both were answered by evidence that contradicted what the plan assumed. **Risk 4, the Quartus subset, is FITTED**: Quartus Prime Lite 17.0.2 Build 602 on a 5CSEBA6U23I7 produced a placed-and-routed netlist with **0 synthesis warnings**, and the 2 KiB array inferred as **2 M10K blocks with 29 total registers** — not 16,413 — from the source style alone, no `ramstyle` attribute. The `initial` block became a real MIF (so a boot ROM lands inside the block) and the `enum` was one-hot encoded. Nine constructs are promoted to *fitted*; plain `case`, `priority case` and `$bits` are deliberately left *documented* because the kitchen sink does not exercise them. **Risk 1, the `sys/` licence, inverts the plan's own hedge**: 57 files, **zero GPL-2.0-only**, and `hps_io.sv` — GPL-3.0-or-later and not optional, since it is how a core receives a ROM and reaches the OSD — forces the combined bitstream **up** to GPL-3.0-or-later, already RustyNES's licence. The emulation core is untouched. Built on **v2.4.2 "Cairn"** — the **rung-0 compare surface**. A cairn is a marker set along a route so you can tell you are still on it, which is what a rolling per-cycle hash checkpoint is. The constraint nobody budgets for in co-simulation is trace *volume*, not simulation time, and it is now **measured**: 3 frames of AccuracyCoin is 89,343 CPU cycles, **5,372,427 bytes** of `irq.csv` against **352 bytes** of `ckpt.bin` — a factor of **15,263** — so both sides chain a hash and compare every 4096 cycles, and only the divergent window is re-run with full capture. **What is hashed is a decision about hardware, not about convenience**: `CycleRecord` carries 29 fields and most are RustyNES's *model*, so `Observable` is the subset a device can genuinely produce, the IRQ pair is OR'd before hashing because hardware has one wire-OR'd /IRQ pin, and `pc` is marked DUT-observable rather than pin-observable. The emulation core is untouched. Built on **v2.4.1 "Fabric"** — RustyNES as the oracle a new implementation is verified against. It opens the **v2.4.1 → v2.5.0 "Fabric"** line: a new NES core in SystemVerilog, written from public hardware documentation in a sibling repository, with this emulator as its verification oracle. RustyNES is not being ported to FPGA and cannot be; `crates/rustynes-cosim` is the boundary, and the firewall extends to HDL (ADR 0037). **v2.5.0 is scoped to "the 6502 rung closes"**, not a finished core. Excluding the crate from the workspace is the load-bearing detail — cargo unifies features, and `irq-timing-trace` selects a *different* per-dot loop in `Bus::tick_one_cpu_cycle`, so the accuracy battery had been validating a scheduler no user runs. It also carries **v2.4.0 "Concordance"**, which merged to `main` and was never tagged: atomic durable writes on every path that persists user data, `Nes::timeline_generation()`, and the 15-anchor release audit. AccuracyCoin **141/141** and nestest 0-diff verified, not asserted. Previously, **v2.3.9 "Crucible"** — what the gates actually cover. A crucible tests to destruction rather than inspects, and this release does that to the project's own checks. **The docs-only CI skip had never worked**: `predicate-quantifier` defaults to `some`, so the `code` filter's leading `'**'` matched everything and all seven `!` exclusions under it were dead from the day they were written — fixed with **two** filter steps, because the quantifier is step-level and `accuracy` is a list of *alternatives* that becomes unsatisfiable under `every`, so the one-line fix would have silently disabled the accuracy battery while repairing a different gate. **`test-roms` now runs at review time**, path-filtered over the chip crates, the core, `rustynes-gamedb`, the harness and `tests/` (11 of the last 40 merged PRs, so ~72% still pay nothing). **A freeze from one cartridge kept writing into the next** — an active per-frame write into the wrong game — closed by a ROM-transition sweep across every panel under one rule: derived output discarded, user-authored input kept, and only input that actively *writes* neutralised. **The config file is written atomically and durably** (seven properties, five from review rather than the first draft). Plus 257 lines of dead code removed, 25 of 29 `#[allow(dead_code)]` attributes found to suppress nothing, `undocumented_unsafe_blocks` made a gate, and two `cargo deny` ignores retired on their own stated condition. `rustynes-apu` and `rustynes-core` both change, so **AccuracyCoin 141/141 and nestest 0-diff are verified, not asserted**. Built on **v2.3.8 "Parallax"** — which pixels differ, not just which frame: `Probe` could say two configurations of the same ROM diverge and *at which frame* and nothing about where or why, because a trial reduces each frame to one `u64`. The **Divergence Lens** keeps the full output instead of its hash and reports the *shape* of the difference (population count, first pixel in raster order, inclusive bounding box), localises on the **index** framebuffer so a palette difference cannot masquerade as a rendering one, hands the located pixel to Pixel Provenance so the answer is a cause rather than a coordinate, and answers `Inconclusive` rather than letting "I stopped looking" wear the shape of "they agree". Built on **v2.3.7 "Overtone"** — the audio-provenance release: a per-register write attribution (*what wrote this, and from which instruction*) plus a per-CPU-cycle mix trace, and the discovery that Pixel Provenance had shipped non-functional for four releases because run-ahead's rollback cleared its store before any UI could read it — in three more places than the v2.3.6 fix had enumerated. Built on **v2.3.6 "Sounding"** — measuring, and what a measurement may claim. Two shipped features are found never to have worked: **Pixel Provenance** returned an empty report for every user on the default `run_ahead = 1` (its rollback is the last thing before the frontend takes the lock, so the panel always looked after the wipe) and "click any pixel" was never implemented — two comments and four doc claims asserted the opposite of their own code, which is why four releases passed unchecked; and **Duck Hunt could never score**, its Zapper probe exactly inverting the "see nothing, then a bright spot" protocol. Two new tools built to **decline rather than guess**: the **Latency Oracle** (measures the game's own input lag; recommends a run-ahead depth and never applies one) and the **RAM Atlas** (classifies all 2 KiB of work RAM, then *verifies* a candidate by perturbing it — `Untested` is a third state distinct from `Inert`, and liveness names its lens). **APU Workstream D is closed** on three measured rejections plus the fat-LTO mechanism explaining them. Tools and Debug are regrouped by task. Core gains one `const fn` getter, so AccuracyCoin 141/141 is verified, not asserted. Built on **v2.3.5 "Manifest"** — the declaration release: what the core says about itself. A user reported RetroArch still showing the pre-relicense MIT/Apache-2.0 terms, and it was: RetroArch reads `dist/info/` from **libretro/libretro-super**, a SEPARATE copy nothing synced, so the v2.2.9 GPL relicense never reached the file users see. Corrected to `GPLv3+` with a standing `libretro_info_audit.rs` that makes the upstream sync a **copy** rather than a re-derivation, and a licence change is now a mandatory upstream-sync trigger. Auditing the wrapper then found **five further defects, every one with correct emulation behind it** — PAL ran 20.2% fast, Reset did nothing ever, unload leaked Game Genie indices, the aspect ratio assumed square pixels, and the Zapper was unreachable — plus a **use-after-free** in the controller tables caught in review. The crate went from zero tests to eight. The APU also gained its first throughput bench and a default-configuration mix specialization (−3.3% to −4.2% on `nes_run_frame_nestest`), so **AccuracyCoin 141/141 was VERIFIED, not asserted**. Built on **v2.3.4 "Ledger"** — the coverage release: three boards (mapper 176 submapper 2 WAIXING-FS005, 154 NAMCOT-3453, 243 Sachen SA-020A, breadth **172 → 174 families**), the coverage harness moved onto the frontend's real load path, and the defect that exposed — the per-game database reading a `0` Mapper column as "force NROM" and overwriting correct headers, leaving **every Sachen cartridge** unloadable since **v1.2.0**. **This release touches the emulation core**, so AccuracyCoin exactly 141/141 is **verified, not asserted by construction**. Its Workstream C (the APU at 18.7% of frame time) was carried to v2.3.5 and delivered there. Built on **v2.3.3 "Cadence"** — the display-pacing release: the run-ahead throttle oscillation traced to a stale median (a gate counting 120 frames of a 600-sample ring), a predictive engage arm that converges a `run_ahead = 3` host in 2.8 s instead of 12.1 s, and the `wp_presentation` measurement apparatus that made the diagnosis possible. **No emulation-core changes** (AccuracyCoin exactly 141/141). Built on **v2.3.2 "Lucid"** (pixel provenance + deterministic replay attestation), **v2.3.1 "Plumb Line"** (ten measured rejections), and **v2.3.0 "Datum II"**, the capstone that **closed** the v2.2.6 → v2.3.0 line (true multi-viewport OS-window detach, the emulator-lock frame-pacing fix, a −5.1% byte-identical PPU optimization, and both forum-reported accuracy items verified already-correct) — all on the **v2.0.0 "Timebase"** MAJOR base (the one-clock / every-cycle-bus-access scheduler rewrite). **v1.0.0** was the first stable, production cut. As of **v2.2.9**, RustyNES is **GPL-3.0-or-later** — a derivative work of GPL-licensed emulators (ADR 0036); a licensing correction, **not** a SemVer break (no public-API or save-state change). `docs/STATUS.md` is the authoritative current-state record; `CHANGELOG.md` carries the full per-release history.
+**Current release: v2.6.5 "Muster"** — rung 5 closes — the AccuracyCoin status vector is identical entry for entry across all 146 entries, with 146 of 146 executed on both sides and none NotRun, where the same gate read 5 of 146 at the version's start. A muster is a roll call where every name is called AND answered, which is the two-clause acceptance exactly. Five PPU defects close the last six differing entries and four were invisible to every gate that existed when the version opened: the background shift registers' RELOAD and their shift clock need SEPARATE gates (with one shared gate the serial-in test was not merely failing but ARITHMETICALLY UNREACHABLE, since reload dots are absolute and the reload discards the low seven bits, so a serial-in one can never reach bit 7 on any alignment — and modelling both structures reproduces BOTH measured shifter values); the sprite X counters are NOT gated on rendering, which AccuracyCoin states outright and the ROM that states it passes either way, because it expects no hit at X=254 and a sprite shoved 18 dots right is also off the line; the PPUADDR second-write v-copy is DELAYED, as the wiki says inside the write sequence itself, swept 1 to 4 dots against a control at 8 and 12 that fails; and the pre-render line CLEARS secondary OAM, without which scanline 0 draws what scanline 239 left — no sprite can ever render on scanline 0, because OAM Y is one less than the display row, and a sprite-0 probe over the full 134 M-cycle battery found 24 hits with four of them there; and the octal latch holding across the read dot, which is verified by exactly ONE gate and was unverifiable until the v-copy delay landed, the two composing the hybrid address together and neither producing it alone. A DIAGNOSIS IS RETRACTED: the residual was read as a two-dot CPU/PPU alignment error from comparing dot spans across two instruments, and at the committed alignment the two consoles execute identical pc, bus_addr and bus_access for 1,695,131 cycles while a two-dot shift moves the first fork back to 593,228 and takes the differing share from 5.13% to 66.80%. The oracle changes on the default path, so AccuracyCoin 141/141 (RAM decoder) and nestest 0-diff are VERIFIED, not asserted. Built on **v2.6.4 "Rubric"** — OAM DMA lands and all nine AccuracyCoin disagreements close, every rule that closed the last three stated by the test ROM and by neither nesdev page — and then the gate that certified them is measured to cover 88 of 146 entries. The emulation core is unchanged. Built on **v2.6.3 "Mainspring"** — the DUT runs on one master clock, and four enables that were never enabling — plus AccuracyCoin end to end and a status vector that names its disagreements by test. The emulation core is unchanged. Built on **v2.6.2 "Witness"** — rung 4 closes: blargg APU battery 11/11 on the co-simulation DUT, six defects no self-written gate could see, and a suite that had been asserting nothing for five minor releases. The emulation core is unchanged. Built on **v2.6.1 "Interleave"** — the DMC and its DMA cycle steal in the MiSTer co-simulation DUT, cycle-exact on the bus. The emulation core is unchanged. Built on **v2.6.0 "Assay"** — the triangle, the noise channel and the sweep unit **in the MiSTer co-simulation DUT** — and an audit of how much of the APU was fitted to the oracle rather than derived from documentation. The emulation core is unchanged. Built on **v2.5.9 "Overture"** — rung 4 opens: the two pulse channels, the frame counter, and four ROM defects the stimulus measurement found first. Built on **v2.5.8 "Blanking"** — VBlank, NMI and the PPUSTATUS race close rung 3 — and both fixes were deletions. Built on **v2.5.7 "Collimation"** — sprite rendering closes exact — the phase was wrong by two dots, and every window was compensating. Built on **v2.5.6 "Vestige"** — Sprite evaluation closes: all 59,993 overlapping cycles match, nine of nine behavioural mutants caught and two proved inert (announced as seven of eight at the cut), and the fix is a byte index that outlives the walk that set it. Built on **v2.5.5 "Raster"** — the first full frame, and three blind spots in the stimulus that fed it. Built on **v2.5.4 "Escapement"** — the background fetch pipeline, and an access two dots early that five gates could not see. Built on **v2.5.3 "Hysteresis"** — toggling rendering takes effect three dots after the write, and four instruments to prove it. Built on **v2.5.2 "Dormant"** — the 2C02 register file, and a gate that passed while testing nothing. Built on **v2.5.1 "Retrace"** — the interrupt sweep closes rung 2, and a gate reported a pass it could not have earned. Built on **v2.5.0 "Rungwork"** — the 6502 rung, and the two gates it cannot reach. Built on **v2.4.9 "Plumbline II"** — the bus half of rung 2, and what it found the day it existed. Built on **v2.4.8 "Palimpsest"** — read-modify-write, and a gate that cannot see its own subject. Built on **v2.4.7 "Keystone"** — the stack closes, and a dead line proves itself dead. Built on **v2.4.6 "Abacus"** — the core learns arithmetic. Built on **v2.4.5 "Compass"** — the core reaches memory, and chooses. Built on **v2.4.4 "Ignition"** — the first real RTL. The 6502's eight-cycle reset and the seventeen single-byte implied opcodes, in SystemVerilog in the sibling repository (`RustyNES_MiSTer@7f092bd`), matching the oracle on all seven CPU fields -- 29 records, and the gate demonstrated to fail on four mutations. The DUT is the **third writer** of the oracle's `CpuBootTrace` format, so `cpu_boot_trace_diff` reads it with no modification and the rung needed no oracle-side change at all. **The oracle settled a question our own prose could not**: reset is EIGHT cycles, and `docs/cpu-6502.md` said both seven and eight -- corrected here. A mutation the test ROM was built to catch came back NOT CAUGHT because `TSX` leaves exactly the flags a wrongly-flagging `TXS` would compute, and a harness bug made every mutation report a catch including the baseline. The emulation core is untouched. Built on **v2.4.3 "Touchstone"** — what the synthesiser accepts, and what the licence requires. A touchstone is a stone you rub gold against; the streak tells you what the metal actually is. This release settles the **two Fabric-plan risks that had to be answered before any RTL exists**, and both were answered by evidence that contradicted what the plan assumed. **Risk 4, the Quartus subset, is FITTED**: Quartus Prime Lite 17.0.2 Build 602 on a 5CSEBA6U23I7 produced a placed-and-routed netlist with **0 synthesis warnings**, and the 2 KiB array inferred as **2 M10K blocks with 29 total registers** — not 16,413 — from the source style alone, no `ramstyle` attribute. The `initial` block became a real MIF (so a boot ROM lands inside the block) and the `enum` was one-hot encoded. Nine constructs are promoted to *fitted*; plain `case`, `priority case` and `$bits` are deliberately left *documented* because the kitchen sink does not exercise them. **Risk 1, the `sys/` licence, inverts the plan's own hedge**: 57 files, **zero GPL-2.0-only**, and `hps_io.sv` — GPL-3.0-or-later and not optional, since it is how a core receives a ROM and reaches the OSD — forces the combined bitstream **up** to GPL-3.0-or-later, already RustyNES's licence. The emulation core is untouched. Built on **v2.4.2 "Cairn"** — the **rung-0 compare surface**. A cairn is a marker set along a route so you can tell you are still on it, which is what a rolling per-cycle hash checkpoint is. The constraint nobody budgets for in co-simulation is trace *volume*, not simulation time, and it is now **measured**: 3 frames of AccuracyCoin is 89,343 CPU cycles, **5,372,427 bytes** of `irq.csv` against **352 bytes** of `ckpt.bin` — a factor of **15,263** — so both sides chain a hash and compare every 4096 cycles, and only the divergent window is re-run with full capture. **What is hashed is a decision about hardware, not about convenience**: `CycleRecord` carries 29 fields and most are RustyNES's *model*, so `Observable` is the subset a device can genuinely produce, the IRQ pair is OR'd before hashing because hardware has one wire-OR'd /IRQ pin, and `pc` is marked DUT-observable rather than pin-observable. The emulation core is untouched. Built on **v2.4.1 "Fabric"** — RustyNES as the oracle a new implementation is verified against. It opens the **v2.4.1 → v2.5.0 "Fabric"** line: a new NES core in SystemVerilog, written from public hardware documentation in a sibling repository, with this emulator as its verification oracle. RustyNES is not being ported to FPGA and cannot be; `crates/rustynes-cosim` is the boundary, and the firewall extends to HDL (ADR 0037). **v2.5.0 is scoped to "the 6502 rung closes"**, not a finished core. Excluding the crate from the workspace is the load-bearing detail — cargo unifies features, and `irq-timing-trace` selects a *different* per-dot loop in `Bus::tick_one_cpu_cycle`, so the accuracy battery had been validating a scheduler no user runs. It also carries **v2.4.0 "Concordance"**, which merged to `main` and was never tagged: atomic durable writes on every path that persists user data, `Nes::timeline_generation()`, and the 15-anchor release audit. AccuracyCoin **141/141** and nestest 0-diff verified, not asserted. Previously, **v2.3.9 "Crucible"** — what the gates actually cover. A crucible tests to destruction rather than inspects, and this release does that to the project's own checks. **The docs-only CI skip had never worked**: `predicate-quantifier` defaults to `some`, so the `code` filter's leading `'**'` matched everything and all seven `!` exclusions under it were dead from the day they were written — fixed with **two** filter steps, because the quantifier is step-level and `accuracy` is a list of *alternatives* that becomes unsatisfiable under `every`, so the one-line fix would have silently disabled the accuracy battery while repairing a different gate. **`test-roms` now runs at review time**, path-filtered over the chip crates, the core, `rustynes-gamedb`, the harness and `tests/` (11 of the last 40 merged PRs, so ~72% still pay nothing). **A freeze from one cartridge kept writing into the next** — an active per-frame write into the wrong game — closed by a ROM-transition sweep across every panel under one rule: derived output discarded, user-authored input kept, and only input that actively *writes* neutralised. **The config file is written atomically and durably** (seven properties, five from review rather than the first draft). Plus 257 lines of dead code removed, 25 of 29 `#[allow(dead_code)]` attributes found to suppress nothing, `undocumented_unsafe_blocks` made a gate, and two `cargo deny` ignores retired on their own stated condition. `rustynes-apu` and `rustynes-core` both change, so **AccuracyCoin 141/141 and nestest 0-diff are verified, not asserted**. Built on **v2.3.8 "Parallax"** — which pixels differ, not just which frame: `Probe` could say two configurations of the same ROM diverge and *at which frame* and nothing about where or why, because a trial reduces each frame to one `u64`. The **Divergence Lens** keeps the full output instead of its hash and reports the *shape* of the difference (population count, first pixel in raster order, inclusive bounding box), localises on the **index** framebuffer so a palette difference cannot masquerade as a rendering one, hands the located pixel to Pixel Provenance so the answer is a cause rather than a coordinate, and answers `Inconclusive` rather than letting "I stopped looking" wear the shape of "they agree". Built on **v2.3.7 "Overtone"** — the audio-provenance release: a per-register write attribution (*what wrote this, and from which instruction*) plus a per-CPU-cycle mix trace, and the discovery that Pixel Provenance had shipped non-functional for four releases because run-ahead's rollback cleared its store before any UI could read it — in three more places than the v2.3.6 fix had enumerated. Built on **v2.3.6 "Sounding"** — measuring, and what a measurement may claim. Two shipped features are found never to have worked: **Pixel Provenance** returned an empty report for every user on the default `run_ahead = 1` (its rollback is the last thing before the frontend takes the lock, so the panel always looked after the wipe) and "click any pixel" was never implemented — two comments and four doc claims asserted the opposite of their own code, which is why four releases passed unchecked; and **Duck Hunt could never score**, its Zapper probe exactly inverting the "see nothing, then a bright spot" protocol. Two new tools built to **decline rather than guess**: the **Latency Oracle** (measures the game's own input lag; recommends a run-ahead depth and never applies one) and the **RAM Atlas** (classifies all 2 KiB of work RAM, then *verifies* a candidate by perturbing it — `Untested` is a third state distinct from `Inert`, and liveness names its lens). **APU Workstream D is closed** on three measured rejections plus the fat-LTO mechanism explaining them. Tools and Debug are regrouped by task. Core gains one `const fn` getter, so AccuracyCoin 141/141 is verified, not asserted. Built on **v2.3.5 "Manifest"** — the declaration release: what the core says about itself. A user reported RetroArch still showing the pre-relicense MIT/Apache-2.0 terms, and it was: RetroArch reads `dist/info/` from **libretro/libretro-super**, a SEPARATE copy nothing synced, so the v2.2.9 GPL relicense never reached the file users see. Corrected to `GPLv3+` with a standing `libretro_info_audit.rs` that makes the upstream sync a **copy** rather than a re-derivation, and a licence change is now a mandatory upstream-sync trigger. Auditing the wrapper then found **five further defects, every one with correct emulation behind it** — PAL ran 20.2% fast, Reset did nothing ever, unload leaked Game Genie indices, the aspect ratio assumed square pixels, and the Zapper was unreachable — plus a **use-after-free** in the controller tables caught in review. The crate went from zero tests to eight. The APU also gained its first throughput bench and a default-configuration mix specialization (−3.3% to −4.2% on `nes_run_frame_nestest`), so **AccuracyCoin 141/141 was VERIFIED, not asserted**. Built on **v2.3.4 "Ledger"** — the coverage release: three boards (mapper 176 submapper 2 WAIXING-FS005, 154 NAMCOT-3453, 243 Sachen SA-020A, breadth **172 → 174 families**), the coverage harness moved onto the frontend's real load path, and the defect that exposed — the per-game database reading a `0` Mapper column as "force NROM" and overwriting correct headers, leaving **every Sachen cartridge** unloadable since **v1.2.0**. **This release touches the emulation core**, so AccuracyCoin exactly 141/141 is **verified, not asserted by construction**. Its Workstream C (the APU at 18.7% of frame time) was carried to v2.3.5 and delivered there. Built on **v2.3.3 "Cadence"** — the display-pacing release: the run-ahead throttle oscillation traced to a stale median (a gate counting 120 frames of a 600-sample ring), a predictive engage arm that converges a `run_ahead = 3` host in 2.8 s instead of 12.1 s, and the `wp_presentation` measurement apparatus that made the diagnosis possible. **No emulation-core changes** (AccuracyCoin exactly 141/141). Built on **v2.3.2 "Lucid"** (pixel provenance + deterministic replay attestation), **v2.3.1 "Plumb Line"** (ten measured rejections), and **v2.3.0 "Datum II"**, the capstone that **closed** the v2.2.6 → v2.3.0 line (true multi-viewport OS-window detach, the emulator-lock frame-pacing fix, a −5.1% byte-identical PPU optimization, and both forum-reported accuracy items verified already-correct) — all on the **v2.0.0 "Timebase"** MAJOR base (the one-clock / every-cycle-bus-access scheduler rewrite). **v1.0.0** was the first stable, production cut. As of **v2.2.9**, RustyNES is **GPL-3.0-or-later** — a derivative work of GPL-licensed emulators (ADR 0036); a licensing correction, **not** a SemVer break (no public-API or save-state change). `docs/STATUS.md` is the authoritative current-state record; `CHANGELOG.md` carries the full per-release history.
RustyNES follows [Semantic Versioning 2.0.0](https://semver.org/).
@@ -109,7 +109,8 @@ The 1.x line was **additive / off-by-default** — every release stayed byte-ide
| **v2.6.1 "Interleave"** | The DMC and its DMA cycle steal in the MiSTer co-simulation DUT, cycle-exact on the bus. The DMC reads its own samples by stopping the CPU and taking a cycle, and the plan set "cycle-exact CPU stall behaviour" as this step's criterion -- so the gate that matters is the per-cycle BUS, not the channel levels. **It went from 323,661 diverging cycles to 0**: all 357,360 overlapping cycles match on `pc`, `bus_addr`, `bus_data` and `bus_access`, with the DMA at 49 bursts of 195 cycles against the oracle's 49 and 195. The channel itself lands complete -- memory reader, the 7-bit delta-modulation output unit with its shift register and bits-remaining counter, the 16-entry rate table (the register value is an INDEX, not a period), the loop flag and the end-of-sample IRQ -- and the DMA sequence is implemented from `nesdev_wiki/DMA.xhtml`: halt on a read cycle, a dummy cycle, an optional alignment cycle, then the get, with the load halting on a get and reloads on a put. The CPU has no RDY pin, so the steal is expressed by holding its clock enable low for one cycle while the APU and PPU keep running -- no CPU change was needed. **Four defects, each found by the next measurement**: the DMC timer ticked on the wrong APU phase (every sample two cycles early); the alignment test was INVERTED, since the wiki conditions it on whether the NEXT cycle is a get rather than the current one, costing the load burst a cycle; the stolen cycles were not MARKED as DMA in the bus trace, so 195 cycles were compared as ordinary reads while their timing was already correct; and the data bus is HELD across a halt, which was established by measurement rather than assumed (for all 144 residual cycles the oracle's value was frozen for the whole burst, only the get driving a new one). **A pre-registered risk is RETRACTED**: before the work began, the DMA's stall placement was recorded as oracle-defined, on the strength of the oracle's own comment describing its scheduler as calibrated and naming an external emulator as the reference to diff against. `DMA.xhtml` documents the whole sequence precisely, so the risk did not exist -- a note that an implementation was CALIBRATED says nothing about whether documentation exists, and checking cost one grep. **A decoding residual dissolved the same way**: the DUT's played bit stream matched the ROM data on all 392 bits and the ORACLE deviated in two, because the first ROM polled `$4015` in its idle loop and provoked the documented DMA / register-read conflict -- a second mechanism the ROM was not written to test. **32 gates green across rungs 1-4; 46 of 46 mutations CAUGHT**, 0 NOT CAUGHT, 0 BUILD-FAILED, with `apudmc037` gated twice because the DMA reaches the CPU and not the mixer. **Zero emulation-core changes** -- no file under the chip crates is touched -- so **AccuracyCoin 141/141 (RAM decoder) and nestest 0-diff hold by construction**. |
| **v2.6.0 "Assay"** | The triangle, the noise channel and the sweep unit -- and an audit of how much of the APU was fitted to the oracle rather than derived from documentation. An assay tests a metal to find out what it is actually made of, which is what this release does to rung 4. **`docs/apu-oracle-vs-documentation.md` is the deliverable the release is named for**: every place the DUT follows RustyNES rather than the NESdev wiki, sorted by risk, each with the text it is measured against and the independent check that would adjudicate it -- because the oracle is an emulator and not silicon, so a shared error between the two is invisible to every rung-4 gate BY CONSTRUCTION. **The oracle was run against blargg's APU battery for the first time in this programme and passes 29/29** (`apu_test` 8/8, `apu_reset` 6/6, `apu_mixer` 4/4, `blargg_apu_2005` 11/11), recorded per audit item because it means different things in different places -- for the `$4017` delay and the 5-step step it moved suspicion off the oracle entirely and onto the RTL. **The headline finding is two errors that were cancelling.** v2.5.9 keyed the `$4017` reset delay on the MODE BIT, which the wiki never mentions (it keys on the write's APU-cycle parity), and that fit was exact ONLY in combination with the 5-step sequencer's step 2 held one tick off the convention its fifteen siblings follow. Measured across all eight APU ROMs: either correction alone costs 2 cycles and **in opposite directions** (46,368 late against 46,366 early); both together are exact. The fitted rule was not merely unfalsified -- it was **load-bearing** for a second error, which is why it survived both a mutation catalog and a documentation audit that looked straight at it. All sixteen sequencer constants are now uniformly documented-minus-one with nothing fitted, and reverting either half is a CAUGHT mutation. The delay is a FIXED two APU ticks, with the documented 3-or-4 CPU-cycle split EMERGING from the countdown living inside `if (apu_phase)` rather than being coded twice. **Three paths that had never been exercised now have gates, and two of them found defects on their first run.** The sweep unit's period update was ABSENT ENTIRELY, which made `sweep_mutes` correct and untestable -- a period nothing updates can never reach an overflowing target -- and `apusweep030` caught it at CPU cycle 47,853. No ROM had ever set the sweep NEGATE bit, so the documented pulse-1 one's-complement / pulse-2 two's-complement asymmetry had never executed; `apuneg033` configures both pulses identically except for which pulse they are, and they mute exactly one half-frame apart. **The frame IRQ window yielded two more**: the coincident-read rule was INVERTED under a source comment asserting the ordering was correct (a `$4015` read on the assertion edge returned 0 *and* destroyed the assertion, where the wiki says it returns 1 and survives), and the flag was asserted on only ONE HALF of its APU cycle where the wiki lists both GET and PUT. Its ROM is the first in rung 4 gated on the **bus** rather than on channel levels, because the IRQ reaches the CPU and never the mixer -- and its 11-cycle poll loop is the whole design, since the obvious 10-cycle loop shares a factor with the 29,830-cycle sequence and would have read the same residue every frame forever while looking entirely reasonable. **Two characterisations are RETRACTED**: v2.5.9's residual was not a `$4003` write-parity sensitivity (it was an inverted power-on tick parity plus a testbench sampling one cycle early, identified from the shape -- all 5,076 divergences satisfied `dut[c] == oracle[c-1]`, unanimously), and `apuquarter032`'s residual was not a rounding effect (it was one sequencer constant off by one, and correcting it closed the residual with no change to any rounding logic). **30 gates green across rungs 1-4; 35 of 35 mutations CAUGHT**, 0 NOT CAUGHT, 0 BUILD-FAILED. **Zero emulation-core changes** -- the diff is the sibling DUT, the excluded `rustynes-cosim` crate and documentation -- so **AccuracyCoin 141/141 (RAM decoder) and nestest 0-diff hold by construction**. |
| **v2.5.9 "Overture"** | Rung 4 opens: the two pulse channels, the frame counter, and four ROM defects the stimulus measurement found first. An overture is the part that plays before the rest of the orchestra arrives, which is what two of five channels is. `rtl/apu2a03.sv` lands both pulses -- timer, 8-step duty sequencer, length counter, envelope, the sweep MUTE -- plus the frame counter in both modes with its IRQ and the `$4015`/`$4017` register file, written from the NESdev wiki with no emulator or HDL source consulted. **The partition was fixed BEFORE the rung**, because the APU is the hardest chip in the console to gate honestly: what it *produces* is an analog level and what an emulator computes is a number. Gates are the `$4015` read value, the `/IRQ` pin and each channel's **integer** DAC input; diagnostics are `MixRecord`'s `f32` mix fields (RustyNES's non-linear mixer, decimator and expansion gain), the frame-sequencer step index and `apu_phase` -- a field that exists only because RustyNES chose to model something that way never becomes a gate, however convenient. **The stimulus measurement earned its place immediately, finding four ROM defects before a single gate ran**: length index 3 is **2** and not 254 (the table alternates long and short, and the index is not the count); the 6502 boots with I set, so without `CLI` **zero** IRQs are taken despite five real line edges; two channels at the same volume are indistinguishable in a channel-level golden; and power-on work RAM is **seeded, not zeroed**, so an uninitialised counter byte came up `0x7D` and the handler's `CMP #3` never matched. Four findings in the DUT: the duty sequencer counts **up** (counting down gave the right period and levels with the wrong phase); the 4-step constants must be consistently 0-based, since `fc_count` reads V on tick V+1 -- three of four were and the last was the wiki's own number, putting the frame IRQ **3 cycles late**; `$4017` bit 7 clocks a quarter and half frame **immediately**, where a latched flag left two divergent cycles at a length expiry; and the `$4017` reset delay depends on **bit 7**, which the wiki's "3 or 4 CPU clock cycles" does not settle -- each constant fixed one ROM and broke the other, and a parity rule separated nothing because both writes land on the same phase. `apulen027` is exact on both surfaces at 178,668 cycles each; `apupulse026`'s bus surface is 3 and its channel levels **1,000 -- which is 500 runs of exactly two cycles, one per pulse edge**: a uniform one-tick offset and a phase sensitivity the first stimulus hid, because adding a five-cycle initialisation (an ODD number) flipped which `apu_phase` the `$4003` writes land on. Two fixes were tried and **both rejected by measurement**; the wiki is right that the period divider is not reset. Carried to v2.6.0 with the ROM that exposes it already written. **Nine of ten mutations CAUGHT**, and the two that were not both indicted the STIMULUS rather than the gate. **Zero emulation-core changes** -- the diff is the excluded `rustynes-cosim` crate plus documentation -- so **AccuracyCoin 141/141 (RAM decoder) and nestest 0-diff hold by construction**. |
-| **v2.6.4 "Rubric"** (current) | OAM DMA lands, all nine AccuracyCoin disagreements close, and then the gate that certified them is measured to cover 88 of 146 entries. **`$4014` was a register the console decoded and did nothing with** -- the DUT had never spent the 513 cycles an OAM DMA costs. It lands as a real bus master (halt on a read cycle, optional alignment, 256 read/write pairs) from `nesdev_wiki/DMA.xhtml`, with the documented DMC-get precedence costing OAM its alignment as well as its slot; its halt and alignment were fitted to the oracle first and corrected from the wiki, recorded rather than squashed. The **`SH` group** closes in two steps, the second named by the residual of the first: the AND with the address high byte is RDY-conditional, and the dummy-read cycle is addressing-mode dependent, so `SHA (d),Y`'s `tcyc==3` is a pointer-high fetch. Those took the vector 9 -> 3. A rubric is the authoritative statement of the rules, written by whoever set the test -- which is literally where all three fixes came from. **AccuracyCoin is MIT-licensed and its assembly source is one `curl` away**; this plan's own note that it "is not vendored" is true of this repository and had been read as unavailable. The source explains every assertion, and settled three entries in minutes. **`Open Bus`**: a read of `$4015` does not drive the data bus and its D5 is open bus -- rules the nesdev pages do not state at all, and its stimulus (`LDX #$16 / LDA $40FF,X`) is the exact instruction the trace divergence had been localised to independently. **`Interrupt flag latency`**: branches poll before cycles 2 and 4 and NEVER before 3, so a taken branch that does not cross a page has no poll at its last cycle. **`NMI Overlap BRK`**: an interrupt sequence does not poll (stated in the wiki, missed here), and the hijack window was one CPU cycle narrow at its late edge -- where the comment above the line had argued at length for the wrong version and named this very entry as the test that would catch it. **A `Fail(N)` names test N, one-based**, and decoding it as an index had made a REGRESSION (test 7 -> test 5) read as progress; v2.6.3's reading that six entries "shared one failure code" and therefore one cause is retracted with it. **A fix that closes one gate and opens another is a scope measurement**: the first poll fix moved the poll for EVERY instruction, closed the entry and regressed `apupulse026` and `blargg08`; narrowed to branches alone, nothing regresses. Three AccuracyCoin sub-test ROMs become standing **verdict** gates (69 -> **72 green, 0 failed**), verdict rather than bus by measurement -- their per-cycle surfaces are dominated by the open PPU I/O-latch item at 2,331,867 of 4,467,082 cycles. **7 of 8 RTL mutations CAUGHT**, the eighth classified INERT by byte-comparing 14,294,736 bytes of trace. **Then the coverage finding.** The vector reported identical across all 146 with **58 entries `NotRun` on BOTH sides**: the 600-frame window reaches the CPU catalog and asks nothing about the APU, PPU, sprite-evaluation or PPU-misc suites. 4500 frames executes all 146 (134,012,761 cycles). `accuracycoin_status` now prints coverage and REFUSES when any entry is unrun on both sides (3 of 3 mutations caught). Widening it found a real defect on its first run, at cycle 20,636,325: a halted CPU mid-`LDA $2007` held `ppu_sel` high through a DMC steal, so the sample fetch got the PPU read buffer instead of the cartridge -- the comment above the line stated the intent it violated. **Rung 5 does NOT close**: measured on both sides, the oracle climbs 88 -> 95 -> 117 -> 120 -> 146 while the DUT goes 88 -> 5 -> 5 -> 5, flat for 74 M cycles. The DUT sits in a three-cycle self-loop at `$80DF` = `INC $EC` / `JMP $80DF`, AccuracyCoin's MENU IDLE LOOP, with only the five results the power-on path writes -- so it RESET and returned to the menu rather than hanging inside a test, a reading published from catalog order and retracted after one PC probe. Reported as unavailable rather than as "141 of 146 differ", which is one defect and not 141. **No `rustynes-{cpu,ppu,apu,mappers,core}` changes**, so AccuracyCoin **141/141 (RAM decoder)** and nestest 0-diff hold by construction. |
+| **v2.6.5 "Muster"** (current) | Rung 5 closes. The AccuracyCoin status vector is **IDENTICAL entry for entry across all 146 entries**, with **146 of 146 executed on both sides** and none `NotRun` -- measured over the 4500-frame golden, 134,012,761 cycles, where the same gate read **5 of 146 executed and 22 differing** at the version's start. A muster is a roll call where every name is called AND answered, which is this release's two-clause acceptance exactly; the second clause is v2.6.4's addition, and without it an identical vector over entries that never executed is a pass, and was one. **Five PPU defects close the last six differing entries, and four were invisible to every gate that existed when the version opened.** The recurring shape is a gate agreeing about a question it was never asked. **The background shift registers' RELOAD and their shift clock need SEPARATE gates**: with one shared gate `BG Serial In` was not merely failing but ARITHMETICALLY UNREACHABLE -- reload dots are absolute, so on a re-enable the next reload is at most seven dots away and the reload takes `hi[14:7]`, discarding every serial-in one, so bit 7 can never hold one on any alignment for any stimulus. Modelling both structures reproduces BOTH measured shifter values, the oracle's `F807` falling out of the split model unfitted and the common gate's repeating `0001 0003 ... 007F 0000` containing the `0003` the DUT reported. **That fix alone left the gate red**, which is why it is recorded separately: **the sprite X counters are NOT gated on rendering**, and AccuracyCoin's `Stale Sprite Shift Registers` test 2 says so outright -- "Rendering was disabled for 18 ppu cycles, but the sprite counters were NOT halted during that time". This core froze them, so a disable/enable pair pushed every sprite right by the width of the window; the probe showed `bopq=1` with sprite zero's counter still reading **18**, the exact width of the test's own off-window. **The ROM that STATES the rule passes either way** -- it expects no hit at X=`$FE`, and a sprite shoved 18 dots further right is also off the end of the line. **The PPUADDR second-write `v <- t` copy is DELAYED**, and `nesdev_wiki/PPU_scrolling.xhtml` says so inside the write sequence itself ("wait 1 to 1.5 dots after the write completes"); this core committed it in the write's own edge. Swept 1 to 4 dots, all of which close `Hybrid Addresses`, against a control at 8 and 12 that fails -- the control is the load-bearing half, since four consecutive passes is also what a build ignoring the override would produce. **The pre-render line CLEARS secondary OAM**: the whole evaluation block INCLUDING the clear was gated on `scanline < 240`, so the pre-render line kept what scanline 239's evaluation had left and the next frame's scanline 0 drew it. **No sprite can ever render on scanline 0**, because OAM Y is stored one less than the display row. A sprite-0 probe over the full battery named it in one run: 24 hits in 134 M cycles, four of them at scanline 0, one per frame, immediately after the single legitimate scanline-239 hit. The fifth, the octal latch holding across the read dot, is verified by exactly ONE gate and was unverifiable until the fourth landed -- the two compose the hybrid address together and neither produces it alone. **A DIAGNOSIS IS RETRACTED.** The residual was read as a two-dot CPU/PPU alignment error, from comparing per-dot record spans across two instruments. Three configurations refute it: at the committed alignment the two consoles execute identical `pc`, `bus_addr` and `bus_access` for **1,695,131 cycles** and issue both PPUADDR writes at the same cycles, while a two-dot power-on shift moves the first divergence back to 593,228 and takes the differing share from **5.13% to 66.80%**, and `LEAD_CPU_CYCLES` 1 -> 2 diverges by scanline 8. The "two dots" was two instruments stamping their records at different points in the cycle -- the v2.5.7 lesson, third occurrence. Also: `rustynes-cosim`'s `state_trace_records_carry_their_cpu_cycle` was gated on a feature no CI step enabled, so it **ran nowhere** -- a regression test the gate could not reach, which is the shape the surrounding CI steps exist to prevent; it runs now, and the test genuinely inapplicable under that feature is gated out with its reason rather than left failing. **The oracle changes on the DEFAULT path** (a `Controller::write_strobe` owed-shift fix), so **AccuracyCoin 141/141 (RAM decoder)** and nestest 0-diff are **VERIFIED, not asserted**. |
+| **v2.6.4 "Rubric"** | OAM DMA lands, all nine AccuracyCoin disagreements close, and then the gate that certified them is measured to cover 88 of 146 entries. **`$4014` was a register the console decoded and did nothing with** -- the DUT had never spent the 513 cycles an OAM DMA costs. It lands as a real bus master (halt on a read cycle, optional alignment, 256 read/write pairs) from `nesdev_wiki/DMA.xhtml`, with the documented DMC-get precedence costing OAM its alignment as well as its slot; its halt and alignment were fitted to the oracle first and corrected from the wiki, recorded rather than squashed. The **`SH` group** closes in two steps, the second named by the residual of the first: the AND with the address high byte is RDY-conditional, and the dummy-read cycle is addressing-mode dependent, so `SHA (d),Y`'s `tcyc==3` is a pointer-high fetch. Those took the vector 9 -> 3. A rubric is the authoritative statement of the rules, written by whoever set the test -- which is literally where all three fixes came from. **AccuracyCoin is MIT-licensed and its assembly source is one `curl` away**; this plan's own note that it "is not vendored" is true of this repository and had been read as unavailable. The source explains every assertion, and settled three entries in minutes. **`Open Bus`**: a read of `$4015` does not drive the data bus and its D5 is open bus -- rules the nesdev pages do not state at all, and its stimulus (`LDX #$16 / LDA $40FF,X`) is the exact instruction the trace divergence had been localised to independently. **`Interrupt flag latency`**: branches poll before cycles 2 and 4 and NEVER before 3, so a taken branch that does not cross a page has no poll at its last cycle. **`NMI Overlap BRK`**: an interrupt sequence does not poll (stated in the wiki, missed here), and the hijack window was one CPU cycle narrow at its late edge -- where the comment above the line had argued at length for the wrong version and named this very entry as the test that would catch it. **A `Fail(N)` names test N, one-based**, and decoding it as an index had made a REGRESSION (test 7 -> test 5) read as progress; v2.6.3's reading that six entries "shared one failure code" and therefore one cause is retracted with it. **A fix that closes one gate and opens another is a scope measurement**: the first poll fix moved the poll for EVERY instruction, closed the entry and regressed `apupulse026` and `blargg08`; narrowed to branches alone, nothing regresses. Three AccuracyCoin sub-test ROMs become standing **verdict** gates (69 -> **72 green, 0 failed**), verdict rather than bus by measurement -- their per-cycle surfaces are dominated by the open PPU I/O-latch item at 2,331,867 of 4,467,082 cycles. **7 of 8 RTL mutations CAUGHT**, the eighth classified INERT by byte-comparing 14,294,736 bytes of trace. **Then the coverage finding.** The vector reported identical across all 146 with **58 entries `NotRun` on BOTH sides**: the 600-frame window reaches the CPU catalog and asks nothing about the APU, PPU, sprite-evaluation or PPU-misc suites. 4500 frames executes all 146 (134,012,761 cycles). `accuracycoin_status` now prints coverage and REFUSES when any entry is unrun on both sides (3 of 3 mutations caught). Widening it found a real defect on its first run, at cycle 20,636,325: a halted CPU mid-`LDA $2007` held `ppu_sel` high through a DMC steal, so the sample fetch got the PPU read buffer instead of the cartridge -- the comment above the line stated the intent it violated. **Rung 5 does NOT close**: measured on both sides, the oracle climbs 88 -> 95 -> 117 -> 120 -> 146 while the DUT goes 88 -> 5 -> 5 -> 5, flat for 74 M cycles. The DUT sits in a three-cycle self-loop at `$80DF` = `INC $EC` / `JMP $80DF`, AccuracyCoin's MENU IDLE LOOP, with only the five results the power-on path writes -- so it RESET and returned to the menu rather than hanging inside a test, a reading published from catalog order and retracted after one PC probe. Reported as unavailable rather than as "141 of 146 differ", which is one defect and not 141. **No `rustynes-{cpu,ppu,apu,mappers,core}` changes**, so AccuracyCoin **141/141 (RAM decoder)** and nestest 0-diff hold by construction. |
| **v2.6.3 "Mainspring"** | The DUT runs on one master clock, and four enables that were never enabling. A mainspring is the single wound source that drives a clock's whole train, which is what `nes_top` becomes here: it took its clock enables as INPUTS and the testbench generated the dot phase; it now takes a single 21.477272 MHz master clock and derives `ce`, `ppu_ce` and `ppu_access` itself -- the shape Quartus compiles. It is built in RustyNES's own v2.0.0 "Timebase" shape, **two independent accumulators in master-clock units, never reset to one another**, and that is not stylistic: a modulo-`CPU_DIV` phase counter looks equivalent on NTSC and cannot express PAL at all, where 16 master clocks per CPU cycle and 5 per dot is 3.2 dots per cycle. `ACCESS_MC` and the PPU phase offset are DERIVED from the oracle's `read_split`/`write_split` rather than swept, and **five testbench phase knobs are retired** -- they existed to find this phase, and the answer is now compiled into the core. **It found four enables that were never enabling.** The old testbench tied `ce` high and pulsed the clock once per CPU cycle, so the clock did the gating the enable was supposed to do and any ungated `always_ff` was correct only by accident; under a real master clock each fires twelve times. Two were already known (the PPU register block at v2.5.7, the open-bus decay reload) and **two were not**: the DMC's DMA acknowledge, where the sample pointer advanced by TWELVE per byte and 324,182 of 357,360 cycles diverged, and the frame-counter IRQ set points, where the IRQ line rose eleven master clocks early so the CPU took the interrupt one instruction sooner -- caught by blargg's `08.irq_timing`, a third-party ROM rather than our own trace agreeing with itself. A **compensating** fix was found and REJECTED: delaying the APU's IRQ by one cycle also gave 66 of 66 and is indistinguishable from the real fix by gate result; `cpu6502.sv` already implements the oracle's second-to-last-cycle recognition, correctly gated, so a second delay would have cancelled an APU-side error. Looking for a cause AFTER the fix worked is what separated them. **blargg's `instr_test-v5` battery becomes a standing gate** -- sixteen third-party ROMs, ~2.68 M cycles each, compared per cycle, **16 of 16 exact**, taking the suite from 50 gates to **66 green, 0 failed**. Every rung-1 ROM before these was written inside the project, so the rung could only ask questions someone there thought to ask; these found **three defects the entire self-written corpus had missed**, none of them in the opcodes the battery was run to validate: `RRA` fed its `ADC` stage the carry from BEFORE the instruction (its bus trace was identical on both sides and only the accumulator differed, by one, surfacing nine cycles later), the 8-cycle indirect read-modify-write forms addressed the indexed target during their POINTER fetch cycles, and the PPU I/O-bus latch never decayed -- a 2C02 defect reached from a CPU ROM, three rungs after rung 3 closed. The five `SH`-group stores close the decoder at **256 of 256** opcodes. **The decay constant is where documentation and oracle contradict each other on a quantity a gate depends on.** The wiki says 3-30 ms; RustyNES uses 558.7 ms. Swept against the full 66-gate suite rather than argued: 30 ms fails 9 gates, 50 ms fails 5, 100 ms 3, 200 ms 2, 300 ms 1, and 558.7 ms is the first value failing none. The binding constraint is one measurable property of one ROM -- `10-branches` has a longest gap between group-0 refreshes of 936,697 CPU cycles, or 2,810,091 dots -- and that prediction was TESTED: 2,809,000 dots leaves 52 divergences and 2,811,000 is exact, so the corpus demands >= 523.4 ms. Documentation and corpus are incompatible by a factor of ~17, this rung has no independent oracle to adjudicate, and the constant stays the oracle's, stays labelled **fitted**, and stays a `localparam` so it can move when something can decide. That is Fabric risk 6 -- the oracle can be wrong -- arriving as a measurement rather than a caveat. **Rung 5 reaches an end-to-end AccuracyCoin run**, and the oracle gains `accuracycoin_status`: a status vector decoded against the 146-entry catalog and comparable **entry for entry**, including `Skipped` and `NotRun`, naming every disagreement by test rather than by address. First measurement: **137 of 146 entries agree, 9 differ**, six sharing one failure code -- a pattern a pass count of 137 would have hidden. Producing the vector is this release's deliverable; making the two agree is v2.6.4. Also: an Android dependency refresh (AGP 9.2.1 -> 9.3.2, Compose compiler 2.3.10 -> 2.3.21, `compose-bom` 2026.08.00) with the Gradle 10 deprecations cleared and the AGP/Kotlin interlock measured out of the published POMs rather than assumed; a Rust and Actions refresh; and `markdownlint-cli` v0.39.0 -> v0.49.1, where the pin held since v2.3.9 as a hazard is finally MEASURED -- `MD060/table-column-style` reads this corpus as `compact` and reports 1,936 findings across 122 files, so the style already in use is pinned instead, measuring zero and rewriting no document. **No `rustynes-{cpu,ppu,apu,mappers,core}` changes**, so AccuracyCoin **141/141 (100.00%, RAM decoder)** and nestest 0-diff hold by construction -- and were run anyway. |
> **Forward path.** The v2.0.x "Harbor", v2.1.x "Fathom", and v2.2.x lines have all shipped; the v2.2.6 → v2.3.0 line has now **closed** with v2.3.0 "Datum II"; the v2.3.x performance campaign has now **shipped in full**, as three releases: **v2.3.1 "Plumb Line"** absorbed both the measurement apparatus and the core hot-path campaign, whose ten items were all measured and all rejected and so had no shippable content of their own; **v2.3.2 "Lucid"** the novel features (pixel provenance + replay attestation); and **v2.3.3 "Cadence"** the display-pacing work — the run-ahead throttle oscillation traced to a stale median, the predictive engage arm, and the `wp_presentation` measurement apparatus that made the diagnosis possible. The campaign closed there; **v2.3.4 "Ledger"** opened the next line with mapper coverage — three boards to **174 families**, and the coverage harness moved onto the frontend's real load path, which exposed a per-game-database defect that had left every Sachen cartridge unloadable since v1.2.0. Its Workstream C, the APU at 18.7% of frame time, was not delivered there and landed in **v2.3.5 "Manifest"**, which is otherwise about what the core declares about itself: the libretro `.info` licence drift a user reported, and the five wrapper defects auditing it uncovered. The line then continued as a **measurement-and-honesty** run rather than a feature one: **v2.3.6 "Sounding"** (two shipped features found never to have worked; the Latency Oracle and RAM Atlas both built to decline rather than guess), **v2.3.7 "Overtone"** (audio provenance, and the same-timeline-restore defect found in three more places than the v2.3.6 fix had enumerated), **v2.3.8 "Parallax"** (the Divergence Lens — which pixels differ, not just which frame), and **v2.3.9 "Crucible"** — which turned the same scrutiny on the project's own gates and found a docs-only CI skip that had never worked, an accuracy battery that only ran after merge, and a freeze from one cartridge writing into the next. Note the codenames diverged from this plan as written: what shipped as v2.3.2 took "Lucid" rather than the planned "Grain"/"Conduit II", and v2.3.3 is "Cadence". RustyNES is **permanently open-source and income-free** (ADR 0035): the earlier "joint Google Play + App Store + AltStore + F-Droid launch" is **withdrawn** — any store listing is a **free** app with **no monetization** (no ads, tracking, or paid unlock), an unversioned later step. `to-dos/ROADMAP.md` is the authoritative forward roadmap.
diff --git a/crates/rustynes-core/src/bus.rs b/crates/rustynes-core/src/bus.rs
index 5e13cd42..d324d5ae 100644
--- a/crates/rustynes-core/src/bus.rs
+++ b/crates/rustynes-core/src/bus.rs
@@ -372,6 +372,30 @@ pub struct LockstepBus {
/// Per-port Four Score read counter (0-7 = primary pad, 8-15 = secondary
/// pad, 16-23 = signature, then 1s). Reset on each strobe.
four_score_idx: [u8; 2],
+ /// Does the Four Score chain owe a clock edge on this port?
+ ///
+ /// The exact counterpart of `Controller::pending_shift`, and it exists for
+ /// the same reason. v2.6.5 made a contiguous read of a port return the same
+ /// bit — `CLK` stays low across the run, so the pads do not advance — but
+ /// the adapter's 24-read multiplexer went on advancing `four_score_idx` and
+ /// shifting `four_score_sig` on EVERY read. The chain then ran ahead of the
+ /// pads feeding it: a contiguous pair at index 7 moved to the pad-3 window
+ /// after only seven advances of pad 1, and a pair inside the signature
+ /// window consumed two signature bits where the hardware returns one twice.
+ ///
+ /// The Four Score is one shift chain, so it clocks on the same edge the
+ /// pads do. Cleared by a strobe, exactly as `pending_shift` is.
+ four_score_pending: [bool; 2],
+ /// CPU cycle of the most recent read of each controller port, or
+ /// `u64::MAX` for "never".
+ ///
+ /// `CLK` on the controller port is LOW only while `$4016`/`$4017` is being
+ /// read, and the shift register advances on its low-to-high transition —
+ /// when the read ENDS. Consecutive read cycles hold it low throughout and
+ /// so produce one edge between them, not two. This is what says whether a
+ /// read continues such a run. Every CPU cycle is a bus access in this core
+ /// (ADR 0029), so cycle adjacency IS address-bus continuity.
+ port_read_cycle: [u64; 2],
/// Per-port Four Score signature shift register, reloaded on each strobe
/// (port 0 = `0x08`, port 1 = `0x04`; shifted out LSB-first).
four_score_sig: [u8; 2],
@@ -863,6 +887,8 @@ impl LockstepBus {
power_up_palette: PaletteInit::Zeroed,
controllers34: [Controller::new(); 2],
four_score_idx: [0; 2],
+ four_score_pending: [false; 2],
+ port_read_cycle: [u64::MAX; 2],
four_score_sig: [0; 2],
#[cfg(feature = "debug-hooks")]
controller_polled: false,
@@ -1098,6 +1124,7 @@ impl LockstepBus {
// transient strobe/read state resets like the controllers above.
self.controllers34 = [Controller::new(); 2];
self.four_score_idx = [0; 2];
+ self.four_score_pending = [false; 2];
self.four_score_sig = [0; 2];
// Vs. System coin/service inputs are transient (DIP switches are
// hardware config and persist across a power-cycle, like the panel).
@@ -2411,6 +2438,39 @@ impl LockstepBus {
&self.controllers34
}
+ /// The CPU cycle of `port`'s most recent read (`u64::MAX` = never), for
+ /// the save state.
+ #[must_use]
+ pub const fn port_read_cycle(&self, port: usize) -> u64 {
+ self.port_read_cycle[port]
+ }
+
+ /// Restore the controller-port CLK run state: four `pending_shift` flags
+ /// (ports 1-2 then the Four Score's 3-4) and the two per-port read cycles.
+ /// The Four Score chain's owed-edge flags, for the snapshot.
+ #[must_use]
+ pub const fn four_score_pending(&self) -> [bool; 2] {
+ self.four_score_pending
+ }
+
+ /// Restore the Four Score chain's owed-edge flags. See
+ /// [`Self::set_controller_run_state`]; kept beside it because the two are
+ /// one piece of state split across two devices.
+ pub const fn set_four_score_pending(&mut self, pending: [bool; 2]) {
+ self.four_score_pending = pending;
+ }
+
+ /// Restore the controller-port CLK run state: four `pending_shift` flags
+ /// (ports 1-2 then the Four Score's 3-4) and the two per-port read cycles.
+ pub const fn set_controller_run_state(&mut self, pending: [bool; 4], cycles: [u64; 2]) {
+ self.controllers[0].pending_shift = pending[0];
+ self.controllers[1].pending_shift = pending[1];
+ self.controllers34[0].pending_shift = pending[2];
+ self.controllers34[1].pending_shift = pending[3];
+ self.port_read_cycle[0] = cycles[0];
+ self.port_read_cycle[1] = cycles[1];
+ }
+
/// Enable/disable the Four Score 4-player adapter. Off by default; while
/// off, `$4016`/`$4017` behave exactly as the standard two controllers
/// (byte-identical reads — determinism + save-states unaffected).
@@ -2519,6 +2579,9 @@ impl LockstepBus {
// (port 0 = 0x08, port 1 = 0x04, shifted out LSB-first).
self.four_score_idx = [0, 0];
self.four_score_sig = [0x08, 0x04];
+ // The chain owes nothing immediately after a strobe, so the FIRST
+ // read serves index 0 rather than advancing past it.
+ self.four_score_pending = [false, false];
}
}
@@ -2526,6 +2589,16 @@ impl LockstepBus {
/// advancing the shift register. Four Score off → just
/// `controllers[port].read()`; on → the multiplexed 24-read sequence
/// (primary pad → secondary pad → signature → 1s).
+ /// Does a read of `port` on this cycle continue an unbroken run of reads
+ /// of the same port? Records this cycle as the port's latest read either
+ /// way, so callers must invoke it exactly once per read.
+ const fn port_continues_run(&mut self, port: usize) -> bool {
+ let last = self.port_read_cycle[port];
+ let cont = last != u64::MAX && self.cycle == last.wrapping_add(1);
+ self.port_read_cycle[port] = self.cycle;
+ cont
+ }
+
fn read_port(&mut self, port: usize) -> u8 {
// v1.6.0 Workstream A3 (`TAStudio` lag log): any read of $4016/$4017
// counts as the game polling input this frame. Output-only; gated.
@@ -2563,25 +2636,33 @@ impl LockstepBus {
if let Some(d) = &mut self.expansion_device[port] {
return d.read();
}
+ let cont = self.port_continues_run(port);
if !self.four_score || self.controllers[port].strobe {
- return self.controllers[port].read();
+ return self.controllers[port].read(cont);
}
+ // ADVANCE FIRST, THEN SERVE — the same shape as `Controller::read`,
+ // and for the same reason. The chain clocks on the rising edge that
+ // ENDS the previous run, so a contiguous read serves the position it
+ // already served instead of stepping past it. Advancing after the
+ // serve, unconditionally, is what let the adapter run ahead of the pads
+ // feeding it once contiguous reads stopped advancing them.
+ if self.four_score_pending[port] && !cont && self.four_score_idx[port] < 24 {
+ if self.four_score_idx[port] >= 16 {
+ self.four_score_sig[port] = (self.four_score_sig[port] >> 1) | 0x80;
+ }
+ self.four_score_idx[port] += 1;
+ }
+ self.four_score_pending[port] = true;
let idx = self.four_score_idx[port];
- let bit = if idx < 8 {
- self.controllers[port].read()
+ if idx < 8 {
+ self.controllers[port].read(cont)
} else if idx < 16 {
- self.controllers34[port].read()
+ self.controllers34[port].read(cont)
} else if idx < 24 {
- let b = self.four_score_sig[port] & 1;
- self.four_score_sig[port] = (self.four_score_sig[port] >> 1) | 0x80;
- b
+ self.four_score_sig[port] & 1
} else {
1
- };
- if idx < 24 {
- self.four_score_idx[port] += 1;
}
- bit
}
/// Side-effect-free companion to [`Self::read_port`] (debugger peek).
@@ -2968,6 +3049,16 @@ impl LockstepBus {
/// APU tick).
#[allow(clippy::too_many_lines)] // Session-21 added per-cycle DMC + bus-access snapshots; splitting the trace push into a helper would force the bus to recompute `trace_*_pre_tick` values across function boundaries.
pub(crate) fn tick_one_cpu_cycle(&mut self) {
+ // Stamp the PPU with the cycle these dots belong to, BEFORE ticking
+ // them, so a state record carries its own cycle rather than the next
+ // one's. `self.cycle` advances at the END of this function.
+ //
+ // Feature-gated: the default build has neither the field nor this
+ // store. `cpu_clock` carries the same store for the running path; see
+ // the note there for why both are needed.
+ #[cfg(feature = "ppu-state-trace")]
+ self.ppu.set_trace_cpu_cycle(self.cycle);
+
// Tick PPU 3 dots in NTSC. PAL would be 3.2 (5 dots per 16 PPU dots);
// we approximate as 3 for now and gate region accuracy behind a
// future Phase 2 follow-up.
@@ -3480,10 +3571,12 @@ impl LockstepBus {
self.apu.clear_frame_irq_immediate_for_dma();
}
0x4016 => {
- let _ = self.controllers[0].read();
+ let cont = self.port_continues_run(0);
+ let _ = self.controllers[0].read(cont);
}
0x4017 => {
- let _ = self.controllers[1].read();
+ let cont = self.port_continues_run(1);
+ let _ = self.controllers[1].read(cont);
}
_ => {}
}
@@ -3510,12 +3603,14 @@ impl LockstepBus {
// built-in microphone. Default-off (mic released) leaves `mic`
// = 0, so the returned byte is byte-identical to prior releases.
let mic = u8::from(self.famicom_mic) << 2;
- let v = (sample & 0xE0) | self.controllers[0].read() | mic;
+ let cont = self.port_continues_run(0);
+ let v = (sample & 0xE0) | self.controllers[0].read(cont) | mic;
self.open_bus = v;
v
}
0x4017 => {
- let v = (sample & 0xE0) | self.controllers[1].read();
+ let cont = self.port_continues_run(1);
+ let v = (sample & 0xE0) | self.controllers[1].read(cont);
self.open_bus = v;
v
}
@@ -4533,6 +4628,20 @@ impl Bus for LockstepBus {
/// (the pivot's working `service_dmc_dma`); Phase 3 wires the
/// `dma_mc_consumed` coherence accounting.
fn cpu_clock(&mut self) {
+ // Stamp the PPU with the cycle whose dots this call is about to run.
+ // See the twin in `tick_one_cpu_cycle` and `Ppu::set_trace_cpu_cycle`.
+ //
+ // BOTH need it, and that is the whole point of having it twice: this is
+ // the path a running console takes, and `tick_one_cpu_cycle` is the one
+ // the harness drives directly. Wiring only the latter left every record
+ // stamped `0` while the field, the column and the plumbing all looked
+ // correct -- caught by
+ // `tests/state_trace_records_carry_their_cpu_cycle.rs`, which exists
+ // because a present-but-constant field reinstates the whole problem it
+ // was added to solve while appearing to fix it.
+ #[cfg(feature = "ppu-state-trace")]
+ self.ppu.set_trace_cpu_cycle(self.cycle);
+
// Diagnostic: snapshot the APU IRQ line (frame-counter | DMC) BEFORE
// `apu_advance_one` runs the frame counter, so `trace_end_cycle` can
// expose the within-cycle frame-counter SET (low=0 -> high=1) vs the
@@ -5129,13 +5238,21 @@ mod four_score_tests {
// more (dmc_halt + 3 uni_oam flags + uni_oam_addr u16 + ppu_clock
// u64 + dma_mc_consumed u64); the v2.1.0 tail appends 2 more (one
// expansion-device tag byte per port, both `None`); the v1.1.0 beta.1
- // tail appends 1 more (the nametable mirroring-override tag, `None`).
- // Truncating all 36 simulates a pre-v1.7.0 save, which must still load
- // with the adapter off (and no expansion device / override).
+ // tail appends 1 more (the nametable mirroring-override tag, `None`);
+ // the v2.6.5 tail appends 22 more: four `pending_shift` bools, two
+ // `port_read_cycle` u64s (the controller-port CLK run state), and two
+ // `four_score_pending` bools (the adapter's own owed edge -- the Four
+ // Score is one shift chain with the pads, so it clocks with them).
+ // Truncating all 58 simulates a pre-v1.7.0 save, which must still load
+ // with the adapter off (and no expansion device / override / run state).
+ //
+ // The constant is deliberately literal rather than computed: it is the
+ // tail's LAYOUT written down, and it is what made a v2.6.5 append fail
+ // loudly here instead of silently shifting every field behind it.
let mut bus = test_bus();
bus.set_four_score(true);
let blob = crate::bus_snapshot::encode_bus(&bus);
- let old = &blob[..blob.len() - 36];
+ let old = &blob[..blob.len() - 58];
let mut restored = test_bus();
restored.set_four_score(true); // prove decode actively turns it off
crate::bus_snapshot::decode_bus(&mut restored, old).unwrap();
@@ -5233,11 +5350,21 @@ mod four_score_tests {
// A pre-v2.1.0 blob lacks the 2 trailing device-tag bytes (one None
// tag per port); a pre-v1.1.0 blob also lacks the mirroring-override
// tag. With nothing attached the encoder writes `[0, 0]` + `[0]`, so
- // truncating those 3 trailing bytes reproduces an older save — which
+ // truncating those trailing bytes reproduces an older save — which
// must still load with both ports unplugged and no override.
+ //
+ // THE COUNT IS 23, NOT 3, AND THAT IS THE POINT. v2.6.5 appended a
+ // 20-byte controller-run tail AFTER those three, so removing three
+ // bytes stopped reproducing an old blob the moment that landed: it
+ // produces a CURRENT blob with a half-eaten tail. That decoded
+ // "successfully" for as long as the tail test was `>= 20` — the
+ // remaining 17 bytes fell through to the legacy path and every port
+ // restored `pending_shift = false`, so the next controller read
+ // repeated a bit. The decoder now refuses a partial tail, which is
+ // what turned this test red and exposed the stale premise.
let bus = test_bus();
let blob = crate::bus_snapshot::encode_bus(&bus);
- let old = &blob[..blob.len() - 3];
+ let old = &blob[..blob.len() - (3 + 4 + 2 * 8 + 2)];
let mut restored = test_bus();
crate::bus_snapshot::decode_bus(&mut restored, old).unwrap();
assert!(restored.expansion_device(0).is_none());
@@ -5245,6 +5372,98 @@ mod four_score_tests {
assert_eq!(restored.mirroring_override(), None);
}
+ #[test]
+ fn a_contiguous_four_score_read_does_not_advance_the_chain() {
+ // The adapter is one shift chain with the pads it multiplexes, so a
+ // contiguous read -- `CLK` staying low across consecutive-cycle reads
+ // of the same port -- must return the SAME bit from the SAME position,
+ // exactly as a bare controller does.
+ //
+ // Before this guard the chain advanced on every read while the pads
+ // advanced only on a rising edge, so it ran ahead of them: reaching the
+ // pad-3 window after seven advances of pad 1 rather than eight, and
+ // consuming two signature bits where the hardware returns one twice.
+ let mut bus = test_bus();
+ bus.set_four_score(true);
+ bus.write(0x4016, 1);
+ bus.write(0x4016, 0);
+
+ // Walk the whole 24-read sequence. At each position, a read on the very
+ // next CPU cycle must repeat it, and must leave the chain where it was.
+ for step in 0..24u8 {
+ let first = bus.read_port(0);
+ // Where the run's OWN rising edge left the chain. The contiguous
+ // read must not move it from here — comparing against the position
+ // before the first read would instead assert the first read does
+ // not advance, which is a different (and wrong) claim.
+ let idx_in_run = bus.four_score_idx[0];
+ bus.cycle = bus.cycle.wrapping_add(1);
+ let contiguous = bus.read_port(0);
+ assert_eq!(
+ first, contiguous,
+ "step {step}: a contiguous read returned a different bit"
+ );
+ assert_eq!(
+ bus.four_score_idx[0], idx_in_run,
+ "step {step}: the chain advanced during a contiguous read"
+ );
+ // Break the run so the next iteration starts a fresh one.
+ bus.cycle = bus.cycle.wrapping_add(4);
+ }
+ }
+
+ #[test]
+ fn the_four_score_owed_edge_survives_a_save_state() {
+ // `four_score_pending` is the adapter's half of the same state
+ // `pending_shift` is for the pads. Restoring one without the other puts
+ // the two halves of one shift chain on different positions.
+ let mut bus = test_bus();
+ bus.set_four_score(true);
+ bus.write(0x4016, 1);
+ bus.write(0x4016, 0);
+ bus.read_port(0);
+ assert_eq!(bus.four_score_pending(), [true, false]);
+
+ let blob = crate::bus_snapshot::encode_bus(&bus);
+ let mut restored = test_bus();
+ restored.set_four_score(true);
+ crate::bus_snapshot::decode_bus(&mut restored, &blob).unwrap();
+ assert_eq!(
+ restored.four_score_pending(),
+ [true, false],
+ "the adapter resumed without the edge it owed"
+ );
+ }
+
+ #[test]
+ fn a_half_truncated_controller_tail_is_refused_not_read_as_legacy() {
+ // The guard the test above exposed the need for. A blob cut anywhere
+ // INSIDE the 20-byte controller-run tail is damage, not an older
+ // layout, and reading it as legacy restores `pending_shift = false`
+ // for every port — silently, and with a consequence: the next
+ // controller read repeats a bit that was already delivered. Zero
+ // trailing bytes is the only absence that means "no tail".
+ //
+ // Every interior cut is checked rather than one representative, because
+ // an off-by-one in the bound is exactly the mistake this guards.
+ let bus = test_bus();
+ let blob = crate::bus_snapshot::encode_bus(&bus);
+ for cut in 1..(4 + 2 * 8 + 2) {
+ let damaged = &blob[..blob.len() - cut];
+ let mut restored = test_bus();
+ assert!(
+ crate::bus_snapshot::decode_bus(&mut restored, damaged).is_err(),
+ "a blob missing {cut} byte(s) of the controller tail decoded cleanly"
+ );
+ }
+ // ... and the whole tail absent still loads, which is the legacy path
+ // this must not break.
+ let legacy = &blob[..blob.len() - (4 + 2 * 8 + 2)];
+ let mut restored = test_bus();
+ crate::bus_snapshot::decode_bus(&mut restored, legacy)
+ .expect("a blob with no controller tail at all is a pre-v2.6.5 save");
+ }
+
#[test]
fn power_pad_state_round_trips_through_save_state() {
use crate::input_device::{InputDevice, PowerPadState};
diff --git a/crates/rustynes-core/src/bus_snapshot.rs b/crates/rustynes-core/src/bus_snapshot.rs
index 4135dcb5..6ae2d94b 100644
--- a/crates/rustynes-core/src/bus_snapshot.rs
+++ b/crates/rustynes-core/src/bus_snapshot.rs
@@ -11,6 +11,11 @@ use crate::input_device::{
FamilyKeyboardState, InputDevice, SnesMouseState, VausState, ZapperState,
};
use crate::save_state::{BinReader, BinWriter, SnapshotError};
+
+/// Bytes in the v2.6.5 controller-run tail: four `bool` port flags plus two
+/// `u64` cycle stamps. Zero trailing bytes is a pre-v2.6.5 blob; anything
+/// between 1 and this is damage, not a legacy layout.
+const CONTROLLER_RUN_TAIL: usize = 4 + 2 * 8 + 2;
use alloc::format;
use alloc::vec::Vec;
@@ -92,6 +97,33 @@ pub fn encode_bus(bus: &LockstepBus) -> Vec {
// v1.1.0 beta.1 (T-110-B4) — per-game nametable mirroring override (trailing
// field; pre-v1.1.0 blobs lack it and decode as `None` = no override).
w.u8(encode_mirroring_override(bus.mirroring_override()));
+ // v2.6.5 — the controller-port CLK run state, appended at the tail rather
+ // than folded into `encode_controller`, which sits in the middle of this
+ // section and cannot grow without breaking every earlier blob.
+ //
+ // Both halves outlive an instruction and so must be carried: a `$4016` read
+ // is the last cycle of `LDA $4016`, so a snapshot taken at that instruction
+ // boundary has a shift owed and a run open. Restoring without them makes
+ // the next read return a bit the timeline already delivered.
+ //
+ // Pre-v2.6.5 blobs lack these bytes and decode as "no shift owed, no run" —
+ // which is the state after any strobe, so a restored pre-v2.6.5 save
+ // behaves exactly as it did when it was written.
+ for c in bus.controllers_ref() {
+ w.bool(c.pending_shift);
+ }
+ for c in bus.controllers34_ref() {
+ w.bool(c.pending_shift);
+ }
+ for port in 0..2 {
+ w.u64(bus.port_read_cycle(port));
+ }
+ // The Four Score chain's own owed edge. It clocks with the pads, so it has
+ // to be restored with them: without it a snapshot taken mid-run resumes
+ // with the adapter and the pads on different positions of one shift chain.
+ for f in bus.four_score_pending() {
+ w.bool(f);
+ }
w.into_vec()
}
@@ -414,6 +446,44 @@ pub fn decode_bus(bus: &mut LockstepBus, data: &[u8]) -> Result<(), SnapshotErro
None
};
bus.set_mirroring_override(mirroring_override);
+ // v2.6.5 — controller-port CLK run state (trailing-default: pre-v2.6.5
+ // blobs have no bytes left and decode as "no shift owed, no run open",
+ // which is the post-strobe state and so reproduces how they behaved when
+ // they were written).
+ //
+ // A PARTIAL TAIL IS REFUSED RATHER THAN READ AS LEGACY. The test used to be
+ // `>= 20`, so a v2.6.5 blob truncated to between 1 and 19 trailing bytes
+ // took the legacy path and restored `pending_shift = false` for every port
+ // -- silently, and with a consequence: the next controller read repeats a
+ // bit that was already delivered. Zero bytes is a pre-v2.6.5 blob and is
+ // the only absence that means "no tail"; anything shorter than the whole
+ // tail is damage, and a save state is untrusted input.
+ if r.remaining() != 0 && r.remaining() < CONTROLLER_RUN_TAIL {
+ return Err(SnapshotError::SectionTruncated {
+ tag: alloc::string::String::from("BUS "),
+ declared: CONTROLLER_RUN_TAIL,
+ got: r.remaining(),
+ });
+ }
+ if r.remaining() >= CONTROLLER_RUN_TAIL {
+ let mut pending = [false; 4];
+ for p in &mut pending {
+ *p = r.bool()?;
+ }
+ let mut cycles = [0u64; 2];
+ for c in &mut cycles {
+ *c = r.u64()?;
+ }
+ // Through a setter, not the locals above: those were installed into the
+ // bus a hundred lines earlier and mutating them here would decode
+ // cleanly and restore nothing.
+ bus.set_controller_run_state(pending, cycles);
+ let mut fs = [false; 2];
+ for f in &mut fs {
+ *f = r.bool()?;
+ }
+ bus.set_four_score_pending(fs);
+ }
bus.set_bus_misc_state(BusMiscState {
dma_pending,
dma_cycles_owed,
diff --git a/crates/rustynes-core/src/controller.rs b/crates/rustynes-core/src/controller.rs
index 19f17ea3..1e537336 100644
--- a/crates/rustynes-core/src/controller.rs
+++ b/crates/rustynes-core/src/controller.rs
@@ -54,6 +54,20 @@ pub struct Controller {
pub(crate) shift: u8,
/// Strobe state (last bit-0 written to `$4016`).
pub(crate) strobe: bool,
+ /// A shift is owed to the CLK edge that ENDS the current read.
+ ///
+ /// `CLK` is low only while `$4016`/`$4017` is being read, and the shift
+ /// register advances on its LOW-TO-HIGH transition — i.e. when the read
+ /// ends, not when it begins (nesdev *Controller reading*). A run of
+ /// consecutive read cycles therefore holds `CLK` low throughout and
+ /// produces ONE rising edge, so it advances the register once and returns
+ /// the same bit each time. Shifting on the read instead made every read
+ /// its own clock, which is Famicom wiring, not NES.
+ ///
+ /// It is applied lazily, on the next read that is NOT a continuation of the
+ /// run, because a read is the only thing that can observe it. That makes it
+ /// state which outlives an instruction, so it is serialized.
+ pub(crate) pending_shift: bool,
}
impl Controller {
@@ -64,6 +78,7 @@ impl Controller {
buttons: Buttons::empty(),
shift: 0,
strobe: false,
+ pending_shift: false,
}
}
@@ -93,6 +108,25 @@ impl Controller {
if new_strobe {
self.shift = self.buttons.bits();
}
+ // Only an ACTUAL strobe drops an owed shift, and only because the
+ // reload leaves nothing for it to advance.
+ //
+ // This cleared unconditionally when `pending_shift` was introduced, so a
+ // write with bit 0 CLEAR -- not a strobe at all -- silently swallowed a
+ // shift the read run had already earned. That is wrong on the mechanism:
+ // `CLK` is low only while $4016/$4017 is being READ, so a write ENDS the
+ // run and produces exactly the rising edge the owed shift represents. A
+ // write cannot cancel it; if anything it is what causes it.
+ //
+ // Caught by the DUT, which models the edge directly and therefore could
+ // not reproduce this: at AccuracyCoin 25,196,442 a `$40` write lands
+ // between a consecutive read pair and the next read, and from there the
+ // two consoles' shift registers sat one bit apart. The co-simulation
+ // found a defect in the ORACLE, which is the direction that is supposed
+ // to be impossible and is the whole reason the DUT is worth building.
+ if new_strobe {
+ self.pending_shift = false;
+ }
self.strobe = new_strobe;
}
@@ -102,15 +136,22 @@ impl Controller {
///
/// Per the wiki, when the shift register has been emptied subsequent
/// reads return 1.
- pub const fn read(&mut self) -> u8 {
+ /// `continues_run` is true when the immediately preceding CPU cycle was
+ /// also a read of this same port — the case an absolute-indexed
+ /// read-modify-write (`SLO $4016,X`) and a DMC-DMA-interrupted read both
+ /// produce. `CLK` stays low across such a run, so the register does not
+ /// advance between the reads and both see the same bit.
+ pub const fn read(&mut self, continues_run: bool) -> u8 {
if self.strobe {
- self.buttons.bits() & 1
- } else {
- let bit = self.shift & 1;
+ return self.buttons.bits() & 1;
+ }
+ if self.pending_shift && !continues_run {
+ // The previous run ended: its rising edge lands here.
// Shift in 1s from the left so post-empty reads yield 1.
self.shift = (self.shift >> 1) | 0x80;
- bit
}
+ self.pending_shift = true;
+ self.shift & 1
}
/// Side-effect-free sample of the next bit (debugger).
@@ -118,6 +159,10 @@ impl Controller {
pub const fn peek(&self) -> u8 {
if self.strobe {
self.buttons.bits() & 1
+ } else if self.pending_shift {
+ // A debugger peek must show what the NEXT read would return, and
+ // that read lands the owed edge first.
+ ((self.shift >> 1) | 0x80) & 1
} else {
self.shift & 1
}
@@ -135,11 +180,11 @@ mod tests {
c.write_strobe(1);
c.write_strobe(0);
for _ in 0..8 {
- assert_eq!(c.read(), 0);
+ assert_eq!(c.read(false), 0);
}
// After 8 reads, ROMs see 1s.
for _ in 0..4 {
- assert_eq!(c.read(), 1);
+ assert_eq!(c.read(false), 1);
}
}
@@ -152,7 +197,7 @@ mod tests {
// A, B, Select, Start, Up, Down, Left, Right
let expected = [1u8, 0, 1, 0, 0, 1, 0, 0];
for &want in &expected {
- assert_eq!(c.read(), want);
+ assert_eq!(c.read(false), want);
}
}
@@ -162,7 +207,7 @@ mod tests {
c.set_buttons(Buttons::A);
c.write_strobe(1);
for _ in 0..16 {
- assert_eq!(c.read(), 1, "while strobing, $4016 returns A bit");
+ assert_eq!(c.read(false), 1, "while strobing, $4016 returns A bit");
}
}
@@ -171,9 +216,9 @@ mod tests {
let mut c = Controller::new();
c.write_strobe(1);
c.set_buttons(Buttons::A);
- assert_eq!(c.read(), 1);
+ assert_eq!(c.read(false), 1);
c.set_buttons(Buttons::empty());
- assert_eq!(c.read(), 0);
+ assert_eq!(c.read(false), 0);
}
#[test]
@@ -184,12 +229,12 @@ mod tests {
c.write_strobe(0);
// Change buttons mid-readout — should NOT affect this scan.
c.set_buttons(Buttons::A | Buttons::B);
- assert_eq!(c.read(), 1, "A");
- assert_eq!(c.read(), 0, "B (latched as not pressed)");
+ assert_eq!(c.read(false), 1, "A");
+ assert_eq!(c.read(false), 0, "B (latched as not pressed)");
// New strobe latches the new state.
c.write_strobe(1);
c.write_strobe(0);
- assert_eq!(c.read(), 1, "A");
- assert_eq!(c.read(), 1, "B (now latched as pressed)");
+ assert_eq!(c.read(false), 1, "A");
+ assert_eq!(c.read(false), 1, "B (now latched as pressed)");
}
}
diff --git a/crates/rustynes-cosim/Cargo.lock b/crates/rustynes-cosim/Cargo.lock
index 3b421d7c..a3aa85c5 100644
--- a/crates/rustynes-cosim/Cargo.lock
+++ b/crates/rustynes-cosim/Cargo.lock
@@ -98,7 +98,7 @@ dependencies = [
[[package]]
name = "rustynes-apu"
-version = "2.6.4"
+version = "2.6.5"
dependencies = [
"bitflags",
"libm",
@@ -107,7 +107,7 @@ dependencies = [
[[package]]
name = "rustynes-core"
-version = "2.6.4"
+version = "2.6.5"
dependencies = [
"bitflags",
"lz4_flex",
@@ -121,7 +121,7 @@ dependencies = [
[[package]]
name = "rustynes-cosim"
-version = "2.6.4"
+version = "2.6.5"
dependencies = [
"rustynes-core",
"sha2",
@@ -129,7 +129,7 @@ dependencies = [
[[package]]
name = "rustynes-cpu"
-version = "2.6.4"
+version = "2.6.5"
dependencies = [
"bitflags",
"thiserror",
@@ -137,7 +137,7 @@ dependencies = [
[[package]]
name = "rustynes-mappers"
-version = "2.6.4"
+version = "2.6.5"
dependencies = [
"bitflags",
"rustynes-apu",
@@ -146,7 +146,7 @@ dependencies = [
[[package]]
name = "rustynes-ppu"
-version = "2.6.4"
+version = "2.6.5"
dependencies = [
"bitflags",
"libm",
diff --git a/crates/rustynes-cosim/Cargo.toml b/crates/rustynes-cosim/Cargo.toml
index 2f1e0f85..e0f9a158 100644
--- a/crates/rustynes-cosim/Cargo.toml
+++ b/crates/rustynes-cosim/Cargo.toml
@@ -8,7 +8,7 @@ description = "RustyNES as a co-simulation oracle for an external HDL device-und
# The duplication is PINNED, not merely noticed: `cosim_manifest_audit.rs` in
# `rustynes-test-harness` asserts these values still match the workspace's, so
# drift fails a test instead of accumulating quietly.
-version = "2.6.4"
+version = "2.6.5"
edition = "2024"
rust-version = "1.96"
license = "GPL-3.0-or-later"
@@ -37,6 +37,29 @@ pedantic = { level = "warn", priority = -1 }
nursery = { level = "warn", priority = -1 }
undocumented_unsafe_blocks = "warn"
+[features]
+# The ONE optional feature here, and it is OFF BY DEFAULT, which the crate's own
+# test suite decided rather than taste.
+#
+# It is a DIAGNOSTIC, and its `rustynes-core` counterpart compiles out the v2.2.3
+# fast dot path -- so enabling it by default changes what the DEFAULT build of
+# this crate runs. `tests/fast_path_does_not_bypass_the_fetch_trace.rs` exists to
+# prove the fast path records the same fetches as the general one, and it does
+# that by running the ROM with the fast path on and off. Made default, the
+# feature compiles the fast path away and that test fails: the property it
+# guards becomes unobservable in the configuration `cargo test` uses.
+#
+# So the default build is the CONTROL, and the diagnostic is opt-in:
+# cargo run --features ppu-state-trace --bin nes_golden_export -- ... \
+# --ppu-state-trace CAP --pst-frames A:B
+#
+# That ordering is also what makes the output-neutrality claim in the dependency
+# comment below testable rather than asserted -- exporting a golden with and
+# without the feature gives byte-identical `obs.bin`, `index_fb.bin`, `ram.bin`
+# and `ckpt.bin`, VERIFIED on `ppu-misc-2007-stress` at 60 frames.
+default = []
+ppu-state-trace = ["rustynes-core/ppu-state-trace"]
+
[dependencies]
# The trace features are NOT optional here. This crate exists to emit them, and a
# build without them would produce an oracle that silently exports nothing —
@@ -70,6 +93,19 @@ rustynes-core = { path = "../rustynes-core", features = [
# assumed: exporting a golden with and without it produces byte-identical
# `obs.bin`, `index_fb.bin` and `ram.bin`.
"debug-hooks",
+ # v2.6.5. The per-dot PPU state fixture, used STRICTLY as a DIAGNOSTIC and
+ # never as a gate -- `RustyNES_MiSTer/docs/rung3-ppu.md` fixes that
+ # partition, because these fields are RustyNES's decomposition of the chip
+ # and not pin-observable facts. It exists here to answer questions the
+ # pin-observable surfaces cannot, such as which internal value a `$2007`
+ # read composed its address from.
+ #
+ # UNLIKE the features above, this one is NOT obviously inert: it compiles
+ # out the v2.2.3 fast dot path so the hook can see every dot. That path is
+ # differential-tested bit-identical, so the OUTPUT should not move -- and
+ # "should" is not the bar this file sets. Verified the same way
+ # `debug-hooks` was: a golden exported with and without the feature is
+ # byte-identical in `obs.bin`, `index_fb.bin` and `ram.bin`.
] }
sha2 = { version = "0.11", default-features = false }
diff --git a/crates/rustynes-cosim/src/bin/nes_golden_export.rs b/crates/rustynes-cosim/src/bin/nes_golden_export.rs
index d95f74e4..3ff7c610 100644
--- a/crates/rustynes-cosim/src/bin/nes_golden_export.rs
+++ b/crates/rustynes-cosim/src/bin/nes_golden_export.rs
@@ -41,6 +41,21 @@ use sha2::{Digest, Sha256};
const INDEX_FB_LEN: usize = 256 * 240;
const RAM_LEN: usize = 2048;
+/// The `--ppu-state-trace` window, as four named fields rather than a nest of
+/// tuples. Named because the tuple form was genuinely unreadable -- three of its
+/// four members are pairs and two of those are optional -- and because a struct
+/// with `None` defaults keeps the test builders below to one line each.
+///
+/// `scanlines` is `i16` rather than `u16` because the pre-render line is -1.
+#[cfg(feature = "ppu-state-trace")]
+#[derive(Clone, Copy)]
+struct PpuStateTraceArgs {
+ capacity: usize,
+ frames: (u32, u32),
+ scanlines: Option<(i16, i16)>,
+ dots: Option<(u16, u16)>,
+}
+
struct Args {
rom: PathBuf,
out: PathBuf,
@@ -63,6 +78,13 @@ struct Args {
/// Capacity for the per-dot PPU bus-address capture (rung 3's v2.5.4 gate).
fetch_trace: Option,
apu_trace: Option,
+ /// v2.6.5 — the per-dot PPU state fixture. A **DIAGNOSTIC**, never a gate:
+ /// `RustyNES_MiSTer/docs/rung3-ppu.md` fixes that partition, and these
+ /// fields are this emulator's decomposition of the chip rather than
+ /// pin-observable facts. Carried as (capacity, frames, scanlines, dots) so
+ /// the window can be narrowed to the handful of dots a question is about.
+ #[cfg(feature = "ppu-state-trace")]
+ ppu_state_trace: Option,
checkpoint_interval: u64,
/// v2.5.1 — rung 2's interrupt sweep. Instruction-indexed, not
/// cycle-indexed: this side cannot assert a pin mid-instruction, so a
@@ -73,6 +95,32 @@ struct Args {
inject_hold: u64,
}
+/// The `--ppu-state-trace` capacity, refused outside a usable range.
+///
+/// `PPU_STATE_TRACE_MAX` is 40 million records — a whole 4500-frame `AccuracyCoin`
+/// run is ~134 M dots, so this is deliberately below "capture everything": the
+/// window flags exist because capturing everything is not the intended use, and
+/// a cap that permits it invites the allocation that killed the exporter.
+#[cfg(feature = "ppu-state-trace")]
+const PPU_STATE_TRACE_MAX: usize = 40_000_000;
+
+#[cfg(feature = "ppu-state-trace")]
+fn check_pst_cap(cap: usize) -> usize {
+ if cap == 0 {
+ eprintln!(
+ "--ppu-state-trace needs a non-zero capacity: 0 would write a header and no records, which reads as a successful capture"
+ );
+ std::process::exit(2);
+ }
+ if cap > PPU_STATE_TRACE_MAX {
+ eprintln!(
+ "--ppu-state-trace capacity {cap} exceeds {PPU_STATE_TRACE_MAX}; narrow the window with --pst-frames / --pst-scanlines / --pst-dots instead"
+ );
+ std::process::exit(2);
+ }
+ cap
+}
+
fn usage() -> ! {
eprintln!(
"usage: nes_golden_export --rom --out [--seed N] [--frames N]\n\
@@ -80,7 +128,9 @@ fn usage() -> ! {
\x20 [--fetch-trace CAP] [--apu-trace CAP] [--checkpoint-interval N]\n\
\x20 [--inject-instructions N] [--inject-hold N]\n\
\x20 [--inject-nmi-at N] [--inject-irq-at N]\n\
- \x20 [--press-start A:B]"
+ \x20 [--press-start A:B]\n\
+ \x20 [--ppu-state-trace CAP --pst-frames A:B\n\
+ \x20 [--pst-scanlines A:B] [--pst-dots A:B]]"
);
std::process::exit(2)
}
@@ -127,6 +177,10 @@ fn parse_args() -> Args {
let (mut boot_trace, mut irq_trace) = (None, None);
let mut fetch_trace: Option = None;
let mut apu_trace: Option = None;
+ #[cfg(feature = "ppu-state-trace")]
+ let (mut pst_cap, mut pst_frames) = (None::, None::<(u32, u32)>);
+ #[cfg(feature = "ppu-state-trace")]
+ let (mut pst_scanlines, mut pst_dots) = (None::<(i16, i16)>, None::<(u16, u16)>);
let mut checkpoint_interval = rustynes_cosim::checkpoint::DEFAULT_INTERVAL;
let (mut inject_instructions, mut inject_hold) = (0u64, 1u64);
let (mut inject_nmi_at, mut inject_irq_at) = (None, None);
@@ -212,6 +266,45 @@ fn parse_args() -> Args {
apu_trace = Some(parse_apu_cap(need(i)));
i += 2;
}
+ #[cfg(not(feature = "ppu-state-trace"))]
+ "--ppu-state-trace" | "--pst-frames" | "--pst-scanlines" | "--pst-dots" => {
+ // REFUSED, not ignored. A control build that accepted the flag
+ // and produced no `ppu_state.csv` would read as "the window held
+ // no dots" -- the same confusion the armed-but-empty warning
+ // exists to prevent, one level up.
+ eprintln!(
+ "{} needs a build with --features ppu-state-trace \
+ (it is off by default; see Cargo.toml)",
+ argv[i]
+ );
+ usage()
+ }
+ #[cfg(feature = "ppu-state-trace")]
+ "--ppu-state-trace" => {
+ // BOUNDED AT BOTH ENDS. Zero produced a header-only CSV that
+ // looks like a successful capture, and an arbitrary `usize`
+ // reached `Vec::with_capacity` and aborted the exporter rather
+ // than reporting anything. A diagnostic that can only be empty
+ // or fatal is worse than one that refuses the argument.
+ let cap: usize = need(i).parse().unwrap_or_else(|_| usage());
+ pst_cap = Some(check_pst_cap(cap));
+ i += 2;
+ }
+ #[cfg(feature = "ppu-state-trace")]
+ "--pst-frames" => {
+ pst_frames = Some(parse_pair_u32(need(i)).unwrap_or_else(|| usage()));
+ i += 2;
+ }
+ #[cfg(feature = "ppu-state-trace")]
+ "--pst-scanlines" => {
+ pst_scanlines = Some(parse_pair_i16(need(i)).unwrap_or_else(|| usage()));
+ i += 2;
+ }
+ #[cfg(feature = "ppu-state-trace")]
+ "--pst-dots" => {
+ pst_dots = Some(parse_pair_u16(need(i)).unwrap_or_else(|| usage()));
+ i += 2;
+ }
"--checkpoint-interval" => {
checkpoint_interval = need(i).parse().unwrap_or_else(|_| usage());
if checkpoint_interval == 0 {
@@ -233,6 +326,25 @@ fn parse_args() -> Args {
irq_trace,
fetch_trace,
apu_trace,
+ // REFUSED rather than defaulted: a frameless window would record every
+ // frame of the run, which at ~46 kB a frame is gigabytes for the
+ // 4500-frame goldens -- and the whole point of this trace is a handful
+ // of dots. An option that silently means "everything" is how a
+ // diagnostic becomes an out-of-disk.
+ #[cfg(feature = "ppu-state-trace")]
+ ppu_state_trace: match (pst_cap, pst_frames) {
+ (Some(capacity), Some(frames)) => Some(PpuStateTraceArgs {
+ capacity,
+ frames,
+ scanlines: pst_scanlines,
+ dots: pst_dots,
+ }),
+ (None, None) => None,
+ _ => {
+ eprintln!("--ppu-state-trace and --pst-frames must be given together");
+ usage()
+ }
+ },
checkpoint_interval,
inject_instructions,
inject_nmi_at,
@@ -482,6 +594,83 @@ fn parse_apu_cap(raw: &str) -> usize {
cap
}
+/// Parse an inclusive `A:B` range. Returns `None` for anything malformed or
+/// inverted, so the caller can refuse rather than silently widen the window.
+///
+/// Three monomorphic wrappers rather than one generic: the three ranges have
+/// three different element types (`u32` frames, `i16` scanlines carrying the
+/// `-1` pre-render line, `u16` dots), and a generic over `FromStr + PartialOrd`
+/// buys nothing at three call sites.
+/// Arm the per-dot PPU state fixture if it was requested.
+///
+/// A free function with a no-op twin rather than a `cfg` block inside `main`:
+/// `main` is already at the pedantic line limit, and a `cfg`-gated block there
+/// makes its length depend on which features are enabled -- so the lint would
+/// fire in one configuration and not the other.
+#[cfg(feature = "ppu-state-trace")]
+fn arm_ppu_state_trace(o: &mut Oracle, args: &Args) {
+ if let Some(w) = args.ppu_state_trace {
+ let (f0, f1) = w.frames;
+ o.enable_ppu_state_trace(
+ w.capacity,
+ f0..=f1,
+ w.scanlines.map(|(a, b)| a..=b),
+ w.dots.map(|(a, b)| a..=b),
+ );
+ }
+}
+
+/// The control build's twin. Present so `main` reads identically either way;
+/// the argument parser still REFUSES the flags rather than ignoring them, so a
+/// control build cannot silently accept a request it cannot serve.
+#[cfg(not(feature = "ppu-state-trace"))]
+const fn arm_ppu_state_trace(_o: &mut Oracle, _args: &Args) {}
+
+/// Arm every trace the arguments asked for.
+///
+/// One place rather than five blocks in `main`: they are the same kind of step,
+/// `main` sits at the pedantic line limit, and keeping them together means
+/// adding a sixth trace does not push an unrelated function over it.
+fn arm_traces(o: &mut Oracle, args: &Args) {
+ if let Some((start, end)) = args.boot_trace {
+ // Capacity is the window, not the whole run: a bounded window is the
+ // design, because a full AccuracyCoin run would be ~1 GB of records.
+ let cap = usize::try_from(end.saturating_sub(start) + 1).unwrap_or(usize::MAX);
+ o.enable_cpu_boot_trace(cap, start, end);
+ }
+ if let Some(cap) = args.irq_trace {
+ o.enable_irq_trace(cap);
+ }
+ if let Some(cap) = args.fetch_trace {
+ o.enable_fetch_trace(cap);
+ }
+ if let Some(cap) = args.apu_trace {
+ o.enable_apu_trace(cap);
+ }
+ arm_ppu_state_trace(o, args);
+}
+
+#[cfg(feature = "ppu-state-trace")]
+fn parse_pair_u32(raw: &str) -> Option<(u32, u32)> {
+ let (a, b) = raw.split_once(':')?;
+ let (a, b) = (a.parse::().ok()?, b.parse::().ok()?);
+ (a <= b).then_some((a, b))
+}
+
+#[cfg(feature = "ppu-state-trace")]
+fn parse_pair_i16(raw: &str) -> Option<(i16, i16)> {
+ let (a, b) = raw.split_once(':')?;
+ let (a, b) = (a.parse::().ok()?, b.parse::().ok()?);
+ (a <= b).then_some((a, b))
+}
+
+#[cfg(feature = "ppu-state-trace")]
+fn parse_pair_u16(raw: &str) -> Option<(u16, u16)> {
+ let (a, b) = raw.split_once(':')?;
+ let (a, b) = (a.parse::().ok()?, b.parse::().ok()?);
+ (a <= b).then_some((a, b))
+}
+
/// Write rung 4's per-CPU-cycle channel-level golden, when armed.
///
/// Fails loudly on a dropped record for the same reason `write_fetch_trace`
@@ -522,6 +711,43 @@ fn write_apu_trace(o: &mut Oracle, base: &Path) {
}
}
+/// Write the per-dot PPU state fixture if it was armed.
+///
+/// Written LAST and unconditionally reported: a diagnostic that silently
+/// produced nothing would be read as "the window held no dots" rather than "the
+/// trace was never armed", which is the exact confusion this project keeps
+/// paying for.
+///
+/// Extracted from `main` for the same reason as `arm_ppu_state_trace`, and with
+/// the same no-op twin, so `main`'s length does not depend on the feature set.
+#[cfg(feature = "ppu-state-trace")]
+fn write_ppu_state_trace(o: &mut Oracle, args: &Args, base: &Path) {
+ if args.ppu_state_trace.is_none() {
+ return;
+ }
+ match o.take_ppu_state_trace_csv() {
+ Some((csv, dropped)) => {
+ let rows = csv.lines().count().saturating_sub(1);
+ write(&suffixed(base, "ppu_state.csv"), csv.as_bytes());
+ println!(" ppu_state.csv: {rows} dot records (DIAGNOSTIC, not a gate)");
+ if dropped != 0 {
+ // Loud, and on stderr: the file exists and parses, so nothing
+ // downstream can tell it is a narrower window than the flags
+ // asked for unless this says so here.
+ eprintln!(
+ " WARNING: the PPU state trace hit its capacity and DROPPED {dropped} record(s). \
+ The window is narrower than --pst-frames/--pst-scanlines/--pst-dots requested; \
+ raise --ppu-state-trace or narrow the window."
+ );
+ }
+ }
+ None => eprintln!(" WARNING: PPU state trace was armed but returned nothing"),
+ }
+}
+
+#[cfg(not(feature = "ppu-state-trace"))]
+const fn write_ppu_state_trace(_o: &mut Oracle, _args: &Args, _base: &Path) {}
+
/// Write the per-dot PPU bus-address golden, when the trace was armed.
///
/// A DROPPED count is a TRUNCATED window, and a comparison over one that does
@@ -656,21 +882,7 @@ fn main() {
// compute them.
let ram_init = o.nes().bus().ram_bytes().to_vec();
- if let Some((start, end)) = args.boot_trace {
- // Capacity is the window, not the whole run: a bounded window is the
- // design, because a full AccuracyCoin run would be ~1 GB of records.
- let cap = usize::try_from(end.saturating_sub(start) + 1).unwrap_or(usize::MAX);
- o.enable_cpu_boot_trace(cap, start, end);
- }
- if let Some(cap) = args.irq_trace {
- o.enable_irq_trace(cap);
- }
- if let Some(cap) = args.fetch_trace {
- o.enable_fetch_trace(cap);
- }
- if let Some(cap) = args.apu_trace {
- o.enable_apu_trace(cap);
- }
+ arm_traces(&mut o, &args);
// `advance_frames`, not a `run_frame()` loop: the first call after power-on
// is swallowed by the frame_complete latch the reset sequence leaves set, so
@@ -718,6 +930,7 @@ fn main() {
write(&suffixed(&base, "index_fb.bin"), &fb_bytes);
write_fetch_trace(&mut o, &base);
+ write_ppu_state_trace(&mut o, &args, &base);
write_apu_trace(&mut o, &base);
// Checked BEFORE the write, and checked on `ram_init` specifically. The
@@ -825,6 +1038,8 @@ mod tests {
// default and giving it one would invent a "valid" argument set that
// no invocation produces.
let base = || Args {
+ #[cfg(feature = "ppu-state-trace")]
+ ppu_state_trace: None,
rom: std::path::PathBuf::from("/dev/null"),
out: std::path::PathBuf::from("/tmp"),
seed: 0,
diff --git a/crates/rustynes-cosim/src/lib.rs b/crates/rustynes-cosim/src/lib.rs
index f9a4af61..cf9726e1 100644
--- a/crates/rustynes-cosim/src/lib.rs
+++ b/crates/rustynes-cosim/src/lib.rs
@@ -67,6 +67,8 @@ use core::ffi::{c_char, c_int, c_uchar, c_uint, c_ulonglong, c_void};
use rustynes_core::Nes;
use rustynes_core::cpu_boot_trace::{CpuBootTrace, CpuBootTraceConfig};
use rustynes_core::rustynes_ppu::fetch_trace::FetchTrace;
+#[cfg(feature = "ppu-state-trace")]
+use rustynes_core::rustynes_ppu::state_trace::{PpuStateTrace, PpuTraceConfig};
/// Bytes per rung-4 channel-level record. See [`Oracle::take_apu_trace`].
const APU_REC_LEN: usize = 16;
@@ -377,6 +379,56 @@ impl Oracle {
.enable_fetch_trace(FetchTrace::with_capacity(capacity));
}
+ /// Arm the per-dot PPU state fixture — a **DIAGNOSTIC**, never a gate.
+ ///
+ /// `RustyNES_MiSTer/docs/rung3-ppu.md` fixes that partition before any of
+ /// it was written: these fields are this emulator's decomposition of the
+ /// chip, not pin-observable facts, so a DUT can be *investigated* against
+ /// them and must never be FAILED on them.
+ ///
+ /// The window is narrow on purpose. Every dot of every frame is ~46 kB per
+ /// frame even filtered to the visible field, and the questions this answers
+ /// are always about a handful of dots around one cycle.
+ #[cfg(feature = "ppu-state-trace")]
+ pub fn enable_ppu_state_trace(
+ &mut self,
+ capacity: usize,
+ frames: core::ops::RangeInclusive,
+ scanlines: Option>,
+ dots: Option>,
+ ) {
+ let cfg = PpuTraceConfig {
+ frame_range: frames,
+ scanline_range: scanlines,
+ dot_range: dots,
+ };
+ self.nes
+ .bus_mut()
+ .ppu_mut()
+ .enable_state_trace(PpuStateTrace::with_capacity(capacity, cfg));
+ }
+
+ /// The PPU state trace as CSV, or `None` if unarmed.
+ ///
+ /// CSV rather than the binary form because this is read by a human at a
+ /// named cycle, not compared by a tool. A gate would want the binary; there
+ /// is deliberately no gate.
+ ///
+ /// Returns the CSV **and how many records did NOT fit**, for the same reason
+ /// `take_fetch_trace` does: a capture that silently stopped at capacity
+ /// produces a valid-looking file describing a window narrower than the one
+ /// asked for, and every conclusion drawn from it inherits that. The fetch
+ /// trace already reported this and the state trace did not -- the asymmetry
+ /// was the finding.
+ #[cfg(feature = "ppu-state-trace")]
+ pub fn take_ppu_state_trace_csv(&mut self) -> Option<(String, u64)> {
+ self.nes
+ .bus_mut()
+ .ppu_mut()
+ .take_state_trace()
+ .map(|t| (t.to_csv(), t.overflow()))
+ }
+
/// The fetch trace in its binary interchange format, or `None` if unarmed.
///
/// Also reports how many reads did NOT fit, because a comparison over a
diff --git a/crates/rustynes-cosim/tests/fast_path_does_not_bypass_the_fetch_trace.rs b/crates/rustynes-cosim/tests/fast_path_does_not_bypass_the_fetch_trace.rs
index 4d7627f8..c983d087 100644
--- a/crates/rustynes-cosim/tests/fast_path_does_not_bypass_the_fetch_trace.rs
+++ b/crates/rustynes-cosim/tests/fast_path_does_not_bypass_the_fetch_trace.rs
@@ -1,5 +1,20 @@
//! The fast dot path must not change what the fetch trace records.
//!
+//! NOT APPLICABLE UNDER `ppu-state-trace`, and gated out rather than left to
+//! fail. That feature compiles `Ppu::tick_visible_render_fast` out entirely so
+//! the per-dot hook sees every dot, so there is no fast path to compare against
+//! the general one -- and this test says so itself, refusing with "the fast
+//! path ran only 0 times ... proving nothing" rather than reporting a vacuous
+//! pass. That refusal is the guard working, but it also made
+//! `cargo test --features ppu-state-trace` red for a property that does not
+//! exist in that build, which is not a defect to report every run.
+//!
+//! Gating it here is what lets CI run the crate's tests WITH the feature, which
+//! is the only way `state_trace_records_carry_their_cpu_cycle` -- feature-gated
+//! itself -- ever executes. Before this it never ran in CI at all: a test
+//! written to catch a present-but-constant field, unreachable by the gate.
+#![cfg(not(feature = "ppu-state-trace"))]
+//!
//! A review of #450 raised this as a blocking correctness finding: that
//! `ppu-state-trace` disables `Ppu::tick_visible_render_fast` while
//! `ppu-fetch-trace` does not, so a build with only the fetch trace would run
diff --git a/crates/rustynes-cosim/tests/state_trace_records_carry_their_cpu_cycle.rs b/crates/rustynes-cosim/tests/state_trace_records_carry_their_cpu_cycle.rs
new file mode 100644
index 00000000..f7ccd7b0
--- /dev/null
+++ b/crates/rustynes-cosim/tests/state_trace_records_carry_their_cpu_cycle.rs
@@ -0,0 +1,96 @@
+//! The per-dot PPU state trace must stamp each record with the CPU cycle it
+//! belongs to.
+//!
+//! Schema 4 added `cpu_cycle` for one reason: without it a record can only be
+//! located by `frame`/`scanline`/`dot`, and none of those is comparable across
+//! two consoles on its own. Diagnosing a single `AccuracyCoin` entry produced
+//! three wrong conclusions in a row for exactly that reason — frames had to be
+//! matched by what they CONTAIN, dots by a relationship measured separately on
+//! each side, and cycles not at all.
+//!
+//! A field that is present but always zero would reinstate the whole problem
+//! while looking fixed, which is why this test asserts the VALUES rather than
+//! the column's existence. It is here rather than in `rustynes-ppu` because the
+//! PPU cannot populate it: the bus stamps it once per CPU cycle, so only an
+//! assembled console exercises the path.
+#![cfg(feature = "ppu-state-trace")]
+
+use rustynes_cosim::Oracle;
+
+/// `nestest.nes`, committed in this repository under `tests/roms/`. Resolved
+/// from `CARGO_MANIFEST_DIR` so the test cannot pass by failing to find its
+/// subject when run from a different working directory.
+fn rom() -> Vec {
+ let p = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
+ .join("../../tests/roms/nestest/nestest.nes");
+ std::fs::read(&p).unwrap_or_else(|e| panic!("read {}: {e}", p.display()))
+}
+
+#[test]
+fn every_record_carries_a_cpu_cycle_that_advances_with_the_run() {
+ let mut o = Oracle::new(&rom(), 0).expect("parse rom");
+ // Two whole frames, unfiltered by scanline: enough dots that a stuck value
+ // cannot hide behind a narrow window.
+ o.enable_ppu_state_trace(200_000, 2..=3, None, None);
+ o.advance_frames(5);
+
+ let (csv, dropped) = o
+ .take_ppu_state_trace_csv()
+ .expect("the trace was armed, so it must produce a table");
+ // A truncated capture would make every assertion below a statement about a
+ // narrower window than the one asked for, so this is checked rather than
+ // assumed -- the capacity is deliberately far above what five frames of two
+ // scanlines can produce.
+ assert_eq!(
+ dropped, 0,
+ "the trace dropped {dropped} record(s): the window is narrower than it asks for"
+ );
+ let mut lines = csv.lines();
+ let header = lines.next().expect("header");
+ let idx = header
+ .split(',')
+ .position(|c| c == "cpu_cycle")
+ .expect("header names cpu_cycle");
+
+ let cycles: Vec = lines
+ .map(|l| {
+ l.split(',')
+ .nth(idx)
+ .expect("row has a cpu_cycle column")
+ .parse()
+ .expect("cpu_cycle parses as a number")
+ })
+ .collect();
+
+ assert!(
+ !cycles.is_empty(),
+ "the window produced no records, so this test asserts nothing"
+ );
+
+ // NOT merely non-zero. A field wired to a constant would pass that, and the
+ // failure this guards is precisely a value that looks present and says
+ // nothing.
+ assert!(
+ cycles.iter().any(|&c| c != 0),
+ "every record carries cpu_cycle = 0, so the bus is not stamping it"
+ );
+ let (lo, hi) = (cycles[0], *cycles.last().expect("non-empty"));
+ assert!(
+ hi > lo,
+ "cpu_cycle does not advance across the window: first {lo}, last {hi}"
+ );
+
+ // Monotonic, and never running ahead of the dots it labels: a record is
+ // stamped BEFORE its cycle's dots are ticked, so consecutive records step
+ // by 0 or 1 and never jump.
+ for w in cycles.windows(2) {
+ let step = w[1] - w[0];
+ assert!(
+ step <= 1,
+ "cpu_cycle jumped by {step} between consecutive dots ({} -> {}); \
+ each CPU cycle covers three dots, so the step is 0 or 1",
+ w[0],
+ w[1]
+ );
+ }
+}
diff --git a/crates/rustynes-libretro/rustynes_libretro.info b/crates/rustynes-libretro/rustynes_libretro.info
index 6008b66a..d4fd8dde 100644
--- a/crates/rustynes-libretro/rustynes_libretro.info
+++ b/crates/rustynes-libretro/rustynes_libretro.info
@@ -5,7 +5,7 @@ supported_extensions = "nes|fds"
corename = "RustyNES"
license = "GPLv3+"
permissions = ""
-display_version = "v2.6.4"
+display_version = "v2.6.5"
categories = "Emulator"
# Hardware Information
diff --git a/crates/rustynes-ppu/src/ppu.rs b/crates/rustynes-ppu/src/ppu.rs
index 7b60d855..5e1cb502 100644
--- a/crates/rustynes-ppu/src/ppu.rs
+++ b/crates/rustynes-ppu/src/ppu.rs
@@ -1054,6 +1054,15 @@ pub struct Ppu {
/// `docs/adr/0005-ppu-state-trace.md`.
#[cfg(feature = "ppu-state-trace")]
pub(crate) state_trace: Option,
+
+ /// The CPU cycle the dots being ticked belong to, stamped into every state
+ /// record. Written once per CPU cycle by the bus, before those dots run.
+ ///
+ /// Feature-gated, so the default build carries neither the field nor the
+ /// store: this is bookkeeping for a diagnostic, and the tick path is the
+ /// hottest loop in the emulator.
+ #[cfg(feature = "ppu-state-trace")]
+ pub(crate) trace_cpu_cycle: u64,
/// Per-dot PPU bus address capture. See [`crate::fetch_trace`].
#[cfg(feature = "ppu-fetch-trace")]
pub(crate) fetch_trace: Option,
@@ -1390,6 +1399,8 @@ impl Ppu {
fast_dotloop: true,
#[cfg(feature = "ppu-state-trace")]
state_trace: None,
+ #[cfg(feature = "ppu-state-trace")]
+ trace_cpu_cycle: 0,
#[cfg(feature = "ppu-fetch-trace")]
fetch_trace: None,
#[cfg(feature = "hd-pack")]
@@ -1792,6 +1803,17 @@ impl Ppu {
self.state_trace = Some(trace);
}
+ /// Stamp the CPU cycle that the dots ticked next belong to.
+ ///
+ /// Called by the bus at the START of each CPU cycle, so a record carries
+ /// the number of the cycle it is part of rather than the following one —
+ /// the off-by-one a co-simulation probe made on exactly this question, and
+ /// which produced a finding that had to be retracted.
+ #[cfg(feature = "ppu-state-trace")]
+ pub const fn set_trace_cpu_cycle(&mut self, cycle: u64) {
+ self.trace_cpu_cycle = cycle;
+ }
+
/// Install a per-dot PPU bus address capture.
///
/// The address bus is pin-observable, which is what makes it usable as a
@@ -1875,6 +1897,8 @@ impl Ppu {
oam_fnv1a64: crate::state_trace::fnv1a64(&self.oam),
nmi_line: self.nmi_line,
oam_bus_copybuffer: self.oam_data_bus_observed(),
+ data_buffer: self.data_buffer,
+ cpu_cycle: self.trace_cpu_cycle,
}
}
diff --git a/crates/rustynes-ppu/src/state_trace.rs b/crates/rustynes-ppu/src/state_trace.rs
index c58118cf..fbae4868 100644
--- a/crates/rustynes-ppu/src/state_trace.rs
+++ b/crates/rustynes-ppu/src/state_trace.rs
@@ -75,7 +75,10 @@ use core::ops::RangeInclusive;
/// Version history:
///
/// * `1` (2026-05-20): initial Session-10 schema.
-pub const PPU_TRACE_SCHEMA_VERSION: u16 = 2;
+/// * `2`: added `oam_bus_copybuffer`.
+/// * `3` (2026-08-28): added `data_buffer`, the `$2007` read buffer.
+/// * `4` (2026-08-28): added `cpu_cycle`, so a record can be located.
+pub const PPU_TRACE_SCHEMA_VERSION: u16 = 4;
/// Magic bytes prefixing every binary trace file. ASCII
/// "`RUSTYNES_PPU`" — distinguishes our format from Mesen2 native
@@ -85,7 +88,7 @@ pub const BINARY_MAGIC: &[u8; 12] = b"RUSTYNES_PPU";
/// Length of a single [`PpuStateRecord`] in the packed binary layout.
///
/// Stable for the lifetime of [`PPU_TRACE_SCHEMA_VERSION`].
-pub const RECORD_SIZE: usize = 114;
+pub const RECORD_SIZE: usize = 123;
/// Header length (magic + 2-byte schema version + 2-byte
/// reserved-for-flags). Records start at this offset.
@@ -229,6 +232,41 @@ pub struct PpuStateRecord {
/// does not expose what the gate reads sends you to the wrong place
/// confidently.
pub oam_bus_copybuffer: u8,
+
+ // === PPUDATA read buffer ===
+ /// The `$2007` read buffer — what the NEXT `$2007` read will return.
+ ///
+ /// Added at schema 3 for the rung-3 `$2007 Stress Test` residual, and the
+ /// reason is the same one that added `oam_bus_copybuffer` at schema 2: the
+ /// question was *when* and *from where* the buffer is filled, and this
+ /// fixture exposed every register the fill is composed from except the
+ /// result. Both consoles read `$2007` at identical cycles there, so the
+ /// difference is which byte landed — a quantity that was, until now,
+ /// observable only after the CPU had already read it back out.
+ ///
+ /// Per-dot capture is what makes it useful: the fill happens four PPU
+ /// cycles after the CPU access ends, so a once-per-access sample shows the
+ /// PREVIOUS read's byte and says nothing about this one.
+ pub data_buffer: u8,
+
+ // === Location ===
+ /// The CPU cycle this dot belongs to, as the bus counts them.
+ ///
+ /// Added at schema 4 because its ABSENCE produced three wrong conclusions
+ /// in a row while diagnosing one `AccuracyCoin` entry. Without it a record
+ /// can only be located by `frame`/`scanline`/`dot`, and none of those is
+ /// comparable across consoles on its own: frames had to be matched by what
+ /// they CONTAIN (a frame number is not a cycle count divided by a frame
+ /// length), dots by an internal relationship measured separately on each
+ /// side, and cycles not at all. Every other trace this project compares —
+ /// `obs.bin`, the IRQ trace, the checkpoint chain — is cycle-keyed, and
+ /// this one was the exception.
+ ///
+ /// Set once per CPU cycle, BEFORE that cycle's dots are ticked, so every
+ /// dot carries the number of the cycle it belongs to rather than the next
+ /// one's. It counts bus-side cycles, DMC DMA included, which is the same
+ /// clock `obs.bin` records — so the two can be joined directly.
+ pub cpu_cycle: u64,
}
/// Total record size as packed by [`PpuStateRecord::to_bytes`].
@@ -254,8 +292,9 @@ const fn compute_record_size() -> usize {
let bg = 2 + 2 + 2 + 2 + 1 + 1 + 1 + 1;
// 32-byte secondary OAM
let secondary = 32;
- // OAM FNV-1a (8 bytes) + NMI line (1 byte) + OAM data bus (1 byte)
- let tail = 8 + 1 + 1;
+ // OAM FNV-1a (8 bytes) + NMI line (1) + OAM data bus (1)
+ // + PPUDATA read buffer (1) + CPU cycle (8)
+ let tail = 8 + 1 + 1 + 1 + 8;
anchor + regs + scroll + eval + spr + bg + secondary + tail
}
@@ -343,6 +382,8 @@ impl PpuStateRecord {
copy_u64(&mut buf, &mut i, self.oam_fnv1a64);
copy_bool(&mut buf, &mut i, self.nmi_line);
copy_u8(&mut buf, &mut i, self.oam_bus_copybuffer);
+ copy_u8(&mut buf, &mut i, self.data_buffer);
+ copy_u64(&mut buf, &mut i, self.cpu_cycle);
debug_assert_eq!(i, RECORD_SIZE, "PpuStateRecord packer underflow/overflow");
buf
@@ -448,6 +489,8 @@ impl PpuStateRecord {
let oam_fnv1a64 = read_u64(buf, &mut i);
let nmi_line = read_bool(buf, &mut i);
let oam_bus_copybuffer = read_u8(buf, &mut i);
+ let data_buffer = read_u8(buf, &mut i);
+ let cpu_cycle = read_u64(buf, &mut i);
debug_assert_eq!(i, RECORD_SIZE);
Some(Self {
@@ -488,6 +531,8 @@ impl PpuStateRecord {
oam_fnv1a64,
nmi_line,
oam_bus_copybuffer,
+ data_buffer,
+ cpu_cycle,
})
}
}
@@ -749,7 +794,7 @@ impl PpuStateTrace {
spr_shift_lo,spr_shift_hi,spr_attr,spr_x,\
bg_shift_lo,bg_shift_hi,at_shift_lo,at_shift_hi,\
nt_latch,at_latch,bg_lo_latch,bg_hi_latch,\
- secondary_oam,oam_fnv1a64,nmi_line,oam_bus\n",
+ secondary_oam,oam_fnv1a64,nmi_line,oam_bus,data_buffer,cpu_cycle\n",
);
let write_arr = |out: &mut String, a: &[u8]| {
let mut first = true;
@@ -809,10 +854,12 @@ impl PpuStateTrace {
write_arr(&mut out, &r.secondary_oam);
let _ = writeln!(
out,
- ",{:016X},{},{:02X}",
+ ",{:016X},{},{:02X},{:02X},{}",
r.oam_fnv1a64,
u8::from(r.nmi_line),
- r.oam_bus_copybuffer
+ r.oam_bus_copybuffer,
+ r.data_buffer,
+ r.cpu_cycle
);
}
out
@@ -863,6 +910,8 @@ mod tests {
sprite_eval_done: false,
sprite_eval_read_latch: 0x77,
oam_bus_copybuffer: 0x5A,
+ data_buffer: 0xB7,
+ cpu_cycle: 0x0123_4567_89AB_CDEF,
spr_count: 5,
spr_zero_in_line: true,
spr_shift_lo: [1, 2, 3, 4, 5, 6, 7, 8],
@@ -1024,6 +1073,7 @@ mod tests {
"oam_fnv1a64",
"nmi_line",
"oam_bus",
+ "data_buffer",
] {
assert!(
header.contains(column),
@@ -1035,26 +1085,41 @@ mod tests {
// wrong value, or be dropped from the row while the name survives --
// and this test passed on both counts before `oam_bus` was asserted
// here, which is exactly how a serialization regression reaches a
- // golden. `sample_record` sets it to 0x5A and the row ends with it.
+ // golden.
+ //
+ // Values are checked BY COLUMN NAME, not by position. Two schema
+ // additions in a row broke the previous form for the wrong reason:
+ // it pinned the trailing columns, so appending a field displaced them
+ // and the test reported a regression in a column that had not changed.
+ // A name lookup also asserts the stronger property — that the header
+ // and the row agree on where each field is — which is the drift worth
+ // guarding, since a column appended to one and not the other is
+ // exactly how a golden becomes unreadable.
let row = csv.lines().nth(1).expect("one record was pushed");
- let last = row.rsplit(',').next().expect("non-empty row");
+ let cols: Vec<&str> = header.split(',').collect();
+ let vals: Vec<&str> = row.split(',').collect();
assert_eq!(
- last, "5A",
- "final CSV column should be oam_bus_copybuffer as two hex digits; row: {row}"
- );
-
- // And the two must line up: whatever position the header gives
- // `oam_bus`, the row must carry the value at that same index. A column
- // appended to one and not the other is the drift this guards.
- let idx = header
- .split(',')
- .position(|c| c == "oam_bus")
- .expect("header names oam_bus");
- assert_eq!(
- row.split(',').nth(idx),
- Some("5A"),
- "oam_bus header index {idx} does not carry the record's value; row: {row}"
+ cols.len(),
+ vals.len(),
+ "header has {} columns and the row {}; header: {header}\nrow: {row}",
+ cols.len(),
+ vals.len()
);
+ for (name, want) in [
+ ("oam_bus", "5A"),
+ ("data_buffer", "B7"),
+ ("cpu_cycle", "81985529216486895"),
+ ] {
+ let idx = cols
+ .iter()
+ .position(|c| *c == name)
+ .unwrap_or_else(|| panic!("header does not name `{name}`: {header}"));
+ assert_eq!(
+ vals[idx], want,
+ "column `{name}` (index {idx}) carries {}, expected {want}; row: {row}",
+ vals[idx]
+ );
+ }
}
/// Guard against silent layout drift: if the field set
diff --git a/crates/rustynes-test-harness/tests/read_joy3.rs b/crates/rustynes-test-harness/tests/read_joy3.rs
index caa1e245..70d3baf9 100644
--- a/crates/rustynes-test-harness/tests/read_joy3.rs
+++ b/crates/rustynes-test-harness/tests/read_joy3.rs
@@ -28,10 +28,32 @@
//! `rustynes_core::bus::Bus::dmc_dma_read` (the `$4016`/`$4017` conflict arms call
//! `controllers[port].read()`, advancing the shift register during the DMC
//! fetch). Verified empirically: `count_errors.nes` runs its 1000-iteration
-//! loop and renders **"Conflicts: 149/1000"** at frame 240 — i.e. our core
-//! produces 149 real DMC-vs-`$4016` conflicts, and the conflict-tolerant
+//! loop and renders **"Conflicts: 134/1000"** at frame 240 — i.e. our core
+//! produces 134 real DMC-vs-`$4016` conflicts, and the conflict-tolerant
//! `read_joy` routine compensates for every one (the ROM never hits its
-//! `test_failed` halt; the loop runs to completion). This older test shell
+//! `test_failed` halt; the loop runs to completion).
+//!
+//! **v2.6.5 moved both counts, deliberately.** `Controller::write_strobe`
+//! dropped an owed shift unconditionally — the first defect this programme's
+//! co-simulation proved in the ORACLE rather than in the DUT — and fixing it
+//! changes exactly the shift-register-during-DMA behaviour these two ROMs
+//! exist to stress. Measured on both sides, `main` against the fix:
+//!
+//! | ROM | before | after |
+//! |---|---|---|
+//! | `count_errors.nes` | Conflicts: 149/1000 | **134/1000** |
+//! | `count_errors_fast.nes` | Errors: 75/1000 | **58/1000** |
+//!
+//! Both counts fall and neither reaches zero, which is the direction a
+//! dropped-shift fix should produce: fewer controller-read errors, with the
+//! conflict model still active. The failure mode this snapshot guards against
+//! — "dropping the count toward 0", i.e. the conflict model being DISABLED —
+//! is not what happened, and that was checked by decoding the screens rather
+//! than by observing that a hash changed. `AccuracyCoin`'s `Controller Clocking`
+//! moves from success code 2 (Famicom) to code 1 (NES / AV Famicom) in the same
+//! change, which is the console this project models.
+//!
+//! This older test shell
//! reports ONLY on-screen (no `$6000` magic), so the framebuffer-FNV-1a
//! snapshot below LOCKS that exact completed screen: a regression that
//! DISABLED the conflict model (dropping the count toward 0) or that broke the
diff --git a/crates/rustynes-test-harness/tests/snapshot_schema_audit.rs b/crates/rustynes-test-harness/tests/snapshot_schema_audit.rs
index b54a7ec2..1382d622 100644
--- a/crates/rustynes-test-harness/tests/snapshot_schema_audit.rs
+++ b/crates/rustynes-test-harness/tests/snapshot_schema_audit.rs
@@ -146,6 +146,20 @@ const CHIPS: &[Chip] = &[
"state_trace",
"diagnostic: `ppu-state-trace` ring buffer, output-only",
),
+ (
+ "trace_cpu_cycle",
+ "diagnostic: the CPU cycle stamped into each `ppu-state-trace` record so a \
+ dot can be located against `obs.bin`, which is cycle-keyed. \
+ Excluded on a STRONGER ground than the other diagnostics here \
+ rather than a weaker one: the bus writes it unconditionally at \
+ the start of every CPU cycle, BEFORE any dot of that cycle is \
+ ticked, so a restore cannot observe a stale value -- the first \
+ cycle after a load overwrites it before the first record exists. \
+ Nothing in the PPU reads it; it is only copied into a record. \
+ Carrying it would also be actively wrong, since the cycle counter \
+ belongs to the run that produced the save and not to the one \
+ resuming it.",
+ ),
(
"fast_path_hits",
"diagnostic: `ppu-fetch-trace` counter of how many dots took the \
diff --git a/crates/rustynes-test-harness/tests/snapshots/read_joy3__read_joy3_count_errors_f240.snap b/crates/rustynes-test-harness/tests/snapshots/read_joy3__read_joy3_count_errors_f240.snap
index bf26862d..36133548 100644
--- a/crates/rustynes-test-harness/tests/snapshots/read_joy3__read_joy3_count_errors_f240.snap
+++ b/crates/rustynes-test-harness/tests/snapshots/read_joy3__read_joy3_count_errors_f240.snap
@@ -2,4 +2,4 @@
source: crates/rustynes-test-harness/tests/read_joy3.rs
expression: snap
---
-rom=nes-test-roms/read_joy3/count_errors.nes frames=240 fb_bytes=245760 fnv1a64=926f44f894794fec
+rom=nes-test-roms/read_joy3/count_errors.nes frames=240 fb_bytes=245760 fnv1a64=d578304b86c3fafd
diff --git a/crates/rustynes-test-harness/tests/snapshots/read_joy3__read_joy3_count_errors_fast_f240.snap b/crates/rustynes-test-harness/tests/snapshots/read_joy3__read_joy3_count_errors_fast_f240.snap
index 6169c82b..2e823e4d 100644
--- a/crates/rustynes-test-harness/tests/snapshots/read_joy3__read_joy3_count_errors_fast_f240.snap
+++ b/crates/rustynes-test-harness/tests/snapshots/read_joy3__read_joy3_count_errors_fast_f240.snap
@@ -2,4 +2,4 @@
source: crates/rustynes-test-harness/tests/read_joy3.rs
expression: snap
---
-rom=nes-test-roms/read_joy3/count_errors_fast.nes frames=240 fb_bytes=245760 fnv1a64=00466783e115c91d
+rom=nes-test-roms/read_joy3/count_errors_fast.nes frames=240 fb_bytes=245760 fnv1a64=c554acceb808e035
diff --git a/docs/STATUS.md b/docs/STATUS.md
index f844de8a..ade78f7a 100644
--- a/docs/STATUS.md
+++ b/docs/STATUS.md
@@ -1,11 +1,11 @@
# RustyNES — Project Status Matrix
-> **Current release: v2.6.4** (2026-08-26) — **"Rubric"**, OAM DMA lands and all nine AccuracyCoin disagreements close, every rule that closed the last three stated by the test ROM and by neither nesdev page — and then the gate that certified them is measured to cover 88 of 146 entries. The emulation core is unchanged. Built on **v2.6.3 "Mainspring"** (2026-08-25) — the DUT runs on one master clock, and four enables that were never enabling — plus AccuracyCoin end to end and a status vector that names its disagreements by test. The emulation core is unchanged. Built on **v2.6.2 "Witness"** (2026-08-24) — rung 4 closes: blargg APU battery 11/11 on the co-simulation DUT, six defects no self-written gate could see, and a suite that had been asserting nothing for five minor releases. The emulation core is unchanged. Built on **v2.6.1 "Interleave"** (2026-08-24) — the DMC and its DMA cycle steal in the MiSTer co-simulation DUT, cycle-exact on the bus. The emulation core is unchanged. Built on **v2.6.0 "Assay"** (2026-08-24) — the triangle, the noise channel and the sweep unit **in the MiSTer co-simulation DUT** — and an audit of how much of the APU was fitted to the oracle rather than derived from documentation. The emulation core is unchanged. Built on **v2.5.9 "Overture"** (2026-08-24) — rung 4 opens: the two pulse channels, the frame counter, and four ROM defects the stimulus measurement found first. Built on **v2.5.8 "Blanking"** (2026-08-24) — VBlank, NMI and the PPUSTATUS race close rung 3 — and both fixes were deletions. Built on **v2.5.7 "Collimation"** (2026-08-24) — sprite rendering closes exact — the phase was wrong by two dots, and every window was compensating. Built on **v2.5.6 "Vestige"** (2026-08-23) — Sprite evaluation closes: all 59,993 overlapping cycles match, nine of nine behavioural mutants caught and two proved inert (announced as seven of eight at the cut), and the fix is a byte index that outlives the walk that set it. Built on **v2.5.5 "Raster"** (2026-08-23) — the first full frame, and three blind spots in the stimulus that fed it. Built on **v2.5.4 "Escapement"** (2026-08-23) — the background fetch pipeline, and an access two dots early that five gates could not see. Built on **v2.5.3 "Hysteresis"** (2026-08-23) — toggling rendering takes effect three dots after the write, and four instruments to prove it. Built on **v2.5.2 "Dormant"** (2026-08-23) — the 2C02 register file, and a gate that passed while testing nothing. Built on **v2.5.1 "Retrace"** (2026-08-23) — the interrupt sweep closes rung 2, and a gate reported a pass it could not have earned. Built on **v2.5.0 "Rungwork"** (2026-08-23) — the 6502 rung, and the two gates it cannot reach. Built on **v2.4.9 "Plumbline II"** (2026-08-23) — the bus half of rung 2, and what it found the day it existed. Built on **v2.4.8 "Palimpsest"** (2026-08-23) — read-modify-write, and a gate that cannot see its own subject. Built on **v2.4.7 "Keystone"** (2026-08-23) — the stack closes, and a dead line proves itself dead. Built on **v2.4.6 "Abacus"** (2026-08-22) — the core learns arithmetic. Built on **v2.4.5 "Compass"** (2026-08-22) — the core reaches memory, and chooses. Built on **v2.4.4 "Ignition"** (2026-08-22) — the first real RTL -- the 6502's eight-cycle reset and the implied opcode group, matching the oracle on all seven CPU fields (29
+> **Current release: v2.6.5** (2026-08-29) — **"Muster"**, rung 5 closes — the AccuracyCoin status vector is identical entry for entry across all 146 entries, with 146 of 146 executed on both sides and none NotRun, where the same gate read 5 of 146 at the version's start. A muster is a roll call where every name is called AND answered, which is the two-clause acceptance exactly. Five PPU defects close the last six differing entries and four were invisible to every gate that existed when the version opened: the background shift registers' RELOAD and their shift clock need SEPARATE gates (with one shared gate the serial-in test was not merely failing but ARITHMETICALLY UNREACHABLE, since reload dots are absolute and the reload discards the low seven bits, so a serial-in one can never reach bit 7 on any alignment — and modelling both structures reproduces BOTH measured shifter values); the sprite X counters are NOT gated on rendering, which AccuracyCoin states outright and the ROM that states it passes either way, because it expects no hit at X=254 and a sprite shoved 18 dots right is also off the line; the PPUADDR second-write v-copy is DELAYED, as the wiki says inside the write sequence itself, swept 1 to 4 dots against a control at 8 and 12 that fails; and the pre-render line CLEARS secondary OAM, without which scanline 0 draws what scanline 239 left — no sprite can ever render on scanline 0, because OAM Y is one less than the display row, and a sprite-0 probe over the full 134 M-cycle battery found 24 hits with four of them there; and the octal latch holding across the read dot, which is verified by exactly ONE gate and was unverifiable until the v-copy delay landed, the two composing the hybrid address together and neither producing it alone. A DIAGNOSIS IS RETRACTED: the residual was read as a two-dot CPU/PPU alignment error from comparing dot spans across two instruments, and at the committed alignment the two consoles execute identical pc, bus_addr and bus_access for 1,695,131 cycles while a two-dot shift moves the first fork back to 593,228 and takes the differing share from 5.13% to 66.80%. The oracle changes on the default path, so AccuracyCoin 141/141 (RAM decoder) and nestest 0-diff are VERIFIED, not asserted. Built on **v2.6.4 "Rubric"** (2026-08-26) — OAM DMA lands and all nine AccuracyCoin disagreements close, every rule that closed the last three stated by the test ROM and by neither nesdev page — and then the gate that certified them is measured to cover 88 of 146 entries. The emulation core is unchanged. Built on **v2.6.3 "Mainspring"** (2026-08-25) — the DUT runs on one master clock, and four enables that were never enabling — plus AccuracyCoin end to end and a status vector that names its disagreements by test. The emulation core is unchanged. Built on **v2.6.2 "Witness"** (2026-08-24) — rung 4 closes: blargg APU battery 11/11 on the co-simulation DUT, six defects no self-written gate could see, and a suite that had been asserting nothing for five minor releases. The emulation core is unchanged. Built on **v2.6.1 "Interleave"** (2026-08-24) — the DMC and its DMA cycle steal in the MiSTer co-simulation DUT, cycle-exact on the bus. The emulation core is unchanged. Built on **v2.6.0 "Assay"** (2026-08-24) — the triangle, the noise channel and the sweep unit **in the MiSTer co-simulation DUT** — and an audit of how much of the APU was fitted to the oracle rather than derived from documentation. The emulation core is unchanged. Built on **v2.5.9 "Overture"** (2026-08-24) — rung 4 opens: the two pulse channels, the frame counter, and four ROM defects the stimulus measurement found first. Built on **v2.5.8 "Blanking"** (2026-08-24) — VBlank, NMI and the PPUSTATUS race close rung 3 — and both fixes were deletions. Built on **v2.5.7 "Collimation"** (2026-08-24) — sprite rendering closes exact — the phase was wrong by two dots, and every window was compensating. Built on **v2.5.6 "Vestige"** (2026-08-23) — Sprite evaluation closes: all 59,993 overlapping cycles match, nine of nine behavioural mutants caught and two proved inert (announced as seven of eight at the cut), and the fix is a byte index that outlives the walk that set it. Built on **v2.5.5 "Raster"** (2026-08-23) — the first full frame, and three blind spots in the stimulus that fed it. Built on **v2.5.4 "Escapement"** (2026-08-23) — the background fetch pipeline, and an access two dots early that five gates could not see. Built on **v2.5.3 "Hysteresis"** (2026-08-23) — toggling rendering takes effect three dots after the write, and four instruments to prove it. Built on **v2.5.2 "Dormant"** (2026-08-23) — the 2C02 register file, and a gate that passed while testing nothing. Built on **v2.5.1 "Retrace"** (2026-08-23) — the interrupt sweep closes rung 2, and a gate reported a pass it could not have earned. Built on **v2.5.0 "Rungwork"** (2026-08-23) — the 6502 rung, and the two gates it cannot reach. Built on **v2.4.9 "Plumbline II"** (2026-08-23) — the bus half of rung 2, and what it found the day it existed. Built on **v2.4.8 "Palimpsest"** (2026-08-23) — read-modify-write, and a gate that cannot see its own subject. Built on **v2.4.7 "Keystone"** (2026-08-23) — the stack closes, and a dead line proves itself dead. Built on **v2.4.6 "Abacus"** (2026-08-22) — the core learns arithmetic. Built on **v2.4.5 "Compass"** (2026-08-22) — the core reaches memory, and chooses. Built on **v2.4.4 "Ignition"** (2026-08-22) — the first real RTL -- the 6502's eight-cycle reset and the implied opcode group, matching the oracle on all seven CPU fields (29
> records, `RustyNES_MiSTer@7f092bd`). The oracle settled a question our own
> prose could not: reset is EIGHT cycles, and `docs/cpu-6502.md` said both
> seven and eight. The emulation core is untouched.
>
-> **Rungs 3 and 4 CLOSED; rung 5 IN PROGRESS.** Rung 3's seven steps are all
+> **Rungs 3, 4 and 5 CLOSED.** Rung 3's seven steps are all
> green and every gate exact — the register file, scroll, background fetch,
> background rendering, sprite evaluation, sprite rendering, and VBlank/NMI with
> the `$2002` race — capped by **nestest 0-diff at 5,002,992 cycles**, the
@@ -55,9 +55,30 @@
> coverage number beside the agreement number; a pass count is a claim about
> what ran. Reaching it cost **two false passes before a real one**: the ROM
> idles on its title screen until START is pressed, and only the framebuffer
-> half had a guard that refused. `sys/` is still empty; there is no `.rbf`.
-> Detail: `docs/mister.md` and the sibling's `docs/rung3-ppu.md` and
-> `docs/rung4-apu.md`.
+> half had a guard that refused.
+>
+> **v2.6.5 closes the rung.** Over that 4500-frame window the vector is
+> **IDENTICAL entry for entry across all 146 entries**, with **146 of 146
+> executed on both sides** and none `NotRun` — where the same gate read **5 of
+> 146 executed and 22 differing** at the version's start. Five PPU defects
+> closed the last six entries: the background reload and shift clock needing
+> **separate** gates (with one shared gate `BG Serial In` was arithmetically
+> unreachable, not merely failing), the sprite X counters **not** being gated on
+> rendering, the PPUADDR second-write `v <- t` copy being **delayed** as the
+> wiki states inside the write sequence, the octal latch holding across the read
+> dot, and the **pre-render line clearing secondary OAM** — without which
+> scanline 0 drew what scanline 239 left, and no sprite can ever render on
+> scanline 0. Four of the five were invisible to every gate that existed when
+> the version opened. A two-dot CPU/PPU alignment diagnosis is **retracted**:
+> at the committed alignment the two consoles execute identical `pc`,
+> `bus_addr` and `bus_access` for 1,695,131 cycles, and a two-dot shift takes
+> the differing share from 5.13% to 66.80%.
+>
+> `sys/` is still empty; there is no `.rbf`. That is **rung 6, at v2.6.6** —
+> deferred there rather than done at v2.6.5 under the ladder rule that a rung
+> may not start until the one below is green. Hardware bring-up remains blocked
+> on a DE10-Nano with the SDRAM add-on. Detail: `docs/mister.md` and the
+> sibling's `docs/rung3-ppu.md` and `docs/rung4-apu.md`.
>
> Built on **v2.4.3** (2026-08-22) — **"Touchstone"**, the two Fabric
> risks settled before any RTL exists. **Risk 4, the Quartus subset, is FITTED**:
diff --git a/docs/accuracy-ledger.md b/docs/accuracy-ledger.md
index 59c049c3..c47b01cd 100644
--- a/docs/accuracy-ledger.md
+++ b/docs/accuracy-ledger.md
@@ -142,6 +142,61 @@ disposition under the v2.1.0 "Fathom" accuracy-remediation line
**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 | second accepted outcome | not investigated |
+| `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** |
+
+**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` is the one genuinely open item.** 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 (all 20)
Every `#[ignore]`'d test in the workspace, with its disposition. **None is an
diff --git a/docs/ppu-trace-tooling.md b/docs/ppu-trace-tooling.md
index 0eec1033..7248a3bd 100644
--- a/docs/ppu-trace-tooling.md
+++ b/docs/ppu-trace-tooling.md
@@ -372,7 +372,31 @@ Output format per divergence:
## Schema versioning
-Binary schema version is `1` (Session-10). Bump
+Binary schema version is **`4`** (v2.6.5), and the record is
+**123 bytes**. It was `1` at Session-10; this section had not
+been moved since, which is how a spec becomes a stale claim
+about the layout beneath it.
+
+| schema | added | why |
+|---|---|---|
+| 2 | `oam_bus_copybuffer` | v2.5.6 — the sprite-eval gate observed a model the diagnostic did not expose |
+| 3 | `data_buffer` | v2.6.5 — the `$2007` read buffer, for the PPU DATA state machine |
+| 4 | `cpu_cycle` | v2.6.5 — stamps each per-dot record with the CPU cycle it belongs to |
+
+**`cpu_cycle` is what makes two consoles comparable at dot
+resolution at all.** Without it a reader has to infer the
+cycle from the dot, and the two sides stamp their records at
+different points within the cycle — which produced, and then
+retracted, a "two-dot CPU/PPU alignment" diagnosis in v2.6.5
+(sibling ledger 3.39 / 3.42). It is written by
+`Bus::set_trace_cpu_cycle` at **two** call sites: `cpu_clock`,
+the path a running console takes, and `tick_one_cpu_cycle`,
+the one the harness drives. Wiring only the latter leaves every
+record `0` while the field, the column and the plumbing all
+look correct, which is what
+`state_trace_records_carry_their_cpu_cycle` exists to catch.
+
+Bump
`PPU_TRACE_SCHEMA_VERSION` in
`crates/rustynes-ppu/src/state_trace.rs` whenever the
`PpuStateRecord` byte layout changes; the `to_binary` /
diff --git a/tests/roms/AccuracyCoin/README.md b/tests/roms/AccuracyCoin/README.md
index 48fcd1c9..9ab22fe7 100644
--- a/tests/roms/AccuracyCoin/README.md
+++ b/tests/roms/AccuracyCoin/README.md
@@ -23,10 +23,16 @@ after the original batch. The recipe, so the next one does not have to be
rediscovered:
```bash
+# Fetch the upstream source into a scratch dir...
mkdir -p /tmp/accoin-src && cd /tmp/accoin-src
for f in AccuracyCoin.asm nesasm.exe Tiles.pcx Sprites.pcx; do
curl -sLO "https://raw.githubusercontent.com/100thCoin/AccuracyCoin/main/$f"
done
+
+# ...then come BACK. Both the builder path and `--out` are repo-relative, and
+# the `cd` above leaves the shell in /tmp/accoin-src, where neither resolves.
+cd "$(git -C ~/Code/OSS_Public-Projects/RustyNES rev-parse --show-toplevel)"
+
python3 scripts/accuracycoin-build/build_sub_test_rom.py /tmp/accoin-src \
--suite 11 --test 1 --name "NMI Overlap BRK" \
--out tests/roms/AccuracyCoin/sub-tests/nmi-overlap-brk.nes
@@ -37,6 +43,32 @@ within that suite's `table "name", ...` lines; the builder's docstring carries
the suite map. It assembles through **wine + the upstream `nesasm.exe`**, which
is the upstream toolchain rather than a substitute.
+**Two more were added in v2.6.5**, for the `$2007` state-machine cluster:
+
+```bash
+python3 scripts/accuracycoin-build/build_sub_test_rom.py /tmp/accoin-src \
+ --suite 18 --test 7 --name "ALE + Read" \
+ --out tests/roms/AccuracyCoin/sub-tests/ppu-misc-ale-read.nes
+python3 scripts/accuracycoin-build/build_sub_test_rom.py /tmp/accoin-src \
+ --suite 18 --test 8 --name "Hybrid Addresses" \
+ --out tests/roms/AccuracyCoin/sub-tests/ppu-misc-hybrid-addresses.nes
+```
+
+Both report at their **catalog** addresses (`$0491`, `$0492`) — verified from a
+RAM diff, not assumed — and both reach a verdict in **8.93M cycles** against the
+full battery's 134M, which is the whole reason to build them.
+
+**`/usr/local/bin/wine` may not be wine.** On the development machine it is a
+symlink to **firejail**, which shadows the real binary at `/usr/bin/wine` on
+`PATH`; the builder then assembles nothing and the failure does not name wine.
+Run it as `PATH=/usr/bin:$PATH python3 scripts/...` if `wine --version` prints
+anything other than a wine version.
+
+Re-fetch `AccuracyCoin.asm` rather than reusing a local copy, and check it
+matches: the builder rewrites one routine in the source it is handed, so a
+source that already drifted produces a ROM that looks fine and tests something
+else.
+
**`sub-tests/cpu-open-bus.nes` does not run `Open Bus`.** Measured in v2.6.4:
its verdict lands at **`$0407`**, which the catalog assigns to *Dummy write
cycles*, and `$0408` (`Open Bus`) is never written. It is off by one row of
@@ -102,3 +134,28 @@ Both directories are referenced by code:
Merging them would require renaming the source files in both crates and
regenerating the per-suite pass-rate baselines. Cost > benefit. The
two-directory layout is the canonical path going forward.
+
+### Sub-tests the harness cannot isolate
+
+Two sub-test ROMs build correctly and are **deliberately unregistered**, because
+the ORACLE does not pass them. `subtest_verdict.py` refuses a comparison whose
+oracle side is not a pass — correctly, since a ROM the reference implementation
+fails cannot adjudicate anything about the DUT.
+
+| ROM | suite/test | oracle verdict | why |
+|---|---|---|---|
+| `sprite-eval-arbitrary-sprite-zero.nes` | — | Fail | pre-existing; see the rung-5 notes |
+| `sprite-zero-hit-behavior.nes` | 17 / 1 | `$457 = $06`, Fail(test 1) | the streamlined boot omits state the full battery establishes |
+
+For `sprite-zero-hit-behavior` the failure is the FIRST assertion — "does a
+sprite zero hit occur in a situation in which it should" — so the test never
+gets as far as the behaviour it exists to check. `TEST_Sprite0Hit_Behavior`
+expects "a solid white square … placed at VRAM address $2001" and a sprite zero
+overlapping it; the builder replaces `AutomaticallyRunEveryTestInROM` with a
+runner that calls `LoadSuiteMenuNoRendering` and `RunTest` once, which does not
+reproduce everything the full battery has done to VRAM and the pattern tables by
+the time this entry runs.
+
+The ROM and its golden are kept rather than deleted: they are the evidence that
+the isolation was attempted and why it does not work, and the entry has to be
+debugged against the full 4500-frame battery instead.
diff --git a/tests/roms/AccuracyCoin/sub-tests/ppu-misc-ale-read.nes b/tests/roms/AccuracyCoin/sub-tests/ppu-misc-ale-read.nes
new file mode 100644
index 00000000..6539ddd1
Binary files /dev/null and b/tests/roms/AccuracyCoin/sub-tests/ppu-misc-ale-read.nes differ
diff --git a/tests/roms/AccuracyCoin/sub-tests/ppu-misc-hybrid-addresses.nes b/tests/roms/AccuracyCoin/sub-tests/ppu-misc-hybrid-addresses.nes
new file mode 100644
index 00000000..1119c00a
Binary files /dev/null and b/tests/roms/AccuracyCoin/sub-tests/ppu-misc-hybrid-addresses.nes differ
diff --git a/tests/roms/AccuracyCoin/sub-tests/sprite-zero-hit-behavior.nes b/tests/roms/AccuracyCoin/sub-tests/sprite-zero-hit-behavior.nes
new file mode 100644
index 00000000..61fe9fea
Binary files /dev/null and b/tests/roms/AccuracyCoin/sub-tests/sprite-zero-hit-behavior.nes differ
diff --git a/to-dos/ROADMAP.md b/to-dos/ROADMAP.md
index 7070c44e..fa22ef22 100644
--- a/to-dos/ROADMAP.md
+++ b/to-dos/ROADMAP.md
@@ -55,12 +55,12 @@ v2.8.0 → v0.9.7; the synthesis itself = **v1.0.0**.
## Status
-- **Current release:** **RustyNES v2.6.4 "Rubric"** (2026-08-26) — OAM DMA lands and all nine AccuracyCoin disagreements close, every rule that closed the last three stated by the test ROM and by neither nesdev page — and then the gate that certified them is measured to cover 88 of 146 entries. The emulation core is unchanged. Built on **v2.6.3 "Mainspring"** (2026-08-25) — the DUT runs on one master clock, and four enables that were never enabling — plus AccuracyCoin end to end and a status vector that names its disagreements by test. The emulation core is unchanged. Built on **v2.6.2 "Witness"** (2026-08-24) — rung 4 closes: blargg APU battery 11/11 on the co-simulation DUT, six defects no self-written gate could see, and a suite that had been asserting nothing for five minor releases. The emulation core is unchanged. Built on **v2.6.1 "Interleave"** (2026-08-24) — the DMC and its DMA cycle steal in the MiSTer co-simulation DUT, cycle-exact on the bus. The emulation core is unchanged. Built on **v2.6.0 "Assay"** (2026-08-24) — the triangle, the noise channel and the sweep unit **in the MiSTer co-simulation DUT** — and an audit of how much of the APU was fitted to the oracle rather than derived from documentation. The emulation core is unchanged. Built on **v2.5.9 "Overture"** (2026-08-24) — rung 4 opens: the two pulse channels, the frame counter, and four ROM defects the stimulus measurement found first. Built on **v2.5.8 "Blanking"** (2026-08-24) — VBlank, NMI and the PPUSTATUS race close rung 3 — and both fixes were deletions. Built on **v2.5.7 "Collimation"** (2026-08-24) — sprite rendering closes exact — the phase was wrong by two dots, and every window was compensating. Built on **v2.5.6 "Vestige"** (2026-08-23) — Sprite evaluation closes: all 59,993 overlapping cycles match, nine of nine behavioural mutants caught and two proved inert (announced as seven of eight at the cut), and the fix is a byte index that outlives the walk that set it. Built on **v2.5.5 "Raster"** (2026-08-23) — the first full frame, and three blind spots in the stimulus that fed it. Built on **v2.5.4 "Escapement"** (2026-08-23) — the background fetch pipeline, and an access two dots early that five gates could not see. Built on **v2.5.3 "Hysteresis"** (2026-08-23) — toggling rendering takes effect three dots after the write, and four instruments to prove it. Built on **v2.5.2 "Dormant"** (2026-08-23) — the 2C02 register file, and a gate that passed while testing nothing. Built on **v2.5.1 "Retrace"** (2026-08-23) — the interrupt sweep closes rung 2, and a gate reported a pass it could not have earned. Built on **v2.5.0 "Rungwork"** (2026-08-23) — the 6502 rung, and the two gates it cannot reach. Built on **v2.4.9 "Plumbline II"** (2026-08-23) — the bus half of rung 2, and what it found the day it existed. Built on **v2.4.8 "Palimpsest"** (2026-08-23) — read-modify-write, and a gate that cannot see its own subject. Built on **v2.4.7 "Keystone"** (2026-08-23) — the stack closes, and a dead line proves itself dead. Built on **v2.4.6 "Abacus"** (2026-08-22) — the core learns arithmetic. Built on **v2.4.5 "Compass"** (2026-08-22) — the core reaches memory, and chooses. Built on **v2.4.4 "Ignition"** (2026-08-22) — the first real RTL. The 6502's eight-cycle reset and the seventeen single-byte implied opcodes, in SystemVerilog in the sibling repository (`RustyNES_MiSTer@7f092bd`), matching the oracle on all seven CPU fields -- 29 records, and the gate demonstrated to fail on four mutations. The DUT is the **third writer** of the oracle's `CpuBootTrace` format, so `cpu_boot_trace_diff` reads it with no modification and the rung needed no oracle-side change at all. **The oracle settled a question our own prose could not**: reset is EIGHT cycles, and `docs/cpu-6502.md` said both seven and eight -- corrected here. The emulation core is untouched. Built on **v2.4.3 "Touchstone"** (2026-08-22) — what the synthesiser accepts, and what the licence requires. A touchstone is a stone you rub gold against; the streak tells you what the metal actually is. This release settles the **two Fabric-plan risks that had to be answered before any RTL exists**, and both were answered by evidence that contradicted what the plan assumed. **Risk 4, the Quartus subset, is FITTED**: Quartus Prime Lite 17.0.2 Build 602 on a 5CSEBA6U23I7 produced a placed-and-routed netlist with **0 synthesis warnings**, and the 2 KiB array inferred as **2 M10K blocks with 29 total registers** — not 16,413 — from the source style alone, no `ramstyle` attribute. The `initial` block became a real MIF (so a boot ROM lands inside the block) and the `enum` was one-hot encoded. Nine constructs are promoted to *fitted*; plain `case`, `priority case` and `$bits` are deliberately left *documented* because the kitchen sink does not exercise them. **Risk 1, the `sys/` licence, inverts the plan's own hedge**: 57 files, **zero GPL-2.0-only**, and `hps_io.sv` — GPL-3.0-or-later and not optional, since it is how a core receives a ROM and reaches the OSD — forces the combined bitstream **up** to GPL-3.0-or-later, already RustyNES's licence. The emulation core is untouched. Built on **v2.4.2 "Cairn"** (2026-08-22) — the **rung-0 compare surface**: rolling per-cycle hash checkpoints, measured at **15,263x** smaller than the equivalent CSV; the v2.4.2 acceptance gate made executable; and the partition between what RustyNES *models* and what a device can *observe*. Built on **v2.4.1 "Fabric"** (2026-08-20) — the **oracle** release, opening the **v2.4.1 → v2.5.0 "Fabric"** line: a new NES core written in SystemVerilog from public hardware documentation, in a sibling repository, with this emulator as its **verification oracle**. RustyNES is not being ported to FPGA and cannot be; `crates/rustynes-cosim` is the boundary (a narrow C ABI a Verilator testbench links, plus `nes_golden_export`), and the provenance firewall extends to HDL per ADR 0037. **v2.5.0 is scoped to "the 6502 rung closes"**, not a finished core. Excluding the crate from the workspace is the load-bearing detail — cargo unifies features, `irq-timing-trace` selects a *different* per-dot loop in `Bus::tick_one_cpu_cycle`, and the accuracy battery had been validating a scheduler no user runs. It also carries **v2.4.0 "Concordance"**, which merged to `main` and was never tagged: atomic durable writes on every path that persists user data, `Nes::timeline_generation()`, and the 15-anchor release audit. AccuracyCoin **141/141** verified, not asserted. Built on **v2.3.9 "Crucible"** (2026-08-20) — the **gates** release. A crucible tests to destruction rather than inspects, and that is what this release does to the project's own checks: what they cover, what they only *appear* to cover, and where a regression could still reach `main` unchallenged. The v2.3.x line added five tools in four releases, and the recurring finding across all of them was never that the emulation was wrong — it was that **a check reported a pass it had not earned**. **The docs-only CI skip had never worked**: `dorny/paths-filter`’s `predicate-quantifier` defaults to `some`, so the `code` filter’s leading `'**'` matched everything and all seven `!` exclusions under it were dead from the day they were written — a markdown-only PR logged `Filter code = true`. Fixed with **two** filter steps, because the quantifier is step-level and `accuracy` is a list of *alternatives* that becomes unsatisfiable under `every`: the naive one-line fix would have silently disabled the accuracy battery while repairing a different gate. **`test-roms` now runs at review time**, path-filtered over the chip crates, the core, `rustynes-gamedb`, the harness and `tests/` — measured first at 11 of the last 40 merged PRs, so ~72% still pay nothing. **A freeze from one cartridge kept writing into the next** — not a stale label but an active per-frame write into the wrong game, closed by a ROM-transition sweep across every panel under one rule: derived output is discarded, user-authored input is kept, and only input that actively *writes* is neutralised. **The config file is now written atomically and durably** (seven properties, five of them from review rather than the first draft). Plus **257 lines of dead code removed**, the SAFETY-comment rule made a clippy gate (`undocumented_unsafe_blocks`, demonstrated to fail), and two `cargo deny` advisory ignores retired on their own stated condition. `rustynes-apu` and `rustynes-core` both change, so **AccuracyCoin 141/141 (100.00%, RAM decoder) and nestest 0-diff are VERIFIED, not asserted.** **`docs/STATUS.md` is the authoritative current-state record.**
+- **Current release:** **RustyNES v2.6.5 "Muster"** (2026-08-29) — rung 5 closes — the AccuracyCoin status vector is identical entry for entry across all 146 entries, with 146 of 146 executed on both sides and none NotRun, where the same gate read 5 of 146 at the version's start. A muster is a roll call where every name is called AND answered, which is the two-clause acceptance exactly. Five PPU defects close the last six differing entries and four were invisible to every gate that existed when the version opened: the background shift registers' RELOAD and their shift clock need SEPARATE gates (with one shared gate the serial-in test was not merely failing but ARITHMETICALLY UNREACHABLE, since reload dots are absolute and the reload discards the low seven bits, so a serial-in one can never reach bit 7 on any alignment — and modelling both structures reproduces BOTH measured shifter values); the sprite X counters are NOT gated on rendering, which AccuracyCoin states outright and the ROM that states it passes either way, because it expects no hit at X=254 and a sprite shoved 18 dots right is also off the line; the PPUADDR second-write v-copy is DELAYED, as the wiki says inside the write sequence itself, swept 1 to 4 dots against a control at 8 and 12 that fails; and the pre-render line CLEARS secondary OAM, without which scanline 0 draws what scanline 239 left — no sprite can ever render on scanline 0, because OAM Y is one less than the display row, and a sprite-0 probe over the full 134 M-cycle battery found 24 hits with four of them there; and the octal latch holding across the read dot, which is verified by exactly ONE gate and was unverifiable until the v-copy delay landed, the two composing the hybrid address together and neither producing it alone. A DIAGNOSIS IS RETRACTED: the residual was read as a two-dot CPU/PPU alignment error from comparing dot spans across two instruments, and at the committed alignment the two consoles execute identical pc, bus_addr and bus_access for 1,695,131 cycles while a two-dot shift moves the first fork back to 593,228 and takes the differing share from 5.13% to 66.80%. The oracle changes on the default path, so AccuracyCoin 141/141 (RAM decoder) and nestest 0-diff are VERIFIED, not asserted. Built on **v2.6.4 "Rubric"** (2026-08-26) — OAM DMA lands and all nine AccuracyCoin disagreements close, every rule that closed the last three stated by the test ROM and by neither nesdev page — and then the gate that certified them is measured to cover 88 of 146 entries. The emulation core is unchanged. Built on **v2.6.3 "Mainspring"** (2026-08-25) — the DUT runs on one master clock, and four enables that were never enabling — plus AccuracyCoin end to end and a status vector that names its disagreements by test. The emulation core is unchanged. Built on **v2.6.2 "Witness"** (2026-08-24) — rung 4 closes: blargg APU battery 11/11 on the co-simulation DUT, six defects no self-written gate could see, and a suite that had been asserting nothing for five minor releases. The emulation core is unchanged. Built on **v2.6.1 "Interleave"** (2026-08-24) — the DMC and its DMA cycle steal in the MiSTer co-simulation DUT, cycle-exact on the bus. The emulation core is unchanged. Built on **v2.6.0 "Assay"** (2026-08-24) — the triangle, the noise channel and the sweep unit **in the MiSTer co-simulation DUT** — and an audit of how much of the APU was fitted to the oracle rather than derived from documentation. The emulation core is unchanged. Built on **v2.5.9 "Overture"** (2026-08-24) — rung 4 opens: the two pulse channels, the frame counter, and four ROM defects the stimulus measurement found first. Built on **v2.5.8 "Blanking"** (2026-08-24) — VBlank, NMI and the PPUSTATUS race close rung 3 — and both fixes were deletions. Built on **v2.5.7 "Collimation"** (2026-08-24) — sprite rendering closes exact — the phase was wrong by two dots, and every window was compensating. Built on **v2.5.6 "Vestige"** (2026-08-23) — Sprite evaluation closes: all 59,993 overlapping cycles match, nine of nine behavioural mutants caught and two proved inert (announced as seven of eight at the cut), and the fix is a byte index that outlives the walk that set it. Built on **v2.5.5 "Raster"** (2026-08-23) — the first full frame, and three blind spots in the stimulus that fed it. Built on **v2.5.4 "Escapement"** (2026-08-23) — the background fetch pipeline, and an access two dots early that five gates could not see. Built on **v2.5.3 "Hysteresis"** (2026-08-23) — toggling rendering takes effect three dots after the write, and four instruments to prove it. Built on **v2.5.2 "Dormant"** (2026-08-23) — the 2C02 register file, and a gate that passed while testing nothing. Built on **v2.5.1 "Retrace"** (2026-08-23) — the interrupt sweep closes rung 2, and a gate reported a pass it could not have earned. Built on **v2.5.0 "Rungwork"** (2026-08-23) — the 6502 rung, and the two gates it cannot reach. Built on **v2.4.9 "Plumbline II"** (2026-08-23) — the bus half of rung 2, and what it found the day it existed. Built on **v2.4.8 "Palimpsest"** (2026-08-23) — read-modify-write, and a gate that cannot see its own subject. Built on **v2.4.7 "Keystone"** (2026-08-23) — the stack closes, and a dead line proves itself dead. Built on **v2.4.6 "Abacus"** (2026-08-22) — the core learns arithmetic. Built on **v2.4.5 "Compass"** (2026-08-22) — the core reaches memory, and chooses. Built on **v2.4.4 "Ignition"** (2026-08-22) — the first real RTL. The 6502's eight-cycle reset and the seventeen single-byte implied opcodes, in SystemVerilog in the sibling repository (`RustyNES_MiSTer@7f092bd`), matching the oracle on all seven CPU fields -- 29 records, and the gate demonstrated to fail on four mutations. The DUT is the **third writer** of the oracle's `CpuBootTrace` format, so `cpu_boot_trace_diff` reads it with no modification and the rung needed no oracle-side change at all. **The oracle settled a question our own prose could not**: reset is EIGHT cycles, and `docs/cpu-6502.md` said both seven and eight -- corrected here. The emulation core is untouched. Built on **v2.4.3 "Touchstone"** (2026-08-22) — what the synthesiser accepts, and what the licence requires. A touchstone is a stone you rub gold against; the streak tells you what the metal actually is. This release settles the **two Fabric-plan risks that had to be answered before any RTL exists**, and both were answered by evidence that contradicted what the plan assumed. **Risk 4, the Quartus subset, is FITTED**: Quartus Prime Lite 17.0.2 Build 602 on a 5CSEBA6U23I7 produced a placed-and-routed netlist with **0 synthesis warnings**, and the 2 KiB array inferred as **2 M10K blocks with 29 total registers** — not 16,413 — from the source style alone, no `ramstyle` attribute. The `initial` block became a real MIF (so a boot ROM lands inside the block) and the `enum` was one-hot encoded. Nine constructs are promoted to *fitted*; plain `case`, `priority case` and `$bits` are deliberately left *documented* because the kitchen sink does not exercise them. **Risk 1, the `sys/` licence, inverts the plan's own hedge**: 57 files, **zero GPL-2.0-only**, and `hps_io.sv` — GPL-3.0-or-later and not optional, since it is how a core receives a ROM and reaches the OSD — forces the combined bitstream **up** to GPL-3.0-or-later, already RustyNES's licence. The emulation core is untouched. Built on **v2.4.2 "Cairn"** (2026-08-22) — the **rung-0 compare surface**: rolling per-cycle hash checkpoints, measured at **15,263x** smaller than the equivalent CSV; the v2.4.2 acceptance gate made executable; and the partition between what RustyNES *models* and what a device can *observe*. Built on **v2.4.1 "Fabric"** (2026-08-20) — the **oracle** release, opening the **v2.4.1 → v2.5.0 "Fabric"** line: a new NES core written in SystemVerilog from public hardware documentation, in a sibling repository, with this emulator as its **verification oracle**. RustyNES is not being ported to FPGA and cannot be; `crates/rustynes-cosim` is the boundary (a narrow C ABI a Verilator testbench links, plus `nes_golden_export`), and the provenance firewall extends to HDL per ADR 0037. **v2.5.0 is scoped to "the 6502 rung closes"**, not a finished core. Excluding the crate from the workspace is the load-bearing detail — cargo unifies features, `irq-timing-trace` selects a *different* per-dot loop in `Bus::tick_one_cpu_cycle`, and the accuracy battery had been validating a scheduler no user runs. It also carries **v2.4.0 "Concordance"**, which merged to `main` and was never tagged: atomic durable writes on every path that persists user data, `Nes::timeline_generation()`, and the 15-anchor release audit. AccuracyCoin **141/141** verified, not asserted. Built on **v2.3.9 "Crucible"** (2026-08-20) — the **gates** release. A crucible tests to destruction rather than inspects, and that is what this release does to the project's own checks: what they cover, what they only *appear* to cover, and where a regression could still reach `main` unchallenged. The v2.3.x line added five tools in four releases, and the recurring finding across all of them was never that the emulation was wrong — it was that **a check reported a pass it had not earned**. **The docs-only CI skip had never worked**: `dorny/paths-filter`’s `predicate-quantifier` defaults to `some`, so the `code` filter’s leading `'**'` matched everything and all seven `!` exclusions under it were dead from the day they were written — a markdown-only PR logged `Filter code = true`. Fixed with **two** filter steps, because the quantifier is step-level and `accuracy` is a list of *alternatives* that becomes unsatisfiable under `every`: the naive one-line fix would have silently disabled the accuracy battery while repairing a different gate. **`test-roms` now runs at review time**, path-filtered over the chip crates, the core, `rustynes-gamedb`, the harness and `tests/` — measured first at 11 of the last 40 merged PRs, so ~72% still pay nothing. **A freeze from one cartridge kept writing into the next** — not a stale label but an active per-frame write into the wrong game, closed by a ROM-transition sweep across every panel under one rule: derived output is discarded, user-authored input is kept, and only input that actively *writes* is neutralised. **The config file is now written atomically and durably** (seven properties, five of them from review rather than the first draft). Plus **257 lines of dead code removed**, the SAFETY-comment rule made a clippy gate (`undocumented_unsafe_blocks`, demonstrated to fail), and two `cargo deny` advisory ignores retired on their own stated condition. `rustynes-apu` and `rustynes-core` both change, so **AccuracyCoin 141/141 (100.00%, RAM decoder) and nestest 0-diff are VERIFIED, not asserted.** **`docs/STATUS.md` is the authoritative current-state record.**
- **Shipped, inside v2.4.1 — v2.4.0 "Concordance".** It merged to `main` and was never tagged, because the workspace version never sat at 2.4.0 on any commit; v2.4.1 carries it. There is deliberately no `v2.4.0` tag. Its scope was: A concordance is an index of where every term actually occurs, and the release is scoped as one: reconcile what the project says about itself with what is true outside it. Four items, each traceable to a recorded deferral rather than newly invented — **(A)** the **owed upstream libretro sync** (`libretro-super` + `libretro/docs`), the one carried obligation with an outside deadline; **(B)** a core-side **timeline generation counter** replacing the last-seen-`cycle()` heuristic for stale telemetry (it covers a restore to a *later* state, which the heuristic cannot), deliberately **not** serialized, so it must land with its consumers and be AccuracyCoin-**verified**; **(C)** a **shared atomic-write helper**, lifting v2.3.9's seven properties out of `config.rs` and giving the Windows tail a real implementation rather than a portable spine; and **(D)** `skip_serializing_if` on `hd_packs` / `shader_presets`, which carry the same false byte-identity claim v2.3.9 corrected in prose only. Explicitly out of scope, and recorded as decisions rather than oversights: the remaining RAM Atlas exports (a cheat is a **write**, so it needs a locked-session predicate the watch export correctly does without), RAM Atlas per-game persistence (a restored verdict without its evidence is a claim that cannot be checked — this panel's whole argument in reverse), APU workstreams **D2 and D4** (unmeasured on purpose; their prior is a null, not an unknown), a CHANGELOG gate (**measured and rejected** — 62% false positives against the project's own history), and any store launch. See [`plans/v2.4.0-concordance-plan.md`](plans/v2.4.0-concordance-plan.md).
- **Programme after v2.4.0 — the v2.4.1 → v2.5.0 "Fabric" line, and the v2.6–v2.9 programme behind it.** An **independently-written NES core in SystemVerilog for MiSTer FPGA and the Retro Remake SuperStation One, verified against RustyNES as an oracle.** Not a port, and it cannot be one: a MiSTer core is SystemVerilog compiled by Quartus 17.0.2 into a Cyclone V bitstream. The reference firewall therefore extends to HDL — `NES_MiSTer` and `fpganes` `rtl/` are **strict black boxes**, instantiable as opaque modules to compare *outputs*, never readable as source. **v2.5.0 is scoped to "the 6502 rung closes"** — the co-simulation harness plus a cycle-exact 6502, gated, **as planned**, on nestest 0-diff and per-cycle bus equality — of which **per-cycle bus equality was achieved and nestest 0-diff was not**: it stops at a `$2002` read where *both sides address it* and only the data differs, because the DUT has no PPU. That and the 5 M-cycle window are **reclassified as rung-3 acceptance criteria** rather than carried as v2.5.0 debt — because the arithmetic does not support more: a from-scratch cycle-accurate NES core is **7–13 months FTE** against a two-to-four-week window at demonstrated cadence. PPU, APU and MiSTer integration are **v2.6–v2.9**; stating that now is better than discovering it at v2.4.6. The design is **replay, not lockstep** (the determinism contract makes a pre-recorded trace exactly the trace a lockstep run would produce, and `Nes` has no per-cycle step to lockstep *with*), **no DPI-C** (it would put `` `ifdef SIMULATION `` guards into RTL that must also pass Quartus — the exact construct that lets a simulated netlist drift from the synthesised one), and **hash first, capture on divergence** (a 4200-frame AccuracyCoin run is ~7.5 GB of per-cycle CSV; 4096-cycle hash checkpoints are ~480 KB). **Two risks are accepted in writing:** the core may be **declined as a duplicate** — `NES_MiSTer` already scores 121/125 on AccuracyCoin, and *real Famicom AV hardware also scores ~121/125*, so there is no published accuracy headroom; and **the oracle can be wrong**, since 141/141 is not "matches silicon", so every rung is labelled by whether it has an **independent** oracle. Retro Remake is a planned fallback home, not a contingency. See ADR 0037, `docs/mister.md`, and [`plans/v2.5.0-fabric-plan.md`](plans/v2.5.0-fabric-plan.md).
- **Programme after v2.5.0 — the v2.5.1 → v2.7.0 line: the rest of the console, and a contributable package.** The Fabric line is delivered and the 6502 rung is closed; this line builds the PPU, APU, mappers and MiSTer integration, and takes the core to a state worth submitting to MiSTer-devel. **Maintainer decisions, 2026-08-23:** hardware is **both boards eventually** — a DE10-Nano **plus the SDRAM add-on** (mandatory: the NES reads cartridge ROM directly and the onboard DDR3 is too slow) and a SuperStation One (128 MB integrated), with **one `.rbf` booting both** turning "SS1 runs MiSTer cores unmodified" from an inherited claim into a measured one; mappers are **the top six** — NROM, MMC1, UxROM, CNROM, MMC3, AxROM, ~90% of the licensed library by title count, explicitly **not** FDS, expansion audio, or the remaining ~168 families; and v2.7.0 is **scoped to what genuinely fits**, with the arithmetic stated up front (**rung 3 8–16 wk · rung 4 4–8 wk · rung 5 2–4 wk + a 4–12 wk tail · rung 6 2–4 wk · rung 7 4–8 wk = 20–40 weeks FTE** before the AccuracyCoin tail, across twenty release slots — **milestones, not dates**). **Rung 6 comes before rung 7 deliberately**: NROM at 327 Kb fits on-chip, so hardware bring-up needs no memory controller, and getting a board in the loop before writing the SDRAM controller de-risks the second largest technical item. Two v2.5.0 gates — **nestest 0-diff and the 5 M-cycle window** — are not carried as debt but reclassified as **rung-3 acceptance criteria**: both stop at a `$2002` read where *both sides address it* and only the data differs, because the DUT has no PPU. The contribution requirements were **fetched from the MiSTer-devel wiki rather than recalled**, and one line of it is the whole case for this programme: on AI-generated code the project asks for *"a minimum reasonable bar for readability and… evidence of quality and accuracy testing"* — the co-simulation apparatus **is** that evidence, and no incumbent core can show its equivalent. See [`plans/v2.7.0-mister-core-plan.md`](plans/v2.7.0-mister-core-plan.md), [`mister/`](mister/), and the four dated research files in `ref-docs/`.
- **Historical detail — v2.2.4** (2026-07-24) — a **libretro / RetroArch distribution** cut whose purpose is that the RustyNES core **builds and installs cleanly through the Libretro buildbot** () for in-RetroArch use. **Zero emulation-core changes** — the deterministic `#![no_std]` chip stack, save-state / TAS / netplay formats, and every golden vector are byte-identical to v2.2.3, so **AccuracyCoin holds 141/141 (100.00%)**, nestest 0-diff, by construction. The work is a libretro-completeness audit + metadata correction: the core is confirmed to inherit every v2.2.3 change automatically (the fast-dot-path default, the `PPU_SNAPSHOT_VERSION` 8 / APU v4 save-state schema handled transparently by the dynamic `snapshot_core_into` sizing, the `Mapper::mix_audio` i32 widening, the Zapper model, and the `mNNN_` mapper rename), and both buildbot cross-ABIs the GitHub gate models — `x86_64-pc-windows-gnu` and `aarch64-linux-android` — build clean. `rustynes_libretro.info` (the metadata RetroArch's core downloader reads) is corrected: **`disk_control` `false` → `true`** (the FDS multi-side Disk Control interface has been wired since the buildbot recipe landed, but was advertised as absent — the real fix), `display_version` `v1.0.0` → `v2.2.4`, and the mapper count `168` → `172`. Also: the reviewer-tooling standardization onto the shared Antigravity template rides along (`scripts/agy-review.sh` + workflow). Documented libretro follow-up: **core options** (region / overscan / palette / accuracy toggles) remain unexposed (`core_options = "false"` is accurate, not stale) — a deliberate future enhancement, not a v2.2.4 gap. See `docs/STATUS.md` (single source of truth) + `CHANGELOG.md` `[2.2.4]` + `docs/libretro/`.
-- **Release line since v2.1.0:** the v2.1.x **"Fathom"** accuracy line (v2.1.0 → v2.1.10) → **v2.2.0 "Capstone"** (the milestone cut closing the "deepen the existing project" run) → **v2.2.1** (housekeeping) → **v2.2.2 "Conduit"** (build / distribution / CI-integrity) → **v2.2.3 "Datum"** (performance appraisal + the last two Holy Mapperel residuals closed) → **v2.2.4 "Cartridge"** (the libretro/RetroArch distribution cut) → **v2.2.5 "Colophon"** → **v2.2.6 "Almanac"** → **v2.2.7 "Timbre II"** → **v2.2.8 "Aperture II"** → **v2.2.9 "Studio II"** → **v2.3.0 "Datum II"** → **v2.3.1 "Plumb Line"** → **v2.3.2 "Lucid"** → **v2.3.3 "Cadence"** → **v2.3.4 "Ledger"** → **v2.3.5 "Manifest"** → **v2.3.6 "Sounding"** → **v2.3.7 "Overtone"** → **v2.3.8 "Parallax"** → **v2.3.9 "Crucible"** → the **v2.4.x "Fabric"** co-simulation line (**v2.4.1 "Fabric"** → **v2.4.2 "Cairn"** → **v2.4.3 "Touchstone"** → **v2.4.4 "Ignition"** → **v2.4.5 "Compass"** → **v2.4.6 "Abacus"** → **v2.4.7 "Keystone"** → **v2.4.8 "Palimpsest"** → **v2.4.9 "Plumbline II"** → **v2.5.0 "Rungwork"** → **v2.5.1 "Retrace"** → **v2.5.2 "Dormant"** → **v2.5.3 "Hysteresis"** → **v2.5.4 "Escapement"** → **v2.5.5 "Raster"** → **v2.5.6 "Vestige"** → **v2.5.7 "Collimation"** → **v2.5.8 "Blanking"** → **v2.5.9 "Overture"** → **v2.6.0 "Assay"** → **v2.6.1 "Interleave"** → **v2.6.2 "Witness"** → **v2.6.3 "Mainspring"** → **v2.6.4 "Rubric"**, the current release). AccuracyCoin holds **141/141** throughout — but not always *by construction*: v2.3.4, v2.3.7, v2.3.9 and the rung-3 releases v2.5.4-v2.5.6 change the core, so for those the number is **verified** rather than inherited, and saying which is which is the point. **Full per-release detail is in `CHANGELOG.md` and `docs/STATUS.md` (the single source of truth)** — the entries below (v2.1.0 "Fathom" was the prior anchor here; v2.0.8 → v2.0.1) are the older historical trail, retained rather than duplicated.
+- **Release line since v2.1.0:** the v2.1.x **"Fathom"** accuracy line (v2.1.0 → v2.1.10) → **v2.2.0 "Capstone"** (the milestone cut closing the "deepen the existing project" run) → **v2.2.1** (housekeeping) → **v2.2.2 "Conduit"** (build / distribution / CI-integrity) → **v2.2.3 "Datum"** (performance appraisal + the last two Holy Mapperel residuals closed) → **v2.2.4 "Cartridge"** (the libretro/RetroArch distribution cut) → **v2.2.5 "Colophon"** → **v2.2.6 "Almanac"** → **v2.2.7 "Timbre II"** → **v2.2.8 "Aperture II"** → **v2.2.9 "Studio II"** → **v2.3.0 "Datum II"** → **v2.3.1 "Plumb Line"** → **v2.3.2 "Lucid"** → **v2.3.3 "Cadence"** → **v2.3.4 "Ledger"** → **v2.3.5 "Manifest"** → **v2.3.6 "Sounding"** → **v2.3.7 "Overtone"** → **v2.3.8 "Parallax"** → **v2.3.9 "Crucible"** → the **v2.4.x "Fabric"** co-simulation line (**v2.4.1 "Fabric"** → **v2.4.2 "Cairn"** → **v2.4.3 "Touchstone"** → **v2.4.4 "Ignition"** → **v2.4.5 "Compass"** → **v2.4.6 "Abacus"** → **v2.4.7 "Keystone"** → **v2.4.8 "Palimpsest"** → **v2.4.9 "Plumbline II"** → **v2.5.0 "Rungwork"** → **v2.5.1 "Retrace"** → **v2.5.2 "Dormant"** → **v2.5.3 "Hysteresis"** → **v2.5.4 "Escapement"** → **v2.5.5 "Raster"** → **v2.5.6 "Vestige"** → **v2.5.7 "Collimation"** → **v2.5.8 "Blanking"** → **v2.5.9 "Overture"** → **v2.6.0 "Assay"** → **v2.6.1 "Interleave"** → **v2.6.2 "Witness"** → **v2.6.3 "Mainspring"** → **v2.6.4 "Rubric"** → **v2.6.5 "Muster"**, the current release). AccuracyCoin holds **141/141** throughout — but not always *by construction*: v2.3.4, v2.3.7, v2.3.9 and the rung-3 releases v2.5.4-v2.5.6 change the core, so for those the number is **verified** rather than inherited, and saying which is which is the point. **Full per-release detail is in `CHANGELOG.md` and `docs/STATUS.md` (the single source of truth)** — the entries below (v2.1.0 "Fathom" was the prior anchor here; v2.0.8 → v2.0.1) are the older historical trail, retained rather than duplicated.
- **Preceding release:** **RustyNES v2.0.8 "Harbor"** (2026-07-09) — the eighth release of the **v2.0.x mobile-finalization train** and the **iOS release candidate** ("Harborlight"), the final release of the iOS finalization window (**v2.0.5 → v2.0.8**). A **host / iOS-only** cut: the cycle-accurate core is **unchanged and byte-identical to v2.0.7** (AccuracyCoin still **141/141, 100.00%**; nestest 0-diff; `#![no_std]` chip stack untouched). It stages the App Store scaffolding for v2.1.0: version-controlled **App Store Connect listing metadata** (`fastlane/metadata/ios/{en-US,es-ES}/`, mirroring the Android tree, files-only), a **dormant App Store `release` lane** in `fastlane/Fastfile` that stages the build + listing but **does not submit** (`submit_for_review: false`) and is **not** CI-wired (the interim channel stays **TestFlight**), and an **App-Review §4.7 self-audit** (no bundled/downloadable ROMs, ownership notice, searchable library, 4+ rating) in `docs/ios-v2.0.8-readiness.md`. Version bump (workspace `2.0.7 → 2.0.8`; iOS `MARKETING_VERSION → 2.0.8`). **No store submission** (that is v2.1.0); screenshots, real signing, the listing upload, and the App-Review submission are the **maintainer / v2.0.9 / v2.1.0** closeout. See `docs/STATUS.md` (single source of truth) + `CHANGELOG.md` `[2.0.8]` + `docs/ios-v2.0.8-readiness.md` + `to-dos/plans/v2.0.5-v2.0.8-ios-finalization-plan.md`.
- **Earlier in the train:** **RustyNES v2.0.7 "Harbor"** (2026-07-09) — the seventh release of the **v2.0.x mobile-finalization train** and the **third iOS finalization release** ("Trim"), continuing the iOS window (**v2.0.5 → v2.0.8**). A **host / iOS-only** cut: the cycle-accurate core is **unchanged and byte-identical to v2.0.6** (AccuracyCoin still **141/141, 100.00%**; nestest 0-diff; `#![no_std]` chip stack untouched). It wires the **App Store submission floor** (Apple mandates the **iOS 26 SDK / Xcode 26** for every App Store Connect upload from **2026-04-28**, so the tag-gated iOS CI now selects the newest Xcode 26.x on the runner — a build-SDK pin, non-breaking fallback on older images), **reconciles the deployment target `iOS 15.0 → 17.0`** to match the code's real API floor (`NavigationStack` iOS 16 + `.topBarTrailing` iOS 17, unguarded at 12+ sites — the prior 15.0 was never buildable), and **re-audits `PrivacyInfo.xcprivacy`** against the v2.0.6 crash reporter (no new data type / required-reason API — local-only, backup-excluded, off by default). Version bump (workspace `2.0.6 → 2.0.7`; iOS `MARKETING_VERSION → 2.0.7`). **TestFlight-only** (App Store + AltStore PAL deferred to v2.1.0); on-device profiling + the Xcode-26 archive are a **maintainer / v2.0.9** step. See `docs/STATUS.md` (single source of truth) + `CHANGELOG.md` `[2.0.7]` + `docs/ios-v2.0.7-readiness.md` + `to-dos/plans/v2.0.5-v2.0.8-ios-finalization-plan.md`.
- **Earlier in the train:** **RustyNES v2.0.6 "Harbor"** (2026-07-09) — the sixth release of the **v2.0.x mobile-finalization train** and the **second iOS finalization release** ("Parity"), continuing the iOS window (**v2.0.5 → v2.0.8**). A **host / iOS-only** cut: the cycle-accurate core is **unchanged and byte-identical to v2.0.5** (AccuracyCoin still **141/141, 100.00%**; nestest 0-diff; `#![no_std]` chip stack untouched), so no accuracy / save-state / determinism number moves. It adds a **new opt-in, privacy-first crash-reporting surface** (off by default — the iOS analogue of the Android v1.8.8 `CrashReporter`, closing the v1.9.9 iOS-applicable deferral): **Settings → Diagnostics** installs an uncaught-`NSException` handler that writes **local** crash logs the user can view + copy in-app — **nothing is uploaded**, so the "Data Not Collected" privacy label is unchanged (EN + ES); the handler re-checks the live opt-in at crash time so opting out stops new logs immediately. It also records the **feature-parity re-verification** of the v1.9.x host features (Game Center, CloudKit save sync, MFi controllers, capture / PiP, accessibility) against the unchanged v2.0.0 bridge surface. Version bump (workspace `2.0.5 → 2.0.6`; iOS `MARKETING_VERSION → 2.0.6`). **TestFlight-only** (App Store + AltStore PAL deferred to v2.1.0); on-device crash-capture verification is a **maintainer / v2.0.9** step. See `docs/STATUS.md` (single source of truth) + `CHANGELOG.md` `[2.0.6]` + `docs/ios-v2.0.6-readiness.md` + `to-dos/plans/v2.0.5-v2.0.8-ios-finalization-plan.md`.
diff --git a/to-dos/mister/TASKS.md b/to-dos/mister/TASKS.md
index e7e9bc12..6d260649 100644
--- a/to-dos/mister/TASKS.md
+++ b/to-dos/mister/TASKS.md
@@ -181,8 +181,8 @@ Legend: `[ ]` open · `[~]` in progress · `[x]` done
routine, so a code is an index WITHIN one routine and two entries
sharing it share nothing. Producing the vector is v2.6.3; matching it is
v2.6.4
-- [ ] v2.6.4 status vector identical **entry-for-entry**, including `Skipped` and
- `NotRun` — **rung 5 closes**. State a floor, not a target
+- [x] status vector identical **entry-for-entry**, including `Skipped` and
+ `NotRun` — **rung 5 CLOSES at v2.6.5**. State a floor, not a target
- [x] All four remaining disagreements closed (`Dummy write cycles`,
`Open Bus`, `Interrupt flag latency`, `NMI Overlap BRK`). Every rule that
closed the last three is stated by AccuracyCoin's own source and two of
@@ -194,18 +194,35 @@ Legend: `[ ]` open · `[~]` in progress · `[x]` done
or PPU-misc suites. Measured: **4500 frames executes all 146**
(134,012,761 cycles). `accuracycoin_status` now prints coverage on every
comparison and **refuses** when any entry is unrun on both sides
- - [ ] The wide-window comparison itself. Its first run found a real RTL defect
+ - [x] The wide-window comparison itself. Its first run found a real RTL defect
at cycle 20,636,325 (`ppu_sel` following the halted CPU on a cycle the
DMA owned — ledger 3.12), fixed; the full-catalog agreement is the
measurement that decides whether the rung closes
+ - [x] **v2.6.5 closes it.** `146 of 146 entries executed on both sides`, none
+ `NotRun`, vector **IDENTICAL entry for entry**. Five PPU defects closed
+ the last six entries (ledger 3.37-3.43): the reload and shift clock
+ needing SEPARATE gates, the sprite X counters not gated on rendering,
+ the octal latch holding across the read dot, the `$2006` second-write
+ `v <- t` copy being DELAYED as the wiki states, and the pre-render line
+ CLEARING secondary OAM. Suite 72 → **87 green, 0 failed**. A two-dot
+ CPU/PPU alignment diagnosis is **retracted** — at the committed
+ alignment the consoles run identical pc/addr/access for 1,695,131
+ cycles, and a two-dot shift takes the differing share 5.13% → 66.80%
-## v2.6.5 – v2.6.6 — rung 6, MiSTer integration and hardware
+## v2.6.6 – v2.6.7 — rung 6, MiSTer integration and hardware
-- [ ] v2.6.5 `sys/` verbatim; `emu` module; `hps_io`; `CE_PIXEL` video; `CONF_STR`
+> **Re-scoped, and recorded rather than done silently.** This block was
+> v2.6.5 – v2.6.6. Rung 5 did not close at v2.6.4, so under the ladder rule —
+> a rung may not start until the one below is green — v2.6.5 was spent closing
+> it and rung 6 moved down one slot. Rung 5 closed at v2.6.5; rung 6 opens at
+> v2.6.6.
+
+- [ ] v2.6.6 `sys/` verbatim; `emu` module; `hps_io`; `CE_PIXEL` video; `CONF_STR`
OSD; `VIDEO_ARX/ARY` at **8:7, set deliberately**; `files.qip`, `.sdc`,
`clean.bat`; **Quartus timing closure**; first `.rbf`
-- [ ] v2.6.6 hardware bring-up: DE10-Nano + SDRAM add-on, SuperStation One,
- **one `.rbf` boots both**; on-device AccuracyCoin — **rung 6 closes**
+- [ ] v2.6.7 hardware bring-up: DE10-Nano + SDRAM add-on, SuperStation One,
+ **one `.rbf` boots both**; on-device AccuracyCoin — **rung 6 closes**.
+ Blocked on hardware this machine does not have
## v2.6.7 – v2.6.9 — rung 7, memory and mappers
diff --git a/to-dos/plans/v2.6.5-rung5-close-plan.md b/to-dos/plans/v2.6.5-rung5-close-plan.md
new file mode 100644
index 00000000..71afc9da
--- /dev/null
+++ b/to-dos/plans/v2.6.5-rung5-close-plan.md
@@ -0,0 +1,184 @@
+# v2.6.5 — rung 5 closes: the DUT runs the whole catalog
+
+## Why this is not the planned v2.6.5
+
+`to-dos/mister/TASKS.md` scheduled v2.6.5 as **rung 6's opening** — `sys/`
+verbatim, the `emu` module, `hps_io`, `CE_PIXEL` video, the `CONF_STR` OSD,
+Quartus timing closure and the first `.rbf`.
+
+That work is **deferred to v2.6.6**, because the ladder rule is strict: *a rung
+may not start until the one below is green*. Rung 5 did not close in v2.6.4, and
+the reason is measured rather than suspected.
+
+This is a re-scope, and it is recorded rather than made silently.
+
+## What v2.6.4 established
+
+All nine AccuracyCoin disagreements closed and the status vector agreed entry
+for entry — **over a 600-frame window that reaches 88 of 146 entries**. Widening
+the golden to 4500 frames, where all 146 execute, showed the DUT does not
+survive the run.
+
+Counting entries that have written a result, both sides, same window:
+
+| run length | oracle | DUT |
+|---|---|---|
+| 17.9 M | 88 | **88** |
+| 20.8 M | 95 | — |
+| 41.7 M | 117 | — |
+| 60.0 M | 120 | **5** |
+| 100 M | — | **5** |
+| 134 M | **146** | **5** |
+
+The five are the whole `Power On State` suite. At 60 M the DUT's PC is a
+three-cycle self-loop at `$80DF` — `INC $EC` / `JMP $80DF`, AccuracyCoin's
+**menu idle loop** — so it is not stuck inside a test.
+
+**One reading was published and retracted**: "a hang in `PPU Behavior`", inferred
+from catalog order. The PC probe refuted it.
+
+## The deliverable
+
+Rung 5's acceptance, over a window where the whole catalog executes:
+
+> `accuracycoin_status` reports **0 of 146 differing** and **0 entries `NotRun`
+> on both sides**, with the golden at 4500 frames.
+
+The second clause is v2.6.4's addition — the acceptance it replaces was
+satisfiable by a window that ran 88 entries, and was.
+
+## The gate
+
+```bash
+make -C tb accuracycoin-gate # goldens/ is the 4500-frame export
+```
+
+Green means the coverage line reads `146 of 146 entries executed on both sides`
+and the vector is identical. `accuracycoin_status` now **refuses** anything less,
+so a partial window cannot report success.
+
+## Task 1 — reset, or re-entry? (the discriminating measurement)
+
+The DUT reaching the menu with only the power-on suite recorded is consistent
+with **two different defects**:
+
+- the console **reset** and re-ran its boot path, or
+- the ROM **re-entered** the battery, clearing results, without any reset.
+
+They need different searches, so this is settled before anything else.
+`VECTOR_PROBE=1` reports every 6502 vector fetch — `$FFFA` / `$FFFC` / `$FFFE` —
+with its cycle and instruction PC. **A `$FFFC` fetch after boot is an
+unambiguous reset**; its absence is equally decisive the other way.
+
+**ANSWERED: neither. The console does not reset.** Over 60 M cycles the probe
+counts **one** `$FFFC` fetch, at cycle 6 — the power-on one — against 1,445 NMIs
+and 67 IRQ/BRKs. The last vector fetch with a non-menu PC is at cycle 20,278,274
+in a counter wait loop matching `TEST_NmiAndIrq`, after which every fetch carries
+the menu idle loop's PC. So the DUT leaves the battery at ~20.28 M and never
+returns. v2.6.4's published claim that it resets is retracted, in the release
+body and in `docs/rung5-accuracycoin.md`.
+
+## Task 2 — bracket it
+
+**SUPERSEDED by a better instrument, and this is the lesson of the version.**
+Bisecting a result count would have hunted the *symptom*. Diffing the oracle's
+own AccuracyCoin run against the DUT's — on `pc`/`bus_addr`/`bus_access`, data
+excluded per ledger 3.1c — names the *cause*, at a cycle, in one pass.
+
+Three defects found this way, each behind the last:
+
+| # | first divergence | mechanism | ledger |
+|---|---|---|---|
+| 1 | 11,434,938 | the absolute-Y RMW illegals read their own instruction stream | 3.13 |
+| 2 | 15,814,594 | a reload DMC DMA armed as a *load*, halting one cycle late | 3.7b |
+| 3 | 20,427,313 | the `$4015` read-clear applied on the CPU cycle, not the APU one | 3.14 |
+| 4 | 20,844,575 | the controller port followed the CPU's address and shifted on the wrong edge | 3.15 |
+| 5 | 29,748,792 | palette RAM was eight bits wide; `$2007` returned the stored byte whole | 3.16 |
+| 6 | 31,656,882 | OAMADDR was never cleared during ticks 257-320 | 3.17 |
+| — | **none over 41.7 M** | — | — |
+
+Records differing on the compare surface: **23.40% -> 1.77%** after the second.
+Defect 3 sits at 20.43 M, inside the departure window Task 2 set out to bracket.
+
+Widening the reference to 1400 frames (41,692,794 cycles) carried the chain to
+**zero**: `pc`, `bus_addr` and `bus_access` are IDENTICAL across all 41,692,786
+overlapping records.
+
+## Where the version stands
+
+Measured on the deliverable's own gate — the 4500-frame battery, 134,012,761
+cycles:
+
+| | at the version's start | at its close |
+|---|---|---|
+| entries executed on both sides | 5 | **146 of 146** |
+| entries `NotRun` on one side only | 141 | **0** |
+| entries differing | 22 | **0** |
+
+**BOTH clauses are met and the rung CLOSES.** `make -C tb accuracycoin-gate`
+exits 0:
+
+```text
+coverage: 146 of 146 entries executed on both sides (0 on neither, 0 on one side only)
+status vectors are IDENTICAL entry for entry across all 146 entries.
+```
+
+Twenty defects closed, ledger 3.13-3.33 in the sibling's
+`docs/oracle-vs-documentation.md`.
+
+**`$2007 Stress Test` CLOSED** (3.30, 3.32, 3.33) — the PPU DATA state machine,
+which no nesdev page describes and AccuracyCoin's own MIT-licensed source
+specifies with a gate-level diagram and a half-cycle timeline. A `$2007` read
+does not fill the buffer at the CPU access dot: the access ENDING starts a
+machine with ALE at t2 and the memory read at t4, the increment lands on the
+read dot as a delay line (so a DMC DMA's repeated access increments twice, which
+is that test's whole subject), and the sprite window drives its nametable
+address on the ALE dots as well as the read dots. Every one of the 170 graded
+entries now matches; the single residual is ungraded, one of the reads the ROM
+itself calls unstable.
+
+The four that remained, all now closed (ledger 3.37-3.43 in the sibling):
+
+| entry | code | what it needs |
+|---|---|---|
+| `ALE + Read` | 2 | the ROM says outright "if you haven't passed the `$2007` Stress Test, you probably won't pass this one" — that prerequisite is now met, so this is the next one to try |
+| `Hybrid Addresses` | 2 | same machinery; the pre-increment low byte composed with post-increment high bits already falls out of 3.30 |
+| `BG Serial In` | 2 | its stated rule IS implemented (3.22); something else that test depends on is missing |
+| `Sprite 0 Hit behavior` | 12 | 3.27 — the mechanism is understood and the literal fix was implemented, measured to break `Sprites On Scanline 0`, and reverted |
+
+Two of the five are already characterised down to the change that will be
+needed, and one carries a rejected fix with its measurement so the next attempt
+does not repeat it.
+
+Two readings from that table are worth keeping. The differing **share is not
+monotone** — defect 4's fix moved it 23.40% -> 25.26% while moving the fork
+8.9 M cycles further in, because the share counts disagreement anywhere in the
+window and a later fork can be followed by faster divergence. Trace position is
+the metric that moves monotonically. And **defect 4 was partly a defect in the
+ORACLE**: `Controller::write_strobe` dropped an owed shift unconditionally on
+both sides, and fixing it moved AccuracyCoin's `Controller Clocking` from success
+code 2 (Famicom) to code 1 (NES / AV Famicom). ADR 0037 says the oracle can be
+wrong; this is the first time the co-simulation has proved it.
+
+Each fix moved the divergence rather than closing it, which is the expected shape
+and not a shortfall: it says the fix was right and that another defect waits
+behind it. Keep a control trace from before each fix — the first difference
+between the pre- and post-fix runs should be exactly the cycle aimed at, and that
+is what proves the stimulus and alignment were identical rather than assuming it.
+
+## Risks
+
+1. **The trail is long.** The first divergence may be millions of cycles before
+ the symptom. Trace position, not the result count, is the metric that moves.
+2. **The oracle can be wrong.** 141/141 is not "matches silicon". If the DUT and
+ the oracle disagree about a documented behaviour, the wiki adjudicates, not
+ the oracle.
+3. **A fix that greens the gate is not evidence the fix is right** — a
+ compensating error is indistinguishable by gate result. Keep looking after it
+ works.
+
+## Out of scope
+
+`sys/`, the `emu` module, `hps_io`, video, the OSD, Quartus timing closure and
+the `.rbf` — all v2.6.6. Hardware bring-up remains blocked on a DE10-Nano with
+the SDRAM add-on, which this machine does not have.