Skip to content

Commit 5b66686

Browse files
doublegateclaude
andcommitted
chore(release): cut v2.8.2 "Solder"
Workspace 2.8.1 -> 2.8.2 (root and the excluded rustynes-cosim), the [2.8.2] CHANGELOG section, the release notes override, and every release anchor moved by bump_release.py with the lead describing THIS release. Hand edits the tool leaves, as it says it will: - ROADMAP.md's status line (its CHAIN shape has no "Built on" to extend, so the tool refuses rather than guessing); - the VERSION-PLAN row, with "(current)" moved; - the to-dos/ROADMAP.md release-line chain, whose tail and "the current release" label are one claim (release_anchor_audit failed on both until they moved together); - the prose around the anchors in AGENTS.md and README.md, which still described v2.8.1 ("changes no emulation behaviour", 2,754 tests, the +0.421/+0.112 ns timing). Nested "Built on" chains were trimmed to one level in the eight files the bump deepened, then RESTORED in OVERVIEW.md and docs/STATUS.md, whose chains continue to v2.7.6: removing v2.8.0 there would have made a chain skip a release. Checked by grepping for "CARGO_TARGET_DIR. Built on **v2.7.6" after the trim. A markdownlint trap, recorded in docs/agents/tooling-traps.md: the lead first said "a $2002 read", bare, and docs/STATUS.md then failed MD038 two lines down at an unchanged code span. The pinned markdownlint reads $...$ as math, so the bare address paired with a later $ and swallowed a backtick. Proved by substitution (XX2002 passes) and by linting the HEAD copy (passes). The address is now a code span in all eight anchors. Figures, measured on this tree: --features test-roms 2,756 passed across 155 suites, 0 failed (v2.8.1's 2,754 plus the two MMC1 tests); AccuracyCoin 144/144 (RAM decoder, 0 fail, 0 not run); nestest 0-diff; fmt, clippy -D warnings for the workspace, scripting+hd-pack, retroachievements, full and both wasm32 feature sets, rustdoc -D warnings and the thumbv7em no_std build all clean. Release audits: anchor 15, state prose 3, contribution checklist 14, notes render 2, libretro .info 8, all passing; pre-release.sh reports nothing wrong. NOT yet in these files: the co-simulation ladder total. The v2.8.2 ladder run is still in progress on the sibling, so AGENTS.md and the notes say exactly that, and the number lands in a follow-up commit before merge. The Quartus figures (+0.429 ns setup, +0.097 ns hold, 22,997 ALMs) are from the compile of the sibling's current RTL. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
1 parent 0bfa871 commit 5b66686

18 files changed

Lines changed: 119 additions & 56 deletions

File tree

‎.github/release-notes/v2.8.2.md‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# RustyNES v2.8.2 — "Solder"
2+
3+
The third release of the v2.8.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**), and its first RTL release. It works through the MiSTer core's correctness findings from the [RTL audit](https://github.com/doublegate/RustyNES/blob/main/docs/audits/rtl-audit-report.md): every row targeted here now has a verdict and evidence in [its ledger](https://github.com/doublegate/RustyNES/blob/main/docs/audits/rtl-disposition.md). Each fix failed a new co-simulation gate before it was made, and each gate catches the fix reverted. The RTL was written from the nesdev wiki and from this emulator's traces; no reference HDL was opened.
4+
5+
**For emulator users:** one change. MMC1 games no longer lose a mapper reset written on the cycle after another write, the case *Shinsenden* relies on. **Everything else here is the MiSTer core**, whose bitstream is attached below. **No hardware has run any bitstream**; that is v2.9.2's subject.
6+
7+
## The emulator was the one that was wrong
8+
9+
The audit said the MiSTer core honoured an MMC1 reset before filtering consecutive writes, where this emulator filtered first, and called the core wrong. The nesdev wiki says the reverse. The MMC1 serial port ignores a *data* write on the cycle after another write, which is how read-modify-write instructions behave on it, but a write with bit 7 set resets it wherever it lands. The core was right. The emulator ignored the reset too, and that is fixed, with a failing test first. A new core gate runs `INC` on the serial port at both values that matter and keeps the two implementations agreeing. The ledger gains a verdict for this case, INVERTED: the finding was real, and it was about the other implementation.
10+
11+
## Four fixes to the MiSTer core
12+
13+
- **An MMC3 IRQ acknowledge is not lost to a counter clock on the same edge.** When a `$E000` write and the A12 rise that took the counter to zero landed on one master clock, the counter won: an IRQ stayed pending with IRQs disabled until the next `$E000`. The console orders the two events, and either order ends with no IRQ pending. A module gate places both on one clock, since on MMC3 IRQ timing this emulator is deliberately not the reference.
14+
- **SNROM's PRG-RAM follows its CHR-bank enable.** SNROM boards, among them *The Legend of Zelda*'s, wire CHR A16 to a second PRG-RAM enable. The core ignored it, so RAM a game had disabled stayed readable and writable. The gate diverged on 3,924 of 178,678 cycles before the fix.
15+
- **The triangle and noise drop a length reload that lands on a length clock**, as the pulse channels already did. The wiki states the rule for no channel, so the question was put to this emulator's output: a reload placed on the clock cycle is dropped there for every channel.
16+
- **A `$2002` read on the vertical-blank dot leaves the value it returned on the data bus.** The latch was rebuilt from the registers, which had not set yet, so a following read of a write-only register returned `$00` where the console returns `$80`. Finding that dot needed a loop whose length is coprime with the frame; the first draft could reach only 3 of every 33 dots.
17+
18+
Pulse 1's sweep with negate and shift 0 (`$4001 = $08`) is now under a gate as well. The core was already right there; the gate guards this emulator's v2.7.0 fix, and the old bug, reintroduced, diverges on every cycle the note should sound.
19+
20+
## Not defects as written, and two left open
21+
22+
Two rows closed without a change, and two stay open. The MMC3 "Sharp reload-to-zero" behaviour the audit flagged is the one the core already implements. The `$2002` suppression case it describes needs two reads one dot apart, and CPU accesses are three dots apart. Two CPU findings (a spurious `last_cycle` on branches, a second NMI edge during NMI entry) match the code but show no effect under any gate, so they are left for the v2.9.0 re-audit rather than changed blind.
23+
24+
## Verification
25+
26+
| Check | Result |
27+
| --- | --- |
28+
| AccuracyCoin (RAM decoder) | 144 / 144 |
29+
| nestest | pass |
30+
| `--features test-roms` workspace suite | 155 suites, 2,756 passed, 0 failed |
31+
| Co-simulation ladder (`tb/regress.sh`) | re-run in progress on the v2.8.2 RTL; the total is added before merge |
32+
| New gates, each red before its fix | `cart-mmc3-gate`, `mapper1snrom067`, `aputrireload070`, `apunoisereload071`, `ppulatch072`; `mapper1rmw068` and `apusweepneg069` guard behaviour the core already had |
33+
| Mutations | each fix reverted fails its own gate; the triangle and noise mutants each fail only their own channel's |
34+
| Quartus 17.0.2, fitter seed 4 | 0 errors; worst setup +0.429 ns, worst hold +0.097 ns (was +0.421 / +0.112); 22,997 ALMs, 468 RAM blocks, 33 DSP |
35+
36+
**Next: v2.8.3**, RTL robustness: reset synchronisers and the audit's logic-cost claims, measured in Quartus rather than quoted.
37+
38+
## Install
39+
40+
- Download the pre-built binaries for Linux, macOS, and Windows below.
41+
- The MiSTer core bitstream, `RustyNES_MiSTer-v2.8.2.rbf`, is attached below. It has not run on hardware.
42+
- The WebAssembly build is live at [doublegate.github.io/RustyNES](https://doublegate.github.io/RustyNES/).
43+
- The RetroArch core is in RetroArch's Online Updater on the platforms the libretro buildbot publishes to.
44+
- The audit ledger is [`docs/audits/rtl-disposition.md`](https://github.com/doublegate/RustyNES/blob/main/docs/audits/rtl-disposition.md), and the plan [`to-dos/plans/v2.8.2-solder-plan.md`](https://github.com/doublegate/RustyNES/blob/main/to-dos/plans/v2.8.2-solder-plan.md).
45+
- Licensed under GPL-3.0-or-later.

‎AGENTS.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ Enforcement lives alongside the prose: `/ref-proj/` is gitignored/`.dockerignore
4343

4444
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`).
4545

46-
**Current release: v2.8.1 "Gasket"** (2026-09-25) — the libretro core fits the frontends around it: four-player games work through a Four Score option, a controller works again after its port leaves the Zapper, RetroArch no longer reads past the core's input-descriptor list, expansion audio no longer clips, the core declares UNIF images, and the Makefile honours PREFIX, platform=win, DEBUG and CARGO_TARGET_DIR. Built on **v2.8.0 "Bulkhead"** (2026-09-25) — the libretro core stops a fault at its own boundary: an internal error no longer closes RetroArch, save states survive plugging in a Zapper, closing a game withdraws its memory maps, the core loads from any libretro frontend, and the save state now carries the 2A03 internal data bus. v2.8.1 changes no emulation behaviour; the libretro core's audio scale moved to full scale at `1.0` (about 6 dB quieter, because expansion audio clipped at the old `0.5`), and the libretro audit ledger (`docs/audits/libretro-disposition.md`) has no open row. 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** were re-measured on the v2.8.1 tree, and the full `--features test-roms` suite passes 2,754 tests. v2.7.4's mobile changes have a device checklist (`docs/mobile-v2.7.4-device-checklist.md`); this repository records no run of it yet. The co-simulation ladder is **155 gates green, 0 failed, 1 expected failure**, and the sibling's bitstream is re-cut at fitter seed 4 (+0.421 ns setup / +0.112 ns hold). **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.
46+
**Current release: v2.8.2 "Solder"** (2026-09-25) — the MiSTer core's on-die RTL, corrected against the oracle and the wiki: an MMC3 IRQ acknowledge is no longer lost to a same-edge counter clock, SNROM's battery RAM obeys its CHR-line enable, the triangle and noise drop a reload landing on a length clock, a `$2002` read leaves the byte it returned on the data bus, and the emulator's MMC1 no longer ignores a reset written on the cycle after another write. Built on **v2.8.1 "Gasket"** (2026-09-25) — the libretro core fits the frontends around it: four-player games work through a Four Score option, a controller works again after its port leaves the Zapper, RetroArch no longer reads past the core's input-descriptor list, expansion audio no longer clips, the core declares UNIF images, and the Makefile honours PREFIX, platform=win, DEBUG and CARGO_TARGET_DIR. v2.8.2 changes one emulation behaviour (MMC1 honours a reset written on the cycle after another write, where the MiSTer core was right and the emulator wrong); the rest is the MiSTer core, and the RTL audit ledger (`docs/audits/rtl-disposition.md`) has closed its v2.8.2 rows. The libretro audit ledger has no open row. 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** were re-measured on the v2.8.2 tree, and the full `--features test-roms` suite passes 2,756 tests. v2.7.4's mobile changes have a device checklist (`docs/mobile-v2.7.4-device-checklist.md`); this repository records no run of it yet. The co-simulation ladder (155 gates green at v2.6.23, plus seven new gates here) is being re-run on the v2.8.2 RTL, and the sibling's bitstream is re-cut at fitter seed 4 (+0.429 ns setup / +0.097 ns hold). **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.
4747

4848
- **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.
4949

@@ -225,7 +225,7 @@ that is a reason to add a tenth — not a reason to grow this section back.
225225
- `ref-docs/` is immutable. Research updates go in dated supplemental files.
226226
- ADRs go in `docs/adr/` (Michael Nygard format).
227227
- `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.
228-
- 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.8.1 "Gasket"** (2026-09-25). **Never claim any version *later* than v2.8.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.
228+
- 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.8.2 "Solder"** (2026-09-25). **Never claim any version *later* than v2.8.2 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.
229229
- **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.
230230
- 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`.
231231
- **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.

‎ARCHITECTURE.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
**Document Version:** 2.1.0
44
**Last Updated:** 2026-08-30
5-
**Applies to:** RustyNES v2.8.1 (the scheduling model is v2.0.0 "Timebase" onward)
5+
**Applies to:** RustyNES v2.8.2 (the scheduling model is v2.0.0 "Timebase" onward)
66

77
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.
88

‎CHANGELOG.md‎

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,26 @@ cycle-accurate core later replaced.
2626

2727
## [Unreleased]
2828

29-
### RTL correctness on the MiSTer core's on-die path (v2.8.2, in progress)
29+
## [2.8.2] - 2026-09-25 - "Solder" (the MiSTer core's on-die RTL, corrected against the oracle and the wiki)
30+
31+
The third release of the v2.8.x line and its first RTL release: the RTL
32+
audit's correctness rows for the MiSTer core's on-die path. One emulator
33+
change (MMC1), which the test-roms suite, AccuracyCoin 144/144 and nestest
34+
were re-run on; the rest is the MiSTer core, whose bitstream is re-cut at
35+
fitter seed 4 (+0.429 ns setup / +0.097 ns hold). **No hardware has run any
36+
bitstream.**
37+
38+
### The emulator
3039

3140
- **MMC1 never ignores a reset write.** The serial port ignores a write on
3241
the cycle after another, but only the data bit: the bit-7 reset always
3342
takes effect (nesdev MMC1). The emulator ignored the reset too, which
3443
*Shinsenden* (a reset on a read-modify-write's second write) needs not to
3544
do. The MiSTer core already had it right.
3645

37-
The rest of this section is the MiSTer core (`RustyNES_MiSTer`). Each fix
46+
### The MiSTer core
47+
48+
Each fix
3849
failed a new co-simulation gate first, and that gate catches the fix's mutant.
3950
The RTL was written from the nesdev wiki and first-divergence reads of this
4051
emulator's traces; no reference HDL was opened (ADR 0037).
@@ -57,9 +68,12 @@ emulator's traces; no reference HDL was opened (ADR 0037).
5768
gate.** The core was already right; the gate guards the emulator's v2.7.0
5869
fix.
5970
- **The RTL audit's v2.8.2 rows are closed** in
60-
`docs/audits/rtl-disposition.md`. Three were not defects as written, and one
61-
(MMC1) was the emulator's defect, not the core's; the ledger gains an
62-
INVERTED verdict for that case.
71+
`docs/audits/rtl-disposition.md`. Two were not defects as written (the
72+
MMC3 Sharp reload-to-zero, which the core already implements, and a
73+
`$2002` case that needs two reads one dot apart), two CPU rows show no
74+
effect under any gate and are left for the v2.9.0 re-audit, and one (MMC1)
75+
was the emulator's defect, not the core's; the ledger gains an INVERTED
76+
verdict for that case.
6377

6478
## [2.8.1] - 2026-09-25 - "Gasket" (the libretro core fits the frontends around it)
6579

0 commit comments

Comments
 (0)