From 75c7749a32c084b37af7958e29e2f2b221db7eec Mon Sep 17 00:00:00 2001 From: DoubleGate Date: Sun, 2 Aug 2026 02:20:02 -0400 Subject: [PATCH 1/2] =?UTF-8?q?feat(accuracysnes):=20bless=20C5.15=20?= =?UTF-8?q?=E2=80=94=20exclude=20the=20one=20undefined=20hi-res=20pixel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The retraction established that the first hi-res pixel of a line is a genuine reference disagreement (RustySNES + ares emit black, snes9x + Mesen2 the backdrop) on a value ares' own source flags as "not confirmed on hardware". Hashing it would have made EVERY hi-res scene permanently unblessable under ADR 0013 rule 4 over one pixel. `HiResEven` now excludes column 0, so its sample is 255x224 rather than 256x224 -- a deliberate part of the contract, stated in the golden file's header, in the variant's doc, and at both host call sites rather than left to look like an off-by-one. With that column out, all three hosts agree bit-for-bit: snes9x 0xf7ed8ab9ecd95d85 Mesen2 0xf7ed8ab9ecd95d85 RustySNES 0xf7ed8ab9ecd95d85 Rule 4 is satisfied, so the golden is blessed from a render the references agree on. 55 scenes match on both hosts, zero unblessed. Coverage 358 -> 359 of 443 (302 on-cart + 55 scenes + 2 host-side). C5 drops to two uncovered rows, C5.06/C5.07, still blocked by the separate mainscreen-column disagreement. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 19 +++++++++++++++++ .../tests/accuracysnes_scenes.rs | 21 ++++++++++++++++--- docs/STATUS.md | 6 +++--- docs/accuracysnes-coverage.md | 4 ++-- docs/accuracysnes-plan.md | 4 ++-- scripts/accuracysnes/libretro_crossval.c | 8 ++++++- scripts/accuracysnes/mesen_scenes.lua | 8 ++++++- tests/golden/accuracysnes-scenes.tsv | 10 ++++++++- 8 files changed, 67 insertions(+), 13 deletions(-) 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..da033e94 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 \ @@ -192,8 +207,8 @@ fn hash_scene(fb: &[u16], width: usize, extract: Extract) -> (u64, Vec) { let mut h: u64 = 0xcbf2_9ce4_8422_2325; let mut px = Vec::with_capacity(SCENE_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 first_col..SCENE_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]; 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..9e7ee106 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,7 +229,7 @@ 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++) { + for (unsigned x = first_col; x < SCENE_W; x++) { unsigned r, g, b; if (pixel_format == FMT_XRGB8888) { uint32_t v = ((const uint32_t *)row)[x * xstep]; diff --git a/scripts/accuracysnes/mesen_scenes.lua b/scripts/accuracysnes/mesen_scenes.lua index fc598b26..a93bbbba 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,7 +159,7 @@ 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 + for x = firstCol, SCENE_W - 1 do local v = buf[row + x * 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. 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 From 172c26cb08557f5630041e446ced363ace68dbbf Mon Sep 17 00:00:00 2001 From: DoubleGate Date: Sun, 2 Aug 2026 02:32:13 -0400 Subject: [PATCH 2/2] refactor(accuracysnes): loop the sample index, not the source column Bot review, both suggestions accepted; no blocking issues. All three hosts now loop over the SAMPLE index and derive the source column from it, rather than looping over source columns. That makes the C, Lua and Rust implementations line up statement for statement, which matters more here than it looks: cross-host drift inside this loop is the one thing a golden cannot detect -- every host would produce a stable, reproducible hash of its own slightly different region. Also: the Rust `px` capacity is `sample_w * SCENE_H` rather than `SCENE_W * SCENE_H`, so the allocation states the sample size instead of over-reserving by a row's worth under HiResEven. VERIFIED AS A PURE REFACTOR, which is the only thing that matters for a change that touches the hashing loop: scene19 still hashes 0xf7ed8ab9ecd95d85 on snes9x and Mesen2, and 55 scenes match on both hosts with zero unblessed. Co-Authored-By: Claude Opus 5 (1M context) --- .../tests/accuracysnes_scenes.rs | 11 ++++++++--- scripts/accuracysnes/libretro_crossval.c | 9 ++++++--- scripts/accuracysnes/mesen_scenes.lua | 6 ++++-- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/crates/rustysnes-test-harness/tests/accuracysnes_scenes.rs b/crates/rustysnes-test-harness/tests/accuracysnes_scenes.rs index da033e94..7b98e6d2 100644 --- a/crates/rustysnes-test-harness/tests/accuracysnes_scenes.rs +++ b/crates/rustysnes-test-harness/tests/accuracysnes_scenes.rs @@ -205,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 first_col..SCENE_W { + 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/scripts/accuracysnes/libretro_crossval.c b/scripts/accuracysnes/libretro_crossval.c index 9e7ee106..88ee7f2e 100644 --- a/scripts/accuracysnes/libretro_crossval.c +++ b/scripts/accuracysnes/libretro_crossval.c @@ -229,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 = first_col; 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 a93bbbba..c262b593 100644 --- a/scripts/accuracysnes/mesen_scenes.lua +++ b/scripts/accuracysnes/mesen_scenes.lua @@ -159,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 = firstCol, 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