Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
5620f71
feat(ppu): model the long scanline (B2.03)
doublegate Aug 1, 2026
3401300
docs(accuracysnes): B2.02/B2.03 landed; B2.07 needs its own design
doublegate Aug 1, 2026
e09aa2b
docs(accuracysnes): work out the E8.01 design before authoring it
doublegate Aug 1, 2026
80bb4be
wip(accuracysnes): E8.01 authored but BROKEN — do not merge
doublegate Aug 1, 2026
a839fde
fix(accuracysnes): E8.01 passes RustySNES + snes9x; Mesen2 still short
doublegate Aug 1, 2026
265eaf3
docs(accuracysnes): E8.01 is not the cause of Mesen2's extra failures
doublegate Aug 1, 2026
0b8bd2a
docs(accuracysnes): record the unreconciled Mesen2 failure count
doublegate Aug 1, 2026
2ca2cbe
docs(accuracysnes): reconcile the count — Mesen2 and MesenCE are two …
doublegate Aug 1, 2026
8fcdba6
docs(accuracysnes): the fourth opinion is blocked, and by what exactly
doublegate Aug 1, 2026
5ef1aac
docs(accuracysnes): the Mesen2 failing SET is unstable — no known-fai…
doublegate Aug 1, 2026
f8c7653
docs(accuracysnes): retract the "unstable runner" claim — my probe wa…
doublegate Aug 1, 2026
8a7e0b4
test(accuracysnes): enumerate Mesen2's divergences; E8.01 is phase-fr…
doublegate Aug 1, 2026
7132ae8
docs(accuracysnes): E8.01 needs a poll-count instrument, not more off…
doublegate Aug 1, 2026
d652687
wip(accuracysnes): E8.01 poll-count instrument — better, not yet correct
doublegate Aug 1, 2026
ecfce83
Merge remote-tracking branch 'origin/main' into feat/accuracysnes-gro…
doublegate Aug 1, 2026
3d12d45
test(accuracysnes): three Group E rows — E8.01, E9.02, E5.06 (T-04-E)
doublegate Aug 1, 2026
7ccb0bc
style(accuracysnes): satisfy the two clippy lints CI caught
doublegate Aug 1, 2026
734f6f9
docs(apu,status): record what the Group E batch pins, and refresh the…
doublegate Aug 1, 2026
32b8639
docs(roadmap): T-04-E is 95 of 129 after the Group E batch
doublegate Aug 1, 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
44 changes: 44 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **AccuracySNES: three Group E rows land — `E8.01`, `E9.02`, `E5.06`.** Coverage **347 → 350 of
443** (297 on-cart + 53 scenes), battery **338 tests at 100%**, and both references agree:
`snes9x: OK (14 known divergence(s))`, `Mesen2: OK (2 known divergence(s))`, 53/53 scenes on each.
`MESEN2_KNOWN_FAILURES` came **down** from 3 to 2 — the one Mesen2 divergence that was ours is
gone.

**`E8.01` (`KON` examined at 16 kHz) — the two earlier drafts were measuring their own timing.**
Both timed a *delay*: write `KON`, count until `ENVX` leaves zero. That delay is `(P − φ) mod P` —
a sawtooth in the write's phase against the DSP's examination grid — and two readings of a
sawtooth cannot recover its period. Worse, `φ` is set by where the IPL handshake left the SPC700,
which is paced by the S-CPU and therefore **differs between the NTSC and PAL images of the same
build**. The poll-count draft duly reported a difference of 2 on NTSC and 0 on PAL. Neither draft
was ever scored.

What replaced it measures **writes the DSP never sees**: `KON` is a register, not a queue, so a
second write before the DSP next looks *cancels* the first and that voice is never keyed. Eight
writes 24 SPC cycles apart span 168 cycles, and a half-open interval of that length contains
`⌊168/P⌋` or `⌊168/P⌋ + 1` grid points **for every phase** — 3 or 4 voices started at 16 kHz,
6 or 7 at 32 kHz, two ranges that cannot meet. RustySNES measures 3; Mesen2, which failed the
phase-fragile draft, passes this one.

**`E9.02` (noise output is bipolar).** `E9.01` already pins the frozen LFSR seed; this row scores
the *transition*, because one step must turn a full-scale negative reading (`$81`) into a positive
one at about half scale (`$3F`, or `$1F` if the window caught two steps). A core emitting the
register's 15-bit value directly reads positive throughout — the DC-heavy noise the errata's
"highpass" remark exists to exclude.

**`E5.06` (BRR wraps at 15 bits) — the first assertion was vacuous, and the injection proved it.**
"At least one of 64 `OUTX` readings is negative" looked airtight for a purely positive drive.
Injecting the named bug — clamping the store instead of letting it wrap — made the row **pass
harder**: 64 negatives out of 64, against the correct decoder's 32. A clamping decoder pins the
buffer at `+7FFEh`, and the gaussian interpolator's three-term partial sum (the truncation
`E5.13` is about) overflows a signed 16-bit intermediate from that constant, so every reading comes
back negative. *The interpolator, not the decoder, was supplying the sign.* The row now asserts
**bipolarity** — a saturating decoder settles, and a settled decoder's readings carry one sign —
and both saturating variants fail it.

- **AccuracySNES: the Mesen2 oracle is FIXED — one `emu.setInput` call too many.** The battery has
never run under MesenCE; it now completes: `magic='ACSN'`, `R_DONE=$A5`, **335/335** status bytes
written, and `crossval.sh` reports `Mesen2: 1 failing test(s)` where it previously timed out.
Expand Down Expand Up @@ -610,6 +647,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- **AccuracySNES: `E9.01` and `E9.02` stand down as SKIP on a menu restart.** `E9.02` steps the noise
LFSR by design and nothing can put it back: `FLG` bit 7 is the DSP's *soft* reset and does not
re-seed the shift register (checked against ares, which re-seeds only in `DSP::power`). A comment
in `voice_program` claimed the opposite and has been corrected. `E9.02` is registered last, both
rows use the same power-on gate `F1.07` uses for its unwritten `$4218`, and the harness's
restart-idempotence check moved from `first_run_passed - 1` to `- 3`.

- **Run-ahead stays opt-in, and the reason is now measured rather than assumed.**
`docs/frontend.md` recorded the per-frame save-state allocation as *the* blocker on making
run-ahead default-on. `v1.25.0` removed that allocation, so the question was re-measured.
Expand Down
62 changes: 56 additions & 6 deletions crates/rustysnes-test-harness/tests/accuracysnes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,51 @@ const A5_08_SLOTS: [(u8, &str); 7] = [
(6, "8x (PHD+PLD) - 16 NOP (expect 76)"),
];

/// The measurement slots the `v1.29.0` Group E batch records.
const GROUP_E_BATCH_SLOTS: [(u16, &str); 7] = [
(256, "E8.01 KON sweep key-on mask (bit 7 = voice 0)"),
(
257,
"E8.01 voices the sweep started (expect 3-4; 6-7 means a 32 kHz poll)",
),
(258, "E9.02 noise OUTX at the seed (expect $81)"),
(259, "E9.02 noise OUTX one step on (expect $3F or $1F)"),
(
260,
"E5.06 negative OUTX readings (of 64; 0 means the decoder clamps)",
),
(261, "E5.06 non-zero OUTX readings (of 64)"),
(262, "E5.06 the last of the 64 readings"),
];

/// Report what the Group E batch measured, for cross-emulator comparison.
///
/// These three rows all score a *shape* — how many voices a burst of `KON` writes started, which
/// way a noise reading's sign went, how often a purely positive drive read back negative — and the
/// verdict byte carries none of the underlying numbers. A disagreement between references is far
/// easier to read here than through a failure code.
#[test]
fn group_e_batch_measurements_are_reported() {
let report = run().expect("battery must run");
assert!(report.done, "battery did not finish");

let mut out = String::from("\n v1.29.0 Group E batch measurements:\n");
for (slot, what) in GROUP_E_BATCH_SLOTS {
let v = report.meas[slot as usize];
let _ = writeln!(out, " slot {slot} {v:5} (${v:04X}) {what}");
}
println!("{out}");

// One crude check, in the same spirit as the timing channel's: an unwritten slot reads zero,
// and every one of these has a value the cart always writes.
let voices = report.meas[257];
assert!(
(1..=8).contains(&voices),
"E8.01 recorded {voices} started voices, which is not a count of eight voices — the slot \
was never written, or the mask reduction overflowed"
);
}

/// Report the raw timing measurements, and sanity-check them against physics.
///
/// This exists because a one-byte verdict cannot carry a dot count. Reporting a 32-`NOP` baseline
Expand Down Expand Up @@ -1889,14 +1934,19 @@ fn the_dpad_navigates_the_pages() {
DONE_MARK,
"the restarted battery never finished"
);
// One fewer than a cold boot: F1.07 stands down as SKIP on a re-run because its phase A needs
// the power-on value of $4218, which a soft restart cannot reproduce (the previous run armed
// auto-read). Its verdict is $FF (skip), not a fail code. Relative to the first run's pass count
// rather than a literal, so adding tests does not break this.
// Three fewer than a cold boot, and every one of them a genuine power-on dependency that
// stands down as SKIP ($FF) rather than reporting a fail code:
//
// * F1.07 -- phase A needs the power-on value of $4218, and the previous run armed auto-read;
// * E9.01 and E9.02 -- both read the noise LFSR's power-on seed, and E9.02 steps the register
// away from it by design. FLG bit 7 does not re-seed it, so nothing can put it back.
//
// Relative to the first run's pass count rather than a literal, so adding tests does not break
// this. A change here means a NEW power-on dependency appeared, which is worth knowing about.
assert_eq!(
rd16(&sys, R_PASSED),
first_run_passed - 1,
"the restarted battery did not reproduce its result (minus the power-on-only F1.07)"
first_run_passed - 3,
"the restarted battery did not reproduce its result (minus the three power-on-only rows)"
);
let f107_idx = catalog()
.iter()
Expand Down
6 changes: 3 additions & 3 deletions docs/STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ compositor (`docs/adr/0014`, T-CA-10) becomes the sole renderer — its compose/
per-dot against live registers (mid-line CGRAM/OAM/`INIDISP` at dot resolution; BG-fetch-ahead is
Phase 4c, not yet landed) — and the first-party AccuracySNES
cartridge matures into a usable instrument (AccuracyCoin-style paged menu + automatic skyline
results + per-test B-skip + a Select WRAM debug viewer) with its battery at **344 of 443** dossier
assertions (291 on-cart + 53 rendered scenes).
results + per-test B-skip + a Select WRAM debug viewer) with its battery at **350 of 443** dossier
assertions (297 on-cart + 53 rendered scenes).
**`v1.22.0 "Horizon"` fixes the long-standing DSP-1 continuous-mode Mode-7 flat floor** (Pilotwings
flight, SMK track): the shared NEC-DSP host-sync (`run_until_rqm`) stopped one host-write too early,
so reading a host input word raised RQM before the firmware cleared DRC to 16-bit — mis-framing the
Expand Down Expand Up @@ -234,7 +234,7 @@ tracked here, always current, reaffirmed every release:
| Core/Curated coprocessors (oracle-gated) | ✅ **3 / 3, honesty gate green** | DSP-1 (4 commercial ROMs), Super FX/GSU (58 Krom ROMs + per-opcode suite), SA-1 (18 commercial carts) — `ORACLE_COPROCESSORS` |
| BestEffort coprocessors, real-title validated | ✅ **10 / 11** | DSP-2, DSP-4, ST010, S-DD1, CX4, OBC1 boot a real commercial title to real gameplay content. **DSP-3 (SD Gundam GX) + ST011 (2-dan Morita Shougi)** are liveness/determinism-validated — detection + `host_accesses > 0` + bit-identical framebuffer (`dsp3_st011_oncart`). **ST018 (Nidan Morita Shogi 2) + S-RTC (Daikaijuu Monogatari II)** are detection + boot + determinism-validated against their real carts (`srtc_st018_oncart`), but their coprocessor's core function is **usage-gated** (the ST018 shogi AI runs only on the computer's move; the S-RTC clock is read at specific moments), so it is not exercised in a headless boot |
| BestEffort coprocessors, no booting dump | ⚠️ **1 / 11** | SPC7110 — **two** local Tengai Makyou Zero dumps both fail to boot to content: the 7 MiB one is a fan-translation ROM hack needing a patch-only memory region no cartridge has (`docs/audit/spc7110-boot-crash-2026-07-08.md`); a 5 MiB dump (sha256 `8620203d…`) freezes at a near-blank screen with zero coprocessor activity, and does not match the documented-good original (`69d06a3f…`). The correct original-cartridge dump remains the ROM-sourcing gap (`docs/rom-test-corpus.md`) |
| AccuracySNES (first-party battery) | ✅ **344 / 443 assertions covered, 100% on-cart pass** | A 332-test self-scoring battery spanning Groups A-G. **344 of 443** dossier assertions covered: **291 on-cart** + **53 via rendered scenes** (`docs/adr/0013`), kept as separate columns on purpose (`docs/accuracysnes-coverage.md`, regenerated with the ROM so it cannot drift). Every scored row is inject-verified for non-vacuity and cross-validated headlessly against **Mesen2, snes9x, and MesenCE** (the last built this cycle as the per-dot compositor's blueprint and exact-frame oracle). Provenance gate green. Ships an AccuracyCoin-style on-cart UI as of `v1.21.0` (paged menu + automatic skyline results + per-test B-skip + a Select WRAM debug viewer). Remaining ~99 assertions are compositor-gated hi-res, second-image G-rows, treacherous CPU/dot-model timing, or provably uncoverable (`docs/accuracysnes-coverability-audit-2026-07-23.md`) |
| AccuracySNES (first-party battery) | ✅ **350 / 443 assertions covered, 100% on-cart pass** | A 338-test self-scoring battery spanning Groups A-G. **350 of 443** dossier assertions covered: **297 on-cart** + **53 via rendered scenes** (`docs/adr/0013`), kept as separate columns on purpose (`docs/accuracysnes-coverage.md`, regenerated with the ROM so it cannot drift). Every scored row is inject-verified for non-vacuity and cross-validated headlessly against **Mesen2, snes9x, and MesenCE** (the last built this cycle as the per-dot compositor's blueprint and exact-frame oracle). Provenance gate green. Ships an AccuracyCoin-style on-cart UI as of `v1.21.0` (paged menu + automatic skyline results + per-test B-skip + a Select WRAM debug viewer). Remaining ~93 assertions are compositor-gated hi-res, second-image G-rows, treacherous CPU/dot-model timing, or provably uncoverable — though "uncoverable" has twice proved too strong (`E5.06` and `B2.02`/`B2.03` all landed after being parked) (`docs/accuracysnes-coverability-audit-2026-07-23.md`) |
| Determinism contract | ✅ **proven** | bit-identical framebuffer/audio across runs; save-state round-trip proven across all three board tiers (no-coprocessor, Curated, BestEffort) |

**Named residuals, tracked not hidden:** the 65816 `e1.e` divergence (`docs/adr/0002`);
Expand Down
8 changes: 4 additions & 4 deletions docs/accuracysnes-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,16 @@ Every sub-group of Part V is enumerated, so this is a **complete** statement of
| `E2` | 10 | 9 | 0 | E2.10 |
| `E3` | 14 | 10 | 0 | E3.07, E3.09, E3.12, E3.13 |
| `E4` | 11 | 7 | 0 | E4.05, E4.07, E4.09, E4.10 |
| `E5` | 13 | 12 | 0 | E5.06 |
| `E5` | 13 | 13 | 0 | |
| `E6` | 11 | 3 | 0 | E6.01, E6.03, E6.04, E6.05, E6.06, E6.07, E6.08, E6.10 |
| `E7` | 18 | 17 | 0 | E7.02 |
| `E8` | 11 | 6 | 0 | E8.01, E8.06, E8.08, E8.09, E8.11 |
| `E9` | 20 | 12 | 0 | E9.02, E9.07, E9.08, E9.09, E9.11, E9.14, E9.16, E9.20 |
| `E8` | 11 | 7 | 0 | E8.06, E8.08, E8.09, E8.11 |
| `E9` | 20 | 13 | 0 | E9.07, E9.08, E9.09, E9.11, E9.14, E9.16, E9.20 |
| `E10` | 6 | 2 | 0 | E10.02, E10.03, E10.04, E10.06 |
| `F1` | 22 | 13 | 0 | F1.13, F1.15, F1.16, F1.17, F1.18, F1.19, F1.20, F1.21, F1.22 |
| `G1` | 18 | 15 | 0 | G1.06, G1.13, G1.18 |

**294 of 443** enumerated assertion rows covered by an on-cart test, plus **53** covered only by a rendered scene (`docs/adr/0013`) — **347 of 443** in total.
**297 of 443** enumerated assertion rows covered by an on-cart test, plus **53** covered only by a rendered scene (`docs/adr/0013`) — **350 of 443** in total.

The two columns are kept apart on purpose. An on-cart result means the same thing on any emulator and on real hardware; a rendered scene needs a host holding the golden. Adding them into one figure would quietly change what the number claims.

Expand Down
Loading
Loading