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, for every dot except the two long ones.
48const MASTER_PER_DOT: u32 = 4;
49
50/// Duration of an automatic joypad read, in master clocks. ares steps `status.autoJoypadCounter`
51/// 0 -> 33 once every 128 master clocks (`joypadCounter() = counter.cpu & 127`), so the read is busy
52/// for 33 * 128 = 4224 master clocks (~3 scanlines) from vblank entry; `$4212` bit 0 reads 1 and the
53/// result is not yet published for that whole window (`sfc/cpu/timing.cpp` `joypadEdge`).
54const AUTO_JOYPAD_CLOCKS: u64 = 33 * 128;
55
56/// Master clocks from the vblank edge to the START of the automatic joypad read. Hardware does not
57/// begin the read at the edge but ~dot 32.5-95.5 of the first vblank line (fullsnes; AccuracySNES
58/// `F1.08`). Mesen2 (`InternalRegisters.cpp`) begins at the first 256-clock boundary after `hclock`
59/// 130 — `hclock` 256, i.e. dot 64 — which is what this models. Units are **master clocks** (not dots
60/// or CPU cycles): 256 master clocks = 64 dots at 4 master clocks/dot. During `[edge, edge + delay)`
61/// the read is not yet in flight: `$4212` bit 0 reads not-busy, exposing the `F1.10` NMI-entry race.
62const AUTO_JOYPAD_START_DELAY: u64 = 256;
63
64/// Open-bus (MDR) masks for the CPU flag registers' floating bits — the positions ares `CPU::readIO`
65/// leaves as the incoming open-bus value: `$4210` RDNMI bits 4-6, `$4211` TIMEUP bits 0-6, `$4212`
66/// HVBJOY bits 1-5.
67const RDNMI_OPEN_BUS_MASK: u8 = 0x70;
68const TIMEUP_OPEN_BUS_MASK: u8 = 0x7F;
69const HVBJOY_OPEN_BUS_MASK: u8 = 0x3E;
70
71// The two 6-clock dots (`T-06-A`) used to be declared here. They now live in the PPU
72// (`rustysnes_ppu::LONG_DOTS` / `dot_clocks`), which owns the dot model and needs the same layout
73// to place the H-IRQ comparator — and two copies of "which dots are six clocks" is exactly the kind
74// of fact that drifts apart. The measurement that settles it, and why hblank is where they sit, are
75// documented at the declaration.
76
77/// Master clocks the dot currently being completed lasts for.
78///
79/// `short_line` is the dossier's `B2.02` scanline — NTSC, progressive, field set, `V = 240` — where
80/// the line is 1360 clocks rather than 1364. Under this model that is exactly "the two long dots
81/// are not long here": `340 x 4 = 1360`, which is the decomposition the references give for the
82/// short line ("340 dots of 4-clocks"). The PPU decides whether the line is short
83/// ([`rustysnes_ppu::Ppu::is_short_scanline`]) because every input is its own; this function only
84/// turns that into clocks.
85const fn dot_length(dot: u16, short_line: bool) -> u32 {
86    rustysnes_ppu::dot_clocks(dot, short_line)
87}
88/// PPU dot at which each visible scanline's HDMA transfer fires — ares' `hdmaPosition` of hcounter
89/// 1104 (`sfc/cpu/timing.cpp`) divided by [`MASTER_PER_DOT`]. Running the table at this exact dot
90/// (rather than the scanline boundary) latches a mid-line `$420C` write on the hardware-correct
91/// scanline, which is what makes the `hdmaen_latch_test` show a banded HDMAEN-vs-latch crossing.
92/// Defined equal to `rustysnes_ppu::RENDER_DOT` (PPU-owned single source of truth, since this is
93/// fundamentally a video-timing fact) — `hdma_run_dot_matches_ppu_render_dot` below asserts the
94/// two never drift apart.
95const HDMA_RUN_DOT: u16 = rustysnes_ppu::RENDER_DOT;
96/// The 5A22 stalls the CPU once per scanline to refresh WRAM. `DRAM_REFRESH_CLOCKS` is the
97/// pause length (fullsnes/anomie; ares `sfc/cpu/timing.cpp` `step(6)*5 + step(2)`; MesenCE
98/// `SnesMemoryManager::IncMasterClock40`). Modelled as a **reallocation**, not an addition: the
99/// stall advances the (PPU-rollover-fixed) master clock, so it costs the *CPU* 40 clocks of work
100/// per line without lengthening the 357,368-clock NTSC frame — it is the CPU falling behind the
101/// PPU by a fixed amount at a fixed point, exactly like a slow access. This is what makes a
102/// mid-line raster write whose ISR straddles the pause land ~10 dots later, matching MesenCE
103/// (`scripts/raster_crossval/`), while frame length stays correct (`docs/scheduler.md` §DRAM
104/// refresh; a steady-state per-frame master-clock probe confirms no drift).
105const DRAM_REFRESH_CLOCKS: u32 = 40;
106/// The stall must be a whole number of dots (the pause lands cleanly at the start of a dot, and the
107/// regression test measures it in dots); pin that at compile time so a clock-constant refactor can't
108/// silently break it.
109const _: () = assert!(DRAM_REFRESH_CLOCKS.is_multiple_of(MASTER_PER_DOT));
110/// The dot on whose completion the refresh pause fires — line-clock `134 * 4 = 536`, the
111/// "multiple of 8 closest to 536" AccuracySNES `B3.02` and ares (`530 + 8 - dmaCounter`) both
112/// name. Well before `HDMA_RUN_DOT` (276) and both long dots (`H >= 323`), so the +40-clock
113/// injection never crosses a scanline boundary or perturbs HDMA/long-dot alignment.
114const DRAM_REFRESH_DOT: u16 = 134;
115/// SPC700 fractional-clock numerator (master ticks → SMP **base** clocks).
116///
117/// The unit the APU advances per [`rustysnes_apu::Apu::advance_smp_cycle`] call is one SMP *base*
118/// clock = `apuFrequency / 12` (ares `SMP::create(apuFrequency()/12, …)`; `apuFrequency =
119/// 32040 × 768 = 24_606_720` Hz → base = `2_050_560` Hz). A normal SMP access is `SMP_WAIT` = 2
120/// base clocks, giving the ~1.025 MHz effective opcode rate and an exact `32_040` Hz S-DSP sample.
121///
122/// The async resync (`docs/scheduler.md` §async-resync, ADR 0004) is an **integer** accumulator —
123/// no floats, so the SPC domain is bit-deterministic. The exact rational is
124/// `2_050_560 / 21_477_270` (SMP base rate over the NTSC master rate); gcd = 30, giving the reduced
125/// `68_352 / 715_909` kept here to bound accumulator growth (`spc_accum` stays below the
126/// denominator). The DENOMINATOR is region-dependent; see `SPC_DEN_PAL` for why.
127const SPC_NUM: u64 = 68_352;
128/// SPC700 fractional-clock denominator on **NTSC**: `21_477_270 / 30`.
129const SPC_DEN_NTSC: u64 = 715_909;
130/// SPC700 fractional-clock denominator on **PAL**: `21_281_370 / 30`.
131///
132/// **The APU's oscillator is region-independent and the master clock's is not**, so this ratio is
133/// the one place in the core where which console a real machine is matters. The APU runs from its
134/// own 24.576 MHz crystal at a fixed `2_050_560` Hz base rate in both regions; the master clock is
135/// 21.477270 MHz on NTSC and 21.281370 MHz on PAL. Holding *the ratio* fixed — which this used to
136/// do — therefore makes the APU scale with the video clock and run **0.92% slow** on PAL.
137///
138/// Both references agree, from opposite directions. ares
139/// (`sfc/system/system.cpp`) sets `cpuFrequency` per region and leaves
140/// `apuFrequency = 32040.0 * 768.0` never region-set at all. snes9x (`apu/apu.cpp`) carries two
141/// explicit ratios — `15664/328125` and `34176/709379` — which both work out to an APU rate of
142/// exactly **1,025,280 Hz**, differing only in the master-clock denominator. `709_379 * 30` is
143/// `21_281_370`, which is where this number comes from.
144const SPC_DEN_PAL: u64 = 709_379;
145
146/// The master-clock phase + the CPU-side timing registers the Bus advances in lockstep.
147#[derive(Debug, Clone)]
148pub struct Clock {
149    /// Cumulative master-clock ticks since power-on.
150    pub master: u64,
151    /// Master cycles owed to the PPU before its next dot.
152    dot_accum: u32,
153    /// Fractional accumulator for the asynchronous SPC700 domain.
154    spc_accum: u64,
155    /// `$420D` MEMSEL bit 0 — FastROM (`true` = 6-clock WS2 ROM, `false` = 8-clock).
156    fast_rom: bool,
157    /// `$4200` NMITIMEN — bit7 NMI-enable, bit5 V-IRQ, bit4 H-IRQ, bit0 auto-joypad.
158    nmitimen: u8,
159    /// Latched NMI edge awaiting the `CPU` poll (set at `VBlank` only when NMI is enabled).
160    nmi_line: bool,
161    /// `$4210` RDNMI bit7 — the `VBlank`-occurred flag. Set at `VBlank` start **regardless** of
162    /// the `NMITIMEN` enable (hardware), cleared on read. ROMs poll this to sync to `VBlank`
163    /// without taking the interrupt (e.g. gilyon's `wait_for_vblank`).
164    rdnmi_flag: bool,
165    /// RDNMI hold: for four master clocks (one dot / one interrupt poll) after the `VBlank` edge
166    /// sets `rdnmi_flag`, a `$4210` read returns bit 7 set but does **not** clear it — the hardware
167    /// holds `/NMI` across the edge (ares `status.nmiHold`, "hold /NMI for four cycles"). Terranigma
168    /// depends on the flag surviving a read that lands in that window. Set with `rdnmi_flag`,
169    /// consumed at the next dot in [`Bus::tick_ppu_dot`].
170    rdnmi_hold: bool,
171    /// Level IRQ line (HV-IRQ / coprocessor / APU timer), cleared on `$4211` read.
172    irq_line: bool,
173    /// TIMEUP hold: the `/IRQ` mirror of [`Clock::rdnmi_hold`] — a `$4211` read within four master
174    /// clocks of the IRQ edge returns bit 7 set without clearing it (ares `status.irqHold`).
175    irq_hold: bool,
176    /// `$4207/8` HTIME — the H-IRQ comparator.
177    htime: u16,
178    /// `$4209/A` VTIME — the V-IRQ comparator.
179    vtime: u16,
180}
181
182impl Default for Clock {
183    fn default() -> Self {
184        Self {
185            master: 0,
186            dot_accum: 0,
187            spc_accum: 0,
188            fast_rom: false,
189            nmitimen: 0,
190            nmi_line: false,
191            rdnmi_flag: false,
192            rdnmi_hold: false,
193            irq_line: false,
194            irq_hold: false,
195            htime: 0x01FF,
196            vtime: 0x01FF,
197        }
198    }
199}
200
201/// The CPU multiply/divide unit (`$4202-$4206` → `$4214-$4217`). The SNES computes these with an
202/// 8-CPU-cycle hardware latency; the deterministic core resolves them instantly (the result is
203/// what tests read), which is accurate for every documented program that waits for the real
204/// hardware's own latency before reading `RDMPY`/`RDDIV` — as every known commercial title does.
205///
206/// **Deliberately not modeled: the SNESdev-documented overlapping-operation errata** ("Starting
207/// a multiplication (`$4203` WRMPYB) or division (`$4206` WRDIVB) while the 5A22 is still
208/// processing a previous multiplication or division can cause the 5A22 to output erroneous
209/// values to `RDDIV` and/or `RDMPY`," <https://snes.nesdev.org/wiki/Errata>). This is genuinely
210/// **undefined** hardware behavior — no canonical "corrupted" value is documented anywhere, so
211/// there is nothing correct to port; inventing a specific fabricated corruption value would
212/// itself violate the determinism contract's spirit (`docs/adr/0004`) by pretending a real, one
213/// true answer exists for a case real hardware itself doesn't define one for. No known program
214/// relies on this (a program that hit it would already be behaving unpredictably on real
215/// hardware), so this is a **documented, intentional non-goal**, not an open gap — see
216/// `to-dos/VERSION-PLAN.md`'s `v0.5.0 "Fidelity"` hardware-gotcha list for the same reasoning.
217#[derive(Debug, Clone)]
218struct MulDiv {
219    mpya: u8,
220    dividend: u16,
221    rddiv: u16,
222    rdmpy: u16,
223}
224
225impl Default for MulDiv {
226    /// Power-on state: `WRMPYA` = `$FF`, `WRDIV` = `$FFFF`, results zeroed.
227    ///
228    /// These registers are write-only, so the values are not readable directly — but they are real
229    /// latches feeding the ALU, and the ALU output is readable, so the state is observable by
230    /// starting an operation without writing its first operand: `$4203 = 2` with `$4202` untouched
231    /// yields `$01FE`. AccuracySNES `B5.05` probes exactly that.
232    ///
233    /// Provenance, recorded because this is asserted rather than merely recorded: anomie's
234    /// `regs.txt` (r1157) states *"$4202 holds the value $ff on power on and is unchanged on
235    /// reset"* and *"WRDIV holds the value $ffff on power on and is unchanged on reset"* — in a
236    /// document that explicitly marks its uncertain claims with `(?)` and marks neither of these.
237    /// nocash's fullsnes independently lists `$4202`-`$4206` as `(FFh)` power-up under a legend
238    /// distinguishing power-up from reset. bsnes (`sfc/cpu/cpu.hpp`), ares and Mesen2
239    /// (`AluMulDiv::Initialize`) all implement it. **snes9x does not** — it blanket-`memset`s
240    /// `$4200-$42FF` to zero — which is a snes9x bug, not counter-evidence.
241    ///
242    /// No hardware test ROM is known to verify this; do not claim ROM-verified provenance for it.
243    fn default() -> Self {
244        Self {
245            mpya: 0xFF,
246            dividend: 0xFFFF,
247            rddiv: 0,
248            rdmpy: 0,
249        }
250    }
251}
252
253impl Clock {
254    fn save_state(&self, s: &mut SaveWriter) {
255        s.write_u64(self.master);
256        s.write_u32(self.dot_accum);
257        s.write_u64(self.spc_accum);
258        s.write_bool(self.fast_rom);
259        s.write_u8(self.nmitimen);
260        s.write_bool(self.nmi_line);
261        s.write_bool(self.rdnmi_flag);
262        s.write_bool(self.rdnmi_hold);
263        s.write_bool(self.irq_line);
264        s.write_bool(self.irq_hold);
265        s.write_u16(self.htime);
266        s.write_u16(self.vtime);
267    }
268
269    fn load_state(&mut self, s: &mut SaveReader) -> Result<(), SaveStateError> {
270        self.master = s.read_u64()?;
271        self.dot_accum = s.read_u32()?;
272        self.spc_accum = s.read_u64()?;
273        self.fast_rom = s.read_bool()?;
274        self.nmitimen = s.read_u8()?;
275        self.nmi_line = s.read_bool()?;
276        self.rdnmi_flag = s.read_bool()?;
277        self.rdnmi_hold = s.read_bool()?;
278        self.irq_line = s.read_bool()?;
279        self.irq_hold = s.read_bool()?;
280        // htime/vtime are 9-bit comparators (write24 masks bit 8 with & 1 at $4208/$420A already).
281        self.htime = s.read_u16()? & 0x01FF;
282        self.vtime = s.read_u16()? & 0x01FF;
283        Ok(())
284    }
285}
286
287impl MulDiv {
288    fn save_state(&self, s: &mut SaveWriter) {
289        s.write_u8(self.mpya);
290        s.write_u16(self.dividend);
291        s.write_u16(self.rddiv);
292        s.write_u16(self.rdmpy);
293    }
294
295    fn load_state(&mut self, s: &mut SaveReader) -> Result<(), SaveStateError> {
296        self.mpya = s.read_u8()?;
297        self.dividend = s.read_u16()?;
298        self.rddiv = s.read_u16()?;
299        self.rdmpy = s.read_u16()?;
300        Ok(())
301    }
302}
303
304/// Everything mutable lives here.
305pub struct Bus {
306    /// The video subsystem (PPU1 + PPU2).
307    pub ppu: Ppu,
308    /// The audio subsystem (SPC700 + S-DSP + ARAM).
309    pub apu: Apu,
310    /// The loaded cartridge (board mapping + any coprocessor), or `None` before a ROM loads.
311    pub cart: Option<Cart>,
312    /// The 8-channel DMA/HDMA controller (`$420B`/`$420C`, `$43xx`).
313    pub dma: Dma,
314    /// The master-clock phase + CPU timing registers.
315    pub clock: Clock,
316    /// 128 KiB work RAM (`$7E0000-$7FFFFF`).
317    wram: Box<[u8; WRAM_SIZE]>,
318    /// WRAM port address (`$2181-$2183`), auto-incremented by `$2180` access.
319    wram_addr: u32,
320    /// The buttons currently held, per player — what the frontend last set, and what
321    /// `$4218-$421F` reports. **Not** the shift register: a manual read must not destroy it, and
322    /// the strobe reloads from it.
323    joypad: [u16; 2],
324    /// The manual-read shift registers behind `$4016`/`$4017`, reloaded from [`Self::joypad`]
325    /// while the strobe is high.
326    ///
327    /// Separate from the buttons because the pad is a *parallel-load* shift register: `$4016.0`
328    /// high loads it from the button lines and low starts clocking, so a program may strobe and
329    /// re-read as often as it likes within one frame and get the same answer each time. Sharing one
330    /// register with the button state made the second read of a frame return all-ones, and made a
331    /// manual read corrupt the auto-read result — both invisible to a frontend that rewrites the
332    /// state every frame, and both found by AccuracySNES `F1.02`.
333    joypad_shift: [u16; 2],
334    /// The automatic-read *result* latched into `$4218`-`$421F`, which is a different thing from
335    /// the live controller state in [`Self::joypad`].
336    ///
337    /// Hardware copies the ports into these registers once per frame, at the start of vblank, and
338    /// **only when `$4200` bit 0 is set**. Reporting [`Self::joypad`] directly instead makes
339    /// `$4218` track the pad continuously, so software that disarms auto-read to poll `$4016` by
340    /// hand still sees the hardware's answer appear underneath it. AccuracySNES `F1.07` — which
341    /// could not detect this until the battery gained a host input contract, because with nothing
342    /// held both behaviours report `$0000`.
343    joypad_auto: [u16; 2],
344    /// The port snapshot taken at the START of a timed automatic read, held until the read completes
345    /// and then committed to [`Self::joypad_auto`]. See [`Self::begin_auto_joypad`].
346    joypad_auto_pending: [u16; 2],
347    /// `clock.master` instant the in-flight automatic joypad read completes (0 = idle). While
348    /// non-zero, `$4212` bit 0 reads busy and `$4218-$421F` still hold the previous result — the read
349    /// publishes at completion, ~[`AUTO_JOYPAD_CLOCKS`] master clocks after vblank entry.
350    auto_joypad_busy_until: u64,
351    /// `clock.master` instant the armed automatic read is due to START (0 = none pending). Hardware
352    /// does not begin the read at the vblank edge but a few dozen cycles into the first vblank line
353    /// (`AUTO_JOYPAD_START_DELAY`), so `$4212` bit 0 reads NOT-busy for that window and the NMI-entry
354    /// race (`F1.10`) is observable. Set at the vblank edge while armed; consumed by
355    /// [`Self::begin_auto_joypad`] when reached.
356    auto_joypad_start_at: u64,
357    joypad_strobe: bool,
358    /// Per-port peripheral state (`v0.9.0`, Phase 7 niche peripherals) — Mouse/Super Scope/Super
359    /// Multitap. Idle (and touching nothing on `$4016`/`$4017`'s `data1` bit) unless a port's
360    /// [`crate::controller::PortDevice`] is explicitly switched away from the default `Gamepad`
361    /// via [`Self::set_port_device`], in which case `joypad[port]`'s own bit is bypassed instead
362    /// of merged — see [`Self::port_clock`].
363    ports: [PortState; 2],
364    /// WRIO ($4201 write / $4213 read) — the programmable I/O port. Bit6 is controller port 1's
365    /// IOBIT pin, bit7 port 2's (only port 2's is wired to the PPU H/V-counter latch on real
366    /// hardware — a Super Scope's own beam-detection strobe). Reset value `0xFF` (ares
367    /// `cpu.hpp`'s `n8 pio = 0xff`).
368    pio: u8,
369    /// Open-bus latch: the last value driven on the data bus.
370    #[allow(clippy::struct_field_names)] // "open_bus" is the hardware name for the latch.
371    open_bus: u8,
372    muldiv: MulDiv,
373    /// The last visible scanline HDMA was serviced on, so [`Bus::advance_master`] runs each line's
374    /// HDMA exactly once — even when the master clock is being advanced *inside* a GP-DMA (real
375    /// hardware interleaves HDMA at the start of every scanline, preempting the general DMA).
376    last_hdma_line: u16,
377    /// Re-entrancy guard: true while an HDMA transfer's own cycle cost is being charged, so the
378    /// nested `advance_master` doesn't recursively re-trigger HDMA for the same line.
379    in_hdma: bool,
380    /// Whether this frame's V=0 HDMA setup (table reset + reload) has already fired, so it runs
381    /// exactly once per frame independent of the per-line run at [`HDMA_RUN_DOT`].
382    hdma_setup_done: bool,
383    /// Re-entrancy guard: true while a DRAM-refresh pause is charging its own `advance_master(40)`,
384    /// so the nested advance can't recursively re-trigger the refresh. Purely transient (the pause
385    /// fires statelessly, once per scanline, on the single sub-tick that completes dot 133 =
386    /// [`DRAM_REFRESH_DOT`] - 1), so it is neither serialized nor part of determinism.
387    in_refresh: bool,
388    /// Active cheat-code patches (`v0.8.0`, T-81-003) — checked on every CPU-visible read in
389    /// [`CpuBus::read24`]. Empty (the default, and the only state possible unless a frontend
390    /// explicitly calls [`Self::set_cheats`]) costs exactly one `is_empty()` branch per read.
391    cheats: alloc::vec::Vec<crate::cheat::CheatPatch>,
392    /// 65C816 read/write watchpoints (`v0.8.0`, T-81-001b) — compiled out entirely when
393    /// `debug-hooks` is off. See [`crate::watchpoint`]'s module doc.
394    #[cfg(feature = "debug-hooks")]
395    watchpoints: crate::watchpoint::WatchpointState,
396    /// The CPU's `PBR:PC` at the moment of its current access, set by [`Self::set_debug_pc`]
397    /// (the scheduler calls it before each [`rustysnes_cpu::Cpu::step`]) — feeds
398    /// [`crate::watchpoint::WatchpointHit::pbr_pc`]. `debug-hooks`-only, same as `watchpoints`.
399    #[cfg(feature = "debug-hooks")]
400    debug_pc: u32,
401    /// Instruction trace, control-flow events, and the WRAM access heat map (`v1.25.0`, T-FP-C1) —
402    /// same gate and same never-in-a-save-state contract as `watchpoints`. Separately armed, so a
403    /// `debug-hooks` build that never turns tracing on costs one `bool` test per hook and no
404    /// allocation at all. See [`crate::trace`]'s module doc.
405    #[cfg(feature = "debug-hooks")]
406    trace: crate::trace::TraceState,
407}
408
409impl Default for Bus {
410    fn default() -> Self {
411        Self::new(Region::Ntsc)
412    }
413}
414
415impl Bus {
416    /// Construct a power-on Bus for the given console region.
417    ///
418    /// # Panics
419    /// Panics only if the 128 KiB WRAM allocation cannot be sized to the fixed `WRAM_SIZE` array
420    /// (an out-of-memory condition at power-on), which cannot happen for the constant size.
421    #[must_use]
422    pub fn new(region: Region) -> Self {
423        let ppu_region = match region {
424            Region::Ntsc => PpuRegion::Ntsc,
425            Region::Pal => PpuRegion::Pal,
426        };
427        Self {
428            ppu: Ppu::with_region(ppu_region),
429            apu: Apu::new(),
430            cart: None,
431            dma: Dma::new(),
432            clock: Clock::default(),
433            wram: alloc::vec![0u8; WRAM_SIZE]
434                .into_boxed_slice()
435                .try_into()
436                .unwrap(),
437            wram_addr: 0,
438            joypad: [0; 2],
439            joypad_shift: [0; 2],
440            joypad_auto: [0; 2],
441            joypad_auto_pending: [0; 2],
442            auto_joypad_busy_until: 0,
443            auto_joypad_start_at: 0,
444            joypad_strobe: false,
445            ports: [PortState::default(), PortState::default()],
446            pio: 0xFF,
447            open_bus: 0,
448            muldiv: MulDiv::default(),
449            last_hdma_line: u16::MAX,
450            in_hdma: false,
451            hdma_setup_done: false,
452            in_refresh: false,
453            cheats: alloc::vec::Vec::new(),
454            #[cfg(feature = "debug-hooks")]
455            watchpoints: crate::watchpoint::WatchpointState::default(),
456            #[cfg(feature = "debug-hooks")]
457            debug_pc: 0,
458            #[cfg(feature = "debug-hooks")]
459            trace: crate::trace::TraceState::default(),
460        }
461    }
462
463    /// Reconfigure the PPU's region (line count / 50-vs-60 Hz status bit) from the installed
464    /// cart's header, auto-detecting NTSC vs PAL rather than requiring the frontend to guess or
465    /// hardcode it. A no-op when no cart is installed.
466    ///
467    /// Region affects the PPU's line-count/status-bit timeline **and one thing more**: the SPC700
468    /// fractional divisor. The core's master-clock counter is a pure tick count rather than
469    /// wall-clock time, so the differing NTSC/PAL master-clock *rate* (Hz) is otherwise the
470    /// frontend's pacing concern (`docs/adr/0004`) — but the APU runs from its **own** crystal at
471    /// a fixed rate in both regions, so master-ticks-to-SMP-clocks is exactly the conversion where
472    /// which oscillator a real console uses does matter. This doc said the opposite until the
473    /// divisor was made region-dependent; see `SPC_DEN_PAL` and `docs/scheduler.md` §async-resync.
474    // Deliberately NOT `const fn`: `Bus` holds heap-allocated/complex nested state (`Box`-owned
475    // WRAM, the PPU/APU), and this method reads a `Cart` (a `Box<dyn Board>` behind it) — pinning
476    // this to a `const` API guarantee for no actual const-context caller buys nothing and would
477    // force a breaking API change the moment any of that state gains a genuinely non-const need
478    // (logging, validation, additional resets).
479    #[allow(clippy::missing_const_for_fn)]
480    pub fn sync_region_from_cart(&mut self) {
481        let Some(cart) = &self.cart else { return };
482        let ppu_region = match cart.header.region {
483            Region::Ntsc => PpuRegion::Ntsc,
484            Region::Pal => PpuRegion::Pal,
485        };
486        self.ppu.set_region(ppu_region);
487    }
488
489    /// Compute the automatic-read result from the current port state (does **not** publish it).
490    ///
491    /// A latch held high (`joypad_strobe`) reloads the shift registers every clock instead of
492    /// shifting, so all sixteen bits read back as the first bit — `$FFFF`/`$0000` per the held bit
493    /// (AccuracySNES `F1.11`). Otherwise the read returns the latched pad word.
494    fn capture_auto_joypad(&self) -> [u16; 2] {
495        if self.joypad_strobe {
496            core::array::from_fn(|i| {
497                if self.joypad[i] & 0x8000 == 0 {
498                    0x0000
499                } else {
500                    0xFFFF
501                }
502            })
503        } else {
504            self.joypad
505        }
506    }
507
508    /// Perform the automatic joypad read **immediately** (no busy window): latch into
509    /// [`Self::joypad_auto`]. The scheduler uses the timed [`Self::begin_auto_joypad`] instead;
510    /// this instant form is the unit-test helper and any legacy caller.
511    #[cfg(test)]
512    fn poll_auto_joypad(&mut self) {
513        self.joypad_auto = self.capture_auto_joypad();
514    }
515
516    /// Begin a timed automatic joypad read at vblank (ares `status.autoJoypadCounter`, modelled as a
517    /// master-clock deadline). The controller state is snapshotted **now** (ares latches at counter
518    /// 0), but published to [`Self::joypad_auto`] only once the read completes ~[`AUTO_JOYPAD_CLOCKS`]
519    /// master clocks later — so `$4218-$421F` read during the window still hold the *previous*
520    /// frame's result and `$4212` bit 0 reads busy. Called at vblank entry while `$4200` bit 0 is set.
521    fn begin_auto_joypad(&mut self) {
522        self.settle_auto_joypad(); // finish any read still nominally in flight from last frame
523        self.joypad_auto_pending = self.capture_auto_joypad();
524        self.auto_joypad_busy_until = self.clock.master + AUTO_JOYPAD_CLOCKS;
525    }
526
527    /// Fire a start scheduled by the vblank edge once its dot arrives. The `$4200` bit-0 enable is
528    /// re-sampled HERE, not at the edge, so a game that arms or disarms auto-read anywhere in the
529    /// `[edge, start)` window is honoured (hardware latches the enable at the read's start). Called
530    /// every dot from [`Self::tick_ppu_dot`]; a no-op when nothing is scheduled.
531    fn maybe_begin_scheduled_auto_joypad(&mut self) {
532        if self.auto_joypad_start_at != 0 && self.clock.master >= self.auto_joypad_start_at {
533            self.auto_joypad_start_at = 0;
534            if self.clock.nmitimen & 0x01 != 0 {
535                self.begin_auto_joypad();
536            }
537        }
538    }
539
540    /// Publish a completed automatic read: once `clock.master` reaches the deadline, commit the
541    /// snapshot to [`Self::joypad_auto`] and clear the busy window. Idempotent and cheap; call before
542    /// any observation of `$4212` bit 0 or `$4218-$421F`.
543    const fn settle_auto_joypad(&mut self) {
544        if self.auto_joypad_busy_until != 0 && self.clock.master >= self.auto_joypad_busy_until {
545            self.joypad_auto = self.joypad_auto_pending;
546            self.auto_joypad_busy_until = 0;
547        }
548    }
549
550    /// The instant [`Self::poll_auto_joypad`] latch, reachable from the unit tests without running a
551    /// whole frame. Production scheduling does NOT use this path — it uses the timed
552    /// [`Self::begin_auto_joypad`] (start the busy window) + [`Self::settle_auto_joypad`] (publish at
553    /// the deadline); this helper exists only so a test can populate `joypad_auto` in one call.
554    #[cfg(test)]
555    fn poll_auto_joypad_for_test(&mut self) {
556        self.poll_auto_joypad();
557    }
558
559    /// Set the latched controller state for a player (`0` = P1, `1` = P2). 12-bit `BYsSUDLR....`.
560    pub fn set_joypad(&mut self, player: usize, state: u16) {
561        if let Some(slot) = self.joypad.get_mut(player) {
562            *slot = state;
563        }
564    }
565
566    /// The latched controller state for a player (`0` = P1, `1` = P2) — the read side of
567    /// [`Self::set_joypad`], for TAS movie recording (`crate::movie::MovieRecorder`) and the
568    /// debugger overlay.
569    #[must_use]
570    pub fn joypad(&self, player: usize) -> u16 {
571        self.joypad.get(player).copied().unwrap_or(0)
572    }
573
574    /// Select which peripheral is connected to controller port `port` (`0` = port 1, `1` = port
575    /// 2). Defaults to [`PortDevice::Gamepad`] on both ports (this project's original,
576    /// unchanged behavior) until a frontend explicitly calls this — a host/session configuration
577    /// choice, not emulated state (matching [`Self::set_cheats`]/`Self::set_watchpoints`'s own
578    /// "re-established by the frontend, not carried in a save-state" posture — a real SNES has no
579    /// memory of what was plugged in across a power cycle either).
580    pub fn set_port_device(&mut self, port: usize, device: PortDevice) {
581        if let Some(p) = self.ports.get_mut(port) {
582            p.device = device;
583        }
584    }
585
586    /// The peripheral currently connected to controller port `port` — for the debugger overlay
587    /// and the frontend's own input-routing (`v0.9.0`).
588    #[must_use]
589    pub fn port_device(&self, port: usize) -> PortDevice {
590        self.ports
591            .get(port)
592            .map_or(PortDevice::Gamepad, |p| p.device)
593    }
594
595    /// Feed one frame's worth of SNES Mouse input for port `port` (only meaningful when that
596    /// port's device is [`PortDevice::Mouse`]). `dx`/`dy` are raw, unscaled host deltas since the
597    /// last call — the SNES Mouse's own speed multiplier and 127-unit clamp are applied
598    /// internally at the hardware-accurate point (latch time), matching real hardware. Same
599    /// "always replace, re-synced once per frame" convention as [`Self::set_joypad`].
600    pub fn set_mouse(&mut self, port: usize, dx: i32, dy: i32, left: bool, right: bool) {
601        if let Some(p) = self.ports.get_mut(port) {
602            p.mouse.set_input(dx, dy, left, right);
603        }
604    }
605
606    /// Feed one frame's worth of Super Scope input for port `port` (only meaningful when that
607    /// port's device is [`PortDevice::SuperScope`]). `x`/`y` are absolute screen coordinates in
608    /// SNES pixel space (`0..256`, `0..240`-ish; a small negative/over-max margin is allowed and
609    /// means "aimed off-screen", matching real hardware). `buttons` is a bitmask over
610    /// [`crate::controller::scope`]'s `TRIGGER`/`CURSOR`/`TURBO`/`PAUSE` bits — the LIVE physical
611    /// switch/button state; this project reproduces real hardware's own edge-detection internally
612    /// (`crate::controller::SuperScopeState`), so the frontend should pass the raw host state,
613    /// not a pre-toggled value. (A packed bitmask rather than one bool per button, matching
614    /// [`Self::set_joypad`]'s own convention.)
615    pub fn set_superscope(&mut self, port: usize, x: i32, y: i32, buttons: u8) {
616        if let Some(p) = self.ports.get_mut(port) {
617            p.super_scope.set_input(x, y, buttons);
618        }
619    }
620
621    /// Feed one frame's worth of input for Super Multitap sub-pad `sub_index` (`0..=3`) of port
622    /// `port` (only meaningful when that port's device is [`PortDevice::Multitap`]) — same 12-bit
623    /// `BYsSUDLR....` format and per-frame convention as [`Self::set_joypad`].
624    pub fn set_multitap_pad(&mut self, port: usize, sub_index: usize, buttons: u16) {
625        if let Some(p) = self.ports.get_mut(port) {
626            p.multitap.set_pad(sub_index, buttons);
627        }
628    }
629
630    /// The current input state of Super Multitap sub-pad `sub_index` of port `port` — the read
631    /// side of [`Self::set_multitap_pad`], for the debugger overlay and TAS movie recording.
632    #[must_use]
633    pub fn multitap_pad(&self, port: usize, sub_index: usize) -> u16 {
634        self.ports
635            .get(port)
636            .map_or(0, |p| p.multitap.pad(sub_index))
637    }
638
639    /// Non-intrusive read of WRAM for the test harness + debugger (does NOT advance the clock,
640    /// touch open bus, or trip register side effects). I/O registers and the cart region return
641    /// `0` — this is for inspecting RAM-resident test-result variables, not for emulation.
642    #[must_use]
643    pub fn peek_wram(&self, addr24: u32) -> u8 {
644        let bank = (addr24 >> 16) & 0xFF;
645        let addr = (addr24 & 0xFFFF) as u16;
646        match bank {
647            0x7E..=0x7F => self.wram[(addr24 & 0x1_FFFF) as usize],
648            0x00..=0x3F | 0x80..=0xBF if addr < 0x2000 => self.wram[(addr & 0x1FFF) as usize],
649            _ => 0,
650        }
651    }
652
653    /// Non-intrusive write of WRAM (the write counterpart to [`Self::peek_wram`], same
654    /// addressing, same "no clock/open-bus/register side effects" contract) — for `rustysnes-
655    /// script`'s Lua `emu.write`. A write to an address outside WRAM's mirrors is silently
656    /// ignored (matching `peek_wram`'s `_ => 0` read side) rather than erroring, since a script
657    /// address is arbitrary user input, not a bug to surface loudly. (Cheat codes, T-81-003, use
658    /// [`Self::set_cheats`]'s CPU-read intercept instead — real Game Genie/Pro Action Replay
659    /// codes overwhelmingly target cartridge ROM, which this WRAM-only accessor cannot reach.)
660    pub fn poke_wram(&mut self, addr24: u32, val: u8) {
661        let bank = (addr24 >> 16) & 0xFF;
662        let addr = (addr24 & 0xFFFF) as u16;
663        match bank {
664            0x7E..=0x7F => self.wram[(addr24 & 0x1_FFFF) as usize] = val,
665            0x00..=0x3F | 0x80..=0xBF if addr < 0x2000 => self.wram[(addr & 0x1FFF) as usize] = val,
666            _ => {}
667        }
668    }
669
670    /// The full 128 KiB WRAM as a flat byte slice (linear address `0..0x1_FFFF`, the same mapping
671    /// [`Self::peek_wram`]'s `0x7E..=0x7F` bank arm uses) — for a host embedder that needs a raw
672    /// memory-map pointer (e.g. a libretro core's `RETRO_MEMORY_SYSTEM_RAM`).
673    #[must_use]
674    pub fn wram(&self) -> &[u8] {
675        &*self.wram
676    }
677
678    /// The mutable counterpart to [`Self::wram`] — same host-embedder use case (a libretro
679    /// frontend's memory-map API hands this pointer to RetroAchievements/cheat tooling that
680    /// writes through it directly).
681    pub fn wram_mut(&mut self) -> &mut [u8] {
682        &mut *self.wram
683    }
684
685    /// Non-intrusive read of an arbitrary 24-bit CPU address, for the debugger overlay's
686    /// disassembly view (`v0.9.0`, T-81-001 PR B). Unlike [`CpuBus::read24`], this does NOT touch
687    /// the open-bus latch, does NOT check watchpoints, and does NOT trigger any I/O register's own
688    /// read side effect (VRAM/CGRAM auto-increment, NMI-flag-clear-on-read, the H/V-counter
689    /// latch, …) — genuinely just peeking. Real 65C816 code only ever executes from WRAM or cart
690    /// ROM/RAM space, so (mirroring [`Self::peek_wram`]'s own "not for register space" posture)
691    /// this only special-cases those two regions; any other address returns `0` rather than
692    /// reaching into a register's live side effects, which is fine since real code never lives
693    /// there anyway. The cart-space branch still calls into the board (some coprocessors gate
694    /// their own ROM/RAM reads on internal state), but passes a neutral `0` open-bus fallback
695    /// rather than the Bus's real, live latch — this peek must never read *or* write that shared
696    /// state.
697    pub fn peek(&mut self, addr24: u32) -> u8 {
698        let bank = (addr24 >> 16) & 0xFF;
699        let addr = (addr24 & 0xFFFF) as u16;
700        match bank {
701            0x7E..=0x7F => self.wram[(addr24 & 0x1_FFFF) as usize],
702            0x00..=0x3F | 0x80..=0xBF if addr < 0x2000 => self.wram[(addr & 0x1FFF) as usize],
703            0x00..=0x3F | 0x80..=0xBF if addr < 0x8000 => 0, // I/O register space; not real code.
704            _ => self.cart.as_mut().map_or(0, |c| c.read24(addr24, 0)),
705        }
706    }
707
708    /// Install the currently-active cheat-code patches (`v0.8.0`, T-81-003), replacing any
709    /// previously installed set. [`CpuBus::read24`] checks this list on every CPU-visible read
710    /// and substitutes a matching patch's value — the same point in the pipeline real Game
711    /// Genie/Pro Action Replay hardware intercepts at, which is why this is a read intercept and
712    /// not a `poke_wram`-style direct write: those codes overwhelmingly target cartridge ROM, not
713    /// WRAM, so a write-based model would silently do nothing for the vast majority of real
714    /// codes. The underlying ROM/RAM byte is never modified — only what the CPU observes reading
715    /// it.
716    pub fn set_cheats(&mut self, patches: &[crate::cheat::CheatPatch]) {
717        self.cheats.clear();
718        self.cheats.extend_from_slice(patches);
719    }
720
721    /// Install the currently-armed read/write watchpoints (`v0.8.0`, T-81-001b), replacing any
722    /// previously installed set. See [`crate::watchpoint::WatchpointState::set_watchpoints`].
723    #[cfg(feature = "debug-hooks")]
724    pub fn set_watchpoints(&mut self, points: &[crate::watchpoint::Watchpoint]) {
725        self.watchpoints.set_watchpoints(points);
726    }
727
728    /// Drain every watchpoint hit recorded since the last call.
729    #[cfg(feature = "debug-hooks")]
730    pub fn take_watchpoint_hits(&mut self) -> alloc::vec::Vec<crate::watchpoint::WatchpointHit> {
731        self.watchpoints.take_hits()
732    }
733
734    /// Set the 8 per-voice audio mute toggles (`v1.0.1`). See
735    /// [`rustysnes_apu::dsp::Dsp::set_voice_mutes`]'s doc for why this is a frontend/debug
736    /// convenience re-synced once per real frame, not real S-DSP hardware state.
737    pub const fn set_voice_mutes(&mut self, mutes: [bool; 8]) {
738        self.apu.set_voice_mutes(mutes);
739    }
740
741    /// Set the 8 per-voice audio gains (`v1.25.0`, T-FP-F). See
742    /// [`rustysnes_apu::dsp::Dsp::set_voice_gains`] — `1.0` is unity and bit-exact, and this is
743    /// host UI state that never enters a save state.
744    pub const fn set_voice_gains(&mut self, gains: [f32; 8]) {
745        self.apu.set_voice_gains(gains);
746    }
747
748    /// The 8 per-voice output taps, for the mixer's VU meters (`v1.25.0`, T-FP-F).
749    #[must_use]
750    pub const fn voice_taps(&self) -> [(i16, i16); 8] {
751        self.apu.voice_taps()
752    }
753
754    /// Record the CPU's current `PBR:PC` (24-bit, `$bank:offset`) so a watchpoint hit during the
755    /// access this instruction is about to make can attribute itself to the right instruction.
756    /// The scheduler calls this once before each [`rustysnes_cpu::Cpu::step`]
757    /// ([`crate::scheduler::System::run_frame`]/[`crate::scheduler::System::step_instruction`]).
758    #[cfg(feature = "debug-hooks")]
759    pub const fn set_debug_pc(&mut self, pbr_pc: u32) {
760        self.debug_pc = pbr_pc;
761    }
762
763    /// Check a bus access against the armed watchpoint list, tagged with the CPU's `PBR:PC` at
764    /// the moment of the access. Shared by [`CpuBus::read24`]/[`write24`](CpuBus::write24) *and*
765    /// [`DmaBus`]'s A-bus/B-bus methods (`v1.1.0`) — DMA/HDMA-driven accesses were previously
766    /// invisible to watchpoints entirely, which blocked tracing the open-bus-via-DMA-latch
767    /// investigation (`docs/scheduler.md` §Open bus via DMA/HDMA); `debug_pc` still reflects the
768    /// CPU instruction that initiated the transfer, since nothing updates it mid-DMA.
769    #[cfg(feature = "debug-hooks")]
770    fn note_bus_access(&mut self, addr24: u32, value: u8, is_write: bool) {
771        let pc = self.debug_pc;
772        self.watchpoints.check(addr24, value, is_write, pc);
773        // The access heat map (`v1.25.0`, T-FP-C1) shares this hook deliberately: it must see the
774        // exact same set of accesses watchpoints do, DMA/HDMA included, or a transfer-driven hot
775        // spot would read as cold.
776        self.trace.note_access(addr24, is_write);
777    }
778
779    /// The instruction trace / event log / access heat map (`v1.25.0`, T-FP-C1), for the debugger
780    /// to arm and read.
781    #[cfg(feature = "debug-hooks")]
782    #[must_use]
783    pub const fn trace(&self) -> &crate::trace::TraceState {
784        &self.trace
785    }
786
787    /// Mutable access to the same, for arming, clearing, and recording.
788    #[cfg(feature = "debug-hooks")]
789    pub const fn trace_mut(&mut self) -> &mut crate::trace::TraceState {
790        &mut self.trace
791    }
792
793    /// Whether the PPU has a finished frame ready to present.
794    #[must_use]
795    pub const fn frame_ready(&self) -> bool {
796        self.ppu.frame_ready()
797    }
798
799    /// The PPU framebuffer (256×239 15-bit BGR).
800    #[must_use]
801    pub fn framebuffer(&self) -> &[u16] {
802        self.ppu.framebuffer()
803    }
804
805    // --- The master-clock advance (the lockstep heart). ------------------------------------
806
807    /// [`Self::advance_master`], reachable from this crate's **tests only**.
808    ///
809    /// `advance_master` stays private on purpose: timing is driven through the scheduler, and a
810    /// crate-wide handle to raw clock stepping would let non-test code advance the PPU and SPC
811    /// domains around it. `scheduler.rs`'s soft-reset test needs to put the PPU somewhere
812    /// distinctive before resetting, which is a test concern and is gated as one.
813    #[cfg(test)]
814    pub(crate) fn advance_master_for_test(&mut self, n: u32) {
815        self.advance_master(n);
816    }
817
818    /// Advance the master clock by `n` ticks, stepping the PPU dot clock + SPC accumulator in
819    /// lockstep and re-deriving the NMI/HV-IRQ phases.
820    fn advance_master(&mut self, n: u32) {
821        // The SPC denominator is the REGION's master clock, not a constant: the APU's crystal is
822        // region-independent and the master clock's is not, so holding the ratio fixed would scale
823        // the APU with the video clock. See `SPC_DEN_PAL`.
824        //
825        // Read from the PPU rather than cached in `Clock` so it cannot go stale across a region
826        // change or a state restore — the accumulator is already serialized, and a second field
827        // agreeing with it is one more thing that can disagree. Resolved ONCE per call rather than
828        // per sub-tick: nothing inside the loop can change the region, and this is the emulator's
829        // hottest path.
830        let spc_den = match self.ppu.region() {
831            PpuRegion::Ntsc => SPC_DEN_NTSC,
832            PpuRegion::Pal => SPC_DEN_PAL,
833        };
834        for _ in 0..n {
835            self.clock.master = self.clock.master.wrapping_add(1);
836            self.clock.dot_accum += 1;
837            // Captured BEFORE `tick_ppu_dot()` (if it fires this sub-tick) increments the PPU's
838            // dot counter — this is the exact dot value [`Ppu::tick_dot`]'s own render-vs-HDMA
839            // ordering decision used internally (it composites the finishing line using the
840            // pre-increment `h`, then increments). Reading `self.ppu.dot()` fresh AFTER the call
841            // instead (an earlier draft did this) observes the POST-increment value, so the HDMA
842            // run-check below would fire a whole dot-window early — on the FIRST of the four
843            // master-clock sub-ticks where the dot reads [`HDMA_RUN_DOT`], not the LAST (the one
844            // coincident with the render call) — silently putting HDMA back ahead of render for
845            // the same line, exactly the ordering this fix exists to prevent.
846            let pre_tick_dot = self.ppu.dot();
847            // The threshold is the length of the dot being *completed*, which is why it is taken
848            // from `pre_tick_dot` rather than read back after the tick.
849            let this_dot = dot_length(pre_tick_dot, self.ppu.is_short_scanline());
850            let dot_ticked = if self.clock.dot_accum >= this_dot {
851                self.clock.dot_accum -= this_dot;
852                self.tick_ppu_dot();
853                true
854            } else {
855                false
856            };
857            // HDMA, clock-driven so both its per-frame init (V=0) and per-line transfers stay
858            // scanline-accurate even while the master clock is being advanced *inside* a GP-DMA —
859            // hardware re-initializes HDMA at V=0 and interleaves a transfer at the start of every
860            // visible scanline, preempting the general DMA, regardless of a DMA spanning the frame
861            // boundary. Driving it from the scheduler instead delayed the V=0 init behind a
862            // frame-crossing framebuffer DMA, shifting the whole HDMA table late (Star Fox's
863            // force-blank then missed its own framebuffer DMA). The `in_hdma` guard stops the
864            // transfer's own cost (the nested `advance_master`) from re-triggering the same line.
865            if !self.in_hdma && self.dma.hdma_enable != 0 {
866                let v = self.ppu.scanline();
867                let vh = self.ppu.visible_height();
868                // ares services HDMA at two distinct points (`sfc/cpu/timing.cpp`): a once-per-frame
869                // *setup* at V=0 (`service_hdma_line(0, …)` resets the tables + reloads), and a
870                // per-visible-line *run* at hcounter 1104 = [`HDMA_RUN_DOT`]. Running the transfer at
871                // that exact dot — not at the scanline boundary — latches a mid-line HDMAEN write on
872                // the hardware-correct scanline (the `hdmaen_latch_test` crossing). `dot_ticked` gates
873                // this to the one sub-tick that actually advanced the dot (see `pre_tick_dot`'s doc).
874                if v == 0 {
875                    if !self.hdma_setup_done {
876                        self.hdma_setup_done = true;
877                        self.service_hdma(0, vh);
878                    }
879                } else {
880                    self.hdma_setup_done = false;
881                    if v <= vh
882                        && dot_ticked
883                        && pre_tick_dot == HDMA_RUN_DOT
884                        && self.last_hdma_line != v
885                    {
886                        self.last_hdma_line = v;
887                        self.service_hdma(v, vh);
888                    }
889                }
890            }
891            // DRAM refresh: the CPU stalls `DRAM_REFRESH_CLOCKS` once per scanline at line-clock
892            // 536 (the completion of `DRAM_REFRESH_DOT - 1`, since every dot up to there is
893            // `MASTER_PER_DOT` long). Fired statelessly on that single sub-tick — it occurs exactly
894            // once per line because `h` is monotone within a line — so no per-line flag, no
895            // serialized state, no determinism impact. The nested `advance_master` advances the PPU
896            // (and SPC/coprocessor, which keep running through a CPU refresh, as on hardware) by 40
897            // clocks toward the frame's fixed length, so the *CPU* falls 40 clocks behind the PPU
898            // here without the frame growing — the reallocation `docs/scheduler.md` §DRAM refresh
899            // calls for. `in_refresh` stops the nested advance from re-entering; dots 134-144 can't
900            // match the trigger anyway, but the guard makes that explicit.
901            if dot_ticked && pre_tick_dot == DRAM_REFRESH_DOT - 1 && !self.in_refresh {
902                self.in_refresh = true;
903                self.advance_master(DRAM_REFRESH_CLOCKS);
904                self.in_refresh = false;
905            }
906            // Super Scope beam-position auto-latch (`v0.9.0`) — gated to the one sub-tick that
907            // actually advanced the dot, same granularity `dot_ticked` already gives the HDMA
908            // check above; a no-op unless port 2 has a Super Scope attached (`Self`'s own doc).
909            if dot_ticked {
910                self.check_superscope_beam();
911            }
912            self.clock.spc_accum += SPC_NUM;
913            while self.clock.spc_accum >= spc_den {
914                self.clock.spc_accum -= spc_den;
915                // Release one SPC700 master cycle in lockstep with the master clock. The four
916                // CPU↔APU port latches live INSIDE the `Apu` (`cpu_read_port`/`cpu_write_port`),
917                // so advancing here at master-clock granularity means a CPU read of $2140-$2143
918                // already observes every SMP port write up to this exact master instant — the
919                // deterministic async resync (T-31-003; `docs/scheduler.md` §async-resync).
920                self.apu.advance_smp_cycle();
921            }
922            // Release a host-synced coprocessor (Super FX/GSU) one master clock at a time, in
923            // lockstep with the CPU's own instruction stream — not drained to completion inside
924            // the single bus write that arms it. Real hardware runs the GSU as a genuinely
925            // concurrent cothread (ares `SuperFX : Thread`); the CPU keeps executing its own
926            // instructions while the GSU works and only observes the result whenever it next
927            // polls, instead of the entire render completing "atomically" before the CPU's next
928            // instruction can run (`Board::coprocessor_tick` doc has the detail).
929            if let Some(c) = self.cart.as_mut() {
930                c.coprocessor_tick();
931            }
932        }
933    }
934
935    /// Run one HDMA phase (`line == 0` → per-frame reset+setup; else the visible-line transfer),
936    /// charging its master-clock cost back onto the scheduler. The `in_hdma` re-entrancy guard
937    /// stops the nested `advance_master(cost)` from re-triggering HDMA for the same line.
938    fn service_hdma(&mut self, line: u16, vh: u16) {
939        self.in_hdma = true;
940        let mut dma = core::mem::take(&mut self.dma);
941        let cost = dma.service_hdma_line(line, vh, self);
942        self.dma = dma;
943        if cost > 0 {
944            self.advance_master(cost);
945        }
946        self.in_hdma = false;
947    }
948
949    /// Tick the PPU one dot through a cart-only view (split borrow), then harvest its NMI/IRQ.
950    fn tick_ppu_dot(&mut self) {
951        // Consume the previous dot's RDNMI/TIMEUP hold. The hardware holds `/NMI` and `/IRQ` for
952        // four master clocks (one interrupt poll = one dot) after the edge, during which a
953        // `$4210`/`$4211` read returns bit 7 set but does NOT clear it; ares does the same via
954        // `status.nmiHold`/`status.irqHold`, cleared one poll later. The hold is set alongside the
955        // flag below, so clearing it here (before this dot can set it again) makes the window exactly
956        // one dot — the dot the flag was raised on.
957        self.clock.rdnmi_hold = false;
958        self.clock.irq_hold = false;
959        // Keep the PPU the single owner of the dot-phase HV-IRQ comparison.
960        let enable_h = self.clock.nmitimen & 0x10 != 0;
961        let enable_v = self.clock.nmitimen & 0x20 != 0;
962        self.ppu
963            .set_hv_irq(enable_h, enable_v, self.clock.htime, self.clock.vtime);
964
965        let mut view = CartView {
966            cart: &mut self.cart,
967            open: self.open_bus,
968        };
969        self.ppu.tick_dot(&mut view);
970
971        if self.ppu.nmi_pending() {
972            self.ppu.ack_nmi();
973            // The RDNMI VBlank flag sets unconditionally; the NMI *interrupt* only when enabled.
974            self.clock.rdnmi_flag = true;
975            // Hold /NMI across this edge: a $4210 read this dot returns the flag without clearing it.
976            self.clock.rdnmi_hold = true;
977            if self.clock.nmitimen & 0x80 != 0 {
978                self.clock.nmi_line = true;
979            }
980            // The automatic joypad read runs at the start of vblank while armed — but NOT at this
981            // edge: hardware begins it ~dot 32.5-95.5 into the first vblank line, so `$4212` bit 0
982            // reads not-busy for that window (the `F1.10` race) and the start position is observable
983            // (`F1.08`). Schedule the potential start for `AUTO_JOYPAD_START_DELAY` clocks from here
984            // UNCONDITIONALLY of the current arm bit — `$4200` bit 0 is re-sampled at the start dot
985            // itself (in `maybe_begin_scheduled_auto_joypad`), so arming or disarming auto-read
986            // anywhere in the window is honoured, matching the hardware that latches the enable at the
987            // start of the read, not at the vblank edge.
988            self.auto_joypad_start_at = self.clock.master + AUTO_JOYPAD_START_DELAY;
989        }
990        self.maybe_begin_scheduled_auto_joypad();
991        // Complete a timed automatic read when its deadline passes, so `$4212` bit 0 and the
992        // `$4218-$421F` result reflect the true ~4224-clock window even without an intervening
993        // register read. The in-flight snapshot + deadline are serialized (`FORMAT_VERSION` 5), so a
994        // save mid-window restores identically.
995        self.settle_auto_joypad();
996        // RDNMI's VBlank flag is cleared by a read *and*, independently, at the end of VBlank.
997        // Modelling only the read left it set through the whole active display, so code that
998        // polls $4210 outside VBlank saw a VBlank that had already ended and acted a frame late.
999        // Stateless because the flag can only ever be raised during VBlank. AccuracySNES B4.05.
1000        if !self.ppu.in_vblank() {
1001            self.clock.rdnmi_flag = false;
1002        }
1003        if self.ppu.irq_pending() {
1004            self.ppu.ack_irq();
1005            self.clock.irq_line = true;
1006            // Hold /IRQ across this edge: a $4211 read this dot returns the flag without clearing it.
1007            self.clock.irq_hold = true;
1008        }
1009    }
1010
1011    // --- B-bus ($2100-$21FF) register access (PPU, APU ports, WRAM port). ------------------
1012
1013    fn b_read(&mut self, low: u8) -> u8 {
1014        match low {
1015            // $2137 (SLHV) is a *software* latch of the H/V counters, and it is gated by the same
1016            // pin the light gun uses: `$4201` bit 7 drives port 2's IOBIT, and the counter latch
1017            // only responds while that bit is set. superfamicom.org's register reference is
1018            // explicit — reading $2137 latches "if bit 7 of $4201 is set", and "when bit a is 0,
1019            // no latching can occur". `Self::set_pio` already models the falling-edge latch on the
1020            // same pin; this is the other half of that wiring, and it lives here rather than in
1021            // the PPU because the Bus is what owns the pin.
1022            //
1023            // The read itself still happens: $2137 carries no data of its own and returns PPU1
1024            // open bus either way, so only the side effect is suppressed. Found by AccuracySNES
1025            // C3.10; snes9x and Mesen2 both gate it and RustySNES did not.
1026            0x37 if self.pio & 0x80 == 0 => self.ppu.ppu1_open_bus(),
1027            0x00..=0x3F => self.ppu.read_reg(0x2100 | u16::from(low)),
1028            // $2140-$2143 — the four CPU↔APU communication ports. A CPU read returns what the
1029            // SMP last wrote to that port (a one-way latch, NOT an echo of the CPU's own write).
1030            // The APU is already advanced up to "now" by the lockstep accumulator in
1031            // `advance_master`, so this observes every SMP write up to this master instant.
1032            0x40..=0x43 => self.apu.cpu_read_port(low & 3),
1033            0x80 => {
1034                let v = self.wram[(self.wram_addr & 0x1_FFFF) as usize];
1035                self.wram_addr = (self.wram_addr + 1) & 0x1_FFFF;
1036                v
1037            }
1038            _ => self.open_bus,
1039        }
1040    }
1041
1042    fn b_write(&mut self, low: u8, val: u8) {
1043        match low {
1044            0x00..=0x3F => self.ppu.write_reg(0x2100 | u16::from(low), val),
1045            // $2140-$2143 — deposit into the CPU→SMP latch the SMP's IPL/program reads at $F4-$F7.
1046            0x40..=0x43 => self.apu.cpu_write_port(low & 3, val),
1047            0x80 => {
1048                self.wram[(self.wram_addr & 0x1_FFFF) as usize] = val;
1049                self.wram_addr = (self.wram_addr + 1) & 0x1_FFFF;
1050            }
1051            0x81 => self.wram_addr = (self.wram_addr & 0x1_FF00) | u32::from(val),
1052            0x82 => self.wram_addr = (self.wram_addr & 0x1_00FF) | (u32::from(val) << 8),
1053            0x83 => self.wram_addr = (self.wram_addr & 0x0_FFFF) | (u32::from(val & 1) << 16),
1054            _ => {}
1055        }
1056    }
1057
1058    // --- CPU registers ($4016/$4017 + $4200-$421F). ---------------------------------------
1059
1060    fn read_cpu_reg(&mut self, addr: u16) -> u8 {
1061        // Publish a completed automatic joypad read (and clear its busy window) before observing
1062        // `$4212` bit 0 or the `$4218-$421F` result, so a read at any dot sees the exact state.
1063        self.settle_auto_joypad();
1064        match addr {
1065            0x4016 => {
1066                let (d1, d2) = self.port_clock(0);
1067                (self.open_bus & 0xFC) | (d2 << 1) | d1
1068            }
1069            0x4017 => {
1070                let (d1, d2) = self.port_clock(1);
1071                (self.open_bus & 0xE0) | 0x1C | (d2 << 1) | d1
1072            }
1073            0x4213 => {
1074                // RDIO — WRIO ($4201) read back verbatim (ares `cpu.io.pio`).
1075                self.pio
1076            }
1077            0x4210 => {
1078                // RDNMI: bit7 = VBlank-occurred flag (read clears), bits4-6 = open bus (the MDR,
1079                // held in `self.open_bus` — the pre-read last-driven value), bits0-3 = CPU version
1080                // (2). ares `CPU::readIO` $4210 leaves bits 4-6 as the incoming data (open bus) and
1081                // only writes `io.version` into bits 0-3 and the flag into bit 7.
1082                let v = (u8::from(self.clock.rdnmi_flag) << 7)
1083                    | (self.open_bus & RDNMI_OPEN_BUS_MASK)
1084                    | 0x02;
1085                // Within four master clocks of the VBlank edge the flag is held: return it set but
1086                // do not clear it (ares `rdnmi()` skips the clear while `nmiHold`).
1087                if !self.clock.rdnmi_hold {
1088                    self.clock.rdnmi_flag = false;
1089                }
1090                v
1091            }
1092            0x4211 => {
1093                // TIMEUP: bit7 = irq flag (read clears), bits0-6 = open bus (MDR). ares `readIO`
1094                // $4211 writes only bit 7 and leaves the rest as the incoming open-bus data.
1095                let v =
1096                    (u8::from(self.clock.irq_line) << 7) | (self.open_bus & TIMEUP_OPEN_BUS_MASK);
1097                // Held for four master clocks after the IRQ edge — see `$4210` above.
1098                if !self.clock.irq_hold {
1099                    self.clock.irq_line = false;
1100                }
1101                v
1102            }
1103            0x4212 => {
1104                // HVBJOY: bit7 vblank, bit6 hblank, bits1-5 open bus, bit0 auto-joypad busy.
1105                // Busy while auto-read is armed (`$4200` bit 0) AND the timed read has not yet
1106                // completed (the `settle_auto_joypad` above zeroed the deadline if it passed) —
1107                // ares `io.autoJoypadPoll && status.autoJoypadCounter < 33`.
1108                let busy =
1109                    u8::from(self.clock.nmitimen & 0x01 != 0 && self.auto_joypad_busy_until != 0);
1110                (u8::from(self.ppu.in_vblank()) << 7)
1111                    | (u8::from(self.ppu.in_hblank()) << 6)
1112                    | (self.open_bus & HVBJOY_OPEN_BUS_MASK)
1113                    | busy
1114            }
1115            0x4214 => self.muldiv.rddiv as u8,
1116            0x4215 => (self.muldiv.rddiv >> 8) as u8,
1117            0x4216 => self.muldiv.rdmpy as u8,
1118            0x4217 => (self.muldiv.rdmpy >> 8) as u8,
1119            0x4218..=0x421F => {
1120                // Auto-joypad read result: $4218/9 = pad1, $421A/B = pad2.
1121                let pad = usize::from(addr >= 0x421A);
1122                if addr & 1 == 0 {
1123                    self.joypad_auto[pad] as u8
1124                } else {
1125                    (self.joypad_auto[pad] >> 8) as u8
1126                }
1127            }
1128            _ => self.open_bus,
1129        }
1130    }
1131
1132    fn write_cpu_reg(&mut self, addr: u16, val: u8) {
1133        match addr {
1134            0x4016 => {
1135                // The one physical strobe line is wired to BOTH controller ports simultaneously
1136                // (`rustysnes_core::controller`'s module doc) — `Gamepad` ignores it exactly as
1137                // before (no functional change to the default path); the other peripherals latch.
1138                let strobe = val & 1 != 0;
1139                // A parallel load, not an edge: while the strobe is high the shift registers track
1140                // the button lines, and the falling edge simply stops them tracking. So the reload
1141                // happens on the way down as well as while high — otherwise a program that raises
1142                // the strobe, changes nothing, and lowers it would freeze whatever the buttons were
1143                // at the *rising* edge rather than at the falling one. Reloading here is what lets
1144                // a program strobe twice in one frame and read the same buttons twice.
1145                if strobe || self.joypad_strobe {
1146                    self.joypad_shift = self.joypad;
1147                }
1148                self.joypad_strobe = strobe;
1149                self.ports[0].latch(strobe);
1150                self.ports[1].latch(strobe);
1151            }
1152            0x4201 => self.set_pio(val),
1153            0x4200 => {
1154                let was_enabled = self.clock.nmitimen & 0x80 != 0;
1155                self.clock.nmitimen = val;
1156                // The NMI enable is a LEVEL, not an edge. RDNMI's flag latches at the start of
1157                // VBlank and stays latched until read, so enabling NMI while it is already up
1158                // delivers the interrupt immediately rather than waiting for the next VBlank.
1159                // Modelling only the VBlank edge meant a program that latched VBlank, then enabled
1160                // NMI, silently lost that frame's interrupt. AccuracySNES `B4.06` [ERRATA], which
1161                // snes9x and Mesen2 both passed while this failed.
1162                if !was_enabled && val & 0x80 != 0 && self.clock.rdnmi_flag {
1163                    self.clock.nmi_line = true;
1164                }
1165            }
1166            0x4202 => self.muldiv.mpya = val,
1167            0x4203 => self.muldiv.rdmpy = u16::from(self.muldiv.mpya) * u16::from(val),
1168            0x4204 => self.muldiv.dividend = (self.muldiv.dividend & 0xFF00) | u16::from(val),
1169            0x4205 => {
1170                self.muldiv.dividend = (self.muldiv.dividend & 0x00FF) | (u16::from(val) << 8);
1171            }
1172            0x4206 => {
1173                if val == 0 {
1174                    self.muldiv.rddiv = 0xFFFF;
1175                    self.muldiv.rdmpy = self.muldiv.dividend;
1176                } else {
1177                    self.muldiv.rddiv = self.muldiv.dividend / u16::from(val);
1178                    self.muldiv.rdmpy = self.muldiv.dividend % u16::from(val);
1179                }
1180            }
1181            0x4207 => self.clock.htime = (self.clock.htime & 0x0100) | u16::from(val),
1182            0x4208 => self.clock.htime = (self.clock.htime & 0x00FF) | (u16::from(val & 1) << 8),
1183            0x4209 => self.clock.vtime = (self.clock.vtime & 0x0100) | u16::from(val),
1184            0x420A => self.clock.vtime = (self.clock.vtime & 0x00FF) | (u16::from(val & 1) << 8),
1185            0x420B => self.run_gp_dma(val),
1186            0x420C => self.dma.hdma_enable = val,
1187            0x420D => self.clock.fast_rom = val & 1 != 0,
1188            _ => {}
1189        }
1190    }
1191
1192    /// One `$4016`/`$4017` clock for controller port `port` — `(data1, data2)`. `Gamepad` (the
1193    /// default) is untouched, using [`Bus::joypad`]'s own original single-bit model exactly as
1194    /// before this module existed; every other [`PortDevice`] dispatches to
1195    /// [`crate::controller::PortState::clock`].
1196    fn port_clock(&mut self, port: usize) -> (u8, u8) {
1197        if self.ports[port].device == PortDevice::Gamepad {
1198            if self.joypad_strobe {
1199                // Held high, the register is being reloaded continuously, so it never advances:
1200                // every read returns the first bit. Software that reads without lowering the
1201                // strobe gets B over and over, which is the behaviour a latch-then-read driver
1202                // depends on not happening by accident.
1203                self.joypad_shift[port] = self.joypad[port];
1204                return (((self.joypad[port] & 0x8000) >> 15) as u8, 0);
1205            }
1206            // Shifting in ones is the pad's real behaviour once its sixteen data bits are gone:
1207            // nothing is left driving the line low, so reads 17-32 return 1. That is how software
1208            // tells a standard pad from a peripheral.
1209            let bit = ((self.joypad_shift[port] & 0x8000) >> 15) as u8;
1210            self.joypad_shift[port] = (self.joypad_shift[port] << 1) | 1;
1211            return (bit, 0);
1212        }
1213        let iobit = self.iobit_pin(port);
1214        let vh = self.ppu.visible_height();
1215        self.ports[port].clock(iobit, vh)
1216    }
1217
1218    /// The IOBIT pin's current level for controller port `port` — WRIO ($4201/$4213) bit 6 for
1219    /// port 1, bit 7 for port 2 (ares `Controller::iobit()`).
1220    const fn iobit_pin(&self, port: usize) -> bool {
1221        self.pio & (0x40 << port) != 0
1222    }
1223
1224    /// WRIO ($4201) write — the falling edge of bit 7 (controller port 2's IOBIT pin) latches the
1225    /// PPU's H/V dot counters, the exact mechanism a Super Scope's light sensor drives when it
1226    /// "sees" the CRT beam (ares `cpu/io.cpp`: `if(io.pio.bit(7) && !data.bit(7))
1227    /// ppu.latchCounters();`). Bit 6 (port 1) has no such wiring on real hardware — a Super Scope
1228    /// in port 1 simply never gets an auto-latch, matching `SuperScopeState`'s own doc.
1229    const fn set_pio(&mut self, val: u8) {
1230        if self.pio & 0x80 != 0 && val & 0x80 == 0 {
1231            self.ppu.latch_hv_counters();
1232        }
1233        self.pio = val;
1234    }
1235
1236    /// Per-master-clock Super Scope beam-detection check (`v0.9.0`) — a no-op, one cheap branch,
1237    /// unless port 2 actually has a Super Scope attached (real hardware: only port 2's IOBIT pin
1238    /// reaches the PPU latch, `Self::set_pio`). Mirrors ares' `SuperScope::main()`: strobe the
1239    /// IOBIT pin low-then-high the instant the beam crosses the target dot on the target
1240    /// scanline, latching the H/V counters exactly as a real light sensor would.
1241    fn check_superscope_beam(&mut self) {
1242        if self.ports[1].device != PortDevice::SuperScope {
1243            return;
1244        }
1245        let vh = self.ppu.visible_height();
1246        let Some((target_v, target_dot)) = self.ports[1].super_scope.beam_target(vh) else {
1247            return;
1248        };
1249        if self.ppu.scanline() == target_v && self.ppu.dot() == target_dot {
1250            self.set_pio(self.pio & !0x80);
1251            self.set_pio(self.pio | 0x80);
1252        }
1253    }
1254
1255    /// Run GP-DMA to completion (CPU halted), advancing the master clock by the transfer cost.
1256    fn run_gp_dma(&mut self, mask: u8) {
1257        let mut dma = core::mem::take(&mut self.dma);
1258        // `run_gp` advances the master clock itself, byte-by-byte, via `DmaBus::step` (so the PPU
1259        // scanline stays current and V-blank-crossing VRAM writes actually land). Do NOT charge
1260        // the returned cost again here — that would double the DMA's wall-time.
1261        let _cost = dma.run_gp(mask, self);
1262        self.dma = dma;
1263    }
1264
1265    // --- The 24-bit memory decode. ---------------------------------------------------------
1266
1267    fn decode_read(&mut self, addr24: u32) -> u8 {
1268        let bank = (addr24 >> 16) & 0xFF;
1269        let addr = (addr24 & 0xFFFF) as u16;
1270        match bank {
1271            0x7E..=0x7F => self.wram[(addr24 & 0x1_FFFF) as usize],
1272            0x00..=0x3F | 0x80..=0xBF => match addr {
1273                0x0000..=0x1FFF => self.wram[(addr & 0x1FFF) as usize],
1274                0x2100..=0x21FF => self.b_read(addr as u8),
1275                0x4016 | 0x4017 | 0x4200..=0x421F => self.read_cpu_reg(addr),
1276                0x4300..=0x437F => self
1277                    .dma
1278                    .read_reg(((addr >> 4) & 0xF) as usize, (addr & 0xF) as u8),
1279                _ => self.cart_read_raw(addr24),
1280            },
1281            _ => self.cart_read_raw(addr24),
1282        }
1283    }
1284
1285    fn decode_write(&mut self, addr24: u32, val: u8) {
1286        let bank = (addr24 >> 16) & 0xFF;
1287        let addr = (addr24 & 0xFFFF) as u16;
1288        match bank {
1289            0x7E..=0x7F => self.wram[(addr24 & 0x1_FFFF) as usize] = val,
1290            0x00..=0x3F | 0x80..=0xBF => match addr {
1291                0x0000..=0x1FFF => self.wram[(addr & 0x1FFF) as usize] = val,
1292                0x2100..=0x21FF => self.b_write(addr as u8, val),
1293                0x4016 | 0x4200..=0x421F => self.write_cpu_reg(addr, val),
1294                0x4300..=0x437F => {
1295                    let ch = ((addr >> 4) & 0xF) as usize;
1296                    let reg = (addr & 0xF) as u8;
1297                    self.dma.write_reg(ch, reg, val);
1298                    // S-DD1's DMA-address/size snoop (Board::notify_dma_channel doc) — only the
1299                    // registers that hold that state are worth reporting on.
1300                    if matches!(reg, 2..=6)
1301                        && let Some(c) = self.dma.channels.get(ch & 7)
1302                    {
1303                        let address = (u32::from(c.source_bank) << 16) | u32::from(c.source_addr);
1304                        if let Some(cart) = self.cart.as_mut() {
1305                            cart.board
1306                                .notify_dma_channel(ch & 7, address, c.count_or_indirect);
1307                        }
1308                    }
1309                }
1310                _ => self.cart_write_raw(addr24, val),
1311            },
1312            _ => self.cart_write_raw(addr24, val),
1313        }
1314    }
1315
1316    fn cart_read_raw(&mut self, addr24: u32) -> u8 {
1317        let open_bus = self.open_bus;
1318        self.cart
1319            .as_mut()
1320            .map_or(open_bus, |c| c.read24(addr24, open_bus))
1321    }
1322
1323    fn cart_write_raw(&mut self, addr24: u32, val: u8) {
1324        if let Some(c) = self.cart.as_mut() {
1325            // Arms a host-synced coprocessor (Super FX/GSU) if this write set Go — it does not
1326            // run it. `advance_master`'s per-tick loop drives it forward one master clock at a
1327            // time via `Board::coprocessor_tick`, genuinely concurrently with the CPU's own
1328            // subsequent instructions (`Board::coprocessor_tick` doc has the detail).
1329            c.write24(addr24, val);
1330        }
1331    }
1332
1333    /// The access speed (master clocks) for a 24-bit CPU access. Ported from ares `CPU::wait`.
1334    const fn access_speed(&self, addr24: u32) -> u32 {
1335        // $00-3F/$80-BF:8000-FFFF and $40-7F/$C0-FF:0000-FFFF (ROM region).
1336        if addr24 & 0x40_8000 != 0 {
1337            return if addr24 & 0x80_0000 != 0 {
1338                if self.clock.fast_rom { 6 } else { 8 }
1339            } else {
1340                8
1341            };
1342        }
1343        // $00-3F/$80-BF:0000-1FFF (WRAM mirror) and :6000-7FFF (expansion).
1344        if addr24.wrapping_add(0x6000) & 0x4000 != 0 {
1345            return 8;
1346        }
1347        // $00-3F/$80-BF:2000-3FFF (PPU/APU) and :4200-5FFF (CPU/DMA regs).
1348        if addr24.wrapping_sub(0x4000) & 0x7E00 != 0 {
1349            return 6;
1350        }
1351        // $00-3F/$80-BF:4000-41FF (joypad serial).
1352        12
1353    }
1354
1355    /// Write the PPU's own section, the APU's own section, the DMA controller's own section,
1356    /// then a `"BUS0"` section for WRAM + the Bus's own timing/register state, then (if a cart is
1357    /// loaded) its battery SRAM + coprocessor state as a final untagged tail (a presence flag,
1358    /// the length-prefixed SRAM bytes, then the board's own `save_state` bytes — the cart has no
1359    /// single section of its own since its payload is really "however many bytes the board's own
1360    /// implementation writes"). The cart's ROM/header are NOT written: the caller must reload the
1361    /// same ROM (`Cart::load`) and install it before calling [`Bus::load_state`], the same "never
1362    /// embed a ROM byte" contract every coprocessor board in `rustysnes-cart` already follows.
1363    pub fn save_state(&self, w: &mut SaveWriter) {
1364        self.ppu.save_state(w);
1365        self.apu.save_state(w);
1366        self.dma.save_state(w);
1367        w.section(*b"BUS0", |s| {
1368            self.clock.save_state(s);
1369            self.muldiv.save_state(s);
1370            s.write_bytes(&*self.wram);
1371            s.write_u32(self.wram_addr);
1372            s.write_u16(self.joypad[0]);
1373            s.write_u16(self.joypad[1]);
1374            s.write_u16(self.joypad_shift[0]);
1375            s.write_u16(self.joypad_shift[1]);
1376            s.write_u16(self.joypad_auto[0]);
1377            s.write_u16(self.joypad_auto[1]);
1378            s.write_bool(self.joypad_strobe);
1379            s.write_u8(self.open_bus);
1380            s.write_u16(self.last_hdma_line);
1381            s.write_bool(self.in_hdma);
1382            s.write_bool(self.hdma_setup_done);
1383            s.write_u8(self.pio);
1384            self.ports[0].save_state(s);
1385            self.ports[1].save_state(s);
1386            // In-flight automatic joypad read (`FORMAT_VERSION` 5): the start snapshot + the busy
1387            // deadline, so a save taken during the ~4224-clock window restores an identical machine
1388            // state — the busy flag and the deferred `$4218-$421F` publish survive exactly.
1389            s.write_u16(self.joypad_auto_pending[0]);
1390            s.write_u16(self.joypad_auto_pending[1]);
1391            s.write_u64(self.auto_joypad_busy_until);
1392            // The scheduled auto-read START (`FORMAT_VERSION` 9): a save taken in the window between
1393            // the VBlank edge and the read's start dot must restore the pending start, or the read
1394            // never begins on load.
1395            s.write_u64(self.auto_joypad_start_at);
1396        });
1397        match &self.cart {
1398            Some(cart) => {
1399                w.write_bool(true);
1400                w.write_len_prefixed(cart.board.sram());
1401                cart.board.save_state(w);
1402            }
1403            None => w.write_bool(false),
1404        }
1405    }
1406
1407    /// The inverse of [`Self::save_state`].
1408    ///
1409    /// # Errors
1410    /// [`SaveStateError`] on truncated/corrupt input, a section with unconsumed trailing bytes,
1411    /// or [`SaveStateError::Invalid`] if the save-state's cart presence doesn't match this
1412    /// `Bus`'s own (a save-state taken with a cart loaded can only be restored onto a `Bus` that
1413    /// already has the SAME cart's ROM loaded — via [`rustysnes_cart::Cart::load`] — installed
1414    /// first; there is no ROM byte in the save-state to reconstruct it from) or if a restored
1415    /// SRAM image's length doesn't match the installed cart's own SRAM size (a mismatched ROM).
1416    pub fn load_state(&mut self, r: &mut SaveReader) -> Result<(), SaveStateError> {
1417        self.ppu.load_state(r)?;
1418        self.apu.load_state(r)?;
1419        self.dma.load_state(r)?;
1420        let mut s = r.expect_section(*b"BUS0")?;
1421        self.clock.load_state(&mut s)?;
1422        self.muldiv.load_state(&mut s)?;
1423        self.wram.copy_from_slice(s.read_bytes(WRAM_SIZE)?);
1424        // wram_addr is a 17-bit register (every use site already masks it & 0x1_FFFF).
1425        self.wram_addr = s.read_u32()? & 0x1_FFFF;
1426        self.joypad[0] = s.read_u16()?;
1427        self.joypad[1] = s.read_u16()?;
1428        self.joypad_shift[0] = s.read_u16()?;
1429        self.joypad_shift[1] = s.read_u16()?;
1430        self.joypad_auto[0] = s.read_u16()?;
1431        self.joypad_auto[1] = s.read_u16()?;
1432        self.joypad_strobe = s.read_bool()?;
1433        self.open_bus = s.read_u8()?;
1434        self.last_hdma_line = s.read_u16()?;
1435        self.in_hdma = s.read_bool()?;
1436        self.hdma_setup_done = s.read_bool()?;
1437        self.pio = s.read_u8()?;
1438        self.ports[0] = crate::controller::PortState::load_state(&mut s)?;
1439        self.ports[1] = crate::controller::PortState::load_state(&mut s)?;
1440        // In-flight automatic joypad read (`FORMAT_VERSION` 5). A pre-5 blob's `BUS0` section ends
1441        // above, so these reads fail loudly on it (the documented "old blob fails, no migration"
1442        // convention), which is why the format major was bumped.
1443        self.joypad_auto_pending[0] = s.read_u16()?;
1444        self.joypad_auto_pending[1] = s.read_u16()?;
1445        self.auto_joypad_busy_until = s.read_u64()?;
1446        // The scheduled auto-read start (`FORMAT_VERSION` 9).
1447        self.auto_joypad_start_at = s.read_u64()?;
1448        if s.remaining() != 0 {
1449            return Err(SaveStateError::Invalid(alloc::format!(
1450                "BUS0 section has {} trailing byte(s)",
1451                s.remaining()
1452            )));
1453        }
1454        let had_cart = r.read_bool()?;
1455        match (&mut self.cart, had_cart) {
1456            (Some(cart), true) => {
1457                let sram = r.read_len_prefixed()?;
1458                if sram.len() != cart.board.sram().len() {
1459                    return Err(SaveStateError::Invalid(alloc::format!(
1460                        "save-state SRAM length {} does not match the installed cart's {} \
1461                         (wrong ROM loaded before restoring?)",
1462                        sram.len(),
1463                        cart.board.sram().len()
1464                    )));
1465                }
1466                cart.board.sram_mut().copy_from_slice(sram);
1467                cart.board.load_state(r)?;
1468            }
1469            (None, false) => {}
1470            (Some(_), false) | (None, true) => {
1471                return Err(SaveStateError::Invalid(alloc::string::String::from(
1472                    "save-state cart presence does not match this Bus's installed cart \
1473                     (load the same ROM before restoring, or restore onto a fresh Bus)",
1474                )));
1475            }
1476        }
1477        Ok(())
1478    }
1479}
1480
1481/// A cart-only view of the Bus for the PPU's `tick_dot` (split borrow: the PPU may need a
1482/// cart-mediated read for Mode 7 / coprocessor boards without aliasing the whole Bus).
1483struct CartView<'a> {
1484    cart: &'a mut Option<Cart>,
1485    open: u8,
1486}
1487
1488impl VideoBus for CartView<'_> {
1489    fn cart_read(&mut self, addr24: u32) -> u8 {
1490        let open = self.open;
1491        self.cart.as_mut().map_or(open, |c| c.read24(addr24, open))
1492    }
1493}
1494
1495/// The DMA controller's view: A-bus (24-bit) via the decode, B-bus via `b_read`/`b_write`.
1496impl DmaBus for Bus {
1497    fn read_a(&mut self, addr: u32) -> u8 {
1498        // The A-bus cannot reach the B-bus or the CPU/DMA I/O registers (ares `validA`).
1499        let bank = (addr >> 16) & 0xFF;
1500        let off = addr & 0xFFFF;
1501        if matches!(bank, 0x00..=0x3F | 0x80..=0xBF)
1502            && matches!(off, 0x2100..=0x21FF | 0x4000..=0x43FF)
1503        {
1504            // The invalid branch sets the memory data register (this project's `open_bus`) to a
1505            // hard `0`, not "leave it unchanged" — corroborated by every reference; see
1506            // `docs/scheduler.md` §Open bus via DMA/HDMA for the full citation trail.
1507            self.open_bus = 0;
1508            return 0;
1509        }
1510        let val = self.decode_read(addr);
1511        // DMA/HDMA-driven A-bus reads update the open-bus latch exactly like a CPU read does —
1512        // the valid branch echoes the bus value and the invalid branch yields 0, with an unmapped
1513        // read returning the latch unchanged (the open-bus echo mechanism itself). Corroborated
1514        // against the references as behavioural oracles; no third-party emulator code is
1515        // incorporated. DMA/HDMA *writes* deliberately do NOT update it (see
1516        // `write_a`/`write_b` below) — ares' `writeA`/`writeB` never touch `mdr` either. See
1517        // `docs/scheduler.md` §Open bus via DMA/HDMA for the full investigation this fix closes.
1518        self.open_bus = val;
1519        #[cfg(feature = "debug-hooks")]
1520        self.note_bus_access(addr, val, false);
1521        val
1522    }
1523    fn write_a(&mut self, addr: u32, val: u8) {
1524        let bank = (addr >> 16) & 0xFF;
1525        let off = addr & 0xFFFF;
1526        if matches!(bank, 0x00..=0x3F | 0x80..=0xBF)
1527            && matches!(off, 0x2100..=0x21FF | 0x4000..=0x43FF)
1528        {
1529            return;
1530        }
1531        #[cfg(feature = "debug-hooks")]
1532        self.note_bus_access(addr, val, true);
1533        self.decode_write(addr, val);
1534    }
1535    fn read_b(&mut self, addr: u8) -> u8 {
1536        let val = self.b_read(addr);
1537        // See `read_a`'s doc above — DMA/HDMA B-bus reads update open_bus too.
1538        self.open_bus = val;
1539        #[cfg(feature = "debug-hooks")]
1540        self.note_bus_access(0x00_2100 | u32::from(addr), val, false);
1541        val
1542    }
1543    fn write_b(&mut self, addr: u8, val: u8) {
1544        #[cfg(feature = "debug-hooks")]
1545        self.note_bus_access(0x00_2100 | u32::from(addr), val, true);
1546        self.b_write(addr, val);
1547    }
1548    fn step(&mut self, clocks: u32) {
1549        // Advance the whole system (PPU dot clock, SPC, host-synced coprocessor) mid-DMA so the
1550        // scanline that gates VRAM/CGRAM/OAM access is current at each transferred byte.
1551        self.advance_master(clocks);
1552    }
1553    fn scanline(&self) -> u16 {
1554        self.ppu.scanline()
1555    }
1556    fn visible_height(&self) -> u16 {
1557        self.ppu.visible_height()
1558    }
1559    fn hdma_last_line(&self) -> u16 {
1560        self.last_hdma_line
1561    }
1562    fn set_hdma_last_line(&mut self, line: u16) {
1563        self.last_hdma_line = line;
1564    }
1565}
1566
1567/// The 65C816's view: route a 24-bit access + drive the master clock in lockstep.
1568impl CpuBus for Bus {
1569    // `decode_read` must always run first for its side effects (e.g. an NMI-flag-clear-on-read
1570    // register) even when a cheat overrides the value the CPU observes — so this can't be
1571    // rephrased as a plain `if/else` expression the way clippy suggests.
1572    #[allow(clippy::useless_let_if_seq)]
1573    fn read24(&mut self, addr24: u32) -> u8 {
1574        let mut val = self.decode_read(addr24);
1575        // Cheat-code intercept (`v0.8.0`, T-81-003) — `self.cheats` is empty in every build that
1576        // never calls `set_cheats`, so this costs one branch when inactive. See `set_cheats`'s
1577        // doc for why this is a read intercept rather than a WRAM poke.
1578        if !self.cheats.is_empty()
1579            && let Some(patch) = self.cheats.iter().find(|p| p.address == addr24)
1580        {
1581            val = patch.value;
1582        }
1583        self.open_bus = val;
1584        // `v0.8.0`, T-81-001b: logs the value actually observed (post-cheat-intercept), matching
1585        // what the CPU itself sees. Compiled out entirely when `debug-hooks` is off.
1586        #[cfg(feature = "debug-hooks")]
1587        self.note_bus_access(addr24, val, false);
1588        val
1589    }
1590
1591    fn write24(&mut self, addr24: u32, val: u8) {
1592        self.open_bus = val;
1593        #[cfg(feature = "debug-hooks")]
1594        self.note_bus_access(addr24, val, true);
1595        self.decode_write(addr24, val);
1596    }
1597
1598    fn access_cycles(&self, addr24: u32) -> u32 {
1599        self.access_speed(addr24)
1600    }
1601
1602    fn advance(&mut self, clocks: u32) {
1603        // ares `CPU::step`: tick the PPU dot clock, SPC, host-synced coprocessor, and HDMA in
1604        // lockstep. The CPU sequences its calls to this around each access (see `CpuBus`) so a
1605        // register write lands at the hardware-exact hcounter.
1606        self.advance_master(clocks);
1607    }
1608
1609    fn poll_nmi(&mut self) -> bool {
1610        core::mem::take(&mut self.clock.nmi_line)
1611    }
1612
1613    fn poll_irq(&mut self) -> bool {
1614        // OR the PPU/APU HV-IRQ level with any on-cart coprocessor IRQ (SA-1 → S-CPU, SPC7110 RTC,
1615        // …). The `Board::irq_pending` hook is documented to be ORed here; base/host-sync boards
1616        // return `false` so non-coprocessor carts are unaffected.
1617        self.clock.irq_line || self.cart.as_ref().is_some_and(|c| c.board.irq_pending())
1618    }
1619}
1620
1621impl core::fmt::Debug for Bus {
1622    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1623        f.debug_struct("Bus")
1624            .field("cart", &self.cart.as_ref().map(|c| c.board.name()))
1625            .field("master", &self.clock.master)
1626            .field("open_bus", &self.open_bus)
1627            .finish_non_exhaustive()
1628    }
1629}
1630
1631#[cfg(test)]
1632mod tests {
1633    use super::*;
1634
1635    /// Dossier `B2.03`: PAL with interlace on has a 1368-clock, **341**-dot scanline at `V = 311`
1636    /// on field-set frames, so the PAL frame alternates 425,568 / 425,572 master clocks.
1637    ///
1638    /// Interlace is enabled through `SETINI $2133` bit 0 rather than by poking the field directly,
1639    /// so the test drives the same path a game would.
1640    #[test]
1641    fn the_pal_interlaced_frame_gains_the_long_scanline() {
1642        let mut bus = Bus::new(Region::Pal);
1643        bus.ppu.write_reg(0x2133, 0x01);
1644        let mut frame = bus.ppu.frame_count();
1645        let mut last = bus.clock.master;
1646        let mut lens = std::vec::Vec::new();
1647        while lens.len() < 4 {
1648            bus.advance_master(MASTER_PER_DOT);
1649            if bus.ppu.frame_count() != frame {
1650                frame = bus.ppu.frame_count();
1651                lens.push(bus.clock.master - last);
1652                last = bus.clock.master;
1653            }
1654        }
1655        // One of the two phases carries the extra 4 clocks; which one depends on the field's
1656        // power-on value, so assert the SET rather than a fixed order -- pinning the order would
1657        // be pinning an initial condition this row does not care about.
1658        let mut kinds = lens.clone();
1659        kinds.sort_unstable();
1660        kinds.dedup();
1661        assert_eq!(
1662            kinds,
1663            std::vec![425_568, 425_572],
1664            "PAL interlaced frames must alternate by the 4 clocks the long scanline adds, got {lens:?}"
1665        );
1666    }
1667
1668    /// The long line is PAL+interlace only. Progressive PAL -- the default -- must not pick it up,
1669    /// which is what separates it from `B2.02`'s NTSC case.
1670    #[test]
1671    fn progressive_pal_does_not_gain_the_long_scanline() {
1672        let mut bus = Bus::new(Region::Pal);
1673        let mut frame = bus.ppu.frame_count();
1674        let mut last = bus.clock.master;
1675        let mut lens = std::vec::Vec::new();
1676        while lens.len() < 3 {
1677            bus.advance_master(MASTER_PER_DOT);
1678            if bus.ppu.frame_count() != frame {
1679                frame = bus.ppu.frame_count();
1680                lens.push(bus.clock.master - last);
1681                last = bus.clock.master;
1682            }
1683        }
1684        assert_eq!(lens, std::vec![425_568, 425_568, 425_568]);
1685    }
1686
1687    /// Dossier `B2.02`: the NTSC progressive frame alternates 357,368 / 357,364 master clocks,
1688    /// because scanline 240 of every other frame is 1360 clocks rather than 1364.
1689    ///
1690    /// Asserted on the frame TOTAL rather than by reaching into `dot_length`, so it measures the
1691    /// behaviour a cart can observe (`B2.07`'s 60.0988 Hz is this number) instead of restating the
1692    /// implementation. Note the total must be read from `clock.master`, not from the amount passed
1693    /// to `advance_master` -- the DRAM-refresh reallocation advances the clock by 40 per line
1694    /// beyond what the caller supplies, and counting the caller's side reports 346,888.
1695    #[test]
1696    fn the_ntsc_frame_alternates_between_the_normal_and_short_scanline() {
1697        let mut bus = Bus::new(Region::Ntsc);
1698        let mut frame = bus.ppu.frame_count();
1699        let mut last = bus.clock.master;
1700        let mut lens = std::vec::Vec::new();
1701        while lens.len() < 4 {
1702            bus.advance_master(MASTER_PER_DOT);
1703            if bus.ppu.frame_count() != frame {
1704                frame = bus.ppu.frame_count();
1705                lens.push(bus.clock.master - last);
1706                last = bus.clock.master;
1707            }
1708        }
1709        assert_eq!(
1710            lens,
1711            std::vec![357_368, 357_364, 357_368, 357_364],
1712            "NTSC frames must alternate by the 4 clocks the short scanline removes"
1713        );
1714    }
1715
1716    /// The short line is NTSC-only: PAL has its own long-scanline case (`B2.03`, not yet modelled)
1717    /// and must not pick up the NTSC one. Without the region term in `is_short_scanline` this fails.
1718    #[test]
1719    fn the_pal_frame_is_not_shortened() {
1720        let mut bus = Bus::new(Region::Pal);
1721        let mut frame = bus.ppu.frame_count();
1722        let mut last = bus.clock.master;
1723        let mut lens = std::vec::Vec::new();
1724        while lens.len() < 3 {
1725            bus.advance_master(MASTER_PER_DOT);
1726            if bus.ppu.frame_count() != frame {
1727                frame = bus.ppu.frame_count();
1728                lens.push(bus.clock.master - last);
1729                last = bus.clock.master;
1730            }
1731        }
1732        assert_eq!(lens, std::vec![425_568, 425_568, 425_568]);
1733    }
1734
1735    /// The APU runs at the **same wall-clock rate in both regions**, because its crystal is not
1736    /// the master clock's.
1737    ///
1738    /// This is the one conversion in the core where which console a real machine is matters, and
1739    /// it was wrong: the divisor was pinned to the NTSC master clock, so the APU scaled with the
1740    /// video clock and ran 0.92% slow on PAL. Both references disagree with that from opposite
1741    /// directions — ares never region-sets `apuFrequency` at all, and snes9x carries two explicit
1742    /// ratios (`15664/328125`, `34176/709379`) that work out to the identical 1,025,280 Hz.
1743    ///
1744    /// Asserted as SMP clocks released per *master tick*, which must differ between the regions by
1745    /// exactly the ratio of the two master clocks — that is what leaves the wall-clock APU rate
1746    /// identical.
1747    #[test]
1748    fn the_apu_rate_is_region_independent() {
1749        /// S-DSP samples emitted over `master_ticks`, which is directly proportional to the SMP
1750        /// clocks released — one sample per 64 base clocks — and needs no new counter to observe.
1751        fn smp_samples(region: Region, frames: u64, ticks_per_frame: u64) -> usize {
1752            let mut bus = Bus::new(region);
1753            let mut sink = std::vec::Vec::new();
1754            let mut total = 0;
1755            for _ in 0..frames {
1756                let mut n = 0;
1757                while n < ticks_per_frame {
1758                    bus.advance_master(MASTER_PER_DOT);
1759                    n += u64::from(MASTER_PER_DOT);
1760                }
1761                // Drained once per frame, not once per dot: ~640 samples a frame is far inside the
1762                // 16,384-entry FIFO, and draining per dot is four million pointless calls.
1763                bus.apu.drain_audio(&mut sink);
1764                total += sink.len();
1765                sink.clear();
1766            }
1767            total
1768        }
1769
1770        // Many frames, so the ~0.92% difference is far larger than the one-sample quantisation.
1771        // Both regions are run over the SAME number of master ticks, so what is compared is
1772        // APU-per-master-tick and not anything about frame length.
1773        let ntsc = smp_samples(Region::Ntsc, 40, 425_568);
1774        let pal = smp_samples(Region::Pal, 40, 425_568);
1775        assert!(
1776            pal > ntsc,
1777            "PAL must produce MORE audio per master tick than NTSC — its master clock is slower \
1778             (21.281370 vs 21.477270 MHz) while the APU crystal is the same, so one master tick is \
1779             worth more APU. Got NTSC {ntsc} samples, PAL {pal}"
1780        );
1781        #[allow(clippy::cast_precision_loss)]
1782        let ratio = (pal as f64) / (ntsc as f64);
1783        // 21_477_270 / 21_281_370 = 1.0092053...; the tolerance is one accumulator tick either way.
1784        assert!(
1785            (ratio - 1.009_205).abs() < 0.000_05,
1786            "the two divisors must differ by exactly the ratio of the two master clocks; got \
1787             {ratio} from NTSC {ntsc} samples, PAL {pal}"
1788        );
1789    }
1790
1791    /// `HDMA_RUN_DOT` is now literally `= rustysnes_ppu::RENDER_DOT`, so this can never actually
1792    /// fail post-refactor -- kept as a named regression lock so a future edit that reintroduces a
1793    /// separate literal (e.g. during a merge) fails loudly instead of silently drifting the two
1794    /// dot values apart again (`docs/ppu.md` §Mid-scanline/HDMA-driven register timing).
1795    #[test]
1796    fn hdma_run_dot_matches_ppu_render_dot() {
1797        assert_eq!(HDMA_RUN_DOT, rustysnes_ppu::RENDER_DOT);
1798    }
1799
1800    /// A strobe reloads the shift register, so two manual reads in one frame agree.
1801    ///
1802    /// The regression this locks: the shift register used to *be* the button word, so the first
1803    /// read consumed it and the second returned all-ones. A frontend rewrites the button state
1804    /// every frame, which hid it; a game that polls twice per frame would not have been hidden
1805    /// from it.
1806    #[test]
1807    fn strobe_reloads_the_gamepad_shift_register() {
1808        let mut bus = Bus::default();
1809        bus.set_joypad(0, 0x8000); // B held, nothing else
1810
1811        let read16 = |bus: &mut Bus| {
1812            bus.write_cpu_reg(0x4016, 0x01);
1813            bus.write_cpu_reg(0x4016, 0x00);
1814            let mut bits = 0u16;
1815            for _ in 0..16 {
1816                bits = (bits << 1) | u16::from(bus.read_cpu_reg(0x4016) & 1);
1817            }
1818            bits
1819        };
1820
1821        assert_eq!(read16(&mut bus), 0x8000, "first read of the frame");
1822        assert_eq!(
1823            read16(&mut bus),
1824            0x8000,
1825            "second read must agree with the first"
1826        );
1827    }
1828
1829    /// The falling edge captures the buttons as they are *then*, not as they were on the way up.
1830    #[test]
1831    fn strobe_captures_buttons_at_the_falling_edge() {
1832        let mut bus = Bus::default();
1833        bus.set_joypad(0, 0x0000);
1834        bus.write_cpu_reg(0x4016, 0x01); // strobe high with nothing held
1835        bus.set_joypad(0, 0x8000); // B goes down while the strobe is still high
1836        bus.write_cpu_reg(0x4016, 0x00); // falling edge: this is what must be captured
1837        assert_eq!(
1838            bus.read_cpu_reg(0x4016) & 1,
1839            1,
1840            "the falling edge froze a stale button word"
1841        );
1842    }
1843
1844    /// Held high, the register never advances: every read is the first bit.
1845    #[test]
1846    fn strobe_held_high_does_not_advance() {
1847        let mut bus = Bus::default();
1848        bus.set_joypad(0, 0x8000); // B held, so the first bit is 1 and the rest are 0
1849        bus.write_cpu_reg(0x4016, 0x01);
1850        for _ in 0..4 {
1851            assert_eq!(
1852                bus.read_cpu_reg(0x4016) & 1,
1853                1,
1854                "a read with the strobe high advanced the shift register"
1855            );
1856        }
1857    }
1858
1859    /// Past sixteen bits a standard pad reads 1 — which is how software identifies it.
1860    #[test]
1861    fn gamepad_reads_one_past_its_data_bits() {
1862        let mut bus = Bus::default();
1863        bus.set_joypad(0, 0x0000);
1864        bus.write_cpu_reg(0x4016, 0x01);
1865        bus.write_cpu_reg(0x4016, 0x00);
1866        for _ in 0..16 {
1867            assert_eq!(
1868                bus.read_cpu_reg(0x4016) & 1,
1869                0,
1870                "a data bit read as pressed"
1871            );
1872        }
1873        for i in 0..4 {
1874            assert_eq!(
1875                bus.read_cpu_reg(0x4016) & 1,
1876                1,
1877                "read {} past the data bits",
1878                i + 17
1879            );
1880        }
1881    }
1882
1883    /// A manual read must not disturb the auto-read result.
1884    #[test]
1885    fn manual_read_does_not_consume_the_auto_read_result() {
1886        let mut bus = Bus::default();
1887        bus.set_joypad(0, 0x1234);
1888        // Stand in for the vblank poll: `$4218` reports the auto-read *result*, not the live pad.
1889        bus.write_cpu_reg(0x4200, 0x01);
1890        bus.poll_auto_joypad_for_test();
1891        bus.write_cpu_reg(0x4200, 0x00);
1892        bus.write_cpu_reg(0x4016, 0x01);
1893        bus.write_cpu_reg(0x4016, 0x00);
1894        for _ in 0..16 {
1895            let _ = bus.read_cpu_reg(0x4016);
1896        }
1897        assert_eq!(bus.read_cpu_reg(0x4218), 0x34);
1898        assert_eq!(bus.read_cpu_reg(0x4219), 0x12);
1899    }
1900
1901    /// A latch held high across the automatic read makes every bit of the result the same.
1902    ///
1903    /// The read clocks the ports' shift registers, and while `$4016` bit 0 is high those registers
1904    /// reload rather than shift — so all sixteen clocks return the first bit. AccuracySNES `F1.11`.
1905    #[test]
1906    fn auto_read_is_corrupted_by_a_held_latch() {
1907        let mut bus = Bus::default();
1908        bus.set_joypad(0, 0x9050); // B is held, so the repeated bit is 1
1909        bus.write_cpu_reg(0x4200, 0x01);
1910        bus.write_cpu_reg(0x4016, 0x01); // latch high, and left there
1911
1912        bus.poll_auto_joypad_for_test();
1913        assert_eq!(bus.read_cpu_reg(0x4218), 0xFF);
1914        assert_eq!(bus.read_cpu_reg(0x4219), 0xFF);
1915
1916        // Released, the same poll reports the buttons.
1917        bus.write_cpu_reg(0x4016, 0x00);
1918        bus.poll_auto_joypad_for_test();
1919        assert_eq!(bus.read_cpu_reg(0x4218), 0x50);
1920        assert_eq!(bus.read_cpu_reg(0x4219), 0x90);
1921    }
1922
1923    /// `$4210`/`$4211` return the CPU open bus (MDR) in the bits hardware leaves floating.
1924    ///
1925    /// `$4210` RDNMI: bit 7 = the read-clearing `VBlank` flag, bits 4-6 = open bus, bits 0-3 = CPU
1926    /// version 2. `$4211` TIMEUP: bit 7 = the read-clearing IRQ flag, bits 0-6 = open bus. Matches
1927    /// ares `CPU::readIO` (which writes only the flag + version and leaves the rest as open bus).
1928    #[test]
1929    #[allow(clippy::field_reassign_with_default)] // a full `Bus` struct literal is impractical
1930    fn rdnmi_timeup_expose_open_bus_in_unused_bits() {
1931        let mut bus = Bus::default();
1932        // 0xAB = 1010_1011: distinctive so every masked open-bus position is exercised.
1933        // $4210 with the flag clear: bits 4-6 = 0xAB & 0x70 = 0x20, version = 0x02 -> 0x22.
1934        bus.open_bus = 0xAB;
1935        assert_eq!(bus.read_cpu_reg(0x4210), 0x22);
1936        // Flag set: bit7 | open-bus 4-6 | version -> 0x80 | 0x20 | 0x02 = 0xA2, and the read clears.
1937        bus.clock.rdnmi_flag = true;
1938        bus.open_bus = 0xAB;
1939        assert_eq!(bus.read_cpu_reg(0x4210), 0xA2);
1940        assert!(
1941            !bus.clock.rdnmi_flag,
1942            "reading $4210 clears the VBlank flag"
1943        );
1944        // $4211 with the IRQ flag clear: bits0-6 = 0xAB & 0x7F = 0x2B.
1945        bus.open_bus = 0xAB;
1946        assert_eq!(bus.read_cpu_reg(0x4211), 0x2B);
1947        // IRQ set: 0x80 | 0x2B = 0xAB, and the read clears.
1948        bus.clock.irq_line = true;
1949        bus.open_bus = 0xAB;
1950        assert_eq!(bus.read_cpu_reg(0x4211), 0xAB);
1951        assert!(!bus.clock.irq_line, "reading $4211 clears the IRQ flag");
1952    }
1953
1954    /// For four master clocks after the edge, a `$4210`/`$4211` read returns the flag set but does
1955    /// NOT clear it — the hardware holds `/NMI` and `/IRQ` across the edge (ares `nmiHold`/`irqHold`;
1956    /// Terranigma depends on the RDNMI flag surviving a read that lands in that window).
1957    #[test]
1958    #[allow(clippy::field_reassign_with_default)] // a full `Bus` struct literal is impractical
1959    fn rdnmi_timeup_are_held_across_the_edge_and_do_not_clear_on_read() {
1960        let mut bus = Bus::default();
1961
1962        // The VBlank edge raised the flag AND set the hold (as `tick_ppu_dot` does at vblank start).
1963        bus.clock.rdnmi_flag = true;
1964        bus.clock.rdnmi_hold = true;
1965        // A read within the window returns bit 7 set and leaves the flag set — repeatedly.
1966        assert_eq!(bus.read_cpu_reg(0x4210) & 0x80, 0x80);
1967        assert!(
1968            bus.clock.rdnmi_flag,
1969            "held: the read must not clear the flag"
1970        );
1971        assert_eq!(
1972            bus.read_cpu_reg(0x4210) & 0x80,
1973            0x80,
1974            "still held on a second read"
1975        );
1976        assert!(bus.clock.rdnmi_flag);
1977        // The next dot consumes the hold; now a read clears normally.
1978        bus.clock.rdnmi_hold = false;
1979        assert_eq!(
1980            bus.read_cpu_reg(0x4210) & 0x80,
1981            0x80,
1982            "flag still set on the clearing read"
1983        );
1984        assert!(
1985            !bus.clock.rdnmi_flag,
1986            "the read after the hold clears the flag"
1987        );
1988        assert_eq!(bus.read_cpu_reg(0x4210) & 0x80, 0x00, "cleared");
1989
1990        // Same for the IRQ flag / $4211, including the back-to-back held read.
1991        bus.clock.irq_line = true;
1992        bus.clock.irq_hold = true;
1993        assert_eq!(bus.read_cpu_reg(0x4211) & 0x80, 0x80);
1994        assert!(
1995            bus.clock.irq_line,
1996            "held: the read must not clear the IRQ flag"
1997        );
1998        assert_eq!(
1999            bus.read_cpu_reg(0x4211) & 0x80,
2000            0x80,
2001            "still held on a second read"
2002        );
2003        assert!(bus.clock.irq_line);
2004        bus.clock.irq_hold = false;
2005        assert_eq!(bus.read_cpu_reg(0x4211) & 0x80, 0x80);
2006        assert!(
2007            !bus.clock.irq_line,
2008            "the read after the hold clears the IRQ flag"
2009        );
2010    }
2011
2012    /// The RDNMI hold's full lifecycle, driven through the real per-dot path rather than by hand:
2013    /// advancing to the `VBlank` edge must set the hold in [`Bus::tick_ppu_dot`], and the *next* dot
2014    /// must consume it there. This is what proves the set/consume logic inside `tick_ppu_dot`, which
2015    /// [`rdnmi_timeup_are_held_across_the_edge_and_do_not_clear_on_read`] (which mutates the hold
2016    /// directly) cannot: were the consume removed, this test's post-dot read would not clear.
2017    #[test]
2018    fn rdnmi_hold_is_set_and_consumed_by_tick_ppu_dot_across_the_vblank_edge() {
2019        let mut bus = Bus::default();
2020        // Advance real dots until VBlank begins. The dot that raises the flag also sets the hold.
2021        let mut prev_vblank = bus.ppu.in_vblank();
2022        let mut at_edge = false;
2023        // Bound: one NTSC frame of dots is far more than enough to reach the first VBlank.
2024        for _ in 0..(u32::from(rustysnes_ppu::DOTS_PER_LINE) * 262) {
2025            bus.advance_master(MASTER_PER_DOT);
2026            let now = bus.ppu.in_vblank();
2027            if now && !prev_vblank {
2028                at_edge = true;
2029                break;
2030            }
2031            prev_vblank = now;
2032        }
2033        assert!(at_edge, "advancing reached the VBlank edge");
2034        // Set by tick_ppu_dot, not by hand:
2035        assert!(
2036            bus.clock.rdnmi_flag,
2037            "the VBlank edge raised the RDNMI flag"
2038        );
2039        assert!(
2040            bus.clock.rdnmi_hold,
2041            "the VBlank edge set the four-clock hold"
2042        );
2043        // A read in the window returns bit 7 set and does not clear the flag.
2044        assert_eq!(bus.read_cpu_reg(0x4210) & 0x80, 0x80);
2045        assert!(
2046            bus.clock.rdnmi_flag,
2047            "held: the read did not clear the flag"
2048        );
2049        // The next dot's tick_ppu_dot consumes the hold; still in VBlank, so the flag stays set.
2050        bus.advance_master(MASTER_PER_DOT);
2051        assert!(!bus.clock.rdnmi_hold, "the next dot consumed the hold");
2052        assert!(
2053            bus.clock.rdnmi_flag,
2054            "the flag itself is untouched — only a read past the hold clears it"
2055        );
2056        // Past the hold, a read clears normally.
2057        assert_eq!(bus.read_cpu_reg(0x4210) & 0x80, 0x80);
2058        assert!(
2059            !bus.clock.rdnmi_flag,
2060            "past the hold, the read clears the flag"
2061        );
2062    }
2063
2064    /// The timed automatic read reads busy on `$4212` bit 0 for ~4224 master clocks and publishes
2065    /// its result only at completion — not instantly at vblank entry.
2066    #[test]
2067    fn auto_joypad_read_is_busy_for_its_window_then_publishes() {
2068        let mut bus = Bus::default();
2069        bus.set_joypad(0, 0x1234);
2070        bus.write_cpu_reg(0x4200, 0x01); // arm auto-read ($4200 bit 0)
2071        bus.clock.master = 1000;
2072        bus.begin_auto_joypad();
2073        // Busy immediately, and the result is NOT yet published (still the power-on $0000).
2074        assert_eq!(
2075            bus.read_cpu_reg(0x4212) & 1,
2076            1,
2077            "busy at the start of the window"
2078        );
2079        assert_eq!(
2080            bus.read_cpu_reg(0x4218),
2081            0x00,
2082            "result not published mid-window"
2083        );
2084        // Still busy one clock before the deadline.
2085        bus.clock.master = 1000 + AUTO_JOYPAD_CLOCKS - 1;
2086        assert_eq!(
2087            bus.read_cpu_reg(0x4212) & 1,
2088            1,
2089            "still busy just before completion"
2090        );
2091        // At the deadline: no longer busy, and the snapshot is now published.
2092        bus.clock.master = 1000 + AUTO_JOYPAD_CLOCKS;
2093        assert_eq!(bus.read_cpu_reg(0x4212) & 1, 0, "not busy after the window");
2094        assert_eq!(
2095            bus.read_cpu_reg(0x4218),
2096            0x34,
2097            "result published at completion"
2098        );
2099        assert_eq!(bus.read_cpu_reg(0x4219), 0x12);
2100    }
2101
2102    /// A save taken DURING the auto-read busy window restores an identical machine state
2103    /// (`FORMAT_VERSION` 5): the busy flag and the deferred snapshot survive save/load exactly.
2104    #[test]
2105    fn auto_joypad_busy_state_survives_save_load() {
2106        use rustysnes_savestate::{SaveReader, SaveWriter};
2107        let mut bus = Bus::default();
2108        bus.set_joypad(0, 0x1234);
2109        bus.write_cpu_reg(0x4200, 0x01); // arm auto-read
2110        bus.clock.master = 5000;
2111        bus.begin_auto_joypad(); // start a read; deadline = 5000 + 4224
2112        assert_eq!(bus.read_cpu_reg(0x4212) & 1, 1, "busy before the save");
2113        // Save mid-window and restore into a fresh Bus.
2114        let mut w = SaveWriter::new();
2115        bus.save_state(&mut w);
2116        let bytes = w.into_bytes();
2117        let mut fresh = Bus::default();
2118        let mut r = SaveReader::new(&bytes);
2119        fresh
2120            .load_state(&mut r)
2121            .expect("mid-window round trip must succeed");
2122        // Still busy, result still deferred (lost if the state were not serialized).
2123        assert_eq!(
2124            fresh.read_cpu_reg(0x4212) & 1,
2125            1,
2126            "busy state survives the round trip"
2127        );
2128        assert_eq!(
2129            fresh.read_cpu_reg(0x4218),
2130            0x00,
2131            "result still deferred after load"
2132        );
2133        // Past the restored deadline, the restored snapshot publishes.
2134        fresh.clock.master = 5000 + AUTO_JOYPAD_CLOCKS;
2135        assert_eq!(
2136            fresh.read_cpu_reg(0x4212) & 1,
2137            0,
2138            "not busy after the restored deadline"
2139        );
2140        assert_eq!(
2141            fresh.read_cpu_reg(0x4218),
2142            0x34,
2143            "restored snapshot publishes at completion"
2144        );
2145    }
2146
2147    /// A save taken in the window between the vblank edge and the auto-read's scheduled START
2148    /// (`FORMAT_VERSION` 9) restores the pending start, so the read still begins on the loaded state.
2149    #[test]
2150    fn auto_joypad_pending_start_survives_save_load() {
2151        use rustysnes_savestate::{SaveReader, SaveWriter};
2152        let mut bus = Bus::default();
2153        bus.set_joypad(0, 0x1234);
2154        bus.write_cpu_reg(0x4200, 0x01);
2155        bus.clock.master = 7000;
2156        // Simulate the VBlank edge scheduling the delayed start (not yet begun).
2157        bus.auto_joypad_start_at = 7000 + AUTO_JOYPAD_START_DELAY;
2158        assert_eq!(
2159            bus.read_cpu_reg(0x4212) & 1,
2160            0,
2161            "not busy yet — the read has not started"
2162        );
2163        let mut w = SaveWriter::new();
2164        bus.save_state(&mut w);
2165        let bytes = w.into_bytes();
2166        let mut fresh = Bus::default();
2167        let mut r = SaveReader::new(&bytes);
2168        fresh.load_state(&mut r).expect("pending-start round trip");
2169        assert_eq!(
2170            fresh.auto_joypad_start_at,
2171            7000 + AUTO_JOYPAD_START_DELAY,
2172            "the pending start deadline survives save/load (else the read never begins)"
2173        );
2174        // Reaching the deadline begins the read (busy), exactly as on the pre-save state.
2175        fresh.clock.master = 7000 + AUTO_JOYPAD_START_DELAY;
2176        fresh.maybe_begin_scheduled_auto_joypad();
2177        assert_eq!(
2178            fresh.read_cpu_reg(0x4212) & 1,
2179            1,
2180            "read begins at the deadline"
2181        );
2182    }
2183
2184    /// Disarming `$4200` bit 0 DURING the `[edge, start)` window cancels the read: the enable is
2185    /// re-sampled at the start dot, not latched at the vblank edge.
2186    #[test]
2187    fn disarming_auto_read_in_the_start_window_cancels_it() {
2188        let mut bus = Bus::default();
2189        bus.set_joypad(0, 0x1234);
2190        bus.write_cpu_reg(0x4200, 0x01); // armed at the "edge"
2191        bus.clock.master = 100;
2192        bus.auto_joypad_start_at = 100 + AUTO_JOYPAD_START_DELAY;
2193        // Software disarms auto-read before the start dot arrives.
2194        bus.write_cpu_reg(0x4200, 0x00);
2195        // Reach the scheduled start dot: the read must NOT begin, because it is disarmed now.
2196        bus.clock.master = 100 + AUTO_JOYPAD_START_DELAY;
2197        bus.maybe_begin_scheduled_auto_joypad();
2198        assert_eq!(
2199            bus.read_cpu_reg(0x4212) & 1,
2200            0,
2201            "a read disarmed during the start window must not begin"
2202        );
2203        assert_eq!(
2204            bus.auto_joypad_start_at, 0,
2205            "the pending start is consumed either way"
2206        );
2207    }
2208
2209    /// `$4218` reports only what an *armed* automatic read put there.
2210    ///
2211    /// With `$4200` bit 0 clear the registers hold their previous contents indefinitely, so
2212    /// software that disarms auto-read to poll `$4016` by hand does not find the hardware's answer
2213    /// appearing underneath it. AccuracySNES `F1.07`.
2214    #[test]
2215    fn auto_read_result_only_updates_while_armed() {
2216        let mut bus = Bus::default();
2217        bus.set_joypad(0, 0x9050);
2218        assert_eq!(bus.read_cpu_reg(0x4218), 0x00, "nothing has polled yet");
2219
2220        bus.write_cpu_reg(0x4200, 0x01);
2221        bus.poll_auto_joypad_for_test(); // the poll this arming performs at the next vblank
2222        assert_eq!(bus.read_cpu_reg(0x4218), 0x50);
2223        assert_eq!(bus.read_cpu_reg(0x4219), 0x90);
2224
2225        bus.write_cpu_reg(0x4200, 0x00);
2226        bus.set_joypad(0, 0x0000); // the pad changes, but nothing is armed to notice
2227        assert_eq!(
2228            bus.read_cpu_reg(0x4218),
2229            0x50,
2230            "a disarmed read must not update"
2231        );
2232        assert_eq!(bus.read_cpu_reg(0x4219), 0x90);
2233    }
2234
2235    #[test]
2236    fn default_bus_has_no_cart_and_reads_open() {
2237        let mut bus = Bus::default();
2238        assert!(bus.cart.is_none());
2239        assert_eq!(<Bus as CpuBus>::read24(&mut bus, 0x00_8000), 0);
2240    }
2241
2242    #[test]
2243    fn wram_round_trips() {
2244        let mut bus = Bus::default();
2245        <Bus as CpuBus>::write24(&mut bus, 0x7E_1234, 0xAB);
2246        assert_eq!(<Bus as CpuBus>::read24(&mut bus, 0x7E_1234), 0xAB);
2247        // Low mirror in bank 0 aliases the same WRAM.
2248        <Bus as CpuBus>::write24(&mut bus, 0x00_0042, 0x99);
2249        assert_eq!(<Bus as CpuBus>::read24(&mut bus, 0x7E_0042), 0x99);
2250    }
2251
2252    #[test]
2253    fn wram_and_wram_mut_expose_the_same_flat_128kib() {
2254        let mut bus = Bus::default();
2255        assert_eq!(bus.wram().len(), 0x2_0000);
2256        <Bus as CpuBus>::write24(&mut bus, 0x7E_1234, 0xAB);
2257        assert_eq!(bus.wram()[0x1234], 0xAB);
2258        bus.wram_mut()[0x5678] = 0xCD;
2259        assert_eq!(<Bus as CpuBus>::read24(&mut bus, 0x7E_5678), 0xCD);
2260    }
2261
2262    #[test]
2263    fn peek_reads_wram_without_side_effects() {
2264        let mut bus = Bus::default();
2265        <Bus as CpuBus>::write24(&mut bus, 0x7E_1234, 0xAB);
2266        // A real CPU read first, so open_bus is a known, distinct value.
2267        assert_eq!(<Bus as CpuBus>::read24(&mut bus, 0x7E_1234), 0xAB);
2268        let open_bus_before = bus.open_bus;
2269        // `peek` must return the same byte `read24` would, but never touch `open_bus`.
2270        assert_eq!(bus.peek(0x7E_1234), 0xAB);
2271        assert_eq!(
2272            bus.open_bus, open_bus_before,
2273            "peek must not perturb open_bus"
2274        );
2275    }
2276
2277    #[test]
2278    fn peek_of_io_register_space_is_zero_not_the_live_register() {
2279        let mut bus = Bus::default();
2280        <Bus as CpuBus>::write24(&mut bus, 0x00_4202, 0x10);
2281        <Bus as CpuBus>::write24(&mut bus, 0x00_4203, 0x10);
2282        // $4216 (RDMPY) genuinely holds 0x0100 now via `read24`, but `peek` never reaches
2283        // register space at all (real code never executes from it) — this documents that
2284        // limitation rather than silently returning a wrong "peek" of live register state.
2285        assert_eq!(<Bus as CpuBus>::read24(&mut bus, 0x00_4216), 0x00);
2286        assert_eq!(bus.peek(0x00_4216), 0);
2287    }
2288
2289    #[test]
2290    fn wrio_rdio_round_trips_and_defaults_to_all_ones() {
2291        let mut bus = Bus::default();
2292        assert_eq!(<Bus as CpuBus>::read24(&mut bus, 0x00_4213), 0xFF);
2293        <Bus as CpuBus>::write24(&mut bus, 0x00_4201, 0x55);
2294        assert_eq!(<Bus as CpuBus>::read24(&mut bus, 0x00_4213), 0x55);
2295    }
2296
2297    #[test]
2298    fn wrio_bit7_falling_edge_latches_hv_counters() {
2299        let mut bus = Bus::default();
2300        // Advance a few dots so the latch has a known, non-zero dot value to observe.
2301        for _ in 0..40 {
2302            bus.advance_master(1);
2303        }
2304        let dot_before = bus.ppu.dot();
2305        // Bit 7 starts high (power-on default 0xFF); a write clearing it is the falling edge that
2306        // should latch the H/V counters (ares `cpu/io.cpp`'s `if(io.pio.bit(7) && !data.bit(7))`).
2307        <Bus as CpuBus>::write24(&mut bus, 0x00_4201, 0x00);
2308        let ophct_lo = <Bus as CpuBus>::read24(&mut bus, 0x00_213C);
2309        #[allow(clippy::cast_possible_truncation)]
2310        let expected = (dot_before & 0xFF) as u8;
2311        assert_eq!(
2312            ophct_lo, expected,
2313            "WRIO bit7 falling edge should latch OPHCT"
2314        );
2315    }
2316
2317    #[test]
2318    fn slhv_read_does_not_latch_while_wrio_bit7_is_clear() {
2319        let mut bus = Bus::default();
2320        for _ in 0..40 {
2321            bus.advance_master(1);
2322        }
2323        // Clearing bit 7 is itself a falling edge and latches once, here. That is the value the
2324        // counters must keep: every later $2137 read is gated off and must not disturb it.
2325        <Bus as CpuBus>::write24(&mut bus, 0x00_4201, 0x00);
2326        let latched = <Bus as CpuBus>::read24(&mut bus, 0x00_213C);
2327        for _ in 0..400 {
2328            bus.advance_master(1);
2329        }
2330        <Bus as CpuBus>::read24(&mut bus, 0x00_2137);
2331        assert_eq!(
2332            <Bus as CpuBus>::read24(&mut bus, 0x00_213C),
2333            latched,
2334            "$2137 latched the counters with WRIO bit 7 clear, where no latching can occur"
2335        );
2336
2337        // And with the gate open again it must latch: the 0->1 transition is not itself an edge
2338        // that latches, so this isolates the read.
2339        <Bus as CpuBus>::write24(&mut bus, 0x00_4201, 0x80);
2340        for _ in 0..400 {
2341            bus.advance_master(1);
2342        }
2343        let dot_before = bus.ppu.dot();
2344        <Bus as CpuBus>::read24(&mut bus, 0x00_2137);
2345        #[allow(clippy::cast_possible_truncation)]
2346        let expected = (dot_before & 0xFF) as u8;
2347        assert_eq!(
2348            <Bus as CpuBus>::read24(&mut bus, 0x00_213C),
2349            expected,
2350            "$2137 did not latch with WRIO bit 7 set"
2351        );
2352    }
2353
2354    #[test]
2355    fn wrio_bit6_falling_edge_does_not_latch() {
2356        let mut bus = Bus::default();
2357        for _ in 0..40 {
2358            bus.advance_master(1);
2359        }
2360        // Port 1's IOBIT (bit 6) has no real-hardware wiring to the PPU latch — only bit 7 does.
2361        <Bus as CpuBus>::write24(&mut bus, 0x00_4201, 0xBF); // clear bit 6, leave bit 7 set
2362        assert_eq!(<Bus as CpuBus>::read24(&mut bus, 0x00_213C), 0);
2363    }
2364
2365    #[test]
2366    fn superscope_beam_latch_fires_at_target_position() {
2367        let mut bus = Bus::default();
2368        bus.set_port_device(1, crate::controller::PortDevice::SuperScope);
2369        bus.set_superscope(1, 10, 5, 0);
2370        let target_dot = 10 + 24;
2371        for _ in 0..2_000_000 {
2372            if bus.ppu.scanline() == 5 && bus.ppu.dot() == target_dot {
2373                break;
2374            }
2375            bus.advance_master(1);
2376        }
2377        assert_eq!(
2378            bus.ppu.scanline(),
2379            5,
2380            "should have reached the target scanline"
2381        );
2382        assert_eq!(
2383            bus.ppu.dot(),
2384            target_dot,
2385            "should have reached the target dot"
2386        );
2387        let ophct_lo = <Bus as CpuBus>::read24(&mut bus, 0x00_213C);
2388        assert_eq!(
2389            ophct_lo, target_dot as u8,
2390            "the beam crossing the target should have auto-latched OPHCT to it"
2391        );
2392    }
2393
2394    #[test]
2395    fn dram_refresh_stalls_the_cpu_forty_clocks_once_per_scanline() {
2396        // A fresh bus sits at line 0, dot 0. Drive the master clock one dot of CPU work at a time.
2397        // With no refresh, N steps leave the PPU at dot N. The once-per-line pause at
2398        // `DRAM_REFRESH_DOT` reallocates `DRAM_REFRESH_CLOCKS` (= 10 dots) of the line to the CPU:
2399        // the instant dot `DRAM_REFRESH_DOT - 1` completes, the PPU jumps 10 dots ahead of the CPU's
2400        // own progress — the CPU has "fallen behind" exactly as it does on hardware losing the bus
2401        // to WRAM refresh. The frame stays the same length (the 40 clocks are PPU dots either way);
2402        // only the CPU-vs-PPU phase moves, which is what a mid-line raster write observes.
2403        let stall_dots = (DRAM_REFRESH_CLOCKS / MASTER_PER_DOT) as u16;
2404        let mut bus = Bus::default();
2405        for _ in 0..DRAM_REFRESH_DOT - 1 {
2406            bus.advance_master(MASTER_PER_DOT);
2407        }
2408        assert_eq!(
2409            bus.ppu.dot(),
2410            DRAM_REFRESH_DOT - 1,
2411            "no stall before the refresh dot: {DRAM_REFRESH_DOT} dots of work, {DRAM_REFRESH_DOT} dots elapsed"
2412        );
2413        bus.advance_master(MASTER_PER_DOT); // completes DRAM_REFRESH_DOT-1 → triggers the pause
2414        assert_eq!(
2415            bus.ppu.dot(),
2416            DRAM_REFRESH_DOT + stall_dots,
2417            "the {DRAM_REFRESH_CLOCKS}-clock refresh pause jumps the PPU {stall_dots} dots ahead of the CPU"
2418        );
2419        // And it fires exactly once per line: ten more dots of work advance ten dots, no second jump.
2420        for _ in 0..10 {
2421            bus.advance_master(MASTER_PER_DOT);
2422        }
2423        assert_eq!(
2424            bus.ppu.dot(),
2425            DRAM_REFRESH_DOT + stall_dots + 10,
2426            "the pause does not re-fire later in the same scanline"
2427        );
2428    }
2429
2430    #[test]
2431    fn access_speed_map() {
2432        let bus = Bus::default();
2433        assert_eq!(bus.access_speed(0x00_0042), 8); // WRAM mirror
2434        assert_eq!(bus.access_speed(0x00_2100), 6); // PPU
2435        assert_eq!(bus.access_speed(0x00_4016), 12); // joypad
2436        assert_eq!(bus.access_speed(0x00_4200), 6); // CPU regs
2437        assert_eq!(bus.access_speed(0x00_8000), 8); // WS1 ROM (always 8)
2438        assert_eq!(bus.access_speed(0x80_8000), 8); // WS2 ROM, SlowROM default
2439    }
2440
2441    #[test]
2442    fn memsel_fastrom_speeds_up_ws2() {
2443        let mut bus = Bus::default();
2444        <Bus as CpuBus>::write24(&mut bus, 0x00_420D, 0x01); // MEMSEL FastROM
2445        assert_eq!(bus.access_speed(0x80_8000), 6);
2446        assert_eq!(bus.access_speed(0x00_8000), 8); // WS1 unaffected
2447    }
2448
2449    #[test]
2450    fn muldiv_unit() {
2451        let mut bus = Bus::default();
2452        <Bus as CpuBus>::write24(&mut bus, 0x00_4202, 0x10); // MPYA
2453        <Bus as CpuBus>::write24(&mut bus, 0x00_4203, 0x10); // MPYB -> 0x100
2454        assert_eq!(<Bus as CpuBus>::read24(&mut bus, 0x00_4216), 0x00);
2455        assert_eq!(<Bus as CpuBus>::read24(&mut bus, 0x00_4217), 0x01);
2456        <Bus as CpuBus>::write24(&mut bus, 0x00_4204, 0x64); // dividend lo = 100
2457        <Bus as CpuBus>::write24(&mut bus, 0x00_4205, 0x00);
2458        <Bus as CpuBus>::write24(&mut bus, 0x00_4206, 0x07); // / 7 -> 14 r 2
2459        assert_eq!(<Bus as CpuBus>::read24(&mut bus, 0x00_4214), 14);
2460        assert_eq!(<Bus as CpuBus>::read24(&mut bus, 0x00_4216), 2);
2461    }
2462
2463    /// `$4202` (MPYA) is a stable latch, not re-armed per multiply — real hardware documents
2464    /// that a fresh `$4203` (WRMPYB) write alone starts a new multiply against whatever MPYA
2465    /// already holds (`SNESdev`'s Multiplication page). The genuinely undefined case (starting a
2466    /// new multiply/divide before the previous one's 8-cycle latency elapses, `SNESdev`'s Errata
2467    /// page) is deliberately NOT covered here — see `MulDiv`'s own doc comment for why there is
2468    /// no correct value to assert against.
2469    #[test]
2470    fn muldiv_mpya_latch_survives_across_sequential_multiplies() {
2471        let mut bus = Bus::default();
2472        <Bus as CpuBus>::write24(&mut bus, 0x00_4202, 0x05); // MPYA = 5
2473        <Bus as CpuBus>::write24(&mut bus, 0x00_4203, 0x06); // MPYB -> 5*6 = 30
2474        assert_eq!(<Bus as CpuBus>::read24(&mut bus, 0x00_4216), 30);
2475        // MPYA is untouched by the write above; a fresh $4203 alone starts another multiply
2476        // against the SAME latched 5.
2477        <Bus as CpuBus>::write24(&mut bus, 0x00_4203, 0x07); // MPYB -> 5*7 = 35
2478        assert_eq!(<Bus as CpuBus>::read24(&mut bus, 0x00_4216), 35);
2479    }
2480
2481    #[test]
2482    fn master_clock_advances_on_access() {
2483        let mut bus = Bus::default();
2484        let before = bus.clock.master;
2485        // SlowROM ($00:8000) costs 8 master clocks; `advance` is what moves the clock.
2486        let speed = <Bus as CpuBus>::access_cycles(&bus, 0x00_8000);
2487        assert_eq!(speed, 8);
2488        <Bus as CpuBus>::advance(&mut bus, speed);
2489        assert_eq!(bus.clock.master, before + 8);
2490        // `read24`/`write24` are pure accesses now — they do not move the clock.
2491        <Bus as CpuBus>::read24(&mut bus, 0x00_8000);
2492        assert_eq!(bus.clock.master, before + 8);
2493    }
2494}