Skip to content

RustyNES v2.6.17 — "Terminus" (a write lands where the cycle ENDS, and this core does not move to meet it)

Choose a tag to compare

@github-actions github-actions released this 11 Sep 21:47
3f15648

A 6502 commits a write at phi2, the last of a CPU cycle's three PPU dots. This core applies PPU register writes at M2-low, the first, because Cpu::start_cycle catches the PPU up before the bus access. That divergence has been in the scheduler since v2.0.0 and nothing had ever measured it, because no test in the corpus states which dot it writes on — until this release's upstream re-sync brought one that does.

It is measured now, on two independent $2001 writes: the ROM names dots 242 and 256, this core applies 240 and 254. Both exactly two dots early. The move was then made, measured against a first-difference control captured beforehand, and not adopted.

rustynes-ppu and rustynes-cpu both change, so AccuracyCoin 143/144 (99.31%, RAM decoder) and nestest 0-diff are VERIFIED, not asserted.

The battery grew, and a re-sync is not an append

The AccuracyCoin oracle re-syncs to upstream 69c8860 and TriCNES to f388af0. The catalog goes 146 → 149 rows across 20 → 22 suites, and assigned tests 141 → 144.

The trap is in the two new pages. Advanced Background Evaluation and Advanced Sprite Evaluation re-home eleven existing PPU tests out of PPU Misc., PPU Behavior and Sprite Evaluation. A suite-keyed baseline therefore has to be regenerated rather than extended — appending three rows to the old one produces a file that parses, matches on count, and attributes eleven results to suites they no longer belong to.

scripts/accuracycoin-build/extract_catalog.py now derives SOURCE_CATALOG.tsv from AccuracyCoin.asm instead of it being hand-transcribed. Validating it against the asm reproduced the committed 146-row file byte-for-byte except one row — a hand-extraction error that had been in the tree since the v2.0.1 re-sync, four months.

The gate pins the failing SET, not "zero failing"

accuracycoin.rs gains KNOWN_FAILING, compared as a set, failing in both directions: a new failure is caught because it is absent from the list, a fix is caught because it is present and no longer failing, and a swap is caught because the set differs while the count does not. A one-directional allowance hides exactly the coverage it was written to tolerate — that is v2.6.9's lesson, and both directions are demonstrated by mutation.

Two of the three new tests close, and one exposes a coincidence

OAM2Address becomes a live counter maintained across sprite fetch, instead of an index derived positionally from the dot. A derived index cannot represent an address that fell behind, so an interval of rendering-disabled time during fetch was invisible to it.

The "OAM2 Overflowed" flag turns out to be load-bearing for the counter and not only for the test named after it: advancing on even dots yields 33 candidate increments across dots 256-320, and it is the flag — raised when the 32nd wraps $1F → 0 — that suppresses the 33rd and leaves the address resting at 0. The documented "$2004 during dots 321-340 reads OAM2[0]" is not a special case but the ordinary end state of the counter. One mechanism, both tests.

Misaligned OAM2 Address had been passing because two errors cancelled. AccuracyCoin's README documents an increment on dot 321 that this core did not model; a second error hid it. Both are now fixed independently, so it passes for the reason it should rather than by coincidence — found only because the README was read after the test already passed.

SAVE-STATE EPOCH — PPU_SNAPSHOT_VERSION 8 → 9

The three new OAM2 fields are live PPU state, so they are serialized rather than allowlisted out. The .rns container compares section version for equality, so a pre-v9 save state no longer loads — an approved maintainer decision, and the failure is a clear error rather than a silent misread of stale bytes.

The write placement: measured, moved, and left where it stands

combination AccuracyCoin independent oracle
shipped (M2-low + the 2-stage compensation) 143/144 clean
phi2, write alone 141/144 ppu_vbl_nmi/10-even_odd_timing FAILS 09
phi2 + dot-321 + skip(1) 142/144 clean

The distinction that decided it: the six framebuffer goldens the move shifts are baselines and would legitimately be re-blessed if phi2 were right, but 10-even_odd_timing is a third-party ROM with its own verdict, and it went pass → 09.

Re-deriving that ROM under phi2 from its own statement of what it measures closed that regression — and in doing so proved mask_for_skip_check is a compensation for the placement, which its own comment had already claimed: under phi2 it needs one delay stage rather than two, producing the identical 08 08 09 07.

Even so the best combination is net −1 against what ships, and costs a save-state epoch plus six re-baselines. Two of the three dependent behaviours are still un-re-derived, and adopting a mechanism while its dependants still compensate for the old one replaces a documented compensation with an undocumented one. So the compensation stays.

The diagnosis is not retracted — it is the release's main finding, rowed in docs/accuracy-ledger.md as a known and deliberately uncorrected divergence, and stated at the site in cpu.rs so a future reader learns it is deliberate rather than inferring an oversight. The apparatus is kept: phi2-write-sweep is default-off and const fn when the feature is absent, so the shipped build is byte-identical by construction.

The measurement that was wrong, and how

The first attempt at costing the move reported "only AccuracyCoin moves". Plain cargo test stops at the first failing binary, so AccuracyCoin failed and nestest, blargg and everything after it never ran. With --no-fail-fast the real picture is 83 binaries ok, 4 failed, 10 failing tests — including the independent-oracle regression that decided the release.

One core fix nothing in the corpus could see

AccuracyCoin's README gives two rules for advancing OAMADDR past an out-of-range sprite: secondary OAM full is +5, not full is +4 then & $FC — which clears the byte index. The second was not implemented.

Both are invisible while OAMADDR is a multiple of four, so only misaligned OAM observes them. Instrumented, that case is reached 114 times per battery run out of 56,953,944 out-of-range branches, and the battery reads 143/144 either way — including AccuracyCoin's own Misaligned OAM behavior, which exists for this area. The rule was therefore recorded and not adopted when it was first checked, because this project's bar is a test that fails without the change.

It is adopted here because that test now exists: misaligned_oam_out_of_range_advance_follows_both_rules drives one y-test from a misaligned OAMADDR past an out-of-range Y and asserts the resulting address for both rules. Against the unmodified code it fails — left: (2, 1), right: (2, 0).

terminus_control.rs, kept

Built to answer "did the experiment change, or did the subject?", it chains a rolling FNV-1a over the pre-palette framebuffer and work RAM per frame across three workloads at 400 frames on the default feature set. It outlives the experiment, because the first divergence must be the cycle you aimed at, or something else moved.

Also

  • TriCNES re-vendored from f388af0 (MIT, in-repo by deliberate exception), carrying the OAM2-address and OAM-evaluation fixes matching AccuracyCoin's new page. The instrumented cross-diff harness was carried across by a 3-way merge against the exact vendored base commit, with all eight instrumentation markers verified present at identical counts.
  • The tricnes-harness ROM path had pointed at a pre-reorg directory that no longer exists.
  • docs/accuracy-ledger.md's headline still read "AccuracyCoin 141/141 (100.00%)" — true before the re-sync and false after it.
  • The planned v2.6.18 is folded into this release. That split existed because the write-placement work was to be a scheduler change with its own ADR and a full re-baseline; the refutation means it is not.

What this release does NOT establish

Frozen OAM2 Increment remains the single failing AccuracyCoin entry, pinned by name so it cannot silently become two. Its cause is named — the write placement above — and the phi2 combination closes it, at the cost of one other test plus an epoch. Reopening that question needs three things first, listed in the plan's CLOSED section: the two remaining dependants re-derived from their own ROMs, a decision on reads (which this work explicitly did not settle, having measured writes only), and the sibling's co-simulation goldens re-exported from a pinned oracle commit.

The sibling's co-simulation gates are not re-opened by this release: RustyNES_MiSTer/tb/ORACLE_COMMIT pins the oracle at 957a63af (the v2.6.14 cut), so its goldens predate every PPU change here. That is a deferral rather than an absence — the OAM2 counter, the frozen fetch and the misaligned-OAM realignment all alter behaviour those goldens capture, so the next bump of that pin should move them, and the first divergence must be attributable to one of the three.