diff --git a/CHANGELOG.md b/CHANGELOG.md index aae3d43c..0b275cb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- **`C5.15` is now blessed coverage: excluding the one undefined pixel made the rest of Mode 5 + agree.** The retraction below establishes that the first hi-res pixel of a line is a genuine + reference disagreement on a value ares flags as *not confirmed on hardware*. Hashing it would have + made **every** hi-res scene permanently unblessable over one pixel. + + `HiResEven` now excludes column 0, so the sample is 255x224 rather than 256x224 — a deliberate + part of the contract, recorded in the golden file's own header and in the variant's doc. With that + one column out, all three hosts agree bit-for-bit: + + ``` + snes9x 0xf7ed8ab9ecd95d85 + Mesen2 0xf7ed8ab9ecd95d85 + RustySNES 0xf7ed8ab9ecd95d85 + ``` + + Rule 4 is satisfied, so the golden is blessed. **55 scenes match on both hosts, zero unblessed.** + Coverage `358 → 359 of 443`; `C5` drops to two uncovered rows (`C5.06`/`C5.07`, still blocked by + the mainscreen-column disagreement). + - **RETRACTION: the Mode-5 first-pixel divergence is a 2-vs-2 reference disagreement, not a RustySNES defect.** The entry below claims the two references agree bit-for-bit against RustySNES, "this project's signature for a real defect". That was published from **two** references without diff --git a/crates/rustysnes-test-harness/tests/accuracysnes_scenes.rs b/crates/rustysnes-test-harness/tests/accuracysnes_scenes.rs index 77e242fe..7b98e6d2 100644 --- a/crates/rustysnes-test-harness/tests/accuracysnes_scenes.rs +++ b/crates/rustysnes-test-harness/tests/accuracysnes_scenes.rs @@ -71,7 +71,15 @@ fn manifest_path() -> PathBuf { enum Extract { /// 256-wide frame, rows `FIRST_ROW..FIRST_ROW + SCENE_H`. Direct, - /// 512-wide frame: the even columns, which are the subscreen half. + /// 512-wide frame: the even columns — the subscreen half — **starting at column 2**. + /// + /// Column 0 is deliberately excluded. It is the first hi-res pixel of the line, and the + /// references genuinely disagree about it: RustySNES and ares emit black, snes9x and Mesen2 the + /// backdrop, and ares' own source says *"exact value initializations are not confirmed on + /// hardware"*. Under ADR 0013 rule 4 a golden may not be blessed from a pixel the references + /// disagree about — so hashing it would make every hi-res scene permanently unblessable over + /// one undefined pixel. Excluding it makes the other 255 columns, which all four + /// implementations agree on, into evidence. HiResEven, } @@ -181,6 +189,13 @@ fn hash_scene(fb: &[u16], width: usize, extract: Extract) -> (u64, Vec) { 2 } }; + // Column 0 is skipped under `HiResEven` — see the variant's own doc. The sample is therefore + // 255 columns wide there, not 256, and that is a deliberate part of the contract rather than an + // off-by-one. + let first_col = match extract { + Extract::Direct => 0, + Extract::HiResEven => 1, + }; assert!( fb.len() >= (FIRST_ROW + SCENE_H) * width, "the framebuffer holds {} pixels, too few for rows {FIRST_ROW}..{} at width {width} — the \ @@ -190,13 +205,18 @@ fn hash_scene(fb: &[u16], width: usize, extract: Extract) -> (u64, Vec) { FIRST_ROW + SCENE_H ); let mut h: u64 = 0xcbf2_9ce4_8422_2325; - let mut px = Vec::with_capacity(SCENE_W * SCENE_H); + // The sample is narrower than the region under `HiResEven` — see `first_col`. All three hosts + // loop over the SAMPLE index and derive the source column from it, rather than looping over + // source columns, so the three implementations line up statement for statement; cross-host + // drift in this loop is the one thing a golden cannot detect. + let sample_w = SCENE_W - first_col; + let mut px = Vec::with_capacity(sample_w * SCENE_H); for y in FIRST_ROW..FIRST_ROW + SCENE_H { - for x in 0..SCENE_W { - // Even columns only under `HiResEven`: the subscreen half, which is the half the three + for x in 0..sample_w { + // Even columns only under `HiResEven`: the subscreen half, which is the half the // references agree on. RustySNES's own framebuffer is not line-doubled, so the row // index needs no step — Mesen2's does, and that is handled in its own host. - let p = fb[y * width + x * step]; + let p = fb[y * width + (x + first_col) * step]; let canonical = ((p & 0x1F) << 10) | (p & 0x03E0) | ((p >> 10) & 0x1F); px.push(canonical); h ^= u64::from(canonical); diff --git a/docs/STATUS.md b/docs/STATUS.md index b7335dc2..015bf4c3 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -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 **358 of 443** dossier -assertions (302 on-cart + 54 rendered scenes + 2 host-side). +results + per-test B-skip + a Select WRAM debug viewer) with its battery at **359 of 443** dossier +assertions (302 on-cart + 55 rendered scenes + 2 host-side). **`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 @@ -241,7 +241,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) | ✅ **358 / 443 assertions covered, 100% on-cart pass** | A 343-test self-scoring battery spanning Groups A-G. **358 of 443** dossier assertions covered: **302 on-cart** + **54 via rendered scenes** (`docs/adr/0013`) + **2 host-side** (`dossier.rs::HOST_COVERED`, admitted only where the cart physically cannot observe the assertion), 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 ares** — three independent references as of `v1.29.0`, with MesenCE additionally serving 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 ~85 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`) | +| AccuracySNES (first-party battery) | ✅ **359 / 443 assertions covered, 100% on-cart pass** | A 343-test self-scoring battery spanning Groups A-G. **359 of 443** dossier assertions covered: **302 on-cart** + **55 via rendered scenes** (`docs/adr/0013`) + **2 host-side** (`dossier.rs::HOST_COVERED`, admitted only where the cart physically cannot observe the assertion), 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 ares** — three independent references as of `v1.29.0`, with MesenCE additionally serving 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 ~84 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`); diff --git a/docs/accuracysnes-coverage.md b/docs/accuracysnes-coverage.md index f2fa5753..36672034 100644 --- a/docs/accuracysnes-coverage.md +++ b/docs/accuracysnes-coverage.md @@ -26,7 +26,7 @@ Every sub-group of Part V is enumerated, so this is a **complete** statement of | `C2` | 10 | 10 | 0 | 0 | — | | `C3` | 10 | 9 | 0 | 0 | C3.10 | | `C4` | 5 | 0 | 5 | 0 | — | -| `C5` | 15 | 0 | 12 | 0 | C5.06, C5.07, C5.15 | +| `C5` | 15 | 0 | 13 | 0 | C5.06, C5.07 | | `C6` | 7 | 0 | 6 | 0 | C6.07 | | `C7` | 16 | 8 | 6 | 0 | C7.07, C7.12 | | `C8` | 12 | 0 | 11 | 0 | C8.09 | @@ -52,7 +52,7 @@ Every sub-group of Part V is enumerated, so this is a **complete** statement of | `F1` | 22 | 13 | 0 | 0 | F1.13, F1.15, F1.16, F1.17, F1.18, F1.19, F1.20, F1.21, F1.22 | | `G1` | 18 | 15 | 0 | 2 | G1.13 | -**302 of 443** enumerated assertion rows covered by an on-cart test, plus **54** covered only by a rendered scene (`docs/adr/0013`) and **2** covered only by a host-side test — **358 of 443** in total. +**302 of 443** enumerated assertion rows covered by an on-cart test, plus **55** covered only by a rendered scene (`docs/adr/0013`) and **2** covered only by a host-side test — **359 of 443** in total. The three columns are kept apart on purpose, in descending order of what the evidence is worth. An on-cart result means the same thing on any emulator and on real hardware; a rendered scene needs a host holding the golden; a **host-side** cover is this project testing its own code, which is the one thing AccuracySNES exists to stop being the only evidence. The host tier is admitted only where the cart *physically cannot* observe the assertion — the stimulus comes from outside the cartridge, or the subject is the loader rather than the machine — and every entry names the test and the reason (`dossier.rs::HOST_COVERED`). Adding the columns into one figure would quietly change what the number claims. diff --git a/docs/accuracysnes-plan.md b/docs/accuracysnes-plan.md index 67050429..fc8e72ac 100644 --- a/docs/accuracysnes-plan.md +++ b/docs/accuracysnes-plan.md @@ -14,8 +14,8 @@ AccuracySNES closed ticket **T-04**. The follow-on tickets minted here are **T-0 | | | |---|---| | Tests | **343** (scoring + golden vectors + region SKIP per image) — *tests, not assertions; see the note below the table* | -| Assertion coverage | **358 of 443** dossier assertions — **302 on-cart** + **54 rendered scenes** + **2 host-side**, kept as separate columns in descending order of what the evidence is worth (`docs/accuracysnes-coverage.md`) | -| Rendered scenes | **54** declared, all blessed and matching on both scene hosts (`docs/adr/0013`); **54** dossier rows have a scene as their only cover | +| Assertion coverage | **359 of 443** dossier assertions — **302 on-cart** + **55 rendered scenes** + **2 host-side**, kept as separate columns in descending order of what the evidence is worth (`docs/accuracysnes-coverage.md`) | +| Rendered scenes | **55** declared, all blessed and matching on both scene hosts (`docs/adr/0013`); **55** dossier rows have a scene as their only cover. One uses the `hires-even` extraction (`C5.15`, Mode 5) | | Pass rate | **100.00%** on-cart, floor enforced at 1.00 by `tests/accuracysnes.rs` | | Cross-validated | **Three references** as of `v1.29.0`. Mesen2 agrees on every test but `F1.03`, which clocks both ports out of one latch and so needs the port-2 input its Lua runner cannot drive; snes9x has 14 recorded divergences and **ares** 3, each with a citation in `scripts/accuracysnes/crossval.sh`. A headless **MesenCE** is separately the per-dot compositor's blueprint + exact-frame oracle. All images. | | Groups shipped | **A** (65C816) · **B** (5A22) · **C** (PPU, on-cart and rendered) · **D** (DMA/HDMA) · **E** (SPC700 + S-DSP) · **F** (controller ports) · **G** (cartridge/memory map) — all seven, all partial | diff --git a/scripts/accuracysnes/libretro_crossval.c b/scripts/accuracysnes/libretro_crossval.c index 60ffaca1..88ee7f2e 100644 --- a/scripts/accuracysnes/libretro_crossval.c +++ b/scripts/accuracysnes/libretro_crossval.c @@ -186,6 +186,12 @@ static void video_refresh(const void *d, unsigned w, unsigned h, size_t p) { * measured 2026-08-02, emits 512x224 and does not). */ const unsigned want_w = (want_extract == EXTRACT_HIRES_EVEN) ? SCENE_W * 2u : SCENE_W; const unsigned xstep = (want_extract == EXTRACT_HIRES_EVEN) ? 2u : 1u; + /* Column 0 is EXCLUDED under `hires-even`, so the sample is 255 columns wide there rather than + * 256. It is the first hi-res pixel of the line and the references genuinely disagree about it + * -- RustySNES and ares emit black, snes9x and Mesen2 the backdrop, and ares' own source says + * "exact value initializations are not confirmed on hardware". Hashing it would make every + * hi-res scene permanently unblessable under ADR 0013 rule 4 over one undefined pixel. */ + const unsigned first_col = (want_extract == EXTRACT_HIRES_EVEN) ? 1u : 0u; const unsigned ystep = (d && h >= (SCENE_H + FIRST_ROW) * 2u) ? 2u : 1u; /* The warn condition is the DROP condition, character for character. They disagreed in the * first draft (`<` here, `!=` below), so a frame taller than the contract was dropped without a @@ -223,13 +229,16 @@ static void video_refresh(const void *d, unsigned w, unsigned h, size_t p) { uint64_t hash = 0xcbf29ce484222325ull; for (unsigned y = 0; y < SCENE_H; y++) { const uint8_t *row = (const uint8_t *)d + (size_t)(y + FIRST_ROW) * ystep * p; - for (unsigned x = 0; x < SCENE_W; x++) { + /* Loop the SAMPLE index and derive the source column, matching the Rust and Lua hosts + * statement for statement -- cross-host drift in this loop is the one thing a golden + * cannot detect. */ + for (unsigned x = 0; x < SCENE_W - first_col; x++) { unsigned r, g, b; if (pixel_format == FMT_XRGB8888) { - uint32_t v = ((const uint32_t *)row)[x * xstep]; + uint32_t v = ((const uint32_t *)row)[(x + first_col) * xstep]; r = (v >> 19) & 0x1F; g = (v >> 11) & 0x1F; b = (v >> 3) & 0x1F; } else { - uint16_t v = ((const uint16_t *)row)[x * xstep]; + uint16_t v = ((const uint16_t *)row)[(x + first_col) * xstep]; if (pixel_format == FMT_RGB565) { /* Green is 6 bits here because the core widened a 5-bit channel; dropping the * low bit recovers the original rather than inventing precision. */ diff --git a/scripts/accuracysnes/mesen_scenes.lua b/scripts/accuracysnes/mesen_scenes.lua index fc598b26..c262b593 100644 --- a/scripts/accuracysnes/mesen_scenes.lua +++ b/scripts/accuracysnes/mesen_scenes.lua @@ -142,6 +142,12 @@ local function hashFrame(id) local hires = EXTRACT[id] == "hires-even" local width = hires and (SCENE_W * 2) or SCENE_W local xstep = hires and 2 or 1 + -- Column 0 is EXCLUDED under `hires-even`, so the sample is 255 columns wide there rather than + -- 256. It is the first hi-res pixel of the line and the references genuinely disagree about it: + -- RustySNES and ares emit black, snes9x and Mesen2 the backdrop, and ares' own source says + -- "exact value initializations are not confirmed on hardware". Hashing it would make every + -- hi-res scene permanently unblessable under ADR 0013 rule 4 over one undefined pixel. + local firstCol = hires and 1 or 0 local ystep = hires and 2 or 1 local wantLen = hires and (SCENE_BUF_LEN * 4) or SCENE_BUF_LEN if #buf ~= wantLen then @@ -153,8 +159,10 @@ local function hashFrame(id) local px = {} for y = 0, SCENE_H - 1 do local row = (y + FIRST_ROW) * ystep * width - for x = 0, SCENE_W - 1 do - local v = buf[row + x * xstep + 1] + -- Loop the SAMPLE index and derive the source column, matching the C and Rust hosts + -- statement for statement; cross-host drift here is the one thing a golden cannot detect. + for x = 0, SCENE_W - 1 - firstCol do + local v = buf[row + (x + firstCol) * xstep + 1] -- Mesen hands back 24-bit RGB; the SNES channels are 5-bit widened to 8, so the top -- five bits recover the original rather than inventing precision. local r = (v >> 19) & 0x1F diff --git a/tests/golden/accuracysnes-scenes.tsv b/tests/golden/accuracysnes-scenes.tsv index 0914903c..bd8aa7d6 100644 --- a/tests/golden/accuracysnes-scenes.tsv +++ b/tests/golden/accuracysnes-scenes.tsv @@ -2,7 +2,14 @@ # # Format: \t # -# The hash is FNV-1a over a fixed 256x224 region of canonical 0RRRRRGGGGGBBBBB pixels. Fixed and +# The hash is FNV-1a over a fixed 256x224 region of canonical 0RRRRRGGGGGBBBBB pixels -- except +# under the `hires-even` extraction, where it is 255x224: the frame is 512 wide, the sample is its +# EVEN columns (the subscreen half), and column 0 is EXCLUDED. That first hi-res pixel is the one +# thing the references genuinely disagree about -- RustySNES and ares emit black, snes9x and Mesen2 +# the backdrop, and ares' own source says "exact value initializations are not confirmed on +# hardware" -- so hashing it would make every hi-res scene permanently unblessable under rule 4 over +# one undefined pixel. Excluding it, all three hosts agree bit-for-bit. Which rule a scene uses is +# declared in build/scenes.tsv's fourth column (docs/adr/0013, 2026-08-02 supplement). Fixed and # canonical because emulators agree about neither geometry nor pixel format, and a golden has to # compare pictures rather than output conventions. Each host declares where its own picture starts # in its own buffer (`FIRST_ROW`): RustySNES 0, snes9x's libretro core 0, Mesen2 7. @@ -202,3 +209,4 @@ c7-hflip-sliver-order 0x863f085bccebd107 c8-force-black-outside-window 0x21a5b8a828e748d7 c5-4bpp-bitplane-order 0xf11def7ad73be325 c11-mode7-product-low-bits-masked 0xc032679c9076440a +c5-mode5-hires-16px-tiles 0xf7ed8ab9ecd95d85