Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .github/release-notes/v2.9.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# RustyNES v2.9.1 — "Hone"

The second release of the v2.9.x line that [ADR 0041](https://github.com/doublegate/RustyNES/blob/main/docs/adr/0041-hardware-release-is-v3.0.0.md) put before the SuperStation One core (**v3.0.0**). It is the optimisation release, and it opens with the tool that judges optimisations: `scripts/perf/ab_check.sh` had been timing the old code on both sides of every code comparison. That is fixed and proven, and every earlier rejection that could be rebuilt was measured again. Emulation output does not change.

**No hardware has run any bitstream**; that is v2.9.2's subject.

## The A/B tool compared the old code with itself

`ab_check.sh` built the reference (a worktree of the base commit) and the candidate (the working tree) into one cargo target directory. Cargo names a workspace member's build by its path relative to the workspace root, so the two trees shared artifact names, and cargo decides freshness by file time. The candidate step therefore ran the reference's binary. The proof: given a candidate that deliberately did its work twice, the old script reported "no change" (46.0 µs on both sides); the fixed one reports **+146%**. Each side now builds into a fresh directory of its own on every run.

A comparison of old code with itself can only ever say "no change", so the verdicts at risk were the rejections. Twelve were rebuilt and measured again, two runs each on a quiet host:

- **Three were wrong.** Dropping the per-dot call to the dead NMI edge detector is 4.1% to 4.7% faster on palette-heavy frames. It is removed at v3.0.0 together with the detector and its save-state fields ([ADR 0042](https://github.com/doublegate/RustyNES/blob/main/docs/adr/0042-v3-removes-the-v2-7-5-deprecations-and-the-dead-nmi-edge-detector.md), maintainer decision), because removing it now would change a deprecated method and two save-state fields in a minor release. Two "the ceiling is zero" results were not zero either, so correct candidates were built under both: a per-mapper capability that lets the bus skip the unmapped-read check for program reads (with a test over every board), and two branch-free palette mirrors. All three are rejected. Each gains at most about 1% on the palette-heavy workload and makes the default path slower in both runs, and a result that helps one workload while hurting another is a rejection, not an average.
- **The rest stay rejected, now on evidence**, several of them measurably slower.

The full table, and the seven older rejections that survive only as prose and so stay unverified, are in [`docs/performance.md`](https://github.com/doublegate/RustyNES/blob/main/docs/performance.md).

## A two-screen Vs. cabinet saves about 9x faster

RetroArch saves the whole machine every frame for rewind and run-ahead. For the four two-screen Vs. System cabinets that meant two full console snapshots with thumbnails, into fresh buffers. `VsDualSystem::snapshot_into` writes both into a buffer RetroArch's call reuses: **−88.9% and −88.5%** on two runs. A pooled buffer for the matching restore measured no change and was not kept.

## The MiSTer core

- **The off-die build keeps CHR and PRG in different SDRAM banks.** A program read used to close the row a pattern fetch had open. Over a rendering workload the memory's row closes fell from 132,235 to 24,880, and the worst pattern fetch from 20 to 18 cycles of its 22. The worst program read stayed at 22 of 24: a refresh closes every bank, so the access after one misses wherever the data lives. A scheduled arbiter that also places refresh waits until after v3.0.0, since nothing before then adds memory traffic.
- **The co-simulation ladder runs every gate in one pass**, the 16-ROM instruction battery included; v2.9.0 ran that battery by hand.
- **The timing check reads the SDRAM clock from the constraints** and checks that the console clock is exactly four SDRAM clocks.
- **Fitter seed 2**, from eight seeds per build, all sixteen closing timing. It has the most on-die margin on both measures. Two clean compiles of each build are byte-identical.

## Verification

| Check | Result |
| --- | --- |
| `cargo test --workspace --features test-roms` | 2,812 passed, 0 failed, 20 ignored |
| AccuracyCoin / nestest | 144/144 / 0-diff |
| fmt, clippy (every feature set, wasm), rustdoc, no_std build | clean |
| Co-simulation, on-die | 171 passed, 0 failed, 1 expected failure (one clean-checkout run) |
| Co-simulation, off-die (`USE_SDRAM=1`) | 172 passed, 0 failed, 1 expected failure (one clean-checkout run) |
| Quartus 17.0.2, seed 2, on-die | setup +0.542 ns, hold +0.115 ns; 22,585 ALMs, 468 RAM blocks, 33 DSP; two clean compiles byte-identical |
| Quartus 17.0.2, seed 2, off-die | setup +0.193 ns, hold +0.080 ns (SDRAM read +0.447 / +1.184 ns); 22,569 ALMs, 84 RAM blocks, 33 DSP; two clean compiles byte-identical |

**Next:** v2.9.2, the release-candidate bitstream pair and the board session on the SuperStation One.

## Install

- Download the pre-built binaries for Linux, macOS, and Windows below.
- The MiSTer core bitstreams are attached below: `RustyNES_MiSTer-v2.9.1.rbf` (on-die) and `RustyNES_MiSTer-v2.9.1-offdie.rbf` (cartridge in SDRAM; its name is provisional until v3.0.0). Neither has run on hardware.
- The WebAssembly build is live at [doublegate.github.io/RustyNES](https://doublegate.github.io/RustyNES/).
- The RetroArch core is in RetroArch's Online Updater on the platforms the libretro buildbot publishes to.
- Licensed under GPL-3.0-or-later.
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Enforcement lives alongside the prose: `/ref-proj/` is gitignored/`.dockerignore

RustyNES is a cycle-accurate Nintendo Entertainment System emulator written in pure Rust. The accuracy bar is Mesen2 / higan / ares: tight lockstep scheduling at PPU-dot resolution on a master-clock-precise timebase, sub-instruction PPU events visible to subsequent CPU code, and a lookup-table non-linear audio mixer with band-limited synthesis. The frontend is pure Rust (`winit` + `wgpu` + `cpal` + `egui`).

**Current release: v2.9.0 "Survey"** (2026-09-26) — every audit re-checked, and the SuperStation One surveyed: a Power Cycle no longer erases your save, the off-die MiSTer build boots without the menu core, and 39 new audit findings are fixed or dispositioned. Built on **v2.8.4 "Tether"** (2026-09-26) — the MiSTer core's SDRAM build, made trustworthy: its controller now reads data on the edge the memory presents it (every off-die read would have been wrong on hardware, and only the new SDRAM timing constraints could see it), the power-up sequence and CAS-latency-3 reads follow the datasheet, the arbiter can no longer return the wrong byte or lose a write, the off-die bitstream builds from a script, both builds are swept and pinned at fitter seed 5, and the co-simulation ladder runs all 165 gates from a clean checkout. Built on **v2.8.3 "Rivet"** (2026-09-25) — the MiSTer core's reset, area and comments, measured: every reset is released on the clock that uses it and the timing analysis now checks each release, the CPU is about 4% smaller by two exact rewrites the fit report confirmed, four false comments are corrected, and the co-simulation ladder runs from a fresh checkout (164 of its 165 gates; the last needs a hand-built ROM no generator produces). v2.9.0 re-ran all four audits: 139 ledger rows re-verified with none regressed, and 39 new findings each fixed red-first or dispositioned (`docs/audits/v2.9.0-*-reaudit.md` and the four ledgers). It changes emulation behaviour in two deliberate places: a Power Cycle keeps battery-backed RAM (it used to clear it, and the v2.7.3 writer then persisted the zeros), and power-on movies and a new TAStudio session start from cleared cartridge RAM (`rustynes_core::power_on_for_movie`). The libretro core is built with `panic = "unwind"`, exercised by a C-ABI harness (`crates/rustynes-libretro/src/abi_tests.rs`), and patches a vendored `rust-libretro-sys` (`vendor/`). **AccuracyCoin 144/144 and nestest 0-diff** hold on the v2.9.0 tree, and the full `--features test-roms` suite passes 2,811 tests. v2.7.4's mobile changes have a device checklist (`docs/mobile-v2.7.4-device-checklist.md`), scheduled as v2.9.3. The co-simulation is **171 passed, 0 failed, 1 expected failure** on-die and **172 / 0 / 1** off-die (`USE_SDRAM=1`), each a clean-checkout ladder plus the 16-gate instruction battery run separately (the ladder's `ORACLE` pointed at `.oracle-pinned`, which lacks those ROMs); both bitstreams compile at fitter seed 5 (on-die +0.122 ns setup / +0.080 ns hold, off-die +0.141 / +0.119; the SDRAM read hold is +1.182 ns, not the +12.822 v2.8.4 recorded; the SDRAM pin constraints stay provisional until the SuperStation One's memory is read). **No hardware has run any bitstream**. The board session is v2.9.2 and the hardware-verified core is **v3.0.0** ([ADR 0041](docs/adr/0041-hardware-release-is-v3.0.0.md)); v2.7.x and v2.8.x act on the four audits in `docs/audits/` first. Per-release detail lives in `CHANGELOG.md` and the GitHub releases; it is deliberately not duplicated here.
**Current release: v2.9.1 "Hone"** (2026-09-27) — what the optimisation bars measure, and what clears them: the A/B tool had been timing the old code on both sides of every code comparison and is fixed, a two-screen Vs. cabinet saves about 9x faster, the off-die MiSTer build keeps CHR in its own SDRAM bank, and both bitstreams are pinned at fitter seed 2 and rebuild byte-identically. Built on **v2.9.0 "Survey"** (2026-09-26) — every audit re-checked, and the SuperStation One surveyed: a Power Cycle no longer erases your save, the off-die MiSTer build boots without the menu core, and 39 new audit findings are fixed or dispositioned. Built on **v2.8.4 "Tether"** (2026-09-26) — the MiSTer core's SDRAM build, made trustworthy: its controller now reads data on the edge the memory presents it (every off-die read would have been wrong on hardware, and only the new SDRAM timing constraints could see it), the power-up sequence and CAS-latency-3 reads follow the datasheet, the arbiter can no longer return the wrong byte or lose a write, the off-die bitstream builds from a script, both builds are swept and pinned at fitter seed 5, and the co-simulation ladder runs all 165 gates from a clean checkout. Built on **v2.8.3 "Rivet"** (2026-09-25) — the MiSTer core's reset, area and comments, measured: every reset is released on the clock that uses it and the timing analysis now checks each release, the CPU is about 4% smaller by two exact rewrites the fit report confirmed, four false comments are corrected, and the co-simulation ladder runs from a fresh checkout (164 of its 165 gates; the last needs a hand-built ROM no generator produces). v2.9.1 fixed `scripts/perf/ab_check.sh`, which had built both sides into one target directory and so ran the reference's binary as the candidate, and re-measured every rebuildable earlier rejection: three verdicts were wrong, the dead NMI detector's per-dot call among them (−4.1% to −4.7% on palette frames, removed at v3.0.0 with ADR 0042), and no candidate cleared the rule except NL-09's dual-cabinet serialize (−88.9%). v2.9.0 re-ran all four audits: 139 ledger rows re-verified with none regressed, and 39 new findings each fixed red-first or dispositioned (`docs/audits/v2.9.0-*-reaudit.md` and the four ledgers). It changes emulation behaviour in two deliberate places: a Power Cycle keeps battery-backed RAM (it used to clear it, and the v2.7.3 writer then persisted the zeros), and power-on movies and a new TAStudio session start from cleared cartridge RAM (`rustynes_core::power_on_for_movie`). The libretro core is built with `panic = "unwind"`, exercised by a C-ABI harness (`crates/rustynes-libretro/src/abi_tests.rs`), and patches a vendored `rust-libretro-sys` (`vendor/`). **AccuracyCoin 144/144 and nestest 0-diff** hold on the v2.9.1 tree, and the full `--features test-roms` suite passes 2,812 tests. v2.7.4's mobile changes have a device checklist (`docs/mobile-v2.7.4-device-checklist.md`), scheduled as v2.9.3. The co-simulation is **171 passed, 0 failed, 1 expected failure** on-die and **172 / 0 / 1** off-die (`USE_SDRAM=1`), each ONE clean-checkout ladder run with the 16-gate instruction battery inside it (v2.9.0 ran the battery separately); both bitstreams compile at fitter seed 2, chosen from eight per build (on-die +0.542 ns setup / +0.115 ns hold, off-die +0.193 / +0.080; the SDRAM read is +0.447 setup / +1.184 hold), and two clean compiles of each are byte-identical. The SDRAM pin constraints stay provisional until the SuperStation One's memory is read. **No hardware has run any bitstream**. The board session is v2.9.2 and the hardware-verified core is **v3.0.0** ([ADR 0041](docs/adr/0041-hardware-release-is-v3.0.0.md)); v2.7.x and v2.8.x act on the four audits in `docs/audits/` first. Per-release detail lives in `CHANGELOG.md` and the GitHub releases; it is deliberately not duplicated here.

- **Timebase (v2.0.0)** — the scheduler substrate is rewritten from a five-counter dot-lockstep model to a single canonical cycle counter, every CPU cycle clocked in two halves (`start_cycle` / `end_cycle`) with any bus access split between them, and the PPU caught up to each half (ADR 0002 / ADR 0029), now the *only* scheduler path. This is a MAJOR-boundary breaking change (ADR 0003): `.rns` save-state and `.rnm` movie format epochs bump (ADR 0028) — a pre-v2.0.0 `.rns` slot now fails to load with a clear error instead of silently misinterpreting stale bytes. Landed across five betas + rc.1 (PRs #217–223). Also new: core-level **Vs. `DualSystem`** dual-console support (`Emu::Dual`, `crates/rustynes-core`) for the four Vs. arcade cabinet boards — core-and-test-harness-only, frontend wiring deferred. The R1/R2 MMC3 IRQ-timing residual is by-design-deferred beyond this release with a mechanism-level finding recorded in ADR 0002 (not closed, not silently dropped). **AccuracyCoin now measures 141/141 (100.00%)**: the v2.0.1 upstream AccuracyCoin re-sync grew the catalog to 146 rows / 141 assigned tests and briefly opened two new PPU gaps ("ALE + Read" $0491, "Hybrid Addresses" $0492), which **v2.0.3** closed by promoting the 2-cycle-ALE PPU fetch model to the unconditional default (both experimental flags retired; additive `PPU_SNAPSHOT_VERSION` v5 tail). AccuracyCoin held 100% (139/139) throughout the v2.0.0 betas and final cut, dipped to 139/141 under the v2.0.1 re-sync, and is back to a full 141/141 from v2.0.3 onward.

Expand Down Expand Up @@ -210,7 +210,7 @@ what a session needs before it knows what it is doing.
| reading a result — what it does and does not prove | [`docs/agents/measurement-discipline.md`](docs/agents/measurement-discipline.md) | 18 |
| the shell, `pre-commit`, `gh`, `/tmp`, long-running jobs | [`docs/agents/tooling-traps.md`](docs/agents/tooling-traps.md) | 13 |
| a dependency bump, or why one is blocked | [`docs/agents/dependencies.md`](docs/agents/dependencies.md) | 2 |
| a performance claim, or a debugger panel that outlives its `Nes` | [`docs/agents/perf-and-panels.md`](docs/agents/perf-and-panels.md) | 6 |
| a performance claim, or a debugger panel that outlives its `Nes` | [`docs/agents/perf-and-panels.md`](docs/agents/perf-and-panels.md) | 7 |

**Read the file, not a summary of it.** Each bullet carries its own evidence —
the command that was run, the number it returned, the mutation that caught it —
Expand All @@ -225,7 +225,7 @@ that is a reason to add a tenth — not a reason to grow this section back.
- `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.9.0 "Survey"** (2026-09-26). **Never claim any version *later* than v2.9.0 is released.** Two distinct "v2.0"s exist and must not be conflated: the **engine-lineage v2.0** master-clock work shipped as the **v1.0.0** production core (2026-06-13) and was the only scheduler through v1.10.0; RustyNES's own **v2.0.0 "Timebase"** (2026-07-03) is a different milestone that REPLACES that dot-lockstep scheduler with the one-clock, every-cycle-bus-access model (ADR 0002 / 0028 / 0029) and is the one release that broke byte-identity and save-state compatibility, by design. The per-release narrative that used to be inlined here is in `CHANGELOG.md`, the per-release notes under `.github/release-notes/`, and the published GitHub releases — three places that are maintained, against one copy here that was not.
- 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.9.1 "Hone"** (2026-09-27). **Never claim any version *later* than v2.9.1 is released.** Two distinct "v2.0"s exist and must not be conflated: the **engine-lineage v2.0** master-clock work shipped as the **v1.0.0** production core (2026-06-13) and was the only scheduler through v1.10.0; RustyNES's own **v2.0.0 "Timebase"** (2026-07-03) is a different milestone that REPLACES that dot-lockstep scheduler with the one-clock, every-cycle-bus-access model (ADR 0002 / 0028 / 0029) and is the one release that broke byte-identity and save-state compatibility, by design. The per-release narrative that used to be inlined here is in `CHANGELOG.md`, the per-release notes under `.github/release-notes/`, and the published GitHub releases — three places that are maintained, against one copy here that was not.
- **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/`<img>`, 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.
Expand Down
Loading
Loading