Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b8b2b0a
docs(plan): v2.6.5 closes rung 5, and the planned rung-6 work defers
doublegate Aug 26, 2026
fa2fd55
docs(plan): record what v2.6.5's two tasks actually returned
doublegate Aug 26, 2026
5e1bc5a
fix(core): clock the controller on the CLK edge that ENDS a read, not…
doublegate Aug 26, 2026
3c00638
docs(accuracy): triage AccuracyCoin's sixteen coded passes
doublegate Aug 26, 2026
2bf4c74
docs: the agy destructive-replace note was stale, and it was repeated…
doublegate Aug 26, 2026
150372f
fix(core): only an actual STROBE drops an owed controller shift
doublegate Aug 26, 2026
8906428
chore(gitignore): add *.dut.bin, the fifth golden suffix this list la…
doublegate Aug 27, 2026
63c7c69
docs: fold v2.6.5's findings into the standing rules and the plan
doublegate Aug 27, 2026
2f8dfcd
feat(cosim): export the per-dot PPU state fixture as an opt-in diagno…
doublegate Aug 28, 2026
32624fd
feat(ppu): carry the $2007 read buffer in the per-dot state trace (sc…
doublegate Aug 28, 2026
96134c0
docs(plan): $2007 Stress Test closes; rung 5 is 4 of 146 differing
doublegate Aug 28, 2026
a7f9d91
test(roms): build ALE + Read and Hybrid Addresses sub-test ROMs
doublegate Aug 28, 2026
69b8bde
feat(ppu): stamp each state-trace record with its CPU cycle (schema 4)
doublegate Aug 28, 2026
abe9262
test(roms): build the Sprite 0 Hit behavior sub-test, and record why …
doublegate Aug 29, 2026
18ccbf9
ci(cosim): run the excluded crate's tests under ppu-state-trace too
doublegate Aug 29, 2026
9f4aa3b
chore(release): cut v2.6.5 "Muster"
doublegate Aug 29, 2026
c45078b
docs(plan): rung 5 closed; rung 6 re-scoped from v2.6.5 to v2.6.6
doublegate Aug 29, 2026
7e19780
fix(ci): three failures the local run missed, and the grep that hid one
doublegate Aug 29, 2026
6ae7554
fix: six CodeRabbit findings, and a stale test one of them exposed
doublegate Aug 29, 2026
3cb4b30
fix(bus): the Four Score chain clocks with the pads, not on every read
doublegate Aug 29, 2026
2edf0f5
style(test): line continuations in the trace_cpu_cycle exclusion reason
doublegate Aug 29, 2026
44e39e5
style(cosim): rustfmt the excluded crate
doublegate Aug 29, 2026
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
38 changes: 38 additions & 0 deletions .github/release-notes/v2.6.5.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# RustyNES v2.6.5 "Muster"

Rung 5 closes: the AccuracyCoin status vector is identical entry for entry across all 146 entries, with 146 of 146 executed on both sides and none `NotRun`.

A muster is a roll call where every name is called **and answered**. That is this release's acceptance exactly, in two clauses: the vector agrees entry for entry, *and* no entry is unrun on both sides. The second clause is v2.6.4's addition — without it, an identical vector over entries that never executed is a pass, and was one.

## The gate

```text
coverage: 146 of 146 entries executed on both sides (0 on neither, 0 on one side only)
status vectors are IDENTICAL entry for entry across all 146 entries.
```

Measured over the 4500-frame golden, 134,012,761 cycles. At the version's start the same gate read **5 of 146 executed** and 22 differing.

## Five defects, and the shape they share

Four of the five were invisible to every gate that existed when the version opened, and the recurring shape is **a gate agreeing about a question it was never asked**.

**The background shift registers' reload and their shift clock need separate gates.** With one shared gate `BG Serial In` was not merely failing, it was *arithmetically unreachable*: reload dots are absolute, so on a render re-enable the next reload is at most seven dots away, and the reload discards the low seven bits — a serial-in one can never reach bit 7, on any alignment, for any stimulus. Modelling both structures reproduces **both** measured shifter values, the oracle's `F807` falling out of the split model without being fitted to it.

**That fix alone left the gate red.** The sprite X counters are **not** gated on rendering, and AccuracyCoin's `Stale Sprite Shift Registers` test 2 states it outright — "Rendering was disabled for 18 ppu cycles, but the sprite counters were NOT halted during that time". This core froze them, so a disable/enable pair pushed every sprite right by the width of the window. **The ROM that states the rule passes either way**: it expects no hit at X=254, and a sprite shoved 18 dots further right is also off the end of the line.

**The PPUADDR second-write `v <- t` copy is delayed**, and the wiki says so inside the write sequence itself — "wait 1 to 1.5 dots after the write completes". This core committed it in the write's own edge. Swept 1 to 4 dots (all close `Hybrid Addresses`) against a control at 8 and 12 (both fail, which is what proves the parameter reached the compiler); the documented minimum ships.

**The pre-render line clears secondary OAM.** The whole evaluation block — *including* the clear — was gated on `scanline < 240`, so the pre-render line kept what scanline 239 had left and the next frame's scanline 0 drew it. **No sprite can ever render on scanline 0**, because OAM Y is stored one less than the display row. A sprite-0 probe over the full battery named it in one run: 24 hits in 134 M cycles, four of them at scanline 0, one per frame.

The fifth, the octal latch holding across the read dot, is verified by exactly one gate and was unverifiable until the fourth landed — the two compose the hybrid address together and neither produces it alone.

## A diagnosis retracted

The residual was read as a **two-dot CPU/PPU alignment error**, from comparing per-dot record spans across two instruments. Three configurations refute it: at the committed alignment the two consoles execute identical `pc`, `bus_addr` and `bus_access` for **1,695,131 cycles**, while a two-dot power-on shift moves the first divergence back to 593,228 and takes the differing share from 5.13% to 66.80%. The "two dots" was two instruments stamping their records at different points in the cycle — the v2.5.7 lesson, third occurrence.

## Also

`rustynes-cosim`'s `state_trace_records_carry_their_cpu_cycle` was gated on a feature no CI step enabled, so it **ran nowhere** — a regression test the gate could not reach, which is the shape the surrounding CI steps exist to prevent. It now runs, and the test that is genuinely inapplicable under that feature is gated out with its reason rather than left failing.

**The oracle changes on the default path** (a `Controller::write_strobe` owed-shift fix), so **AccuracyCoin 141/141 (RAM decoder)** and nestest 0-diff are **verified, not asserted**.
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,22 @@ jobs:
--all-targets -- -D warnings
- name: "test (excluded crate: rustynes-cosim)"
run: cargo test --manifest-path crates/rustynes-cosim/Cargo.toml
# ... and again WITH `ppu-state-trace`, because that crate's
# `state_trace_records_carry_their_cpu_cycle` test is itself gated on the
# feature and so ran NOWHERE in CI. It exists to catch a field that is
# present but constant -- the exact defect that shipped when the bus
# stamped only one of its two call sites -- and a regression test the gate
# cannot reach is the shape this whole block of steps exists to prevent.
#
# `fast_path_does_not_bypass_the_fetch_trace` is `#![cfg(not(...))]` out
# under this feature, which compiles the fast dot path away entirely: with
# no fast path to compare against the general one, that test refuses
# rather than passing vacuously, and a red gate for a property the build
# does not have is noise rather than signal.
- name: "test (excluded crate: rustynes-cosim, ppu-state-trace)"
run: >
cargo test --manifest-path crates/rustynes-cosim/Cargo.toml
--features ppu-state-trace
# The four TRACE features, linted explicitly. `--workspace --all-targets`
# covers each crate's DEFAULT feature set only, and the step above lints
# the `rustynes-cosim` package while compiling `rustynes-core` and
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ Cargo.lock
*.ram_init.bin
*.fetch.bin
*.irq.csv
# A FIFTH, found the same way in v2.6.5. The sibling's `cpu-bus-gate` writes
# `<rom>.dut.bin` -- the DUT's own capture, compared against `<rom>.obs.bin` --
# and its output path is a variable, so a run pointed at this tree offers a file
# this list did not name. A 134 M-cycle window is 2.0 GB. Verified to match
# nothing tracked before being added.
*.dut.bin
# A FOURTH, found the same way and for the same reason: `apu.bin` arrived with
# rung 4 (the 2A03's per-cycle channel levels) at v2.5.9 and was never added, so
# every APU golden the sibling's gates consume has been offered for commit since.
Expand Down
14 changes: 11 additions & 3 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.4 (the scheduling model is v2.0.0 "Timebase" onward)
**Applies to:** RustyNES v2.6.5 (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
79 changes: 79 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,85 @@ cycle-accurate core later replaced.

## [Unreleased]

## [2.6.5] - 2026-08-29 - "Muster" (rung 5 closes — the AccuracyCoin status vector is identical entry for entry across all 146 entries, with 146 of 146 executed on both sides and none NotRun. Five PPU defects close the last six differing entries, and one of the release's own diagnoses is retracted)

A muster is a roll call where every name is called **and answered**. That is this
release's acceptance exactly, in two clauses: the vector agrees entry for entry,
*and* no entry is unrun on both sides. The second clause is v2.6.4's addition —
without it, an identical vector over entries that never executed is a pass, and
was one.

### The gate

```text
coverage: 146 of 146 entries executed on both sides (0 on neither, 0 on one side only)
status vectors are IDENTICAL entry for entry across all 146 entries.
```

Measured over the 4500-frame golden, 134,012,761 cycles. At the version's start
the same gate read **5 of 146 executed** and 22 differing.

### Five defects, and the shape they share

Four of the five were invisible to every gate that existed when the version
opened, and the recurring shape is **a gate agreeing about a question it was
never asked**.

**The background shift registers' reload and their shift clock need separate
gates.** With one shared gate `BG Serial In` was not merely failing, it was
*arithmetically unreachable*: reload dots are absolute, so on a render re-enable
the next reload is at most seven dots away, and the reload discards the low seven
bits — a serial-in one can never reach bit 7, on any alignment, for any stimulus.
Modelling both structures reproduces **both** measured shifter values, the
oracle's `F807` falling out of the split model without being fitted to it.

**That fix alone left the gate red.** The sprite X counters are **not** gated on
rendering, and AccuracyCoin's `Stale Sprite Shift Registers` test 2 states it
outright — "Rendering was disabled for 18 ppu cycles, but the sprite counters
were NOT halted during that time". This core froze them, so a disable/enable pair
pushed every sprite right by the width of the window. **The ROM that states the
rule passes either way**: it expects no hit at X=254, and a sprite shoved 18 dots
further right is also off the end of the line.

**The PPUADDR second-write `v <- t` copy is delayed**, and the wiki says so inside
the write sequence itself — "wait 1 to 1.5 dots after the write completes". This
core committed it in the write's own edge. Swept 1 to 4 dots (all close
`Hybrid Addresses`) against a control at 8 and 12 (both fail, which is what
proves the parameter reached the compiler); the documented minimum ships.

**The pre-render line clears secondary OAM.** The whole evaluation block —
*including* the clear — was gated on `scanline < 240`, so the pre-render line kept
what scanline 239 had left and the next frame's scanline 0 drew it. **No sprite
can ever render on scanline 0**, because OAM Y is stored one less than the display
row. A sprite-0 probe over the full battery named it in one run: 24 hits in
134 M cycles, four of them at scanline 0, one per frame.

The fifth, the octal latch holding across the read dot, is verified by exactly one
gate and was unverifiable until the fourth landed — the two compose the hybrid
address together and neither produces it alone.

### A diagnosis retracted

The residual was read as a **two-dot CPU/PPU alignment error**, from comparing
per-dot record spans across two instruments. Three configurations refute it: at
the committed alignment the two consoles execute identical `pc`, `bus_addr` and
`bus_access` for **1,695,131 cycles**, while a two-dot power-on shift moves the
first divergence back to 593,228 and takes the differing share from 5.13% to
66.80%. The "two dots" was two instruments stamping their records at different
points in the cycle — the v2.5.7 lesson, third occurrence.

### Also

`rustynes-cosim`'s `state_trace_records_carry_their_cpu_cycle` was gated on a
feature no CI step enabled, so it **ran nowhere** — a regression test the gate
could not reach, which is the shape the surrounding CI steps exist to prevent. It
now runs, and the test that is genuinely inapplicable under that feature is gated
out with its reason rather than left failing.

**The oracle changes on the default path** (a `Controller::write_strobe` owed-shift
fix), so **AccuracyCoin 141/141 (RAM decoder)** and nestest 0-diff are **verified,
not asserted**.

## [2.6.4] - 2026-08-26 - "Rubric" (OAM DMA lands and all nine AccuracyCoin disagreements close, every rule that closed the last three stated by the test ROM and by neither nesdev page — and then the gate that certified them is found to have covered 88 of 146 entries. The emulation core is unchanged)

A rubric is the authoritative statement of the rules, written in the margin by
Expand Down
42 changes: 21 additions & 21 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.4"
version = "2.6.5"
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.4
**Applies to:** RustyNES v2.6.5

---

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.4 "Rubric"**. The never-tagged v2.4.0 "Concordance" shipped inside **v2.4.1 "Fabric"** — this sentence had attached that fact to whichever release was current, carried forward by three mechanical version bumps, and said it of v2.4.2, v2.4.3 and v2.4.4 in turn.
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.5 "Muster"**. 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.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

> RustyNES's emulation core descends from an extensively-documented accuracy program. Where this and related docs reference deep "v1.x"/"v2.x" engine narrative, read it as upstream engine lineage (engineering history), not as RustyNES release versions. Two distinct "v2.0"s exist and must not be conflated: the engine-lineage v2.0 master-clock work shipped as RustyNES **v1.0.0**, while RustyNES's own **v2.0.0 "Timebase"** (2026-07-03) is the later release that *replaced* that same scheduler. The current release is **v2.6.4**.
> 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.5**.

---

Expand Down
Loading