Skip to content

Lockstep Scheduler

DoubleGate edited this page Jul 8, 2026 · 1 revision

Lockstep Scheduler

References: docs/scheduler.md, docs/adr/0001

The scheduler lives in rustysnes-core and is the timebase every chip phase derives from. It owns the single 21.477270 MHz NTSC (21.281370 MHz PAL) master clock and advances the CPU, PPU, DMA, and HDMA in lockstep on it, while the SPC700/S-DSP run in a second, asynchronous clock domain resynced continuously. This is the central architectural choice behind RustySNES's accuracy, and the reason mid-instruction events work without per-quirk patches.

Not Pure Dot-Lockstep — a Master-Clock Model

A console whose CPU cycle is always a fixed ratio of the PPU dot can get away with integer dot-lockstep. The SNES cannot: a 65C816 CPU cycle is 6, 8, or 12 master clocks, depending on which address region is accessed and the FastROM bit ($420D bit 0), and the PPU's dot and scanline lengths vary (1360/1364/1368 master clocks per scanline, depending on region/interlace phase). So RustySNES models the master clock directly: tick() advances the master clock, the CPU bus reports the access speed for each memory cycle, the scheduler advances that many master ticks, then re-evaluates the PPU dot, HDMA, and IRQ-timer phases.

The Memory-Access-Speed Map

Address region Speed Master clocks
WRAM mirror $0000-$1FFF Slow 8
PPU/APU/B-bus I/O $2100-$21FF Fast 6
Old-style joypad $4016/$4017 XSlow 12
DMA/CPU registers $4200-$5FFF Fast 6
Expansion $6000-$7FFF Slow 8
ROM $8000-$FFFF (WS1, banks $00-$3F) Slow 8
ROM $8000-$FFFF (WS2, banks $80+) MEMSEL-dependent 6 or 8
WRAM $7E0000-$7FFFFF Slow 8
Internal cycle (no bus access) Fast 6

Resulting effective CPU frequencies: 6 clocks -> 3.58 MHz, 8 -> 2.68 MHz, 12 -> 1.79 MHz.

The Divisor Table

Chip Advances per Notes
Master clock 1 tick the finest quantum the scheduler models
65C816 (CPU) 6/8/12 master clocks variable per access, per the map above
PPU dot 4 master clocks (nominal) with documented long-dot exceptions
SPC700 (S-SMP) a separate ~1.024 MHz domain asynchronous — resynced via an accumulator
S-DSP 24.576 MHz resonator / 768 one stereo sample per 768 resonator cycles

The SPC700 is not a master-clock divisor at all — it is a genuinely second clock domain.

The SPC700 Async Resync

The SPC700/S-DSP run on their own timebase, asynchronous to the main master clock. RustySNES tracks "how far ahead is the CPU vs. the SMP" with a single signed integer relative-time accumulator — no floating point, so the counter is exact and stays bit-deterministic (ADR 0004). The accumulator is stepped inside the same per-master-tick loop that drives the PPU dot clock, so the SMP advances in true lockstep rather than periodic catch-up: the CPU never observes an SMP that is arbitrarily stale when it reads the $2140-$2143 communication ports. This is the higan/bsnes cooperative-threaded technique, implemented single-threaded so save-states and (future) netplay rollback stay bit-deterministic.

SA-1 — the Second CPU

SA-1 carts install a genuine second WDC 65C816, clocked at master/2. Because the crate graph forbids rustysnes-cart from depending on rustysnes-cpu, the SA-1 board's system state lives in the cart crate while rustysnes-core owns and steps the second CPU instance directly, through the board's second-CPU hooks. The stepping model is deterministic catch-up rather than a free-running thread: after every main-CPU instruction, the scheduler measures how much master clock has elapsed and converts it into an SA-1 cycle budget, then steps the second CPU against that budget. Because the budget is a pure function of the (already-deterministic) main CPU's elapsed master clock, installing and stepping the SA-1 never perturbs the main CPU's own timing or accuracy.

The Future Timebase Refactor

ADR 0002 proposes eventually moving to a fractional master-clock model with sub-cycle (phi1/phi2) resolution, to close any hard-tier accuracy residuals the current whole-tick model can't reach. It is designed in from day one but the refactor itself is explicitly deferred — status "Proposed," not scheduled against any release rung — until the accuracy program actually needs it. It is also flagged as the one future release expected to break save-state byte-compatibility, so it will not be undertaken casually.

Clone this wiki locally