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}