References: ref-docs/research-report.md §§1–7, plus docs/scheduler.md,
docs/adr/0001–docs/adr/0004.
These cross-cutting decisions span every crate. Read this before any chip doc — reading a chip spec in isolation will mislead, because the timing model and bus ownership are global.
RustySNES is a cycle-accurate Super Nintendo / Super Famicom emulator in pure Rust. The
accuracy bar is Mesen-S / ares (per ref-docs/research-report.md "State of the art"): the
one reference core legally adaptable is ares (ISC); bsnes / Mesen-S / higan are GPLv3
clone-only and Snes9x is non-commercial. We build clean-room from the test ROMs and the
ref-docs/ corpus, not from copyleft source lines.
Per ref-docs/research-report.md §1, the whole main system derives from a single
21.477270 MHz NTSC (PAL 21.281370 MHz) master clock. The scheduler advances the master
clock and steps every other chip on its divisor — lockstep, not catch-up. This makes
mid-instruction events (a mid-scanline scroll / Mode-7 / CGRAM write landing at an exact dot,
HDMA firing at H≈$116, an H/V-IRQ at a precise counter position) visible to subsequent CPU
code without per-quirk patches. The SNES twist over the NES: the CPU cycle is variable
(6/8/12 master clocks per access) and the dot / scanline lengths vary
(1360/1364/1368 clocks). See docs/scheduler.md and docs/adr/0001.
rustysnes-core::Bus holds the PPU, APU/SMP, cart (with its coprocessor / board logic),
WRAM, the DMA/HDMA controller, controllers, and the open-bus latch. The CPU borrows
&mut Bus during tick(). As in RustyNES (the TetaNES-postmortem lesson), this single
choice avoids the borrow-checker fight that "CPU holds PPU, but PPU also needs the CPU bus"
creates. The PPU and SMP each see a narrow trait (PpuBus, SmpBus / DspBus) for only
what they need — VRAM/CGRAM/OAM access and ARAM access respectively.
rustysnes-cpu (65C816 — no PPU/APU/cart dep)
rustysnes-ppu (PPU1+PPU2 — VRAM/CGRAM/OAM only)
rustysnes-apu (SPC700 + S-DSP + ARAM — independent)
rustysnes-cart (memory map + coprocessor families — independent)
\ | / /
rustysnes-core (ties them together, re-exports public types + the `facade` embedding API)
|
rustysnes-{frontend, netplay, cheevos, script, test-harness, libretro}
No chip crate depends on another. rustysnes-core is the only crate that knows all four.
Result: each chip is fuzzable and benchmarkable in isolation. Adding a cross-chip dependency
breaks this invariant — don't. Downstream consumers depend on rustysnes-core, never the
chip crates directly.
Per ref-docs/2026-06-24-coprocessors.md, 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. All of it lives behind a Cart / Coprocessor trait in
rustysnes-cart with default-no-op hooks — the PPU and CPU never special-case a board. Six
of the chips share one µPD77C25 / µPD96050 LLE core (DSP-1/2/3/4 + ST010/011), so the
cart crate implements that engine once. See docs/cart.md.
Same seed + ROM + input sequence ⇒ bit-identical framebuffer and audio. Power-on
CPU/PPU/SMP phase alignment comes from a seeded PRNG; reset preserves it. The async
SPC700 domain is tracked by an integer relative-time accumulator (no floating point, no
host wall-clock) — real-hardware resonator drift (±0.5%) and RTC chips are deliberately
frozen out of the deterministic path. This is required for save-state round-trip, regression
tests, TAS replay, and netplay rollback. See docs/adr/0004 and docs/apu.md §2.
rustysnes-frontend is winit + wgpu + cpal + egui, and egui runs every frame — a
persistent menu bar + status bar + tabbed Settings, with toggleable debugger panels layered
on top. The shell never holds the emu lock inside the egui closure: menu interactions return
a MenuAction dispatched after the egui pass. On native the emulator runs on a dedicated
thread; the winit thread only does UI + present. Rate control and run-ahead live here, never
in the core synthesis (that is what keeps fact #5 intact). See docs/frontend.md.
When the docs and a passing test ROM disagree, the ROM wins and the docs get updated. The
oracle is two-layer (ref-docs/research-report.md "Standards / test-ROM corpora"): the
SingleStepTests 65816 + spc700 JSON per-opcode suites, the committable
gilyon / undisbeliever ROMs, blargg's spc_* for audio, and the 240p Suite for video. See
docs/testing-strategy.md.
| Crate | Owns |
|---|---|
rustysnes-cpu |
WDC 65C816 (5A22 core): emulation / native modes, variable access cycles, vectors. |
rustysnes-ppu |
PPU1 (5C77) sprites / Mode-7-multiply + PPU2 (5C78) CGRAM / output / counters. |
rustysnes-apu |
SPC700 (S-SMP) + S-DSP + 64 KiB ARAM; the async domain + BRR + 8 voices. |
rustysnes-cart |
LoROM/HiROM/ExHiROM map + header detect + the coprocessor families. |
rustysnes-core |
Bus, the master-clock scheduler, DMA/HDMA, multiply / divide units, joypad auto-read; re-exports chip types. Also the EmuCore embedding facade (facade module, std-only, v1.2.0) — load/step/framebuffer/audio/save-state, for any headless embedder (a libretro core, rustysnes-frontend's own thin wrapper). |
rustysnes-frontend |
The egui shell, audio ring, pacing, gamepads, save-states, rewind, wasm. |
rustysnes-netplay |
Rollback netplay (frontend-orchestrated; deterministic core required). |
rustysnes-cheevos |
RetroAchievements (opt-in, native FFI). |
rustysnes-script |
Lua scripting / TAS API. |
rustysnes-test-harness |
Golden-log differ, run_until_complete, JSON-oracle runner, screenshot baseline. |
rustysnes-libretro |
A libretro core (v1.2.0) — a thin C-ABI wrapper over rustysnes-core::facade::EmuCore, loadable by RetroArch. See docs/libretro.md. |
The chip stack is #![no_std] + extern crate alloc;; rustysnes-core is conditionally so
(#![cfg_attr(not(feature = "std"), no_std)], v1.2.0) — its default std feature enables the
facade module, and disabling it (the thumbv7em no_std CI gate) restores unconditional
no_std, proving the facade compiles out entirely rather than merely going unused. Only
rustysnes-frontend, rustysnes-cheevos, and rustysnes-libretro (all FFI) carry unsafe
(each with a // SAFETY:
comment).
Per ref-docs/research-report.md "Architecture options":
- Cooperative-threaded coroutines (higan / bsnes libco model). The proven accuracy model; used as the conceptual reference. Rejected as the literal implementation: coroutines fit Rust awkwardly and complicate bit-deterministic save-states / netplay. We borrow its relative-time-accumulator + sync-on-access technique for the SPC700 domain only.
- Catch-up / lazy sync (Snes9x model). Run the CPU freely, catch the PPU/APU up at sync points. Rejected: loses the sub-instruction accuracy the bar demands.
The chosen path is single-threaded master-clock lockstep (fact #1) with the SPC700 resync borrowed from the coroutine model.
- Whether any hard-tier residual forces the fractional-timebase refactor before v1.0 (it is
designed in from day one regardless — see
docs/adr/0002). - Per-board SRAM / coprocessor bus windows are not canonically tabulated; build them from the
cartridge database + ares board definitions during Phase 4 (
ref-docs/research-report.md"Open questions" #3).