From 9cd93315c252b27962b12faa2d9650dc943c5014 Mon Sep 17 00:00:00 2001
From: DoubleGate
Date: Tue, 25 Aug 2026 21:24:39 -0400
Subject: [PATCH 1/7] feat(harness): AccuracyCoin's status vector, decoded and
comparable entry-for-entry
Rung 5's acceptance criterion is a status vector that can be compared
ENTRY FOR ENTRY between the oracle and the co-simulation DUT, including
Skipped and NotRun. Producing one is the v2.6.3 deliverable; making the
two agree is v2.6.4. This lands the oracle half of that comparison.
## What the tool does
`accuracycoin_status` reads an AccuracyCoin work-RAM dump, decodes it
against the 146-entry catalog in `accuracy_coin_catalog.rs`, and prints
one line per catalog entry. Given two dumps it diffs them entry by entry
and names every disagreement by test, not by address.
It is a binary rather than a test because its input is a golden produced
outside the workspace: `nes_golden_export --ram` on the oracle side, and
the DUT's own RAM dump on the sibling side. `required-features =
["test-roms"]` because the catalog it decodes against lives behind that
feature.
## Why the status vector rather than the RAM
Byte-comparing 2 KiB of work RAM answers a different question and answers
it wrongly in both directions. It reports scratch bytes -- a stack slot, a
loop counter, a partially written result the ROM is about to overwrite --
as failures, and it reports two runs that never started the battery as a
pass, because two idle title screens have identical RAM. The catalog
decode discards both classes: an entry the run never reached reads
NotRun, and NotRun is a distinct verdict from Pass rather than a byte
that happens to match.
The tool refuses an all-NotRun vector with exit 3. That case is precisely
the one that looks like success to a naive comparison -- two vectors
agreeing on 146 entries of nothing -- and it is exactly the shape of the
vacuous status-address assertion v2.6.2 found in the NTSC blargg suite,
where an unmapped read returned blargg's own success code for five minor
releases. A comparison that cannot distinguish "agreed" from "never ran"
is not a gate.
## The manifest records the controller press, and its absence
The frames-mode manifest emitted by nes_golden_export gains press_start,
written as A:B when a press window was given and the literal none when it
was not.
The manifest exists so a golden's provenance is recoverable from the
golden itself. A controller press changes what the ROM EXECUTES: an
AccuracyCoin export with no press captures an idle title screen, and one
with a press captures 88 test results. Without the field the manifest
describes those two completely different runs identically, which makes it
worse than silent -- it asserts equivalence between them.
Found by needing it. The shipped AccuracyCoin golden plainly contains a
pressed run (80 clean passes are not reachable from a title screen), and
its own manifest could not say which window produced them.
## Current measurement
Against the DUT's first end-to-end run the vector reports 137 of 146
entries agreeing and 9 differing, six of those sharing Fail(code 7) --
five SH-group stores and Open Bus -- which is a pattern suggesting one
shared address-bus cause rather than six independent defects. Naming that
pattern is what the entry-for-entry form buys over a pass count; a count
of 137 would have hidden it.
## Verification
- cargo fmt --all --check clean
- cargo clippy -p rustynes-test-harness --features test-roms
--all-targets -- -D warnings clean
- cargo clippy --all-targets -- -D warnings clean inside the excluded
rustynes-cosim crate (it is not reachable from a workspace build)
No rustynes-{cpu,ppu,apu,mappers,core} changes, so AccuracyCoin 141/141
(RAM decoder) and nestest 0-diff hold by construction.
---
.../src/bin/nes_golden_export.rs | 16 +-
crates/rustynes-test-harness/Cargo.toml | 11 ++
.../src/bin/accuracycoin_status.rs | 173 ++++++++++++++++++
3 files changed, 199 insertions(+), 1 deletion(-)
create mode 100644 crates/rustynes-test-harness/src/bin/accuracycoin_status.rs
diff --git a/crates/rustynes-cosim/src/bin/nes_golden_export.rs b/crates/rustynes-cosim/src/bin/nes_golden_export.rs
index 451758b5..d95f74e4 100644
--- a/crates/rustynes-cosim/src/bin/nes_golden_export.rs
+++ b/crates/rustynes-cosim/src/bin/nes_golden_export.rs
@@ -574,12 +574,26 @@ fn run_mode_block(args: &Args, calls: u64, frames_actual: u64) -> String {
args.inject_hold,
)
} else {
+ // `press_start` is recorded, and its absence is recorded too.
+ //
+ // The manifest exists so a golden's provenance is recoverable from the
+ // golden. A controller press changes what the ROM EXECUTES -- an
+ // AccuracyCoin export without one captures an idle title screen and
+ // with one captures 88 test results -- so a manifest that omits it
+ // describes two completely different runs identically.
+ //
+ // Found by needing it: the shipped `AccuracyCoin` golden plainly
+ // contains a pressed run (80 clean passes), and its own manifest could
+ // not say what window produced them.
format!(
"run_mode = frames\n\
frames_req = {}\n\
frames_actual= {frames_actual}\n\
- run_frame_calls = {calls}\n",
+ run_frame_calls = {calls}\n\
+ press_start = {}\n",
args.frames,
+ args.press_start
+ .map_or_else(|| "none".to_owned(), |(a, b)| format!("{a}:{b}")),
)
}
}
diff --git a/crates/rustynes-test-harness/Cargo.toml b/crates/rustynes-test-harness/Cargo.toml
index d10ff0db..77480594 100644
--- a/crates/rustynes-test-harness/Cargo.toml
+++ b/crates/rustynes-test-harness/Cargo.toml
@@ -120,6 +120,17 @@ name = "dump_battery_ram"
path = "src/bin/dump_battery_ram.rs"
required-features = ["test-roms"]
+# v2.6.3 — AccuracyCoin's status vector, decoded and compared ENTRY FOR ENTRY.
+#
+# Rung 5's acceptance is a comparable status vector, not a byte-equal work RAM:
+# 2 KiB of RAM answers a different question, reporting scratch bytes as failures
+# and reporting two idle title screens as a pass. `required-features` because the
+# catalog it decodes against lives behind `test-roms`.
+[[bin]]
+name = "accuracycoin_status"
+path = "src/bin/accuracycoin_status.rs"
+required-features = ["test-roms"]
+
# v2.3.6 — Zapper light-timing probe. Answers, from the game's own `$4017`
# traffic, whether a light-gun title ever sees light: per frame it pairs each
# read's returned byte (bit 3 = light NOT detected) with whether the aim point
diff --git a/crates/rustynes-test-harness/src/bin/accuracycoin_status.rs b/crates/rustynes-test-harness/src/bin/accuracycoin_status.rs
new file mode 100644
index 00000000..01d9ebde
--- /dev/null
+++ b/crates/rustynes-test-harness/src/bin/accuracycoin_status.rs
@@ -0,0 +1,173 @@
+//! Decode an `AccuracyCoin` work-RAM dump into its status vector, and optionally
+//! compare two of them **entry for entry**.
+//!
+//! # Why this exists
+//!
+//! Rung 5's acceptance is not "the DUT's RAM matches". It is that an end-to-end
+//! `AccuracyCoin` run "produces a status vector that can be compared entry-for-entry
+//! against the oracle's" — and a byte comparison of 2 KiB of work RAM cannot do
+//! that. It answers a different question, and answers it badly in both
+//! directions:
+//!
+//! * it reports a difference in any scratch byte the suite happens to leave
+//! lying around as though a test had failed, and
+//! * it reports **success** for two runs that both sat on the title screen and
+//! ran nothing, which is exactly how the first co-simulation run of this ROM
+//! read as a pass.
+//!
+//! The status vector is the thing with meaning: 146 catalog entries, each a byte
+//! the ROM writes at a known address. Decoding it turns "2048 bytes differ" into
+//! "these tests disagree, and here is what each side said".
+//!
+//! # The anti-vacuity guard is the point
+//!
+//! `fb_diff.py` refuses a reference framebuffer with fewer than eight distinct
+//! values, because a uniform frame cannot distinguish a working renderer from a
+//! broken one. The RAM comparison had no such guard and read as a pass on an
+//! idle menu.
+//!
+//! So this tool refuses too: a vector that is entirely `NotRun` is reported as
+//! **vacuous**, with a non-zero exit, whichever side it came from. A run that
+//! executed nothing is not a passing run, and it must not be possible to
+//! mistake one for the other.
+
+use std::path::PathBuf;
+use std::process::ExitCode;
+
+use rustynes_test_harness::accuracy_coin_catalog::{
+ TestStatus, catalog, decode_results, summarise,
+};
+
+fn usage() -> ! {
+ eprintln!(
+ "usage: accuracycoin_status []\n\
+ \x20 one file -- decode and summarise that run's status vector\n\
+ \x20 two files -- compare them ENTRY FOR ENTRY (first = reference)"
+ );
+ std::process::exit(2)
+}
+
+fn read_ram(p: &PathBuf) -> Vec {
+ std::fs::read(p).unwrap_or_else(|e| {
+ eprintln!("read {}: {e}", p.display());
+ std::process::exit(2)
+ })
+}
+
+fn describe(s: TestStatus) -> String {
+ match s {
+ TestStatus::NotRun => "NotRun".into(),
+ TestStatus::Pass => "Pass".into(),
+ TestStatus::PassWithCode(n) => format!("Pass(code {n})"),
+ TestStatus::Fail(n) => format!("Fail(code {n})"),
+ TestStatus::Skipped => "Skipped".into(),
+ TestStatus::Unknown(b) => format!("Unknown(${b:02X})"),
+ }
+}
+
+/// 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))
+}
+
+fn main() -> ExitCode {
+ let args: Vec = std::env::args_os().skip(1).map(PathBuf::from).collect();
+ if args.is_empty() || args.len() > 2 {
+ usage();
+ }
+
+ let decode = |p: &PathBuf| -> Vec {
+ let ram = read_ram(p);
+ decode_results(&ram).unwrap_or_else(|| {
+ eprintln!(
+ "{} is {} bytes -- too short to hold the result vector; \
+ pass the full 2 KiB work RAM",
+ p.display(),
+ ram.len()
+ );
+ std::process::exit(2)
+ })
+ };
+
+ let a = decode(&args[0]);
+ let sum = summarise(&a);
+ println!(
+ "{}: total={} pass={} pass_with_code={} fail={} skipped={} not_run={} unknown={}",
+ args[0].display(),
+ sum.total,
+ sum.pass,
+ sum.pass_with_code,
+ sum.fail,
+ sum.skipped,
+ sum.not_run,
+ sum.unknown
+ );
+
+ if vacuous(&a) {
+ eprintln!(
+ "\nVACUOUS: every one of the {} entries is NotRun. This run executed no \
+ tests -- AccuracyCoin sits on its title screen until START is pressed. \
+ Re-export with --press-start; a comparison against this proves nothing.",
+ a.len()
+ );
+ return ExitCode::from(3);
+ }
+
+ let Some(second) = args.get(1) else {
+ // Single-file mode: list anything that is not a clean pass, so the
+ // interesting entries are visible without diffing against anything.
+ let names: Vec<_> = catalog()
+ .iter()
+ .zip(&a)
+ .filter(|(_, s)| !matches!(s, TestStatus::Pass))
+ .map(|(e, s)| format!(" {:<44} {}", e.name, describe(*s)))
+ .collect();
+ if names.is_empty() {
+ println!("every catalog entry is a clean Pass.");
+ } else {
+ println!("\nentries that are not a clean Pass ({}):", names.len());
+ for l in names {
+ println!("{l}");
+ }
+ }
+ return ExitCode::SUCCESS;
+ };
+
+ let b = decode(second);
+ if vacuous(&b) {
+ eprintln!(
+ "\nVACUOUS: {} has every entry NotRun -- see above.",
+ second.display()
+ );
+ return ExitCode::from(3);
+ }
+
+ let diffs: Vec<_> = catalog()
+ .iter()
+ .zip(a.iter().zip(b.iter()))
+ .filter(|(_, (x, y))| x != y)
+ .map(|(e, (x, y))| {
+ format!(
+ " {:<44} ref={:<16} actual={}",
+ e.name,
+ describe(*x),
+ describe(*y)
+ )
+ })
+ .collect();
+
+ if diffs.is_empty() {
+ println!(
+ "\nstatus vectors are IDENTICAL entry for entry across all {} entries.",
+ a.len()
+ );
+ ExitCode::SUCCESS
+ } else {
+ println!("\n{} of {} entries differ:", diffs.len(), a.len());
+ for d in &diffs {
+ println!("{d}");
+ }
+ ExitCode::FAILURE
+ }
+}
From 5afc4d5acf2724aadcc1ba7620b43340fab9ad69 Mon Sep 17 00:00:00 2001
From: DoubleGate
Date: Tue, 25 Aug 2026 21:31:40 -0400
Subject: [PATCH 2/7] chore(release): cut 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 in this release. It took its clock
enables as INPUTS and let the testbench generate 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.
The emulation core is unchanged. No rustynes-{cpu,ppu,apu,mappers,core}
changes, so AccuracyCoin and nestest hold by construction -- and both
were run anyway (numbers below).
## What the release contains
The substantive work landed in the sibling repository (RustyNES_MiSTer,
`main` at 2aa07bb) and in this repository's harness. This commit is the
ceremony: version, CHANGELOG, the sixteen release anchors, the plan row,
and the notes.
**The divider 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 and the open-bus decay reload. 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.
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 interrupt recognition, correctly gated, so a second
delay would have cancelled an APU-side error rather than removed it.
**Two accumulators, not a phase counter.** The divider is built in
RustyNES's own v2.0.0 "Timebase" shape: two independent accumulators in
master-clock units, never reset to one another. 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.
**blargg's instr_test-v5 is a standing gate at 16 of 16 exact**, taking
the suite from 50 gates to 66 green, 0 failed, and closing the decoder at
256 of 256 opcodes. It found three defects the entire self-written corpus
had missed, none in the opcodes it was run to validate.
**The decay constant is a measured three-way disagreement.** The wiki
says 3-30 ms; RustyNES uses 558.7 ms. Swept against the full 66-gate
suite: 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, and the prediction was tested:
2,809,000 dots leaves 52 divergences, 2,811,000 is exact. Documentation
and corpus are incompatible by a factor of ~17 and this rung has no
independent oracle to adjudicate, so the constant stays the oracle's,
stays labelled fitted, and stays a localparam.
**Rung 5 reaches an end-to-end AccuracyCoin run** -- 17,868,316 cycles --
and the harness gains `accuracycoin_status`, committed separately. First
measurement: 137 of 146 entries agree, 9 differ, six sharing one failure
code. Producing the vector is this release's deliverable; making the two
agree is v2.6.4.
## The ceremony itself
Cut with `scripts/release-automation/bump_release.py --apply` rather than
by hand. That tool exists because a mechanical marker swap produced ten
confidently-wrong anchors at v2.4.4 -- version moved, codename moved,
description still described the previous release. It classified all
sixteen anchors by shape (5 bare, 5 dash, 2 paren, 2 chain, 1 period, 1
dated_code), demoted each one's prose behind the new lead rather than
overwriting it, and refused nothing.
Two edits it does not do, done by hand: the VERSION-PLAN table row, and
the ROADMAP chain tail that names its own last entry the current release.
`release_anchor_audit` named both, by test.
## Verification
- AccuracyCoin (RAM): pass rate = 100.00% over 141 assigned tests
(total=146 pass=130 pass_with_code=11 fail=0 skipped=0 not_run=5).
Read with --nocapture: the line is captured by default, and an empty
grep is not a pass.
- nestest: nestest_pc_c000_matches_golden_log ok, 0-diff.
- cargo fmt --all --check clean.
- cargo clippy --workspace --all-targets -- -D warnings clean.
- All five standing release audits green: release_anchor_audit 10/10,
release_state_prose_audit 8/8, release_notes_render_audit 2/2,
libretro_info_audit 3/3, cosim_manifest_audit 4/4.
- markdownlint clean on every changed document.
The libretro `.info` display_version moves with the workspace, as the
audit requires. No upstream sync is opened: the amended cadence defers it
until the MiSTer core is complete, and a licence change -- the one
override -- has not occurred.
---
.github/release-notes/v2.6.3.md | 75 +++++++++++++++++++
AGENTS.md | 4 +-
ARCHITECTURE.md | 2 +-
CHANGELOG.md | 39 +++++++++-
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 | 20 +++--
docs/mister.md | 38 ++++++++++
to-dos/ROADMAP.md | 4 +-
18 files changed, 210 insertions(+), 51 deletions(-)
create mode 100644 .github/release-notes/v2.6.3.md
diff --git a/.github/release-notes/v2.6.3.md b/.github/release-notes/v2.6.3.md
new file mode 100644
index 00000000..8bedc7d5
--- /dev/null
+++ b/.github/release-notes/v2.6.3.md
@@ -0,0 +1,75 @@
+# RustyNES v2.6.3 — "Mainspring"
+
+**The DUT runs on one master clock.** `nes_top` took its clock enables as *inputs* and let the testbench generate 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. A mainspring is the single wound source that drives a clock's whole train, which is what that top level becomes.
+
+**The emulation core is unchanged.** No `rustynes-{cpu,ppu,apu,mappers,core}` changes, so AccuracyCoin **141/141 (100.00%, RAM decoder)** and nestest 0-diff hold by construction — and both were run anyway.
+
+**Suite: 66 gates green, 0 failed** in the sibling repository, up from 50.
+
+## It found four enables that were never enabling
+
+The old testbench tied `ce` high and pulsed the clock once per CPU cycle. That means 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 such block fires twelve times instead of once.
+
+Two were already known — the PPU register block, found at v2.5.7, and the open-bus decay reload. **Two were not:**
+
+- **The DMC's DMA acknowledge.** The sample pointer advanced by **twelve** per byte, and **324,182 of 357,360 cycles diverged**.
+- **The frame-counter IRQ set points.** 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 produced 66 of 66, and is indistinguishable from the real fix by gate result. `cpu6502.sv` already implements the oracle's second-to-last-cycle interrupt recognition, correctly gated, so a second delay would have cancelled an APU-side error rather than removed it. Looking for a cause *after* the fix worked is what separated them.
+
+## Two accumulators, not a phase counter
+
+The divider is built in RustyNES's own v2.0.0 "Timebase" shape: **two independent accumulators in master-clock units, never reset to one another.** That is not a stylistic preference. 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 CPU 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.
+
+## blargg's instruction battery becomes a standing gate, and finds three defects
+
+Sixteen third-party ROMs from `instr_test-v5`, ~2.68 M cycles each, compared per cycle: **16 of 16 exact.** Every rung-1 ROM before these was written inside this project, so the rung could only ask questions someone here thought to ask.
+
+**None of the three defects it found was in the opcodes the battery was run to validate:**
+
+- **`RRA` fed its `ADC` stage the carry from *before* the instruction**, not the one the rotate had just produced. The instruction's own bus trace was **identical on both sides** — read, dummy write, write — and only the accumulator differed, by one, surfacing nine cycles later in the `STA` that spilled it. A gate on the memory side of read-modify-write would have passed it.
+- **The 8-cycle indirect read-modify-write forms addressed the indexed target during their *pointer* fetch cycles.**
+- **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 (`SHA`, `TAS`, `SHY`, `SHX`) close the decoder at **256 of 256 opcodes**.
+
+## Where documentation and oracle contradict each other, measurably
+
+The PPU open-bus decay is implemented, and **the fitted part is disclosed in the RTL itself.** That the latch decays, in three independent groups, and which accesses refresh which group, are documented facts. The *deadline* is not: the wiki says 3–30 ms "faster when the PPU is warm", and RustyNES uses 558.7 ms.
+
+Swept against the full 66-gate suite rather than argued: **30 ms** (the documented upper bound) fails **9** gates, **50 ms** fails 5, **100 ms** 3, **200 ms** 2, **300 ms** 1, and **558.7 ms is the first value that fails 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. The corpus demands **≥ 523.4 ms**.
+
+**Documentation and corpus are therefore incompatible by a factor of ~17**, and this rung has no independent oracle to say which describes a 2C02. That is risk 6 of the Fabric plan — *the oracle can be wrong* — arriving as a measurement rather than a caveat, and the first time in this programme that documentation and oracle have been shown to contradict each other on a quantity a gate depends on. The constant stays the oracle's, stays labelled **fitted**, and stays a `localparam` so it can move when something can adjudicate.
+
+## Rung 5: AccuracyCoin end to end, and a status vector
+
+The full AccuracyCoin run now completes on the DUT — 17,868,316 cycles, where it previously halted early — and the oracle gains `accuracycoin_status`.
+
+Rung 5's stated acceptance is a status vector comparable **entry for entry**, including `Skipped` and `NotRun`. The tool reads a work-RAM dump, decodes it against the 146-entry catalog, prints one line per entry, and given two dumps names every disagreement **by test rather than by address**.
+
+First measurement: **137 of 146 entries agree, 9 differ** — six of those sharing one failure code (five `SH`-group stores and Open Bus), a pattern that suggests one shared address-bus cause rather than six independent defects. A pass count of 137 would have hidden that pattern.
+
+**Producing the vector is this release's deliverable; making the two agree is v2.6.4.**
+
+Byte-comparing 2 KiB of work RAM answers a different question and answers it wrongly in both directions: it reports scratch bytes as failures, and it reports two runs that never started the battery as a pass, because two idle title screens have identical RAM. So the tool **refuses an all-`NotRun` vector** with a non-zero exit — that case is exactly the shape of the vacuous status-address assertion v2.6.2 found in the NTSC blargg suite, which reported 11/11 for five minor releases while asserting nothing.
+
+The golden manifest also gains `press_start`, recorded as `A:B` or the literal `none`. A controller press changes what the ROM *executes* — an AccuracyCoin export without one captures an idle title screen and with one captures 88 test results — so a manifest omitting it describes two completely different runs identically. Found by needing it: the shipped golden plainly contains a pressed run, and its own manifest could not say which window produced it.
+
+## Dependencies, and a held pin finally measured
+
+- **Android:** AGP **9.2.1 → 9.3.2**, Compose compiler plugin **2.3.10 → 2.3.21**, `compose-bom` **2026.06.00 → 2026.08.00**, the three `material3.adaptive` artifacts **1.2.0 → 1.3.0**, plus `jna` and `play-services-games-v2`. Four Gradle 10 deprecations cleared, each verified against the published artifact rather than the warning text. The AGP/Kotlin interlock was **measured out of the published POMs**: 9.2.1 and 9.3.2 declare the same `kotlin-gradle-plugin` coordinate, so crossing that minor does not move the Kotlin requirement — and there is no `kotlin-gradle-plugin` version in this build to set at all.
+- **Rust and Actions:** 17 crates to their newest semver-compatible versions, `directories` 5 → 6, and four GitHub Actions advanced.
+- **`markdownlint-cli` v0.39.0 → v0.49.1, and MD060 becomes a live gate.** The pin had been held since v2.3.9 precisely because the newer binary reported a rule the pinned one did not — recorded at the time as a hazard rather than measured. Measured now: `MD060/table-column-style`'s inferred default reads this corpus as style `compact`, producing **1,936 findings across 122 files** and nothing else. Every one is a table the project already writes the same way, so the style in use is pinned instead — **zero findings, no document rewritten.**
+
+**Held, deliberately:** egui 0.36 / wgpu 30 (and now `naga` 30, enforced — it is a *direct* dependency, so Dependabot would have broken the hold from a direction the existing ignores did not cover), the `getrandom` 0.2 + 0.3 pair, Rust **1.96.0**, and Quartus **17.0.2**.
+
+## Verification
+
+- **AccuracyCoin 141/141 (100.00%, RAM decoder)** and **nestest 0-diff** — run, not asserted.
+- `cargo fmt --all --check`, `cargo clippy --workspace --all-targets -- -D warnings` and every gated feature combination, and `RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps`.
+- The five standing release audits — anchors, state prose, notes rendering, the libretro `.info`, and the cosim manifest.
+- Sibling repository: **66 gates green, 0 failed.**
diff --git a/AGENTS.md b/AGENTS.md
index 4b047255..81027b73 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -41,7 +41,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.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.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.
@@ -203,7 +203,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.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.2 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.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.
- **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 8c550eb8..db6d3d9d 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.2 (the scheduling model is v2.0.0 "Timebase" onward)
+**Applies to:** RustyNES v2.6.3 (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 d4c6df69..e5bd13eb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,7 +24,44 @@ the documentary lineage of how that core was built (not standalone user
releases), and `v0.1.0`–`v0.8.6` are the original pre-1.0 engine that the
cycle-accurate core later replaced.
-## [Unreleased]
+## [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
+
+- **AccuracyCoin's status vector, decoded and comparable entry for entry.**
+ Rung 5's stated acceptance is a status vector comparable **entry for
+ entry** — including `Skipped` and `NotRun` — between the oracle and the
+ co-simulation DUT. `accuracycoin_status` is the oracle half: it reads a
+ work-RAM dump, decodes it against the 146-entry catalog, prints one line
+ per entry, and given two dumps names every disagreement **by test rather
+ than by address**.
+
+ Producing one is this release's deliverable; making the two agree is
+ v2.6.4. The first end-to-end DUT run reports **137 of 146 entries
+ agreeing and 9 differing**, six of those sharing `Fail(code 7)` — five
+ SH-group stores and Open Bus — a pattern that suggests one shared
+ address-bus cause rather than six independent defects. A pass count of
+ 137 would have hidden that pattern; naming the entries is what the
+ entry-for-entry form buys.
+
+ **Byte-comparing 2 KiB of work RAM answers a different question, and
+ answers it wrongly in both directions**: it reports scratch bytes as
+ failures, and it reports two runs that never started the battery as a
+ pass, because two idle title screens have identical RAM. So the tool
+ refuses an all-`NotRun` vector with a non-zero exit. That case — two
+ vectors agreeing on 146 entries of nothing — is exactly the shape of the
+ vacuous status-address assertion v2.6.2 found in the NTSC blargg suite,
+ which reported 11/11 for five minor releases while asserting nothing.
+
+- **The golden manifest records the controller press, and its absence.**
+ The frames-mode manifest gains `press_start`, written as `A:B` when a
+ window was given and the literal `none` when it was not. A controller
+ press changes what the ROM *executes* — an AccuracyCoin export without
+ one captures an idle title screen and with one captures 88 test results
+ — so a manifest omitting it describes two completely different runs
+ identically. Found by needing it: the shipped `AccuracyCoin` golden
+ plainly contains a pressed run, and its own manifest could not say which
+ window produced it.
### Changed
diff --git a/Cargo.lock b/Cargo.lock
index 5a82aac9..85c3f3e3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4290,7 +4290,7 @@ dependencies = [
[[package]]
name = "rustynes-android"
-version = "2.6.2"
+version = "2.6.3"
dependencies = [
"android-activity",
"android_logger",
@@ -4308,7 +4308,7 @@ dependencies = [
[[package]]
name = "rustynes-apu"
-version = "2.6.2"
+version = "2.6.3"
dependencies = [
"bitflags 2.13.1",
"criterion",
@@ -4321,7 +4321,7 @@ dependencies = [
[[package]]
name = "rustynes-cheevos"
-version = "2.6.2"
+version = "2.6.3"
dependencies = [
"cc",
"ureq",
@@ -4329,7 +4329,7 @@ dependencies = [
[[package]]
name = "rustynes-core"
-version = "2.6.2"
+version = "2.6.3"
dependencies = [
"bitflags 2.13.1",
"criterion",
@@ -4346,7 +4346,7 @@ dependencies = [
[[package]]
name = "rustynes-cpu"
-version = "2.6.2"
+version = "2.6.3"
dependencies = [
"bitflags 2.13.1",
"criterion",
@@ -4357,7 +4357,7 @@ dependencies = [
[[package]]
name = "rustynes-frontend"
-version = "2.6.2"
+version = "2.6.3"
dependencies = [
"anstyle",
"arboard",
@@ -4416,18 +4416,18 @@ dependencies = [
[[package]]
name = "rustynes-gamedb"
-version = "2.6.2"
+version = "2.6.3"
dependencies = [
"rustynes-core",
]
[[package]]
name = "rustynes-gfx-shaders"
-version = "2.6.2"
+version = "2.6.3"
[[package]]
name = "rustynes-hdpack"
-version = "2.6.2"
+version = "2.6.3"
dependencies = [
"lewton",
"png",
@@ -4438,7 +4438,7 @@ dependencies = [
[[package]]
name = "rustynes-ios"
-version = "2.6.2"
+version = "2.6.3"
dependencies = [
"bytemuck",
"cpal",
@@ -4452,7 +4452,7 @@ dependencies = [
[[package]]
name = "rustynes-libretro"
-version = "2.6.2"
+version = "2.6.3"
dependencies = [
"libc",
"rust-libretro",
@@ -4461,7 +4461,7 @@ dependencies = [
[[package]]
name = "rustynes-mappers"
-version = "2.6.2"
+version = "2.6.3"
dependencies = [
"bitflags 2.13.1",
"criterion",
@@ -4473,7 +4473,7 @@ dependencies = [
[[package]]
name = "rustynes-mobile"
-version = "2.6.2"
+version = "2.6.3"
dependencies = [
"rustynes-core",
"rustynes-hdpack",
@@ -4488,7 +4488,7 @@ dependencies = [
[[package]]
name = "rustynes-netplay"
-version = "2.6.2"
+version = "2.6.3"
dependencies = [
"futures-util",
"js-sys",
@@ -4504,7 +4504,7 @@ dependencies = [
[[package]]
name = "rustynes-ppu"
-version = "2.6.2"
+version = "2.6.3"
dependencies = [
"bitflags 2.13.1",
"criterion",
@@ -4516,21 +4516,21 @@ dependencies = [
[[package]]
name = "rustynes-probe"
-version = "2.6.2"
+version = "2.6.3"
dependencies = [
"rustynes-core",
]
[[package]]
name = "rustynes-ra"
-version = "2.6.2"
+version = "2.6.3"
dependencies = [
"rustynes-cheevos",
]
[[package]]
name = "rustynes-script"
-version = "2.6.2"
+version = "2.6.3"
dependencies = [
"mlua",
"piccolo",
@@ -4541,7 +4541,7 @@ dependencies = [
[[package]]
name = "rustynes-test-harness"
-version = "2.6.2"
+version = "2.6.3"
dependencies = [
"insta",
"png",
diff --git a/Cargo.toml b/Cargo.toml
index 51f8d6bf..285f9f3b 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.2"
+version = "2.6.3"
edition = "2024"
rust-version = "1.96"
license = "GPL-3.0-or-later"
diff --git a/OVERVIEW.md b/OVERVIEW.md
index 9c4d567e..8523e711 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.2
+**Applies to:** RustyNES v2.6.3
---
@@ -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.2 "Witness"**. 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.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.
-> 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.2**.
+> 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**.
---
diff --git a/README.md b/README.md
index 7bb4d2e8..d3d9177d 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
-

+


@@ -674,7 +674,7 @@ and the Material-for-MkDocs documentation handbook at
## Current Release
-RustyNES's current release is **v2.6.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.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 211c0a1b..247259e4 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.2 "Witness" released — the current head of the line, on **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.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.
---
## Where we are
-RustyNES is well past v1.0.0. The current release is **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.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 291170ce..aa8814ab 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -2,7 +2,7 @@
## Supported Versions
-The current release is **v2.6.2 "Witness"**, on **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.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
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 9ef55472..199a64e7 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.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.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 d7d6cdb0..d5073797 100644
--- a/VERSION-PLAN.md
+++ b/VERSION-PLAN.md
@@ -1,6 +1,6 @@
# RustyNES Version Plan
-**Current release: 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.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/).
@@ -105,10 +105,11 @@ The 1.x line was **additive / off-by-default** — every release stayed byte-ide
| **v2.5.6 "Vestige"** | Sprite evaluation closes, and a byte index that outlives the walk that set it. A vestige is what remains after the thing that made it is gone, which is exactly the residual address the 2C02 leaves behind. The evaluation FSM, secondary OAM, the eight-sprite limit, the documented overflow-search bug and the wiki's step 4 all land, gated on what a CPU read of `$2004` returns while rendering, and **all 59,993 overlapping cycles match** with **seven of eight mutations CAUGHT**. The plan named this the hardest single item in the programme and it was, for a reason nothing in the plan anticipated: **the gate observed a model the diagnostic did not expose.** `ppu-state-trace` carries `sprite_eval_n`, `sprite_eval_m` and `sprite_eval_found`, which belong to the oracle's real evaluation FSM -- and `$2004` does not come from that machine at all. It comes from `tick_oam_bus`, a second, side-effect-free model kept alongside it. Two edits made faithful to the traced fields each made things WORSE (41 -> 112 and 39 -> 68) and were reverted as regressions; both were faithful to the wrong model. Adding `oam_bus_copybuffer` to `PpuStateRecord` at schema **2** is what made every later measurement valid, and it immediately showed the FSM sitting frozen at `n = 34` while the bus kept walking. **One of those two "regressions" was then right.** The overflow halt had been measured while phase 4 was itself mis-implemented, so it moved the DUT into a broken destination -- re-measured against a correct phase 4 it is worth 28 of the 39. A change rejected against a broken baseline is not a rejected change. **And the final fix is the opposite of the obvious one**: the wiki says `OAM[n][0]`, but pinning the byte index to 0 is right on line 55 and wrong on line 58, because phase 4 advances only the high half of the address and the low half keeps whatever ended the walk -- three of the four paths that finish evaluation clear it, and the sprite-eval bug path does not. `eval_hold`, a latched byte two earlier findings had been built on, became dead the moment the write dot presented `sec_oam[sec_idx]`, and Verilator said so before the gate ran. **Nine of nine behavioural mutants are CAUGHT and two are proved INERT**, and a first pass got that wrong: it reported the `eval_ovf_cnt` reset as an uncaught defect the stimulus could not reach. Retracted -- the code is UNREACHABLE, established three ways (a probe firing zero times at 528 window ends while its inverted predicate fires 528; a byte-identical trace with the reset removed; and a structural bound of 88 decide steps to the latest possible hit, consumed by 91, in a 96-step window). The reset is kept as DEFENSIVE code, not as a fix. The second inert mutant -- the hit not setting `sprite_overflow` -- is out of SCOPE rather than unreachable, the flag reaching the CPU only via `$2002` which this ROM never reads, and that is the declared compare surface showing up as a measurement. Both were classified by byte comparison, because NOT CAUGHT has meant four different things here and only a comparison separates them. `rustynes-ppu` changes, so **AccuracyCoin 141/141 (RAM decoder) and nestest 0-diff are VERIFIED, not asserted**, and every earlier rung was re-run: 61,440 pixels, 6,247 fetches, 59,554 nestest cycles. |
| **v2.5.7 "Collimation"** | Sprite rendering closes exact — the phase was wrong by two dots, and every window was compensating. Collimation is the act of bringing two optical axes true; until it is done, two sightlines can agree and both be wrong. That is literally what this release found: the boot traces agreed on `scanline`/`dot` at every instruction boundary while the per-cycle mappings differed by exactly two dots — a CPU–PPU power-on phase error hidden by an equal record-point offset in the testbench, two errors cancelling. The earlier sweep had tested `PPU_LEAD` 0 and 3 and the answer, **2**, sat between them, never tried until the two mappings were reconciled by hand. `PPU_LEAD=2` closed `ppuspr020` exactly and broke three other gates — the signature of compensations fitted at the wrong phase — and the coherent set moves every OAM window from documented-minus-three to **documented-minus-one, which is registered-assignment semantics and no residual fudge at all**: the RTL's own comments had claimed MINUS ONE all along while the code sat at minus-three, so the phase fix brought the code to the prose. Sprite rendering itself lands first — eight slots, priority, left-8 masks, sprite-0 hit with the no-hit-at-x=255 quirk, garbage NT fetches, the 337/339 dummies — plus the **odd-frame skipped dot**, whose absence was a *drift* (one dot per odd frame; `ppuspr020`'s first frame agreed exactly and its second did not) and whose gate is **`ppu-phase-gate`**, the inverse of `cpu-gate`'s skip list: `scanline` and `dot` only, twelve frames, 98,562 records. **Every gate in the rung reports zero divergences for the first time**, and **all ten of the mutation catalog are CAUGHT** — re-run in full at the corrected phase, the tenth (sprite-0 flag read from the register only) caught by `ppuspr020` at exactly one divergence, the read landing on the hit dot, and the `$2004` readback mutant reproducing the old catalog's exact 110. Also found by instrument rather than argument: **the DUT's CPU register file was gated by nothing** — `PPU_SUBDOT`, built to test whether the half-dot between `read_split` and `write_split` is observable (it is not, now measured), instead exposed every `$2000`–`$2007` write latching on every clock edge, correct only because the harness pulsed the decode once per cycle; latent until v2.6.5, where a held address would latch twelve times per access; fixed with `cpu_ce`, a one-clock commit strobe, the pre-fix behaviour CAUGHT by two independent gates. The remaining NOT CAUGHT results each carry evidence and an owner — three deferred coverage gaps and two inert mutants: the vblank register-only read (stimulus gap on the v2.5.8 race dot), the skip-check delay (blargg `10-even_odd_timing`, needs v2.5.8), `chr_wr` (**0** assertions measured across all eight CHR-ROM ROMs; v2.6.3), and two provably inert mutants. **Zero emulation-core changes on the oracle side** — the diff is one diagnostic printout in the test harness — so **AccuracyCoin 141/141 (RAM decoder) and nestest 0-diff hold by construction**. |
| **v2.5.8 "Blanking"** | VBlank, NMI and the `$2002` race close rung 3 — and both fixes were deletions. Blanking is the interval the whole frame exists to reach, and this is the step where the DUT's CPU can finally be told about it: the VBlank flag's full CPU-visible behaviour lands (`suppress_vbl` for the read-one-clock-before-the-set race; the read-on-the-set-dot case needing **no register at all**, because the read's clear is the last assignment in the `always_ff` and wins over the same-edge set), and **the PPU's /NMI reaches the CPU for the first time**. Four purpose-built ROMs carry the step, every one with its stimulus **measured from the oracle's own trace before anything ran** — and the first draft of one put its handler inside the power-on NOP slide, where reset *executed* it; both sides agreed about all of it, because both read the same wrong ROM. Two structural findings, both ending in a deletion. **The testbench's cycle split was `[2 pre-dots \| access \| 1 post]` and the oracle's is `[1 \| access \| 2]`**: a `$2002` read racing the VBL set produces a ~2-dot /NMI pulse the DUT's end-of-cycle sample could not see (one NMI in 24 frames, missed); `PPU_LEAD=3` with `ACCESS_DOT=1` keeps the access on the same absolute dot and moves the boundary, and the pulse-stretcher built first was measured dead and deleted. **The skip-check delay does not exist**: `ppuvbl024` caught the DUT skipping ten pre-renders the oracle never skipped — invisible to the bus gate for eight frames because NMI delivery quantizes away single-dot drifts — and the two-PPU-clock rule plus the commit-edge sampling asymmetry lands exactly on the rendering enable itself, so `render_for_skip` is deleted. **Twelve of twelve mutations CAUGHT**, three at exactly one divergence; the last needed a **cadence-breaking frame**, because the landing it fires on (an enable write at pre-render dot 338 of an odd frame) is unreachable by any fixed-cadence ROM — the CPU's 3-dot quantum and the skip's 1-dot drift co-evolve, locking odd-frame landings to one residue mod 3, and one frame that branches past both writes shifts the class. **nestest 0-diff at 5,002,992 cycles — the 5M-cycle window, standing since v2.5.0, closes**, and rung 3's acceptance criteria are met in full: the next divergence anywhere is an APU or controller surface, which is rung 4. **Zero emulation-core changes on the oracle side** — the diff is documentation — so **AccuracyCoin 141/141 (RAM decoder) and nestest 0-diff hold by construction**. |
-| **v2.6.2 "Witness"** (current) | Rung 4 closes. blargg's `blargg_apu_2005.07.30` battery -- a third-party corpus tested on real hardware -- is the first rung-4 evidence with an **independent** oracle, and it went from **0 of 11 to 11 of 11 exact**. **48 gates green, 0 failed; 56 of 56 mutations CAUGHT**, 0 NOT CAUGHT, 0 BUILD-FAILED, across 25 arms with all 25 baselines passing. **Every one of the six root causes was invisible to the gates this project wrote for itself.** An intermediate pass reached 3 of 11 and recorded four changes that each "bought a blargg ROM by breaking a gate", concluding the surfaces were in tension -- they were not, and a proof was available: the oracle passes blargg 11/11 AND generates every golden those gates compare against, so ONE model satisfies both and an apparent trade-off is a bug report about the change. All four dissolved. The half-rate frame counter was also provably incapable: with the landing at L, a half-frame fires at L+14910+d against the oracle's L0+14913 while the IRQ fires at L+29828+d' against L0+29828, so both hold only if d'=d-3 -- which is why the constants had been calibrated per step, exactly as the model's own comment suspected. **The sequencer now counts CPU cycles** with the documented positions (7457 / 14913 / 22371 / 29828 / 29829 / 29830), derived from the wiki's APU-cycle table plus its GET/PUT column ("3728, PUT" is CPU 2*3728+1) -- no rounding, no per-step fudge, and a 16-bit counter because the 5-step wrap is 37,282. **The six defects**: a `$4017` reset counted as a sequence WRAP (so every `$4017` write raised a frame IRQ the hardware never raises, forking blargg03 for 678,517 cycles from one bit); the reset landing one CPU cycle late (register latency, not the documented number -- one line took the battery 3 of 11 to 7 of 11); `$4015` not seeing a same-cycle length clock; a sequence with 29,831 states instead of 29,830, caught because the divergence RUNS lengthened 1, 2, 3 cycles -- a drift, not an offset; the length halt applying a cycle early; and the mode-1 immediate quarter+half firing at the WRITE rather than the reset landing, which had been calibrated against a reset that itself landed late -- two errors that cancelled. **Two of the six rules are absent from the nesdev wiki** and stated in blargg's own `readme.txt`, shipped beside the ROMs the whole time: the length-halt delay and the reload drop. **Three items open at the battery's close were then closed too**: the power-up `$4017` state (`apupower041`, the only ROM here that never writes `$4017`, so the state had been invisible by construction rather than by measurement -- the DUT's first frame IRQ rose two cycles late); and the length-reload drop (`apureload042`, whose `$4003` write is PLACED on CPU 29,827 because a walking stimulus provably cannot resolve one cycle inside a 7,457-cycle period), which exposed the drop as **dead code**: the promote block ran ~90 lines above the half-frame clock under a comment claiming it ran after it, so the clock overrode every reload. That ordering was accidentally correct, which is exactly why nothing caught it. **One item stays open by measurement rather than absence**: flipping the ORACLE's own power-on `apu_phase` leaves the battery at 11/11, so the strongest independent oracle available is provably indifferent to the divider phase -- blargg's readme says why (it is random on silicon and the ROMs tolerate both), and closing it needs hardware. **Oracle-side, the NTSC blargg suite had been asserting nothing since it was written**: it read `$6000`, which is unmapped on this vintage and returns `0` -- blargg's SUCCESS code -- so all eleven assertions were statements about an unmapped address. The identical defect was fixed for the PAL half in v2.1.5 and the NTSC half was never migrated. The obvious repair is also wrong (these ROMs report a numeric result code, not `PASSED`), so a third reader was needed. The same false oracle is why `tests/roms/extra/apu` was dismissed as audio-only: ten of its nineteen ROMs do report, and **four report TEST FAILED** -- surfaced and recorded, not chased, because they are oracle-side accuracy findings rather than rung-4 ones. **Zero emulation-core changes** -- only `rustynes-test-harness` and documentation -- so **AccuracyCoin 141/141 (RAM decoder) and nestest 0-diff hold by construction**, and both were run anyway. |
+| **v2.6.2 "Witness"** | Rung 4 closes. blargg's `blargg_apu_2005.07.30` battery -- a third-party corpus tested on real hardware -- is the first rung-4 evidence with an **independent** oracle, and it went from **0 of 11 to 11 of 11 exact**. **48 gates green, 0 failed; 56 of 56 mutations CAUGHT**, 0 NOT CAUGHT, 0 BUILD-FAILED, across 25 arms with all 25 baselines passing. **Every one of the six root causes was invisible to the gates this project wrote for itself.** An intermediate pass reached 3 of 11 and recorded four changes that each "bought a blargg ROM by breaking a gate", concluding the surfaces were in tension -- they were not, and a proof was available: the oracle passes blargg 11/11 AND generates every golden those gates compare against, so ONE model satisfies both and an apparent trade-off is a bug report about the change. All four dissolved. The half-rate frame counter was also provably incapable: with the landing at L, a half-frame fires at L+14910+d against the oracle's L0+14913 while the IRQ fires at L+29828+d' against L0+29828, so both hold only if d'=d-3 -- which is why the constants had been calibrated per step, exactly as the model's own comment suspected. **The sequencer now counts CPU cycles** with the documented positions (7457 / 14913 / 22371 / 29828 / 29829 / 29830), derived from the wiki's APU-cycle table plus its GET/PUT column ("3728, PUT" is CPU 2*3728+1) -- no rounding, no per-step fudge, and a 16-bit counter because the 5-step wrap is 37,282. **The six defects**: a `$4017` reset counted as a sequence WRAP (so every `$4017` write raised a frame IRQ the hardware never raises, forking blargg03 for 678,517 cycles from one bit); the reset landing one CPU cycle late (register latency, not the documented number -- one line took the battery 3 of 11 to 7 of 11); `$4015` not seeing a same-cycle length clock; a sequence with 29,831 states instead of 29,830, caught because the divergence RUNS lengthened 1, 2, 3 cycles -- a drift, not an offset; the length halt applying a cycle early; and the mode-1 immediate quarter+half firing at the WRITE rather than the reset landing, which had been calibrated against a reset that itself landed late -- two errors that cancelled. **Two of the six rules are absent from the nesdev wiki** and stated in blargg's own `readme.txt`, shipped beside the ROMs the whole time: the length-halt delay and the reload drop. **Three items open at the battery's close were then closed too**: the power-up `$4017` state (`apupower041`, the only ROM here that never writes `$4017`, so the state had been invisible by construction rather than by measurement -- the DUT's first frame IRQ rose two cycles late); and the length-reload drop (`apureload042`, whose `$4003` write is PLACED on CPU 29,827 because a walking stimulus provably cannot resolve one cycle inside a 7,457-cycle period), which exposed the drop as **dead code**: the promote block ran ~90 lines above the half-frame clock under a comment claiming it ran after it, so the clock overrode every reload. That ordering was accidentally correct, which is exactly why nothing caught it. **One item stays open by measurement rather than absence**: flipping the ORACLE's own power-on `apu_phase` leaves the battery at 11/11, so the strongest independent oracle available is provably indifferent to the divider phase -- blargg's readme says why (it is random on silicon and the ROMs tolerate both), and closing it needs hardware. **Oracle-side, the NTSC blargg suite had been asserting nothing since it was written**: it read `$6000`, which is unmapped on this vintage and returns `0` -- blargg's SUCCESS code -- so all eleven assertions were statements about an unmapped address. The identical defect was fixed for the PAL half in v2.1.5 and the NTSC half was never migrated. The obvious repair is also wrong (these ROMs report a numeric result code, not `PASSED`), so a third reader was needed. The same false oracle is why `tests/roms/extra/apu` was dismissed as audio-only: ten of its nineteen ROMs do report, and **four report TEST FAILED** -- surfaced and recorded, not chased, because they are oracle-side accuracy findings rather than rung-4 ones. **Zero emulation-core changes** -- only `rustynes-test-harness` and documentation -- so **AccuracyCoin 141/141 (RAM decoder) and nestest 0-diff hold by construction**, and both were run anyway. |
| **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. |
> **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 af50e8d8..ffc2d939 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.2"
+version = "2.6.3"
dependencies = [
"bitflags",
"libm",
@@ -107,7 +107,7 @@ dependencies = [
[[package]]
name = "rustynes-core"
-version = "2.6.2"
+version = "2.6.3"
dependencies = [
"bitflags",
"lz4_flex",
@@ -121,7 +121,7 @@ dependencies = [
[[package]]
name = "rustynes-cosim"
-version = "2.6.2"
+version = "2.6.3"
dependencies = [
"rustynes-core",
"sha2",
@@ -129,7 +129,7 @@ dependencies = [
[[package]]
name = "rustynes-cpu"
-version = "2.6.2"
+version = "2.6.3"
dependencies = [
"bitflags",
"thiserror",
@@ -137,7 +137,7 @@ dependencies = [
[[package]]
name = "rustynes-mappers"
-version = "2.6.2"
+version = "2.6.3"
dependencies = [
"bitflags",
"rustynes-apu",
@@ -146,7 +146,7 @@ dependencies = [
[[package]]
name = "rustynes-ppu"
-version = "2.6.2"
+version = "2.6.3"
dependencies = [
"bitflags",
"libm",
diff --git a/crates/rustynes-cosim/Cargo.toml b/crates/rustynes-cosim/Cargo.toml
index a8dbb79d..3f5c4304 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.2"
+version = "2.6.3"
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 7a5a23ea..6c7bafc5 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.2"
+display_version = "v2.6.3"
categories = "Emulator"
# Hardware Information
diff --git a/docs/STATUS.md b/docs/STATUS.md
index 1d37150a..bee115a1 100644
--- a/docs/STATUS.md
+++ b/docs/STATUS.md
@@ -1,6 +1,6 @@
# RustyNES — Project Status Matrix
-> **Current release: v2.6.2** (2026-08-24) — **"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"** (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.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
> 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.
@@ -33,11 +33,19 @@
> were already known; two were not — the DMC's DMA acknowledge (the sample
> pointer advanced by TWELVE per byte) and the frame-counter IRQ set points (the
> /IRQ line rose eleven master clocks early, so the CPU took the interrupt one
-> instruction sooner, which `blargg08` caught). The first AccuracyCoin run has
-> been attempted and
-> produced **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`.
+> instruction sooner, which `blargg08` caught).
+>
+> **AccuracyCoin now runs end to end on the DUT** — the full 17,868,316 cycles,
+> where it previously halted early — and the oracle gains `accuracycoin_status`,
+> 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
+> idles on its title screen until START is pressed, and only the framebuffer
+> half had a guard that refused. `sys/` is still empty; there is no `.rbf`.
> Detail: `docs/mister.md` and the sibling's `docs/rung3-ppu.md` and
> `docs/rung4-apu.md`.
>
diff --git a/docs/mister.md b/docs/mister.md
index 5bcfd9f0..80cbaf47 100644
--- a/docs/mister.md
+++ b/docs/mister.md
@@ -1091,6 +1091,44 @@ that was wrong (NROM's PRG-RAM window), and the sharpest category-1 entry yet:
the PPU's open-bus decay deadline, where the documentation says 3-30 ms, this
emulator uses 558.7 ms, and the DUT's corpus forces >= 523.4 ms.
+### AccuracyCoin runs end to end, and the gate is a status vector
+
+The full run now completes on the DUT — **17,868,316 cycles**, where it
+previously halted early — which is what "first end-to-end AccuracyCoin run"
+in the plan's v2.6.3 row asked for.
+
+The comparison is `accuracycoin_status`, on this side. It reads a work-RAM
+dump, decodes it against the 146-entry catalog in
+`accuracy_coin_catalog.rs`, prints one line per catalog entry, and given two
+dumps names every disagreement **by test rather than by address**. First
+measurement: **137 of 146 entries agree, 9 differ**, six of those sharing one
+failure code — five `SH`-group stores and Open Bus — which reads as one shared
+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.
+
+Two properties are worth recording, because both are about what the comparison
+*refuses* rather than what it reports:
+
+- **It is not a RAM byte-compare, deliberately.** Comparing 2 KiB of work RAM
+ answers a different question and answers it wrongly in both directions: it
+ reports scratch bytes — a stack slot, a loop counter, a result the ROM is
+ about to overwrite — as failures, and it reports two runs that never started
+ the battery as a *pass*, because two idle title screens have identical RAM.
+- **An all-`NotRun` vector is refused with a non-zero exit.** That case — two
+ vectors agreeing on 146 entries of nothing — is precisely the shape of the
+ vacuous status-address assertion v2.6.2 found in the NTSC blargg suite, which
+ reported 11/11 for five minor releases while asserting nothing. A comparison
+ that cannot distinguish *agreed* from *never ran* is not a gate.
+
+Reaching the run cost **two false passes before a real one**: AccuracyCoin idles
+on its title screen until START is pressed, and only the framebuffer half of the
+export had a guard that refused an idle capture. The manifest now records
+`press_start` — as `A:B`, or the literal `none` — because a controller press
+changes what the ROM *executes*, and a manifest omitting it describes an idle
+title screen and an 88-result battery identically.
+
## Rung 4 — the 2A03, and the audit it prompted
Rung 4 opened at v2.5.9 with the two pulse channels and the frame counter, and
diff --git a/to-dos/ROADMAP.md b/to-dos/ROADMAP.md
index 8a5dc6d7..ef9170ee 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.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.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"**, 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"**, 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`.
From 8ae404662871f614a29eedd224e831912c01f825 Mon Sep 17 00:00:00 2001
From: DoubleGate
Date: Tue, 25 Aug 2026 21:34:00 -0400
Subject: [PATCH 3/7] docs(mister): close v2.6.3's task rows against what was
actually measured
`to-dos/mister/TASKS.md` still carried v2.6.3 as `[~]` with its
AccuracyCoin row open, describing an attempt rather than a result. Both
are now settled and the tracker should say so, with the numbers rather
than the verdict.
The run row moves to `[x]`: AccuracyCoin completes the full 17,868,316
cycles, where it previously halted early. The two false passes that
preceded it are KEPT in the row rather than replaced by the success --
they are the reason the manifest gained `press_start`, and a tracker that
records only the outcome loses why the guard exists.
A second row is added for the status vector, because it is a distinct
deliverable from the run: 137 of 146 entries agree, 9 differ, six sharing
one failure code (five SH-group stores and Open Bus), which reads as one
shared address-bus cause rather than six independent defects. It also
records the tool's refusal of an all-NotRun vector, since that is the
property that makes the comparison a gate rather than a report.
The v2.6.3/v2.6.4 split -- producing the vector versus matching it --
was stated in the plan before the run rather than after it, and both rows
say so.
---
to-dos/mister/TASKS.md | 27 +++++++++++++++++++--------
1 file changed, 19 insertions(+), 8 deletions(-)
diff --git a/to-dos/mister/TASKS.md b/to-dos/mister/TASKS.md
index f69100f3..204af814 100644
--- a/to-dos/mister/TASKS.md
+++ b/to-dos/mister/TASKS.md
@@ -61,7 +61,7 @@ Legend: `[ ]` open · `[~]` in progress · `[x]` done
## v2.6.3 – v2.6.4 — rung 5, NROM + AccuracyCoin
-- [~] v2.6.3 NROM cartridge; first end-to-end AccuracyCoin run
+- [x] v2.6.3 NROM cartridge; the master-clock divider; first end-to-end AccuracyCoin run
- [x] `rtl/cart/cart_nrom.sv` — written from `nesdev_wiki/NROM.xhtml`
(public source: ),
instantiated beneath the existing gates and cross-checked per access
@@ -160,13 +160,24 @@ Legend: `[ ]` open · `[~]` in progress · `[x]` done
second-to-last-cycle recognition, correctly gated, so a second delay
would have cancelled an APU-side error. Looking for the cause after the
fix worked is what separated them
- - [ ] First end-to-end AccuracyCoin run — attempted, and it produced **two
- false passes before a real one**. The first reported RAM 2048/2048 and
- pixels 0/61440 identical: both true and worthless, because AccuracyCoin
- idles on its title screen until START is pressed. `fb_diff.py` REFUSED
- the framebuffer half ("2 distinct values, need 8"); the RAM half had no
- such guard. With START pressed, 208 of 2048 bytes change. Producing a
- status vector is v2.6.3; matching it is v2.6.4
+ - [x] First end-to-end AccuracyCoin run — **it completes**, the full
+ 17,868,316 cycles, where it previously halted early. Reaching it cost
+ **two false passes before a real one**: the first reported RAM 2048/2048
+ and pixels 0/61440 identical, both true and worthless, because
+ AccuracyCoin idles on its title screen until START is pressed.
+ `fb_diff.py` REFUSED the framebuffer half ("2 distinct values, need 8");
+ the RAM half had no such guard. With START pressed, 208 of 2048 bytes
+ change, and the golden manifest now records `press_start` so the two
+ runs can never again be described identically
+ - [x] The status vector exists and is comparable. `accuracycoin_status`
+ (oracle side) decodes a work-RAM dump against the 146-entry catalog and
+ 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
- [ ] v2.6.4 status vector identical **entry-for-entry**, including `Skipped` and
`NotRun` — **rung 5 closes**. State a floor, not a target
From 60ea9d8353616ce94ee282ae76234e127cbbadb5 Mon Sep 17 00:00:00 2001
From: DoubleGate
Date: Tue, 25 Aug 2026 21:40:11 -0400
Subject: [PATCH 4/7] test(harness): pin the anti-vacuity guard, demonstrated
to fail on six mutations
`accuracycoin_status` shipped with no tests. Its module docs call the
anti-vacuity guard "the point" and the release notes claim it exists, but
nothing asserted it -- so mutate `vacuous` to return false and the tool
reports two empty vectors as agreement, which is the exact failure mode
the binary was written to refuse. An untested guard is a claim.
This is the project's own standing rule from v2.4.0 -- extract the
decision so a test can reach it -- applied to the one decision this tool
is justified by.
## The tests, and what each is for
- `an_all_not_run_vector_is_vacuous` -- the guard itself.
- `one_real_result_is_enough_to_be_non_vacuous` -- the other half, and the
half an `any`-for-`all` swap needs. Without it that swap still passes
the first test, which is the two-assertions-need-two-mutations rule.
- `skipped_is_not_the_same_as_never_run` -- `Skipped` is a verdict the ROM
writes deliberately (`$FF`), not an absence. A vector of skips is a run
that HAPPENED and must not be refused. This is the test that catches the
plausible-looking widening to `NotRun | Skipped`, and nothing else does.
- `a_blank_work_ram_decodes_to_a_vacuous_vector` -- pins the guard to the
real input rather than a hand-built vector. All-zero work RAM is what an
idle title screen actually looks like on disk, and `$00` decoding to
`NotRun` is the link that makes the guard fire at all.
- `decoded_vectors_are_always_catalog_length` and `a_short_dump_is_refused`
-- see below.
- `describe_carries_the_code` -- the codes are what a reader acts on, so a
status must not render as a bare variant name that drops its code. Six
of the nine current disagreements are distinguished only by their code.
## A concern checked and found to be a non-defect
`main` zips three iterators to compare entry for entry, and `zip`
truncates silently to the shortest. If the two decoded vectors could ever
differ in length the comparison would cover a prefix while reporting
agreement across `a.len()` entries -- a comparison claiming more coverage
than it performed, precisely the class of defect this tool exists to
prevent.
It is unreachable: `decode_results` maps over `catalog()`, so its output
is always exactly `catalog().len()`, and a dump too short to hold the
vector is refused with `None` rather than decoded into a short one. Both
properties are now pinned by test, so a future change to `decode_results`
that returns a shorter vector fails loudly instead of turning the `zip`
into a silent truncation.
Recorded rather than "fixed": verifying the claim before writing the fix
is the rule, and this one did not survive verification.
## Mutation results
Six mutations, all CAUGHT, baseline passing first:
M1 all -> any CAUGHT one_real_result_...
M2 guard always false CAUGHT a_blank_work_ram_...
M3 NotRun renders as "Skipped" CAUGHT describe_carries_the_code
M4 Fail drops its code CAUGHT describe_carries_the_code
M5 vacuous widened to NotRun|Skipped CAUGHT skipped_is_not_the_same_...
M6 Unknown drops its byte CAUGHT describe_carries_the_code
A first pass reported M1, M2 and M5 as BUILD-FAILED. That was a defect in
the mutation harness, not in the mutants: the inline classifier's regex
misfired and every one of those three actually compiled and was caught.
BUILD-FAILED is not a data point, so a harness that emits it wrongly
manufactures three false absences of evidence -- the same shape as the
v2.4.4 harness bug that reported every mutation as a catch including the
baseline. Rewritten as a script that requires the baseline to PASS before
any verdict is trusted, matches `^error[E...]`/`could not compile` rather
than any line containing "error", and names the test that caught each one
so a verdict cannot be read without its cause.
---
.../src/bin/accuracycoin_status.rs | 98 +++++++++++++++++++
1 file changed, 98 insertions(+)
diff --git a/crates/rustynes-test-harness/src/bin/accuracycoin_status.rs b/crates/rustynes-test-harness/src/bin/accuracycoin_status.rs
index 01d9ebde..f45b27d7 100644
--- a/crates/rustynes-test-harness/src/bin/accuracycoin_status.rs
+++ b/crates/rustynes-test-harness/src/bin/accuracycoin_status.rs
@@ -171,3 +171,101 @@ fn main() -> ExitCode {
ExitCode::FAILURE
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::{describe, vacuous};
+ use rustynes_test_harness::accuracy_coin_catalog::{TestStatus, catalog, decode_results};
+
+ /// 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.
+ #[test]
+ fn an_all_not_run_vector_is_vacuous() {
+ let v = vec![TestStatus::NotRun; catalog().len()];
+ assert!(vacuous(&v), "a vector of only NotRun must be vacuous");
+ }
+
+ /// The other half, and the half a mutation to `all` would break silently:
+ /// **one** real result is enough to make a vector non-vacuous. Without this
+ /// an `any`-for-`all` swap still passes the test above.
+ #[test]
+ fn one_real_result_is_enough_to_be_non_vacuous() {
+ let mut v = vec![TestStatus::NotRun; catalog().len()];
+ v[0] = TestStatus::Pass;
+ assert!(!vacuous(&v), "a single Pass must defeat the vacuity guard");
+
+ let mut v = vec![TestStatus::NotRun; catalog().len()];
+ *v.last_mut().expect("catalog is non-empty") = TestStatus::Fail(7);
+ assert!(
+ !vacuous(&v),
+ "a single Fail must defeat the guard too -- a run that executed \
+ tests and failed them is a real run"
+ );
+ }
+
+ /// `Skipped` is a verdict the ROM writes deliberately (`$FF`), not an
+ /// absence. A vector of skips is a run that happened, so it must NOT be
+ /// refused as vacuous -- only `NotRun` means "never executed".
+ #[test]
+ fn skipped_is_not_the_same_as_never_run() {
+ let v = vec![TestStatus::Skipped; catalog().len()];
+ assert!(
+ !vacuous(&v),
+ "Skipped is a result the ROM wrote; only NotRun is an absence"
+ );
+ }
+
+ /// An all-zero work RAM is what a run that never left the title screen
+ /// actually looks like on disk, and it must decode to a vacuous vector.
+ /// This pins the guard to the real input rather than to a hand-built
+ /// vector -- `$00` is `NotRun`, and that link is what makes the guard fire.
+ #[test]
+ fn a_blank_work_ram_decodes_to_a_vacuous_vector() {
+ let ram = vec![0u8; 2048];
+ let v = decode_results(&ram).expect("2 KiB is long enough for the catalog");
+ assert_eq!(v.len(), catalog().len());
+ assert!(
+ vacuous(&v),
+ "blank work RAM is an idle run, not a passing one"
+ );
+ }
+
+ /// Both decoded vectors are `catalog().len()` by construction, because
+ /// `decode_results` maps over the catalog. The comparison in `main` zips
+ /// three iterators and `zip` truncates silently, so that equal-length
+ /// property is what keeps it from reporting agreement over a prefix while
+ /// claiming the full count. Pinned here so a future change to
+ /// `decode_results` that returns a shorter vector fails loudly.
+ #[test]
+ fn decoded_vectors_are_always_catalog_length() {
+ for len in [2048usize, 4096] {
+ let ram = vec![0u8; len];
+ let v = decode_results(&ram).expect("long enough");
+ assert_eq!(
+ v.len(),
+ catalog().len(),
+ "decode_results must return one entry per catalog entry"
+ );
+ }
+ }
+
+ /// A short dump is refused rather than decoded into a short vector, which
+ /// is what would make the `zip` above truncate.
+ #[test]
+ fn a_short_dump_is_refused() {
+ assert!(decode_results(&[0u8; 8]).is_none());
+ }
+
+ /// The codes are what a reader acts on, so a status must not render as a
+ /// bare variant name that drops its code.
+ #[test]
+ fn describe_carries_the_code() {
+ assert_eq!(describe(TestStatus::Pass), "Pass");
+ assert_eq!(describe(TestStatus::NotRun), "NotRun");
+ assert_eq!(describe(TestStatus::Skipped), "Skipped");
+ assert_eq!(describe(TestStatus::PassWithCode(1)), "Pass(code 1)");
+ assert_eq!(describe(TestStatus::Fail(7)), "Fail(code 7)");
+ assert_eq!(describe(TestStatus::Unknown(0xAB)), "Unknown($AB)");
+ }
+}
From ce69f9d86aa3a9aa1488deeff0494c59de024d44 Mon Sep 17 00:00:00 2001
From: DoubleGate
Date: Tue, 25 Aug 2026 21:42:02 -0400
Subject: [PATCH 5/7] refactor(harness): borrow paths as &Path in
accuracycoin_status
`read_ram` and the `decode` closure took `&PathBuf` and used only
`fs::read` and `Display`, both available through the `Deref` target. `&Path`
is the borrowed form and avoids the double indirection. Call sites are
unchanged -- `&args[0]` coerces.
Raised by the Antigravity reviewer. Adopted because it is a pure signature
change with no behaviour implication; the seven guard tests and clippy are
green after it.
Worth noting that `clippy::ptr_arg` did NOT fire on this, despite being an
on-by-default style lint whose whole subject is exactly this shape. The
gate passing is not evidence a borrow is idiomatic.
---
crates/rustynes-test-harness/src/bin/accuracycoin_status.rs | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/crates/rustynes-test-harness/src/bin/accuracycoin_status.rs b/crates/rustynes-test-harness/src/bin/accuracycoin_status.rs
index f45b27d7..4cfbfbf9 100644
--- a/crates/rustynes-test-harness/src/bin/accuracycoin_status.rs
+++ b/crates/rustynes-test-harness/src/bin/accuracycoin_status.rs
@@ -31,7 +31,7 @@
//! executed nothing is not a passing run, and it must not be possible to
//! mistake one for the other.
-use std::path::PathBuf;
+use std::path::{Path, PathBuf};
use std::process::ExitCode;
use rustynes_test_harness::accuracy_coin_catalog::{
@@ -47,7 +47,7 @@ fn usage() -> ! {
std::process::exit(2)
}
-fn read_ram(p: &PathBuf) -> Vec {
+fn read_ram(p: &Path) -> Vec {
std::fs::read(p).unwrap_or_else(|e| {
eprintln!("read {}: {e}", p.display());
std::process::exit(2)
@@ -77,7 +77,7 @@ fn main() -> ExitCode {
usage();
}
- let decode = |p: &PathBuf| -> Vec {
+ let decode = |p: &Path| -> Vec {
let ram = read_ram(p);
decode_results(&ram).unwrap_or_else(|| {
eprintln!(
From 791abb43f0186977c1d378e0c9aa0d16ff6f55a7 Mon Sep 17 00:00:00 2001
From: DoubleGate
Date: Tue, 25 Aug 2026 21:58:58 -0400
Subject: [PATCH 6/7] fix(changelog): restore the [Unreleased] heading, and
gate it where the cut happens
The v2.6.3 cut RENAMED `## [Unreleased]` into `## [2.6.3] - ...` instead
of inserting the new section BELOW a retained `[Unreleased]`, so the file
went out with no `[Unreleased]` heading at all. Every prior tag has one --
`v2.6.0`, `v2.6.1` and `v2.6.2` each carry an empty `## [Unreleased]`
immediately above the newest release -- and the Keep a Changelog
convention this file declares requires it.
## How it surfaced, and why that is the real finding
It turned `main`'s test matrix red on BOTH legs -- `ubuntu-24.04-arm` and
`ubuntu-latest` -- through three failing tests in
`crates/rustynes-frontend/src/debugger/doc_panel.rs`:
changelog_splits_into_releases
"an [Unreleased] section should be present"
changelog_display_order_puts_unreleased_last
"[Unreleased] must be displayed last, got \"[0.1.0] ...\""
changelog_display_order_is_cached_and_stable
"[Unreleased] must sort last, got \"[0.1.0] ...\""
The in-app documentation panel parses `CHANGELOG.md` at runtime, so a
CHANGELOG defect surfaces as a FRONTEND unit-test failure. That is a long
way from the edit, and it is the wrong place to learn it: the failure text
names a frontend module, the version bump is nine commits and five
documents earlier, and nothing in between mentions the CHANGELOG.
Worse, those tests do not run on every PR path -- they are on the
full-workspace matrix legs. Had the paths filter scheduled differently
this would have reached `main` and turned it red there, which is the exact
shape recorded in AGENTS.md for the v2.3.4 vector move: a defect landing
on `main` rather than on the PR that caused it.
## The gate moves to where the mistake is made
`release_anchor_audit` already reads and parses `CHANGELOG.md` for the
header shape, so the assertion belongs beside the other claims a cut must
satisfy -- naming the CHANGELOG by name at the moment the version is
bumped, in the same test binary the ceremony already runs.
`the_changelog_keeps_an_unreleased_section` asserts three things, and the
second and third are the ones a naive existence check would miss:
- the heading EXISTS;
- there is exactly ONE, because a cut that leaves its old heading behind
produces two and a reader cannot tell which is live;
- it comes FIRST, because a heading that has drifted below a released
section still satisfies existence while telling a reader the opposite of
the truth about where new entries go.
It deliberately does NOT assert the section is empty. Carrying an entry
destined for the next release is legitimate, and an emptiness check would
fail a tree that is merely ahead.
## Demonstrated to fail
Three mutations of `CHANGELOG.md`, baseline passing before and after:
C1 heading removed (the actual defect) CAUGHT
C2 heading duplicated CAUGHT
C3 heading demoted below the newest release CAUGHT
`cargo test -p rustynes-frontend --lib doc_panel` is green again: 10
passed, 0 failed.
---
CHANGELOG.md | 2 +
.../tests/release_anchor_audit.rs | 63 +++++++++++++++++++
2 files changed, 65 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e5bd13eb..5c588448 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,6 +24,8 @@ the documentary lineage of how that core was built (not standalone user
releases), and `v0.1.0`–`v0.8.6` are the original pre-1.0 engine that the
cycle-accurate core later replaced.
+## [Unreleased]
+
## [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/crates/rustynes-test-harness/tests/release_anchor_audit.rs b/crates/rustynes-test-harness/tests/release_anchor_audit.rs
index cd59adc1..be40424b 100644
--- a/crates/rustynes-test-harness/tests/release_anchor_audit.rs
+++ b/crates/rustynes-test-harness/tests/release_anchor_audit.rs
@@ -483,6 +483,69 @@ fn every_release_anchor_names_the_workspace_version() {
);
}
+/// The CHANGELOG must retain an `[Unreleased]` section after a release cut.
+///
+/// **Why this exists.** Cutting v2.6.3 renamed `## [Unreleased]` into
+/// `## [2.6.3] - ...` instead of inserting the new section *below* a retained
+/// `[Unreleased]`, so the file shipped with no `[Unreleased]` heading at all.
+/// Every prior tag has one — `v2.6.0`, `v2.6.1` and `v2.6.2` each carry an empty
+/// `## [Unreleased]` immediately above the newest release — and the Keep a
+/// Changelog convention the file declares requires it.
+///
+/// It was caught, but three crates away and by accident: `rustynes-frontend`'s
+/// in-app documentation panel parses this file, and three of its tests assert an
+/// `[Unreleased]` section exists and sorts last. So a CHANGELOG defect surfaced
+/// as a frontend unit-test failure on the full-workspace matrix leg, which is
+/// both a long way from the edit and a gate that does not run on every PR.
+///
+/// The release audit already parses this file for the header shape, so the
+/// assertion belongs here, next to the other claims the cut has to satisfy —
+/// where it names the CHANGELOG by name at the moment the version is bumped.
+///
+/// Deliberately checks only that the heading EXISTS, not that it is empty:
+/// carrying an entry destined for the next release is legitimate, and asserting
+/// emptiness would fail a tree that is merely ahead.
+#[test]
+fn the_changelog_keeps_an_unreleased_section() {
+ let changelog = read("CHANGELOG.md");
+ let count = changelog
+ .lines()
+ .filter(|l| l.trim_end() == "## [Unreleased]")
+ .count();
+
+ assert!(
+ count > 0,
+ "CHANGELOG.md has no `## [Unreleased]` heading.\n\n\
+ A release cut inserts the new version section BELOW a retained\n\
+ `## [Unreleased]`; it does not rename that heading into the new\n\
+ version. Every tag from v2.6.0 onward carries an empty one.\n\n\
+ Without it, `rustynes-frontend`'s documentation panel fails three\n\
+ tests -- but only on the full-workspace matrix leg, so this is the\n\
+ cheaper place to find out."
+ );
+
+ assert_eq!(
+ count, 1,
+ "CHANGELOG.md has {count} `## [Unreleased]` headings; exactly one is \
+ expected. More than one means a previous cut left its heading behind."
+ );
+
+ // `[Unreleased]` must come FIRST. A heading that has drifted below a
+ // released section still satisfies the existence check above while telling
+ // a reader the opposite of the truth about where new entries go.
+ let first_section = changelog
+ .lines()
+ .find(|l| l.starts_with("## ["))
+ .expect("CHANGELOG.md has no `## [` section heading at all");
+ assert_eq!(
+ first_section.trim_end(),
+ "## [Unreleased]",
+ "the first `## [` section in CHANGELOG.md is {first_section:?}, but \
+ `## [Unreleased]` must lead the file so new entries have an unambiguous \
+ home."
+ );
+}
+
/// The CHANGELOG must carry a section for the version the workspace claims.
#[test]
fn the_changelog_has_a_section_for_the_workspace_version() {
From 07e61011c12ede7567eea7b9395e86dc2a2e57e2 Mon Sep 17 00:00:00 2001
From: DoubleGate
Date: Tue, 25 Aug 2026 22:52:37 -0400
Subject: [PATCH 7/7] docs: accuracycoin_status filters its output; it does not
print every entry
Three documents said the tool "prints one line per catalog entry". It does
not, in either mode, and the claim was mine in all three places.
`main` filters what it PRINTS:
single-file .filter(|(_, s)| !matches!(s, TestStatus::Pass))
two-file .filter(|(_, (x, y))| x != y)
So one dump lists the entries that are not a clean `Pass`, and two dumps
list only the entries that disagree. On the measured AccuracyCoin run that
is 9 lines out of 146, not 146.
The distinction matters because the two halves are easy to conflate and
only one of them is filtered. The full 146-entry vector IS decoded and IS
compared in both modes -- the filtering is on the OUTPUT. A reader who
took the docs literally and saw 9 lines would reasonably conclude the
comparison had covered 9 entries, which is the opposite of the property
the tool exists to provide, and precisely the "claiming coverage it did
not perform" failure the vacuity guard was written against.
Corrected in `docs/mister.md`, `.github/release-notes/v2.6.3.md` and
`CHANGELOG.md`, each now saying what is filtered and stating explicitly
that the comparison is not.
Raised by CodeRabbit, and verified against the source before writing the
fix rather than adopted on the reviewer's word -- the two `.filter` calls
above are that verification. Its only actionable finding on this PR, and
it was right.
---
.github/release-notes/v2.6.3.md | 2 +-
CHANGELOG.md | 7 ++++---
docs/mister.md | 7 +++++--
3 files changed, 10 insertions(+), 6 deletions(-)
diff --git a/.github/release-notes/v2.6.3.md b/.github/release-notes/v2.6.3.md
index 8bedc7d5..66d6c929 100644
--- a/.github/release-notes/v2.6.3.md
+++ b/.github/release-notes/v2.6.3.md
@@ -49,7 +49,7 @@ The binding constraint is one measurable property of one ROM — `10-branches` h
The full AccuracyCoin run now completes on the DUT — 17,868,316 cycles, where it previously halted early — and the oracle gains `accuracycoin_status`.
-Rung 5's stated acceptance is a status vector comparable **entry for entry**, including `Skipped` and `NotRun`. The tool reads a work-RAM dump, decodes it against the 146-entry catalog, prints one line per entry, and given two dumps names every disagreement **by test rather than by address**.
+Rung 5's stated acceptance is a status vector comparable **entry for entry**, including `Skipped` and `NotRun`. The tool reads a work-RAM dump, decodes it against the 146-entry catalog, and reports **by test rather than by address**. It filters what it prints: one dump lists the entries that are not a clean `Pass`, two dumps list only the entries that disagree. The full vector is decoded and compared either way — the filtering is on the output, not the comparison.
First measurement: **137 of 146 entries agree, 9 differ** — six of those sharing one failure code (five `SH`-group stores and Open Bus), a pattern that suggests one shared address-bus cause rather than six independent defects. A pass count of 137 would have hidden that pattern.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5c588448..5dabec4e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -34,9 +34,10 @@ cycle-accurate core later replaced.
Rung 5's stated acceptance is a status vector comparable **entry for
entry** — including `Skipped` and `NotRun` — between the oracle and the
co-simulation DUT. `accuracycoin_status` is the oracle half: it reads a
- work-RAM dump, decodes it against the 146-entry catalog, prints one line
- per entry, and given two dumps names every disagreement **by test rather
- than by address**.
+ work-RAM dump, decodes it against the 146-entry catalog, and reports **by
+ test rather than by address** — printing the entries that are not a clean
+ `Pass` given one dump, and only the entries that disagree given two. The
+ full vector is compared either way; the filtering is on the output.
Producing one is this release's deliverable; making the two agree is
v2.6.4. The first end-to-end DUT run reports **137 of 146 entries
diff --git a/docs/mister.md b/docs/mister.md
index 80cbaf47..b1ea8f9e 100644
--- a/docs/mister.md
+++ b/docs/mister.md
@@ -1099,8 +1099,11 @@ in the plan's v2.6.3 row asked for.
The comparison is `accuracycoin_status`, on this side. It reads a work-RAM
dump, decodes it against the 146-entry catalog in
-`accuracy_coin_catalog.rs`, prints one line per catalog entry, and given two
-dumps names every disagreement **by test rather than by address**. First
+`accuracy_coin_catalog.rs`, and reports by test rather than by address. It
+filters in both modes rather than dumping all 146 rows: given one dump it
+prints the entries that are **not a clean `Pass`**, and given two it prints only
+the entries where they **disagree**. The vector is decoded in full either way --
+what is filtered is the output, not the comparison. First
measurement: **137 of 146 entries agree, 9 differ**, six of those sharing one
failure code — five `SH`-group stores and Open Bus — which reads as one shared
address-bus cause rather than six independent defects.