RustyNES v2.6.1 — "Interleave" (the DMC and its DMA cycle steal in the MiSTer co-simulation DUT, cycle-exact on the bus. The emulation core is unchanged)
v2.6.1 "Interleave"
The DMC and its DMA cycle steal in the MiSTer co-simulation DUT, cycle-exact on the bus. The emulation core is unchanged.
The DMC reads its own samples by stopping the CPU and taking a cycle. This release implements that in the SystemVerilog DUT and proves it cycle-exact against the oracle's per-cycle bus — which is the criterion the plan set for this step, and the reason the channel gate alone was not enough.
The gate that mattered
v2.6.1's stated criterion is "cycle-exact CPU stall behaviour", and that is the bus gate, not the channel gate. The channel gate was exact early; the bus gate was not.
323,661 diverging cycles → 0. All 357,360 overlapping cycles match on pc, bus_addr, bus_data and bus_access, with the DMA at 49 bursts of 195 cycles against the oracle's 49 and 195.
What landed
The channel is complete: the memory reader, the 7-bit delta-modulation output unit with its 8-bit shift register and bits-remaining counter, the 16-entry rate table (the register value is an index, not a period), the loop flag, and the end-of-sample IRQ.
The DMA sequence is implemented from nesdev_wiki/DMA.xhtml: halt on a read cycle, a dummy cycle, an optional alignment cycle, then the get — with the load halting on a get cycle and reloads on a put. The CPU has no RDY pin, so the steal is expressed by holding its clock enable low for one cycle while the APU and PPU keep running. That is a stolen cycle, and no CPU change was needed.
Four defects, each found by the next measurement
- The DMC timer ticked on the wrong APU phase, playing every sample two CPU cycles early.
- The alignment test was inverted. The wiki conditions it on whether the next cycle is a get, not the current one, which cost the load burst a cycle.
- The stolen cycles were not marked as DMA in the bus trace, so 195 cycles were compared as ordinary reads while their timing was already correct.
- The data bus is held across a halt — established by measurement rather than assumed: for all 144 residual cycles the oracle's value was frozen for the whole burst, with only the get driving a new one.
A pre-registered risk, retracted
Before the work began, the DMA's stall placement was recorded as oracle-defined — on the strength of the oracle's own comment describing its scheduler as calibrated and naming an external emulator as the reference to diff against.
DMA.xhtml documents the whole sequence precisely, so the risk did not exist. A note that an implementation was calibrated says nothing about whether documentation exists, and checking cost one grep.
A decoding residual that dissolved the same way
The DUT's played bit stream matched the ROM's sample data on all 392 bits; the oracle deviated in two. Neither was wrong: the first version of the ROM polled $4015 in its idle loop and provoked the documented DMA / register-read conflict, a second mechanism the ROM was not written to test. A ROM that tests two mechanisms at once cannot say which diverged.
Verification
- 32 gates green across rungs 1–4; 46 of 46 mutations CAUGHT, 0 NOT CAUGHT, 0 BUILD-FAILED.
apudmc037is gated twice — channel levels and the per-cycle bus — because the DMA's cost and placement reach the CPU and not the mixer.- AccuracyCoin 141/141 (100.00%, RAM decoder) and nestest 0-diff, by construction: no file under
crates/rustynes-{cpu,ppu,apu,mappers,core,gamedb}/src/changes.
Still open, and named
- The DMA / register-read conflict: documented, modelled by the oracle, and structurally unreachable by a harness that reads a flat array.
- "DMA can only halt on CPU read cycles": implemented, and no ROM reaches it — of
apudmc037's 49 bursts, zero are preceded by a write cycle, and removing the condition is byte-identical.
Both are recorded in docs/apu-oracle-vs-documentation.md in the sibling repository, with the measurement that established each.