Skip to content

Architecture Overview

DoubleGate edited this page Jul 8, 2026 · 1 revision

Architecture Overview

Purpose

This page fixes the high-level architectural decisions for RustySNES. Subsystem docs (and the per-chip wiki pages) take these as given and elaborate one chip each.

Decision: Master-Clock Lockstep, Not Catch-Up

Per Architecture-Decision-Records (ADR 0001), the whole system derives from a single 21.477270 MHz NTSC (21.281370 MHz PAL) master clock, and the scheduler steps every chip on its own divisor in lockstep rather than running one chip ahead and catching the others up later. This is what makes mid-instruction events — a mid-scanline scroll write, an HDMA firing at an exact dot, an H/V-IRQ at a precise counter position — visible to subsequent CPU code without per-quirk patches.

The SNES makes this harder than a fixed-ratio console: the 65C816 CPU cycle is variable (6, 8, or 12 master clocks depending on which address region is accessed and the FastROM bit), and dot/scanline lengths vary (1360/1364/1368 master clocks). See Lockstep-Scheduler for the full timing model.

Workspace Shape (Cargo Workspace)

The crate graph is strictly one-directional — no chip crate depends on another:

rustysnes-cpu     # WDC 65C816 (5A22 core) — no PPU/APU/cart dependency
rustysnes-ppu     # PPU1 (5C77) + PPU2 (5C78) — VRAM/CGRAM/OAM only
rustysnes-apu     # SPC700 (S-SMP) + S-DSP + 64 KiB ARAM — independent
rustysnes-cart    # LoROM/HiROM/ExHiROM/ExLoROM map + coprocessor families
        \            |            /
         rustysnes-core            # ties them together: Bus, scheduler, DMA/HDMA
                     |
   rustysnes-{frontend, netplay, cheevos, script, test-harness}

rustysnes-core is the only crate that knows all four chip crates. Downstream consumers (frontend, tooling) depend on rustysnes-core, never on a chip crate directly. This keeps each chip independently fuzzable and benchmarkable, and means adding a cross-chip dependency is a structural violation, not just a style nit.

rustysnes-netplay, rustysnes-cheevos, and rustysnes-script are currently 1-line stubs — their crates exist to fix the workspace shape ahead of time, but no implementation has started (see Roadmap and Deferred-Features).

Bus Ownership

rustysnes-core::Bus owns everything mutable: the PPU, the APU/SMP, the cart (with its coprocessor/board logic), WRAM, the DMA/HDMA controller, controllers, and the open-bus latch. The CPU borrows &mut Bus during tick(). This single choice avoids the borrow-checker fight that "CPU holds the PPU, but the PPU also needs the CPU bus" creates — the PPU and SMP each see a narrow trait for only what they need (VRAM/CGRAM/OAM access and ARAM access, respectively).

Board / Coprocessor Logic Lives in the Cart Crate

Each SNES coprocessor (DSP-1..4, Super FX/GSU, SA-1, S-DD1, SPC7110, CX4, OBC1, ST01x/ST018, S-RTC) is a "mapper-equivalent" with its own bus window and clock, living behind a Board / Coprocessor trait in rustysnes-cart with default-no-op hooks — the CPU and PPU never special-case a board. Several of the DSP-family chips share one µPD77C25/µPD96050 LLE core, so the cart crate implements that engine once. See Cartridge-and-Coprocessors.

Determinism Contract

Per ADR 0004, the same seed + ROM + input sequence yields a bit-identical framebuffer and audio stream. The asynchronous SPC700 clock domain is tracked with an integer relative-time accumulator (no floating point, no host wall-clock); real-hardware resonator drift and real-time-clock coprocessors are deliberately frozen out of the deterministic path. Rate control and run-ahead live entirely in the frontend, never in core synthesis — see Frontend-Architecture. This is required for save-state round-trips (ADR 0006), and is the foundation the (not-yet-started) netplay rollback and TAS features will build on.

Concurrency Model

The core is single-threaded. The frontend can optionally run emulation on a dedicated thread (Arc<Mutex<EmuCore>>), while winit's event loop and the egui shell drive the present path — the shell never holds the emulator lock inside the egui closure; menu interactions return an action dispatched after the egui pass completes.

Test ROMs Are the Spec

When a doc and a passing test ROM disagree, the ROM wins and the doc gets updated. The oracle is two-layered: the SingleStepTests 65816 + spc700 JSON per-opcode suites, plus the committable gilyon / undisbeliever ROM corpora, blargg's spc_* suite for audio, and additional video test suites. See Testing-Strategy.

Architectural Alternative Considered (Rejected)

A cooperative-threaded coroutine scheduler (the higan/bsnes libco model) was the proven reference for the async SPC700 domain, but was rejected as the literal implementation — coroutines complicate bit-deterministic save-states. RustySNES borrows the model's relative-time-accumulator technique for the SPC700 resync only, implemented single-threaded so the whole core stays one deterministic state machine. See Lockstep-Scheduler for the resync mechanics.

Clone this wiki locally