From 56a3a80a229dbe6fe8004ce51149295a1d6b659b Mon Sep 17 00:00:00 2001 From: DoubleGate Date: Sat, 1 Aug 2026 04:42:34 -0400 Subject: [PATCH] feat(accuracysnes): cover C11.03, and retract the interlace-gate claim (T-04-H) 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) --- CHANGELOG.md | 73 ++++++++++++++++++ docs/accuracysnes-coverage.md | 2 +- docs/accuracysnes-plan.md | 61 ++++++++------- tests/golden/accuracysnes-scenes.tsv | 1 + tests/roms/AccuracySNES/asm/scenes.s | 71 +++++++---------- .../AccuracySNES/build/accuracysnes-pal.sfc | Bin 262144 -> 262144 bytes .../roms/AccuracySNES/build/accuracysnes.sfc | Bin 262144 -> 262144 bytes tests/roms/AccuracySNES/build/scenes.tsv | 2 +- tests/roms/AccuracySNES/gen/src/scenes.rs | 71 +++++++---------- 9 files changed, 162 insertions(+), 119 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d98bc940..7590ee85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,79 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **The H-IRQ comparator moves into the clock domain (`T-06-A`), and nothing below the long dots + moves with it.** `HIRQ_TRIGGER_DELAY = 4` was a *dot-domain rounding* of ares' + `hcounter(10) == (HTIME+1)<<2` — exact only while every dot is four clocks, which stopped being + true when dots 323 and 327 became six. The match is now computed where it actually happens: + clock `4·HTIME + 14` (`hirq_match_clock`), mapped to the first dot boundary at or after it + (`hirq_trigger_dot`). + + **Below the long dots the two agree exactly**, because `4·HTIME + 14` is never a multiple of 4 and + the next boundary is `HTIME + 4`. They diverge only for `HTIME` **321..=337**, where the six-clock + dots have displaced every later boundary — the old constant fired up to a whole dot late, and at + `HTIME = 336` suppressed an IRQ that does fire. `HTIME = 337` lands on dot 340's boundary, which + exists only on the long line, so it is honoured there and suppressed elsewhere. + + This is the change the plan recorded as *"attempted and reverted because it moves + `hdmaen_latch_test_2`'s golden"*. It does not, this time: **no framebuffer golden moved**, the + undisbeliever suite passes unchanged, and cross-validation is byte-identical + (`snes9x: OK (14 known)`, `Mesen2: OK (2 known)`). + + **`B4.16` is a weaker guard than its own doc claimed, and that is worth knowing.** Measured either + side of the change, *both* of its readings are unchanged — including the `HTIME = 330` one, whose + trigger dot moved 334 → 333. The CPU takes an IRQ at an instruction boundary, so the handler-entry + dot quantises to the spin loop's instruction length and a one-dot shift is absorbed. `B4.16` can + say "nothing regressed"; it cannot say "the change took effect". A unit test does that, sweeping + every `HTIME` and asserting equality with the old constant below the long dots and strict + inequality above them. + + `LONG_DOTS` and the per-dot clock count also move to `rustysnes-ppu`, which owns the dot model and + now needs the same layout twice. `rustysnes-core`'s scheduler delegates to it rather than keeping + a second copy. + +- **AccuracySNES: the scene protocol publishes on a known field.** `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 + *cart-side* 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, 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. + + **RETRACTION.** This entry first claimed the gate had 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.** Running the *identical ROM* twice under Mesen2 gives `0x16f7ab8c7f97b7f8` then + `0x266241e43ab85064` — exactly the two field-parity outcomes, 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 is + deterministic: it publishes only on its chosen field. What is not pinned is which *rendered frame* + a host associates with the `R_SCENE` value it read — Mesen2's headless runner does not make that + association deterministically. The interlace scene was therefore **withdrawn**, not left + unblessed: a scene reporting a different hash each run is noise in the gate output. The gate stays + — it is the cart-side half, it moves no golden, and it costs only battery runtime — but nothing + exploits it until a host can pin the frame it hashes. `C9.03`/`C9.06` are not written on top of + it for the same reason. + + The lesson is `E8.01`'s, again: a result that depends on a phase nobody controls looks stable + until you run it twice. + +- **AccuracySNES: `C11.03` covered — each `M7x * ORG` product is masked to a multiple of 64 before + accumulation.** A new 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. + + **The coverage total does not move**, and the regenerated report is why: `C11.03` replaces `C7.12` + one-for-one, because the withdrawn interlace scene was being counted as scene-cover for `C7.12` + while it was still unblessed. 350 of 443 either way — what changed is that the row now counted is + one whose golden all three references agree on. + + Blessed at `0xc032679c9076440a`, which **all three** references produce, and non-vacuity confirmed + by removing the mask from `fetch_mode7_column`: this scene's hash moves. Worth recording that two + existing scenes (`c11-mode7-rotate-scale`, `c11-mode7-window`) also move under that injection, so + the mask was already witnessed in aggregate — what this adds is a scene whose stated purpose is + the mask, which is what makes the coverage claim mean something. + - **The H-IRQ comparator moves into the clock domain (`T-06-A`), and nothing below the long dots moves with it.** `HIRQ_TRIGGER_DELAY = 4` was a *dot-domain rounding* of ares' `hcounter(10) == (HTIME+1)<<2` — exact only while every dot is four clocks, which stopped being diff --git a/docs/accuracysnes-coverage.md b/docs/accuracysnes-coverage.md index 115f2601..2fc36c5e 100644 --- a/docs/accuracysnes-coverage.md +++ b/docs/accuracysnes-coverage.md @@ -130,7 +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 +- **`C11.03`** — c11-mode7-product-low-bits-masked ## Tests with no enumerated assertion diff --git a/docs/accuracysnes-plan.md b/docs/accuracysnes-plan.md index 680081ec..e9f23489 100644 --- a/docs/accuracysnes-plan.md +++ b/docs/accuracysnes-plan.md @@ -15,7 +15,7 @@ AccuracySNES closed ticket **T-04**. The follow-on tickets minted here are **T-0 |---|---| | Tests | **338** (scoring + golden vectors + region SKIP per image) — *tests, not assertions; see the note below the table* | | Assertion coverage | **350 of 443** dossier assertions — **297 on-cart** + **53 rendered scenes**, kept as separate columns (`docs/accuracysnes-coverage.md`) | -| Rendered scenes | **53**, all cross-validated (`docs/adr/0013`) | +| Rendered scenes | **54** declared and all cross-validated (`docs/adr/0013`); **53** are the dossier rows they are the only cover for | | Pass rate | **100.00%** on-cart, floor enforced at 1.00 by `tests/accuracysnes.rs` | | 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. | | 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 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.** +**BUILT 2026-08-01 — and then measured properly, which changed the conclusion.** `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. +the host counts is the same *cart-side* 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: +**The first reading of the result was wrong, and it was wrong because it was a single run.** An OBJ +interlace scene was added, the three-way hash split became a two-way one — snes9x and Mesen2 +produced the *identical* `0x266241e43ab85064` — and that was published as "the gate works". It does +not hold. Running the **identical ROM** twice under Mesen2 gives: -| | `c7-obj-interlace-halves-height` | +| run | `c7-obj-interlace-halves-height` under Mesen2 | |---|---| -| 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. +| 1 | `0x16f7ab8c7f97b7f8` | +| 2 | `0x266241e43ab85064` | + +Those are exactly the two field-parity outcomes, and Mesen2 alternates between them **run to run on +one build**. RustySNES and snes9x are each stable; Mesen2 is not. + +**So the gate is necessary but NOT sufficient, and the missing half is host-side.** The cart's own +behaviour is deterministic — it publishes only on its chosen field. What is not pinned is which +*rendered frame* a host associates with the `R_SCENE` value it read: if the host's frame boundary +sits a frame away from the cart's vblank, it hashes frame N while the cart published on frame N−1's +field. Mesen2's headless runner does not make that association deterministically. + +**The scene was therefore withdrawn**, not merely left unblessed: a scene that reports a different +hash on each run is noise in the gate output, and its "the references agree" reading was an artifact +of looking once. The gate itself is kept — it is the cart-side half of the fix, it costs only +battery runtime, and it moves no golden — but nothing exploits it until a host can pin the frame it +hashes. + +**The lesson, and it is the same one `E8.01` taught in the same session:** a result that depends on a +phase nobody controls looks stable until you run it twice. Run an interlace or field-dependent +capture **at least twice per host** before believing any part of it. `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. +parity dependency, so they would land in the identical state. Write them when the tiebreaker exists. ### `E8.03` — "clears `ENDX` even when suppressed" does not mean suppressed by `KOFF` diff --git a/tests/golden/accuracysnes-scenes.tsv b/tests/golden/accuracysnes-scenes.tsv index 0f45767b..0914903c 100644 --- a/tests/golden/accuracysnes-scenes.tsv +++ b/tests/golden/accuracysnes-scenes.tsv @@ -201,3 +201,4 @@ c7-name-select-picks-second-table 0x11e9afd7545c0b25 c7-hflip-sliver-order 0x863f085bccebd107 c8-force-black-outside-window 0x21a5b8a828e748d7 c5-4bpp-bitplane-order 0xf11def7ad73be325 +c11-mode7-product-low-bits-masked 0xc032679c9076440a diff --git a/tests/roms/AccuracySNES/asm/scenes.s b/tests/roms/AccuracySNES/asm/scenes.s index 3d92369e..c2ea5cd2 100644 --- a/tests/roms/AccuracySNES/asm/scenes.s +++ b/tests/roms/AccuracySNES/asm/scenes.s @@ -2247,55 +2247,38 @@ 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 +; c11-mode7-product-low-bits-masked — C11.03 +; 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. +.proc scene_c11_mode7_product_low_bits_masked .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 + lda #$07 + sta $2105 ; BGMODE 7 + jsr scene_mode7_vram 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 + stz $211A ; M7SEL: no repeat, no flips + lda #$01 + sta $211B + sta $211B ; M7A = $0101 — the low byte is what makes the mask observable + sta $211C + sta $211C ; M7B = $0101, so the discarded part is line MOD 64 + stz $211D + stz $211D ; M7C = 0 + stz $211E + sta $211E ; M7D = $0100 + stz $211F + stz $211F ; M7X = 0 + stz $2120 + stz $2120 ; M7Y = 0 + stz $210D + stz $210D ; M7HOFS = 0 + stz $210E + stz $210E ; M7VOFS = 0 + lda #$01 + sta $212C ; BG1 on the main screen lda #$0F sta $2100 rep #$30 @@ -2363,4 +2346,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 + .addr scene_c11_mode7_product_low_bits_masked diff --git a/tests/roms/AccuracySNES/build/accuracysnes-pal.sfc b/tests/roms/AccuracySNES/build/accuracysnes-pal.sfc index 8da1f043ffe30293909e97892972fda4f59c9233..9a4f50a1ca25fc9cf2a61ce8573d0d4dc07e8a11 100644 GIT binary patch delta 262 zcmZo@5NK!+*l>nj_{&2D^>&3{?dzEy)JZ8Qbu9SxaPxikct&B4M?i^33M)DKjULn; zGH6IzAT&9UQ^NQkNCQxu0V2Ng|Nnlz2Xzew3wWU#cu*z87cg(0!|A}nXuSC@uMD%Q zI7k`L!bKu13UAvN$V0j7tsc~A{%3$XVX~Z{f{+SKit9ri)MWvJx&f0NVz~I{NGYyl z?3GpoAsNNq$@WoFd~FnjnB$Rxdb`4}_Vr8;>ZBBuIu`If+I*iqo>5o^B=JaLB}c!}gStZo z4M_{sCI@m#Ob*}_moZqN2$ki5$gceVzn||xU4!@n@y&BM9atFMH{a!zVOG_Kn!ZSc zMd5Ax0%MqERuAel|1&`Cn=B`&AY=iP;`&erbw+@oZouTd@e-4zA1l9Ssnjh4+Kd5H}Vy5j6>X{W906c0}O#lD@ diff --git a/tests/roms/AccuracySNES/build/accuracysnes.sfc b/tests/roms/AccuracySNES/build/accuracysnes.sfc index 0a8463d867e3bcee1e5d1283efae08bc5ade06d6..733b944ed04b0aacff4735d7a986941a810a94c3 100644 GIT binary patch delta 262 zcmZo@5NK!+*l>nj_{&2D^>&3{?dzEy)JZ8Qbu9SxaPxikct&B4M?i^33M)DKjULn; zGH6IzAT&9UQ^NQkNCQxu0V2Ng|Nnlz2Xzew3wWU#cu*z87cg(0!|A}nXuSC@uMD%Q zI7k`L!bKu13UAvN$V0j7tsc~A{%3$XVX~Z{f{+SKit9ri)MWvJx&f0NVz~I{NGYyl z?3GpoAsNNq$@WoFd~FnjnB$Rxdb`4}_Vr8;>ZBBuIu`If+I*iqo>5o^B=JaLB}c!}gStZo z4M_{sCI@m#Ob*}_moZqN2$ki5$gceVzn||xU4!@n@y&BM9atFMH{a!zVOG_Kn!ZSc zMd5Ax0%MqERuAel|1&`Cn=B`&AY=iP;`&erbw+@oZouTd@e-4zAMAdn>njh4+Kd5H}Vy5j6>X{W906c70O#lD@ diff --git a/tests/roms/AccuracySNES/build/scenes.tsv b/tests/roms/AccuracySNES/build/scenes.tsv index 1a163acf..7c4a9d58 100644 --- a/tests/roms/AccuracySNES/build/scenes.tsv +++ b/tests/roms/AccuracySNES/build/scenes.tsv @@ -52,4 +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 +54 c11-mode7-product-low-bits-masked C11.03 diff --git a/tests/roms/AccuracySNES/gen/src/scenes.rs b/tests/roms/AccuracySNES/gen/src/scenes.rs index 2857ea1d..cdfaee01 100644 --- a/tests/roms/AccuracySNES/gen/src/scenes.rs +++ b/tests/roms/AccuracySNES/gen/src/scenes.rs @@ -1893,54 +1893,35 @@ pub const SCENES: &[Scene] = &[ ], }, 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.", + id: "c11-mode7-product-low-bits-masked", + dossier: "C11.03", + what: "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.", 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", + "lda #$07", + "sta $2105 ; BGMODE 7", + "jsr scene_mode7_vram", "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", + "stz $211A ; M7SEL: no repeat, no flips", + "lda #$01", + "sta $211B", + "sta $211B ; M7A = $0101 — the low byte is what makes the mask observable", + "sta $211C", + "sta $211C ; M7B = $0101, so the discarded part is line MOD 64", + "stz $211D", + "stz $211D ; M7C = 0", + "stz $211E", + "sta $211E ; M7D = $0100", + "stz $211F", + "stz $211F ; M7X = 0", + "stz $2120", + "stz $2120 ; M7Y = 0", + "stz $210D", + "stz $210D ; M7HOFS = 0", + "stz $210E", + "stz $210E ; M7VOFS = 0", + "lda #$01", + "sta $212C ; BG1 on the main screen", "lda #$0F", "sta $2100", ],