rustysnes_core/bus.rs
1//! The Bus owns everything mutable.
2//!
3//! It holds the PPU1/PPU2, the SPC700+S-DSP, the cart (→ board / coprocessor), WRAM,
4//! controllers, the open-bus latch, the CPU-side registers (`$4200-$421F`), the mul/div unit,
5//! and the DMA/HDMA controller. The 65C816 borrows `&mut Bus` during an instruction; the PPU and
6//! DMA see narrower bus traits ([`rustysnes_ppu::VideoBus`], [`crate::dma_bus::DmaBus`])
7//! implemented on this same struct. The APU owns its ARAM/DSP internally; the Bus drives it
8//! through [`rustysnes_apu::Apu`] directly — the four `$2140-$2143` port latches via
9//! [`rustysnes_apu::Apu::cpu_read_port`]/[`rustysnes_apu::Apu::cpu_write_port`] and the SPC clock
10//! via [`rustysnes_apu::Apu::advance_smp_cycle`] (the integer-accumulator async resync).
11//!
12//! ## The master clock lives here
13//!
14//! The SNES CPU cycle is **6, 8, or 12 master clocks** depending on the address region (and the
15//! FastROM bit). The CPU asks the Bus for the access cost via [`CpuBus::access_cycles`] (ares
16//! `wait`) and drives the clock with [`CpuBus::advance`] (ares `step`), sequencing the advance
17//! around each [`CpuBus::read24`]/[`CpuBus::write24`] so the access lands at the hardware-exact
18//! instant — a write at the end of its cycle, a read four clocks before it. Each master-clock
19//! advance steps the PPU dot clock (4 master/dot) and the SPC accumulator in lockstep, so a
20//! mid-instruction PPU event (an HV-IRQ at a precise dot, a mid-scanline register write seen at
21//! the right hcounter) lands at the right time without per-quirk patches.
22
23// Byte-splitting a 16-bit register into its low/high `u8` (`reg as u8`, `(reg >> 8) as u8`) and
24// folding addresses to `u16`/`usize` is the bread-and-butter of a memory bus; flagging each
25// deliberate narrowing cast would bury real issues, so the cast-precision family is allowed for
26// this module only (mirrors `rustysnes-cpu/src/exec.rs`).
27#![allow(
28 clippy::cast_possible_truncation,
29 clippy::cast_lossless,
30 clippy::struct_excessive_bools
31)]
32
33use alloc::boxed::Box;
34
35use rustysnes_apu::Apu;
36use rustysnes_cart::{Cart, Region};
37use rustysnes_cpu::Bus as CpuBus;
38use rustysnes_ppu::{Ppu, Region as PpuRegion, VideoBus};
39use rustysnes_savestate::{SaveReader, SaveStateError, SaveWriter};
40
41use crate::controller::{PortDevice, PortState};
42use crate::dma::Dma;
43use crate::dma_bus::DmaBus;
44
45/// WRAM size — the SNES has 128 KiB of work RAM (`$7E0000-$7FFFFF`).
46const WRAM_SIZE: usize = 128 * 1024;
47/// Master clocks per PPU dot (nominal; long-dot remainder folded into the 1364/1360/1368 line).
48const MASTER_PER_DOT: u32 = 4;
49/// PPU dot at which each visible scanline's HDMA transfer fires — ares' `hdmaPosition` of hcounter
50/// 1104 (`sfc/cpu/timing.cpp`) divided by [`MASTER_PER_DOT`]. Running the table at this exact dot
51/// (rather than the scanline boundary) latches a mid-line `$420C` write on the hardware-correct
52/// scanline, which is what makes the `hdmaen_latch_test` show a banded HDMAEN-vs-latch crossing.
53/// Defined equal to `rustysnes_ppu::RENDER_DOT` (PPU-owned single source of truth, since this is
54/// fundamentally a video-timing fact) — `hdma_run_dot_matches_ppu_render_dot` below asserts the
55/// two never drift apart.
56const HDMA_RUN_DOT: u16 = rustysnes_ppu::RENDER_DOT;
57/// SPC700 fractional-clock numerator (master ticks → SMP **base** clocks).
58///
59/// The unit the APU advances per [`rustysnes_apu::Apu::advance_smp_cycle`] call is one SMP *base*
60/// clock = `apuFrequency / 12` (ares `SMP::create(apuFrequency()/12, …)`; `apuFrequency =
61/// 32040 × 768 = 24_606_720` Hz → base = `2_050_560` Hz). A normal SMP access is `SMP_WAIT` = 2
62/// base clocks, giving the ~1.025 MHz effective opcode rate and an exact `32_040` Hz S-DSP sample.
63///
64/// The async resync (`docs/scheduler.md` §async-resync, ADR 0004) is an **integer** accumulator —
65/// no floats, so the SPC domain is bit-deterministic. The exact rational is
66/// `2_050_560 / 21_477_270` (SMP base rate over the NTSC master rate); gcd = 30, giving the reduced
67/// `68_352 / 715_909` kept here to bound accumulator growth (`spc_accum` stays below `SPC_DEN`).
68const SPC_NUM: u64 = 68_352;
69/// SPC700 fractional-clock denominator: the NTSC master clock Hz, reduced by gcd = 30.
70const SPC_DEN: u64 = 715_909;
71
72/// The master-clock phase + the CPU-side timing registers the Bus advances in lockstep.
73#[derive(Debug, Clone)]
74pub struct Clock {
75 /// Cumulative master-clock ticks since power-on.
76 pub master: u64,
77 /// Master cycles owed to the PPU before its next dot.
78 dot_accum: u32,
79 /// Fractional accumulator for the asynchronous SPC700 domain.
80 spc_accum: u64,
81 /// `$420D` MEMSEL bit 0 — FastROM (`true` = 6-clock WS2 ROM, `false` = 8-clock).
82 fast_rom: bool,
83 /// `$4200` NMITIMEN — bit7 NMI-enable, bit5 V-IRQ, bit4 H-IRQ, bit0 auto-joypad.
84 nmitimen: u8,
85 /// Latched NMI edge awaiting the `CPU` poll (set at `VBlank` only when NMI is enabled).
86 nmi_line: bool,
87 /// `$4210` RDNMI bit7 — the `VBlank`-occurred flag. Set at `VBlank` start **regardless** of
88 /// the `NMITIMEN` enable (hardware), cleared on read. ROMs poll this to sync to `VBlank`
89 /// without taking the interrupt (e.g. gilyon's `wait_for_vblank`).
90 rdnmi_flag: bool,
91 /// Level IRQ line (HV-IRQ / coprocessor / APU timer), cleared on `$4211` read.
92 irq_line: bool,
93 /// `$4207/8` HTIME — the H-IRQ comparator.
94 htime: u16,
95 /// `$4209/A` VTIME — the V-IRQ comparator.
96 vtime: u16,
97}
98
99impl Default for Clock {
100 fn default() -> Self {
101 Self {
102 master: 0,
103 dot_accum: 0,
104 spc_accum: 0,
105 fast_rom: false,
106 nmitimen: 0,
107 nmi_line: false,
108 rdnmi_flag: false,
109 irq_line: false,
110 htime: 0x01FF,
111 vtime: 0x01FF,
112 }
113 }
114}
115
116/// The CPU multiply/divide unit (`$4202-$4206` → `$4214-$4217`). The SNES computes these with an
117/// 8-CPU-cycle hardware latency; the deterministic core resolves them instantly (the result is
118/// what tests read), which is accurate for every documented program that waits for the real
119/// hardware's own latency before reading `RDMPY`/`RDDIV` — as every known commercial title does.
120///
121/// **Deliberately not modeled: the SNESdev-documented overlapping-operation errata** ("Starting
122/// a multiplication (`$4203` WRMPYB) or division (`$4206` WRDIVB) while the 5A22 is still
123/// processing a previous multiplication or division can cause the 5A22 to output erroneous
124/// values to `RDDIV` and/or `RDMPY`," <https://snes.nesdev.org/wiki/Errata>). This is genuinely
125/// **undefined** hardware behavior — no canonical "corrupted" value is documented anywhere, so
126/// there is nothing correct to port; inventing a specific fabricated corruption value would
127/// itself violate the determinism contract's spirit (`docs/adr/0004`) by pretending a real, one
128/// true answer exists for a case real hardware itself doesn't define one for. No known program
129/// relies on this (a program that hit it would already be behaving unpredictably on real
130/// hardware), so this is a **documented, intentional non-goal**, not an open gap — see
131/// `to-dos/VERSION-PLAN.md`'s `v0.5.0 "Fidelity"` hardware-gotcha list for the same reasoning.
132#[derive(Debug, Clone, Default)]
133struct MulDiv {
134 mpya: u8,
135 dividend: u16,
136 rddiv: u16,
137 rdmpy: u16,
138}
139
140impl Clock {
141 fn save_state(&self, s: &mut SaveWriter) {
142 s.write_u64(self.master);
143 s.write_u32(self.dot_accum);
144 s.write_u64(self.spc_accum);
145 s.write_bool(self.fast_rom);
146 s.write_u8(self.nmitimen);
147 s.write_bool(self.nmi_line);
148 s.write_bool(self.rdnmi_flag);
149 s.write_bool(self.irq_line);
150 s.write_u16(self.htime);
151 s.write_u16(self.vtime);
152 }
153
154 fn load_state(&mut self, s: &mut SaveReader) -> Result<(), SaveStateError> {
155 self.master = s.read_u64()?;
156 self.dot_accum = s.read_u32()?;
157 self.spc_accum = s.read_u64()?;
158 self.fast_rom = s.read_bool()?;
159 self.nmitimen = s.read_u8()?;
160 self.nmi_line = s.read_bool()?;
161 self.rdnmi_flag = s.read_bool()?;
162 self.irq_line = s.read_bool()?;
163 // htime/vtime are 9-bit comparators (write24 masks bit 8 with & 1 at $4208/$420A already).
164 self.htime = s.read_u16()? & 0x01FF;
165 self.vtime = s.read_u16()? & 0x01FF;
166 Ok(())
167 }
168}
169
170impl MulDiv {
171 fn save_state(&self, s: &mut SaveWriter) {
172 s.write_u8(self.mpya);
173 s.write_u16(self.dividend);
174 s.write_u16(self.rddiv);
175 s.write_u16(self.rdmpy);
176 }
177
178 fn load_state(&mut self, s: &mut SaveReader) -> Result<(), SaveStateError> {
179 self.mpya = s.read_u8()?;
180 self.dividend = s.read_u16()?;
181 self.rddiv = s.read_u16()?;
182 self.rdmpy = s.read_u16()?;
183 Ok(())
184 }
185}
186
187/// Everything mutable lives here.
188pub struct Bus {
189 /// The video subsystem (PPU1 + PPU2).
190 pub ppu: Ppu,
191 /// The audio subsystem (SPC700 + S-DSP + ARAM).
192 pub apu: Apu,
193 /// The loaded cartridge (board mapping + any coprocessor), or `None` before a ROM loads.
194 pub cart: Option<Cart>,
195 /// The 8-channel DMA/HDMA controller (`$420B`/`$420C`, `$43xx`).
196 pub dma: Dma,
197 /// The master-clock phase + CPU timing registers.
198 pub clock: Clock,
199 /// 128 KiB work RAM (`$7E0000-$7FFFFF`).
200 wram: Box<[u8; WRAM_SIZE]>,
201 /// WRAM port address (`$2181-$2183`), auto-incremented by `$2180` access.
202 wram_addr: u32,
203 /// Controller shift latches (`$4016/$4017`) + the auto-read result (`$4218-$421F`).
204 joypad: [u16; 2],
205 joypad_strobe: bool,
206 /// Per-port peripheral state (`v0.9.0`, Phase 7 niche peripherals) — Mouse/Super Scope/Super
207 /// Multitap. Idle (and touching nothing on `$4016`/`$4017`'s `data1` bit) unless a port's
208 /// [`crate::controller::PortDevice`] is explicitly switched away from the default `Gamepad`
209 /// via [`Self::set_port_device`], in which case `joypad[port]`'s own bit is bypassed instead
210 /// of merged — see [`Self::port_clock`].
211 ports: [PortState; 2],
212 /// WRIO ($4201 write / $4213 read) — the programmable I/O port. Bit6 is controller port 1's
213 /// IOBIT pin, bit7 port 2's (only port 2's is wired to the PPU H/V-counter latch on real
214 /// hardware — a Super Scope's own beam-detection strobe). Reset value `0xFF` (ares
215 /// `cpu.hpp`'s `n8 pio = 0xff`).
216 pio: u8,
217 /// Open-bus latch: the last value driven on the data bus.
218 #[allow(clippy::struct_field_names)] // "open_bus" is the hardware name for the latch.
219 open_bus: u8,
220 muldiv: MulDiv,
221 /// The last visible scanline HDMA was serviced on, so [`Bus::advance_master`] runs each line's
222 /// HDMA exactly once — even when the master clock is being advanced *inside* a GP-DMA (real
223 /// hardware interleaves HDMA at the start of every scanline, preempting the general DMA).
224 last_hdma_line: u16,
225 /// Re-entrancy guard: true while an HDMA transfer's own cycle cost is being charged, so the
226 /// nested `advance_master` doesn't recursively re-trigger HDMA for the same line.
227 in_hdma: bool,
228 /// Whether this frame's V=0 HDMA setup (table reset + reload) has already fired, so it runs
229 /// exactly once per frame independent of the per-line run at [`HDMA_RUN_DOT`].
230 hdma_setup_done: bool,
231 /// Active cheat-code patches (`v0.8.0`, T-81-003) — checked on every CPU-visible read in
232 /// [`CpuBus::read24`]. Empty (the default, and the only state possible unless a frontend
233 /// explicitly calls [`Self::set_cheats`]) costs exactly one `is_empty()` branch per read.
234 cheats: alloc::vec::Vec<crate::cheat::CheatPatch>,
235 /// 65C816 read/write watchpoints (`v0.8.0`, T-81-001b) — compiled out entirely when
236 /// `debug-hooks` is off. See [`crate::watchpoint`]'s module doc.
237 #[cfg(feature = "debug-hooks")]
238 watchpoints: crate::watchpoint::WatchpointState,
239 /// The CPU's `PBR:PC` at the moment of its current access, set by [`Self::set_debug_pc`]
240 /// (the scheduler calls it before each [`rustysnes_cpu::Cpu::step`]) — feeds
241 /// [`crate::watchpoint::WatchpointHit::pbr_pc`]. `debug-hooks`-only, same as `watchpoints`.
242 #[cfg(feature = "debug-hooks")]
243 debug_pc: u32,
244}
245
246impl Default for Bus {
247 fn default() -> Self {
248 Self::new(Region::Ntsc)
249 }
250}
251
252impl Bus {
253 /// Construct a power-on Bus for the given console region.
254 ///
255 /// # Panics
256 /// Panics only if the 128 KiB WRAM allocation cannot be sized to the fixed `WRAM_SIZE` array
257 /// (an out-of-memory condition at power-on), which cannot happen for the constant size.
258 #[must_use]
259 pub fn new(region: Region) -> Self {
260 let ppu_region = match region {
261 Region::Ntsc => PpuRegion::Ntsc,
262 Region::Pal => PpuRegion::Pal,
263 };
264 Self {
265 ppu: Ppu::with_region(ppu_region),
266 apu: Apu::new(),
267 cart: None,
268 dma: Dma::new(),
269 clock: Clock::default(),
270 wram: alloc::vec![0u8; WRAM_SIZE]
271 .into_boxed_slice()
272 .try_into()
273 .unwrap(),
274 wram_addr: 0,
275 joypad: [0; 2],
276 joypad_strobe: false,
277 ports: [PortState::default(), PortState::default()],
278 pio: 0xFF,
279 open_bus: 0,
280 muldiv: MulDiv::default(),
281 last_hdma_line: u16::MAX,
282 in_hdma: false,
283 hdma_setup_done: false,
284 cheats: alloc::vec::Vec::new(),
285 #[cfg(feature = "debug-hooks")]
286 watchpoints: crate::watchpoint::WatchpointState::default(),
287 #[cfg(feature = "debug-hooks")]
288 debug_pc: 0,
289 }
290 }
291
292 /// Reconfigure the PPU's region (line count / 50-vs-60 Hz status bit) from the installed
293 /// cart's header, auto-detecting NTSC vs PAL rather than requiring the frontend to guess or
294 /// hardcode it. A no-op when no cart is installed. Region only ever affects the PPU's
295 /// line-count/status-bit timeline here — the differing NTSC/PAL master-clock *rate* (Hz) is a
296 /// real-world audio/video pacing concern the frontend owns (`docs/adr/0004`); the core's
297 /// master-clock counter is a pure tick count, not wall-clock time, so nothing else in the
298 /// core depends on which oscillator frequency a real console would use.
299 // Deliberately NOT `const fn`: `Bus` holds heap-allocated/complex nested state (`Box`-owned
300 // WRAM, the PPU/APU), and this method reads a `Cart` (a `Box<dyn Board>` behind it) — pinning
301 // this to a `const` API guarantee for no actual const-context caller buys nothing and would
302 // force a breaking API change the moment any of that state gains a genuinely non-const need
303 // (logging, validation, additional resets).
304 #[allow(clippy::missing_const_for_fn)]
305 pub fn sync_region_from_cart(&mut self) {
306 let Some(cart) = &self.cart else { return };
307 let ppu_region = match cart.header.region {
308 Region::Ntsc => PpuRegion::Ntsc,
309 Region::Pal => PpuRegion::Pal,
310 };
311 self.ppu.set_region(ppu_region);
312 }
313
314 /// Set the latched controller state for a player (`0` = P1, `1` = P2). 12-bit `BYsSUDLR....`.
315 pub fn set_joypad(&mut self, player: usize, state: u16) {
316 if let Some(slot) = self.joypad.get_mut(player) {
317 *slot = state;
318 }
319 }
320
321 /// The latched controller state for a player (`0` = P1, `1` = P2) — the read side of
322 /// [`Self::set_joypad`], for TAS movie recording (`crate::movie::MovieRecorder`) and the
323 /// debugger overlay.
324 #[must_use]
325 pub fn joypad(&self, player: usize) -> u16 {
326 self.joypad.get(player).copied().unwrap_or(0)
327 }
328
329 /// Select which peripheral is connected to controller port `port` (`0` = port 1, `1` = port
330 /// 2). Defaults to [`PortDevice::Gamepad`] on both ports (this project's original,
331 /// unchanged behavior) until a frontend explicitly calls this — a host/session configuration
332 /// choice, not emulated state (matching [`Self::set_cheats`]/`Self::set_watchpoints`'s own
333 /// "re-established by the frontend, not carried in a save-state" posture — a real SNES has no
334 /// memory of what was plugged in across a power cycle either).
335 pub fn set_port_device(&mut self, port: usize, device: PortDevice) {
336 if let Some(p) = self.ports.get_mut(port) {
337 p.device = device;
338 }
339 }
340
341 /// The peripheral currently connected to controller port `port` — for the debugger overlay
342 /// and the frontend's own input-routing (`v0.9.0`).
343 #[must_use]
344 pub fn port_device(&self, port: usize) -> PortDevice {
345 self.ports
346 .get(port)
347 .map_or(PortDevice::Gamepad, |p| p.device)
348 }
349
350 /// Feed one frame's worth of SNES Mouse input for port `port` (only meaningful when that
351 /// port's device is [`PortDevice::Mouse`]). `dx`/`dy` are raw, unscaled host deltas since the
352 /// last call — the SNES Mouse's own speed multiplier and 127-unit clamp are applied
353 /// internally at the hardware-accurate point (latch time), matching real hardware. Same
354 /// "always replace, re-synced once per frame" convention as [`Self::set_joypad`].
355 pub fn set_mouse(&mut self, port: usize, dx: i32, dy: i32, left: bool, right: bool) {
356 if let Some(p) = self.ports.get_mut(port) {
357 p.mouse.set_input(dx, dy, left, right);
358 }
359 }
360
361 /// Feed one frame's worth of Super Scope input for port `port` (only meaningful when that
362 /// port's device is [`PortDevice::SuperScope`]). `x`/`y` are absolute screen coordinates in
363 /// SNES pixel space (`0..256`, `0..240`-ish; a small negative/over-max margin is allowed and
364 /// means "aimed off-screen", matching real hardware). `buttons` is a bitmask over
365 /// [`crate::controller::scope`]'s `TRIGGER`/`CURSOR`/`TURBO`/`PAUSE` bits — the LIVE physical
366 /// switch/button state; this project reproduces real hardware's own edge-detection internally
367 /// (`crate::controller::SuperScopeState`), so the frontend should pass the raw host state,
368 /// not a pre-toggled value. (A packed bitmask rather than one bool per button, matching
369 /// [`Self::set_joypad`]'s own convention.)
370 pub fn set_superscope(&mut self, port: usize, x: i32, y: i32, buttons: u8) {
371 if let Some(p) = self.ports.get_mut(port) {
372 p.super_scope.set_input(x, y, buttons);
373 }
374 }
375
376 /// Feed one frame's worth of input for Super Multitap sub-pad `sub_index` (`0..=3`) of port
377 /// `port` (only meaningful when that port's device is [`PortDevice::Multitap`]) — same 12-bit
378 /// `BYsSUDLR....` format and per-frame convention as [`Self::set_joypad`].
379 pub fn set_multitap_pad(&mut self, port: usize, sub_index: usize, buttons: u16) {
380 if let Some(p) = self.ports.get_mut(port) {
381 p.multitap.set_pad(sub_index, buttons);
382 }
383 }
384
385 /// The current input state of Super Multitap sub-pad `sub_index` of port `port` — the read
386 /// side of [`Self::set_multitap_pad`], for the debugger overlay and TAS movie recording.
387 #[must_use]
388 pub fn multitap_pad(&self, port: usize, sub_index: usize) -> u16 {
389 self.ports
390 .get(port)
391 .map_or(0, |p| p.multitap.pad(sub_index))
392 }
393
394 /// Non-intrusive read of WRAM for the test harness + debugger (does NOT advance the clock,
395 /// touch open bus, or trip register side effects). I/O registers and the cart region return
396 /// `0` — this is for inspecting RAM-resident test-result variables, not for emulation.
397 #[must_use]
398 pub fn peek_wram(&self, addr24: u32) -> u8 {
399 let bank = (addr24 >> 16) & 0xFF;
400 let addr = (addr24 & 0xFFFF) as u16;
401 match bank {
402 0x7E..=0x7F => self.wram[(addr24 & 0x1_FFFF) as usize],
403 0x00..=0x3F | 0x80..=0xBF if addr < 0x2000 => self.wram[(addr & 0x1FFF) as usize],
404 _ => 0,
405 }
406 }
407
408 /// Non-intrusive write of WRAM (the write counterpart to [`Self::peek_wram`], same
409 /// addressing, same "no clock/open-bus/register side effects" contract) — for `rustysnes-
410 /// script`'s Lua `emu.write`. A write to an address outside WRAM's mirrors is silently
411 /// ignored (matching `peek_wram`'s `_ => 0` read side) rather than erroring, since a script
412 /// address is arbitrary user input, not a bug to surface loudly. (Cheat codes, T-81-003, use
413 /// [`Self::set_cheats`]'s CPU-read intercept instead — real Game Genie/Pro Action Replay
414 /// codes overwhelmingly target cartridge ROM, which this WRAM-only accessor cannot reach.)
415 pub fn poke_wram(&mut self, addr24: u32, val: u8) {
416 let bank = (addr24 >> 16) & 0xFF;
417 let addr = (addr24 & 0xFFFF) as u16;
418 match bank {
419 0x7E..=0x7F => self.wram[(addr24 & 0x1_FFFF) as usize] = val,
420 0x00..=0x3F | 0x80..=0xBF if addr < 0x2000 => self.wram[(addr & 0x1FFF) as usize] = val,
421 _ => {}
422 }
423 }
424
425 /// The full 128 KiB WRAM as a flat byte slice (linear address `0..0x1_FFFF`, the same mapping
426 /// [`Self::peek_wram`]'s `0x7E..=0x7F` bank arm uses) — for a host embedder that needs a raw
427 /// memory-map pointer (e.g. a libretro core's `RETRO_MEMORY_SYSTEM_RAM`).
428 #[must_use]
429 pub fn wram(&self) -> &[u8] {
430 &*self.wram
431 }
432
433 /// The mutable counterpart to [`Self::wram`] — same host-embedder use case (a libretro
434 /// frontend's memory-map API hands this pointer to RetroAchievements/cheat tooling that
435 /// writes through it directly).
436 pub fn wram_mut(&mut self) -> &mut [u8] {
437 &mut *self.wram
438 }
439
440 /// Non-intrusive read of an arbitrary 24-bit CPU address, for the debugger overlay's
441 /// disassembly view (`v0.9.0`, T-81-001 PR B). Unlike [`CpuBus::read24`], this does NOT touch
442 /// the open-bus latch, does NOT check watchpoints, and does NOT trigger any I/O register's own
443 /// read side effect (VRAM/CGRAM auto-increment, NMI-flag-clear-on-read, the H/V-counter
444 /// latch, …) — genuinely just peeking. Real 65C816 code only ever executes from WRAM or cart
445 /// ROM/RAM space, so (mirroring [`Self::peek_wram`]'s own "not for register space" posture)
446 /// this only special-cases those two regions; any other address returns `0` rather than
447 /// reaching into a register's live side effects, which is fine since real code never lives
448 /// there anyway. The cart-space branch still calls into the board (some coprocessors gate
449 /// their own ROM/RAM reads on internal state), but passes a neutral `0` open-bus fallback
450 /// rather than the Bus's real, live latch — this peek must never read *or* write that shared
451 /// state.
452 pub fn peek(&mut self, addr24: u32) -> u8 {
453 let bank = (addr24 >> 16) & 0xFF;
454 let addr = (addr24 & 0xFFFF) as u16;
455 match bank {
456 0x7E..=0x7F => self.wram[(addr24 & 0x1_FFFF) as usize],
457 0x00..=0x3F | 0x80..=0xBF if addr < 0x2000 => self.wram[(addr & 0x1FFF) as usize],
458 0x00..=0x3F | 0x80..=0xBF if addr < 0x8000 => 0, // I/O register space; not real code.
459 _ => self.cart.as_mut().map_or(0, |c| c.read24(addr24, 0)),
460 }
461 }
462
463 /// Install the currently-active cheat-code patches (`v0.8.0`, T-81-003), replacing any
464 /// previously installed set. [`CpuBus::read24`] checks this list on every CPU-visible read
465 /// and substitutes a matching patch's value — the same point in the pipeline real Game
466 /// Genie/Pro Action Replay hardware intercepts at, which is why this is a read intercept and
467 /// not a `poke_wram`-style direct write: those codes overwhelmingly target cartridge ROM, not
468 /// WRAM, so a write-based model would silently do nothing for the vast majority of real
469 /// codes. The underlying ROM/RAM byte is never modified — only what the CPU observes reading
470 /// it.
471 pub fn set_cheats(&mut self, patches: &[crate::cheat::CheatPatch]) {
472 self.cheats.clear();
473 self.cheats.extend_from_slice(patches);
474 }
475
476 /// Install the currently-armed read/write watchpoints (`v0.8.0`, T-81-001b), replacing any
477 /// previously installed set. See [`crate::watchpoint::WatchpointState::set_watchpoints`].
478 #[cfg(feature = "debug-hooks")]
479 pub fn set_watchpoints(&mut self, points: &[crate::watchpoint::Watchpoint]) {
480 self.watchpoints.set_watchpoints(points);
481 }
482
483 /// Drain every watchpoint hit recorded since the last call.
484 #[cfg(feature = "debug-hooks")]
485 pub fn take_watchpoint_hits(&mut self) -> alloc::vec::Vec<crate::watchpoint::WatchpointHit> {
486 self.watchpoints.take_hits()
487 }
488
489 /// Set the 8 per-voice audio mute toggles (`v1.0.1`). See
490 /// [`rustysnes_apu::dsp::Dsp::set_voice_mutes`]'s doc for why this is a frontend/debug
491 /// convenience re-synced once per real frame, not real S-DSP hardware state.
492 pub const fn set_voice_mutes(&mut self, mutes: [bool; 8]) {
493 self.apu.set_voice_mutes(mutes);
494 }
495
496 /// Record the CPU's current `PBR:PC` (24-bit, `$bank:offset`) so a watchpoint hit during the
497 /// access this instruction is about to make can attribute itself to the right instruction.
498 /// The scheduler calls this once before each [`rustysnes_cpu::Cpu::step`]
499 /// ([`crate::scheduler::System::run_frame`]/[`crate::scheduler::System::step_instruction`]).
500 #[cfg(feature = "debug-hooks")]
501 pub const fn set_debug_pc(&mut self, pbr_pc: u32) {
502 self.debug_pc = pbr_pc;
503 }
504
505 /// Check a bus access against the armed watchpoint list, tagged with the CPU's `PBR:PC` at
506 /// the moment of the access. Shared by [`CpuBus::read24`]/[`write24`](CpuBus::write24) *and*
507 /// [`DmaBus`]'s A-bus/B-bus methods (`v1.1.0`) — DMA/HDMA-driven accesses were previously
508 /// invisible to watchpoints entirely, which blocked tracing the open-bus-via-DMA-latch
509 /// investigation (`docs/scheduler.md` §Open bus via DMA/HDMA); `debug_pc` still reflects the
510 /// CPU instruction that initiated the transfer, since nothing updates it mid-DMA.
511 #[cfg(feature = "debug-hooks")]
512 fn note_bus_access(&mut self, addr24: u32, value: u8, is_write: bool) {
513 let pc = self.debug_pc;
514 self.watchpoints.check(addr24, value, is_write, pc);
515 }
516
517 /// Whether the PPU has a finished frame ready to present.
518 #[must_use]
519 pub const fn frame_ready(&self) -> bool {
520 self.ppu.frame_ready()
521 }
522
523 /// The PPU framebuffer (256×239 15-bit BGR).
524 #[must_use]
525 pub fn framebuffer(&self) -> &[u16] {
526 self.ppu.framebuffer()
527 }
528
529 // --- The master-clock advance (the lockstep heart). ------------------------------------
530
531 /// Advance the master clock by `n` ticks, stepping the PPU dot clock + SPC accumulator in
532 /// lockstep and re-deriving the NMI/HV-IRQ phases.
533 fn advance_master(&mut self, n: u32) {
534 for _ in 0..n {
535 self.clock.master = self.clock.master.wrapping_add(1);
536 self.clock.dot_accum += 1;
537 // Captured BEFORE `tick_ppu_dot()` (if it fires this sub-tick) increments the PPU's
538 // dot counter — this is the exact dot value [`Ppu::tick_dot`]'s own render-vs-HDMA
539 // ordering decision used internally (it composites the finishing line using the
540 // pre-increment `h`, then increments). Reading `self.ppu.dot()` fresh AFTER the call
541 // instead (an earlier draft did this) observes the POST-increment value, so the HDMA
542 // run-check below would fire a whole dot-window early — on the FIRST of the four
543 // master-clock sub-ticks where the dot reads [`HDMA_RUN_DOT`], not the LAST (the one
544 // coincident with the render call) — silently putting HDMA back ahead of render for
545 // the same line, exactly the ordering this fix exists to prevent.
546 let pre_tick_dot = self.ppu.dot();
547 let dot_ticked = if self.clock.dot_accum >= MASTER_PER_DOT {
548 self.clock.dot_accum -= MASTER_PER_DOT;
549 self.tick_ppu_dot();
550 true
551 } else {
552 false
553 };
554 // HDMA, clock-driven so both its per-frame init (V=0) and per-line transfers stay
555 // scanline-accurate even while the master clock is being advanced *inside* a GP-DMA —
556 // hardware re-initializes HDMA at V=0 and interleaves a transfer at the start of every
557 // visible scanline, preempting the general DMA, regardless of a DMA spanning the frame
558 // boundary. Driving it from the scheduler instead delayed the V=0 init behind a
559 // frame-crossing framebuffer DMA, shifting the whole HDMA table late (Star Fox's
560 // force-blank then missed its own framebuffer DMA). The `in_hdma` guard stops the
561 // transfer's own cost (the nested `advance_master`) from re-triggering the same line.
562 if !self.in_hdma && self.dma.hdma_enable != 0 {
563 let v = self.ppu.scanline();
564 let vh = self.ppu.visible_height();
565 // ares services HDMA at two distinct points (`sfc/cpu/timing.cpp`): a once-per-frame
566 // *setup* at V=0 (`service_hdma_line(0, …)` resets the tables + reloads), and a
567 // per-visible-line *run* at hcounter 1104 = [`HDMA_RUN_DOT`]. Running the transfer at
568 // that exact dot — not at the scanline boundary — latches a mid-line HDMAEN write on
569 // the hardware-correct scanline (the `hdmaen_latch_test` crossing). `dot_ticked` gates
570 // this to the one sub-tick that actually advanced the dot (see `pre_tick_dot`'s doc).
571 if v == 0 {
572 if !self.hdma_setup_done {
573 self.hdma_setup_done = true;
574 self.service_hdma(0, vh);
575 }
576 } else {
577 self.hdma_setup_done = false;
578 if v <= vh
579 && dot_ticked
580 && pre_tick_dot == HDMA_RUN_DOT
581 && self.last_hdma_line != v
582 {
583 self.last_hdma_line = v;
584 self.service_hdma(v, vh);
585 }
586 }
587 }
588 // Super Scope beam-position auto-latch (`v0.9.0`) — gated to the one sub-tick that
589 // actually advanced the dot, same granularity `dot_ticked` already gives the HDMA
590 // check above; a no-op unless port 2 has a Super Scope attached (`Self`'s own doc).
591 if dot_ticked {
592 self.check_superscope_beam();
593 }
594 self.clock.spc_accum += SPC_NUM;
595 while self.clock.spc_accum >= SPC_DEN {
596 self.clock.spc_accum -= SPC_DEN;
597 // Release one SPC700 master cycle in lockstep with the master clock. The four
598 // CPU↔APU port latches live INSIDE the `Apu` (`cpu_read_port`/`cpu_write_port`),
599 // so advancing here at master-clock granularity means a CPU read of $2140-$2143
600 // already observes every SMP port write up to this exact master instant — the
601 // deterministic async resync (T-31-003; `docs/scheduler.md` §async-resync).
602 self.apu.advance_smp_cycle();
603 }
604 // Release a host-synced coprocessor (Super FX/GSU) one master clock at a time, in
605 // lockstep with the CPU's own instruction stream — not drained to completion inside
606 // the single bus write that arms it. Real hardware runs the GSU as a genuinely
607 // concurrent cothread (ares `SuperFX : Thread`); the CPU keeps executing its own
608 // instructions while the GSU works and only observes the result whenever it next
609 // polls, instead of the entire render completing "atomically" before the CPU's next
610 // instruction can run (`Board::coprocessor_tick` doc has the detail).
611 if let Some(c) = self.cart.as_mut() {
612 c.coprocessor_tick();
613 }
614 }
615 }
616
617 /// Run one HDMA phase (`line == 0` → per-frame reset+setup; else the visible-line transfer),
618 /// charging its master-clock cost back onto the scheduler. The `in_hdma` re-entrancy guard
619 /// stops the nested `advance_master(cost)` from re-triggering HDMA for the same line.
620 fn service_hdma(&mut self, line: u16, vh: u16) {
621 self.in_hdma = true;
622 let mut dma = core::mem::take(&mut self.dma);
623 let cost = dma.service_hdma_line(line, vh, self);
624 self.dma = dma;
625 if cost > 0 {
626 self.advance_master(cost);
627 }
628 self.in_hdma = false;
629 }
630
631 /// Tick the PPU one dot through a cart-only view (split borrow), then harvest its NMI/IRQ.
632 fn tick_ppu_dot(&mut self) {
633 // Keep the PPU the single owner of the dot-phase HV-IRQ comparison.
634 let enable_h = self.clock.nmitimen & 0x10 != 0;
635 let enable_v = self.clock.nmitimen & 0x20 != 0;
636 self.ppu
637 .set_hv_irq(enable_h, enable_v, self.clock.htime, self.clock.vtime);
638
639 let mut view = CartView {
640 cart: &mut self.cart,
641 open: self.open_bus,
642 };
643 self.ppu.tick_dot(&mut view);
644
645 if self.ppu.nmi_pending() {
646 self.ppu.ack_nmi();
647 // The RDNMI VBlank flag sets unconditionally; the NMI *interrupt* only when enabled.
648 self.clock.rdnmi_flag = true;
649 if self.clock.nmitimen & 0x80 != 0 {
650 self.clock.nmi_line = true;
651 }
652 }
653 if self.ppu.irq_pending() {
654 self.ppu.ack_irq();
655 self.clock.irq_line = true;
656 }
657 }
658
659 // --- B-bus ($2100-$21FF) register access (PPU, APU ports, WRAM port). ------------------
660
661 fn b_read(&mut self, low: u8) -> u8 {
662 match low {
663 0x00..=0x3F => self.ppu.read_reg(0x2100 | u16::from(low)),
664 // $2140-$2143 — the four CPU↔APU communication ports. A CPU read returns what the
665 // SMP last wrote to that port (a one-way latch, NOT an echo of the CPU's own write).
666 // The APU is already advanced up to "now" by the lockstep accumulator in
667 // `advance_master`, so this observes every SMP write up to this master instant.
668 0x40..=0x43 => self.apu.cpu_read_port(low & 3),
669 0x80 => {
670 let v = self.wram[(self.wram_addr & 0x1_FFFF) as usize];
671 self.wram_addr = (self.wram_addr + 1) & 0x1_FFFF;
672 v
673 }
674 _ => self.open_bus,
675 }
676 }
677
678 fn b_write(&mut self, low: u8, val: u8) {
679 match low {
680 0x00..=0x3F => self.ppu.write_reg(0x2100 | u16::from(low), val),
681 // $2140-$2143 — deposit into the CPU→SMP latch the SMP's IPL/program reads at $F4-$F7.
682 0x40..=0x43 => self.apu.cpu_write_port(low & 3, val),
683 0x80 => {
684 self.wram[(self.wram_addr & 0x1_FFFF) as usize] = val;
685 self.wram_addr = (self.wram_addr + 1) & 0x1_FFFF;
686 }
687 0x81 => self.wram_addr = (self.wram_addr & 0x1_FF00) | u32::from(val),
688 0x82 => self.wram_addr = (self.wram_addr & 0x1_00FF) | (u32::from(val) << 8),
689 0x83 => self.wram_addr = (self.wram_addr & 0x0_FFFF) | (u32::from(val & 1) << 16),
690 _ => {}
691 }
692 }
693
694 // --- CPU registers ($4016/$4017 + $4200-$421F). ---------------------------------------
695
696 fn read_cpu_reg(&mut self, addr: u16) -> u8 {
697 match addr {
698 0x4016 => {
699 let (d1, d2) = self.port_clock(0);
700 (self.open_bus & 0xFC) | (d2 << 1) | d1
701 }
702 0x4017 => {
703 let (d1, d2) = self.port_clock(1);
704 (self.open_bus & 0xE0) | 0x1C | (d2 << 1) | d1
705 }
706 0x4213 => {
707 // RDIO — WRIO ($4201) read back verbatim (ares `cpu.io.pio`).
708 self.pio
709 }
710 0x4210 => {
711 // RDNMI: bit7 = VBlank-occurred flag (read clears), bits0-3 = CPU version (2).
712 let v = (u8::from(self.clock.rdnmi_flag) << 7) | 0x02;
713 self.clock.rdnmi_flag = false;
714 v
715 }
716 0x4211 => {
717 // TIMEUP: bit7 = irq flag (read clears).
718 let v = u8::from(self.clock.irq_line) << 7;
719 self.clock.irq_line = false;
720 v
721 }
722 0x4212 => {
723 // HVBJOY: bit7 vblank, bit6 hblank.
724 (u8::from(self.ppu.in_vblank()) << 7) | (u8::from(self.ppu.in_hblank()) << 6)
725 }
726 0x4214 => self.muldiv.rddiv as u8,
727 0x4215 => (self.muldiv.rddiv >> 8) as u8,
728 0x4216 => self.muldiv.rdmpy as u8,
729 0x4217 => (self.muldiv.rdmpy >> 8) as u8,
730 0x4218..=0x421F => {
731 // Auto-joypad read result: $4218/9 = pad1, $421A/B = pad2.
732 let pad = usize::from(addr >= 0x421A);
733 if addr & 1 == 0 {
734 self.joypad[pad] as u8
735 } else {
736 (self.joypad[pad] >> 8) as u8
737 }
738 }
739 _ => self.open_bus,
740 }
741 }
742
743 fn write_cpu_reg(&mut self, addr: u16, val: u8) {
744 match addr {
745 0x4016 => {
746 // The one physical strobe line is wired to BOTH controller ports simultaneously
747 // (`rustysnes_core::controller`'s module doc) — `Gamepad` ignores it exactly as
748 // before (no functional change to the default path); the other peripherals latch.
749 let strobe = val & 1 != 0;
750 self.joypad_strobe = strobe;
751 self.ports[0].latch(strobe);
752 self.ports[1].latch(strobe);
753 }
754 0x4201 => self.set_pio(val),
755 0x4200 => self.clock.nmitimen = val,
756 0x4202 => self.muldiv.mpya = val,
757 0x4203 => self.muldiv.rdmpy = u16::from(self.muldiv.mpya) * u16::from(val),
758 0x4204 => self.muldiv.dividend = (self.muldiv.dividend & 0xFF00) | u16::from(val),
759 0x4205 => {
760 self.muldiv.dividend = (self.muldiv.dividend & 0x00FF) | (u16::from(val) << 8);
761 }
762 0x4206 => {
763 if val == 0 {
764 self.muldiv.rddiv = 0xFFFF;
765 self.muldiv.rdmpy = self.muldiv.dividend;
766 } else {
767 self.muldiv.rddiv = self.muldiv.dividend / u16::from(val);
768 self.muldiv.rdmpy = self.muldiv.dividend % u16::from(val);
769 }
770 }
771 0x4207 => self.clock.htime = (self.clock.htime & 0x0100) | u16::from(val),
772 0x4208 => self.clock.htime = (self.clock.htime & 0x00FF) | (u16::from(val & 1) << 8),
773 0x4209 => self.clock.vtime = (self.clock.vtime & 0x0100) | u16::from(val),
774 0x420A => self.clock.vtime = (self.clock.vtime & 0x00FF) | (u16::from(val & 1) << 8),
775 0x420B => self.run_gp_dma(val),
776 0x420C => self.dma.hdma_enable = val,
777 0x420D => self.clock.fast_rom = val & 1 != 0,
778 _ => {}
779 }
780 }
781
782 /// One `$4016`/`$4017` clock for controller port `port` — `(data1, data2)`. `Gamepad` (the
783 /// default) is untouched, using [`Bus::joypad`]'s own original single-bit model exactly as
784 /// before this module existed; every other [`PortDevice`] dispatches to
785 /// [`crate::controller::PortState::clock`].
786 fn port_clock(&mut self, port: usize) -> (u8, u8) {
787 if self.ports[port].device == PortDevice::Gamepad {
788 let bit = ((self.joypad[port] & 0x8000) >> 15) as u8;
789 self.joypad[port] = (self.joypad[port] << 1) | 1;
790 return (bit, 0);
791 }
792 let iobit = self.iobit_pin(port);
793 let vh = self.ppu.visible_height();
794 self.ports[port].clock(iobit, vh)
795 }
796
797 /// The IOBIT pin's current level for controller port `port` — WRIO ($4201/$4213) bit 6 for
798 /// port 1, bit 7 for port 2 (ares `Controller::iobit()`).
799 const fn iobit_pin(&self, port: usize) -> bool {
800 self.pio & (0x40 << port) != 0
801 }
802
803 /// WRIO ($4201) write — the falling edge of bit 7 (controller port 2's IOBIT pin) latches the
804 /// PPU's H/V dot counters, the exact mechanism a Super Scope's light sensor drives when it
805 /// "sees" the CRT beam (ares `cpu/io.cpp`: `if(io.pio.bit(7) && !data.bit(7))
806 /// ppu.latchCounters();`). Bit 6 (port 1) has no such wiring on real hardware — a Super Scope
807 /// in port 1 simply never gets an auto-latch, matching `SuperScopeState`'s own doc.
808 const fn set_pio(&mut self, val: u8) {
809 if self.pio & 0x80 != 0 && val & 0x80 == 0 {
810 self.ppu.latch_hv_counters();
811 }
812 self.pio = val;
813 }
814
815 /// Per-master-clock Super Scope beam-detection check (`v0.9.0`) — a no-op, one cheap branch,
816 /// unless port 2 actually has a Super Scope attached (real hardware: only port 2's IOBIT pin
817 /// reaches the PPU latch, `Self::set_pio`). Mirrors ares' `SuperScope::main()`: strobe the
818 /// IOBIT pin low-then-high the instant the beam crosses the target dot on the target
819 /// scanline, latching the H/V counters exactly as a real light sensor would.
820 fn check_superscope_beam(&mut self) {
821 if self.ports[1].device != PortDevice::SuperScope {
822 return;
823 }
824 let vh = self.ppu.visible_height();
825 let Some((target_v, target_dot)) = self.ports[1].super_scope.beam_target(vh) else {
826 return;
827 };
828 if self.ppu.scanline() == target_v && self.ppu.dot() == target_dot {
829 self.set_pio(self.pio & !0x80);
830 self.set_pio(self.pio | 0x80);
831 }
832 }
833
834 /// Run GP-DMA to completion (CPU halted), advancing the master clock by the transfer cost.
835 fn run_gp_dma(&mut self, mask: u8) {
836 let mut dma = core::mem::take(&mut self.dma);
837 // `run_gp` advances the master clock itself, byte-by-byte, via `DmaBus::step` (so the PPU
838 // scanline stays current and V-blank-crossing VRAM writes actually land). Do NOT charge
839 // the returned cost again here — that would double the DMA's wall-time.
840 let _cost = dma.run_gp(mask, self);
841 self.dma = dma;
842 }
843
844 // --- The 24-bit memory decode. ---------------------------------------------------------
845
846 fn decode_read(&mut self, addr24: u32) -> u8 {
847 let bank = (addr24 >> 16) & 0xFF;
848 let addr = (addr24 & 0xFFFF) as u16;
849 match bank {
850 0x7E..=0x7F => self.wram[(addr24 & 0x1_FFFF) as usize],
851 0x00..=0x3F | 0x80..=0xBF => match addr {
852 0x0000..=0x1FFF => self.wram[(addr & 0x1FFF) as usize],
853 0x2100..=0x21FF => self.b_read(addr as u8),
854 0x4016 | 0x4017 | 0x4200..=0x421F => self.read_cpu_reg(addr),
855 0x4300..=0x437F => self
856 .dma
857 .read_reg(((addr >> 4) & 0xF) as usize, (addr & 0xF) as u8),
858 _ => self.cart_read_raw(addr24),
859 },
860 _ => self.cart_read_raw(addr24),
861 }
862 }
863
864 fn decode_write(&mut self, addr24: u32, val: u8) {
865 let bank = (addr24 >> 16) & 0xFF;
866 let addr = (addr24 & 0xFFFF) as u16;
867 match bank {
868 0x7E..=0x7F => self.wram[(addr24 & 0x1_FFFF) as usize] = val,
869 0x00..=0x3F | 0x80..=0xBF => match addr {
870 0x0000..=0x1FFF => self.wram[(addr & 0x1FFF) as usize] = val,
871 0x2100..=0x21FF => self.b_write(addr as u8, val),
872 0x4016 | 0x4200..=0x421F => self.write_cpu_reg(addr, val),
873 0x4300..=0x437F => {
874 let ch = ((addr >> 4) & 0xF) as usize;
875 let reg = (addr & 0xF) as u8;
876 self.dma.write_reg(ch, reg, val);
877 // S-DD1's DMA-address/size snoop (Board::notify_dma_channel doc) — only the
878 // registers that hold that state are worth reporting on.
879 if matches!(reg, 2..=6)
880 && let Some(c) = self.dma.channels.get(ch & 7)
881 {
882 let address = (u32::from(c.source_bank) << 16) | u32::from(c.source_addr);
883 if let Some(cart) = self.cart.as_mut() {
884 cart.board
885 .notify_dma_channel(ch & 7, address, c.count_or_indirect);
886 }
887 }
888 }
889 _ => self.cart_write_raw(addr24, val),
890 },
891 _ => self.cart_write_raw(addr24, val),
892 }
893 }
894
895 fn cart_read_raw(&mut self, addr24: u32) -> u8 {
896 let open_bus = self.open_bus;
897 self.cart
898 .as_mut()
899 .map_or(open_bus, |c| c.read24(addr24, open_bus))
900 }
901
902 fn cart_write_raw(&mut self, addr24: u32, val: u8) {
903 if let Some(c) = self.cart.as_mut() {
904 // Arms a host-synced coprocessor (Super FX/GSU) if this write set Go — it does not
905 // run it. `advance_master`'s per-tick loop drives it forward one master clock at a
906 // time via `Board::coprocessor_tick`, genuinely concurrently with the CPU's own
907 // subsequent instructions (`Board::coprocessor_tick` doc has the detail).
908 c.write24(addr24, val);
909 }
910 }
911
912 /// The access speed (master clocks) for a 24-bit CPU access. Ported from ares `CPU::wait`.
913 const fn access_speed(&self, addr24: u32) -> u32 {
914 // $00-3F/$80-BF:8000-FFFF and $40-7F/$C0-FF:0000-FFFF (ROM region).
915 if addr24 & 0x40_8000 != 0 {
916 return if addr24 & 0x80_0000 != 0 {
917 if self.clock.fast_rom { 6 } else { 8 }
918 } else {
919 8
920 };
921 }
922 // $00-3F/$80-BF:0000-1FFF (WRAM mirror) and :6000-7FFF (expansion).
923 if addr24.wrapping_add(0x6000) & 0x4000 != 0 {
924 return 8;
925 }
926 // $00-3F/$80-BF:2000-3FFF (PPU/APU) and :4200-5FFF (CPU/DMA regs).
927 if addr24.wrapping_sub(0x4000) & 0x7E00 != 0 {
928 return 6;
929 }
930 // $00-3F/$80-BF:4000-41FF (joypad serial).
931 12
932 }
933
934 /// Write the PPU's own section, the APU's own section, the DMA controller's own section,
935 /// then a `"BUS0"` section for WRAM + the Bus's own timing/register state, then (if a cart is
936 /// loaded) its battery SRAM + coprocessor state as a final untagged tail (a presence flag,
937 /// the length-prefixed SRAM bytes, then the board's own `save_state` bytes — the cart has no
938 /// single section of its own since its payload is really "however many bytes the board's own
939 /// implementation writes"). The cart's ROM/header are NOT written: the caller must reload the
940 /// same ROM (`Cart::load`) and install it before calling [`Bus::load_state`], the same "never
941 /// embed a ROM byte" contract every coprocessor board in `rustysnes-cart` already follows.
942 pub fn save_state(&self, w: &mut SaveWriter) {
943 self.ppu.save_state(w);
944 self.apu.save_state(w);
945 self.dma.save_state(w);
946 w.section(*b"BUS0", |s| {
947 self.clock.save_state(s);
948 self.muldiv.save_state(s);
949 s.write_bytes(&*self.wram);
950 s.write_u32(self.wram_addr);
951 s.write_u16(self.joypad[0]);
952 s.write_u16(self.joypad[1]);
953 s.write_bool(self.joypad_strobe);
954 s.write_u8(self.open_bus);
955 s.write_u16(self.last_hdma_line);
956 s.write_bool(self.in_hdma);
957 s.write_bool(self.hdma_setup_done);
958 s.write_u8(self.pio);
959 self.ports[0].save_state(s);
960 self.ports[1].save_state(s);
961 });
962 match &self.cart {
963 Some(cart) => {
964 w.write_bool(true);
965 w.write_len_prefixed(cart.board.sram());
966 cart.board.save_state(w);
967 }
968 None => w.write_bool(false),
969 }
970 }
971
972 /// The inverse of [`Self::save_state`].
973 ///
974 /// # Errors
975 /// [`SaveStateError`] on truncated/corrupt input, a section with unconsumed trailing bytes,
976 /// or [`SaveStateError::Invalid`] if the save-state's cart presence doesn't match this
977 /// `Bus`'s own (a save-state taken with a cart loaded can only be restored onto a `Bus` that
978 /// already has the SAME cart's ROM loaded — via [`rustysnes_cart::Cart::load`] — installed
979 /// first; there is no ROM byte in the save-state to reconstruct it from) or if a restored
980 /// SRAM image's length doesn't match the installed cart's own SRAM size (a mismatched ROM).
981 pub fn load_state(&mut self, r: &mut SaveReader) -> Result<(), SaveStateError> {
982 self.ppu.load_state(r)?;
983 self.apu.load_state(r)?;
984 self.dma.load_state(r)?;
985 let mut s = r.expect_section(*b"BUS0")?;
986 self.clock.load_state(&mut s)?;
987 self.muldiv.load_state(&mut s)?;
988 self.wram.copy_from_slice(s.read_bytes(WRAM_SIZE)?);
989 // wram_addr is a 17-bit register (every use site already masks it & 0x1_FFFF).
990 self.wram_addr = s.read_u32()? & 0x1_FFFF;
991 self.joypad[0] = s.read_u16()?;
992 self.joypad[1] = s.read_u16()?;
993 self.joypad_strobe = s.read_bool()?;
994 self.open_bus = s.read_u8()?;
995 self.last_hdma_line = s.read_u16()?;
996 self.in_hdma = s.read_bool()?;
997 self.hdma_setup_done = s.read_bool()?;
998 self.pio = s.read_u8()?;
999 self.ports[0] = crate::controller::PortState::load_state(&mut s)?;
1000 self.ports[1] = crate::controller::PortState::load_state(&mut s)?;
1001 if s.remaining() != 0 {
1002 return Err(SaveStateError::Invalid(alloc::format!(
1003 "BUS0 section has {} trailing byte(s)",
1004 s.remaining()
1005 )));
1006 }
1007 let had_cart = r.read_bool()?;
1008 match (&mut self.cart, had_cart) {
1009 (Some(cart), true) => {
1010 let sram = r.read_len_prefixed()?;
1011 if sram.len() != cart.board.sram().len() {
1012 return Err(SaveStateError::Invalid(alloc::format!(
1013 "save-state SRAM length {} does not match the installed cart's {} \
1014 (wrong ROM loaded before restoring?)",
1015 sram.len(),
1016 cart.board.sram().len()
1017 )));
1018 }
1019 cart.board.sram_mut().copy_from_slice(sram);
1020 cart.board.load_state(r)?;
1021 }
1022 (None, false) => {}
1023 (Some(_), false) | (None, true) => {
1024 return Err(SaveStateError::Invalid(alloc::string::String::from(
1025 "save-state cart presence does not match this Bus's installed cart \
1026 (load the same ROM before restoring, or restore onto a fresh Bus)",
1027 )));
1028 }
1029 }
1030 Ok(())
1031 }
1032}
1033
1034/// A cart-only view of the Bus for the PPU's `tick_dot` (split borrow: the PPU may need a
1035/// cart-mediated read for Mode 7 / coprocessor boards without aliasing the whole Bus).
1036struct CartView<'a> {
1037 cart: &'a mut Option<Cart>,
1038 open: u8,
1039}
1040
1041impl VideoBus for CartView<'_> {
1042 fn cart_read(&mut self, addr24: u32) -> u8 {
1043 let open = self.open;
1044 self.cart.as_mut().map_or(open, |c| c.read24(addr24, open))
1045 }
1046}
1047
1048/// The DMA controller's view: A-bus (24-bit) via the decode, B-bus via `b_read`/`b_write`.
1049impl DmaBus for Bus {
1050 fn read_a(&mut self, addr: u32) -> u8 {
1051 // The A-bus cannot reach the B-bus or the CPU/DMA I/O registers (ares `validA`).
1052 let bank = (addr >> 16) & 0xFF;
1053 let off = addr & 0xFFFF;
1054 if matches!(bank, 0x00..=0x3F | 0x80..=0xBF)
1055 && matches!(off, 0x2100..=0x21FF | 0x4000..=0x43FF)
1056 {
1057 // Matches ares/bsnes `CPU::Channel::readA` exactly: the invalid branch sets `mdr`
1058 // (this project's `open_bus`) to a hard `0`, not "leave it unchanged" — see
1059 // `docs/scheduler.md` §Open bus via DMA/HDMA for the full citation trail.
1060 self.open_bus = 0;
1061 return 0;
1062 }
1063 let val = self.decode_read(addr);
1064 // DMA/HDMA-driven A-bus reads update the open-bus latch exactly like a CPU read does —
1065 // confirmed by direct citation of ares' AND bsnes' `CPU::Channel::readA`
1066 // (`cpu.r.mdr = validA(address) ? bus.read(address, cpu.r.mdr) : 0;`) and their shared
1067 // `Bus::read`'s default unmapped reader (`[](n24, n8 data) { return data; }` — the
1068 // open-bus echo mechanism itself). DMA/HDMA *writes* deliberately do NOT update it (see
1069 // `write_a`/`write_b` below) — ares' `writeA`/`writeB` never touch `mdr` either. See
1070 // `docs/scheduler.md` §Open bus via DMA/HDMA for the full investigation this fix closes.
1071 self.open_bus = val;
1072 #[cfg(feature = "debug-hooks")]
1073 self.note_bus_access(addr, val, false);
1074 val
1075 }
1076 fn write_a(&mut self, addr: u32, val: u8) {
1077 let bank = (addr >> 16) & 0xFF;
1078 let off = addr & 0xFFFF;
1079 if matches!(bank, 0x00..=0x3F | 0x80..=0xBF)
1080 && matches!(off, 0x2100..=0x21FF | 0x4000..=0x43FF)
1081 {
1082 return;
1083 }
1084 #[cfg(feature = "debug-hooks")]
1085 self.note_bus_access(addr, val, true);
1086 self.decode_write(addr, val);
1087 }
1088 fn read_b(&mut self, addr: u8) -> u8 {
1089 let val = self.b_read(addr);
1090 // See `read_a`'s doc above — DMA/HDMA B-bus reads update open_bus too.
1091 self.open_bus = val;
1092 #[cfg(feature = "debug-hooks")]
1093 self.note_bus_access(0x00_2100 | u32::from(addr), val, false);
1094 val
1095 }
1096 fn write_b(&mut self, addr: u8, val: u8) {
1097 #[cfg(feature = "debug-hooks")]
1098 self.note_bus_access(0x00_2100 | u32::from(addr), val, true);
1099 self.b_write(addr, val);
1100 }
1101 fn step(&mut self, clocks: u32) {
1102 // Advance the whole system (PPU dot clock, SPC, host-synced coprocessor) mid-DMA so the
1103 // scanline that gates VRAM/CGRAM/OAM access is current at each transferred byte.
1104 self.advance_master(clocks);
1105 }
1106 fn scanline(&self) -> u16 {
1107 self.ppu.scanline()
1108 }
1109 fn visible_height(&self) -> u16 {
1110 self.ppu.visible_height()
1111 }
1112 fn hdma_last_line(&self) -> u16 {
1113 self.last_hdma_line
1114 }
1115 fn set_hdma_last_line(&mut self, line: u16) {
1116 self.last_hdma_line = line;
1117 }
1118}
1119
1120/// The 65C816's view: route a 24-bit access + drive the master clock in lockstep.
1121impl CpuBus for Bus {
1122 // `decode_read` must always run first for its side effects (e.g. an NMI-flag-clear-on-read
1123 // register) even when a cheat overrides the value the CPU observes — so this can't be
1124 // rephrased as a plain `if/else` expression the way clippy suggests.
1125 #[allow(clippy::useless_let_if_seq)]
1126 fn read24(&mut self, addr24: u32) -> u8 {
1127 let mut val = self.decode_read(addr24);
1128 // Cheat-code intercept (`v0.8.0`, T-81-003) — `self.cheats` is empty in every build that
1129 // never calls `set_cheats`, so this costs one branch when inactive. See `set_cheats`'s
1130 // doc for why this is a read intercept rather than a WRAM poke.
1131 if !self.cheats.is_empty()
1132 && let Some(patch) = self.cheats.iter().find(|p| p.address == addr24)
1133 {
1134 val = patch.value;
1135 }
1136 self.open_bus = val;
1137 // `v0.8.0`, T-81-001b: logs the value actually observed (post-cheat-intercept), matching
1138 // what the CPU itself sees. Compiled out entirely when `debug-hooks` is off.
1139 #[cfg(feature = "debug-hooks")]
1140 self.note_bus_access(addr24, val, false);
1141 val
1142 }
1143
1144 fn write24(&mut self, addr24: u32, val: u8) {
1145 self.open_bus = val;
1146 #[cfg(feature = "debug-hooks")]
1147 self.note_bus_access(addr24, val, true);
1148 self.decode_write(addr24, val);
1149 }
1150
1151 fn access_cycles(&self, addr24: u32) -> u32 {
1152 self.access_speed(addr24)
1153 }
1154
1155 fn advance(&mut self, clocks: u32) {
1156 // ares `CPU::step`: tick the PPU dot clock, SPC, host-synced coprocessor, and HDMA in
1157 // lockstep. The CPU sequences its calls to this around each access (see `CpuBus`) so a
1158 // register write lands at the hardware-exact hcounter.
1159 self.advance_master(clocks);
1160 }
1161
1162 fn poll_nmi(&mut self) -> bool {
1163 core::mem::take(&mut self.clock.nmi_line)
1164 }
1165
1166 fn poll_irq(&mut self) -> bool {
1167 // OR the PPU/APU HV-IRQ level with any on-cart coprocessor IRQ (SA-1 → S-CPU, SPC7110 RTC,
1168 // …). The `Board::irq_pending` hook is documented to be ORed here; base/host-sync boards
1169 // return `false` so non-coprocessor carts are unaffected.
1170 self.clock.irq_line || self.cart.as_ref().is_some_and(|c| c.board.irq_pending())
1171 }
1172}
1173
1174impl core::fmt::Debug for Bus {
1175 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1176 f.debug_struct("Bus")
1177 .field("cart", &self.cart.as_ref().map(|c| c.board.name()))
1178 .field("master", &self.clock.master)
1179 .field("open_bus", &self.open_bus)
1180 .finish_non_exhaustive()
1181 }
1182}
1183
1184#[cfg(test)]
1185mod tests {
1186 use super::*;
1187
1188 /// `HDMA_RUN_DOT` is now literally `= rustysnes_ppu::RENDER_DOT`, so this can never actually
1189 /// fail post-refactor -- kept as a named regression lock so a future edit that reintroduces a
1190 /// separate literal (e.g. during a merge) fails loudly instead of silently drifting the two
1191 /// dot values apart again (`docs/ppu.md` §Mid-scanline/HDMA-driven register timing).
1192 #[test]
1193 fn hdma_run_dot_matches_ppu_render_dot() {
1194 assert_eq!(HDMA_RUN_DOT, rustysnes_ppu::RENDER_DOT);
1195 }
1196
1197 #[test]
1198 fn default_bus_has_no_cart_and_reads_open() {
1199 let mut bus = Bus::default();
1200 assert!(bus.cart.is_none());
1201 assert_eq!(<Bus as CpuBus>::read24(&mut bus, 0x00_8000), 0);
1202 }
1203
1204 #[test]
1205 fn wram_round_trips() {
1206 let mut bus = Bus::default();
1207 <Bus as CpuBus>::write24(&mut bus, 0x7E_1234, 0xAB);
1208 assert_eq!(<Bus as CpuBus>::read24(&mut bus, 0x7E_1234), 0xAB);
1209 // Low mirror in bank 0 aliases the same WRAM.
1210 <Bus as CpuBus>::write24(&mut bus, 0x00_0042, 0x99);
1211 assert_eq!(<Bus as CpuBus>::read24(&mut bus, 0x7E_0042), 0x99);
1212 }
1213
1214 #[test]
1215 fn wram_and_wram_mut_expose_the_same_flat_128kib() {
1216 let mut bus = Bus::default();
1217 assert_eq!(bus.wram().len(), 0x2_0000);
1218 <Bus as CpuBus>::write24(&mut bus, 0x7E_1234, 0xAB);
1219 assert_eq!(bus.wram()[0x1234], 0xAB);
1220 bus.wram_mut()[0x5678] = 0xCD;
1221 assert_eq!(<Bus as CpuBus>::read24(&mut bus, 0x7E_5678), 0xCD);
1222 }
1223
1224 #[test]
1225 fn peek_reads_wram_without_side_effects() {
1226 let mut bus = Bus::default();
1227 <Bus as CpuBus>::write24(&mut bus, 0x7E_1234, 0xAB);
1228 // A real CPU read first, so open_bus is a known, distinct value.
1229 assert_eq!(<Bus as CpuBus>::read24(&mut bus, 0x7E_1234), 0xAB);
1230 let open_bus_before = bus.open_bus;
1231 // `peek` must return the same byte `read24` would, but never touch `open_bus`.
1232 assert_eq!(bus.peek(0x7E_1234), 0xAB);
1233 assert_eq!(
1234 bus.open_bus, open_bus_before,
1235 "peek must not perturb open_bus"
1236 );
1237 }
1238
1239 #[test]
1240 fn peek_of_io_register_space_is_zero_not_the_live_register() {
1241 let mut bus = Bus::default();
1242 <Bus as CpuBus>::write24(&mut bus, 0x00_4202, 0x10);
1243 <Bus as CpuBus>::write24(&mut bus, 0x00_4203, 0x10);
1244 // $4216 (RDMPY) genuinely holds 0x0100 now via `read24`, but `peek` never reaches
1245 // register space at all (real code never executes from it) — this documents that
1246 // limitation rather than silently returning a wrong "peek" of live register state.
1247 assert_eq!(<Bus as CpuBus>::read24(&mut bus, 0x00_4216), 0x00);
1248 assert_eq!(bus.peek(0x00_4216), 0);
1249 }
1250
1251 #[test]
1252 fn wrio_rdio_round_trips_and_defaults_to_all_ones() {
1253 let mut bus = Bus::default();
1254 assert_eq!(<Bus as CpuBus>::read24(&mut bus, 0x00_4213), 0xFF);
1255 <Bus as CpuBus>::write24(&mut bus, 0x00_4201, 0x55);
1256 assert_eq!(<Bus as CpuBus>::read24(&mut bus, 0x00_4213), 0x55);
1257 }
1258
1259 #[test]
1260 fn wrio_bit7_falling_edge_latches_hv_counters() {
1261 let mut bus = Bus::default();
1262 // Advance a few dots so the latch has a known, non-zero dot value to observe.
1263 for _ in 0..40 {
1264 bus.advance_master(1);
1265 }
1266 let dot_before = bus.ppu.dot();
1267 // Bit 7 starts high (power-on default 0xFF); a write clearing it is the falling edge that
1268 // should latch the H/V counters (ares `cpu/io.cpp`'s `if(io.pio.bit(7) && !data.bit(7))`).
1269 <Bus as CpuBus>::write24(&mut bus, 0x00_4201, 0x00);
1270 let ophct_lo = <Bus as CpuBus>::read24(&mut bus, 0x00_213C);
1271 #[allow(clippy::cast_possible_truncation)]
1272 let expected = (dot_before & 0xFF) as u8;
1273 assert_eq!(
1274 ophct_lo, expected,
1275 "WRIO bit7 falling edge should latch OPHCT"
1276 );
1277 }
1278
1279 #[test]
1280 fn wrio_bit6_falling_edge_does_not_latch() {
1281 let mut bus = Bus::default();
1282 for _ in 0..40 {
1283 bus.advance_master(1);
1284 }
1285 // Port 1's IOBIT (bit 6) has no real-hardware wiring to the PPU latch — only bit 7 does.
1286 <Bus as CpuBus>::write24(&mut bus, 0x00_4201, 0xBF); // clear bit 6, leave bit 7 set
1287 assert_eq!(<Bus as CpuBus>::read24(&mut bus, 0x00_213C), 0);
1288 }
1289
1290 #[test]
1291 fn superscope_beam_latch_fires_at_target_position() {
1292 let mut bus = Bus::default();
1293 bus.set_port_device(1, crate::controller::PortDevice::SuperScope);
1294 bus.set_superscope(1, 10, 5, 0);
1295 let target_dot = 10 + 24;
1296 for _ in 0..2_000_000 {
1297 if bus.ppu.scanline() == 5 && bus.ppu.dot() == target_dot {
1298 break;
1299 }
1300 bus.advance_master(1);
1301 }
1302 assert_eq!(
1303 bus.ppu.scanline(),
1304 5,
1305 "should have reached the target scanline"
1306 );
1307 assert_eq!(
1308 bus.ppu.dot(),
1309 target_dot,
1310 "should have reached the target dot"
1311 );
1312 let ophct_lo = <Bus as CpuBus>::read24(&mut bus, 0x00_213C);
1313 assert_eq!(
1314 ophct_lo, target_dot as u8,
1315 "the beam crossing the target should have auto-latched OPHCT to it"
1316 );
1317 }
1318
1319 #[test]
1320 fn access_speed_map() {
1321 let bus = Bus::default();
1322 assert_eq!(bus.access_speed(0x00_0042), 8); // WRAM mirror
1323 assert_eq!(bus.access_speed(0x00_2100), 6); // PPU
1324 assert_eq!(bus.access_speed(0x00_4016), 12); // joypad
1325 assert_eq!(bus.access_speed(0x00_4200), 6); // CPU regs
1326 assert_eq!(bus.access_speed(0x00_8000), 8); // WS1 ROM (always 8)
1327 assert_eq!(bus.access_speed(0x80_8000), 8); // WS2 ROM, SlowROM default
1328 }
1329
1330 #[test]
1331 fn memsel_fastrom_speeds_up_ws2() {
1332 let mut bus = Bus::default();
1333 <Bus as CpuBus>::write24(&mut bus, 0x00_420D, 0x01); // MEMSEL FastROM
1334 assert_eq!(bus.access_speed(0x80_8000), 6);
1335 assert_eq!(bus.access_speed(0x00_8000), 8); // WS1 unaffected
1336 }
1337
1338 #[test]
1339 fn muldiv_unit() {
1340 let mut bus = Bus::default();
1341 <Bus as CpuBus>::write24(&mut bus, 0x00_4202, 0x10); // MPYA
1342 <Bus as CpuBus>::write24(&mut bus, 0x00_4203, 0x10); // MPYB -> 0x100
1343 assert_eq!(<Bus as CpuBus>::read24(&mut bus, 0x00_4216), 0x00);
1344 assert_eq!(<Bus as CpuBus>::read24(&mut bus, 0x00_4217), 0x01);
1345 <Bus as CpuBus>::write24(&mut bus, 0x00_4204, 0x64); // dividend lo = 100
1346 <Bus as CpuBus>::write24(&mut bus, 0x00_4205, 0x00);
1347 <Bus as CpuBus>::write24(&mut bus, 0x00_4206, 0x07); // / 7 -> 14 r 2
1348 assert_eq!(<Bus as CpuBus>::read24(&mut bus, 0x00_4214), 14);
1349 assert_eq!(<Bus as CpuBus>::read24(&mut bus, 0x00_4216), 2);
1350 }
1351
1352 /// `$4202` (MPYA) is a stable latch, not re-armed per multiply — real hardware documents
1353 /// that a fresh `$4203` (WRMPYB) write alone starts a new multiply against whatever MPYA
1354 /// already holds (`SNESdev`'s Multiplication page). The genuinely undefined case (starting a
1355 /// new multiply/divide before the previous one's 8-cycle latency elapses, `SNESdev`'s Errata
1356 /// page) is deliberately NOT covered here — see `MulDiv`'s own doc comment for why there is
1357 /// no correct value to assert against.
1358 #[test]
1359 fn muldiv_mpya_latch_survives_across_sequential_multiplies() {
1360 let mut bus = Bus::default();
1361 <Bus as CpuBus>::write24(&mut bus, 0x00_4202, 0x05); // MPYA = 5
1362 <Bus as CpuBus>::write24(&mut bus, 0x00_4203, 0x06); // MPYB -> 5*6 = 30
1363 assert_eq!(<Bus as CpuBus>::read24(&mut bus, 0x00_4216), 30);
1364 // MPYA is untouched by the write above; a fresh $4203 alone starts another multiply
1365 // against the SAME latched 5.
1366 <Bus as CpuBus>::write24(&mut bus, 0x00_4203, 0x07); // MPYB -> 5*7 = 35
1367 assert_eq!(<Bus as CpuBus>::read24(&mut bus, 0x00_4216), 35);
1368 }
1369
1370 #[test]
1371 fn master_clock_advances_on_access() {
1372 let mut bus = Bus::default();
1373 let before = bus.clock.master;
1374 // SlowROM ($00:8000) costs 8 master clocks; `advance` is what moves the clock.
1375 let speed = <Bus as CpuBus>::access_cycles(&bus, 0x00_8000);
1376 assert_eq!(speed, 8);
1377 <Bus as CpuBus>::advance(&mut bus, speed);
1378 assert_eq!(bus.clock.master, before + 8);
1379 // `read24`/`write24` are pure accesses now — they do not move the clock.
1380 <Bus as CpuBus>::read24(&mut bus, 0x00_8000);
1381 assert_eq!(bus.clock.master, before + 8);
1382 }
1383}