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
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **AccuracySNES: the scene protocol publishes on a known field, and the interlace three-way split
is down to a two-way one.** `run_scenes` now sets the scene ID only on frames whose `$213F` bit 7
is set, so every sighting the host counts is the same field. `SCENE_FRAMES` grew 8 → 12: at half
the publication rate the old value put the host's fourth sighting on the window's *last* frame,
which is one of the two ends the capture protocol exists to avoid.

The 53 blessed scenes are unaffected on all three hosts — a still picture hashes the same on either
field, so gating only moves which wall-clock frame the fourth sighting is.

**On the interlace scene it did exactly what it was built to do.** `c7-obj-interlace-halves-height`
(`C7.12`, a 16x32 sprite under OBJ interlace, identical to `c7-objsel-size-6` but for one `SETINI`
bit) previously produced **three** different hashes on three emulators — the only three-way split
any scene has produced, and not a disagreement about interlace at all: each host landed on
whichever field its own frame counter happened to be on. With the gate, snes9x and Mesen2 produce
the **identical** hash `0x266241e43ab85064`.

**The residual is one source row, and it is not blessed.** Pixel dumps show both renders agree on
everything the row asserts — the 16x32 sprite occupies 16 display rows — and differ only in which
field's source rows are drawn: the references take the even ones, RustySNES the odd. That is not
"RustySNES alone", this project's signature for a real defect: RustySNES's `row + field` and its
`$213F` bit 7 are both ares' (`sfc/ppu/object.cpp:122`, `sfc/ppu/io.cpp:178`), and both toggle the
field at the same V wrap. So it is the bsnes/ares lineage against the other two — **2 vs 1** under
this project's own provenance rule — and no primary source says which field maps to which rows.
Recorded as a variant set; the tiebreaker is ares running the cart, the same blocked item `A2.10`
waits on. `C9.03`/`C9.06` are deliberately not written on top of it: screen interlace carries the
identical parity dependency, so they would land unblessed for the identical reason.

- **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.
Expand Down
1 change: 1 addition & 0 deletions docs/accuracysnes-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ Declared in `gen/src/scenes.rs`. Each is reported by the host framebuffer oracle
- **`C7.03`** — c7-hflip-sliver-order
- **`C8.12`** — c8-force-black-outside-window
- **`C5.14`** — c5-4bpp-bitplane-order
- **`C7.12`** — c7-obj-interlace-halves-height

## Tests with no enumerated assertion

Expand Down
44 changes: 40 additions & 4 deletions docs/accuracysnes-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,40 @@ Any `C9`/`C7.12` interlace assertion needs the cart to publish a scene only on a
a change to `run_scenes`, not to a scene. Worth doing: it unblocks the interlace half of `C9`
(`C9.03`, `C9.06`) as well as `C7.12`.

**BUILT 2026-08-01, and it did what it was supposed to — but the scene is still unblessable.**
`run_scenes` now publishes the scene ID only on frames whose `$213F` bit 7 is set, so every sighting
the host counts is the same field (`SCENE_FRAMES` grew 8 → 12, because at half the publication rate
the old value put the host's fourth sighting on the window's last frame — one of the two ends the
protocol exists to avoid). The 53 blessed scenes are untouched by it on all three hosts: a still
picture hashes the same on either field.

On the interlace scene it worked exactly as intended. The **three**-way split became a **two**-way
one, and snes9x and Mesen2 now produce the *identical* hash:

| | `c7-obj-interlace-halves-height` |
|---|---|
| snes9x | `0x266241e43ab85064` |
| Mesen2 | `0x266241e43ab85064` |
| RustySNES | `0x16f7ab8c7f97b7f8` |

**And the residual is fully characterised**, which is the part worth keeping. Dumping the pixels
shows both renders agree on everything the row is *about* — the 16x32 sprite occupies 16 display
rows, its 32x64 neighbour 32 — and differ only in **which field's source rows** are drawn:
snes9x/Mesen2 draw the even ones, RustySNES the odd ones. One source row, nothing else.

**Not blessed, and not called a RustySNES bug either.** "RustySNES alone" is this project's
signature for a real defect, but RustySNES is not alone here: its `row + field` and its
`ppu2.mdr.bit(7) = field()` are both ares' (`sfc/ppu/object.cpp:122`, `sfc/ppu/io.cpp:178`), and both
cores toggle the field at the same V wrap. So this is the bsnes/ares lineage against snes9x and
Mesen2 — **2 vs 1** by this project's own provenance rule, not 2 vs 2, and no primary source says
which field maps to which rows. Recorded as a variant set per the same rule the Mode-5 hi-res
cluster is under: *do not pick a winner*. The tiebreaker is ares actually running the cart, which is
the same blocked item `A2.10` is waiting on.

`C9.03`/`C9.06` are deliberately NOT written on top of this. Screen interlace has the identical
parity dependency, so they would land unblessed for the identical reason — two more unblessable
scenes is not progress. Write them when the tiebreaker exists.

### `E8.03` — "clears `ENDX` even when suppressed" does not mean suppressed by `KOFF`

The dossier row reads: *"KON restarts even if playing, zeroing the envelope, and clears ENDX even
Expand Down Expand Up @@ -929,10 +963,12 @@ documentation-anchored only, because the Mesen2 headless runner times out in thi
`C7.05`/`C7.06` landed on the far-IRQ shim, so it is worth saying what that shim does **not**
unblock.

- **`C7.12`** (16x32 under OBJ interlace renders as 16x16) is a **scene**, and needs the cart to
publish a scene only on a *known field* — it was written once and produced three different hashes
on three emulators because it asked a question whose answer alternates every frame. That is
`v1.29.0` machinery (`run_scenes`, not a scene), shared with `C9.03`/`C9.06`.
- **`C7.12`** (16x32 under OBJ interlace renders as 16x16) is a **scene**. The `v1.29.0` machinery
it needed — `run_scenes` publishing only on a known field — **is built** (2026-08-01), and it
turned the three-way split into a two-way one: snes9x and Mesen2 now agree exactly, and RustySNES
differs by one field of OBJ-interlace source rows and nothing else. Still unblessed, because that
residual is the ares lineage against the other two and no source settles it. Full account in
§"Interlace scenes need frame-parity control".

- **`C7.07`** (Time Over false positive: first sprite 16x16+ at `X = 0-255` with others at negative
X) is reachable *as a test* with `C7.06`'s setup, but it would fail — because RustySNES does not
Expand Down
8 changes: 7 additions & 1 deletion tests/roms/AccuracySNES/asm/runtime.inc
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ V_HW_H = VAR_BASE + $4A ; H from the most recent wide latch (u16)
V_HW_V = VAR_BASE + $4C ; V from that same latch (u16)
V_HW_V0 = VAR_BASE + $4E ; V at hv_begin_wide (u16)
V_HW_DV = VAR_BASE + $50 ; scratch: lines crossed, counted down (u16)
V_SCENE_ID = VAR_BASE + $52 ; the scene ID `run_scenes` publishes on its field (u16)
; The longest span hv_end_wide will report. Beyond this the varying line length has accumulated past
; usefulness, and far beyond it V wraps the field and the delta goes negative — so both are refused
; with $FFFF rather than returned as a plausible number. 64 lines is ~21,800 dots, two orders of
Expand Down Expand Up @@ -222,7 +223,12 @@ MEAS_SLOTS = 512
; occasionally landed on a transition and the gate went red on a different scene each run. An
; intermittently-red gate is worse than a slow one, because it gets ignored.
SCENE_SETTLE = 6
SCENE_FRAMES = 8
; 12, not 8, because the published window is FIELD-GATED (see `run_scenes`): the ID appears on
; alternate frames only, so 8 wall-clock frames give exactly four sightings and the host's fourth —
; the one it hashes — lands on the window's last frame. That is one of the two ends the host
; protocol exists to avoid; snes9x caught a scene mid-transition there once already. 12 frames give
; six sightings, so the fourth is comfortably interior.
SCENE_FRAMES = 12

; --- results block ---------------------------------------------------------------------------
RESULTS = $7EF000
Expand Down
50 changes: 43 additions & 7 deletions tests/roms/AccuracySNES/asm/runtime.s
Original file line number Diff line number Diff line change
Expand Up @@ -2086,18 +2086,54 @@ test_restore := test_restore_impl
jsr hold_frames
plx

sep #$20
.a8
txa
inc a
sta f:R_SCENE ; scene IDs are 1-based; 0 means "none yet"

; --- The published window is FIELD-GATED -------------------------------------------------
;
; The ID appears only on frames whose PPU2 field flag ($213F bit 7) is SET, so every sighting
; the host counts is the same field. Without that, an interlace scene asks a question whose
; answer alternates every frame: `C7.12` (a 16x32 sprite under OBJ interlace renders as 16x16)
; was written as a scene and produced THREE different hashes on three emulators, the only
; three-way split any scene has produced. It was not three cores disagreeing about interlace —
; each host simply landed on whichever field its own frame counter happened to be on.
;
; Costs one bit of nothing for the 53 non-interlaced scenes: a still picture hashes the same on
; either field, so gating only moves WHICH wall-clock frame the host's fourth sighting is. That
; is why SCENE_FRAMES had to grow — at half the publication rate, the old 8 gave exactly four
; sightings and the fourth landed on the window's last frame, the one end the host protocol
; deliberately avoids.
;
; What this does NOT fix, and cannot: if two hosts sample WRAM at different points relative to
; the field toggle, they gate on opposite fields and an interlace scene still disagrees. That is
; a measurable question, not an assumption — cross-validate any interlace scene before blessing
; a golden for it (ADR 0013 rule 4).
rep #$30
.a16
.i16
txa
inc a
and #$00FF
sta f:V_SCENE_ID ; scene IDs are 1-based; 0 means "none yet"
phx
ldx #SCENE_FRAMES
jsr hold_frames
@hold_field:
phx
jsr wait_vblank
plx
sep #$20
.a8
lda $213F ; PPU2 status; bit 7 is the field flag
and #$80
beq @not_our_field
lda f:V_SCENE_ID
bra @publish
@not_our_field:
lda #$00
@publish:
sta f:R_SCENE
rep #$30
.a16
.i16
dex
bne @hold_field
plx

sep #$20
Expand Down
60 changes: 59 additions & 1 deletion tests/roms/AccuracySNES/asm/scenes.s
Original file line number Diff line number Diff line change
Expand Up @@ -2247,11 +2247,68 @@ SCENES_IMPL = 1
rts
.endproc

; c7-obj-interlace-halves-height — C7.12
; `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.
.proc scene_c7_obj_interlace_halves_height
.a16
.i16
sep #$20
.a8
stz $2105 ; BGMODE 0
jsr scene_oam_reset
sep #$20
.a8
lda #$C0
sta $2101 ; OBJSEL pair 6, name base word $0000 — same as the twin
rep #$30
.a16
.i16
ldx #$0000
stx $2102
sep #$20
.a8
lda #40
sta $2104 ; sprite 0 X
lda #60
sta $2104 ; sprite 0 Y
lda #$10
sta $2104 ; tile $10 — printable at 4bpp
lda #$30
sta $2104 ; attr: palette 0, priority 3
lda #120
sta $2104 ; sprite 1 X
lda #60
sta $2104 ; sprite 1 Y
lda #$10
sta $2104
lda #$30
sta $2104
rep #$30
.a16
.i16
ldx #$0100
stx $2102 ; the high table
sep #$20
.a8
lda #$08
sta $2104 ; sprite 0 small (16x32), sprite 1 large (32x64)
lda #$02
sta $2133 ; SETINI bit 1: OBJ interlace — the ONE difference from the twin
lda #$10
sta $212C
lda #$0F
sta $2100
rep #$30
.a16
.i16
rts
.endproc

.segment "CATALOG"
.export _scene_count
.export _scene_entries
_scene_count:
.word 53
.word 54
_scene_entries:
.addr scene_c5_mode1_bg_priority
.addr scene_c8_fixed_colour_add
Expand Down Expand Up @@ -2306,3 +2363,4 @@ _scene_entries:
.addr scene_c7_hflip_sliver_order
.addr scene_c8_force_black_outside_window
.addr scene_c5_4bpp_bitplane_order
.addr scene_c7_obj_interlace_halves_height
Binary file modified tests/roms/AccuracySNES/build/accuracysnes-pal.sfc
Binary file not shown.
Binary file modified tests/roms/AccuracySNES/build/accuracysnes.sfc
Binary file not shown.
1 change: 1 addition & 0 deletions tests/roms/AccuracySNES/build/scenes.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,4 @@
51 c7-hflip-sliver-order C7.03
52 c8-force-black-outside-window C8.12
53 c5-4bpp-bitplane-order C5.14
54 c7-obj-interlace-halves-height C7.12
53 changes: 53 additions & 0 deletions tests/roms/AccuracySNES/gen/src/scenes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1892,6 +1892,59 @@ pub const SCENES: &[Scene] = &[
"sta $2100 ; brightness 15, forced blank off",
],
},
Scene {
id: "c7-obj-interlace-halves-height",
dossier: "C7.12",
what: "`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.",
setup: &[
"sep #$20",
"stz $2105 ; BGMODE 0",
"jsr scene_oam_reset",
"sep #$20",
"lda #$C0",
"sta $2101 ; OBJSEL pair 6, name base word $0000 — same as the twin",
"rep #$30",
"ldx #$0000",
"stx $2102",
"sep #$20",
"lda #40",
"sta $2104 ; sprite 0 X",
"lda #60",
"sta $2104 ; sprite 0 Y",
"lda #$10",
"sta $2104 ; tile $10 — printable at 4bpp",
"lda #$30",
"sta $2104 ; attr: palette 0, priority 3",
"lda #120",
"sta $2104 ; sprite 1 X",
"lda #60",
"sta $2104 ; sprite 1 Y",
"lda #$10",
"sta $2104",
"lda #$30",
"sta $2104",
"rep #$30",
"ldx #$0100",
"stx $2102 ; the high table",
"sep #$20",
"lda #$08",
"sta $2104 ; sprite 0 small (16x32), sprite 1 large (32x64)",
"lda #$02",
"sta $2133 ; SETINI bit 1: OBJ interlace — the ONE difference from the twin",
"lda #$10",
"sta $212C",
"lda #$0F",
"sta $2100",
],
},
];

/// The comment block `scene_low_tiles` carries, split out only to keep `low_tiles_helper` inside
Expand Down
Loading