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

**The bus half of rung 2, and what it found the day it existed.**

A plumb line does not describe vertical — it *is* vertical, and everything else is measured against it. v2.3.1 borrowed the name for measurement that replaces intuition. This release borrows it again, one rung up: rung 1 compares seven CPU registers at instruction boundaries, and this adds a reference the cycles themselves are held against.

`make -C tb cpu-bus-gate` compares per-cycle `bus_addr`, `bus_data` and `bus_access` against the oracle's `.obs.bin`. **Both mutations v2.4.8 recorded as NOT CAUGHT are caught by it** — so the release named for the read-modify-write double write can, one release later, actually verify one.

Rung 1 also grows to **seven ROMs, 1663 records**, with the logical group and the undocumented opcodes.

## It found two real defects on its first run

Neither is visible to rung 1, by construction — `CpuBootTrace` carries `pc`, `a`, `x`, `y`, `p`, `s` and `cycle`, and neither defect changes any of them.

**Indexed read-modify-write skipped its dummy read.** The RMW branch drove the bus only from the access cycle onward and left everything before it at the default `addr = pc`, so `LSR $30,X` never performed the dummy read of the un-indexed `$0030` that its plain-read counterpart does.

**`STA $xxxx,X` without a page cross wrote twice.** The comment directly above the line said *"`we` stays low even for a store, which is why a write always needs cycle 4"* — and the code read `we = d_ir.writes && !idx_page_cross`. Same memory, same cycle count, same registers. On hardware a mapper register written twice is not a register written once. The prose was right and the code was wrong, which is exactly why nobody re-checked it.

Divergences went **7 → 1 → 0** across 793 cycles as these were fixed.

## Both sides must start from the same work RAM

The oracle fills its 2 KiB from a seeded PRNG, so a testbench with flat zeroed memory disagrees on every read of a location the program has not written — and the dummy read of an un-indexed zero-page address is one, constantly. That accounted for 5 of the original 7 divergences.

`nes_golden_export` now emits `<stem>.ram_init.bin`, captured before a single cycle runs, and the testbench loads it. Exported rather than reimplemented: a PRNG written a second time in C++ is a copy that drifts, and the drift would surface as a CPU divergence at an unrelated cycle.

## `pc` is populated, and still not compared

`Observable.pc` was zero in every record, because `CycleRecord.pc` is documented as *"0 when `cpu-instr-cycle-trace` is not enabled"* and the cosim crate did not enable it.

Enabling a trace feature on that crate is not automatically safe — `irq-timing-trace` selects a *different* per-dot loop, which is why the crate sits outside the workspace at all. So it was measured: re-exported before and after, **900 records differing only in those two bytes, zero differing in any other field, boot trace byte-identical.** Observation-only, unlike its neighbour.

It is still not a gate. The two sides do not mean the same thing by it: the oracle holds the *instruction's* opcode-fetch PC across every cycle of that instruction, while the DUT exposes its live PC register, which advances during operand fetches. Measured, they agree on **45%** of cycles — high enough to look nearly right, far too low to gate on. It labels divergences instead: `[bus diff @ cycle=40 in $C016]`.

## The logical group, and why documented opcodes land here

`AND`, `ORA`, `EOR` and `BIT` were simply not implemented — and they are a hard prerequisite for the undocumented combinations, since `SLO` is `ASL` then `ORA`, `RLA` is `ROL` then `AND`, `SRE` is `LSR` then `EOR`. Saying so is better than quietly widening the scope.

`BIT` is the trap: it sets N and V from the **memory** byte's top two bits and Z from `A & M`, writing no register. An implementation routing it through the `AND` path agrees on Z and is wrong on N and V — and agrees on all three whenever the operand's top bits match the result's, which is the case for the obvious test operand `$FF`. The ROM's operands are chosen so they differ.

## The undocumented opcodes cost almost no new datapath

Every one of the six combinations is a read-modify-write plus a documented operation, so `d.rmw` carries them unchanged — the double write, the dummy read and the always-slow absolute-indexed path all come for free. Only the second half is new.

## Three tests agreed with their own mutations

Each is the same shape, and none was visible by reading.

**`SLO`'s flags: NOT CAUGHT.** With `A = $05` and a shifted byte of `$42`, the OR is `$47` — same sign, both non-zero, so the flags are identical whichever value they come from. `A = $80` separates them.

**`DCP` writing `A`: NOT CAUGHT, twice.** First because the next instruction was `LDA`, which overwrote `A` anyway. Then, after capturing `A` to memory first, *still* not caught — because `A` was `$42` and the decremented byte was also `$42`. That is v2.4.4's `TXS`/`TSX` exactly: the mutation's wrong value equalled the right one.

**`RRA`'s overflow mutation BUILD-FAILED**, which proves nothing and is reported as its own outcome rather than as a catch.

Final tally: eight mutations, all caught, baseline control correctly NOT CAUGHT.

## What the gate does not compare, said on every run

Four of the eight `Observable` fields. `put_cycle` is the M2 phase this core does not express; `nmi_line` and both IRQ samples need pins `cpu6502` does not have. The tool prints that it skipped them on every successful run rather than leaving it to documentation — comparing four constants against the oracle's real values would fail for reasons having nothing to do with the bus, and a gate that fails for the wrong reason gets switched off.

**Interrupts remain v2.5.0.** Alignment is by cycle number and never by index — the DUT emits its reset sequence while the oracle's trace starts at cycle 8 — and `bus_diff.py` exits 2, not 0, when the windows do not overlap at all.

## Verification

No crate under `rustynes-{cpu,ppu,apu,mappers,core}` changes, so **AccuracyCoin 141/141 (100.00%, RAM decoder) and nestest 0-diff hold by construction.**
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.4.8 (the scheduling model is v2.0.0 "Timebase" onward)
**Applies to:** RustyNES v2.4.9 (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
44 changes: 44 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,50 @@ cycle-accurate core later replaced.

## [Unreleased]

## [2.4.9] - 2026-08-23 - "Plumbline II" (the bus half of rung 2, and what it found the day it existed)

### Added

- **Rung 2's per-cycle bus comparison** (`RustyNES_MiSTer@715952b`). `make -C tb
cpu-bus-gate` compares `bus_addr`, `bus_data` and `bus_access` against the
oracle's `.obs.bin`. **Both mutations v2.4.8 recorded as NOT CAUGHT are caught
by it** — the release named for the read-modify-write double write can finally
verify one.
- **`<stem>.ram_init.bin`**, the power-on work RAM captured before a cycle runs.
The oracle fills its 2 KiB from a seeded PRNG, so a zeroed testbench disagrees
on every read of a location the program has not written. Exported as a golden
rather than reimplemented in C++, where a second copy of a PRNG would drift.
- **The logical group** — `AND`, `ORA`, `EOR`, `BIT` across six addressing modes.
Documented opcodes that were simply missing, and a hard prerequisite for the
undocumented combinations.
- **The undocumented opcodes** — `LAX`, `SAX`, `SLO`/`RLA`/`SRE`/`RRA`/`DCP`/`ISC`,
and the multi-byte `NOP`s. Rung 1 now stands at **seven ROMs, 1663 records**.

### Fixed

- **Indexed read-modify-write skipped its dummy read.** The RMW branch drove the
bus only from the access cycle onward, leaving earlier cycles at `addr = pc`.
- **`STA $xxxx,X` without a page cross wrote TWICE.** The comment above the line
said *"`we` stays low even for a store"*; the code read
`we = d_ir.writes && !idx_page_cross`. Identical memory, cycles and registers —
and on hardware a mapper register written twice is not one written once.

### Notes

- **`pc` is populated but deliberately not compared.** Enabling
`cpu-instr-cycle-trace` fills it — verified observation-only first, all 900
records differing in *those two bytes and nothing else*. But the two sides mean
different things by it and agree on only **45%** of cycles, so it labels
divergences instead of gating them.
- **Three tests agreed with their own mutations**, each a wrong answer coinciding
with a right one: `SLO`'s flags, `DCP` writing `A` (twice), and one mutant that
did not compile — reported as its own outcome, never as a catch.
- **Interrupts remain v2.5.0.** `cpu6502` has no `nmi_n`/`irq_n` pins, so
`put_cycle`, `nmi_line` and both IRQ samples are skipped — stated on every
successful run rather than left to documentation.
- No crate under `rustynes-{cpu,ppu,apu,mappers,core}` changes, so **AccuracyCoin
141/141 (100.00%, RAM decoder) and nestest 0-diff hold by construction.**

## [2.4.8] - 2026-08-23 - "Palimpsest" (read-modify-write, and a gate that cannot see its own subject)

### 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.4.8"
version = "2.4.9"
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.4.8
**Applies to:** RustyNES v2.4.9

---

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.4.8 "Palimpsest"**. 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.4.9 "Plumbline II"**. 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.4.8**.
> 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.4.9**.

---

Expand Down
Loading
Loading