From 5c418ad3fe22df36a18d3682cbe122b92ede07b3 Mon Sep 17 00:00:00 2001 From: DoubleGate Date: Wed, 26 Aug 2026 00:15:58 -0400 Subject: [PATCH 01/11] docs: the v2.6.4 plan, and the firewall rule its first task uncovered Opens v2.6.4 on the oracle side. The work itself is in the sibling; this is the plan document and one standing rule. ## The plan `to-dos/plans/v2.6.4-rung5-close-plan.md`. Rung 5's acceptance is a status vector that agrees ENTRY FOR ENTRY, and the gate is stated as a measurement before the work rather than after it: accuracycoin_status .ram.bin .ram.bin -> 0 of 146 differ with the standing rule for the AccuracyCoin tail applied: **a floor, not a target.** If the nine do not all close, the version ships the number it reached and names every remaining entry. It does not widen the comparison, exclude an entry, or restate the count to look better. The document records the refuted hypotheses alongside the confirmed one, because a dead hypothesis is a result: phantom PRG-RAM at $6000-$7FFF (refuted by a byte-identical re-run) and the SH implementation itself (refuted by 29,774 exact cycles with six of six mutations caught). ## The firewall rule `AGENTS.md` gains: **the provenance firewall is per-REGION, not per-repo.** "RustyNES's code is ours, so it is readable" is true of the repository and NOT uniformly true of every block in it. Eleven files carry a `// Provenance:` header disclosing that a region is derived from a GPL reference emulator, and those regions are black boxes for HDL purposes even though the file is ours -- writing SystemVerilog from them launders the original expression into the DUT through us, which is what ADR 0037 exists to prevent. Found on v2.6.4's first task: its headline job is the five SH-group stores in the DUT, and `crates/rustynes-cpu/src/cpu.rs` line 3 discloses exactly that group as derived from Mesen2's `SyaSxaAxa`. The single most relevant block of oracle source for this version is one that must not be read to write the RTL, and nothing in the tooling said so. The rule carries the maintainer-directed escalation ladder -- vendored docs, then the open Internet, then black-box output comparison, then the derived source as a last resort with the derivation declared in the SIBLING too. In practice rungs 1-3 were sufficient and the escalation went unspent, which is worth recording: the pull toward reading a working implementation is strongest exactly when the two disagree, and that is the moment the rule matters. ## Note on this branch Cut from `origin/main` at the v2.6.3 merge (700d7c58), per the standing rule to branch from `origin/main` and never from an in-flight or just-merged branch head. --- AGENTS.md | 2 + to-dos/plans/v2.6.4-rung5-close-plan.md | 165 ++++++++++++++++++++++++ 2 files changed, 167 insertions(+) create mode 100644 to-dos/plans/v2.6.4-rung5-close-plan.md diff --git a/AGENTS.md b/AGENTS.md index 81027b73..317c380c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -35,6 +35,8 @@ - **NO OVER-ATTRIBUTION.** Do not tag a genuine oracle *comparison* ("matches Mesen2's behavior," "cross-checked against ares") as "derived from." Attribute real ports; leave genuinely-independent code independent. - **DO NOT SELF-CERTIFY.** Never assert "no third-party code is incorporated" / "license-clean" as a finished claim. Surface provenance status for human + expert review; state uncertainty. AI self-attestation of license compliance is not trustworthy — an outside NESdev reviewer, not the tooling, is what caught this. +- **THE FIREWALL IS PER-REGION, NOT PER-REPO — the oracle's own source has exceptions (2026-08-26).** "RustyNES's code is ours, so it is readable" is true of the repository and **not uniformly true of every block in it.** Eleven files carry a `// Provenance:` header disclosing that a REGION of them is derived from a GPL reference emulator, and those regions are **black boxes for HDL purposes even though the file is ours** — writing SystemVerilog from them launders the original expression into the DUT *through* us, which is precisely what ADR 0037 exists to prevent. This was found on the first task of v2.6.4: its headline job is fixing the five `SH`-group stores in the DUT, and `crates/rustynes-cpu/src/cpu.rs` line 3 discloses `SHA/SHX/SHY/SHS/TAS` as **derived from Mesen2's `SyaSxaAxa`** (`Core/NES/NesCpu.h`) — so the single most relevant block of oracle source for that version is one that must not be read to write the RTL. Nothing in the tooling said so. **Before reading oracle source to inform sibling/HDL work, `grep -n "Provenance:" `**; regenerate the list with `grep -rn "^// Provenance:" crates/*/src/*.rs` rather than trusting a snapshot. The ones that bite HDL work are `rustynes-cpu/src/cpu.rs` (SH group — rungs 1/5), `rustynes-apu/src/blip.rs` (BLEP), and six mapper files (rung 7). **The escalation ladder, maintainer-directed, in order — exhaust each rung before the next:** (1) vendored public documentation; (2) **the open Internet** — the vendored wiki is PARTIAL, documenting `SHX`/`SHY` in full and carrying nothing on `SHA`/`TAS`, which one web search supplied; (3) **black-box comparison** — a per-cycle golden diff needs no source at all and usually resolves faster, because at that point the question is "which cycle differs", not "what is the rule"; (4) the derived oracle source, **last resort**. Rung 4 is permitted — the licences are compatible, both repos being GPL-3.0-or-later — **but the existing attributions live in the ORACLE, and the sibling is a separate repository**, so reading it obliges declaring the derivation there too: a site comment, the sibling's provenance doc, `NOTICE`, and an ADR 0037 amendment naming who authorised it. Never silently. On v2.6.4 rungs 1-3 were sufficient and the escalation went unspent — worth knowing, because the pull toward rung 4 is strongest exactly when the DUT and the oracle disagree, which is the moment this rule matters. + Enforcement lives alongside the prose: `/ref-proj/` is gitignored/`.dockerignore`d/`.markdownlintignore`d and excluded from CodeRabbit; `deny.toml` gates dependency licenses; every derived file carries an SPDX + provenance header. A rule the tooling enforces beats a rule you are merely asked to follow. ## What this is diff --git a/to-dos/plans/v2.6.4-rung5-close-plan.md b/to-dos/plans/v2.6.4-rung5-close-plan.md new file mode 100644 index 00000000..97c5558b --- /dev/null +++ b/to-dos/plans/v2.6.4-rung5-close-plan.md @@ -0,0 +1,165 @@ +# v2.6.4 — rung 5 closes: the status vector agrees entry for entry + +**Line:** v2.4.1 → v2.7.0 "Fabric". **Rung:** 5 (NROM + AccuracyCoin). +**Repos:** the work is in `RustyNES_MiSTer`; the oracle carries the ceremony. + +## What v2.6.3 closed, and what it did not + +v2.6.3 put the DUT on one master clock, closed the decoder at 256 of 256 +opcodes, made blargg's `instr_test-v5` a standing gate at 16 of 16, and ran +AccuracyCoin **end to end** — the full 17,868,316 cycles. + +It did **not** make the two sides agree. The status vector reports **137 of 146 +entries agreeing and 9 differing**. v2.6.3's stated scope was to *produce* a +comparable vector; closing the nine is this version. + +## The nine + +| entry | oracle | DUT | +|---|---|---| +| Dummy write cycles | Pass | Fail(1) | +| Open Bus | Pass | Fail(7) | +| `$93` SHA indirect,Y | Pass(1) | Fail(7) | +| `$9F` SHA absolute,Y | Pass(1) | Fail(7) | +| `$9B` SHS absolute,Y | Pass(1) | Fail(7) | +| `$9C` SHY absolute,X | Pass | Fail(7) | +| `$9E` SHX absolute,Y | Pass | Fail(7) | +| Interrupt flag latency | Pass | Fail(10) | +| NMI Overlap BRK | Pass | Fail(2) | + +**Six share `Fail(code 7)`.** v2.6.3's own notes require testing the +shared-cause hypothesis before treating them as six items, and that is task 1. + +## The gate + +Measurable, and stated before the work: + +``` +accuracycoin_status goldens/AccuracyCoin.ram.bin /tmp/AccuracyCoin.dut.ram.bin +``` + +reports **0 of 146 entries differ**, exit 0, on a vector that is not vacuous. + +**A floor, not a target** (the standing rule for the AccuracyCoin tail): if the +nine do not all close, this version ships the number it reached, names each +remaining entry, and says what is known about it. It does **not** widen the +comparison, exclude an entry, or restate the count to look better. Rung 5 closes +only at zero. + +Plus, unchanged: the 66-gate suite stays green, every new or changed gate is +demonstrated to fail by mutation, and the oracle's own AccuracyCoin 141/141 (RAM +decoder) and nestest 0-diff are re-run rather than assumed. + +## Task 1 — the shared cause, tested not assumed + +**Hypothesis A (leading): PRG-RAM at `$6000-$7FFF`.** `accuracycoin-gate` does +not pass `--prg-ram`, so `cart_has_prg_ram = 0` and the DUT answers **open bus** +there. The oracle's `m000_nrom.rs` allocates 8 KiB **unconditionally**, so it +answers RAM. AccuracyCoin's iNES header declares **0** PRG-RAM units and no +battery, so on a real NROM board there is nothing there — which makes the DUT +the more accurate side and the oracle's phantom RAM a known, already-recorded +limitation. + +The discriminating experiment is one run: re-run the gate **with** `--prg-ram` +and diff the resulting status vector against the baseline. If the six move +together, the cause is shared and is this. If they do not, the hypothesis is +dead and the six are separate. + +Either outcome is a result. **If the oracle turns out to be the wrong side, the +fix is oracle-side and the ledger entry says so** — the ladder has already +caught the oracle once at this rung. + +**Hypothesis B:** the SH group's `H` for the indirect-Y forms. `$93` resolves +its base through a zero-page pointer, so "high byte of the literal address" is +the pointer's high byte, not an operand's. A DUT using the wrong `adh` there +would fail `$93` but not `$9F` — the vector shows **both** failing, which is +evidence against B being the whole story but not against it contributing. + +## Progress — task 1 is closed, and the premise was wrong + +**AccuracyCoin: 9 differ -> 4 differ. All five SH-group entries are closed.** + +Both shared-cause hypotheses were refuted by measurement, and the fix came from +somewhere neither predicted: + +| step | result | +|---|---| +| Hypothesis A — phantom PRG-RAM at `$6000-$7FFF` | **dead**: re-run with `--prg-ram` gave a **byte-identical** RAM dump, so the ROM never touches the window | +| Hypothesis B — the SH implementation | **dead**: `shgroup046` matched on **29,774/29,774** cycles, 6 of 6 mutations caught. Value rule, page-cross mangle and register selection were already correct | +| Hypothesis C — the RDY clause | **held**: `shdma047` (same stores, DMC running underneath) diverged on **30** cycles, every one a mangled byte where the oracle stored the plain register | +| Residual after fixing C | four absolute forms closed, **`$93` alone still failing** — which named the next defect | +| The `tcyc` is addressing-mode dependent | `SHA (d),Y` is six cycles, so `tcyc==3` is its **pointer-high fetch**, not its dummy read. Closed `$93`. **0 divergences on both ROMs.** | + +**The `Fail(code 7)` grouping is retired.** It assumed error codes are +cause-specific; nothing documents them, AccuracyCoin's source is not vendored, +and the outcome disproved it — the five SH entries and `Open Bus` share the code +and did **not** share a cause. + +Two findings worth carrying forward: + +- **The defect was a conditional clause of the same wiki sentence that gave the + rule.** A conditional never exercised looks identical to a rule that is simply + correct. Two stimulus ROMs were built to find what the second half of one + sentence already said. +- **A fix that improves without closing is evidence.** Four-of-five closing, and + the one indirect form remaining, pointed straight at the addressing-mode + dependence — and the residual's polarity was *inverted* from the original + defect, which only became reachable after the first fix. + +## Remaining four + +`Dummy write cycles` (1), `Open Bus` (7), `Interrupt flag latency` (10), +`NMI Overlap BRK` (2). None is SH. The last two are both interrupt-sequence +behaviour and may share a cause **with each other** — stated as a hypothesis to +test, not as a finding, per the lesson above. + +## Task 2 — the three that name their own mechanism + +`Dummy write cycles` (code 1), `Interrupt flag latency` (code 10) and +`NMI Overlap BRK` (code 2) are separate and each gets its own stimulus ROM, +golden and per-cycle diff, in the established rung-1/2 shape. + +`Interrupt flag latency` and `NMI Overlap BRK` are both interrupt-sequence +behaviour, so they may share a cause with each other even though they do not +share a code. + +## Provenance constraint — binding on this version specifically + +Five of the nine are the `SH` group, and `crates/rustynes-cpu/src/cpu.rs` +discloses that group as **derived from Mesen2** (`SyaSxaAxa`, `Core/NES/NesCpu.h`). +The escalation ladder, in order: + +1. vendored public documentation +2. the open Internet +3. **black-box comparison** — per-cycle golden diff, which needs no source +4. the derived oracle source, last resort + +Rungs 1-2 are done: the wiki fully specifies `SHX`/`SHY` and the public +literature gives the value formulas for all five (`A&X&(H+1)`, `X&(H+1)`, +`Y&(H+1)`, and `TAS` additionally `SP = A&X`). The DUT already matches those +formulas, so the defect is **not** the value rule and rung 3 is the right tool. + +If rung 4 is reached, the maintainer has authorised it (2026-08-26) — but the +existing attributions live in the **oracle**, and the sibling is a separate +repository, so the derivation must then be declared there too: site comment, +sibling provenance doc, `NOTICE`, and an ADR 0037 amendment naming the +authorisation. Never silently. + +## Out of scope + +`sys/`, the `.rbf`, and MiSTer integration are **v2.6.5**. Hardware bring-up is +v2.6.6 and needs a DE10-Nano plus the SDRAM add-on, which this machine does not +have. Mappers beyond NROM are rung 7. + +## Risks + +1. **The nine may not share a cause**, making this nine investigations rather + than one. Mitigated by testing the shared-cause hypothesis first and cheaply. +2. **The oracle may be the wrong side** on `Open Bus`, which does not close the + entry so much as move it. That is a result, not a failure, and it goes in the + accuracy ledger. +3. **AccuracyCoin's error codes are not documented** in this corpus, so a code + names a mechanism only by inference. Per-cycle diffing does not depend on + decoding them. +4. **The run is 17.8 M cycles**, so iteration is slow. Prefer a targeted + stimulus ROM with its own golden over re-running the full battery. From 4e4e1097d465dd80467761939b3f3081090b5a35 Mon Sep 17 00:00:00 2001 From: DoubleGate Date: Wed, 26 Aug 2026 03:25:41 -0400 Subject: [PATCH 02/11] test(accuracycoin): add two per-entry sub-test ROMs, and record one that is misnamed The full AccuracyCoin battery is 17,868,316 CPU cycles and needs a START press at frame 300, so observing ONE entry costs a full run and yields a byte that says only pass or fail. That is the right acceptance surface for rung 5 and the wrong iteration loop, and v2.6.4 spent a working session paying for it. `scripts/accuracycoin-build/build_sub_test_rom.py` already solves this: it patches the upstream source to boot straight into one entry and assembles through wine plus the upstream `nesasm.exe`. Twenty-six such ROMs were vendored in Session 23 and nothing had been built since. Two more land here: * `sub-tests/open-bus.nes` (--suite 0 --test 7) verdict at $0408 * `sub-tests/nmi-overlap-brk.nes` (--suite 11 --test 1) verdict at $0462 Both reach their verdict from a cold boot with no input, in 4,467,08x cycles against the battery's 17.9M, and both PASS on the oracle -- so a DUT divergence against them is unambiguous. `sub-tests/iflag-latency.nes`, already vendored, reaches its verdict in 893,422 cycles, a factor of twenty. MEASURED WHILE DOING IT: `sub-tests/cpu-open-bus.nes` DOES NOT RUN `Open Bus`. Its verdict lands at $0407, which the catalog assigns to `Dummy write cycles`, and $0408 is never written -- it is off by one row of `Suite_CPUBehavior`. A valid stimulus under the wrong name, and nothing had caught it because a fixture's name is not evidence about its content; anyone reading $0408 out of its dump would have read a byte the ROM never wrote. It is kept as-is in case a gate already references it, and the README now says so plainly. The README also carries the build recipe (the four files to fetch, the suite and test indices, the wine invocation) so the next one does not have to be rediscovered, and the warning that result addresses are not always the catalog's. --- tests/roms/AccuracyCoin/README.md | 43 ++++++++++++++++++ .../sub-tests/nmi-overlap-brk.nes | Bin 0 -> 40976 bytes .../roms/AccuracyCoin/sub-tests/open-bus.nes | Bin 0 -> 40976 bytes to-dos/plans/v2.6.4-rung5-close-plan.md | 36 +++++++++++++++ 4 files changed, 79 insertions(+) create mode 100644 tests/roms/AccuracyCoin/sub-tests/nmi-overlap-brk.nes create mode 100644 tests/roms/AccuracyCoin/sub-tests/open-bus.nes diff --git a/tests/roms/AccuracyCoin/README.md b/tests/roms/AccuracyCoin/README.md index 76ecee7a..48fcd1c9 100644 --- a/tests/roms/AccuracyCoin/README.md +++ b/tests/roms/AccuracyCoin/README.md @@ -16,6 +16,49 @@ The runtime `.nes` ROM lives at [`../accuracycoin/AccuracyCoin.nes`](../accuracy harness loads the ROM from a workspace-root-relative path while the compile-time `include_str!` reaches for a different one. +## Building another sub-test (v2.6.4) + +`sub-tests/nmi-overlap-brk.nes` was added in v2.6.4 and is the first one built +after the original batch. The recipe, so the next one does not have to be +rediscovered: + +```bash +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 +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 +``` + +`--suite` is the 0-based index into `TableTable` and `--test` the 0-based row +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. + +**`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 +`Suite_CPUBehavior` — a valid stimulus under the wrong name. It is kept as-is +rather than renamed, because a gate may already reference it; the correctly +built one is **`sub-tests/open-bus.nes`** (`--suite 0 --test 7`), added in the +same release and verified to report at `$0408`. + +Two of the three ROMs used for v2.6.4's rung-5 work therefore report at their +catalog addresses and one does not, which is why the paragraph below says to +read the address out of a RAM diff. + +**Why these matter for co-simulation.** The full battery is 17,868,316 CPU +cycles and needs a START press at a specific frame. A sub-test reaches its +verdict in **0.9M** (`iflag-latency`) to **4.5M** (`nmi-overlap-brk`) cycles from +boot, with no input at all — so a DUT iteration that took minutes takes seconds, +and the verdict byte names one assertion instead of one entry among 146. The +result addresses are **not** always the catalog's: `iflag-latency` and +`nmi-overlap-brk` report at their catalog addresses (`$0461`, `$0462`) and +`cpu-open-bus` reports at **`$0407`**, one below its catalog `$0408`. Read the +address out of a RAM diff rather than assuming. + ## Catalog format ```text diff --git a/tests/roms/AccuracyCoin/sub-tests/nmi-overlap-brk.nes b/tests/roms/AccuracyCoin/sub-tests/nmi-overlap-brk.nes new file mode 100644 index 0000000000000000000000000000000000000000..ebb31432c882029410337751cdc979e6f7f9691a GIT binary patch literal 40976 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

S3>%vyPcyz~q#O<<+M_<3tmoFam~Ao^&|%X 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 literal 0 HcmV?d00001 diff --git a/tests/roms/AccuracyCoin/sub-tests/open-bus.nes b/tests/roms/AccuracyCoin/sub-tests/open-bus.nes new file mode 100644 index 0000000000000000000000000000000000000000..80ecebab5aa92b2c58b7752bbdd5136732bda69d GIT binary patch literal 40976 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 d%aI test 5) read as + *progress* earlier in this same session. +- `Open Bus` code 7 is "Reading from `$4015` does not update the data bus", and + its stimulus is `LDX #$16 / LDA $40FF,X` — the exact instruction the trace + divergence had already been localised to, independently. +- `Interrupt flag latency` code 10 is test A: "Do branches poll for interrupts + before cycle 3? (They should not)". +- `NMI Overlap BRK` code 2 is its answer-key check — a 32-entry sweep of the + hijack window, one PPU cycle later per iteration. + +**A test ROM is stimulus, not a reference implementation**, so reading it raises +no firewall question. The rule this adds to the project: check the licence, then +read the ROM's source *before* theorising about its verdict. + +**A second thing that was already in the corpus**: +`tests/roms/AccuracyCoin/sub-tests/` holds 26 ROMs that boot straight into one +target test. `iflag-latency.nes` reaches its verdict in 30 frames / 893,422 +cycles against the full battery's 17,868,316 — a targeted stimulus that needed +no building at all. + ## Task 2 — the three that name their own mechanism `Dummy write cycles` (code 1), `Interrupt flag latency` (code 10) and From 743a8f36783a529aa14da58b66e1357ccad84444 Mon Sep 17 00:00:00 2001 From: DoubleGate Date: Wed, 26 Aug 2026 04:13:32 -0400 Subject: [PATCH 03/11] feat(test-harness): refuse an AccuracyCoin comparison that covers only part of the catalog `accuracycoin_status` reported total=146 pass=80 pass_with_code=8 fail=0 skipped=0 not_run=58 unknown=0 status vectors are IDENTICAL entry for entry across all 146 entries. and that sentence is true. **Fifty-eight of the 146 entries were NotRun on BOTH sides.** Two consoles agreeing about a test neither executed is not evidence about that test. The comparator was working as designed: its vacuity guard refuses an ALL-NotRun vector, which this is not. What was short was the STIMULUS WINDOW. Broken down by suite, the 600-frame golden reaches the CPU catalog and stops partway through `CPU Interrupts`: reached CPU Behavior, addressing-mode wraparound, all seven unofficial opcode groups, Unofficial Immediates, Power On State, and 2 of 3 CPU Interrupts 88 NOT reached APU Registers and DMA (10), APU Tests (9), PPU Behavior (7), PPU VBlank Timing (7), Sprite Evaluation (9), PPU Misc. (10), CPU Behavior 2 (5), and `NMI Overlap IRQ` 58 So the run that certified three closed CPU entries asked the co-simulation DUT no question at all about its PPU or its APU -- the two chips rungs 3 and 4 exist for. MEASURED, NOT ESTIMATED: 4500 frames executes all 146 (134,012,761 CPU cycles), where the oracle's own vector reads `pass=130 pass_with_code=16 fail=0 not_run=0`. WHAT LANDS HERE. The property is enforced rather than documented: * `both_not_run` counts entries NotRun on BOTH sides. A ONE-sided NotRun is a real disagreement -- it is the case the acceptance wording was written for -- and must not be absorbed into this count. * `coverage_gate` prints a coverage line on EVERY two-file comparison, so a reader who sees "identical" can tell how much of the catalog that covers, and REFUSES with a non-zero exit when the count is non-zero. * Both are extracted from `main` and tested directly. A check that exists only inside `main` is a check no test can reach, which is how this property came to be missing in the first place. Four tests, three mutations, all three CAUGHT (`both_not_run` using OR; `coverage_gate` never refusing; one dead entry tolerated). The mutation run needed its classifier fixed first: `cargo test` prints `error: test failed` when a TEST fails, so a classifier keyed on `^error` reported BUILD-FAILED for three mutations that were all catches -- discarding evidence rather than manufacturing it, but wrong either way. `Skipped` on both sides is deliberately NOT missing coverage: the ROM reached the entry and declined it, which is the same distinction the vacuity guard draws. Docs: STATUS.md, README, docs/mister.md, the plan and the mister task board all carry the coverage number beside the agreement number. v2.6.3's reading that six entries "sharing one failure code" implied one shared cause is retracted in the same pass -- `TEST_Fail` reports `(ErrorCode << 2) | 2` and the runner sets `ErrorCode` to 1 before every routine, so a code indexes within one routine and two entries sharing it share nothing. No chip-crate change, so AccuracyCoin 141/141 (RAM decoder) and nestest 0-diff hold by construction. --- README.md | 15 ++- .../src/bin/accuracycoin_status.rs | 118 +++++++++++++++++- docs/STATUS.md | 20 ++- docs/mister.md | 29 +++++ to-dos/mister/TASKS.md | 11 +- to-dos/plans/v2.6.4-rung5-close-plan.md | 61 +++++++++ 6 files changed, 239 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index d3d9177d..30f0635c 100644 --- a/README.md +++ b/README.md @@ -683,8 +683,12 @@ It builds on **v2.4.1 "Fabric"**, which opened the **v2.5.1 → v2.7.0** line that builds the rest of the console. **Rung 3 (the 2C02) closed at v2.5.8 and rung 4 (the 2A03) at v2.6.2**; rung 5 — the NROM cartridge and the console's own bus — is in progress, with the sibling at -**66 gates green** and its `nes_top` now dividing one master clock rather than -taking its clock enables from the testbench. Fabric's subject was: a new NES core written in SystemVerilog from +**72 gates green** and its `nes_top` now dividing one master clock rather than +taking its clock enables from the testbench. AccuracyCoin's status vector agrees +entry for entry, and the number to read beside that is **how many entries the +run reaches**: the window used through v2.6.3 covered 88 of 146 and asked +nothing about the PPU or APU suites, so the golden moved to a measured +4500-frame window where all 146 execute. Fabric's subject was: 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 into a Cyclone V bitstream, and Rust does @@ -899,10 +903,11 @@ green: rung 0 the compare surface, rung 1 the 6502, rung 2 the bus and interrupts, rung 3 the 2C02, rung 4 the 2A03, rung 5 AccuracyCoin parity, rung 6 hardware bring-up, rung 7 mappers. **Rungs 0-4 are closed; rung 5 is the current work** — the NROM cartridge, the work RAM, the CPU bus, the controller ports and -DMC DMA are all landed and gated (**66 gates green**), the core's own top level +DMC DMA are all landed and gated (**72 gates green**), the core's own top level assembles them, and it divides one master clock rather than taking its clock -enables from the testbench. The first end-to-end AccuracyCoin run is what -remains. Every rung is labelled in +enables from the testbench. AccuracyCoin runs end to end and its status vector +is compared entry for entry; what remains is coverage — the run window is now +wide enough to execute all 146 entries rather than the CPU half. Every rung is labelled in [`docs/mister.md`](docs/mister.md) by whether it has an **independent** oracle — because 141/141 on AccuracyCoin is not the same as "matches silicon", and a rung verified only against this emulator inherits whatever this emulator has wrong. diff --git a/crates/rustynes-test-harness/src/bin/accuracycoin_status.rs b/crates/rustynes-test-harness/src/bin/accuracycoin_status.rs index 4cfbfbf9..37faaeeb 100644 --- a/crates/rustynes-test-harness/src/bin/accuracycoin_status.rs +++ b/crates/rustynes-test-harness/src/bin/accuracycoin_status.rs @@ -71,6 +71,56 @@ fn vacuous(v: &[TestStatus]) -> bool { v.iter().all(|s| matches!(s, TestStatus::NotRun)) } +/// Entries that are `NotRun` on **both** sides. +/// +/// The vacuity guard above catches an all-`NotRun` vector. It does not catch the +/// PARTIAL case, and v2.6.4 walked straight into it: this tool reported +/// "IDENTICAL entry for entry across all 146 entries" while **58 of the 146** +/// were `NotRun` on both sides, because the run window reached the CPU suites +/// and stopped. Two consoles agreeing about a test neither executed is not +/// evidence about that test, and the suites that went unasked were the APU and +/// PPU ones -- exactly what the preceding rungs exist for. +/// +/// A comparison is only the rung-5 gate when the whole catalog EXECUTED, so this +/// is reported on every two-file run and refused when non-zero. +fn both_not_run(a: &[TestStatus], b: &[TestStatus]) -> usize { + a.iter() + .zip(b) + .filter(|(x, y)| matches!(x, TestStatus::NotRun) && matches!(y, TestStatus::NotRun)) + .count() +} + +/// Report how much of the catalog the two runs actually EXECUTED, and refuse a +/// partial comparison. +/// +/// Returns `Some(exit)` when the comparison must not proceed. Split out of +/// `main` so it can be tested directly: the property it enforces is the one +/// v2.6.4 found missing, and a check that only exists inside `main` is a check +/// no test can reach. +fn coverage_gate(a: &[TestStatus], b: &[TestStatus]) -> Option { + // Printed unconditionally so the number is visible even on a clean run -- a + // reader who sees only "identical" has no way to tell how much of the + // catalog that sentence covers. + let dead = both_not_run(a, b); + println!( + "\ncoverage: {} of {} entries executed on both sides ({} NotRun on both)", + a.len() - dead, + a.len(), + dead + ); + if dead == 0 { + return None; + } + eprintln!( + "\nPARTIAL: {dead} entries are NotRun on BOTH sides, so this comparison \ + says nothing about them. AccuracyCoin needs a long enough window to \ + reach the whole catalog -- 4500 frames executes all 146, where 600 \ + reaches only the CPU suites. Re-export the golden with more --frames; \ + agreement over a subset is not the rung-5 gate." + ); + Some(ExitCode::from(4)) +} + fn main() -> ExitCode { let args: Vec = std::env::args_os().skip(1).map(PathBuf::from).collect(); if args.is_empty() || args.len() > 2 { @@ -143,6 +193,10 @@ fn main() -> ExitCode { return ExitCode::from(3); } + if let Some(code) = coverage_gate(&a, &b) { + return code; + } + let diffs: Vec<_> = catalog() .iter() .zip(a.iter().zip(b.iter())) @@ -174,9 +228,71 @@ fn main() -> ExitCode { #[cfg(test)] mod tests { - use super::{describe, vacuous}; + use super::{both_not_run, coverage_gate, describe, vacuous}; use rustynes_test_harness::accuracy_coin_catalog::{TestStatus, catalog, decode_results}; + /// The refusal itself, not just its predicate. Reached directly because a + /// check that only exists inside `main` is a check no test can reach -- + /// which is how the missing property got missed in the first place. + #[test] + fn a_partial_comparison_is_refused_and_a_full_one_is_not() { + let n = catalog().len(); + let full = vec![TestStatus::Pass; n]; + assert!( + coverage_gate(&full, &full).is_none(), + "a comparison covering the whole catalog must proceed" + ); + + let mut a = vec![TestStatus::Pass; n]; + let mut b = vec![TestStatus::Pass; n]; + a[0] = TestStatus::NotRun; + b[0] = TestStatus::NotRun; + assert!( + coverage_gate(&a, &b).is_some(), + "one entry neither side ran is enough to refuse: agreement over a \ + subset is not the gate" + ); + } + + /// The PARTIAL-coverage guard, added in v2.6.4 after the tool reported + /// "IDENTICAL entry for entry across all 146 entries" over a comparison in + /// which 58 of those entries were `NotRun` on both sides. + #[test] + fn entries_not_run_on_both_sides_are_counted() { + let n = catalog().len(); + let mut a = vec![TestStatus::Pass; n]; + let mut b = vec![TestStatus::Pass; n]; + assert_eq!(both_not_run(&a, &b), 0, "two full vectors hide nothing"); + + a[3] = TestStatus::NotRun; + b[3] = TestStatus::NotRun; + assert_eq!(both_not_run(&a, &b), 1, "one entry neither side ran"); + + // NotRun on ONE side is a real disagreement, not dead coverage -- it is + // the case the acceptance wording was written for and must not be + // absorbed into the partial count. + a[4] = TestStatus::NotRun; + assert_eq!( + both_not_run(&a, &b), + 1, + "a one-sided NotRun is a difference, not missing coverage" + ); + } + + /// `Skipped` is a result the ROM wrote, so a pair of them is coverage, not + /// its absence -- the same distinction the vacuity guard draws. + #[test] + fn skipped_on_both_sides_is_not_missing_coverage() { + let n = catalog().len(); + let a = vec![TestStatus::Skipped; n]; + let b = vec![TestStatus::Skipped; n]; + assert_eq!( + both_not_run(&a, &b), + 0, + "Skipped means the ROM reached the entry and declined it" + ); + } + /// The guard this tool exists for. A vector of nothing but `NotRun` /// describes a run that executed no tests, and reporting two of those as /// agreement is the failure mode the whole binary is built to refuse. diff --git a/docs/STATUS.md b/docs/STATUS.md index bee115a1..533e431b 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -17,7 +17,7 @@ > **no observation ports**, so the unit Quartus compiles is the unit that lints > clean. The DUT's 6502 now decodes **all 256 opcodes**, and **blargg's > `instr_test-v5` battery is a standing gate at 16 of 16 exact** — rung 1's first -> *independent* oracle, taking the suite to **66 gates green, 0 failed**. It +> *independent* oracle, taking the suite to **72 gates green, 0 failed**. It > found three defects the self-written corpus had missed: `RRA` taking the > pre-instruction carry into its ADC stage, the indirect RMW forms addressing > the indexed target during their pointer fetch, and the PPU I/O-bus latch never @@ -40,10 +40,20 @@ > which decodes a work-RAM dump against the 146-entry catalog and compares two > of them **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 — five `SH`-group -> stores and Open Bus — which is a pattern a pass count of 137 would have -> hidden. **Producing the vector is v2.6.3's deliverable; making the two agree -> is v2.6.4.** Reaching it cost **two false passes before a real one**: the ROM +> entries agree, 9 differ** — a pattern a pass count of 137 would have hidden. +> (v2.6.3 read six of those as "sharing one failure code" and therefore one +> cause; **retracted in v2.6.4** — `TEST_Fail` reports `(ErrorCode << 2) | 2` +> and the runner sets `ErrorCode` to 1 before every routine, so a code indexes +> within one routine and two entries sharing it share nothing.) +> +> **v2.6.4 closed all nine and then found the number underneath them.** The +> vector reported identical entry for entry across all 146 — with **58 of those +> entries `NotRun` on both sides**, because the 600-frame window reaches the CPU +> suites and stops partway through `CPU Interrupts`. It asks nothing about the +> APU, PPU, sprite-evaluation or PPU-misc suites. The golden is now the measured +> **4500-frame** window where all 146 execute (134,012,761 cycles). Read the +> 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 diff --git a/docs/mister.md b/docs/mister.md index b1ea8f9e..972dfeed 100644 --- a/docs/mister.md +++ b/docs/mister.md @@ -1111,6 +1111,35 @@ address-bus cause rather than six independent defects. **Producing the vector is v2.6.3's deliverable. Making the two agree is v2.6.4**, and the plan says so in its own acceptance row. +**The "six sharing one failure code" reading was wrong, and v2.6.4 measured why.** +AccuracyCoin's `TEST_Fail` reports `(ErrorCode << 2) | 2` and the runner sets +`ErrorCode` to 1 before *every* test routine, so the code is an index **within +one routine**: `Open Bus`'s code 7 is its own seventh assertion and `SHA (abs),Y`'s +code 7 is that routine's seventh. Two entries sharing a code share nothing. What +actually closed the five `SH` entries was the SH group's RDY-conditional store +and its addressing-mode-dependent dummy cycle — a real shared cause, identified +from the opcodes. `Open Bus` was untouched by that work and remained, which +should have refuted the shape argument at the time. + +**And the number underneath the agreement.** Once all nine closed, the vector +reported identical entry for entry across all 146 — with **58 of those entries +`NotRun` on both sides**. The comparator was right; the run window was short. +Broken down by suite, 600 frames reaches the CPU catalog and stops partway +through `CPU Interrupts`, so the run asked the DUT nothing about the APU, PPU, +sprite-evaluation or PPU-misc suites — the chips rungs 3 and 4 exist for. +Measured rather than estimated, **4500 frames reaches all 146** (134,012,761 +cycles), and that is the golden the gate now uses. A pass count is a claim about +what ran, and what ran has to be measured separately. + +Decoding the codes properly is also what closed two of the last three, in +v2.6.4: `Open Bus` (a `$4015` read does not drive the data bus, and its D5 is +open bus) and `Interrupt flag latency` (the interrupt poll is the second-to-last +cycle, and branches poll before cycles 2 and 4 but never before 3). Both rules +are stated by the test ROM's own comments and by neither of the nesdev pages the +implementation was written from. `NMI Overlap BRK` is carried to v2.6.5 with its +disagreement measured per sweep step rather than as one byte — see the sibling's +`docs/rung5-accuracycoin.md`. + Two properties are worth recording, because both are about what the comparison *refuses* rather than what it reports: diff --git a/to-dos/mister/TASKS.md b/to-dos/mister/TASKS.md index 204af814..f3009086 100644 --- a/to-dos/mister/TASKS.md +++ b/to-dos/mister/TASKS.md @@ -174,10 +174,13 @@ Legend: `[ ]` open · `[~]` in progress · `[x]` done diffs two of them **entry for entry**, including `Skipped` and `NotRun`, naming disagreements by test rather than by address. It refuses an all-`NotRun` vector — the case that looks like success to a - naive comparison. **First measurement: 137 of 146 agree, 9 differ**, six - sharing one failure code (five `SH`-group stores and Open Bus), which - reads as one shared address-bus cause. Producing the vector is v2.6.3; - matching it is v2.6.4 + naive comparison. **First measurement: 137 of 146 agree, 9 differ**. The + reading that six of those "shared one failure code" and so shared a + cause is **retracted** (v2.6.4): `TEST_Fail` reports + `(ErrorCode << 2) | 2` and the runner sets `ErrorCode` to 1 before every + 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 diff --git a/to-dos/plans/v2.6.4-rung5-close-plan.md b/to-dos/plans/v2.6.4-rung5-close-plan.md index 856f8e3c..b16fcbc9 100644 --- a/to-dos/plans/v2.6.4-rung5-close-plan.md +++ b/to-dos/plans/v2.6.4-rung5-close-plan.md @@ -159,6 +159,67 @@ golden and per-cycle diff, in the established rung-1/2 shape. behaviour, so they may share a cause with each other even though they do not share a code. +## Progress — all three closed + +| entry | before | after | ledger | +|---|---|---|---| +| `Open Bus` | Fail(7) | **Pass** | 3.9 — a `$4015` read does not drive the data bus; its D5 is open bus | +| `Interrupt flag latency` | Fail(10) | **Pass** | 3.10 — branches do not poll before their third cycle | +| `NMI Overlap BRK` | Fail(2) | **Pass** | 3.11 — the interrupt sequence does not poll; the hijack window was one cycle narrow | + +All three closures came from the ROM's own source rather than from more tracing, +and every one is **category 1** — the implementation followed documentation that +is true and insufficient: + +- `Open Bus` needed a rule the nesdev wiki does not state at all. +- `Interrupt flag latency` needed a branch exception the wiki does not mention, + under a sentence about poll timing that reads naturally as the DUT's existing + behaviour. +- `NMI Overlap BRK` needed one rule the wiki *does* state and this core had + missed (an interrupt sequence does not poll), plus a hijack window the wiki + describes only qualitatively. + +**Two wrong readings are recorded with their corrections**, because each was +plausible and each cost a measurement: + +1. A `Fail(N)` decoded as a zero-based index made a **regression** (test 7 -> + test 5) read as *progress*, and that description reached a code comment before + the ROM's `TEST_Fail` macro and its initialiser were read. +2. The first poll fix moved the poll for **every** instruction, not just + branches. It closed the failing entry and regressed two standing APU-IRQ + gates — which is the measurement saying the change was wider than the defect, + since all three compare against the same oracle. Narrowed to branches alone, + nothing regresses. + +**Three per-entry stimuli are now standing verdict gates** (`iflag-latency`, +`open-bus`, `nmi-overlap-brk`), deliberately not bus gates — their per-cycle +surfaces are dominated by ledger 3.1c, which is still open. + +## The gate met its own wording, and the wording was not enough + +With the three closed, `accuracycoin_status` reported: + +``` +total=146 pass=80 pass_with_code=8 fail=0 skipped=0 not_run=58 unknown=0 +status vectors are IDENTICAL entry for entry across all 146 entries. +``` + +That is this plan's acceptance row, word for word — *identical entry for entry, +including `Skipped` and `NotRun`*. **And 58 of the 146 are `NotRun` on both +sides.** The wording was chosen so a DUT that skipped a test could not pass by +skipping it; it does not cover the case where *neither* side runs it, which is +what a short stimulus window produces. + +By suite, the 600-frame golden reaches the CPU catalog and stops partway through +`CPU Interrupts`. Every APU, PPU, sprite-evaluation and PPU-misc entry is +`NotRun` — 58 of them, and they are the entries rungs 3 and 4 were built for. + +**Measured: 4500 frames reaches all 146** (134,012,761 CPU cycles; the oracle +there reports `pass=130 pass_with_code=16 fail=0 not_run=0`). The golden moves to +that window. Whether the DUT agrees over the full vector is the measurement that +actually decides whether rung 5 closes, and this plan's acceptance row should +have said *and no entry is `NotRun` on both sides*. + ## Provenance constraint — binding on this version specifically Five of the nine are the `SH` group, and `crates/rustynes-cpu/src/cpu.rs` From f580f8f0f734cb6e39e3b642f060e41848029e88 Mon Sep 17 00:00:00 2001 From: DoubleGate Date: Wed, 26 Aug 2026 04:57:19 -0400 Subject: [PATCH 04/11] =?UTF-8?q?chore(release):=20cut=20v2.6.4=20"Rubric"?= =?UTF-8?q?=20=E2=80=94=20the=20last=20four=20AccuracyCoin=20disagreements?= =?UTF-8?q?=20close,=20and=20then=20the=20gate=20that=20certified=20them?= =?UTF-8?q?=20is=20measured=20to=20cover=2088=20of=20146=20entries?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A rubric is the authoritative statement of the rules, written by whoever set the test. That is literally where all three of this release's fixes came from. THE THREE ENTRIES. v2.6.3 left nine differing; the SH group closed five, and this release closes the last three. Every one is category 1 -- the implementation followed documentation that is true and insufficient. AccuracyCoin is MIT-licensed and its assembly source is one `curl` away. The v2.6.4 plan's own note that it "is not vendored" is true of this repository and had been read as "unavailable"; they are not the same statement. The source carries a prose explanation of every assertion, written by the author who chose the stimulus, and it settled all three in minutes: * `Open Bus` -- a read of $4015 does not drive the data bus, and its D5 is open bus. The nesdev pages document the $4016/$4017 case and say nothing about $4015 being exempt; the ROM states the mechanism outright, and its stimulus 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. Not in the wiki at all. * `NMI Overlap BRK` -- an interrupt sequence does not poll (stated plainly in the wiki and simply missed here), and the BRK hijack window was one CPU cycle too narrow at its late edge. TWO WRONG READINGS, BOTH KEPT WITH THEIR CORRECTIONS. A `Fail(N)` names test N of that routine, ONE-BASED: `TEST_Fail` reports `(ErrorCode << 2) | 2` and the runner sets `ErrorCode` to 1 before dispatching. Decoded as a zero-based index it is off by one, and that error made a change from test 7 to test 5 read as partial progress when it was a REGRESSION that also broke a standing gate. v2.6.3's reading that six entries "sharing one failure code" implied one shared cause is retracted with it: the code indexes within one routine, so two entries sharing it share nothing. A fix that closes one gate and opens another is a scope measurement, not a trade-off. The first poll fix also moved the GENERIC poll to the second-to-last cycle; it closed the entry and regressed `apupulse026` and `blargg08`. All three compare against the same oracle, so one model satisfies all three. Narrowed to the branch exception alone, nothing regresses. PER-ENTRY STIMULUS. `build_sub_test_rom.py` builds a ROM that boots straight into one catalog entry; twenty-six were vendored in Session 23 and nothing had been built since. Two more land here, and three are standing VERDICT gates (69 -> 72 green, 0 failed) -- verdict rather than bus by measurement, since their per-cycle surfaces are dominated by the open PPU I/O-latch item at 2,331,867 of 4,467,082 cycles on one of them. Also measured: `sub-tests/cpu-open-bus.nes` does not run `Open Bus` -- its verdict lands at $0407, which the catalog assigns to `Dummy write cycles`. THEN THE COVERAGE FINDING, WHICH IS WHAT THE RELEASE IS ACTUALLY ABOUT. With the four closed the vector reported "identical entry for entry across all 146 entries" -- with 58 of those entries `NotRun` on BOTH sides. The comparator was correct; the stimulus window was short. By suite it reaches the CPU catalog and stops partway through `CPU Interrupts`, asking nothing about the APU, PPU, sprite-evaluation or PPU-misc suites -- the chips rungs 3 and 4 exist for. Measured: 4500 frames executes all 146 (134,012,761 cycles). `accuracycoin_status` now enforces it: a coverage line on every comparison and a non-zero exit when any entry is unrun on both sides, with the predicate and the refusal extracted from `main` and tested directly. A check that exists only inside `main` is a check no test can reach, which is how this property came to be missing. 3 of 3 mutations caught. WIDENING IT PAID ON THE FIRST RUN. The wider window aborted at cycle 20,636,325, 2.8 M cycles past where every previous run had stopped: a halted CPU mid-LDA $2007 held `ppu_sel` high through a DMC steal, so on the get cycle the mux preferred the PPU and handed the sample fetch the read buffer instead of the cartridge byte. The comment above the line stated the intent it violated. RUNG 5 DOES NOT CLOSE. Measured on both sides over the same window, the oracle climbs 88 -> 95 -> 117 -> 120 -> 146 while the DUT goes 88 -> 5 -> 5 -> 5, flat for 74 M cycles. That is a hang in the suites that had never executed, not a restart. Reported as unavailable rather than as "141 of 146 differ", which is one defect and not 141. VERIFICATION sibling ./tb/regress.sh 72 passed, 0 failed RTL mutations 7 of 8 CAUGHT; the eighth INERT by byte-compare accuracycoin_status mutations 3 of 3 CAUGHT AccuracyCoin (RAM decoder) pass rate = 100.00% over 141 assigned tests nestest 0-diff release_anchor_audit 11 passed libretro_info_audit 3 passed fmt / clippy -D warnings / rustdoc / no_std / markdownlint clean No `rustynes-{cpu,ppu,apu,mappers,core}` changes, so the two accuracy numbers hold by construction -- and were run anyway. --- .github/release-notes/v2.6.4.md | 176 ++++++++++++++++++ AGENTS.md | 4 +- ARCHITECTURE.md | 2 +- CHANGELOG.md | 148 +++++++++++++++ Cargo.lock | 38 ++-- Cargo.toml | 2 +- OVERVIEW.md | 6 +- README.md | 4 +- ROADMAP.md | 4 +- SECURITY.md | 2 +- SUPPORT.md | 2 +- VERSION-PLAN.md | 5 +- crates/rustynes-cosim/Cargo.lock | 12 +- crates/rustynes-cosim/Cargo.toml | 2 +- .../rustynes-libretro/rustynes_libretro.info | 2 +- docs/STATUS.md | 2 +- to-dos/ROADMAP.md | 4 +- to-dos/mister/TASKS.md | 15 ++ 18 files changed, 385 insertions(+), 45 deletions(-) create mode 100644 .github/release-notes/v2.6.4.md diff --git a/.github/release-notes/v2.6.4.md b/.github/release-notes/v2.6.4.md new file mode 100644 index 00000000..b3db3cc2 --- /dev/null +++ b/.github/release-notes/v2.6.4.md @@ -0,0 +1,176 @@ +# RustyNES v2.6.4 "Rubric" + +**The last four AccuracyCoin disagreements close — every rule that closed them +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: no file under `crates/rustynes-{cpu,ppu,apu,mappers,core}` +is touched, so **AccuracyCoin 141/141 (100.00%, RAM decoder)** and **nestest +0-diff** hold by construction — and were run anyway. + +A rubric is the authoritative statement of the rules, written in the margin by +the person who set the test. That is literally where all three of this release's +fixes came from. + +## The three entries, and where their rules actually live + +v2.6.3 left nine entries differing; the SH group closed five, and this release +closes the last three. **Every one is category 1 — the implementation followed +documentation that is true and insufficient.** + +- **`Open Bus`.** A read of `$4015` does not drive the data bus, and its D5 is + open bus. Both rules are properties of the board, so both live in + `cpu_bus.sv`. The nesdev `Open_bus_behavior` page documents the + `$4016`/`$4017` case and says nothing about `$4015` being exempt; AccuracyCoin + states the mechanism outright — "all the values read here are internal to the + 2A03 chip, so the data bus isn't used" — and its stimulus is `LDX #16 / LDA + 40FF,X`, 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. Not in the wiki at all. +- **`NMI Overlap BRK`.** Two rules: an interrupt sequence does not poll (stated + plainly in the wiki and simply missed here), and the BRK hijack window was one + CPU cycle too narrow at its late edge. + +## What the codes meant, and the regression that read as progress + +AccuracyCoin's `TEST_Fail` reports `(ErrorCode << 2) | 2`, and the runner sets +`ErrorCode` to **1** before every test routine — so `Fail(N)` names test **N** of +that routine, one-based. Read as a zero-based index it is off by one, and that +error made a change from test 7 to test 5 read as *partial progress* when it was +a **regression** that also broke a standing gate. The description reached a code +comment before the macro was read. v2.6.3's reading that six entries "sharing one +failure code" implied one shared cause is retracted with it: the code is an index +within one routine, so two entries sharing it share nothing. + +## A fix that closes one gate and opens another is a scope measurement + +The first poll fix moved the poll for **every** instruction from the last cycle to +the second-to-last, on the strength of the same ROM's walk-through of `CLI`. It +closed the failing entry and regressed `apupulse026` and `blargg08`. All three +compare against the same oracle, so one model satisfies all three and a change +that cannot is not that model. Narrowed to the branch exception alone, the entry +still passes and nothing regresses. + +Similarly, the hijack's comment argued at length for a delayed pending flag and +named this very AccuracyCoin entry as the case that would catch the alternative. +The entry was then run, and it says the opposite. The wrong comment is kept +beside the fix. + +## Per-entry stimulus: the battery was the wrong iteration loop + +`scripts/accuracycoin-build/build_sub_test_rom.py` builds a ROM that boots +straight into one catalog entry. Twenty-six were vendored in Session 23 and +nothing had been built since; two more land here — `open-bus.nes` and +`nmi-overlap-brk.nes` — and all three used by this release are **standing verdict +gates**. They reach their verdict from a cold boot with no input in 0.9M to 4.5M +cycles against the battery's 17.9M. + +They are verdict gates rather than bus gates by measurement, not preference: +their per-cycle surfaces are dominated by the PPU I/O-latch difference this +project has left open, 2,331,867 of 4,467,082 cycles on one of them. A gate +cannot distinguish an open ledger item from a defect; the ROM's own verdict byte +can. `tb/subtest_verdict.py` refuses when the oracle side is not itself a pass. + +Also measured: **`sub-tests/cpu-open-bus.nes` does not run `Open Bus`** — its +verdict lands at `$0407`, which the catalog assigns to *Dummy write cycles*. Off +by one table row, and nothing had caught it because a fixture's name is not +evidence about its content. + +## The gate met its own wording, and the wording was not enough + +With the four closed, the status vector reported **identical entry for entry +across all 146 entries** — and **58 of those entries were `NotRun` on both +sides**. The acceptance was worded so a DUT could not pass a test by skipping +it; it does not cover the case where *neither* side runs it, which is what a +short window produces. + +By suite, the 600-frame golden reaches the CPU catalog and stops partway through +`CPU Interrupts`. Every APU, PPU, sprite-evaluation and PPU-misc entry was +`NotRun` — the entries rungs 3 and 4 exist for. **Measured: 4500 frames executes +all 146** (134,012,761 cycles), and that is the golden now. + +`accuracycoin_status` enforces it rather than merely documenting it: it prints a +**coverage** line on every comparison and **refuses** with a non-zero exit when +any entry is `NotRun` on both sides. The predicate and the refusal are both +extracted from `main` and tested directly — a check that exists only inside +`main` is a check no test can reach, which is how this property came to be +missing. + +## Widening the window found a defect on its first run + +Moving the golden to 4500 frames was coverage work. The first run of the wider +window **aborted at cycle 20,636,325** — 2.8 million cycles past where every +previous run in this programme had stopped: + +```text +DMA data disagrees at cycle 20636325 addr=$FFC0: rtl=$04 harness=$00 +``` + +A halted CPU mid-`LDA $2007` held `ppu_sel` high through a DMC steal, so on the +DMC's get cycle — where the bus address is the sample address — the mux preferred +the PPU and handed the sample fetch the read buffer instead of the cartridge +byte. **The comment directly above the offending line stated the intent it +violated**: the write half of the exception was there (an OAM DMA targets +`$2004` and the PPU must see it), the read half was not. `ppu_sel` now follows +whichever master owns the bus. + +It was localised with `DMA_PROBE_CYC`, a new env-gated per-cycle dump of both bus +masters — necessary because the harness's cross-checks abort the run and the +abort path does not flush `--bus-out`, so a divergence they catch cannot be read +back from the trace. + +## Rung 5 does NOT close, and the reason is a DUT defect + +The full-catalog comparison is unavailable, and the first explanation for that +was wrong. Measured on **both sides over the same window**, counting entries that have +written a result: + +| run length | oracle | DUT | +|---|---|---| +| 17.9M | 88 | **88** | +| 20.8M | 95 | — | +| 41.7M | 117 | — | +| 60.0M | 120 | **5** | +| 100M | — | **5** | +| 134M | **146** | **5** | + +The oracle climbs monotonically to the full catalog. The DUT falls to five and +stays there for seventy-four million cycles. A battery that had merely completed +a pass and restarted would show the count climbing again. + +The five are the whole **Power On State** suite and nothing else — the group a +pass writes early. So the DUT restarts somewhere after 17.9M, completes Power On +State, and then **produces no further result for 74M cycles**. Catalog order puts +`PPU Behavior` immediately after it: the first of the suites that had never once +executed under the old window. That is a hypothesis with an obvious next +measurement, and it is recorded as one. + +Reported as unavailable rather than as a result. Taken at face value the +comparison reads `141 of 146 entries differ`, which would be a badly wrong thing +to publish — it is one defect, not 141. The comparator does classify them as +disagreements rather than as missing coverage, which is correct: they are +`NotRun` on one side only, the exact distinction the acceptance wording exists +for. + +--- + +## Verification + +| gate | result | +|---|---| +| sibling `./tb/regress.sh` | **72 passed, 0 failed** (69 + three new rung-5 verdict gates) | +| RTL mutations | **7 of 8 CAUGHT**; the eighth classified INERT by byte-comparison | +| `accuracycoin_status` mutations | **3 of 3 CAUGHT** | +| AccuracyCoin (oracle, RAM decoder) | **141/141 (100.00%)** | +| nestest | **0-diff** | +| `cargo fmt` / `clippy -D warnings` / rustdoc / `no_std` | clean | + +## What is carried to v2.6.5 + +Rung 5. The full-catalog comparison is blocked on a **DUT hang** in the suites +that had never executed — measured on both sides over the same window, the +oracle climbs 88 → 95 → 117 → 120 → 146 while the DUT goes 88 → 5 → 5 → 5. +Catalog order puts `PPU Behavior` immediately after the last suite the DUT +completes, which is the next measurement rather than a finding. diff --git a/AGENTS.md b/AGENTS.md index 317c380c..3a2698b5 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.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.4 "Rubric"** (2026-08-26) — the last four AccuracyCoin disagreements close, every rule that closed them 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.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.3 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.4 "Rubric"** (the last four AccuracyCoin disagreements close, every rule that closed them 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. - **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. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index db6d3d9d..b4dfac5b 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.3 (the scheduling model is v2.0.0 "Timebase" onward) +**Applies to:** RustyNES v2.6.4 (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 5dabec4e..2322a9eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,154 @@ cycle-accurate core later replaced. ## [Unreleased] +## [2.6.4] - 2026-08-26 - "Rubric" (the last four AccuracyCoin disagreements close, every rule 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 +the person who set the test. That is literally where all three of this release's +fixes came from. + +### The three entries, and where their rules actually live + +v2.6.3 left nine entries differing; the SH group closed five, and this release +closes the last three. **Every one is category 1 — the implementation followed +documentation that is true and insufficient.** + +- **`Open Bus`.** A read of `$4015` does not drive the data bus, and its D5 is + open bus. Both rules are properties of the board, so both live in + `cpu_bus.sv`. The nesdev `Open_bus_behavior` page documents the + `$4016`/`$4017` case and says nothing about `$4015` being exempt; AccuracyCoin + states the mechanism outright — "all the values read here are internal to the + 2A03 chip, so the data bus isn't used" — and its stimulus is `LDX #16 / LDA + 40FF,X`, 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. Not in the wiki at all. +- **`NMI Overlap BRK`.** Two rules: an interrupt sequence does not poll (stated + plainly in the wiki and simply missed here), and the BRK hijack window was one + CPU cycle too narrow at its late edge. + +### What the codes meant, and the regression that read as progress + +AccuracyCoin's `TEST_Fail` reports `(ErrorCode << 2) | 2`, and the runner sets +`ErrorCode` to **1** before every test routine — so `Fail(N)` names test **N** of +that routine, one-based. Read as a zero-based index it is off by one, and that +error made a change from test 7 to test 5 read as *partial progress* when it was +a **regression** that also broke a standing gate. The description reached a code +comment before the macro was read. v2.6.3's reading that six entries "sharing one +failure code" implied one shared cause is retracted with it: the code is an index +within one routine, so two entries sharing it share nothing. + +### A fix that closes one gate and opens another is a scope measurement + +The first poll fix moved the poll for **every** instruction from the last cycle to +the second-to-last, on the strength of the same ROM's walk-through of `CLI`. It +closed the failing entry and regressed `apupulse026` and `blargg08`. All three +compare against the same oracle, so one model satisfies all three and a change +that cannot is not that model. Narrowed to the branch exception alone, the entry +still passes and nothing regresses. + +Similarly, the hijack's comment argued at length for a delayed pending flag and +named this very AccuracyCoin entry as the case that would catch the alternative. +The entry was then run, and it says the opposite. The wrong comment is kept +beside the fix. + +### Per-entry stimulus: the battery was the wrong iteration loop + +`scripts/accuracycoin-build/build_sub_test_rom.py` builds a ROM that boots +straight into one catalog entry. Twenty-six were vendored in Session 23 and +nothing had been built since; two more land here — `open-bus.nes` and +`nmi-overlap-brk.nes` — and all three used by this release are **standing verdict +gates**. They reach their verdict from a cold boot with no input in 0.9M to 4.5M +cycles against the battery's 17.9M. + +They are verdict gates rather than bus gates by measurement, not preference: +their per-cycle surfaces are dominated by the PPU I/O-latch difference this +project has left open, 2,331,867 of 4,467,082 cycles on one of them. A gate +cannot distinguish an open ledger item from a defect; the ROM's own verdict byte +can. `tb/subtest_verdict.py` refuses when the oracle side is not itself a pass. + +Also measured: **`sub-tests/cpu-open-bus.nes` does not run `Open Bus`** — its +verdict lands at `$0407`, which the catalog assigns to *Dummy write cycles*. Off +by one table row, and nothing had caught it because a fixture's name is not +evidence about its content. + +### The gate met its own wording, and the wording was not enough + +With the four closed, the status vector reported **identical entry for entry +across all 146 entries** — and **58 of those entries were `NotRun` on both +sides**. The acceptance was worded so a DUT could not pass a test by skipping +it; it does not cover the case where *neither* side runs it, which is what a +short window produces. + +By suite, the 600-frame golden reaches the CPU catalog and stops partway through +`CPU Interrupts`. Every APU, PPU, sprite-evaluation and PPU-misc entry was +`NotRun` — the entries rungs 3 and 4 exist for. **Measured: 4500 frames executes +all 146** (134,012,761 cycles), and that is the golden now. + +`accuracycoin_status` enforces it rather than merely documenting it: it prints a +**coverage** line on every comparison and **refuses** with a non-zero exit when +any entry is `NotRun` on both sides. The predicate and the refusal are both +extracted from `main` and tested directly — a check that exists only inside +`main` is a check no test can reach, which is how this property came to be +missing. + +### Widening the window found a defect on its first run + +Moving the golden to 4500 frames was coverage work. The first run of the wider +window **aborted at cycle 20,636,325** — 2.8 million cycles past where every +previous run in this programme had stopped: + +```text +DMA data disagrees at cycle 20636325 addr=$FFC0: rtl=$04 harness=$00 +``` + +A halted CPU mid-`LDA $2007` held `ppu_sel` high through a DMC steal, so on the +DMC's get cycle — where the bus address is the sample address — the mux preferred +the PPU and handed the sample fetch the read buffer instead of the cartridge +byte. **The comment directly above the offending line stated the intent it +violated**: the write half of the exception was there (an OAM DMA targets +`$2004` and the PPU must see it), the read half was not. `ppu_sel` now follows +whichever master owns the bus. + +It was localised with `DMA_PROBE_CYC`, a new env-gated per-cycle dump of both bus +masters — necessary because the harness's cross-checks abort the run and the +abort path does not flush `--bus-out`, so a divergence they catch cannot be read +back from the trace. + +### Rung 5 does NOT close, and the reason is a DUT defect + +The full-catalog comparison is unavailable, and the first explanation for that +was wrong. Measured on **both sides over the same window**, counting entries that have +written a result: + +| run length | oracle | DUT | +|---|---|---| +| 17.9M | 88 | **88** | +| 20.8M | 95 | — | +| 41.7M | 117 | — | +| 60.0M | 120 | **5** | +| 100M | — | **5** | +| 134M | **146** | **5** | + +The oracle climbs monotonically to the full catalog. The DUT falls to five and +stays there for seventy-four million cycles. A battery that had merely completed +a pass and restarted would show the count climbing again. + +The five are the whole **Power On State** suite and nothing else — the group a +pass writes early. So the DUT restarts somewhere after 17.9M, completes Power On +State, and then **produces no further result for 74M cycles**. Catalog order puts +`PPU Behavior` immediately after it: the first of the suites that had never once +executed under the old window. That is a hypothesis with an obvious next +measurement, and it is recorded as one. + +Reported as unavailable rather than as a result. Taken at face value the +comparison reads `141 of 146 entries differ`, which would be a badly wrong thing +to publish — it is one defect, not 141. The comparator does classify them as +disagreements rather than as missing coverage, which is correct: they are +`NotRun` on one side only, the exact distinction the acceptance wording exists +for. + ## [2.6.3] - 2026-08-25 - "Mainspring" (the DUT runs on one master clock, and four enables that were never enabling — plus AccuracyCoin end to end, a status vector that names its disagreements by test, and a decay constant the documentation and the corpus disagree about by a factor of ~17. The emulation core is unchanged) ### Added diff --git a/Cargo.lock b/Cargo.lock index 85c3f3e3..5e860420 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4290,7 +4290,7 @@ dependencies = [ [[package]] name = "rustynes-android" -version = "2.6.3" +version = "2.6.4" dependencies = [ "android-activity", "android_logger", @@ -4308,7 +4308,7 @@ dependencies = [ [[package]] name = "rustynes-apu" -version = "2.6.3" +version = "2.6.4" dependencies = [ "bitflags 2.13.1", "criterion", @@ -4321,7 +4321,7 @@ dependencies = [ [[package]] name = "rustynes-cheevos" -version = "2.6.3" +version = "2.6.4" dependencies = [ "cc", "ureq", @@ -4329,7 +4329,7 @@ dependencies = [ [[package]] name = "rustynes-core" -version = "2.6.3" +version = "2.6.4" dependencies = [ "bitflags 2.13.1", "criterion", @@ -4346,7 +4346,7 @@ dependencies = [ [[package]] name = "rustynes-cpu" -version = "2.6.3" +version = "2.6.4" dependencies = [ "bitflags 2.13.1", "criterion", @@ -4357,7 +4357,7 @@ dependencies = [ [[package]] name = "rustynes-frontend" -version = "2.6.3" +version = "2.6.4" dependencies = [ "anstyle", "arboard", @@ -4416,18 +4416,18 @@ dependencies = [ [[package]] name = "rustynes-gamedb" -version = "2.6.3" +version = "2.6.4" dependencies = [ "rustynes-core", ] [[package]] name = "rustynes-gfx-shaders" -version = "2.6.3" +version = "2.6.4" [[package]] name = "rustynes-hdpack" -version = "2.6.3" +version = "2.6.4" dependencies = [ "lewton", "png", @@ -4438,7 +4438,7 @@ dependencies = [ [[package]] name = "rustynes-ios" -version = "2.6.3" +version = "2.6.4" dependencies = [ "bytemuck", "cpal", @@ -4452,7 +4452,7 @@ dependencies = [ [[package]] name = "rustynes-libretro" -version = "2.6.3" +version = "2.6.4" dependencies = [ "libc", "rust-libretro", @@ -4461,7 +4461,7 @@ dependencies = [ [[package]] name = "rustynes-mappers" -version = "2.6.3" +version = "2.6.4" dependencies = [ "bitflags 2.13.1", "criterion", @@ -4473,7 +4473,7 @@ dependencies = [ [[package]] name = "rustynes-mobile" -version = "2.6.3" +version = "2.6.4" dependencies = [ "rustynes-core", "rustynes-hdpack", @@ -4488,7 +4488,7 @@ dependencies = [ [[package]] name = "rustynes-netplay" -version = "2.6.3" +version = "2.6.4" dependencies = [ "futures-util", "js-sys", @@ -4504,7 +4504,7 @@ dependencies = [ [[package]] name = "rustynes-ppu" -version = "2.6.3" +version = "2.6.4" dependencies = [ "bitflags 2.13.1", "criterion", @@ -4516,21 +4516,21 @@ dependencies = [ [[package]] name = "rustynes-probe" -version = "2.6.3" +version = "2.6.4" dependencies = [ "rustynes-core", ] [[package]] name = "rustynes-ra" -version = "2.6.3" +version = "2.6.4" dependencies = [ "rustynes-cheevos", ] [[package]] name = "rustynes-script" -version = "2.6.3" +version = "2.6.4" dependencies = [ "mlua", "piccolo", @@ -4541,7 +4541,7 @@ dependencies = [ [[package]] name = "rustynes-test-harness" -version = "2.6.3" +version = "2.6.4" dependencies = [ "insta", "png", diff --git a/Cargo.toml b/Cargo.toml index 285f9f3b..fcbe97ab 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.3" +version = "2.6.4" edition = "2024" rust-version = "1.96" license = "GPL-3.0-or-later" diff --git a/OVERVIEW.md b/OVERVIEW.md index 8523e711..846776ce 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.3 +**Applies to:** RustyNES v2.6.4 --- @@ -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.3 "Mainspring"**. 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.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. -> 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.3**. +> 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**. --- diff --git a/README.md b/README.md index 30f0635c..a4685713 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@

- Build Status License: GPL-3.0-or-later Version Rust: 1.96
+ Build Status License: GPL-3.0-or-later Version Rust: 1.96
AccuracyCoin nestest Try in browser
Platform

@@ -674,7 +674,7 @@ and the Material-for-MkDocs documentation handbook at ## Current Release -RustyNES's current release is **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.4 "Rubric"** — the last four AccuracyCoin disagreements close, every rule that closed them 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 247259e4..57487659 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.3 "Mainspring" released — the current head of the line, on **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.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. --- ## Where we are -RustyNES is well past v1.0.0. The current release is **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.4 "Rubric"** (2026-08-26) — the last four AccuracyCoin disagreements close, every rule that closed them 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 aa8814ab..61e4259f 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,7 +2,7 @@ ## Supported Versions -The current release is **v2.6.3 "Mainspring"**, on **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.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 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 199a64e7..e8c5fdd9 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.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.4 "Rubric"** (the last four AccuracyCoin disagreements close, every rule that closed them 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 d5073797..8b6521d4 100644 --- a/VERSION-PLAN.md +++ b/VERSION-PLAN.md @@ -1,6 +1,6 @@ # RustyNES Version Plan -**Current release: 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.4 "Rubric"** — the last four AccuracyCoin disagreements close, every rule that closed them 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.3 "Mainspring"** (current) | 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. | +| **v2.6.4 "Rubric"** (current) | The last four AccuracyCoin disagreements close, and then the gate that certified them is measured to cover 88 of 146 entries. 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. That is a hang, not a restart, in the suites that had never executed. 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-cosim/Cargo.lock b/crates/rustynes-cosim/Cargo.lock index ffc2d939..3b421d7c 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.3" +version = "2.6.4" dependencies = [ "bitflags", "libm", @@ -107,7 +107,7 @@ dependencies = [ [[package]] name = "rustynes-core" -version = "2.6.3" +version = "2.6.4" dependencies = [ "bitflags", "lz4_flex", @@ -121,7 +121,7 @@ dependencies = [ [[package]] name = "rustynes-cosim" -version = "2.6.3" +version = "2.6.4" dependencies = [ "rustynes-core", "sha2", @@ -129,7 +129,7 @@ dependencies = [ [[package]] name = "rustynes-cpu" -version = "2.6.3" +version = "2.6.4" dependencies = [ "bitflags", "thiserror", @@ -137,7 +137,7 @@ dependencies = [ [[package]] name = "rustynes-mappers" -version = "2.6.3" +version = "2.6.4" dependencies = [ "bitflags", "rustynes-apu", @@ -146,7 +146,7 @@ dependencies = [ [[package]] name = "rustynes-ppu" -version = "2.6.3" +version = "2.6.4" dependencies = [ "bitflags", "libm", diff --git a/crates/rustynes-cosim/Cargo.toml b/crates/rustynes-cosim/Cargo.toml index 3f5c4304..2f1e0f85 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.3" +version = "2.6.4" edition = "2024" rust-version = "1.96" license = "GPL-3.0-or-later" diff --git a/crates/rustynes-libretro/rustynes_libretro.info b/crates/rustynes-libretro/rustynes_libretro.info index 6c7bafc5..6008b66a 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.3" +display_version = "v2.6.4" categories = "Emulator" # Hardware Information diff --git a/docs/STATUS.md b/docs/STATUS.md index 533e431b..c0632da1 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -1,6 +1,6 @@ # RustyNES — Project Status Matrix -> **Current release: v2.6.3** (2026-08-25) — **"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"** (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.4** (2026-08-26) — **"Rubric"**, the last four AccuracyCoin disagreements close, every rule that closed them 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. diff --git a/to-dos/ROADMAP.md b/to-dos/ROADMAP.md index ef9170ee..fd600fff 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.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.4 "Rubric"** (2026-08-26) — the last four AccuracyCoin disagreements close, every rule that closed them 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"**, 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"**, 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 f3009086..e7e9bc12 100644 --- a/to-dos/mister/TASKS.md +++ b/to-dos/mister/TASKS.md @@ -183,6 +183,21 @@ Legend: `[ ]` open · `[~]` in progress · `[x]` done 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] 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 + them by no nesdev page. Three sub-test ROMs are standing **verdict** + gates; suite 69 → **72 green, 0 failed** + - [x] **The acceptance wording needed one more clause.** It was met with **58 + of 146 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. 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 + 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 ## v2.6.5 – v2.6.6 — rung 6, MiSTer integration and hardware From bfe633cab83f669aba5611df3d592ea4a2463987 Mon Sep 17 00:00:00 2001 From: DoubleGate Date: Wed, 26 Aug 2026 05:00:10 -0400 Subject: [PATCH 05/11] =?UTF-8?q?docs(release):=20correct=20v2.6.4's=20run?= =?UTF-8?q?g-5=20carry=20=E2=80=94=20the=20DUT=20resets,=20it=20does=20not?= =?UTF-8?q?=20hang=20in=20a=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The release notes and CHANGELOG published an hour ago recorded the DUT as hung in the PPU suites, on the strength of catalog order: `PPU Behavior` follows `Power On State`, and those suites had never executed. Reasonable, and wrong. Probing the PC puts the DUT in a three-cycle self-loop at $80DF, which the ROM spells `INC $EC` / `JMP $80DF` -- the MENU IDLE LOOP, where AccuracyCoin spins while its NMI handler does the work. Only the five results the power-on path writes are present, so the console re-ran power-on and returned to the menu: a RESET. It then idles because START is pressed once, at frames 300-306. Corrected in place rather than deleted: the wrong reading was published, and the thing that separated it from the right one was one probe rather than more thought. --- .github/release-notes/v2.6.4.md | 16 ++++++++++++---- CHANGELOG.md | 16 ++++++++++++---- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/.github/release-notes/v2.6.4.md b/.github/release-notes/v2.6.4.md index b3db3cc2..a2c77a48 100644 --- a/.github/release-notes/v2.6.4.md +++ b/.github/release-notes/v2.6.4.md @@ -142,10 +142,18 @@ a pass and restarted would show the count climbing again. The five are the whole **Power On State** suite and nothing else — the group a pass writes early. So the DUT restarts somewhere after 17.9M, completes Power On -State, and then **produces no further result for 74M cycles**. Catalog order puts -`PPU Behavior` immediately after it: the first of the suites that had never once -executed under the old window. That is a hypothesis with an obvious next -measurement, and it is recorded as one. +State, and then **produces no further result for 74M cycles**. + +**The obvious reading was a hang inside `PPU Behavior`** — it follows +`Power On State` in catalog order and had never once executed. Probing the PC +refutes it: the DUT sits in a three-cycle self-loop at `$80DF`, which the ROM +spells `INC $EC` / `JMP $80DF` — AccuracyCoin's **menu idle loop**, where the +menu spins while its NMI handler works. The DUT is back at the menu with only +the results the power-on path writes, so the console **reset**; it then idles +because START is pressed once, at frames 300-306. + +A reset is a different defect to chase than a hang, and v2.6.5 chases the right +one: bracket where the result count collapses between 17.9M and 60M. Reported as unavailable rather than as a result. Taken at face value the comparison reads `141 of 146 entries differ`, which would be a badly wrong thing diff --git a/CHANGELOG.md b/CHANGELOG.md index 2322a9eb..7702a456 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -162,10 +162,18 @@ a pass and restarted would show the count climbing again. The five are the whole **Power On State** suite and nothing else — the group a pass writes early. So the DUT restarts somewhere after 17.9M, completes Power On -State, and then **produces no further result for 74M cycles**. Catalog order puts -`PPU Behavior` immediately after it: the first of the suites that had never once -executed under the old window. That is a hypothesis with an obvious next -measurement, and it is recorded as one. +State, and then **produces no further result for 74M cycles**. + +**The obvious reading was a hang inside `PPU Behavior`** — it follows +`Power On State` in catalog order and had never once executed. Probing the PC +refutes it: the DUT sits in a three-cycle self-loop at `$80DF`, which the ROM +spells `INC $EC` / `JMP $80DF` — AccuracyCoin's **menu idle loop**, where the +menu spins while its NMI handler works. The DUT is back at the menu with only +the results the power-on path writes, so the console **reset**; it then idles +because START is pressed once, at frames 300-306. + +A reset is a different defect to chase than a hang, and v2.6.5 chases the right +one: bracket where the result count collapses between 17.9M and 60M. Reported as unavailable rather than as a result. Taken at face value the comparison reads `141 of 146 entries differ`, which would be a badly wrong thing From 783ef340702ec20a2ce0996807825dea209317e1 Mon Sep 17 00:00:00 2001 From: DoubleGate Date: Wed, 26 Aug 2026 05:02:40 -0400 Subject: [PATCH 06/11] docs: carry v2.6.4's findings into the standing rules and the session state Five rules into AGENTS.md, each earned in this cycle: * A test ROM's own source is the SPECIFICATION, and it may be one `curl` away. AccuracyCoin is MIT-licensed; its comments named three mechanisms in minutes that a full session of tracing had not. A test ROM is stimulus, not a reference implementation, so no firewall question arises. Decode its failure codes from the macro -- `Fail(N)` is one-based, and reading it as an index made a regression read as progress. * A pass count is a claim about what RAN. 146 of 146 'identical' with 58 entries NotRun on both sides. Break results down by group and look for a group at zero; write acceptance a vacuous result cannot satisfy; and treat coverage work as load-bearing, since widening the window found a real defect on its first run. * Comparing at a fixed cycle count assumes both sides reach the same PLACE. Measure the reference over the same axis, do not conclude from two points, and when the question is 'where did it stop', probe the program counter -- catalog order is a suspect, not evidence. * `cargo test` prints `error:` on a TEST FAILURE, so a classifier keyed on `^error` turns catches into BUILD-FAILED. Run the baseline through the same classifier. * An abort path does not flush the trace; use an env-gated per-cycle probe. CLAUDE.local.md moves to the v2.6.4 position and leads with the one thing to pick up first: the DUT RESETS between 17.9 M and 60 M cycles and returns to AccuracyCoin's menu idle loop at $80DF. It is not hung in a PPU test. --- AGENTS.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 3a2698b5..d3145fb9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -258,6 +258,11 @@ 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. +- **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. +- **An abort path does not flush the trace file.** The co-simulation harness's cross-checks `return 3`, and `--bus-out` is written at exit, so a divergence they catch cannot be examined from the trace afterwards. The remedy is an env-gated stderr probe printing the relevant state per cycle around a target cycle — `NMI_PROBE_CYC`, and `DMA_PROBE_CYC` added in v2.6.4, which localised a defect in one run with four lines where a trace diff could not have shown it at all. - **`cargo test ` that matches nothing prints `0 passed` and exits 0 — that is NOT a pass.** Bit twice in one session: `cargo test --workspace --features test-roms accuracycoin` and `cargo test -p rustynes-cpu nestest` both matched zero tests and looked green. The real invocations are `cargo test -p rustynes-test-harness --features test-roms --test accuracycoin` (prints `pass rate = 100.00% over 141 assigned tests`; **the RAM decoder is authoritative — the framebuffer decoder reports 120 and is known-buggy**) and `--test nestest`. Always confirm a non-zero test count before reporting a gate as green. - **A SKIPPED job satisfies `CI success`, and `CI success` is `main`'s only required check.** The ruleset `Protect (Default)` requires exactly one status context. That job runs `if: always()` and fails on `contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')` — `skipped` is in **neither** list. So a gate that did not run reports a pass for a property it never tested. This is not theoretical: `test-roms` (the accuracy battery) was FULL-run only, so an accuracy regression could not be caught on the PR that caused it — it landed, turned `main` red, and needed a second PR (#396 introduced the vector move, #403 fixed it; **both PRs were correct, the process was not**). **There is no merge queue** — verified, no `merge_group` event appears in the run history — so nothing caught it later either. v2.3.9 A5 adds an `accuracy` paths-filter (chip crates, `rustynes-core`, `rustynes-gamedb`, `rustynes-test-harness`, `tests/`) so the battery runs on PRs that can actually break it; measured first, 11 of the last 40 merged PRs touch those paths, so ~72% still skip it. When judging whether a gate covers something, check three things separately: which contexts are *required*, what the aggregate treats as failure, and whether the job that tests the property actually **ran**. From 7e0cdf0f56ec179220efbbe9f35ce0dc26435d4a Mon Sep 17 00:00:00 2001 From: DoubleGate Date: Wed, 26 Aug 2026 05:05:09 -0400 Subject: [PATCH 07/11] fix(test-harness): the coverage line reported entries executed on AT LEAST ONE side, not both Found in review of the change that introduced it, on a release whose subject is a count that described a set it did not measure. `coverage: {} of {} entries executed on both sides` printed `a.len() - dead`, where `dead` counts entries NotRun on BOTH sides. That subtraction is the number executed on at least ONE side. Against the actual run it is not a subtle difference: the reference had zero NotRun, so `dead` was 0 and the line claimed all 146 entries ran on both sides while the DUT had executed five. `executed_on_both` counts what the sentence claims -- neither side NotRun -- and the line now also breaks out how many ran on neither and on exactly one side, which is the shape that would have made the original wrong number obvious. THE FIRST FIX WAS NOT ENOUGH AND THE MUTATION SAID SO. With the arithmetic corrected, reverting the LINE to the subtraction came back NOT CAUGHT: the tests asserted on `both_not_run` and `executed_on_both`, and nothing reached the message. The defect was in the sentence. `coverage_line` is therefore built rather than printed, and a test reads it -- after which the same mutation is CAUGHT. Also applies the review's nitpick: `TestStatus` derives `PartialEq`, so the predicates compare directly instead of via `matches!`. Two tests added (12 total). Mutations: the sentence reverting -> CAUGHT; `executed_on_both` using OR -> CAUGHT. --- .../src/bin/accuracycoin_status.rs | 104 ++++++++++++++++-- 1 file changed, 96 insertions(+), 8 deletions(-) diff --git a/crates/rustynes-test-harness/src/bin/accuracycoin_status.rs b/crates/rustynes-test-harness/src/bin/accuracycoin_status.rs index 37faaeeb..c98e0081 100644 --- a/crates/rustynes-test-harness/src/bin/accuracycoin_status.rs +++ b/crates/rustynes-test-harness/src/bin/accuracycoin_status.rs @@ -86,10 +86,45 @@ fn vacuous(v: &[TestStatus]) -> bool { fn both_not_run(a: &[TestStatus], b: &[TestStatus]) -> usize { a.iter() .zip(b) - .filter(|(x, y)| matches!(x, TestStatus::NotRun) && matches!(y, TestStatus::NotRun)) + .filter(|(x, y)| **x == TestStatus::NotRun && **y == TestStatus::NotRun) .count() } +/// Entries that BOTH sides executed — neither is `NotRun`. +/// +/// Not the complement of [`both_not_run`], and the difference is the whole +/// point. `len - both_not_run` counts entries executed on **at least one** side, +/// which is a different and much weaker statement: with the reference complete +/// and the DUT stalled after five entries, `both_not_run` is zero and that +/// subtraction claims all 146 ran on both sides. Caught in review of the change +/// that introduced it, on a release whose subject is a count that described a +/// set it did not measure. +fn executed_on_both(a: &[TestStatus], b: &[TestStatus]) -> usize { + a.iter() + .zip(b) + .filter(|(x, y)| **x != TestStatus::NotRun && **y != TestStatus::NotRun) + .count() +} + +/// The coverage sentence, built rather than printed, so a test can read it. +/// +/// The v2.6.4 review found this line claiming `len - both_not_run` entries had +/// "executed on both sides" — which is the count executed on **at least one**. +/// Fixing the arithmetic was not enough: a mutation reverting the line came back +/// NOT CAUGHT, because the tests asserted on the predicates and nothing reached +/// the message. The defect was in the sentence, so the sentence is what a test +/// has to be able to see. +fn coverage_line(a: &[TestStatus], b: &[TestStatus]) -> String { + let dead = both_not_run(a, b); + let both = executed_on_both(a, b); + format!( + "coverage: {both} of {} entries executed on both sides \ + ({dead} on neither, {} on one side only)", + a.len(), + a.len() - both - dead + ) +} + /// Report how much of the catalog the two runs actually EXECUTED, and refuse a /// partial comparison. /// @@ -102,12 +137,7 @@ fn coverage_gate(a: &[TestStatus], b: &[TestStatus]) -> Option { // reader who sees only "identical" has no way to tell how much of the // catalog that sentence covers. let dead = both_not_run(a, b); - println!( - "\ncoverage: {} of {} entries executed on both sides ({} NotRun on both)", - a.len() - dead, - a.len(), - dead - ); + println!("\n{}", coverage_line(a, b)); if dead == 0 { return None; } @@ -228,9 +258,67 @@ fn main() -> ExitCode { #[cfg(test)] mod tests { - use super::{both_not_run, coverage_gate, describe, vacuous}; + use super::{both_not_run, coverage_gate, coverage_line, describe, executed_on_both, vacuous}; use rustynes_test_harness::accuracy_coin_catalog::{TestStatus, catalog, decode_results}; + /// The SENTENCE, not the predicates behind it. Reverting the line to the + /// subtraction the review flagged came back NOT CAUGHT while the tests only + /// reached `both_not_run` and `executed_on_both` — the defect was in the + /// message, so the message is what this asserts on. + #[test] + fn the_coverage_sentence_reports_what_it_claims() { + let n = catalog().len(); + let reference = vec![TestStatus::Pass; n]; + let mut dut = vec![TestStatus::NotRun; n]; + for e in dut.iter_mut().take(5) { + *e = TestStatus::Pass; + } + let line = coverage_line(&reference, &dut); + assert!( + line.starts_with(&format!( + "coverage: 5 of {n} entries executed on both sides" + )), + "the sentence must say FIVE, not {n}: {line}" + ); + assert!( + line.contains("(0 on neither,"), + "nothing is unrun on both sides here: {line}" + ); + assert!( + line.contains(&format!("{} on one side only)", n - 5)), + "the rest ran on exactly one side: {line}" + ); + } + + /// `len - both_not_run` is NOT the number both sides executed, and the + /// v2.6.4 review caught the coverage line claiming it was. With the + /// reference complete and the DUT stalled, the two differ by the whole run. + #[test] + fn executed_on_both_is_not_the_complement_of_both_not_run() { + let n = catalog().len(); + let reference = vec![TestStatus::Pass; n]; + let mut dut = vec![TestStatus::NotRun; n]; + for e in dut.iter_mut().take(5) { + *e = TestStatus::Pass; + } + + assert_eq!( + both_not_run(&reference, &dut), + 0, + "the reference ran everything, so nothing is unrun on BOTH sides" + ); + assert_eq!( + executed_on_both(&reference, &dut), + 5, + "only five entries ran on both sides" + ); + assert_ne!( + n - both_not_run(&reference, &dut), + executed_on_both(&reference, &dut), + "the subtraction the review flagged would have reported all of them" + ); + } + /// The refusal itself, not just its predicate. Reached directly because a /// check that only exists inside `main` is a check no test can reach -- /// which is how the missing property got missed in the first place. From 8f7ae082523f75274757fc061752453aea0be033 Mon Sep 17 00:00:00 2001 From: DoubleGate Date: Wed, 26 Aug 2026 05:24:50 -0400 Subject: [PATCH 08/11] =?UTF-8?q?fix(release-notes):=20reflow=20v2.6.4's?= =?UTF-8?q?=20notes=20=E2=80=94=20the=20render=20audit=20is=20a=20real=20g?= =?UTF-8?q?ate=20and=20I=20had=20not=20run=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI failed `release_notes_are_not_hard_wrapped` on BOTH test legs. The notes file was hand-wrapped at ~76 columns; GitHub renders release bodies with GFM hard line breaks, so every one of those newlines becomes a
and the notes would have displayed as a ragged column broken mid-sentence. Fixed with the tool the audit's own failure message names, `scripts/release-automation/reflow.py`. TWO THINGS WORTH RECORDING. The failure arrived first on `test (ubuntu-24.04-arm)`, a leg this project's notes record as having a history of apt provisioning stalls -- so it wore the costume of a known flake. It is not one, and the `ubuntu-latest` leg failing on the identical assertion settles it: this is a property of a file, not of a runner. A documented flake history makes a real failure easier to dismiss, not harder to spot. The ordering miss is the real one. Phase 3's gates ran BEFORE the release ceremony created `.github/release-notes/v2.6.4.md`, the new CHANGELOG section, the bumped anchors and the VERSION-PLAN row. A local suite cannot audit files that do not yet exist, so "all gates green" was true of a tree without the release's own artifacts in it. The gate order wants a second pass: build -> gates -> docs -> ceremony -> gates again. Re-run here, workspace clean. --- .github/release-notes/v2.6.4.md | 145 ++++++-------------------------- 1 file changed, 27 insertions(+), 118 deletions(-) diff --git a/.github/release-notes/v2.6.4.md b/.github/release-notes/v2.6.4.md index a2c77a48..2360f705 100644 --- a/.github/release-notes/v2.6.4.md +++ b/.github/release-notes/v2.6.4.md @@ -1,131 +1,60 @@ # RustyNES v2.6.4 "Rubric" -**The last four AccuracyCoin disagreements close — every rule that closed them -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 last four AccuracyCoin disagreements close — every rule that closed them 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: no file under `crates/rustynes-{cpu,ppu,apu,mappers,core}` -is touched, so **AccuracyCoin 141/141 (100.00%, RAM decoder)** and **nestest -0-diff** hold by construction — and were run anyway. +The emulation core is unchanged: no file under `crates/rustynes-{cpu,ppu,apu,mappers,core}` is touched, so **AccuracyCoin 141/141 (100.00%, RAM decoder)** and **nestest 0-diff** hold by construction — and were run anyway. -A rubric is the authoritative statement of the rules, written in the margin by -the person who set the test. That is literally where all three of this release's -fixes came from. +A rubric is the authoritative statement of the rules, written in the margin by the person who set the test. That is literally where all three of this release's fixes came from. ## The three entries, and where their rules actually live -v2.6.3 left nine entries differing; the SH group closed five, and this release -closes the last three. **Every one is category 1 — the implementation followed -documentation that is true and insufficient.** - -- **`Open Bus`.** A read of `$4015` does not drive the data bus, and its D5 is - open bus. Both rules are properties of the board, so both live in - `cpu_bus.sv`. The nesdev `Open_bus_behavior` page documents the - `$4016`/`$4017` case and says nothing about `$4015` being exempt; AccuracyCoin - states the mechanism outright — "all the values read here are internal to the - 2A03 chip, so the data bus isn't used" — and its stimulus is `LDX #16 / LDA - 40FF,X`, 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. Not in the wiki at all. -- **`NMI Overlap BRK`.** Two rules: an interrupt sequence does not poll (stated - plainly in the wiki and simply missed here), and the BRK hijack window was one - CPU cycle too narrow at its late edge. +v2.6.3 left nine entries differing; the SH group closed five, and this release closes the last three. **Every one is category 1 — the implementation followed documentation that is true and insufficient.** + +- **`Open Bus`.** A read of `$4015` does not drive the data bus, and its D5 is open bus. Both rules are properties of the board, so both live in `cpu_bus.sv`. The nesdev `Open_bus_behavior` page documents the `$4016`/`$4017` case and says nothing about `$4015` being exempt; AccuracyCoin states the mechanism outright — "all the values read here are internal to the 2A03 chip, so the data bus isn't used" — and its stimulus is `LDX #16 / LDA 40FF,X`, 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. Not in the wiki at all. +- **`NMI Overlap BRK`.** Two rules: an interrupt sequence does not poll (stated plainly in the wiki and simply missed here), and the BRK hijack window was one CPU cycle too narrow at its late edge. ## What the codes meant, and the regression that read as progress -AccuracyCoin's `TEST_Fail` reports `(ErrorCode << 2) | 2`, and the runner sets -`ErrorCode` to **1** before every test routine — so `Fail(N)` names test **N** of -that routine, one-based. Read as a zero-based index it is off by one, and that -error made a change from test 7 to test 5 read as *partial progress* when it was -a **regression** that also broke a standing gate. The description reached a code -comment before the macro was read. v2.6.3's reading that six entries "sharing one -failure code" implied one shared cause is retracted with it: the code is an index -within one routine, so two entries sharing it share nothing. +AccuracyCoin's `TEST_Fail` reports `(ErrorCode << 2) | 2`, and the runner sets `ErrorCode` to **1** before every test routine — so `Fail(N)` names test **N** of that routine, one-based. Read as a zero-based index it is off by one, and that error made a change from test 7 to test 5 read as *partial progress* when it was a **regression** that also broke a standing gate. The description reached a code comment before the macro was read. v2.6.3's reading that six entries "sharing one failure code" implied one shared cause is retracted with it: the code is an index within one routine, so two entries sharing it share nothing. ## A fix that closes one gate and opens another is a scope measurement -The first poll fix moved the poll for **every** instruction from the last cycle to -the second-to-last, on the strength of the same ROM's walk-through of `CLI`. It -closed the failing entry and regressed `apupulse026` and `blargg08`. All three -compare against the same oracle, so one model satisfies all three and a change -that cannot is not that model. Narrowed to the branch exception alone, the entry -still passes and nothing regresses. +The first poll fix moved the poll for **every** instruction from the last cycle to the second-to-last, on the strength of the same ROM's walk-through of `CLI`. It closed the failing entry and regressed `apupulse026` and `blargg08`. All three compare against the same oracle, so one model satisfies all three and a change that cannot is not that model. Narrowed to the branch exception alone, the entry still passes and nothing regresses. -Similarly, the hijack's comment argued at length for a delayed pending flag and -named this very AccuracyCoin entry as the case that would catch the alternative. -The entry was then run, and it says the opposite. The wrong comment is kept -beside the fix. +Similarly, the hijack's comment argued at length for a delayed pending flag and named this very AccuracyCoin entry as the case that would catch the alternative. The entry was then run, and it says the opposite. The wrong comment is kept beside the fix. ## Per-entry stimulus: the battery was the wrong iteration loop -`scripts/accuracycoin-build/build_sub_test_rom.py` builds a ROM that boots -straight into one catalog entry. Twenty-six were vendored in Session 23 and -nothing had been built since; two more land here — `open-bus.nes` and -`nmi-overlap-brk.nes` — and all three used by this release are **standing verdict -gates**. They reach their verdict from a cold boot with no input in 0.9M to 4.5M -cycles against the battery's 17.9M. +`scripts/accuracycoin-build/build_sub_test_rom.py` builds a ROM that boots straight into one catalog entry. Twenty-six were vendored in Session 23 and nothing had been built since; two more land here — `open-bus.nes` and `nmi-overlap-brk.nes` — and all three used by this release are **standing verdict gates**. They reach their verdict from a cold boot with no input in 0.9M to 4.5M cycles against the battery's 17.9M. -They are verdict gates rather than bus gates by measurement, not preference: -their per-cycle surfaces are dominated by the PPU I/O-latch difference this -project has left open, 2,331,867 of 4,467,082 cycles on one of them. A gate -cannot distinguish an open ledger item from a defect; the ROM's own verdict byte -can. `tb/subtest_verdict.py` refuses when the oracle side is not itself a pass. +They are verdict gates rather than bus gates by measurement, not preference: their per-cycle surfaces are dominated by the PPU I/O-latch difference this project has left open, 2,331,867 of 4,467,082 cycles on one of them. A gate cannot distinguish an open ledger item from a defect; the ROM's own verdict byte can. `tb/subtest_verdict.py` refuses when the oracle side is not itself a pass. -Also measured: **`sub-tests/cpu-open-bus.nes` does not run `Open Bus`** — its -verdict lands at `$0407`, which the catalog assigns to *Dummy write cycles*. Off -by one table row, and nothing had caught it because a fixture's name is not -evidence about its content. +Also measured: **`sub-tests/cpu-open-bus.nes` does not run `Open Bus`** — its verdict lands at `$0407`, which the catalog assigns to *Dummy write cycles*. Off by one table row, and nothing had caught it because a fixture's name is not evidence about its content. ## The gate met its own wording, and the wording was not enough -With the four closed, the status vector reported **identical entry for entry -across all 146 entries** — and **58 of those entries were `NotRun` on both -sides**. The acceptance was worded so a DUT could not pass a test by skipping -it; it does not cover the case where *neither* side runs it, which is what a -short window produces. +With the four closed, the status vector reported **identical entry for entry across all 146 entries** — and **58 of those entries were `NotRun` on both sides**. The acceptance was worded so a DUT could not pass a test by skipping it; it does not cover the case where *neither* side runs it, which is what a short window produces. -By suite, the 600-frame golden reaches the CPU catalog and stops partway through -`CPU Interrupts`. Every APU, PPU, sprite-evaluation and PPU-misc entry was -`NotRun` — the entries rungs 3 and 4 exist for. **Measured: 4500 frames executes -all 146** (134,012,761 cycles), and that is the golden now. +By suite, the 600-frame golden reaches the CPU catalog and stops partway through `CPU Interrupts`. Every APU, PPU, sprite-evaluation and PPU-misc entry was `NotRun` — the entries rungs 3 and 4 exist for. **Measured: 4500 frames executes all 146** (134,012,761 cycles), and that is the golden now. -`accuracycoin_status` enforces it rather than merely documenting it: it prints a -**coverage** line on every comparison and **refuses** with a non-zero exit when -any entry is `NotRun` on both sides. The predicate and the refusal are both -extracted from `main` and tested directly — a check that exists only inside -`main` is a check no test can reach, which is how this property came to be -missing. +`accuracycoin_status` enforces it rather than merely documenting it: it prints a **coverage** line on every comparison and **refuses** with a non-zero exit when any entry is `NotRun` on both sides. The predicate and the refusal are both extracted from `main` and tested directly — a check that exists only inside `main` is a check no test can reach, which is how this property came to be missing. ## Widening the window found a defect on its first run -Moving the golden to 4500 frames was coverage work. The first run of the wider -window **aborted at cycle 20,636,325** — 2.8 million cycles past where every -previous run in this programme had stopped: +Moving the golden to 4500 frames was coverage work. The first run of the wider window **aborted at cycle 20,636,325** — 2.8 million cycles past where every previous run in this programme had stopped: ```text DMA data disagrees at cycle 20636325 addr=$FFC0: rtl=$04 harness=$00 ``` -A halted CPU mid-`LDA $2007` held `ppu_sel` high through a DMC steal, so on the -DMC's get cycle — where the bus address is the sample address — the mux preferred -the PPU and handed the sample fetch the read buffer instead of the cartridge -byte. **The comment directly above the offending line stated the intent it -violated**: the write half of the exception was there (an OAM DMA targets -`$2004` and the PPU must see it), the read half was not. `ppu_sel` now follows -whichever master owns the bus. +A halted CPU mid-`LDA $2007` held `ppu_sel` high through a DMC steal, so on the DMC's get cycle — where the bus address is the sample address — the mux preferred the PPU and handed the sample fetch the read buffer instead of the cartridge byte. **The comment directly above the offending line stated the intent it violated**: the write half of the exception was there (an OAM DMA targets `$2004` and the PPU must see it), the read half was not. `ppu_sel` now follows whichever master owns the bus. -It was localised with `DMA_PROBE_CYC`, a new env-gated per-cycle dump of both bus -masters — necessary because the harness's cross-checks abort the run and the -abort path does not flush `--bus-out`, so a divergence they catch cannot be read -back from the trace. +It was localised with `DMA_PROBE_CYC`, a new env-gated per-cycle dump of both bus masters — necessary because the harness's cross-checks abort the run and the abort path does not flush `--bus-out`, so a divergence they catch cannot be read back from the trace. ## Rung 5 does NOT close, and the reason is a DUT defect -The full-catalog comparison is unavailable, and the first explanation for that -was wrong. Measured on **both sides over the same window**, counting entries that have -written a result: +The full-catalog comparison is unavailable, and the first explanation for that was wrong. Measured on **both sides over the same window**, counting entries that have written a result: | run length | oracle | DUT | |---|---|---| @@ -136,31 +65,15 @@ written a result: | 100M | — | **5** | | 134M | **146** | **5** | -The oracle climbs monotonically to the full catalog. The DUT falls to five and -stays there for seventy-four million cycles. A battery that had merely completed -a pass and restarted would show the count climbing again. +The oracle climbs monotonically to the full catalog. The DUT falls to five and stays there for seventy-four million cycles. A battery that had merely completed a pass and restarted would show the count climbing again. -The five are the whole **Power On State** suite and nothing else — the group a -pass writes early. So the DUT restarts somewhere after 17.9M, completes Power On -State, and then **produces no further result for 74M cycles**. +The five are the whole **Power On State** suite and nothing else — the group a pass writes early. So the DUT restarts somewhere after 17.9M, completes Power On State, and then **produces no further result for 74M cycles**. -**The obvious reading was a hang inside `PPU Behavior`** — it follows -`Power On State` in catalog order and had never once executed. Probing the PC -refutes it: the DUT sits in a three-cycle self-loop at `$80DF`, which the ROM -spells `INC $EC` / `JMP $80DF` — AccuracyCoin's **menu idle loop**, where the -menu spins while its NMI handler works. The DUT is back at the menu with only -the results the power-on path writes, so the console **reset**; it then idles -because START is pressed once, at frames 300-306. +**The obvious reading was a hang inside `PPU Behavior`** — it follows `Power On State` in catalog order and had never once executed. Probing the PC refutes it: the DUT sits in a three-cycle self-loop at `$80DF`, which the ROM spells `INC $EC` / `JMP $80DF` — AccuracyCoin's **menu idle loop**, where the menu spins while its NMI handler works. The DUT is back at the menu with only the results the power-on path writes, so the console **reset**; it then idles because START is pressed once, at frames 300-306. -A reset is a different defect to chase than a hang, and v2.6.5 chases the right -one: bracket where the result count collapses between 17.9M and 60M. +A reset is a different defect to chase than a hang, and v2.6.5 chases the right one: bracket where the result count collapses between 17.9M and 60M. -Reported as unavailable rather than as a result. Taken at face value the -comparison reads `141 of 146 entries differ`, which would be a badly wrong thing -to publish — it is one defect, not 141. The comparator does classify them as -disagreements rather than as missing coverage, which is correct: they are -`NotRun` on one side only, the exact distinction the acceptance wording exists -for. +Reported as unavailable rather than as a result. Taken at face value the comparison reads `141 of 146 entries differ`, which would be a badly wrong thing to publish — it is one defect, not 141. The comparator does classify them as disagreements rather than as missing coverage, which is correct: they are `NotRun` on one side only, the exact distinction the acceptance wording exists for. --- @@ -177,8 +90,4 @@ for. ## What is carried to v2.6.5 -Rung 5. The full-catalog comparison is blocked on a **DUT hang** in the suites -that had never executed — measured on both sides over the same window, the -oracle climbs 88 → 95 → 117 → 120 → 146 while the DUT goes 88 → 5 → 5 → 5. -Catalog order puts `PPU Behavior` immediately after the last suite the DUT -completes, which is the next measurement rather than a finding. +Rung 5. The full-catalog comparison is blocked on a **DUT hang** in the suites that had never executed — measured on both sides over the same window, the oracle climbs 88 → 95 → 117 → 120 → 146 while the DUT goes 88 → 5 → 5 → 5. Catalog order puts `PPU Behavior` immediately after the last suite the DUT completes, which is the next measurement rather than a finding. From 4b2d0aead0a1969f7860b0899fde6ad37920b27d Mon Sep 17 00:00:00 2001 From: DoubleGate Date: Wed, 26 Aug 2026 05:29:03 -0400 Subject: [PATCH 09/11] docs(release): v2.6.4 closes NINE AccuracyCoin entries and lands OAM DMA, not four and no DMA A scope error in my own release notes, caught by their internal inconsistency: the headline said "the last four disagreements close" while the body said "the SH group closed five, and this release closes the last three". Five plus three is eight, and v2.6.3 left nine. Checking the branch rather than my memory of it settles it. The sibling carries `d0fa185` ("all five SH entries close") and `9701445` ("OAM DMA -- the 513 cycles the console was never spending"), so BOTH are v2.6.4. The release closes all nine, and its largest single deliverable -- OAM DMA -- was missing from the notes entirely, because I described the third of the release I had worked on. Now recorded: $4014 was a register the console decoded and then did nothing with. OAM DMA lands as a real bus master from `nesdev_wiki/DMA.xhtml` -- halt on a read cycle, optional alignment, 256 read/write pairs -- with the documented DMC-get precedence costing OAM its alignment as well as its slot, and a DMA write now driving the open-bus latch. Its halt and alignment were fitted to the oracle first and corrected from the wiki, which is in the ledger rather than squashed. The SH group closes in two steps, the second named by the residual of the first. Those took the vector 9 -> 3; the three in the notes are the tail. Also corrects the VERSION-PLAN row, which still carried the retracted "hang" reading of the rung-5 carry. It is a RESET -- the DUT sits in AccuracyCoin's menu idle loop at $80DF. The anchor lead is corrected in all eight places it appears across seven documents. Audits: release_anchor_audit 11 passed, release_notes_render_audit 2 passed, markdownlint clean. --- .github/release-notes/v2.6.4.md | 14 ++++++++++++-- AGENTS.md | 4 ++-- CHANGELOG.md | 30 +++++++++++++++++++++++++++--- README.md | 2 +- ROADMAP.md | 2 +- SUPPORT.md | 2 +- VERSION-PLAN.md | 4 ++-- docs/STATUS.md | 2 +- to-dos/ROADMAP.md | 2 +- 9 files changed, 48 insertions(+), 14 deletions(-) diff --git a/.github/release-notes/v2.6.4.md b/.github/release-notes/v2.6.4.md index 2360f705..41e421dc 100644 --- a/.github/release-notes/v2.6.4.md +++ b/.github/release-notes/v2.6.4.md @@ -1,14 +1,24 @@ # RustyNES v2.6.4 "Rubric" -**The last four AccuracyCoin disagreements close — every rule that closed them 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.** +**OAM DMA lands, 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: no file under `crates/rustynes-{cpu,ppu,apu,mappers,core}` is touched, so **AccuracyCoin 141/141 (100.00%, RAM decoder)** and **nestest 0-diff** hold by construction — and were run anyway. A rubric is the authoritative statement of the rules, written in the margin by the person who set the test. That is literally where all three of this release's fixes came from. +## OAM DMA, and the SH group — the two thirds of this release that came first + +`$4014` was a register the console decoded and then did nothing with: **the DUT had never spent the 513 cycles an OAM DMA costs.** It lands here as a real bus master — halt on a read cycle, an optional alignment cycle, then 256 read/write pairs — implemented from `nesdev_wiki/DMA.xhtml`, with the documented **DMC-get precedence over OAM-get** (a DMC fetch delays the OAM transfer, and costs it its alignment as well as its slot). A DMA write now drives the open-bus latch too, which until an OAM DMA existed there was no bus master here to do. + +Its halt and alignment were **fitted to the oracle first and then corrected from the wiki** — the correction is in the ledger rather than quietly squashed, because "measured rather than assumed" was written about a fit. + +The **`SH` group** closes in two steps, and the second was named by the residual of the first: the stored value's AND with the address high byte is **RDY-conditional**, and the dummy-read cycle is **addressing-mode dependent** (`SHA (d),Y` is six cycles, so its `tcyc==3` is a pointer-high fetch, not a dummy read). Four absolute forms closed on the first fix and `$93` alone did not, which is what pointed at the second. + +Together those took the vector from **9 differing to 3**. The three below are the tail. + ## The three entries, and where their rules actually live -v2.6.3 left nine entries differing; the SH group closed five, and this release closes the last three. **Every one is category 1 — the implementation followed documentation that is true and insufficient.** +v2.6.3 left nine entries differing. This release closes all nine; the three below are the last of them. **Every one is category 1 — the implementation followed documentation that is true and insufficient.** - **`Open Bus`.** A read of `$4015` does not drive the data bus, and its D5 is open bus. Both rules are properties of the board, so both live in `cpu_bus.sv`. The nesdev `Open_bus_behavior` page documents the `$4016`/`$4017` case and says nothing about `$4015` being exempt; AccuracyCoin states the mechanism outright — "all the values read here are internal to the 2A03 chip, so the data bus isn't used" — and its stimulus is `LDX #16 / LDA 40FF,X`, 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. Not in the wiki at all. diff --git a/AGENTS.md b/AGENTS.md index d3145fb9..dd7f5db9 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) — the last four AccuracyCoin disagreements close, every rule that closed them 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.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"** (the last four AccuracyCoin disagreements close, every rule that closed them 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.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. - **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. diff --git a/CHANGELOG.md b/CHANGELOG.md index 7702a456..684c8578 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,16 +26,40 @@ cycle-accurate core later replaced. ## [Unreleased] -## [2.6.4] - 2026-08-26 - "Rubric" (the last four AccuracyCoin disagreements close, every rule 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) +## [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 the person who set the test. That is literally where all three of this release's fixes came from. +### OAM DMA, and the SH group — the two thirds of this release that came first + +`$4014` was a register the console decoded and then did nothing with: **the DUT +had never spent the 513 cycles an OAM DMA costs.** It lands here as a real bus +master — halt on a read cycle, an optional alignment cycle, then 256 read/write +pairs — implemented from `nesdev_wiki/DMA.xhtml`, with the documented **DMC-get +precedence over OAM-get** (a DMC fetch delays the OAM transfer, and costs it its +alignment as well as its slot). A DMA write now drives the open-bus latch too, +which until an OAM DMA existed there was no bus master here to do. + +Its halt and alignment were **fitted to the oracle first and then corrected from +the wiki** — the correction is in the ledger rather than quietly squashed, +because "measured rather than assumed" was written about a fit. + +The **`SH` group** closes in two steps, and the second was named by the residual +of the first: the stored value's AND with the address high byte is +**RDY-conditional**, and the dummy-read cycle is **addressing-mode dependent** +(`SHA (d),Y` is six cycles, so its `tcyc==3` is a pointer-high fetch, not a dummy +read). Four absolute forms closed on the first fix and `$93` alone did not, which +is what pointed at the second. + +Together those took the vector from **9 differing to 3**. The three below are the +tail. + ### The three entries, and where their rules actually live -v2.6.3 left nine entries differing; the SH group closed five, and this release -closes the last three. **Every one is category 1 — the implementation followed +v2.6.3 left nine entries differing. This release closes all nine; the three +below are the last of them. **Every one is category 1 — the implementation followed documentation that is true and insufficient.** - **`Open Bus`.** A read of `$4015` does not drive the data bus, and its D5 is diff --git a/README.md b/README.md index a4685713..885c6b5a 100644 --- a/README.md +++ b/README.md @@ -674,7 +674,7 @@ and the Material-for-MkDocs documentation handbook at ## Current Release -RustyNES's current release is **v2.6.4 "Rubric"** — the last four AccuracyCoin disagreements close, every rule that closed them 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.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 57487659..bbd9c1b1 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -8,7 +8,7 @@ ## Where we are -RustyNES is well past v1.0.0. The current release is **v2.6.4 "Rubric"** (2026-08-26) — the last four AccuracyCoin disagreements close, every rule that closed them 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.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/SUPPORT.md b/SUPPORT.md index e8c5fdd9..6d39b83e 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"** (the last four AccuracyCoin disagreements close, every rule that closed them 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.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 8b6521d4..d806c832 100644 --- a/VERSION-PLAN.md +++ b/VERSION-PLAN.md @@ -1,6 +1,6 @@ # RustyNES Version Plan -**Current release: v2.6.4 "Rubric"** — the last four AccuracyCoin disagreements close, every rule that closed them 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.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,7 @@ 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) | The last four AccuracyCoin disagreements close, and then the gate that certified them is measured to cover 88 of 146 entries. 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. That is a hang, not a restart, in the suites that had never executed. 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.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.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/docs/STATUS.md b/docs/STATUS.md index c0632da1..f844de8a 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -1,6 +1,6 @@ # RustyNES — Project Status Matrix -> **Current release: v2.6.4** (2026-08-26) — **"Rubric"**, the last four AccuracyCoin disagreements close, every rule that closed them 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.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 > 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. diff --git a/to-dos/ROADMAP.md b/to-dos/ROADMAP.md index fd600fff..7070c44e 100644 --- a/to-dos/ROADMAP.md +++ b/to-dos/ROADMAP.md @@ -55,7 +55,7 @@ v2.8.0 → v0.9.7; the synthesis itself = **v1.0.0**. ## Status -- **Current release:** **RustyNES v2.6.4 "Rubric"** (2026-08-26) — the last four AccuracyCoin disagreements close, every rule that closed them 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.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/`. From cc470b8b3cb16f798470beceba9ea738664b24f2 Mon Sep 17 00:00:00 2001 From: DoubleGate Date: Wed, 26 Aug 2026 05:32:03 -0400 Subject: [PATCH 10/11] docs(release): attribute each mutation figure to what it actually covered The verification table's "RTL mutations: 7 of 8 CAUGHT" was my own session's run against the interrupt-timing work, presented as the release's figure. Same shape as the scope error one commit earlier: a number from the part of the release I worked on, standing for the whole. The SH group carried its own run and `280e065` records it -- PROGRAM46 cleared the implementation at 29,774 of 29,774 cycles exact with SIX OF SIX mutations caught, covering the value rule, the page-cross address mangle and register selection across all five opcodes. Both rows are now labelled by subject. Not aggregated into one number, deliberately: they are different gates against different stimulus, and a summed "13 of 14" would imply a single sweep that was never run. --- .github/release-notes/v2.6.4.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/release-notes/v2.6.4.md b/.github/release-notes/v2.6.4.md index 41e421dc..9ad9da46 100644 --- a/.github/release-notes/v2.6.4.md +++ b/.github/release-notes/v2.6.4.md @@ -92,7 +92,8 @@ Reported as unavailable rather than as a result. Taken at face value the compari | gate | result | |---|---| | sibling `./tb/regress.sh` | **72 passed, 0 failed** (69 + three new rung-5 verdict gates) | -| RTL mutations | **7 of 8 CAUGHT**; the eighth classified INERT by byte-comparison | +| RTL mutations (interrupt timing) | **7 of 8 CAUGHT**; the eighth classified INERT by byte-comparison | +| RTL mutations (SH group) | **6 of 6 CAUGHT** — value rule, page-cross mangle, register selection, all five opcodes | | `accuracycoin_status` mutations | **3 of 3 CAUGHT** | | AccuracyCoin (oracle, RAM decoder) | **141/141 (100.00%)** | | nestest | **0-diff** | From b36be5d366ac333ad56981a135aa2a97be29ab32 Mon Sep 17 00:00:00 2001 From: DoubleGate Date: Wed, 26 Aug 2026 06:12:10 -0400 Subject: [PATCH 11/11] fix(test-harness): guard the coverage arithmetic against unequal-length vectors Raised in review, and the claim is conditionally true: `coverage_line` subtracts two `zip`-derived counts from `a.len()`, and `zip` truncates silently to the shorter side -- so unequal lengths would report the tail of `a` as "on one side only". That would be a THIRD wrong count in a tool whose subject is counts that describe the wrong set. The condition is unreachable today, and saying so is part of the answer: both vectors come from `decode_results`, which returns one entry per catalog entry or refuses the dump, and the file already tests exactly that (`decoded_vectors_are_always_catalog_length`, `a_short_dump_is_refused`). Added anyway, because the coupling is invisible at the point that depends on it, and anything that later feeds `coverage_line` from another source gets a loud failure instead of a plausible number. Demonstrated to fire by a `should_panic` test -- the only thing that can reach it, since `main` cannot -- and removing the guard is a CAUGHT mutation. The round's nitpick asked for `matches!` where the PREVIOUS round asked for `==`, so rather than flip-flop the file is unified on `==`: `vacuous` now matches the two predicates instead of the other way round. HOW THIS WAS NEARLY MISSED, which is the part worth recording. The reviewer edits ONE comment per PR and folds earlier rounds into a collapsed archive. Its visible head now reads only "Error: timeout waiting for response" -- the round against the final head timed out -- so the PR showed a green build, zero unresolved threads, and a comment that appeared to contain nothing. The finding was inside the
archive, and the only tell was `updated_at` (09:37) differing from `created_at` (09:01). --- .../src/bin/accuracycoin_status.rs | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/crates/rustynes-test-harness/src/bin/accuracycoin_status.rs b/crates/rustynes-test-harness/src/bin/accuracycoin_status.rs index c98e0081..661b9504 100644 --- a/crates/rustynes-test-harness/src/bin/accuracycoin_status.rs +++ b/crates/rustynes-test-harness/src/bin/accuracycoin_status.rs @@ -68,7 +68,7 @@ fn describe(s: TestStatus) -> String { /// A vector with no test result at all describes a run that executed nothing. /// Reporting that as agreement is the failure this tool exists to prevent. fn vacuous(v: &[TestStatus]) -> bool { - v.iter().all(|s| matches!(s, TestStatus::NotRun)) + v.iter().all(|s| *s == TestStatus::NotRun) } /// Entries that are `NotRun` on **both** sides. @@ -115,6 +115,22 @@ fn executed_on_both(a: &[TestStatus], b: &[TestStatus]) -> usize { /// the message. The defect was in the sentence, so the sentence is what a test /// has to be able to see. fn coverage_line(a: &[TestStatus], b: &[TestStatus]) -> String { + // The arithmetic below subtracts two `zip`-derived counts from `a.len()`, + // and `zip` truncates silently to the shorter side -- so unequal lengths + // would report the tail of `a` as "on one side only", which is a third + // wrong count in a tool whose subject is counts that describe the wrong + // set. The invariant holds upstream (`decode_results` returns one entry per + // catalog entry or refuses the dump; see `decoded_vectors_are_always_catalog_length` + // and `a_short_dump_is_refused`), so this cannot fire today. It is here + // because the coupling is otherwise invisible at the point that depends on + // it. Raised in review of v2.6.4. + assert_eq!( + a.len(), + b.len(), + "coverage arithmetic needs equal-length vectors; got {} and {}", + a.len(), + b.len() + ); let dead = both_not_run(a, b); let both = executed_on_both(a, b); format!( @@ -261,6 +277,17 @@ mod tests { use super::{both_not_run, coverage_gate, coverage_line, describe, executed_on_both, vacuous}; use rustynes_test_harness::accuracy_coin_catalog::{TestStatus, catalog, decode_results}; + /// The guard the v2.6.4 review asked for, demonstrated to fire. It cannot + /// be reached through `main` — both vectors come from `decode_results` — + /// so a test is the only thing that can reach it at all. + #[test] + #[should_panic(expected = "coverage arithmetic needs equal-length vectors")] + fn coverage_arithmetic_refuses_unequal_lengths() { + let a = vec![TestStatus::Pass; catalog().len()]; + let b = vec![TestStatus::Pass; catalog().len() - 1]; + let _ = coverage_line(&a, &b); + } + /// The SENTENCE, not the predicates behind it. Reverting the line to the /// subtraction the review flagged came back NOT CAUGHT while the tests only /// reached `both_not_run` and `executed_on_both` — the defect was in the