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
43 changes: 43 additions & 0 deletions .github/release-notes/v2.5.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# RustyNES v2.5.1 "Retrace"

**A return address, and a gate that reported a pass it could not have earned.**

Retrace is where execution goes when an interrupt finishes, and it is also what this release had to do to its own last one. **Rung 2 closes**: `tb/interrupt_sweep.py` asserts /NMI, /IRQ, or **both together** before instruction K and holds it, for every K across a hazard program, driving the identical stimulus into both sides — **60 injection points, 0 divergences** on all seven CPU fields. Getting there took three defects, and none of them was in the place I was looking.

## A hardware interrupt pushed the wrong return address

`RTI` returned one byte too high. The cause was **a shared block with two writers**, not the interrupt entry path. The generic operand-fetch step advances PC at `tcyc == 1` for every addressing mode except three — and `AM_BRK` was not among them. `BRK` and a hardware interrupt *share* that mode and **disagree** about it: `BRK` advances over its second byte, an interrupt does not. So for `BRK` both writers assigned `pc + 1`, same value, last one wins, no visible fault; a hardware interrupt received the generic increment with nothing overriding it.

**`BRK` passing 186/186 is what kept it hidden.** The only opcode exercising `AM_BRK` was the one on which the defect was invisible, so the shared block was never examined. Rung 1 cannot see it at all — it has no interrupt stimulus — which is precisely why rung 2 exists.

## The injection was wired to a dead path

It first targeted `Bus::poll_nmi` / `poll_irq`. Those look like the right functions and are not the ones the production CPU uses: it samples `nmi_level()` / `irq_level()` every cycle and edge-detects itself. The oracle therefore never took an injected NMI while the DUT always did — a divergence at every single injection point, which reads as a catastrophic RTL failure and was a one-line harness error. Moving it took **IRQ 0/8 to 4/8 and NMI 0/8 to 5/8**.

## A gate that measured nothing and reported the answer it wanted

[ADR 0038](https://github.com/doublegate/RustyNES/blob/main/docs/adr/0038-cosim-interrupt-injection-api.md) makes the injection API conditional on a precondition: a default build must emit none of it. The ADR specified `cargo expand -p rustynes-core --lib 2>/dev/null | grep -c inject_` **must be 0**. `cargo-expand` is a separate binary and is not installed here. The redirect swallows "no such command", `grep -c` counts an empty stream, and the gate prints **the 0 it is looking for** — while measuring nothing at all. Run as written it passes on a build where the feature is fully enabled.

It was caught by the control, not by reading: the same command **with the feature on** also returned 0, which is impossible if the instrument were live. Reading the OFF number first would have banked a false pass on a merge precondition. Replaced with the toolchain's own expander, and the ADR now requires reading the control first. **Measured: off = 0, on = 17.**

## A published finding, retracted

The previous commit reported this core's interrupt sequence as **five cycles where hardware is seven**. That is wrong. The sequence was seven cycles throughout. Both sides showed `SEI` at cycle 8; the oracle reached its handler at 17 and the DUT at 15, and `15 - 8 - 2` was read as a five-cycle sequence. In fact the DUT took the interrupt **one instruction earlier** — it never ran `SEI` — so its sequence ran 8..14 and reached the handler at `8 + 7 = 15`. Seven cycles, starting two cycles early. The cause was in the harness: `cur_instr` was `0` before the first opcode fetch, so `--nmi-at-instr 0` put cycle 0 inside the window and asserted the pin throughout the eight-cycle reset. **Two cycle numbers were differenced without checking which instruction each belonged to.** Retracted in place rather than deleted, because it was published as a defect against the RTL.

## The gate has a gap, and a mutation found it

Seven mutations, three outcomes, baseline captured once and verified first. Five CAUGHT. **Inverting NMI/IRQ priority came back NOT CAUGHT** — because sweeping one pin at a time, an inverted priority is indistinguishable from a correct one; nothing ever asserts both. The sweep gained a `both` mode, and the same mutation is now caught. The remaining two non-catches are explained rather than excused: NMI-with-no-recognition-delay is structurally unreachable at this rung (every injection point asserts from the *start* of an instruction, and ADR 0038's API is instruction-granular by design), and removing the dispatch-site `nmi_pending` clear changes nothing observable because the `AM_BRK` hijack window already clears it — evidence about the RTL, not about the gate.

## Two v2.5.0 gates, reclassified rather than carried

nestest 0-diff and the 5 M-cycle window both stop at a `$2002` read where **both sides address it** and only the data differs, because the DUT has no PPU. They are **rung-3 acceptance criteria**, not v2.5.1 debt. Carried debt implies someone dropped it; these were never reachable from here.

## The programme to v2.7.0

The Fabric line is delivered, so this release also lands the plan for the rest of the console — PPU, APU, mappers, MiSTer integration — with the maintainer's three decisions recorded: **both boards eventually** (DE10-Nano plus the mandatory SDRAM add-on, and a SuperStation One, with one `.rbf` booting both), **the top six mappers** (~90% of the licensed library, explicitly not FDS or expansion audio), and **v2.7.0 scoped to what genuinely fits** with the arithmetic stated up front — 20–40 weeks FTE across twenty release slots, milestones rather than 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 first de-risks the SDRAM work. Four dated `ref-docs/` research files land with it, including a hardware **source map** — a map, not a summary, because a paraphrase would become a third source that drifts from both the wiki and `docs/ppu-2c02.md`. Its citations are pinned by a new audit, which caught three bare filenames on its first run, and whose own extractor is guarded because the hand-run that preceded it reported "4 cited paths, 0 missing" against a file holding 32 — the pattern omitted `.xhtml`, the extension every real citation uses.

## Verified, not asserted

`rustynes-core` changes, so the accuracy numbers were re-run rather than inherited: **AccuracyCoin 141/141 (100.00%, RAM decoder)**, **nestest 0-diff**, workspace **2233 passed / 128 suites / 0 failed**. DUT side: lint 0 findings, nine opcode-group ROMs at **2115 records / 0 divergences** (`opgroup8` unchanged at 186, so `BRK` survived the change to the block it had been relying on), sweep **60/60**.

**No upstream libretro/RetroArch sync**, per the amended cadence: it waits for the MiSTer core to be complete. A licence change would still override that.
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.5.0 (the scheduling model is v2.0.0 "Timebase" onward)
**Applies to:** RustyNES v2.5.1 (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
75 changes: 75 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,81 @@ cycle-accurate core later replaced.

## [Unreleased]

## [2.5.1] - 2026-08-23 - "Retrace" (a return address, and a gate that reported a pass it could not have earned)

### Added

- **The interrupt sweep, and rung 2 closes** (`RustyNES_MiSTer@9425d73`).
`tb/interrupt_sweep.py` asserts /NMI, /IRQ, or **both together** before
instruction K and holds it, for every K across a hazard program, driving
identical stimulus into both sides: **60 injection points, 0 divergences** on
all seven CPU fields (20 instructions x three pin configurations).
- **The core-side injection API** ([ADR 0038](docs/adr/0038-cosim-interrupt-injection-api.md)),
behind a default-off `cosim-interrupt-inject` feature that only the excluded
`rustynes-cosim` crate enables. `Nes::inject_nmi` / `inject_irq` drive the
**level functions** the CPU actually samples, and `Oracle::run_with_injection`
steps instructions while toggling the pins.
- **`mister_source_map_audit.rs`**, pinning every citation in the new hardware
**source map** to a file that exists. Under the firewall those pages are the
*only* permitted sources, so a dangling citation is a behaviour with no source.
- **The v2.5.1 -> v2.7.0 programme** ([`to-dos/plans/v2.7.0-mister-core-plan.md`](to-dos/plans/v2.7.0-mister-core-plan.md)),
four dated `ref-docs/` research files, and `to-dos/mister/`.

### Fixed

- **A hardware interrupt pushed the wrong return address.** `RTI` returned one
byte too high. The cause was a **shared block with two writers**: the generic
operand-fetch step advances PC at `tcyc == 1` for every addressing mode except
three, and `AM_BRK` was not among them. `BRK` and a hardware interrupt *share*
that mode and disagree about it -- `BRK` advances over its second byte, an
interrupt does not -- so for `BRK` both writers assigned the same value and the
fault was invisible. **`BRK` passing 186/186 is what kept it hidden**: the only
opcode exercising the mode was the one on which the defect did not show.
- **The injection was wired to a dead path.** It first targeted `Bus::poll_nmi` /
`poll_irq`, which look like the right functions and are not the ones the
production CPU uses -- it samples `nmi_level()` / `irq_level()` every cycle and
edge-detects itself. The oracle never took an injected NMI while the DUT always
did. Moving it took **IRQ 0/8 to 4/8 and NMI 0/8 to 5/8**.
- **Second-to-last-cycle interrupt recognition** in the DUT, per the documented
rule. It did not move the sweep's numbers and is in because the rule says so;
said plainly rather than credited with a fix it did not make.
- **`mutate.sh` announced a baseline it had not captured.** Sourced from a
non-bash shell, `BASH_SOURCE` was unset and `ROOT` resolved to `/`; the `cp`
failed and the next line still printed "captured baseline". It now fails there,
and the echo is joined to the copy with `&&`.

### Changed

- **ADR 0038's own gate 2a reported a false pass.** As written it piped
`cargo expand` -- a separate binary, not installed here -- through
`2>/dev/null | grep -c inject_`, so it counted an empty stream and printed the
**0 it was looking for** while measuring nothing. Caught by the control, not by
reading: the same command with the feature *enabled* also returned 0. Replaced
with the toolchain's own expander, and the ADR now requires reading the control
first. **Measured: off = 0, on = 17.**
- **The sweep gained its both-pins case because a mutation found the gap.**
Inverting NMI/IRQ priority came back NOT CAUGHT: sweeping one pin at a time,
an inverted priority is indistinguishable from a correct one. It is caught now.
- **A published finding is retracted.** The previous commit reported this core's
interrupt sequence as **five cycles where hardware is seven**. It was seven
throughout; two cycle numbers were differenced without checking which
instruction each belonged to. The real fault was in the harness -- `cur_instr`
was `0` before the first opcode fetch, so `--nmi-at-instr 0` asserted the pin
throughout the eight-cycle reset. Retracted in place rather than deleted,
because it was published as a defect against the RTL.
- **nestest 0-diff and the 5 M-cycle window are reclassified, not carried.** Both
stop at a `$2002` read where *both sides address it* and only the data differs
-- the DUT has no PPU. They are rung-3 acceptance criteria.

### Verified

`rustynes-core` changes (the feature gate, its fields and setters), so the
accuracy numbers are **verified, not asserted**: **AccuracyCoin 141/141 (100.00%,
RAM decoder)**, **nestest 0-diff**, workspace **2233 passed / 128 suites / 0
failed**. DUT side: lint 0 findings, nine opcode-group ROMs **2115 records / 0
divergences** (`opgroup8` unchanged at 186), sweep **60/60**. Seven mutations
against the sweep: five CAUGHT, two NOT CAUGHT and both explained.

## [2.5.0] - 2026-08-23 - "Rungwork" (the 6502 rung, and the two gates it cannot reach)

### Added
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.5.0"
version = "2.5.1"
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-23
**Applies to:** RustyNES v2.5.0
**Applies to:** RustyNES v2.5.1

---

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.5.0 "Rungwork"**. 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.5.1 "Retrace"**. 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.5.0**.
> 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.5.1**.

---

Expand Down
Loading