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
75 changes: 75 additions & 0 deletions .github/release-notes/v2.6.3.md
Original file line number Diff line number Diff line change
@@ -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, 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.

**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.**
4 changes: 2 additions & 2 deletions AGENTS.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
40 changes: 40 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,46 @@ 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, 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
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

- **Gradle 10 deprecations cleared in the Android build.** Four call sites, each
Expand Down
38 changes: 19 additions & 19 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 3 additions & 3 deletions OVERVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

---

Expand All @@ -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**.

---

Expand Down
Loading