Skip to content

RustyNES v2.6.22 — "Rigging" (the instruments for the board, built before the board)

Latest

Choose a tag to compare

@github-actions github-actions released this 19 Sep 21:54
8ac3576

v2.7.0 "Shakedown" is the session with the board. This is not it. A SuperStation One is in hand and no hardware has run any bitstream — that sentence is still true, and every anchor asserting it is deliberately untouched. v2.6.22 is the non-hardware half of the Shakedown plan, cut separately on a maintainer decision so that v2.7.0 keeps meaning what its name says.

The emulation core is unchanged, so AccuracyCoin 144/144 and nestest 0-diff hold by construction, and were re-run anyway. The co-simulation ladder's AccuracyCoin gate was re-run in full against re-exported goldens: 144 of 144 scored entries identical entry for entry over 134,012,760 cycles.

Three of the four findings below were already written down somewhere in this repository and enforced nowhere.

AccuracyCoin reads back from hardware as bytes

docs/bringup.md had stated the limit exactly: "a photograph is not 149 status bytes, and reading one is a human transcribing a picture." AccuracyCoin's result vector lives in CPU RAM at $0300-$04FF, outside the $6000-$7FFF window the save path persists, so none of it reached .sav.

scripts/accuracycoin-build/build_mirror_rom.py patches upstream's ROM to copy that window to $6000-$61FF once the battery finishes, and sets the iNES battery bit — the one bit that both creates the PRG-RAM window (emu.sv:609) and arms the save controller (emu.sv:353), read out of the RTL rather than assumed.

The patch moves nothing, and the builder asserts it. AccuracyCoin is full of cycle-exact tests sensitive to page crossings in their own code, so an insertion displacing later routines could flip a verdict for reasons unrelated to the console — and the flipped verdict would look exactly like a result. The diff is 1 header byte, 5 at the call site (LDA #0 / STA $4015JSR + two NOPs), 29 in bank 2's end-of-bank padding, and 0 anywhere else. Any other budget and the build is refused.

The byte budget is not the control. A budget proves nothing moved; it says nothing about answers. accuracycoin_mirror.rs runs both ROMs through the same driver the shipped gate uses and requires the window byte-identical, the vector identical entry for entry, and the mirror to reproduce the live window without being vacuous — then runs the real comparator over a real save file, because the conjunction of two separately verified halves is a third claim.

Three mutations, all CAUGHT: half-copy (144/512 bytes differ), inert routine (148/512), cleared battery bit (budget 34 ≠ 35).

That last one is a finding in itself: simulation cannot see the battery bit at all, because the oracle's NROM hands out PRG-RAM regardless of the header — a known oracle-vs-board divergence on record since v2.6.3. It is checked structurally or not at all, and the assertion says so at the site.

The catalog has 149 rows and 144 results

Upstream defines it with the reason attached:

result_DrawTest = $03FF   ; page 3 omits the test from the all-test-result-table.

and AutomaticallyRunEveryTestInROM acts on it, branching past any test whose result pointer lands on page 3. All five Power On State rows — PPU Reset Flag, CPU RAM, CPU Registers, PPU RAM, Palette RAM — share that one sentinel.

The sharing was already documented, on CatalogEntry::result_addr's own rustdoc, and acted on nowhere. So every consumer counted the five as results, and the headline became a function of when the run was sampled:

window decoded
4500 frames pass_with_code=16, not_run=0 → reads as 149 of 149
6600 frames pass_with_code=11, not_run=5 → reads as 144 of 144

One ROM, nothing wrong in between, because $03FF is live scratch the results-page renderer writes and later abandons. Both numbers were already in this project's records — rung 5 said "149 of 149", the shipped gate said "144 of 144" — and nobody had reconciled them.

RESULT_DRAW_TEST, is_scored, scored_len and scored exclude it in one place, so a consumer cannot get the zip right and the filter wrong. All three windows now report the same thing, and EXPECTED_PASS_COUNT never moved: 144/144, 100.00%, fail=0. The count did not change; it stopped depending on the sampling instant, and five rows left every "entry for entry" claim that were never entries.

Five documents said 149 where they meant executed entries — including the sibling README's headline Status line. Review flagged two of them; the tree had five.

rom_sha256 was written into every golden manifest and compared to nothing

nes_golden_export has recorded the ROM's digest in every manifest since the ladder existed. Nothing ever checked it, and it had already drifted: the 46199ae4 AccuracyCoin re-sync moved the vendored ROM and rebuilt two sub-test ROMs without re-exporting the goldens. Three of them went on naming a hash for a ROM no longer in the tree — one pointing at a build-cache directory since deleted.

Every gate stayed green, correctly. The re-sync's own prediction that it "changes no verdict" holds, and this release measured it rather than quoting it: the vector is identical entry for entry, and the RAM difference is three bytes per golden, all outside the catalog — zero-page scratch and stack, +2 each. In frozen-oam2-increment one of the three is $0010, which the assembly calls ErrorCode, going $03 → $04: the upstream fix itself, visible in the golden.

So the goldens were not wrong. They were unattributable — a later difference could not have been told apart from a corpus change, and nothing in the tree could say which. That is the quieter failure.

RustyNES_MiSTer/tb/check_golden_provenance.py compares it now, as rung 0 of regress.sh, failing closed on STALE, on UNRESOLVED, and on nothing-examined. All three goldens re-exported; both affected sub-test gates re-run and report both Pass.

Widening the resolver turned a skip into a finding. The first run reported 3 STALE and 20 UNRESOLVED; most of the twenty were the checker looking in the wrong place, since a manifest's rom is relative to the exporter's working directory. Handling that took UNRESOLVED 20 → 1 and exposed a third STALE golden hiding behind an unfindable path. A skip count is a place findings hide — which regress.sh already said in its own comments.

run_battery.sh would have reported the mirror ROM as pass

It read sav[0] as a blargg verdict for any ROM whatsoever. The mirror ROM's save holds a 512-byte status vector at $6000, and a real one begins 00 00 00 00 — so a byte of $00 would have scored the entire 149-entry battery as a pass. A confident verdict on a question nobody asked is the worst output an unattended runner can produce, and it looks exactly like a result.

The verdict is now licensed by blargg's own $DE $B0 $61 signature at $6001-$6003; a fresh save without it reports vector-not-verdict and is adjudicated by the entry-for-entry comparator instead. Measured on a real mirror save and on synthetic blargg pass/fail blocks, since the console is not here to test it on.

The bring-up evidence sheet

RustyNES_MiSTer/docs/bringup-log.md is the instrument the v2.7.0 gate demands, written before the measurement: one row per bring-up step, the four properties no gate can reach, Tiers 1–4, and the two board-gated tickets — each with a named artefact column and a verdict. Every cell reads NOT RUN, which is the accurate current state.

It carries the rule in its own header: "runs on real hardware" is not a pass criterion, because a board that reaches the menu and shows black satisfies it — and a row whose artefact column is empty is NOT RUN whatever the verdict says.

The submission case stops quoting a stale number

docs/submission-case.md asserted the incumbent scores 121/125 on AccuracyCoin. That figure is pre-2026-09-15, and the incumbent published a burst of accuracy work on 2026-09-15/16. It is withdrawn rather than updated: the document now makes no comparative accuracy claim at all until the bring-up log's F1 row carries a measurement of the current incumbent on the same corpus. The README's copy had been corrected at v2.6.21; this one was missed.

It also now states the feature delta in a table rather than leaving a reviewer to find it: save states, cheats, palettes, Four Score, Zapper, PAL, FDS and expansion audio against six mapper families and battery saves. On features the incumbent wins outright, and the document says so.

Also in this release

  • A full-suite run reported exited with code 0 while a test had failed. The command was piped through tail -60, so the exit status was the pipe's and the log was truncated to 48 lines — "no failures visible" was a statement about the tail, not the suite. The failure was real: summarise now pairs positionally against the catalog, and a fixture passed a seven-element array that described no particular rows. Fixed, plus a test for the refusal. Re-run writing to a file with the exit code recorded explicitly: 2,603 passed, 0 failed.
  • Two carried-forward items in the v2.7.0 plan were already closed when the plan listed them — the corpus re-sync and VERSION-PLAN.md's row order, both done in #528. They came from CLAUDE.local.md's "Open" section, which had not been updated after that merge: a note copied forward becoming a fact.
  • A --verify-budget flag named in the mirror builder's own docstring does not exist. The budget check is unconditional with no opt-out — stronger than the sentence claimed, but a reader following it would have hunted for nothing. A citation nobody can run is worse than the bare statement it replaced.
  • All fifteen release anchors moved together, and the audit caught three things a blanket replace would have got wrong: the release-line chains skipped v2.6.21, ten anchors named the new version beside the old codename, and a chain's tail and its "current release" label are one claim.
  • Three standing rules added to AGENTS.md, each for a finding above.

No hardware has run any bitstream. That is v2.7.0.