Skip to main content

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}