-
-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture Overview
This document fixes the high-level architectural decisions for RustyNES. Subsystem docs take these as given and elaborate one chip each.
Three scheduling strategies were viable: (A) tight lockstep with PPU as master, (B) catch-up at CPU-instruction granularity, (C) hybrid. Option A is selected.
- The user's stated goal is cycle accuracy; lockstep is the only model that makes mid-instruction PPU events (sprite-zero hit, mid-scanline scroll writes, MMC3 IRQ at PPU dot 260) trivially correct rather than requiring per-quirk patches.
- Mesen2 and ares — the two leading accuracy-first emulators — both use lockstep; this puts our design on the same axis they have validated.
- The performance penalty is modest on modern hardware (≤ 2 ms per frame core work on a 2018-era laptop chip).
The no_std-friendly chip stack + the glue core form the deterministic emulation foundation. The remaining crates are additive host/platform/tooling layers that never perturb the core's per-frame output.
crates/
├── rustynes-core/ # Glue: NES struct, run loop, scheduler, save state, region config
├── rustynes-cpu/ # Ricoh 2A03 CPU (6502 + interrupt logic). No PPU/APU deps.
├── rustynes-ppu/ # 2C02 PPU. Depends on rustynes-mappers.
├── rustynes-apu/ # 2A03 APU. Depends on rustynes-cpu only for DMC DMA hooks.
├── rustynes-mappers/ # Cartridge + mapper trait + 172 mapper families.
├── rustynes-frontend/ # winit + wgpu + cpal + egui desktop app.
├── rustynes-netplay/ # GGPO-style rollback netplay (UDP + WebRTC).
├── rustynes-script/ # Sandboxed Lua 5.4 scripting engine.
├── rustynes-mobile/ # Shared UniFFI mobile bridge.
└── rustynes-android/ # Android JNI platform glue.
- Standalone components so they can be fuzzed and benchmarked in isolation.
-
rustynes-mappersstandalone so that adding a mapper does not touch CPU, PPU, or APU code. -
Bus ownership: The bus type owns the PPU, APU, mapper, controllers, and WRAM. The CPU borrows the bus during
tick(). This single choice avoids borrow-checker fights.
NTSC: 5.369318 MHz. PAL: 5.320342 MHz. Dendy: 5.320342 MHz. The CPU advances 1/3 of the time on NTSC and Dendy, 1/3.2 on PAL. The APU clocks every other CPU cycle.
fn tick_one_dot(&mut self) {
self.ppu.tick(&mut self.ppu_bus);
self.dot_count += 1;
if self.dot_count % 3 == self.cpu_phase {
self.cpu_tick();
}
}The bus owns: PPU, APU, mapper (via cart), WRAM, controllers, open-bus latch. The CPU borrows &mut Bus during tick(). PPU and APU each get their own bus trait for the things they need:
-
PpuBus:ppu_read/write(delegates to mapper);notify_a12(mapper IRQ). -
ApuBus:dmc_read;dmc_halt_request(queues DMA);raise_irq.
DMA only halts on a CPU read cycle. DMC DMA gets precedence over OAM DMA. Load and reload DMC DMA are strictly separated to preserve accurate $4015 observable side-effects.
- CPU never touches PPU or APU directly. Always through the bus, which already has them synced.
- PPU owns its 2 KB internal VRAM but reads CHR through the mapper.
- APU owns its register state and channel timers, and asks the CPU bus for DMC sample bytes via a DMA request channel.
-
Mappers see two independent buses: CPU bus (
$4020-$FFFF) and PPU bus ($0000-$1FFF+$2000-$3FFF). - Open-bus state belongs to the device that drives the bus.
- Region timing is data, not a build-time fork.
The core is single-threaded. Frontend runs the audio callback on cpal's audio thread, which reads from a lock-free SPSC ring buffer that the run-loop thread fills. Rendering (wgpu) and emulator stepping share the main thread; winit's event loop drives Nes::run_frame().
The scheduler is fully deterministic given:
- A fixed
cpu_phase(chosen at power-cycle from a seedable PRNG). - A fixed initial WRAM pattern (deterministic seeded fill).
- A fixed sequence of controller inputs.
- A fixed initial DMA get/put phase.
- A fixed region timing profile and reset/power-up mode.
This guarantees that save/load round-trips and a re-played input sequence produce bit-identical framebuffer + audio output. Required for TAS playback, regression tests, and rollback netplay.
RustyNES is a cycle-accurate Nintendo Entertainment System emulator written in pure Rust. Licensed under MIT OR Apache-2.0. | GitHub Repository | Web Demo