-
Notifications
You must be signed in to change notification settings - Fork 0
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.
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.
| 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.
| 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/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 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.
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.
RustySNES is a cycle-accurate Super Nintendo / Super Famicom emulator written in pure Rust. Licensed under MIT OR Apache-2.0. | GitHub Repository | Web Demo | API Docs