You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 390848e
Browse filesBrowse the repository at this point in the historyBrowse files
feat(accuracysnes): cover C11.03, and retract the interlace-gate claim (T-04-H) (#302)
C11.03 -- each M7x * ORG product is masked to a multiple of 64 before the
terms accumulate -- is now covered by a Mode 7 scene with M7A and M7B both
$0101, deliberately not round numbers. With M7B = $0101 the discarded part is
`line MOD 64`, a different amount on every line, so a core that accumulates
the full products samples a different texel on roughly a quarter of the
columns of most lines. Every other Mode 7 scene uses round matrix values,
which hide this completely -- none of them was evidence for the row.
Blessed at 0xc032679c9076440a, which all three references produce, stable
across repeated runs. Non-vacuity confirmed by removing the mask from
fetch_mode7_column: this scene's hash moves. Two existing scenes move under
that injection too, so the mask was already witnessed in aggregate -- what
this adds is a scene whose stated purpose is the mask.
RETRACTION. PR #299 claimed the field gate turned C7.12's three-way emulator
split into a two-way one, citing snes9x and Mesen2 producing the identical
hash. That was ONE RUN and it does not hold: the identical ROM run twice under
Mesen2 gives 0x16f7ab8c7f97b7f8 then 0x266241e43ab85064, the two field
parities alternating run to run. RustySNES and snes9x are each stable; Mesen2
is not.
The gate is necessary but not sufficient and the missing half is host-side:
the cart publishes only on its chosen field, but which RENDERED frame a host
associates with the R_SCENE value it read is not pinned, and Mesen2's headless
runner does not make that association deterministically. The interlace scene
is withdrawn rather than left unblessed, because a scene reporting a different
hash each run is noise in the gate output. The gate itself stays -- cart-side
half, moves no golden, costs only battery runtime.
Coverage is unchanged at 350 of 443: C11.03 replaces C7.12 one-for-one,
because the withdrawn scene was being counted as C7.12's scene-cover while
still unblessed. What changed is that the row now counted has a golden all
three references agree on.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: docs/accuracysnes-plan.md
+33-28Lines changed: 33 additions & 28 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -15,7 +15,7 @@ AccuracySNES closed ticket **T-04**. The follow-on tickets minted here are **T-0
15
15
|---|---|
16
16
| Tests |**338** (scoring + golden vectors + region SKIP per image) — *tests, not assertions; see the note below the table*|
17
17
| Assertion coverage |**350 of 443** dossier assertions — **297 on-cart** + **53 rendered scenes**, kept as separate columns (`docs/accuracysnes-coverage.md`) |
18
-
| Rendered scenes |**53**, all cross-validated (`docs/adr/0013`) |
18
+
| Rendered scenes |**54** declared and all cross-validated (`docs/adr/0013`); **53** are the dossier rows they are the only cover for|
19
19
| Pass rate |**100.00%** on-cart, floor enforced at 1.00 by `tests/accuracysnes.rs`|
20
20
| Cross-validated | RustySNES and Mesen2 agree on every test but two `PAD2_CONTRACT` rows the Lua runner cannot drive; snes9x agrees on every test but a handful of recorded reference bugs with citations in `scripts/accuracysnes/crossval.sh`; a headless **MesenCE** (this cycle) is the per-dot compositor's blueprint + exact-frame oracle. All images. |
21
21
| 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 |
@@ -430,39 +430,44 @@ Any `C9`/`C7.12` interlace assertion needs the cart to publish a scene only on a
430
430
a change to `run_scenes`, not to a scene. Worth doing: it unblocks the interlace half of `C9`
431
431
(`C9.03`, `C9.06`) as well as `C7.12`.
432
432
433
-
**BUILT 2026-08-01, and it did what it was supposed to — but the scene is still unblessable.**
433
+
**BUILT 2026-08-01 — and then measured properly, which changed the conclusion.**
434
434
`run_scenes` now publishes the scene ID only on frames whose `$213F` bit 7 is set, so every sighting
435
-
the host counts is the same field (`SCENE_FRAMES` grew 8 → 12, because at half the publication rate
436
-
the old value put the host's fourth sighting on the window's last frame — one of the two ends the
437
-
protocol exists to avoid). The 53 blessed scenes are untouched by it on all three hosts: a still
438
-
picture hashes the same on either field.
435
+
the host counts is the same *cart-side*field (`SCENE_FRAMES` grew 8 → 12, because at half the
436
+
publication rate the old value put the host's fourth sighting on the window's last frame — one of
437
+
the two ends the protocol exists to avoid). The 53 blessed scenes are untouched by it on all three
438
+
hosts: a still picture hashes the same on either field.
439
439
440
-
On the interlace scene it worked exactly as intended. The **three**-way split became a **two**-way
441
-
one, and snes9x and Mesen2 now produce the *identical* hash:
440
+
**The first reading of the result was wrong, and it was wrong because it was a single run.** An OBJ
441
+
interlace scene was added, the three-way hash split became a two-way one — snes9x and Mesen2
442
+
produced the *identical*`0x266241e43ab85064` — and that was published as "the gate works". It does
443
+
not hold. Running the **identical ROM** twice under Mesen2 gives:
442
444
443
-
||`c7-obj-interlace-halves-height`|
445
+
|run |`c7-obj-interlace-halves-height` under Mesen2|
444
446
|---|---|
445
-
| snes9x |`0x266241e43ab85064`|
446
-
| Mesen2 |`0x266241e43ab85064`|
447
-
| RustySNES |`0x16f7ab8c7f97b7f8`|
448
-
449
-
**And the residual is fully characterised**, which is the part worth keeping. Dumping the pixels
450
-
shows both renders agree on everything the row is *about* — the 16x32 sprite occupies 16 display
451
-
rows, its 32x64 neighbour 32 — and differ only in **which field's source rows** are drawn:
452
-
snes9x/Mesen2 draw the even ones, RustySNES the odd ones. One source row, nothing else.
453
-
454
-
**Not blessed, and not called a RustySNES bug either.** "RustySNES alone" is this project's
455
-
signature for a real defect, but RustySNES is not alone here: its `row + field` and its
456
-
`ppu2.mdr.bit(7) = field()` are both ares' (`sfc/ppu/object.cpp:122`, `sfc/ppu/io.cpp:178`), and both
457
-
cores toggle the field at the same V wrap. So this is the bsnes/ares lineage against snes9x and
458
-
Mesen2 — **2 vs 1** by this project's own provenance rule, not 2 vs 2, and no primary source says
459
-
which field maps to which rows. Recorded as a variant set per the same rule the Mode-5 hi-res
460
-
cluster is under: *do not pick a winner*. The tiebreaker is ares actually running the cart, which is
461
-
the same blocked item `A2.10` is waiting on.
447
+
| 1 |`0x16f7ab8c7f97b7f8`|
448
+
| 2 |`0x266241e43ab85064`|
449
+
450
+
Those are exactly the two field-parity outcomes, and Mesen2 alternates between them **run to run on
451
+
one build**. RustySNES and snes9x are each stable; Mesen2 is not.
452
+
453
+
**So the gate is necessary but NOT sufficient, and the missing half is host-side.** The cart's own
454
+
behaviour is deterministic — it publishes only on its chosen field. What is not pinned is which
455
+
*rendered frame* a host associates with the `R_SCENE` value it read: if the host's frame boundary
456
+
sits a frame away from the cart's vblank, it hashes frame N while the cart published on frame N−1's
457
+
field. Mesen2's headless runner does not make that association deterministically.
458
+
459
+
**The scene was therefore withdrawn**, not merely left unblessed: a scene that reports a different
460
+
hash on each run is noise in the gate output, and its "the references agree" reading was an artifact
461
+
of looking once. The gate itself is kept — it is the cart-side half of the fix, it costs only
462
+
battery runtime, and it moves no golden — but nothing exploits it until a host can pin the frame it
463
+
hashes.
464
+
465
+
**The lesson, and it is the same one `E8.01` taught in the same session:** a result that depends on a
466
+
phase nobody controls looks stable until you run it twice. Run an interlace or field-dependent
467
+
capture **at least twice per host** before believing any part of it.
462
468
463
469
`C9.03`/`C9.06` are deliberately NOT written on top of this. Screen interlace has the identical
464
-
parity dependency, so they would land unblessed for the identical reason — two more unblessable
465
-
scenes is not progress. Write them when the tiebreaker exists.
470
+
parity dependency, so they would land in the identical state. Write them when the tiebreaker exists.
466
471
467
472
### `E8.03` — "clears `ENDX` even when suppressed" does not mean suppressed by `KOFF`
Copy file name to clipboardExpand all lines: tests/roms/AccuracySNES/asm/scenes.s
+27-44Lines changed: 27 additions & 44 deletions
Original file line number
Diff line number
Diff line change
@@ -2247,55 +2247,38 @@ SCENES_IMPL = 1
2247
2247
rts
2248
2248
.endproc
2249
2249
2250
-
; c7-obj-interlace-halves-height — C7.12
2251
-
; `c7-objsel-size-6` with OBJ interlace on (SETINI bit 1) and NOTHING else changed. Under interlace a sprite's displayed line maps to twice its internal row, so the 16x32 small member and the 32x64 large one each occupy half the vertical space they otherwise would — a core that ignores SETINI bit 1 renders this scene identically to its twin, which is exactly the comparison the pair exists to make. THIS SCENE IS FIELD-DEPENDENT: interlace selects even or odd sprite rows by field parity, which is why `run_scenes` gates the published window on the field flag. It was written once WITHOUT that gate and produced three different hashes on three emulators, the only three-way split any scene has produced.
2252
-
.procscene_c7_obj_interlace_halves_height
2250
+
; c11-mode7-product-low-bits-masked — C11.03
2251
+
; Mode 7 with `M7A` and `M7B` both `$0101` — deliberately NOT round numbers. Each `M7x * ORG` product has its low six bits masked off (`AND NOT $3F`) before the terms are accumulated, so the per-line origin is quantised to a multiple of 64 in the 1/256-texel fixed point. With `M7B = $0101` the discarded part is `line MOD 64`, which is a different amount on every line — a core that accumulates the full products samples a different texel on roughly a quarter of the columns of most lines, and the two pictures are nothing alike. Round matrix values hide this completely, which is why every other Mode 7 scene here uses them and none of them is evidence for this row.
2252
+
.procscene_c11_mode7_product_low_bits_masked
2253
2253
.a16
2254
2254
.i16
2255
2255
sep #$20
2256
2256
.a8
2257
-
stz $2105; BGMODE 0
2258
-
jsrscene_oam_reset
2259
-
sep #$20
2260
-
.a8
2261
-
lda #$C0
2262
-
sta $2101; OBJSEL pair 6, name base word $0000 — same as the twin
2263
-
rep #$30
2264
-
.a16
2265
-
.i16
2266
-
ldx #$0000
2267
-
stx $2102
2268
-
sep #$20
2269
-
.a8
2270
-
lda #40
2271
-
sta $2104; sprite 0 X
2272
-
lda #60
2273
-
sta $2104; sprite 0 Y
2274
-
lda #$10
2275
-
sta $2104; tile $10 — printable at 4bpp
2276
-
lda #$30
2277
-
sta $2104; attr: palette 0, priority 3
2278
-
lda #120
2279
-
sta $2104; sprite 1 X
2280
-
lda #60
2281
-
sta $2104; sprite 1 Y
2282
-
lda #$10
2283
-
sta $2104
2284
-
lda #$30
2285
-
sta $2104
2286
-
rep #$30
2287
-
.a16
2288
-
.i16
2289
-
ldx #$0100
2290
-
stx $2102; the high table
2257
+
lda #$07
2258
+
sta $2105; BGMODE 7
2259
+
jsrscene_mode7_vram
2291
2260
sep #$20
2292
2261
.a8
2293
-
lda #$08
2294
-
sta $2104; sprite 0 small (16x32), sprite 1 large (32x64)
2295
-
lda #$02
2296
-
sta $2133; SETINI bit 1: OBJ interlace — the ONE difference from the twin
2297
-
lda #$10
2298
-
sta $212C
2262
+
stz $211A; M7SEL: no repeat, no flips
2263
+
lda #$01
2264
+
sta $211B
2265
+
sta $211B; M7A = $0101 — the low byte is what makes the mask observable
2266
+
sta $211C
2267
+
sta $211C; M7B = $0101, so the discarded part is line MOD 64
0 commit comments