RustyNES v2.6.2 — "Witness" (rung 4 closes — blargg's APU battery as the first independent oracle since rung 1, six defects no self-written gate could see, and a suite that had been asserting nothing for five minor releases. The emulation core is unchanged)
RustyNES v2.6.2 — "Witness"
Rung 4 closes. blargg's blargg_apu_2005.07.30 battery — a third-party corpus tested on real hardware — is the first rung-4 evidence with an independent oracle, and it went from 0 of 11 to 11 of 11 exact against the MiSTer co-simulation DUT. 48 gates green, 0 failed. 56 of 56 mutations CAUGHT, 0 NOT CAUGHT, 0 BUILD-FAILED, 0 PATCH-FAILED, across 25 arms with all 25 baselines passing.
The emulation core is unchanged. Only rustynes-test-harness and documentation are touched on this side, so AccuracyCoin 141/141 (RAM decoder) and nestest 0-diff hold by construction — and both were run anyway.
Every one of the six defects was invisible to the gates written here
That is the point of an independent oracle, and it is why this release exists. Every rung-4 ROM written for this project writes $4017 once during setup, long before its measurement window — so a frame IRQ raised spuriously by that write fired where nothing was looking. blargg's 03.irq_flag writes $4017 repeatedly and reads $4015 immediately afterwards, which is what a ROM written to probe hardware does and what a ROM written to confirm a model does not. Its first divergence forked the run for 678,517 further cycles from one bit.
The framing error that cost the most, and the proof that ended it
An intermediate pass reached 3 of 11 and recorded four changes that each "bought a blargg ROM by breaking a gate", concluding the two surfaces were in tension.
They were not, and a proof was available the whole time: the oracle passes blargg 11/11 and generates every golden those gates compare against, so one model satisfies both. A change that fixes a blargg ROM and breaks a gate is therefore a bug report about the change, not a compromise. All four dissolved.
The half-rate frame counter was also provably incapable, and the arithmetic is short. With the reset landing at L, a half-frame fires at L + 14910 + d (d ∈ {0,1}) against the oracle's L₀ + 14913, while the IRQ fires at L + 29828 + d′ against L₀ + 29828. Both hold only if d′ = d − 3, which is impossible. That is why the constants had been calibrated per step — and it is what the model's own comment had been saying about itself: "a model whose constants are calibrated per step is a model whose origin is slightly wrong somewhere else."
The sequencer counts CPU cycles now
The wiki's step table is indexed in APU cycles and carries a GET/PUT column, and that column is the missing half: an APU cycle is two CPU cycles, so "3728, PUT" is CPU 2·3728 + 1 = 7457 exactly. Every entry resolves the same way in both modes — no rounding, no per-step fudge — and the counter widens to 16 bits because the 5-step wrap is 37,282.
The six defects
- A
$4017reset counted as a sequence wrap. The wiki's third frame-IRQ set point is written "0 (14915), GET" because the counter has wrapped, andfc_count == 0is equally true after a reset. - The reset landed one CPU cycle late — register latency, not the documented number. A counter that assigns on reaching 1 makes the zero visible the next cycle. Derived rather than swept; one line took the battery 3 of 11 to 7 of 11, with four ROMs closing together.
- A
$4015read landing on a length clock did not see it. - The sequence had 29,831 states, not 29,830 — a drift, not an offset, and the tell was that the divergence runs lengthened 1, 2, 3 cycles.
- The length halt applied one cycle early.
- The mode-1 immediate quarter+half fired at the write rather than the reset landing — calibrated against a reset that itself landed late, two errors that cancelled.
Two rules the nesdev wiki does not carry
APU_Length_Counter.xhtml says nothing about either. blargg's own readme.txt, shipped beside the ROMs in this repository's tree the whole time, states both:
"Write to halt flag is delayed by one clock" "Length reload is completely ignored if written during length clocking and length counter is non-zero before clocking"
Both were derived from the oracle's code first. The primary source had them.
Three items open at the battery's close
- The power-up
$4017state — closed.apupower041is the only ROM here that never writes$4017, which is why the state had been invisible by construction, not by measurement. The DUT's first frame IRQ rose two cycles late. - The length-reload drop — closed, and it was dead code. A walking stimulus provably cannot resolve one cycle inside a 7,457-cycle period, so
apureload042places its$4003write on CPU 29,827. With the write placed, disabling the drop still changed nothing — the promote block ran ~90 lines above the half-frame clock under a comment claiming it ran after it, so the clock overrode every reload. That ordering was accidentally correct, which is exactly why nothing caught it. - The power-on divider phase — still open, now for a measured reason. Flipping the oracle's own phase leaves the battery at 11/11, so the strongest independent oracle available is provably indifferent to it. blargg's readme says why: it is random on silicon and the ROMs tolerate both. Closing it needs hardware.
Oracle side: a suite that had been asserting nothing
blargg_apu_2005.rs read the $6000 status protocol and asserted status == 0. These ROMs are plain NROM with no PRG-RAM, so $6000 is unmapped and reads back 0 — blargg's success code. All eleven assertions were statements about an unmapped address.
The identical defect was found and fixed for the PAL half of the same corpus in v2.1.5, whose header has called it "a false oracle that validated nothing" ever since. The NTSC half was never migrated: the fix covered one of two files.
The obvious repair is also wrong. These ROMs never print PASSED or FAILED — their tests.txt says "a result code of 1 always indicates that all tests were passed" — so the screen decoder returns Unresolved for all eleven while the screen plainly reads $01. A third reader, run_nes_result_code, decodes it.
The figure is unchanged at 11/11. It is now earned, and demonstrated to fail: moving the half-frame clock is caught by two ROMs and the frame IRQ by seven. Moving the quarter clock is not caught, and correctly so — tests.txt states the corpus does not test envelope, sweep or linear-counter clocking.
Surfaced, recorded, and deliberately not chased
The same false oracle is why tests/roms/extra/apu had been dismissed as audio-only. Re-read with the screen decoder, ten of its nineteen ROMs report a verdict, and four report TEST FAILED. They are a distinct corpus — their hashes differ from every ROM in the suite that passes 8/8 — and they carry no title text, so what each tests is not yet known.
These are oracle-side accuracy findings rather than rung-4 ones, and fixing four APU defects is its own work with its own risk to the 141/141 and byte-identity guarantees. Recorded with its measurement: the emulator has four failing APU test ROMs that no gate has ever seen.
Verification
- 48 gates green, 0 failed (rungs 1–4); 56 of 56 mutations CAUGHT
- AccuracyCoin 141/141 (100.00%, RAM decoder), nestest 0-diff
blargg_apu_200512/12,pal_apu_tests10/10,f2_accuracy_audit4/4- fmt, clippy, rustdoc (warnings-as-errors), markdownlint: clean
- sibling
subset,lint,selftest,boot-selftest: clean