Skip to content

RustyNES v2.6.18 — "Errata" (the recorded cause was wrong in three ways, and the last AccuracyCoin entry closes)

Latest

Choose a tag to compare

@github-actions github-actions released this 12 Sep 13:49
57e7ebc

Advanced Sprite Evaluation :: Frozen OAM2 Increment has been the single failing entry in the accuracy battery since the 2026-09 upstream re-sync. It closes here, and AccuracyCoin reads 144/144 (100.00%, RAM decoder) with fail=0KNOWN_FAILING is now empty.

The interesting part is not the fix. It is that every claim this project had recorded about the entry was wrong, and each wrong claim had already cost a refuted attempt.

Three retractions

"A $2001 enable on dot 256 takes effect a dot early." Measured per dot against the four writes the ROM names — 242, 256, 325 and 340 — all four take effect during dot N-1, so the new mask is in force from the start of dot N: exactly where the ROM says. Three independent instances of the 256 write agree, and the offset is constant, so there is no alignment dependence either. ppu-state-trace records are END-OF-DOT, and reading a transition at N-1 as "early" is the trap that produced the claim.

The failing sub-test was 4, not 2 or 3. TEST_FrozenOAM2Inc has no INC <ErrorCode between tests 3 and 4, so both report $0E and the code cannot distinguish them. Test 3 passes. This is the same one-line upstream defect that was fixed for Misaligned OAM2 Address on 2026-09-11 and is still present here.

The OAM2 freeze machinery was never broken. Instrumented per dot it is byte-identical to the passing case — addr=0, ovf=1, frozen=1 across the whole fetch window. The earlier end-to-end verification ("raised 809 times, reaching sprite fetch exactly once") was accurate, and was measuring something that was not the defect.

One rule, two dots

A mask change during dot N must not act on dot N.

Dot 256, the vertical increment. The ROM states the rule outright: "Rendering is enabled on dot 256, but the PPU's vertical scroll is NOT incremented." That sentence only parses if the enable is on dot 256. The increment now reads rendering_enabled_delayed2 — rendering as of two dots ago — and is hoisted out of the shared render_line && rendering_gate block. Conjoining the two would put the DISABLE edge back on the shallower gate, and that edge is precisely what test 4 measures.

Dot 339, the OAM2 address reset. The counter's gate conjoined the live mask, putting its disable edge a dot ahead of every other consumer's — min(r, d) for a conjunction of two delayed views. A disable whose effect lands during dot 339 therefore skipped that dot's reset, left the "OAM2 Overflowed" flag raised, and produced exactly the sprite-zero hit that test 4 exists to forbid. The counter now rides the shared one-dot gate, which its enclosing block already applies.

The depth is derived, not fitted: the ROM over-determines it (the dot-256 increment must not fire, the dot-257 latch must), giving d = 257 - 255 = 2. Both neighbours fail — one dot shallower is byte-identical to the unfixed build, one dot deeper breaks test 2.

Why it stayed open

SCROLL_GATE_LAG already existed and was recorded as refuted. It had been swept against the battery verdict, which cannot separate "the knob is inert" from "the knob worked and something else still fails" — and it indexes sweep_mask_history[depth - 1], so the derived two dots of deferral is depth 3 while depth 2, the cell the sweep treated as the derived one, is byte-identical to shipped. Sweeping the knob against the increment itself separated those in a single run.

Mutations, including one that is not caught

Three CAUGHT: the dot-256 gate one dot shallower (Fail(2)); the OAM2 counter conjoining the live mask again (Fail(3)); stage 2 read from the field after the tick-end update rather than the top-of-tick local (Fail(2)).

One NOT CAUGHT, and disclosed rather than papered over: deleting rendering_enabled_delayed2 from the fast-path dispatch guard survives the byte-identity corpus, the full battery, and a twelve-alignment test added specifically for it. Writes land on CPU-cycle boundaries, so reachable effect dots are three apart, and no stimulus in the tree lands one at 254→255. The term is kept and the NOT CAUGHT is documented at the site: it can only make the fast path be taken less often, so its presence cannot cause a divergence, while its absence is a latent one waiting for a ROM that writes there.

Save-state epoch

PPU_SNAPSHOT_VERSION 9 → 10, carrying stage 2 of the rendering gate. It looks derivable from stage 1 and is not: the two differ for exactly the one dot after a $2001 rendering edge, and that dot is the whole subject. Run-ahead snapshots and restores every frame. Per the container's version-exact rule, pre-v10 .rns states no longer load.

Verification

AccuracyCoin 144/144 (RAM decoder) · nestest 0-diff · fast_dotloop_diff 5/5 byte-identical · accuracycoin_runahead · snapshot_schema_audit · release_anchor_audit 13/13 · cosim_manifest_audit · libretro_info_audit · full workspace suite with test-roms: 145 results, 0 failed. cargo fmt, workspace clippy plus seven feature-combo clippy runs, rustdoc with warnings denied, and the thumbv7em-none-eabihf no_std build all clean.

rustynes-ppu changes on the default path, so the accuracy figures are verified, not asserted.

Not established here

No sibling RTL changed, and no hardware has run any bitstream. The co-simulation goldens are recorded debt: the emulation core moved this release, so they are no longer a no-op re-export and must be regenerated from a pinned oracle commit at the next ORACLE_COMMIT bump.