rustynes_core/bus.rs
1//! Lockstep bus for the `Nes` facade.
2//!
3//! Per `docs/scheduler.md` §Bus design: this bus owns CPU RAM, the PPU, the
4//! APU, the cartridge mapper, and the controller stub. Each
5//! `cpu_read`/`cpu_write` ticks the PPU exactly 3 times (NTSC) and dispatches
6//! the access to the right device. PPU register reads have side effects;
7//! OAM DMA and DMC DMA are handled by `cpu_cycles_owed`-style state machines
8//! that drain stolen cycles before completing the access that triggered them.
9
10use alloc::collections::BTreeMap;
11use alloc::format;
12use alloc::{boxed::Box, vec::Vec};
13
14use crate::genie::{GenieCode, GenieError};
15use rustynes_apu::{Apu, ApuSnapshotError, Region as ApuRegion};
16
17/// v2.0 R1c-1 DIAGNOSTIC (gated `cpu-instr-cycle-trace`).
18///
19/// A per-CPU-instruction `(PC, cumulative cpu_cycle)` ring buffer (keeps the
20/// LAST `CAP` instructions). `Cpu::step` calls `trace_instr` at each opcode
21/// fetch; the harness dumps the ring (R1 + default) and diffs the
22/// per-instruction cycle deltas to pin the odd-cycle cumulative divergence (the
23/// Y=3-vs-4 source). Read via `rustynes_core::instr_trace`.
24#[cfg(feature = "cpu-instr-cycle-trace")]
25pub mod instr_trace {
26 use core::sync::atomic::{AtomicU32, AtomicU64, Ordering::Relaxed};
27 /// Ring capacity (last CAP instructions kept).
28 pub const CAP: usize = 1 << 18; // 262144
29 /// Per-entry instruction PC.
30 pub static PC: [AtomicU32; CAP] = [const { AtomicU32::new(0) }; CAP];
31 /// Per-entry cumulative CPU cycle.
32 pub static CYC: [AtomicU64; CAP] = [const { AtomicU64::new(0) }; CAP];
33 /// Monotonic write index (total instructions; ring slot = `IDX % CAP`).
34 pub static IDX: AtomicU64 = AtomicU64::new(0);
35
36 /// Record one instruction `(pc, cpu_cycle)` into the ring.
37 #[allow(clippy::cast_possible_truncation)]
38 pub fn record(pc: u16, cpu_cycle: u64) {
39 let slot = (IDX.fetch_add(1, Relaxed) % CAP as u64) as usize;
40 PC[slot].store(u32::from(pc), Relaxed);
41 CYC[slot].store(cpu_cycle, Relaxed);
42 }
43}
44use rustynes_cpu::Bus;
45use rustynes_mappers::{Cartridge, Mapper, MapperError, MapperFrameEvents, RomError};
46use rustynes_ppu::{
47 BgSplitState as PpuBgSplitState, ExAttribute as PpuExAttribute, PaletteInit, Ppu, PpuBus,
48 PpuPalette, PpuRegion, PpuRevision, PpuSnapshotError,
49};
50
51use crate::Cpu2A03Revision;
52use crate::controller::{Buttons, Controller};
53#[cfg(feature = "irq-timing-trace")]
54use crate::irq_trace::{A12Event, BusAccess, CycleRecord, IrqTrace};
55use crate::save_state::{self, SnapshotError};
56use crate::scheduler::M2Phase;
57
58/// CPU RAM (2 KiB).
59const RAM_SIZE: usize = 0x0800;
60
61/// OAM DMA source-page write target (`$4014`). Triggers a 256-byte DMA on
62/// the next CPU read cycle.
63const REG_OAM_DMA: u16 = 0x4014;
64
65/// Default audio sample rate. The frontend may rebuild the bus with a
66/// different rate when CPAL picks something else.
67pub const DEFAULT_SAMPLE_RATE: u32 = 44_100;
68
69/// Map the cartridge-layer [`rustynes_mappers::VsPpuPalette`] to the PPU's
70/// [`PpuPalette`]. `rustynes-core` is the one crate that depends on both `rustynes-ppu`
71/// and `rustynes-mappers`, so the bridge lives here rather than creating a
72/// cross-crate dependency edge.
73const fn vs_palette_to_ppu(p: rustynes_mappers::VsPpuPalette) -> PpuPalette {
74 match p {
75 rustynes_mappers::VsPpuPalette::Composite2C02 => PpuPalette::Composite2C02,
76 rustynes_mappers::VsPpuPalette::Rgb2C03 => PpuPalette::Rgb2C03,
77 rustynes_mappers::VsPpuPalette::Rgb2C04_0001 => PpuPalette::Rgb2C04_0001,
78 rustynes_mappers::VsPpuPalette::Rgb2C04_0002 => PpuPalette::Rgb2C04_0002,
79 rustynes_mappers::VsPpuPalette::Rgb2C04_0003 => PpuPalette::Rgb2C04_0003,
80 rustynes_mappers::VsPpuPalette::Rgb2C04_0004 => PpuPalette::Rgb2C04_0004,
81 rustynes_mappers::VsPpuPalette::Rgb2C05 => PpuPalette::Rgb2C05,
82 }
83}
84
85/// Initial reset state for the bus.
86fn fresh_ram() -> Box<[u8; RAM_SIZE]> {
87 // Deterministic seeded fill — for now zero, matching most emulators'
88 // "post-power-on" approximation.
89 Box::new([0u8; RAM_SIZE])
90}
91
92/// v1.1.0 beta.2 (Workstream C, T-110-C3) — the class of a captured CPU write.
93///
94/// One per event-viewer timeline entry: PPU `$2000-$3FFF`, APU `$4000-$4017`,
95/// or mapper `$4020-$FFFF`, tagged (in [`EventRec`]) with the PPU position at
96/// the moment of the write.
97#[cfg(feature = "debug-hooks")]
98#[derive(Clone, Copy, Debug, Eq, PartialEq)]
99pub enum EventKind {
100 /// A `$2000-$3FFF` PPU-register write.
101 PpuWrite,
102 /// A `$4000-$4017` APU / I/O-register write.
103 ApuWrite,
104 /// A `$4020-$FFFF` mapper-register write.
105 MapperWrite,
106 /// A `$2000-$3FFF` PPU-register read (v1.5.0 Workstream A2 — the graphical
107 /// PPU Event Viewer draws reads as well as writes, so the read/write heatmap
108 /// + the register-access table can show both directions).
109 PpuRead,
110}
111
112#[cfg(feature = "debug-hooks")]
113impl EventKind {
114 /// Whether this event is a CPU read (vs a write). Used by the v1.5.0 PPU
115 /// Event Viewer heatmap to colour reads (blue) vs writes (red).
116 #[must_use]
117 pub const fn is_read(self) -> bool {
118 matches!(self, Self::PpuRead)
119 }
120}
121
122/// One event-viewer record: kind + the PPU `(scanline, dot)` + the address +
123/// (v1.5.0 A2) the byte read or written.
124#[cfg(feature = "debug-hooks")]
125#[derive(Clone, Copy, Debug)]
126pub struct EventRec {
127 /// What happened.
128 pub kind: EventKind,
129 /// PPU scanline at the event (`-1` = pre-render, `0..=239` visible, ...).
130 pub scanline: i16,
131 /// PPU dot (`0..=340`).
132 pub dot: u16,
133 /// The accessed address.
134 pub addr: u16,
135 /// The byte written, or the byte the read returned (v1.5.0 Workstream A2).
136 pub value: u8,
137}
138
139/// Max events captured per frame (bounded so a write-heavy frame can't grow the
140/// log without limit; a frame has at most a few thousand CPU writes).
141#[cfg(feature = "debug-hooks")]
142const EVENT_CAP: usize = 20_000;
143
144/// v1.1.0 beta.3 (Workstream E, T-110-E2) — one CPU bus-access record for the
145/// Lua `onRead` / `onWrite` callbacks: direction + full address + the byte.
146///
147/// Distinct from [`EventRec`] (which is the scanline/dot-oriented event-viewer
148/// record): this captures *every* CPU read and write across the whole address
149/// space, with the value, so a script can react to a specific access. Output-
150/// only and gated behind `access_logging`; the host (Lua engine) enables it
151/// only while `onRead`/`onWrite` callbacks are registered.
152#[cfg(feature = "debug-hooks")]
153#[derive(Clone, Copy, Debug)]
154pub struct AccessRec {
155 /// `true` for a CPU write, `false` for a CPU read.
156 pub write: bool,
157 /// The accessed CPU address (`$0000-$FFFF`).
158 pub addr: u16,
159 /// The byte written, or the byte the read returned.
160 pub value: u8,
161}
162
163/// Max bus accesses captured per frame. A frame issues on the order of 30k CPU
164/// cycles; this caps the worst case so a tight loop can't grow the log
165/// unbounded. A frame that overflows the cap is truncated (the tail is dropped).
166#[cfg(feature = "debug-hooks")]
167const ACCESS_CAP: usize = 60_000;
168
169/// v1.2.0 (Workstream E, T-110-E1) — one interrupt-service record for the Lua
170/// `onNmi` / `onIrq` callbacks: the service direction + the vector the CPU
171/// fetched its new PC from.
172///
173/// Captured at the commit point — [`Bus::notify_irq_service`], called once per
174/// real interrupt entry right before the CPU reads the service vector. This is
175/// the *committed* service (the same point the IRQ trace records), NOT the
176/// speculative `poll_nmi` / `poll_irq` sampler that ADR 0010 flagged as
177/// unreliable — so a script that watches `onNmi`/`onIrq` sees exactly the
178/// interrupts the CPU actually serviced this frame, in order. Output-only and
179/// gated behind `interrupt_logging`; the host (Lua engine) enables it only
180/// while `onNmi`/`onIrq` callbacks are registered.
181#[cfg(feature = "debug-hooks")]
182#[derive(Clone, Copy, Debug, Eq, PartialEq)]
183pub struct InterruptRec {
184 /// `true` for an NMI service entry (`$FFFA`), `false` for an IRQ/BRK
185 /// service entry (`$FFFE`).
186 pub is_nmi: bool,
187 /// The service vector the CPU fetched its new PC from (`$FFFA` for an NMI,
188 /// `$FFFE` for IRQ/BRK).
189 pub vector: u16,
190}
191
192/// Max interrupt-service records captured per frame. A frame services at most a
193/// few hundred interrupts (NMI once + mapper/APU IRQs); this caps a pathological
194/// case so the log can't grow unbounded. A frame that overflows is truncated.
195#[cfg(feature = "debug-hooks")]
196const INTERRUPT_CAP: usize = 4_096;
197
198/// v1.4.0 Workstream D (D2) — the class of hardware event an event-driven
199/// breakpoint can trigger on.
200///
201/// These are tapped at the SAME observational commit points the event-viewer /
202/// interrupt-service / bus-access logs already use (`Bus::cpu_read`,
203/// `Bus::cpu_write`, `Bus::notify_irq_service`, the DMC-DMA GET, the `$4014`
204/// write). A hit only RECORDS the event (kind + PPU position); it never mutates
205/// emulator-visible state, so the determinism contract holds and the
206/// feature-off build is byte-identical.
207///
208/// The 16 categories are packed into a `u16` arm mask (see
209/// [`LockstepBus::set_event_breakpoints`]); the bit index is the discriminant.
210#[cfg(feature = "debug-hooks")]
211#[derive(Clone, Copy, Debug, Eq, PartialEq)]
212#[repr(u8)]
213pub enum EventBpKind {
214 /// An NMI service entry (`$FFFA`), observed at the interrupt-service commit.
215 Nmi = 0,
216 /// An IRQ / BRK service entry (`$FFFE`), observed at the same commit.
217 Irq = 1,
218 /// A sprite-0 hit, observed when the CPU reads `$2002` with bit 6 set (the
219 /// point games actually detect the hit; purely observational).
220 Sprite0Hit = 2,
221 /// An OAM DMA, observed at the `$4014` write that starts it.
222 OamDma = 3,
223 /// A DMC DMA sample fetch (the GET cycle).
224 DmcDma = 4,
225 /// A PPU-register read (`$2000-$3FFF`).
226 PpuRead = 5,
227 /// A PPU-register write (`$2000-$3FFF`).
228 PpuWrite = 6,
229 /// An APU / I/O-register read (`$4000-$4017`).
230 ApuRead = 7,
231 /// An APU / I/O-register write (`$4000-$4017`).
232 ApuWrite = 8,
233 /// A mapper-register read (`$4020-$FFFF`).
234 MapperRead = 9,
235 /// A mapper-register write (`$4020-$FFFF`).
236 MapperWrite = 10,
237}
238
239#[cfg(feature = "debug-hooks")]
240impl EventBpKind {
241 /// The arm-mask bit for this kind.
242 #[must_use]
243 pub const fn bit(self) -> u16 {
244 1u16 << (self as u8)
245 }
246
247 /// A human-readable label (used by the debugger UI + tests).
248 #[must_use]
249 pub const fn label(self) -> &'static str {
250 match self {
251 Self::Nmi => "NMI entry",
252 Self::Irq => "IRQ entry",
253 Self::Sprite0Hit => "Sprite-0 hit",
254 Self::OamDma => "OAM DMA",
255 Self::DmcDma => "DMC DMA",
256 Self::PpuRead => "PPU read",
257 Self::PpuWrite => "PPU write",
258 Self::ApuRead => "APU read",
259 Self::ApuWrite => "APU write",
260 Self::MapperRead => "Mapper read",
261 Self::MapperWrite => "Mapper write",
262 }
263 }
264
265 /// All categories, in discriminant order (for the UI checkbox list).
266 #[must_use]
267 pub const fn all() -> [Self; 11] {
268 [
269 Self::Nmi,
270 Self::Irq,
271 Self::Sprite0Hit,
272 Self::OamDma,
273 Self::DmcDma,
274 Self::PpuRead,
275 Self::PpuWrite,
276 Self::ApuRead,
277 Self::ApuWrite,
278 Self::MapperRead,
279 Self::MapperWrite,
280 ]
281 }
282}
283
284/// v1.4.0 Workstream D (D2) — one event-driven breakpoint hit.
285///
286/// Carries the kind, the associated address (`0` for the interrupt entries that
287/// carry none), and the full timing context (frame / CPU cycle / PPU
288/// scanline+dot) at the moment of the event. Recorded by the first armed-event
289/// tap of a frame; the frontend takes it via
290/// [`crate::Nes::take_event_break_hit`] to pause + report.
291#[cfg(feature = "debug-hooks")]
292#[derive(Clone, Copy, Debug, Eq, PartialEq)]
293pub struct EventBreakHit {
294 /// Which event fired.
295 pub kind: EventBpKind,
296 /// The associated CPU address (the read/write address, the OAM-DMA `$4014`,
297 /// the DMC sample address, or the service vector for NMI/IRQ).
298 pub addr: u16,
299 /// PPU frame counter at the event.
300 pub frame: u64,
301 /// Cumulative CPU cycle at the event.
302 pub cycle: u64,
303 /// PPU scanline (`-1` pre-render .. `260`).
304 pub scanline: i16,
305 /// PPU dot (`0..=340`).
306 pub dot: u16,
307}
308
309/// Lockstep bus.
310///
311/// Owns the entire emulator's mutable state. The CPU borrows `&mut LockstepBus`
312/// during `Cpu::step`. The PPU and APU are ticked from the bus's
313/// `cpu_read`/`cpu_write` implementations (3 dots per CPU cycle, NTSC; APU
314/// every CPU cycle).
315// The per-phase IRQ snapshots (Phase B2 of the C1 IRQ-timing rework) add
316// 4 bools beyond the original 3 (last_nmi_level / nmi_edge_latch /
317// in_dmc_dma), plus another `trace_last_a12` when the trace feature is
318// on. They're independent state words, not a single enum-modelled
319// machine — silencing the lint is the right call.
320#[allow(clippy::struct_excessive_bools)]
321pub struct LockstepBus {
322 /// CPU RAM (2 KiB), mirrored every 0x800 bytes from `$0000-$1FFF`.
323 pub(crate) ram: Box<[u8; RAM_SIZE]>,
324 /// PPU instance.
325 pub(crate) ppu: Ppu,
326 /// APU instance.
327 pub(crate) apu: Apu,
328 /// Cartridge metadata (kept for save-state and debugger).
329 pub(crate) cart: Cartridge,
330 /// Boxed mapper.
331 pub(crate) mapper: Box<dyn Mapper>,
332 /// v2.8.0 Phase 4 — the mapper's capability flags, cached at
333 /// construction (and refreshed when [`Self::power_cycle`] rebuilds the
334 /// mapper) so the per-CPU-cycle hot loop can skip the up-to-four
335 /// virtual dispatches (`notify_cpu_cycle` / `mix_audio` /
336 /// `notify_frame_event` / `irq_pending`) on boards that don't use
337 /// them. Constant per mapper type; NOT part of the save-state.
338 mapper_caps: rustynes_mappers::MapperCaps,
339 /// The original iNES/NES-2.0 ROM bytes, kept so [`Self::power_cycle`] can
340 /// rebuild the mapper to a true power-on state (fresh bank registers,
341 /// cleared CHR-RAM + volatile PRG-RAM). `None` on the FDS path (which has
342 /// no iNES image; FDS netplay is unsupported). NOT part of the save-state
343 /// (constant; the encoder skips it).
344 rom_bytes: Option<Box<[u8]>>,
345 /// Standard NES controllers (player 1 on `$4016`, player 2 on `$4017`).
346 pub(crate) controllers: [Controller; 2],
347 /// Four Score 4-player adapter. When `true`, `$4016`/`$4017` multiplex
348 /// four controllers + an adapter signature over a 24-read serial sequence
349 /// (nesdev "Four score"; matches `Mesen2` / `TetaNES`). When `false` (default)
350 /// the read path is byte-identical to the standard two-controller
351 /// behavior, so the determinism contract and existing save-states are
352 /// unaffected.
353 four_score: bool,
354 /// v2.1.7 P5 — power-on 2 KiB work-RAM fill selection. [`crate::nes::PowerOnRam::Zeroed`]
355 /// (default) leaves the established all-zero power-up state; the other
356 /// variants are opt-in and deterministic. Stored so [`Self::power_cycle`] can
357 /// re-apply the same fill after it zeroes RAM, keeping `power_cycle == fresh
358 /// boot`. At the default this is inert (the zero fill matches `fresh_ram()`).
359 power_on_ram: crate::nes::PowerOnRam,
360 /// v2.1.7 P5 — selected 2C02 die revision (see [`PpuRevision`]). Stored so
361 /// [`Self::power_cycle`] can re-apply it after the PPU is reconstructed
362 /// (the PPU field is lost on rebuild, like the Vs. palette).
363 /// [`PpuRevision::default`] models no extra behavior → byte-identical.
364 ppu_die_revision: PpuRevision,
365 /// v2.1.7 P5 — selected power-up palette pattern (see [`PaletteInit`]).
366 /// Re-applied on [`Self::power_cycle`] after the PPU (and thus its palette
367 /// RAM) is rebuilt. [`PaletteInit::default`] is all-zero → byte-identical.
368 power_up_palette: PaletteInit,
369 /// Players 3 (`$4016`) and 4 (`$4017`) — only polled when
370 /// [`Self::four_score`] is set.
371 controllers34: [Controller; 2],
372 /// Per-port Four Score read counter (0-7 = primary pad, 8-15 = secondary
373 /// pad, 16-23 = signature, then 1s). Reset on each strobe.
374 four_score_idx: [u8; 2],
375 /// Per-port Four Score signature shift register, reloaded on each strobe
376 /// (port 0 = `0x08`, port 1 = `0x04`; shifted out LSB-first).
377 four_score_sig: [u8; 2],
378 /// Output-only `TAStudio` lag-log flag (v1.6.0 Workstream A3): set `true`
379 /// whenever the running program reads a controller port (`$4016`/`$4017`)
380 /// during the current frame; cleared at the top of each
381 /// [`crate::Nes::run_frame`]. A frame still `false` at frame end is a "lag
382 /// frame" (the game polled no input that frame). `debug-hooks`-gated and
383 /// never read back into emulation, so the shipped build stays byte-identical
384 /// and the determinism contract is unaffected.
385 #[cfg(feature = "debug-hooks")]
386 controller_polled: bool,
387 /// Vs. System DIP switches (8 bits, switch 1 = bit 0 .. switch 8 = bit 7).
388 /// Read through the upper bits of `$4016`/`$4017` per the Vs. protocol
389 /// (nesdev "Vs. System"). Only consulted when the cart is
390 /// [`rustynes_mappers::ConsoleType::VsSystem`]; on a standard NES cart the
391 /// `$4016`/`$4017` read path is byte-identical regardless of this value.
392 vs_dip: u8,
393 /// Vs. System coin-acceptor state: bit 0 = acceptor #1 ($4016 bit 5),
394 /// bit 1 = acceptor #2 ($4016 bit 6). A real coin pulse reads true for
395 /// ~40-70 ms; the frontend latches it for a configurable number of frames
396 /// via [`LockstepBus::insert_coin`] and clears it with
397 /// [`LockstepBus::clear_coin`]. Vs.-System carts only.
398 vs_coin: u8,
399 /// Vs. System service button ($4016 bit 2). Vs.-System carts only.
400 vs_service: bool,
401 /// v2.0.0 beta.5 (Vs. `DualSystem`): `true` when this console is the SUB
402 /// half of a `DualSystem` pair — `$4016` reads then return bit 7 = `0x80`
403 /// (the main/sub identity bit the ROM polls; hard-pinned `0` on a single
404 /// console, byte-identically). Set only by the `VsDualSystem` wrapper.
405 vs_is_sub: bool,
406 /// v2.0.0 beta.5 (Vs. `DualSystem`): the external `/IRQ` line driven by the
407 /// PARTNER console's `$4016` bit-1 signal (Mesen2 `IRQSource::External`
408 /// via `UpdateMainSubBit`). OR'd into [`Bus::irq_level`]; always `false`
409 /// on a single console, so the default IRQ path is byte-identical.
410 vs_external_irq: bool,
411 /// v2.0.0 beta.5 (Vs. `DualSystem`): the last `$4016`-write bit-1 value
412 /// (the main/sub comms signal) + a dirty latch the wrapper polls after
413 /// each step batch. The bus only RECORDS the LEVEL (deliberately not
414 /// edge-filtered — see [`Self::vs_4016_bit1_dirty`]); the cross-console
415 /// wiring (asserting the partner's `/IRQ`, the shared-WRAM swap) lives in
416 /// the wrapper — no bus ever references the other console.
417 vs_4016_bit1: bool,
418 /// See [`Self::vs_4016_bit1`] — set on EVERY `$4016` write, regardless
419 /// of whether bit 1 changed; cleared by [`Self::take_vs_mainsub_edge`].
420 /// Deliberately level-driven, not edge-filtered: at reset both consoles
421 /// write `$4016 = $00` to establish the wrapper's seeded main/sub
422 /// levels, and an edge filter starting from a `false` latch would
423 /// swallow that seeded-HIGH -> written-LOW transition and deadlock the
424 /// boot handshake (see the `cpu_write` `$4016` arm for the full
425 /// rationale). Re-applying an unchanged level is idempotent in the
426 /// wrapper, so marking every write dirty (not just changed ones) is
427 /// correct, if conservatively named.
428 vs_4016_bit1_dirty: bool,
429 /// Optional non-standard input-device overlay per port (`$4016`/`$4017`).
430 /// When a port has `Some(device)`, [`Self::read_port`] returns that
431 /// device's byte instead of the standard controller / Four Score serial
432 /// byte. `None` (the default) leaves the existing path byte-identical, so
433 /// the default + Four Score reads and the determinism contract are
434 /// unaffected unless a device is explicitly attached.
435 expansion_device: [Option<crate::input_device::InputDevice>; 2],
436 /// A3 (v2.2.3), default **off**: serve a Zapper's light bit from the
437 /// beam-relative temporal model instead of the frame-granular one. See
438 /// [`Bus::set_zapper_temporal_light`].
439 zapper_temporal_light: bool,
440 /// Famicom built-in **microphone** signal (v2.2.0 "Capstone"). The hardwired
441 /// second Famicom controller carries a push-to-talk microphone whose state is
442 /// read on **`$4016` bit 2** (not `$4017`) — games such as *The Legend of
443 /// Zelda* (killing Pols Voice), *Kid Icarus*, *Raid on Bungeling Bay*, and
444 /// *Takeshi no Chōsenjō* poll it. Modelled as a single live bit (the analog
445 /// mic is quantized to "loud enough / not" by the frontend, matching how the
446 /// Famicom's comparator fed the port): `true` ORs `1` into `$4016.D2`.
447 /// Default `false` leaves the `$4016` read byte-identical (bit 2 is otherwise
448 /// open-bus / 0), so the standard controller path is unaffected until a
449 /// frontend explicitly drives the mic via [`Self::set_microphone`].
450 famicom_mic: bool,
451 /// v1.1.0 beta.1 (T-110-B4) — optional per-game nametable mirroring
452 /// override. `None` (default) defers to the mapper's `nametable_address`
453 /// (byte-identical). When `Some`, the standard `$2000-$3EFF` nametable
454 /// translation uses this mirroring instead — a load-time correction for
455 /// ROMs with a wrong iNES mirroring flag, supplied by the frontend's game
456 /// database. Does NOT affect mapper-supplied VRAM (`nametable_fetch`, e.g.
457 /// 4-screen). Persisted in the save-state so rollback / restore stay
458 /// consistent. The core test suites never set it, so `AccuracyCoin` / the
459 /// oracle are unaffected.
460 nt_mirroring_override: Option<rustynes_mappers::Mirroring>,
461 /// v1.1.0 beta.2 (T-110-C3) — event-viewer log (this frame's CPU-write
462 /// events). Output-only; populated only while `event_logging`, cleared per
463 /// frame. Gated on `debug-hooks` so the default hot path is untouched.
464 #[cfg(feature = "debug-hooks")]
465 events: alloc::vec::Vec<EventRec>,
466 /// Whether the event viewer is recording. Default `false`.
467 #[cfg(feature = "debug-hooks")]
468 event_logging: bool,
469 /// v1.1.0 beta.3 (T-110-E2) — full CPU bus-access log (reads + writes +
470 /// values) for the Lua `onRead`/`onWrite` callbacks. Output-only; populated
471 /// only while `access_logging`, cleared per frame.
472 #[cfg(feature = "debug-hooks")]
473 accesses: alloc::vec::Vec<AccessRec>,
474 /// Whether the bus-access log is recording. Default `false`.
475 #[cfg(feature = "debug-hooks")]
476 access_logging: bool,
477 /// v1.2.0 (T-110-E1) — per-frame interrupt-service log (this frame's
478 /// committed NMI / IRQ / BRK service entries) for the Lua `onNmi`/`onIrq`
479 /// callbacks. Output-only; populated only while `interrupt_logging`, cleared
480 /// per frame.
481 #[cfg(feature = "debug-hooks")]
482 interrupts: alloc::vec::Vec<InterruptRec>,
483 /// Whether the interrupt-service log is recording. Default `false`.
484 #[cfg(feature = "debug-hooks")]
485 interrupt_logging: bool,
486 /// v1.4.0 Workstream D (D2) — armed event-breakpoint categories, packed as a
487 /// bitmask of [`EventBpKind::bit`]. `0` (default) disarms every category, so
488 /// the per-access tap is a single `mask == 0` early-out — the default + the
489 /// feature-off build are byte-identical and pay no per-cycle cost. Output-
490 /// only: a hit records [`Self::event_break_hit`] but never mutates state.
491 #[cfg(feature = "debug-hooks")]
492 event_bp_mask: u16,
493 /// The first event-breakpoint hit of the current frame (`None` until one
494 /// fires). Recorded by the taps, taken by the frontend after `run_frame`.
495 #[cfg(feature = "debug-hooks")]
496 event_break_hit: Option<EventBreakHit>,
497 /// Cumulative CPU cycle counter.
498 pub(crate) cycle: u64,
499
500 /// OAM DMA pending source page (set by `$4014` write; consumed on the
501 /// next `cpu_read`/`cpu_write`).
502 dma_pending: Option<u8>,
503 /// Cycles owed to the OAM DMA before the original access can complete.
504 dma_cycles_owed: u32,
505 /// OAM DMA scratch byte: read on even cycles, written on odd cycles.
506 dma_byte: u8,
507 /// OAM DMA progress index (0..256).
508 dma_idx: u16,
509 /// OAM DMA active source page (latched from `dma_pending`).
510 dma_page: u8,
511 /// CPU read address that OAM DMA halted. While the CPU is halted,
512 /// no-op DMA cycles keep this address on the 6502 core bus.
513 dma_halt_addr: u16,
514 /// Stage-D (`mc-r1-full-cpu`): the OAM DMA's original total cycle count
515 /// (513 or 514) latched at set-up, so the CPU-driven per-cycle
516 /// `oam_dma_step` can recompute `consumed = dma_total - dma_cycles_owed`
517 /// and the alignment across calls. 0 when no OAM DMA is in flight.
518 dma_total: u32,
519
520 /// Edge-detector latch for the PPU NMI line, used by `poll_nmi`.
521 last_nmi_level: bool,
522 /// Latched NMI edge (consumed by `poll_nmi`).
523 nmi_edge_latch: bool,
524 /// v2.5.1 (ADR 0038) — externally asserted /NMI, for co-simulation only.
525 ///
526 /// Active-high here (`true` = the pin is asserted, i.e. /NMI low). It is
527 /// OR'd into the poll rather than replacing it, so an injected NMI and a
528 /// PPU-generated one are the same event to the CPU -- which is the point:
529 /// the API sets the pin the CPU samples and does nothing else. It does not
530 /// bypass the poll, force a vector, or short-circuit the sequence.
531 ///
532 /// The field does not exist in a default build.
533 #[cfg(feature = "cosim-interrupt-inject")]
534 inject_nmi: bool,
535 /// v2.5.1 (ADR 0038) — externally asserted /IRQ. Level-sensitive, exactly
536 /// as the pin is, so it is masked by `I` through the CPU's own logic and a
537 /// pulse shorter than a poll is missed. Modelling it as a latch would make
538 /// injected IRQs behave unlike real ones.
539 #[cfg(feature = "cosim-interrupt-inject")]
540 inject_irq: bool,
541
542 /// v2.0 master-clock R1 substrate (Phase 1): PPU progress in master-clock
543 /// units, consumed by `run_ppu_to(target)` (ticks a dot while
544 /// `ppu_clock + ppu_divider <= target`). Only used under the R1 CPU loop.
545 ppu_clock: u64,
546 /// v2.0 master-clock R1 substrate: the cartridge region's `(cpu_divider,
547 /// ppu_divider)` in master clocks (NTSC 12/4, PAL 16/5, Dendy 15/5),
548 /// computed once at construction. The region never changes after power-on,
549 /// so caching these removes the per-CPU-cycle `match self.cart.region` from
550 /// the hottest R1 paths (`cpu_divider`, `run_ppu_to`). Behaviour-identical:
551 /// the value equals what the prior `region_dividers()` match returned.
552 cpu_div_cached: u8,
553 ppu_div_cached: u8,
554 /// v2.0 master-clock R1 substrate (Phase 1): master clocks consumed by
555 /// bus-side DMA cycles since the CPU last drained the accumulator (folded
556 /// into `Cpu::master_clock` in `end_cycle` to keep the CPU<->PPU phase
557 /// coherent across a DMA span). Drained by `take_dma_mc_consumed`.
558 dma_mc_consumed: u64,
559
560 /// External CPU data bus latch: last value driven onto the bus
561 /// by ANY device (CPU, DMC DMA, OAM DMA conflict reads).
562 ///
563 /// This is the classic "open bus" floating-latch value that NES
564 /// emulation refers to. Reads from unmapped or open-bus regions
565 /// return this value; the upper 3 bits of the controller-strobe
566 /// register reads (`$4016` / `$4017`) bleed through from this
567 /// latch. DMC DMA fetches update this latch (because the DMC
568 /// drives the external bus during halt).
569 open_bus: u8,
570 /// Internal CPU data bus latch: last value driven onto the bus
571 /// by a CPU-initiated read or write.
572 ///
573 /// The 2A03 silicon has two distinct data buses. The
574 /// **internal** bus is driven only by CPU operations (instruction
575 /// fetch, operand read, ALU result, write). DMC DMA fetches
576 /// drive only the **external** bus (`open_bus` above) — the
577 /// internal bus retains its prior value across a DMC halt. This
578 /// distinction is invisible while the CPU runs unimpeded (the
579 /// two buses carry the same value), but it surfaces on the SH*
580 /// unstable-store family when DMC DMA interleaves with the
581 /// store's address-high-byte AND computation, and on the `$4015`
582 /// bit-5 open-bus read after a DMC DMA fetch.
583 ///
584 /// Phase 1 of the v1.0.0-final `linked-puzzling-sutherland`
585 /// brief (`to-dos/phase-6-v1.0.0-final/sprint-6-sh-unstable-stores.md`).
586 /// Mirrored from every `cpu_read` / `cpu_write` path; explicitly
587 /// NOT updated by `dmc_dma_read` (the DMC fetch path).
588 internal_data_bus: u8,
589
590 /// Most recent CPU bus access — used by the 2A03 DMC-DMA readout-bug
591 /// emulation. (Address only; some bug variants need the address, the
592 /// bus value is the open-bus latch above.)
593 last_read_addr: u16,
594 /// Side-effect register read whose absolute high-byte operand was
595 /// halted by DMC DMA one CPU read before the actual register access.
596 deferred_dma_replay_addr: u16,
597 /// True while we're servicing a DMC DMA fetch — used to suppress
598 /// recursion / re-entrancy when the DMA controller invokes `raw_cpu_read`.
599 in_dmc_dma: bool,
600 /// v2.0 interleaved-DMA Phase B (`mc-r1-substrate`): the `TriCNES`
601 /// `DMCDMA_Halt` flag — set when the interleaved DMC DMA starts, cleared
602 /// after a GET cycle. Gates whether the current get cycle is the halt
603 /// re-read or the actual sample fetch. Only used by `dmc_dma_step`.
604 dmc_halt: bool,
605 /// Program M (M-2, `mc-r1-dmc-oam-overlap`): whether the most recent
606 /// `dmc_dma_step` performed the GET (vs a halt/dummy/align). Read by the
607 /// read1 overlap loop to decide whether the DMC cycle can share an OAM cycle.
608 dmc_step_was_get: bool,
609 /// W3-Stage-1 (`mc-r1-dma-unified`): the unified engine's OAM-DMA-active
610 /// flag (`TriCNES` `DoOAMDMA` once latched). The 513/514 length is EMERGENT
611 /// from `uni_oam_halt`/`uni_oam_aligned` + the per-cycle dispatch — no
612 /// owed-cycle counter.
613 uni_oam_active: bool,
614 /// W3-Stage-1: `TriCNES` `OAMDMA_Halt` — set when the OAM DMA's FIRST
615 /// serviced cycle lands on the OAM engine's read half (at floor parity:
616 /// `put_cycle == true`, the floor's `self.cycle & 1 == 0` -> 514 case);
617 /// cleared at the end of every OAM-read-half cycle.
618 uni_oam_halt: bool,
619 /// W3-Stage-1: `TriCNES` `OAMDMA_Aligned` — set by the OAM read, consumed
620 /// by the OAM write; force-cleared by a DMC GET (the emergent post-GET
621 /// realign: the next write half becomes an alignment dummy and the byte
622 /// is re-read).
623 uni_oam_aligned: bool,
624 /// W3-Stage-1: `TriCNES` `DMAAddress` — the OAM byte index (0..=255;
625 /// reaching 256 on a write completes the DMA). Only increments on writes,
626 /// so a DMC-GET-stalled byte is re-read.
627 uni_oam_addr: u16,
628
629 /// v2.1.7 "Hardware Revisions & DMA Frontier" — the emulated Ricoh 2A03 die
630 /// revision, gating the DMA unit's "unexpected DMA" extra halt-read on the
631 /// DMC-halt-overlaps-OAM-halt cycle. **Default [`Cpu2A03Revision::Rp2A03G`]**
632 /// = byte-identical to the pre-v2.1.7 core; it performs the extra read *in
633 /// the model*, but that read is a documented no-op on every committed oracle
634 /// (the parked address during a DMC+OAM overlap is never a side-effect
635 /// register — see the enum docs + ADR 0033), so it changes nothing
636 /// observable. [`Cpu2A03Revision::Rp2A03H`] omits the modeled read and is
637 /// consequently byte-identical to `Rp2A03G` across the entire committed DMA
638 /// corpus today (opt-in, deterministic, unverified direction). A config
639 /// knob, NOT part of the save-state: the only state it influences (the
640 /// parked-address side-effect re-read count during a DMC+OAM overlap) is
641 /// fully re-derived from the deterministic timeline, so a save/restore
642 /// round-trip stays byte-identical for a fixed revision.
643 cpu_2a03_revision: Cpu2A03Revision,
644
645 /// Active Game Genie codes, keyed by the PRG address they patch
646 /// (`$8000-$FFFF`). Applied on the CPU read path; empty by default, so
647 /// with no codes active reads are byte-identical to a build without the
648 /// feature (the determinism contract is preserved). NOT part of the
649 /// save-state — codes are a user overlay persisted by the frontend, not
650 /// emulation state. See [`crate::genie`].
651 genie_codes: BTreeMap<u16, GenieCode>,
652
653 /// Which half of the current CPU cycle the lockstep scheduler is in.
654 /// See [`M2Phase`] for the convention; see
655 /// [`LockstepBus::current_m2_phase`] for the read accessor.
656 ///
657 /// Maintained by `tick_one_cpu_cycle`: enters each cycle at
658 /// [`M2Phase::Low`], transitions to [`M2Phase::High`] after sub-dot
659 /// 1 of the 3-PPU-dot tick loop (the M2-rising boundary), then
660 /// resets to [`M2Phase::Low`] at end-of-cycle. As of Phase B2 of
661 /// the C1 IRQ-timing rework this is still informational; the
662 /// production [`Bus::poll_irq_at_phase`] path reads from the
663 /// `irq_snapshot_*` fields below.
664 m2_phase: M2Phase,
665
666 /// Deferred controller strobe write (Session-24 / Phase 3 of the
667 /// v1.0.0-final brief). Mirrors Mesen2's `NesControlManager`
668 /// `_writeAddr` / `_writeValue` / `_writePending` triplet (see
669 /// `Core/NES/NesControlManager.cpp` lines 252-273): a CPU write to
670 /// `$4016` (or `$4017`) does NOT directly update the controllers'
671 /// strobe state. Instead the write is buffered here.
672 /// `controller_write_pending` is set to 1 (odd-cycle write) or 2
673 /// (even-cycle write) at the moment of the CPU write, then
674 /// decremented every CPU cycle at the START of `tick_one_cpu_cycle`
675 /// (BEFORE the 3-dot PPU loop runs); when it reaches 0 the buffered
676 /// value is committed to `Controller::write_strobe`. Multiple
677 /// writes within the commit window collapse — the latest value
678 /// wins (the buffer is single-slot, the previous value is
679 /// silently overwritten).
680 ///
681 /// This is the load-bearing structural change for `AccuracyCoin`
682 /// `Controller Strobing` Test 4 (a 1-cycle DEC `$4016` strobe pulse
683 /// whose 0→1→0 sequence must NOT fire the latch when it happens
684 /// to span an L→H half-cycle pair — under deferred commit both
685 /// writes target the SAME commit cycle, the second overwrites the
686 /// first, no edge is observed). See
687 /// `docs/audit/session-24-phase3-controller-strobing-2026-05-23.md`.
688 controller_write_pending: u8,
689 /// Buffered controller-write value (latched at the moment of the
690 /// CPU write; committed when `controller_write_pending` reaches 0).
691 controller_write_value: u8,
692
693 /// Mapper-side IRQ line snapshotted at the conventional M2-low
694 /// boundary of the current CPU cycle (between PPU sub-dot 0 and
695 /// sub-dot 1, per the [`M2Phase`] convention). Updated by
696 /// [`LockstepBus::tick_one_cpu_cycle`] every cycle; read by
697 /// [`Bus::poll_irq_at_phase`] when `phase == M2Phase::Low`.
698 ///
699 /// Phase B2 of the C1 IRQ-timing rework: the storage is
700 /// unconditional (not gated on the `irq-timing-trace` feature) so
701 /// it's available on every build of the bus. Per Phase A's
702 /// empirical finding the M2-low and M2-high values are byte-
703 /// identical for every baseline trace ROM, but the storage is kept
704 /// separate so Phase B4's MMC3 sub_dot-aware A12 filter can change
705 /// the two halves' values independently.
706 irq_snapshot_mapper_at_low: bool,
707 /// APU-side IRQ line snapshotted at the conventional M2-low
708 /// boundary. See [`Self::irq_snapshot_mapper_at_low`].
709 irq_snapshot_apu_at_low: bool,
710 /// Mapper-side IRQ line snapshotted at the conventional M2-high
711 /// boundary of the current CPU cycle. The exact intra-cycle
712 /// position is "between the end of PPU sub-dot 2 and the call to
713 /// `mapper.notify_cpu_cycle`" — i.e. the historical query point the
714 /// pre-Phase-B2 `Bus::poll_irq` impl used when called from
715 /// `Cpu::idle_tick` after `bus.on_cpu_cycle()` returned.
716 ///
717 /// Read by [`Bus::poll_irq`] and by
718 /// [`Bus::poll_irq_at_phase`] when `phase == M2Phase::High`.
719 irq_snapshot_mapper_at_high: bool,
720 /// APU-side IRQ line snapshotted at the conventional M2-high
721 /// boundary. See [`Self::irq_snapshot_mapper_at_high`].
722 irq_snapshot_apu_at_high: bool,
723
724 /// Optional IRQ-timing trace buffer (Track C1 pre-work, gated on the
725 /// `irq-timing-trace` cargo feature). See `crates/rustynes-core/src/irq_trace.rs`
726 /// and ADR-0002 "Decision (revised, 2026-05-13)".
727 #[cfg(feature = "irq-timing-trace")]
728 pub(crate) irq_trace: Option<IrqTrace>,
729 /// Scratch latch the `PpuBusAdapter` writes when the mapper sees an
730 /// `notify_a12` call. Polled between every PPU sub-dot tick inside
731 /// `tick_one_cpu_cycle` and drained into the current cycle record's
732 /// `a12_events`. Only populated when the trace feature is on.
733 #[cfg(feature = "irq-timing-trace")]
734 pub(crate) trace_a12_latest: Option<bool>,
735 /// Last A12 level seen across cycle boundaries; used to filter out
736 /// "no transition" sub-dots so the trace records only the actual
737 /// rising / falling edges.
738 #[cfg(feature = "irq-timing-trace")]
739 pub(crate) trace_last_a12: bool,
740 /// Scratch buffer for A12 events accumulated during a single CPU
741 /// cycle's 3 PPU dots. Drained into the trace record at end-of-cycle.
742 #[cfg(feature = "irq-timing-trace")]
743 pub(crate) trace_a12_scratch: alloc::vec::Vec<A12Event>,
744 /// Session-21 (Sprint 1 iteration 2 prereq) bus-access tracker.
745 ///
746 /// Set by `cpu_read` / `cpu_write` / the DMC DMA service path / the
747 /// OAM DMA service path BEFORE `tick_one_cpu_cycle` records the
748 /// per-cycle bus-access columns; consumed (and reset to
749 /// `BusAccess::Idle` / 0) inside `tick_one_cpu_cycle` after the
750 /// record is pushed. A single CPU cycle has at most one external
751 /// bus access — burn cycles (`idle_tick`) leave the tracker at
752 /// `BusAccess::Idle`, which is the correct semantics for the trace
753 /// (CPU internal cycles do not drive the bus).
754 ///
755 /// The DMA paths set this directly because the bus owns the cycle
756 /// during DMA halt and the CPU's `cpu_read` / `cpu_write` is not
757 /// invoked (the bus's `raw_cpu_read` is invoked instead, which
758 /// does not advance time on its own — `tick_one_cpu_cycle` is
759 /// called separately).
760 #[cfg(feature = "irq-timing-trace")]
761 pub(crate) trace_bus_access: BusAccess,
762 #[cfg(feature = "irq-timing-trace")]
763 pub(crate) trace_bus_addr: u16,
764 #[cfg(feature = "irq-timing-trace")]
765 pub(crate) trace_bus_data: u8,
766 /// PC of the instruction currently executing, latched by the
767 /// `trace_instr` hook (`cpu-instr-cycle-trace`). Copied into each
768 /// `CycleRecord.pc` so the per-cycle trace can be diffed against
769 /// `TriCNES` by ROM PC. Stays at the halted instruction's PC across
770 /// DMA-insertion cycles. `0` unless `cpu-instr-cycle-trace` is on.
771 #[cfg(feature = "irq-timing-trace")]
772 pub(crate) trace_last_pc: u16,
773 /// R1-path PPU position captured at cycle-start (`cpu_clock`) for the
774 /// `trace_end_cycle` diagnostic push (the R1 loop bypasses
775 /// `tick_one_cpu_cycle`'s own snapshot).
776 #[cfg(feature = "irq-timing-trace")]
777 pub(crate) trace_r1_scanline_start: i16,
778 #[cfg(feature = "irq-timing-trace")]
779 pub(crate) trace_r1_dot_start: u16,
780 #[cfg(feature = "irq-timing-trace")]
781 pub(crate) trace_r1_frame_start: u64,
782}
783
784impl LockstepBus {
785 /// Construct from a parsed ROM with a default 44.1 kHz audio sample rate.
786 ///
787 /// # Errors
788 ///
789 /// Returns the underlying [`RomError`] if the bytes don't parse.
790 pub fn new(rom_bytes: &[u8]) -> Result<Self, RomError> {
791 Self::with_sample_rate(rom_bytes, DEFAULT_SAMPLE_RATE)
792 }
793
794 /// Construct with an explicit audio sample rate.
795 ///
796 /// # Errors
797 ///
798 /// Returns the underlying [`RomError`] if the bytes don't parse.
799 // The struct-literal init grows with every feature-gated field; the W3
800 // unified-engine fields pushed it past the line gate.
801 #[allow(clippy::too_many_lines)]
802 pub fn with_sample_rate(rom_bytes: &[u8], sample_rate: u32) -> Result<Self, RomError> {
803 let (cart, mapper) = rustynes_mappers::parse(rom_bytes)?;
804 let mut bus = Self::from_cart_and_mapper(cart, mapper, sample_rate);
805 // Keep the iNES bytes so `power_cycle` can rebuild the mapper to a true
806 // power-on state. Cheap relative to the cart it already holds, and never
807 // serialized into the save-state.
808 bus.rom_bytes = Some(Box::from(rom_bytes));
809 Ok(bus)
810 }
811
812 /// Construct a bus directly from an already-parsed cartridge + boxed mapper.
813 ///
814 /// This is the shared core of [`Self::with_sample_rate`] (iNES / NES 2.0
815 /// path) and [`Self::with_disk`] (Famicom Disk System path). Both produce a
816 /// [`Cartridge`] metadata value plus a `Box<dyn Mapper>`; this routine wires
817 /// up the PPU/APU region, the R1 master-clock dividers, and the rest of the
818 /// bus state identically for both.
819 // The struct-literal init grows with every feature-gated field; the W3
820 // unified-engine fields pushed it past the line gate.
821 #[allow(clippy::too_many_lines)]
822 pub(crate) fn from_cart_and_mapper(
823 cart: Cartridge,
824 mapper: Box<dyn Mapper>,
825 sample_rate: u32,
826 ) -> Self {
827 let region = match cart.region {
828 rustynes_mappers::Region::Pal => PpuRegion::Pal,
829 rustynes_mappers::Region::Dendy => PpuRegion::Dendy,
830 _ => PpuRegion::Ntsc,
831 };
832 let apu_region = match cart.region {
833 rustynes_mappers::Region::Pal => ApuRegion::Pal,
834 rustynes_mappers::Region::Dendy => ApuRegion::Dendy,
835 _ => ApuRegion::Ntsc,
836 };
837 // R1 master-clock dividers, cached once (region is immutable after parse).
838 // Identical to the prior `region_dividers()` match: NTSC 12/4, PAL 16/5,
839 // Dendy 15/5.
840 let (cpu_div_cached, ppu_div_cached): (u8, u8) = match cart.region {
841 rustynes_mappers::Region::Pal => (16, 5),
842 rustynes_mappers::Region::Dendy => (15, 5),
843 _ => (12, 4),
844 };
845 // v2.8.0 Phase 4 — cache the capability flags once (constant per
846 // mapper type); the per-cycle hot loop reads the copy.
847 let mapper_caps = mapper.caps();
848 let mut bus = Self {
849 ram: fresh_ram(),
850 ppu: Ppu::new(region),
851 apu: Apu::new(apu_region, sample_rate),
852 cart,
853 mapper,
854 mapper_caps,
855 // Set by `with_sample_rate` (iNES path); stays `None` for FDS.
856 rom_bytes: None,
857 controllers: [Controller::new(); 2],
858 four_score: false,
859 // v2.1.7 P5 — power-on config knobs, all at their byte-identical
860 // defaults (zeroed RAM, default revision, all-zero power-up palette).
861 power_on_ram: crate::nes::PowerOnRam::Zeroed,
862 ppu_die_revision: PpuRevision::Rp2c02H,
863 power_up_palette: PaletteInit::Zeroed,
864 controllers34: [Controller::new(); 2],
865 four_score_idx: [0; 2],
866 four_score_sig: [0; 2],
867 #[cfg(feature = "debug-hooks")]
868 controller_polled: false,
869 vs_dip: 0,
870 vs_coin: 0,
871 vs_service: false,
872 vs_is_sub: false,
873 vs_external_irq: false,
874 vs_4016_bit1: false,
875 vs_4016_bit1_dirty: false,
876 expansion_device: [None, None],
877 // v2.3.6: ON by default. See `set_zapper_temporal_light` — the frame
878 // model made a Duck Hunt hit impossible.
879 zapper_temporal_light: true,
880 famicom_mic: false,
881 nt_mirroring_override: None,
882 #[cfg(feature = "debug-hooks")]
883 events: alloc::vec::Vec::new(),
884 #[cfg(feature = "debug-hooks")]
885 event_logging: false,
886 #[cfg(feature = "debug-hooks")]
887 accesses: alloc::vec::Vec::new(),
888 #[cfg(feature = "debug-hooks")]
889 access_logging: false,
890 #[cfg(feature = "debug-hooks")]
891 interrupts: alloc::vec::Vec::new(),
892 #[cfg(feature = "debug-hooks")]
893 interrupt_logging: false,
894 #[cfg(feature = "debug-hooks")]
895 event_bp_mask: 0,
896 #[cfg(feature = "debug-hooks")]
897 event_break_hit: None,
898 cycle: 0,
899 dma_pending: None,
900 dma_cycles_owed: 0,
901 dma_byte: 0,
902 dma_idx: 0,
903 dma_page: 0,
904 dma_halt_addr: 0,
905 dma_total: 0,
906 last_nmi_level: false,
907 nmi_edge_latch: false,
908 #[cfg(feature = "cosim-interrupt-inject")]
909 inject_nmi: false,
910 #[cfg(feature = "cosim-interrupt-inject")]
911 inject_irq: false,
912 ppu_clock: 0,
913 cpu_div_cached,
914 ppu_div_cached,
915 dma_mc_consumed: 0,
916 open_bus: 0,
917 internal_data_bus: 0,
918 last_read_addr: 0,
919 deferred_dma_replay_addr: 0,
920 in_dmc_dma: false,
921 dmc_step_was_get: false,
922 uni_oam_active: false,
923 uni_oam_halt: false,
924 uni_oam_aligned: false,
925 uni_oam_addr: 0,
926 cpu_2a03_revision: Cpu2A03Revision::default(),
927 dmc_halt: false,
928 genie_codes: BTreeMap::new(),
929 m2_phase: M2Phase::Low,
930 irq_snapshot_mapper_at_low: false,
931 irq_snapshot_apu_at_low: false,
932 irq_snapshot_mapper_at_high: false,
933 irq_snapshot_apu_at_high: false,
934 controller_write_pending: 0,
935 controller_write_value: 0,
936 #[cfg(feature = "irq-timing-trace")]
937 irq_trace: None,
938 #[cfg(feature = "irq-timing-trace")]
939 trace_a12_latest: None,
940 #[cfg(feature = "irq-timing-trace")]
941 trace_last_a12: false,
942 #[cfg(feature = "irq-timing-trace")]
943 trace_a12_scratch: alloc::vec::Vec::new(),
944 #[cfg(feature = "irq-timing-trace")]
945 trace_bus_access: BusAccess::Idle,
946 #[cfg(feature = "irq-timing-trace")]
947 trace_bus_addr: 0,
948 #[cfg(feature = "irq-timing-trace")]
949 trace_bus_data: 0,
950 #[cfg(feature = "irq-timing-trace")]
951 trace_last_pc: 0,
952 #[cfg(feature = "irq-timing-trace")]
953 trace_r1_scanline_start: 0,
954 #[cfg(feature = "irq-timing-trace")]
955 trace_r1_dot_start: 0,
956 #[cfg(feature = "irq-timing-trace")]
957 trace_r1_frame_start: 0,
958 };
959 // Vs. System / PlayChoice-10: the arcade boards replace the 2C02 with a
960 // 2C03 / 2C04 / 2C05 RGB PPU. For ConsoleType::Nes (the default), the
961 // resolved type is VsPpuType::None -> Composite2C02, is_2c05 = false, so
962 // this is byte-for-byte a no-op on normal carts.
963 bus.reapply_vs_palette();
964 // F-2: under R1 the DMC byte-timer is driven at end-of-cycle by
965 // `cpu_clock_apu_dmc` (main's DMC fire-phase for DMASync).
966 {
967 bus.apu.set_dmc_driven_externally(true);
968 // Interleaved-DMA Phase A: seed the get/put + DMC fire-phase from one
969 // APUAlignment value. Fixed alignment 0 for now; Phase B drives it
970 // from the power-on PRNG (the 2 AccuracyCoin answer-key alignments).
971 bus.apu.seed_apu_alignment(0);
972 }
973 bus
974 }
975
976 /// Construct a Famicom Disk System bus from a `.fds` disk image and a
977 /// user-supplied 8 KiB BIOS (`disksys.rom`).
978 ///
979 /// Parses the disk container ([`rustynes_mappers::parse_fds`]), constructs the
980 /// FDS device ([`rustynes_mappers::Fds`]) as the bus's `Box<dyn Mapper>`, and
981 /// wires the bus exactly like a cartridge build (shared internal
982 /// `from_cart_and_mapper`). The FDS is NTSC/Famicom hardware, so the
983 /// synthetic [`Cartridge`] metadata reports [`rustynes_mappers::Region::Ntsc`].
984 ///
985 /// # Errors
986 ///
987 /// Returns [`RomError`] if the disk image is unparseable, or the BIOS is not
988 /// exactly 8 KiB.
989 pub fn with_disk(
990 disk_bytes: &[u8],
991 bios_bytes: &[u8],
992 sample_rate: u32,
993 ) -> Result<Self, RomError> {
994 let disk = rustynes_mappers::parse_fds(disk_bytes)?;
995 let fds = rustynes_mappers::Fds::new(disk, bios_bytes)?;
996 // Synthetic cartridge metadata: the bus only consults `cart.region`
997 // (verified — see `docs/audit` FDS Stage 1). The FDS device owns all
998 // PRG/CHR/BIOS storage, so the ROM byte fields are empty.
999 let cart = Cartridge {
1000 prg_rom: Box::default(),
1001 chr_rom: Box::default(),
1002 mapper_id: 20,
1003 submapper: 0,
1004 mirroring: rustynes_mappers::Mirroring::Horizontal,
1005 region: rustynes_mappers::Region::Ntsc,
1006 console_type: rustynes_mappers::ConsoleType::Nes,
1007 vs_ppu_type: rustynes_mappers::VsPpuType::None,
1008 vs_dual_system: false,
1009 prg_ram_size: 0x8000,
1010 chr_ram_size: 0x2000,
1011 has_battery: false,
1012 has_trainer: false,
1013 is_nes2: false,
1014 };
1015 Ok(Self::from_cart_and_mapper(cart, Box::new(fds), sample_rate))
1016 }
1017
1018 /// Build a bus that plays an NSF music file. Parses the `.nsf`, builds an
1019 /// [`rustynes_mappers::NsfMapper`] (a synthetic driver + the program image)
1020 /// as the bus's `Box<dyn Mapper>`, and reports synthetic NTSC cartridge
1021 /// metadata (the file carries no CHR / PPU program).
1022 ///
1023 /// # Errors
1024 ///
1025 /// Returns [`RomError::InvalidConfig`] when the NSF header is malformed.
1026 pub fn with_nsf(nsf_bytes: &[u8], sample_rate: u32) -> Result<Self, RomError> {
1027 let nsf = rustynes_mappers::parse_nsf(nsf_bytes)
1028 .map_err(|e| RomError::InvalidConfig(alloc::format!("{e}")))?;
1029 let mapper = rustynes_mappers::NsfMapper::new(&nsf);
1030 let cart = Cartridge {
1031 prg_rom: Box::default(),
1032 chr_rom: Box::default(),
1033 mapper_id: 31, // NSF banking is conventionally documented as mapper 31-like
1034 submapper: 0,
1035 mirroring: rustynes_mappers::Mirroring::Horizontal,
1036 // Playback is NTSC 60 Hz (vblank-NMI-driven) regardless of the
1037 // file's region preference; the PAL flag only feeds the driver's
1038 // init X-register. Exact non-60 Hz play rates are a documented
1039 // deferral (see `nsf.rs` module docs).
1040 region: rustynes_mappers::Region::Ntsc,
1041 console_type: rustynes_mappers::ConsoleType::Nes,
1042 vs_ppu_type: rustynes_mappers::VsPpuType::None,
1043 vs_dual_system: false,
1044 prg_ram_size: 0x2000,
1045 chr_ram_size: 0,
1046 has_battery: false,
1047 has_trainer: false,
1048 is_nes2: false,
1049 };
1050 Ok(Self::from_cart_and_mapper(
1051 cart,
1052 Box::new(mapper),
1053 sample_rate,
1054 ))
1055 }
1056
1057 /// Reset (warm). Defers to `Ppu::reset` and clears DMA state. CPU is
1058 /// reset by the caller.
1059 pub fn reset(&mut self) {
1060 self.ppu.reset();
1061 self.apu.reset();
1062 {
1063 self.apu.set_dmc_driven_externally(true);
1064 self.apu.seed_apu_alignment(0);
1065 }
1066 self.dma_pending = None;
1067 self.dma_cycles_owed = 0;
1068 self.dma_idx = 0;
1069 self.dma_halt_addr = 0;
1070 self.deferred_dma_replay_addr = 0;
1071 self.unified_dma_clear();
1072 }
1073
1074 /// Power-cycle. Zeroes RAM and resets all state. Caller resets the CPU.
1075 pub fn power_cycle(&mut self) {
1076 self.ram.fill(0);
1077 self.ppu = Ppu::new(self.ppu_region());
1078 // Re-apply the Vs./PC10 RGB-PPU configuration (lost when the PPU is
1079 // reconstructed). No-op for ConsoleType::Nes carts.
1080 self.reapply_vs_palette();
1081 // v2.1.7 P5 — re-apply the PPU-revision + power-up-palette config lost
1082 // when the PPU was reconstructed above, so `power_cycle == fresh boot`
1083 // holds for these knobs too (a core-only consumer that power-cycles
1084 // without a frontend still gets the configured hardware). All no-ops at
1085 // their defaults, so a default power-cycle stays byte-identical.
1086 self.ppu.set_revision(self.ppu_die_revision);
1087 self.ppu.apply_power_up_palette(self.power_up_palette);
1088 // v2.1.7 P5 — re-apply the power-on work-RAM fill after the `fill(0)`
1089 // above. At the default (`Zeroed`) this is the same zero fill.
1090 self.apply_power_on_ram();
1091 self.apu = Apu::new(self.apu_region(), self.apu.sample_rate);
1092 {
1093 self.apu.set_dmc_driven_externally(true);
1094 self.apu.seed_apu_alignment(0);
1095 }
1096 self.controllers = [Controller::new(); 2];
1097 // The Four Score stays "plugged in" (it's hardware config), but its
1098 // transient strobe/read state resets like the controllers above.
1099 self.controllers34 = [Controller::new(); 2];
1100 self.four_score_idx = [0; 2];
1101 self.four_score_sig = [0; 2];
1102 // Vs. System coin/service inputs are transient (DIP switches are
1103 // hardware config and persist across a power-cycle, like the panel).
1104 self.vs_coin = 0;
1105 self.vs_service = false;
1106 // v2.0.0 beta.5 (Vs. DualSystem): the comms latch + external IRQ are
1107 // transient signals; the sub identity is cabinet wiring and persists
1108 // (re-applied by the wrapper anyway).
1109 self.vs_external_irq = false;
1110 self.vs_4016_bit1 = false;
1111 self.vs_4016_bit1_dirty = false;
1112 // Non-standard input devices are unplugged on power-cycle (they are
1113 // re-attached explicitly by the frontend, like the controllers above).
1114 self.expansion_device = [None, None];
1115 // The microphone is a transient live signal; a power-cycle releases it
1116 // (the frontend re-drives it each frame while a key is held).
1117 self.famicom_mic = false;
1118 self.cycle = 0;
1119 self.dma_pending = None;
1120 self.dma_cycles_owed = 0;
1121 self.dma_idx = 0;
1122 self.dma_halt_addr = 0;
1123 self.last_nmi_level = false;
1124 self.nmi_edge_latch = false;
1125 self.open_bus = 0;
1126 self.internal_data_bus = 0;
1127 self.deferred_dma_replay_addr = 0;
1128 self.m2_phase = M2Phase::Low;
1129 self.irq_snapshot_mapper_at_low = false;
1130 self.irq_snapshot_apu_at_low = false;
1131 self.irq_snapshot_mapper_at_high = false;
1132 self.irq_snapshot_apu_at_high = false;
1133 self.unified_dma_clear();
1134 // A cold boot must reset EVERY run-history-dependent field, or the
1135 // post-power-cycle machine depends on how long it ran before — breaking
1136 // the `power_cycle == fresh boot` equivalence (netplay power-cycles
1137 // both peers at session start and requires byte-identical state). A
1138 // residual `ppu_clock` in particular carries the old master-clock
1139 // CPU/PPU phase into the "new" boot, diverging timing-sensitive games
1140 // from frame 0. Mirrors the `with_sample_rate` initial values.
1141 self.ppu_clock = 0;
1142 self.dma_byte = 0;
1143 self.dma_page = 0;
1144 self.dma_total = 0;
1145 self.dma_mc_consumed = 0;
1146 self.last_read_addr = 0;
1147 self.in_dmc_dma = false;
1148 self.dmc_step_was_get = false;
1149 self.dmc_halt = false;
1150 self.controller_write_pending = 0;
1151 self.controller_write_value = 0;
1152 // Rebuild the mapper to its power-on state (fresh bank registers, cleared
1153 // CHR-RAM + volatile PRG-RAM), so a power-cycle is a true cold boot for
1154 // mapper-stateful games (MMC1/MMC3/…) too — without this, a stateful
1155 // mapper's banking + CHR-RAM survive, so two netplay peers that power-
1156 // cycled from different running states would desync. The existing `cart`
1157 // metadata (incl. any post-load `set_vs_ppu_type` override) is kept; only
1158 // the mapper is replaced. FDS (`rom_bytes == None`) keeps its mapper.
1159 // This also clears battery PRG-RAM (a battery-pull); RustyNES does not
1160 // persist standard battery saves to disk, so nothing on-disk is lost.
1161 if let Some(bytes) = self.rom_bytes.take() {
1162 if let Ok((_cart, mapper)) = rustynes_mappers::parse(&bytes) {
1163 self.mapper = mapper;
1164 // v2.8.0 Phase 4 — re-cache the capability flags for the
1165 // fresh mapper instance (same type, same flags, but keep
1166 // the invariant mechanical).
1167 self.mapper_caps = self.mapper.caps();
1168 }
1169 self.rom_bytes = Some(bytes);
1170 }
1171 }
1172
1173 /// Developer-mode power-on randomization (Phase 7 / T-72-005).
1174 ///
1175 /// Fills the 2 KiB CPU work RAM and the external open-bus latch from a
1176 /// deterministic `xorshift64` PRNG. Real hardware powers up with
1177 /// unreliable RAM (see nesdev "CPU power up state"); games that depend on
1178 /// a particular post-power-on RAM pattern are buggy, and this option
1179 /// surfaces such bugs the way Mesen2's "randomize RAM on power-on" does.
1180 ///
1181 /// The fill is **seeded and deterministic** — the same `seed` always
1182 /// yields the same power-on state, so the
1183 /// `same seed + ROM + input ⇒ bit-identical` determinism contract (and
1184 /// therefore save-state round-trip and the regression oracle) is
1185 /// preserved. CI and tests use the default (zeroed) path; this is opt-in
1186 /// via [`crate::Nes::from_rom_with_power_on_seed`].
1187 ///
1188 /// CPU/PPU phase alignment and DMA get/put phase are intentionally **not**
1189 /// randomized here: the lockstep scheduler's phase is deterministic by
1190 /// design and randomizing it is entangled with the v2.0 master-clock
1191 /// scheduling refactor (see `docs/audit/phase-7-assessment-2026-05-24.md`).
1192 pub fn randomize_power_on_ram(&mut self, seed: u64) {
1193 // Avoid the xorshift64 zero fixed point.
1194 let mut s = if seed == 0 {
1195 0x9E37_79B9_7F4A_7C15
1196 } else {
1197 seed
1198 };
1199 let mut next = || {
1200 s ^= s << 13;
1201 s ^= s >> 7;
1202 s ^= s << 17;
1203 // Byte 3 (bits 24-31) — extracted without a truncating cast.
1204 s.to_le_bytes()[3]
1205 };
1206 for byte in self.ram.iter_mut() {
1207 *byte = next();
1208 }
1209 self.open_bus = next();
1210 }
1211
1212 /// Assert or release the injected /NMI pin. See [`crate::Nes::inject_nmi`].
1213 #[cfg(feature = "cosim-interrupt-inject")]
1214 pub const fn set_inject_nmi(&mut self, asserted: bool) {
1215 self.inject_nmi = asserted;
1216 }
1217
1218 /// Assert or release the injected /IRQ pin. See [`crate::Nes::inject_irq`].
1219 #[cfg(feature = "cosim-interrupt-inject")]
1220 pub const fn set_inject_irq(&mut self, asserted: bool) {
1221 self.inject_irq = asserted;
1222 }
1223
1224 /// Borrow the framebuffer (RGBA8, 256x240).
1225 #[must_use]
1226 pub fn framebuffer(&self) -> &[u8] {
1227 self.ppu.framebuffer()
1228 }
1229
1230 /// v1.7.0 "Forge" Workstream B (B3) — overwrite the RGBA8 output framebuffer
1231 /// (the Lua `emu:setScreenBuffer`). Output-only; see
1232 /// [`rustynes_ppu::Ppu::debug_set_framebuffer`]. `debug-hooks`-gated.
1233 #[cfg(feature = "debug-hooks")]
1234 pub fn debug_set_framebuffer(&mut self, rgba: &[u8]) {
1235 self.ppu.debug_set_framebuffer(rgba);
1236 }
1237
1238 /// Borrow the parallel palette-index framebuffer (256x240 `u16`s) for the
1239 /// `NES_NTSC` composite filter. See [`rustynes_ppu::Ppu::index_framebuffer`].
1240 #[must_use]
1241 pub fn index_framebuffer(&self) -> &[u16] {
1242 self.ppu.index_framebuffer()
1243 }
1244
1245 /// v1.2.0 C3 (hd-pack) — borrow the per-pixel HD-pack tile-source buffer.
1246 /// See [`rustynes_ppu::Ppu::hd_tile_source`]. Output-only telemetry.
1247 #[cfg(feature = "hd-pack")]
1248 #[must_use]
1249 pub fn hd_tile_source(&self) -> &[rustynes_ppu::HdTileSource] {
1250 self.ppu.hd_tile_source()
1251 }
1252
1253 /// The per-frame NTSC composite colour phase for the `NES_NTSC` filter
1254 /// (`0..=2` on NTSC; frame parity `0..=1` on PAL/Dendy). See
1255 /// [`rustynes_ppu::Ppu::ntsc_phase`].
1256 #[must_use]
1257 pub const fn ntsc_phase(&self) -> u8 {
1258 self.ppu.ntsc_phase()
1259 }
1260
1261 /// v1.1.0 beta.1 — install (`Some`) or clear (`None`) a custom 64-entry base
1262 /// palette from a loaded `.pal` file. A presentation override; `None` (default)
1263 /// is byte-identical to the built-in palette.
1264 pub const fn set_custom_palette(&mut self, base: Option<[[u8; 3]; 64]>) {
1265 self.ppu.set_custom_palette(base);
1266 }
1267
1268 /// v1.7.0 "Forge" F3 — set the PPU extra-scanlines overclock (extra idle
1269 /// vblank lines per frame). `0` (default) is byte-identical to stock timing.
1270 pub const fn set_extra_scanlines(&mut self, lines: u16) {
1271 self.ppu.set_extra_scanlines(lines);
1272 }
1273
1274 /// v1.7.0 F3 — the configured extra-scanline count (`0` = stock).
1275 #[must_use]
1276 pub const fn extra_scanlines(&self) -> u16 {
1277 self.ppu.extra_scanlines()
1278 }
1279
1280 /// v2.1.8 A1 — enable/disable the specialized visible-scanline fast dot
1281 /// path. `false` (default) is byte-identical to a build without it. See
1282 /// [`rustynes_ppu::Ppu::set_fast_dotloop`].
1283 pub const fn set_fast_dotloop(&mut self, enabled: bool) {
1284 self.ppu.set_fast_dotloop(enabled);
1285 }
1286
1287 /// v2.1.8 A1 — whether the visible-scanline fast dot path is enabled.
1288 #[must_use]
1289 pub const fn fast_dotloop(&self) -> bool {
1290 self.ppu.fast_dotloop()
1291 }
1292
1293 /// v2.1.4 F2.3 — enable/disable the optional OAM-decay accuracy model.
1294 /// `false` (default) is byte-identical to a decay-free PPU. See
1295 /// [`rustynes_ppu::Ppu::set_oam_decay`].
1296 pub const fn set_oam_decay(&mut self, enabled: bool) {
1297 self.ppu.set_oam_decay(enabled);
1298 }
1299
1300 /// v2.1.7 P5 — select the emulated 2C02 die revision, storing it so a
1301 /// power-cycle re-applies it, and applying it to the live PPU now. The
1302 /// default revision is byte-identical. See [`PpuRevision`].
1303 pub const fn set_ppu_revision(&mut self, revision: PpuRevision) {
1304 self.ppu_die_revision = revision;
1305 self.ppu.set_revision(revision);
1306 }
1307
1308 /// v2.1.7 P5 — the currently-selected 2C02 die revision.
1309 #[must_use]
1310 pub const fn ppu_revision(&self) -> PpuRevision {
1311 self.ppu_die_revision
1312 }
1313
1314 /// v2.1.7 P5 — apply a power-up palette-RAM pattern, storing it so a
1315 /// power-cycle re-applies it and writing it to the live PPU's palette RAM
1316 /// now. The default ([`PaletteInit::Zeroed`]) is byte-identical. See
1317 /// [`PaletteInit`].
1318 pub const fn set_power_up_palette(&mut self, init: PaletteInit) {
1319 self.power_up_palette = init;
1320 self.ppu.apply_power_up_palette(init);
1321 }
1322
1323 /// v2.1.7 P5 — the currently-selected power-up palette pattern.
1324 #[must_use]
1325 pub const fn power_up_palette(&self) -> PaletteInit {
1326 self.power_up_palette
1327 }
1328
1329 /// v2.1.7 P5 — select the power-on work-RAM fill, storing it so a
1330 /// power-cycle re-applies it, and applying it to the current RAM now. The
1331 /// default ([`crate::nes::PowerOnRam::Zeroed`]) is byte-identical. See [`crate::nes::PowerOnRam`].
1332 pub fn set_power_on_ram(&mut self, ram: crate::nes::PowerOnRam) {
1333 self.power_on_ram = ram;
1334 self.apply_power_on_ram();
1335 }
1336
1337 /// v2.1.7 P5 — the currently-selected power-on work-RAM fill.
1338 #[must_use]
1339 pub const fn power_on_ram(&self) -> crate::nes::PowerOnRam {
1340 self.power_on_ram
1341 }
1342
1343 /// v2.1.7 P5 — apply the stored [`Self::power_on_ram`] selection to the 2 KiB
1344 /// work RAM (and the open-bus latch). Called by [`Self::set_power_on_ram`]
1345 /// and re-applied by [`Self::power_cycle`] after it zeroes RAM. RAM is not
1346 /// consulted during the reset sequence (only the `$FFFC/D` vector is), so
1347 /// applying it here is correct. Deterministic: no wall-clock / OS RNG.
1348 fn apply_power_on_ram(&mut self) {
1349 match self.power_on_ram {
1350 crate::nes::PowerOnRam::Zeroed => {
1351 self.ram.fill(0);
1352 self.open_bus = 0;
1353 }
1354 crate::nes::PowerOnRam::Seeded(seed) => self.randomize_power_on_ram(seed),
1355 crate::nes::PowerOnRam::Filled(byte) => {
1356 self.ram.fill(byte);
1357 self.open_bus = byte;
1358 }
1359 }
1360 }
1361
1362 /// v2.1.4 F2.3 — whether the optional OAM-decay model is enabled.
1363 #[must_use]
1364 pub const fn oam_decay_enabled(&self) -> bool {
1365 self.ppu.oam_decay_enabled()
1366 }
1367
1368 /// v2.1.7 — set the emulated 2A03 die revision (DMA "unexpected read" axis).
1369 /// [`Cpu2A03Revision::Rp2A03G`] (default) is byte-identical to the pre-v2.1.7
1370 /// core; [`Cpu2A03Revision::Rp2A03H`] is the opt-in later-die model. See the
1371 /// [`Cpu2A03Revision`] docs + ADR 0033.
1372 pub const fn set_cpu_2a03_revision(&mut self, revision: Cpu2A03Revision) {
1373 self.cpu_2a03_revision = revision;
1374 }
1375
1376 /// v2.1.7 — the configured 2A03 die revision (default
1377 /// [`Cpu2A03Revision::Rp2A03G`]).
1378 #[must_use]
1379 pub const fn cpu_2a03_revision(&self) -> Cpu2A03Revision {
1380 self.cpu_2a03_revision
1381 }
1382
1383 /// Cartridge region (NTSC / PAL / Dendy / Multi). Drives wall-clock
1384 /// frame pacing in the frontend and clock-divider selection inside the
1385 /// PPU + APU.
1386 #[must_use]
1387 pub const fn region(&self) -> rustynes_mappers::Region {
1388 self.cart.region
1389 }
1390
1391 /// Length in bytes of the loaded cartridge's PRG-ROM (read-only metadata).
1392 #[must_use]
1393 pub const fn prg_rom_len(&self) -> usize {
1394 self.cart.prg_rom.len()
1395 }
1396
1397 /// Length in bytes of the loaded cartridge's CHR-ROM (0 when the board uses
1398 /// CHR-RAM). Read-only metadata.
1399 #[must_use]
1400 pub const fn chr_rom_len(&self) -> usize {
1401 self.cart.chr_rom.len()
1402 }
1403
1404 /// Enable the per-CPU-cycle IRQ-timing trace fixture with the given
1405 /// record capacity. Records past the cap are silently dropped (see
1406 /// `IrqTrace::overflow`). See ADR-0002 "Decision (revised,
1407 /// 2026-05-13)" → "Test fixture" and
1408 /// `crates/rustynes-core/src/irq_trace.rs`.
1409 #[cfg(feature = "irq-timing-trace")]
1410 pub fn enable_irq_trace(&mut self, capacity: usize) {
1411 self.irq_trace = Some(IrqTrace::with_capacity(capacity));
1412 self.trace_a12_latest = None;
1413 self.trace_a12_scratch.clear();
1414 // Snapshot whatever A12 level the PPU last drove so the first
1415 // recorded transition matches reality (the PPU's `last_a12` is
1416 // private to its module; we accept "first cycle may miss a level
1417 // assignment" as a cold-start artifact, matching every existing
1418 // diagnostic probe).
1419 self.trace_last_a12 = false;
1420 // Session-21: reset bus-access tracker so the first traced cycle
1421 // reflects accurate (CPU-driven) state rather than a stale
1422 // pre-trace driver.
1423 self.trace_bus_access = BusAccess::Idle;
1424 self.trace_bus_addr = 0;
1425 self.trace_bus_data = 0;
1426 }
1427
1428 /// Take the accumulated IRQ trace, leaving the bus's trace slot empty.
1429 /// Returns `None` if tracing was never enabled.
1430 #[cfg(feature = "irq-timing-trace")]
1431 #[must_use]
1432 pub const fn take_irq_trace(&mut self) -> Option<IrqTrace> {
1433 self.irq_trace.take()
1434 }
1435
1436 /// Borrow the in-flight IRQ trace for inspection without taking it.
1437 #[cfg(feature = "irq-timing-trace")]
1438 #[must_use]
1439 pub const fn irq_trace(&self) -> Option<&IrqTrace> {
1440 self.irq_trace.as_ref()
1441 }
1442
1443 /// Direct CPU-bus probe (does **not** advance time). Intended for
1444 /// blargg-style status polls at `$6000-$7FFF` and the test harness's
1445 /// mapper-resident WRAM peek. Note that this still has side effects on
1446 /// PPU registers (`$2002` clears VBL and toggle, `$2007` reads advance
1447 /// the buffer); callers should avoid touching `$2000-$3FFF` via peek.
1448 pub fn peek_cpu(&mut self, addr: u16) -> u8 {
1449 self.raw_cpu_read(addr)
1450 }
1451
1452 /// Add a Game Genie code (6 or 8 characters, case-insensitive). The code
1453 /// patches a PRG address (`$8000-$FFFF`) on the CPU read path; adding a
1454 /// code at an address that already has one replaces it.
1455 ///
1456 /// # Errors
1457 ///
1458 /// Returns [`GenieError`] if the code string cannot be decoded.
1459 pub fn add_genie_code(&mut self, code: &str) -> Result<(), GenieError> {
1460 let gc = GenieCode::new(code)?;
1461 self.genie_codes.insert(gc.addr(), gc);
1462 Ok(())
1463 }
1464
1465 /// Remove the active Game Genie code whose canonical (upper-case) string
1466 /// matches `code`. No-op if no such code is active.
1467 pub fn remove_genie_code(&mut self, code: &str) {
1468 let want = code.to_ascii_uppercase();
1469 self.genie_codes.retain(|_, gc| gc.code() != want.as_str());
1470 }
1471
1472 /// Remove all active Game Genie codes.
1473 pub fn clear_genie_codes(&mut self) {
1474 self.genie_codes.clear();
1475 }
1476
1477 /// Iterate the active Game Genie codes (address-sorted).
1478 pub fn genie_codes(&self) -> impl Iterator<Item = &GenieCode> {
1479 self.genie_codes.values()
1480 }
1481
1482 /// Apply any active Game Genie code at `addr` to a freshly-read byte.
1483 /// Fast-paths (single branch) when no codes are active.
1484 fn apply_genie(&self, addr: u16, original: u8) -> u8 {
1485 if self.genie_codes.is_empty() {
1486 return original;
1487 }
1488 self.genie_codes
1489 .get(&addr)
1490 .map_or(original, |gc| gc.read(original))
1491 }
1492
1493 /// Side-effect-free CPU bus sample for the debugger hex viewer.
1494 ///
1495 /// Returns the bus's view of the byte at `addr` without the side
1496 /// effects `peek_cpu` / `raw_cpu_read` carry on PPU register space
1497 /// (no VBL clear, no PPUDATA buffer advance, no open-bus update). For
1498 /// PPU registers we read back the cached snapshot; for mappers we go
1499 /// through `cpu_read` — the overwhelming majority of mappers are
1500 /// idempotent on `$8000-$FFFF` reads, and the few that latch on read
1501 /// (MMC2 in particular) document that behavior as inherent.
1502 ///
1503 /// Takes `&mut self` because mapper `cpu_read` is `&mut` — but no
1504 /// emulator-visible state advances. The CPU cycle counter, PPU
1505 /// scheduler, and APU all stay put.
1506 pub fn debug_peek_cpu(&mut self, addr: u16) -> u8 {
1507 match addr {
1508 0x0000..=0x1FFF => self.ram[(addr & 0x07FF) as usize],
1509 0x2000..=0x3FFF => {
1510 let reg = (addr & 7) as u8;
1511 let regs = self.ppu.debug_registers();
1512 match reg {
1513 0 => regs[0],
1514 1 => regs[1],
1515 2 => regs[2],
1516 3 => regs[3],
1517 _ => 0,
1518 }
1519 }
1520 0x4015 => {
1521 let mut v = 0u8;
1522 if self.apu.pulse1_out() != 0 {
1523 v |= 0x01;
1524 }
1525 if self.apu.pulse2_out() != 0 {
1526 v |= 0x02;
1527 }
1528 if self.apu.triangle_out() != 0 {
1529 v |= 0x04;
1530 }
1531 if self.apu.noise_out() != 0 {
1532 v |= 0x08;
1533 }
1534 if self.apu.frame_irq_pending() {
1535 v |= 0x40;
1536 }
1537 if self.apu.dmc_irq_pending() {
1538 v |= 0x80;
1539 }
1540 v
1541 }
1542 0x4016 => 0x40 | self.peek_port(0) | (u8::from(self.famicom_mic) << 2),
1543 0x4017 => 0x40 | self.peek_port(1),
1544 0x4000..=0x4014 | 0x4018..=0x401F => self.open_bus,
1545 0x4020..=0xFFFF => {
1546 // Mirror the production read path so the debugger hex viewer
1547 // shows the Game-Genie-substituted byte the CPU would see.
1548 let raw = self.mapper.cpu_read(addr);
1549 self.apply_genie(addr, raw)
1550 }
1551 }
1552 }
1553
1554 /// Side-effect-free PPU bus sample (`$0000-$3FFF`).
1555 ///
1556 /// `$0000-$1FFF` -> mapper CHR, `$2000-$3EFF` -> nametable
1557 /// (via mapper's mirroring), `$3F00-$3FFF` -> palette RAM.
1558 pub fn debug_peek_ppu(&mut self, addr: u16) -> u8 {
1559 let addr = addr & 0x3FFF;
1560 match addr {
1561 0x0000..=0x1FFF => self.mapper.ppu_read(addr),
1562 0x2000..=0x3EFF => {
1563 if let Some(v) = self.mapper.nametable_fetch(addr) {
1564 v
1565 } else {
1566 let phys = match self.nt_mirroring_override {
1567 Some(m) => override_nt_addr(m, addr) as usize,
1568 None => self.mapper.nametable_address(addr) as usize,
1569 };
1570 let ciram = self.ppu.ciram();
1571 ciram[phys % ciram.len()]
1572 }
1573 }
1574 0x3F00..=0x3FFF => {
1575 let idx = (addr & 0x1F) as usize;
1576 let palette = self.ppu.palette_ram();
1577 // Mirror sprite-palette zero into BG-palette zero.
1578 let idx = if idx & 0x13 == 0x10 { idx & 0x0F } else { idx };
1579 palette[idx]
1580 }
1581 _ => 0,
1582 }
1583 }
1584
1585 /// v1.7.0 "Forge" Workstream A1 — debugger writeback. The structural mirror
1586 /// of [`Self::debug_peek_ppu`]: `$0000-$1FFF` → mapper CHR (`ppu_write`,
1587 /// a no-op on CHR-ROM), `$2000-$3EFF` → nametable (mapper-absorbed, else
1588 /// CIRAM via the active mirroring), `$3F00-$3FFF` → palette RAM.
1589 ///
1590 /// Side-effect-free w.r.t. the run loop: it is reached *only* through the
1591 /// gated post-frame poke path (the same caller-side, after-`run_frame` stage
1592 /// the raw RAM cheats use), so the deterministic core run loop is unchanged
1593 /// and the no-edit path is byte-identical. `debug-hooks`-gated.
1594 #[cfg(feature = "debug-hooks")]
1595 pub fn debug_poke_ppu(&mut self, addr: u16, value: u8) {
1596 let addr = addr & 0x3FFF;
1597 match addr {
1598 0x0000..=0x1FFF => self.mapper.ppu_write(addr & 0x1FFF, value),
1599 0x2000..=0x3EFF => {
1600 let nt_addr = if addr >= 0x3000 { addr - 0x1000 } else { addr };
1601 // Give the mapper a chance to absorb the write (ExRAM
1602 // nametables, fill-mode drops), exactly like `write_vram`.
1603 if !self.mapper.nametable_write(nt_addr, value) {
1604 let phys = match self.nt_mirroring_override {
1605 Some(m) => override_nt_addr(m, nt_addr) as usize,
1606 None => self.mapper.nametable_address(nt_addr) as usize,
1607 };
1608 self.ppu.debug_poke_ciram(phys, value);
1609 }
1610 }
1611 0x3F00..=0x3FFF => self.ppu.debug_poke_palette((addr & 0x1F) as u8, value),
1612 _ => {}
1613 }
1614 }
1615
1616 /// v1.7.0 "Forge" Workstream A1 — debugger writeback for one OAM byte
1617 /// (`idx` = 0..256). `debug-hooks`-gated; reached only through the gated
1618 /// post-frame poke path, so the default build is byte-identical.
1619 #[cfg(feature = "debug-hooks")]
1620 pub const fn debug_poke_oam(&mut self, idx: u8, value: u8) {
1621 self.ppu.debug_poke_oam(idx, value);
1622 }
1623
1624 /// Borrow the PPU (debugger / tests).
1625 #[must_use]
1626 pub const fn ppu(&self) -> &Ppu {
1627 &self.ppu
1628 }
1629
1630 /// Mutably borrow the PPU (debugger / tests).
1631 pub const fn ppu_mut(&mut self) -> &mut Ppu {
1632 &mut self.ppu
1633 }
1634
1635 /// Borrow the APU (debugger / tests).
1636 #[must_use]
1637 pub const fn apu(&self) -> &Apu {
1638 &self.apu
1639 }
1640
1641 /// Mutably borrow the APU (debugger / tests).
1642 pub const fn apu_mut(&mut self) -> &mut Apu {
1643 &mut self.apu
1644 }
1645
1646 /// Set the buttons currently held on player `port`. Ports 0/1 are the
1647 /// standard `$4016`/`$4017` controllers; ports 2/3 are players 3/4 on the
1648 /// Four Score adapter (only polled when [`Self::set_four_score`] is on).
1649 /// The change takes effect on the next strobe edge.
1650 ///
1651 /// # Panics
1652 ///
1653 /// Panics if `port` is not in `0..=3`.
1654 pub const fn set_buttons(&mut self, port: usize, buttons: Buttons) {
1655 assert!(
1656 port < 4,
1657 "controller port must be 0..=3 (2/3 are the Four Score)"
1658 );
1659 match port {
1660 0 | 1 => self.controllers[port].set_buttons(buttons),
1661 _ => self.controllers34[port - 2].set_buttons(buttons),
1662 }
1663 }
1664
1665 /// Enable the **beam-relative** Zapper light model.
1666 ///
1667 /// **Default ON since v2.3.6.** The light bit is derived from where the CRT
1668 /// beam is at the moment of the `$4016`/`$4017` read: dark before the beam
1669 /// paints the aim row, lit while the photodiode holds (~19-26 scanlines,
1670 /// per the `NESdev` wiki's capacitor model), dark once it drains. That is what
1671 /// real hardware does, and the frame-granular model structurally cannot
1672 /// express it — it returns one answer for the whole frame, sampled at
1673 /// end-of-frame, so every read during frame N reports frame N-1.
1674 ///
1675 /// # Why it was promoted (v2.3.6)
1676 ///
1677 /// A3 (v2.2.3) shipped this off, on the reasoning that "there is no pass/fail
1678 /// light-gun test ROM… the supported titles re-poll every frame and are
1679 /// satisfied by either model". **The second half of that was false**, and no
1680 /// test ROM was needed to show it — the game itself is the oracle.
1681 ///
1682 /// *Duck Hunt* requires the gun to see **nothing for one frame** and then a
1683 /// bright spot in the next. Under the frame model it received exactly the
1684 /// inverse: on the blanked frame it read the previous (bright) frame's
1685 /// answer, and on the target frame it read the blanked frame's. The shot was
1686 /// discarded before hit-testing, so the gun fired and **nothing could ever be
1687 /// hit** — reported by the maintainer, then reproduced headlessly from the
1688 /// game's own `$4017` traffic (`zapper_light_probe`).
1689 ///
1690 /// Measured A/B on the same ROM, aim and inputs: frame model → score 000000,
1691 /// duck still flying; beam-relative → score 000500, duck marked hit.
1692 ///
1693 /// Turning it off restores the pre-v2.3.6 frame-granular behaviour.
1694 /// Deterministic either way: the answer is a pure function of framebuffer +
1695 /// aim + scanline and holds no state, so it adds nothing to serialize.
1696 pub const fn set_zapper_temporal_light(&mut self, on: bool) {
1697 self.zapper_temporal_light = on;
1698 }
1699
1700 /// Whether the beam-relative Zapper light model is enabled (A3).
1701 #[must_use]
1702 pub const fn zapper_temporal_light(&self) -> bool {
1703 self.zapper_temporal_light
1704 }
1705
1706 /// Attach (or replace) a non-standard overlay device on `port` (0 =
1707 /// `$4016`, 1 = `$4017`). Pass `None` to unplug the device and return the
1708 /// port to the standard controller / Four Score path (byte-identical).
1709 ///
1710 /// # Panics
1711 ///
1712 /// Panics if `port` is not in `0..=1`.
1713 pub fn set_expansion_device(
1714 &mut self,
1715 port: usize,
1716 device: Option<crate::input_device::InputDevice>,
1717 ) {
1718 assert!(port < 2, "expansion-device port must be 0..=1");
1719 self.expansion_device[port] = device;
1720 }
1721
1722 /// Borrow the overlay device attached to `port`, if any.
1723 ///
1724 /// # Panics
1725 ///
1726 /// Panics if `port` is not in `0..=1`.
1727 #[must_use]
1728 pub const fn expansion_device(&self, port: usize) -> &Option<crate::input_device::InputDevice> {
1729 assert!(port < 2, "expansion-device port must be 0..=1");
1730 &self.expansion_device[port]
1731 }
1732
1733 /// Update an attached Vaus paddle's position + fire state on `port`. No-op
1734 /// if the attached device is not a Vaus (or no device is attached).
1735 ///
1736 /// # Panics
1737 ///
1738 /// Panics if `port` is not in `0..=1`.
1739 pub const fn set_paddle(&mut self, port: usize, position: u8, fire: bool) {
1740 assert!(port < 2, "paddle port must be 0..=1");
1741 if let Some(crate::input_device::InputDevice::Vaus(v)) = &mut self.expansion_device[port] {
1742 v.set(position, fire);
1743 }
1744 }
1745
1746 /// Update an attached Zapper's aim point + trigger on `port`. No-op if the
1747 /// attached device is not a Zapper (or no device is attached).
1748 ///
1749 /// # Panics
1750 ///
1751 /// Panics if `port` is not in `0..=1`.
1752 pub const fn set_zapper(&mut self, port: usize, x: u16, y: u16, trigger: bool) {
1753 assert!(port < 2, "zapper port must be 0..=1");
1754 if let Some(crate::input_device::InputDevice::Zapper(z)) = &mut self.expansion_device[port]
1755 {
1756 z.set(x, y, trigger);
1757 }
1758 }
1759
1760 /// Drive the Famicom built-in microphone signal (read on `$4016` bit 2).
1761 ///
1762 /// `pressed` = the frontend's quantized "mic is loud" verdict. Additive:
1763 /// leaving it `false` (the default) keeps the `$4016` read byte-identical.
1764 pub const fn set_microphone(&mut self, pressed: bool) {
1765 self.famicom_mic = pressed;
1766 }
1767
1768 /// Whether the Famicom microphone signal is currently asserted.
1769 #[must_use]
1770 pub const fn microphone(&self) -> bool {
1771 self.famicom_mic
1772 }
1773
1774 /// Update an attached Power Pad's live button mask (bit `i` = mat button
1775 /// `i+1`) on `port`. No-op if the attached device is not a Power Pad.
1776 ///
1777 /// # Panics
1778 ///
1779 /// Panics if `port` is not in `0..=1`.
1780 pub const fn set_power_pad(&mut self, port: usize, buttons: u16) {
1781 assert!(port < 2, "power pad port must be 0..=1");
1782 if let Some(crate::input_device::InputDevice::PowerPad(p)) =
1783 &mut self.expansion_device[port]
1784 {
1785 p.set(buttons);
1786 }
1787 }
1788
1789 /// Update an attached SNES mouse's movement + buttons + sensitivity on
1790 /// `port`. No-op if the attached device is not a mouse.
1791 ///
1792 /// # Panics
1793 ///
1794 /// Panics if `port` is not in `0..=1`.
1795 pub const fn set_snes_mouse(
1796 &mut self,
1797 port: usize,
1798 dx: i16,
1799 dy: i16,
1800 left: bool,
1801 right: bool,
1802 sensitivity: u8,
1803 ) {
1804 assert!(port < 2, "mouse port must be 0..=1");
1805 if let Some(crate::input_device::InputDevice::SnesMouse(m)) =
1806 &mut self.expansion_device[port]
1807 {
1808 m.set(dx, dy, left, right, sensitivity);
1809 }
1810 }
1811
1812 /// Update an attached Family BASIC keyboard's pressed-key bitmap on `port`
1813 /// (one byte per matrix row). No-op if the attached device is not a keyboard.
1814 ///
1815 /// # Panics
1816 ///
1817 /// Panics if `port` is not in `0..=1`.
1818 pub const fn set_family_keyboard(&mut self, port: usize, keys: [u8; 9]) {
1819 assert!(port < 2, "keyboard port must be 0..=1");
1820 if let Some(crate::input_device::InputDevice::FamilyKeyboard(k)) =
1821 &mut self.expansion_device[port]
1822 {
1823 k.set_keys(keys);
1824 }
1825 }
1826
1827 /// v1.3.0 Workstream F1 — update an attached Family Trainer mat's 12-button
1828 /// mask on `port`. No-op if the attached device is not a Family Trainer.
1829 ///
1830 /// # Panics
1831 ///
1832 /// Panics if `port` is not in `0..=1`.
1833 pub const fn set_family_trainer(&mut self, port: usize, buttons: u16) {
1834 assert!(port < 2, "family trainer port must be 0..=1");
1835 if let Some(crate::input_device::InputDevice::FamilyTrainer(p)) =
1836 &mut self.expansion_device[port]
1837 {
1838 p.set(buttons);
1839 }
1840 }
1841
1842 /// v1.3.0 Workstream F1 — update an attached Subor keyboard's pressed-key
1843 /// bitmap on `port`. No-op if the attached device is not a Subor keyboard.
1844 ///
1845 /// # Panics
1846 ///
1847 /// Panics if `port` is not in `0..=1`.
1848 pub const fn set_subor_keyboard(&mut self, port: usize, keys: [u8; 9]) {
1849 assert!(port < 2, "subor keyboard port must be 0..=1");
1850 if let Some(crate::input_device::InputDevice::SuborKeyboard(k)) =
1851 &mut self.expansion_device[port]
1852 {
1853 k.set_keys(keys);
1854 }
1855 }
1856
1857 /// v1.3.0 Workstream F1 — update an attached Konami Hyper Shot's 4-button
1858 /// mask on `port`. No-op if the attached device is not a Konami Hyper Shot.
1859 ///
1860 /// # Panics
1861 ///
1862 /// Panics if `port` is not in `0..=1`.
1863 pub const fn set_konami_hyper_shot(&mut self, port: usize, buttons: u8) {
1864 assert!(port < 2, "konami hyper shot port must be 0..=1");
1865 if let Some(crate::input_device::InputDevice::KonamiHyperShot(h)) =
1866 &mut self.expansion_device[port]
1867 {
1868 h.set(buttons);
1869 }
1870 }
1871
1872 /// v1.3.0 Workstream F1 — update an attached Bandai Hyper Shot's 8-sensor
1873 /// mask on `port`. No-op if the attached device is not a Bandai Hyper Shot.
1874 ///
1875 /// # Panics
1876 ///
1877 /// Panics if `port` is not in `0..=1`.
1878 pub const fn set_bandai_hyper_shot(&mut self, port: usize, sensors: u8) {
1879 assert!(port < 2, "bandai hyper shot port must be 0..=1");
1880 if let Some(crate::input_device::InputDevice::BandaiHyperShot(b)) =
1881 &mut self.expansion_device[port]
1882 {
1883 b.set(sensors);
1884 }
1885 }
1886
1887 /// v1.1.0 beta.1 (T-110-B4) — set (`Some`) or clear (`None`) the per-game
1888 /// nametable mirroring override. A frontend load-time correction; `None`
1889 /// (default) defers to the mapper (byte-identical).
1890 pub const fn set_mirroring_override(&mut self, m: Option<rustynes_mappers::Mirroring>) {
1891 self.nt_mirroring_override = m;
1892 }
1893
1894 /// The current per-game mirroring override (for the save-state).
1895 #[must_use]
1896 pub const fn mirroring_override(&self) -> Option<rustynes_mappers::Mirroring> {
1897 self.nt_mirroring_override
1898 }
1899
1900 /// Whether the loaded mapper hardwires its nametable mirroring (so an
1901 /// external mirroring correction is safe to honor). See
1902 /// [`rustynes_mappers::Mapper::has_hardwired_mirroring`].
1903 #[must_use]
1904 pub fn mapper_has_hardwired_mirroring(&self) -> bool {
1905 self.mapper.has_hardwired_mirroring()
1906 }
1907
1908 /// v1.1.0 beta.2 (T-110-C3) — start/stop event-viewer recording.
1909 #[cfg(feature = "debug-hooks")]
1910 pub const fn set_event_logging(&mut self, enabled: bool) {
1911 self.event_logging = enabled;
1912 }
1913
1914 /// Whether event-viewer recording is on.
1915 #[cfg(feature = "debug-hooks")]
1916 #[must_use]
1917 pub const fn event_logging(&self) -> bool {
1918 self.event_logging
1919 }
1920
1921 /// The events captured so far this frame.
1922 #[cfg(feature = "debug-hooks")]
1923 #[must_use]
1924 #[allow(clippy::missing_const_for_fn)] // Vec->slice deref is not const.
1925 pub fn events(&self) -> &[EventRec] {
1926 &self.events
1927 }
1928
1929 /// v1.1.0 beta.3 (T-110-E2) — start/stop the Lua bus-access log.
1930 #[cfg(feature = "debug-hooks")]
1931 pub const fn set_access_logging(&mut self, enabled: bool) {
1932 self.access_logging = enabled;
1933 }
1934
1935 /// Whether the bus-access log is recording.
1936 #[cfg(feature = "debug-hooks")]
1937 #[must_use]
1938 pub const fn access_logging(&self) -> bool {
1939 self.access_logging
1940 }
1941
1942 /// The CPU bus accesses captured so far this frame.
1943 #[cfg(feature = "debug-hooks")]
1944 #[must_use]
1945 #[allow(clippy::missing_const_for_fn)] // Vec->slice deref is not const.
1946 pub fn accesses(&self) -> &[AccessRec] {
1947 &self.accesses
1948 }
1949
1950 /// Clear the bus-access log (called per frame by the run loop).
1951 #[cfg(feature = "debug-hooks")]
1952 pub fn clear_accesses(&mut self) {
1953 self.accesses.clear();
1954 }
1955
1956 /// v1.2.0 (T-110-E1) — start/stop the Lua interrupt-service log.
1957 #[cfg(feature = "debug-hooks")]
1958 pub const fn set_interrupt_logging(&mut self, enabled: bool) {
1959 self.interrupt_logging = enabled;
1960 }
1961
1962 /// Whether the interrupt-service log is recording.
1963 #[cfg(feature = "debug-hooks")]
1964 #[must_use]
1965 pub const fn interrupt_logging(&self) -> bool {
1966 self.interrupt_logging
1967 }
1968
1969 /// The interrupt-service entries captured so far this frame.
1970 #[cfg(feature = "debug-hooks")]
1971 #[must_use]
1972 #[allow(clippy::missing_const_for_fn)] // Vec->slice deref is not const.
1973 pub fn interrupts(&self) -> &[InterruptRec] {
1974 &self.interrupts
1975 }
1976
1977 /// Clear the interrupt-service log (called per frame by the run loop).
1978 #[cfg(feature = "debug-hooks")]
1979 pub fn clear_interrupts(&mut self) {
1980 self.interrupts.clear();
1981 }
1982
1983 /// v1.4.0 Workstream D (D2) — set the armed event-breakpoint category mask
1984 /// (a bit-OR of [`EventBpKind::bit`]). `0` disarms all (the default + the
1985 /// per-cycle-cheap path).
1986 #[cfg(feature = "debug-hooks")]
1987 pub const fn set_event_breakpoints(&mut self, mask: u16) {
1988 self.event_bp_mask = mask;
1989 }
1990
1991 /// The armed event-breakpoint category mask.
1992 #[cfg(feature = "debug-hooks")]
1993 #[must_use]
1994 pub const fn event_breakpoints(&self) -> u16 {
1995 self.event_bp_mask
1996 }
1997
1998 /// Take the first event-breakpoint hit of the current frame (cleared on
1999 /// read). The frontend polls this after `run_frame`.
2000 #[cfg(feature = "debug-hooks")]
2001 pub const fn take_event_break_hit(&mut self) -> Option<EventBreakHit> {
2002 self.event_break_hit.take()
2003 }
2004
2005 /// Clear any recorded event-breakpoint hit (called per frame by the run
2006 /// loop so each frame starts fresh).
2007 #[cfg(feature = "debug-hooks")]
2008 pub const fn clear_event_break_hit(&mut self) {
2009 self.event_break_hit = None;
2010 }
2011
2012 /// v1.4.0 Workstream D (D2) — observational event-breakpoint tap. If `kind`
2013 /// is armed and no hit has been recorded yet this frame, latch the event
2014 /// with its full timing context. Pure observation — no emulator-visible
2015 /// state changes, so determinism holds. The `mask == 0` fast path keeps the
2016 /// default (no armed categories) cheap.
2017 #[cfg(feature = "debug-hooks")]
2018 const fn record_event_break(&mut self, kind: EventBpKind, addr: u16) {
2019 if self.event_bp_mask & kind.bit() == 0 || self.event_break_hit.is_some() {
2020 return;
2021 }
2022 self.event_break_hit = Some(EventBreakHit {
2023 kind,
2024 addr,
2025 frame: self.ppu.frame(),
2026 cycle: self.cycle,
2027 scanline: self.ppu.scanline(),
2028 dot: self.ppu.dot(),
2029 });
2030 }
2031
2032 /// Clear the event log (called at each frame start while recording).
2033 #[cfg(feature = "debug-hooks")]
2034 pub fn clear_events(&mut self) {
2035 self.events.clear();
2036 }
2037
2038 /// Clear the per-frame `TAStudio` lag-log "controller polled" flag (called at
2039 /// the top of each [`crate::Nes::run_frame`]). `debug-hooks`-gated;
2040 /// output-only, so the shipped build is byte-identical.
2041 #[cfg(feature = "debug-hooks")]
2042 pub(crate) const fn clear_controller_polled(&mut self) {
2043 self.controller_polled = false;
2044 }
2045
2046 /// `true` if a controller port (`$4016`/`$4017`) was read since the last
2047 /// [`Self::clear_controller_polled`] — i.e. during the current frame.
2048 #[cfg(feature = "debug-hooks")]
2049 #[must_use]
2050 pub(crate) const fn controller_polled(&self) -> bool {
2051 self.controller_polled
2052 }
2053
2054 /// Sample the framebuffer luminance at each attached Zapper's aim point.
2055 /// Called once per frame (only does work when a Zapper is attached, so the
2056 /// no-device path is byte-identical).
2057 pub fn sample_zapper_light(&mut self) {
2058 let has_zapper = self
2059 .expansion_device
2060 .iter()
2061 .any(|d| matches!(d, Some(crate::input_device::InputDevice::Zapper(_))));
2062 if !has_zapper {
2063 return;
2064 }
2065 // Borrow the framebuffer once; copy the per-port aim sample.
2066 for port in 0..2 {
2067 if let Some(crate::input_device::InputDevice::Zapper(_)) = &self.expansion_device[port]
2068 {
2069 // Take the device out to avoid the &mut self / &self.ppu borrow
2070 // conflict, sample, then put it back.
2071 let mut dev = self.expansion_device[port].take();
2072 if let Some(crate::input_device::InputDevice::Zapper(z)) = &mut dev {
2073 z.sample_light(self.ppu.framebuffer());
2074 }
2075 self.expansion_device[port] = dev;
2076 }
2077 }
2078 }
2079
2080 /// Borrow controller `port` (0/1 = `$4016`/`$4017`; 2/3 = Four Score
2081 /// players 3/4).
2082 ///
2083 /// # Panics
2084 ///
2085 /// Panics if `port` is not in `0..=3`.
2086 #[must_use]
2087 pub const fn controller(&self, port: usize) -> &Controller {
2088 match port {
2089 0 | 1 => &self.controllers[port],
2090 _ => &self.controllers34[port - 2],
2091 }
2092 }
2093
2094 /// Has the PPU completed a frame? Drains the latch.
2095 pub const fn take_frame_complete(&mut self) -> bool {
2096 self.ppu.take_frame_complete()
2097 }
2098
2099 /// Drain finalized audio samples (host sample rate, normalized `[0, ~1]`).
2100 pub fn drain_audio(&mut self) -> Vec<f32> {
2101 self.apu.drain_audio()
2102 }
2103
2104 /// Drain into a slice.
2105 pub fn drain_audio_into(&mut self, out: &mut [f32]) -> usize {
2106 self.apu.drain_audio_into(out)
2107 }
2108
2109 /// Cumulative CPU cycle count.
2110 #[must_use]
2111 pub const fn cycle(&self) -> u64 {
2112 self.cycle
2113 }
2114
2115 /// Returns the M2 phase the lockstep scheduler is currently in.
2116 ///
2117 /// The scheduler ticks the PPU 3 dots per CPU cycle. Convention:
2118 /// [`M2Phase::Low`] is the FIRST half of the cycle (the cycle's
2119 /// pre-sub-dot-1 portion, corresponding to silicon's `φ1`);
2120 /// [`M2Phase::High`] is the SECOND half (post-sub-dot-1, silicon's
2121 /// `φ2`). The boundary is the M2-rising edge between sub-dot 1 and
2122 /// sub-dot 2.
2123 ///
2124 /// At the start of `tick_one_cpu_cycle` the bus is in [`M2Phase::Low`].
2125 /// After sub-dot 1 of the 3-PPU-dot tick loop, it transitions to
2126 /// [`M2Phase::High`]. After the cycle's last sub-dot the bus
2127 /// advances the cycle counter and returns to [`M2Phase::Low`] for
2128 /// the next cycle.
2129 ///
2130 /// This accessor is informational. As of Phase B2 the bus stores
2131 /// per-phase IRQ snapshots (read by [`Bus::poll_irq_at_phase`])
2132 /// independently of this accessor — `current_m2_phase()` itself is
2133 /// not consulted by the CPU's IRQ sample path.
2134 #[must_use]
2135 pub const fn current_m2_phase(&self) -> M2Phase {
2136 self.m2_phase
2137 }
2138
2139 /// Set the Vs. System 8-bit DIP switch bank (switch 1 = bit 0 ..
2140 /// switch 8 = bit 7). No effect on non-Vs. carts. Default 0.
2141 pub const fn set_vs_dip(&mut self, dip: u8) {
2142 self.vs_dip = dip;
2143 }
2144
2145 /// Current Vs. System DIP switch bank.
2146 #[must_use]
2147 pub const fn vs_dip(&self) -> u8 {
2148 self.vs_dip
2149 }
2150
2151 /// Push the cartridge's current [`rustynes_mappers::VsPpuType`] into the PPU
2152 /// (output palette + 2C05 `$2000`/`$2001` swap + `$2002` identifier).
2153 ///
2154 /// Called from the constructor, [`Self::power_cycle`], and
2155 /// [`Self::set_vs_ppu_type`]. For [`rustynes_mappers::ConsoleType::Nes`] carts
2156 /// the resolved type is [`rustynes_mappers::VsPpuType::None`] -> `Composite2C02`,
2157 /// `is_2c05 = false`, so this is byte-for-byte a no-op on normal carts.
2158 const fn reapply_vs_palette(&mut self) {
2159 let vs = self.cart.vs_ppu_type;
2160 let palette = vs_palette_to_ppu(vs.ppu_palette());
2161 self.ppu
2162 .set_palette(palette, vs.is_2c05(), vs.ppu_2c05_id());
2163 }
2164
2165 /// Override the Vs. System PPU type and re-apply the output palette / 2C05
2166 /// quirks immediately.
2167 ///
2168 /// iNES-1.0 dumps carry no NES 2.0 byte-13, so the parser defaults a Vs.
2169 /// cart to [`rustynes_mappers::VsPpuType::Rp2C03`]; a per-game database (keyed on
2170 /// the ROM SHA-256) supplies the correct 2C04-000x / 2C05 type, which the
2171 /// frontend applies through this setter. No effect on the running game's
2172 /// logic — only the colour LUT the PPU emits through. No-op shape on
2173 /// non-Vs. carts (the default path never calls this).
2174 pub const fn set_vs_ppu_type(&mut self, t: rustynes_mappers::VsPpuType) {
2175 self.cart.vs_ppu_type = t;
2176 self.reapply_vs_palette();
2177 }
2178
2179 /// Latch a Vs. System coin insertion. `acceptor` 0 = acceptor #1 ($4016
2180 /// bit 5), 1 = acceptor #2 ($4016 bit 6); any other value is ignored. The
2181 /// frontend should clear the latch (see [`Self::clear_coin`]) after the
2182 /// real-hardware ~40-70 ms window (a few frames). No effect on non-Vs.
2183 /// carts.
2184 pub const fn insert_coin(&mut self, acceptor: u8) {
2185 match acceptor {
2186 0 => self.vs_coin |= 0x01,
2187 1 => self.vs_coin |= 0x02,
2188 _ => {}
2189 }
2190 }
2191
2192 /// Clear all latched Vs. System coin-insert signals.
2193 pub const fn clear_coin(&mut self) {
2194 self.vs_coin = 0;
2195 }
2196
2197 /// Set / clear the Vs. System service button ($4016 bit 2).
2198 pub const fn set_vs_service(&mut self, pressed: bool) {
2199 self.vs_service = pressed;
2200 }
2201
2202 /// v2.0.0 beta.5 (Vs. `DualSystem`): mark this console as the SUB half of a
2203 /// `DualSystem` pair (`$4016` reads return bit 7 = `0x80`). Wrapper-only.
2204 pub const fn set_vs_sub(&mut self, is_sub: bool) {
2205 self.vs_is_sub = is_sub;
2206 }
2207
2208 /// v2.0.0 beta.5 (Vs. `DualSystem`): drive this console's external `/IRQ`
2209 /// line (the partner console's `$4016` bit-1 signal, Mesen2
2210 /// `IRQSource::External`). Wrapper-only; OR'd into the IRQ level.
2211 pub const fn set_vs_external_irq(&mut self, asserted: bool) {
2212 self.vs_external_irq = asserted;
2213 }
2214
2215 /// v2.0.0 beta.5 (Vs. `DualSystem`): poll-and-clear the latched `$4016`
2216 /// bit-1 (main/sub comms signal) LEVEL. Returns `Some(level)` whenever
2217 /// this console wrote `$4016` since the last poll — deliberately
2218 /// level-driven, not edge-filtered (see the `vs_4016_bit1_dirty` field
2219 /// doc); the wrapper turns the level into the partner's external-IRQ
2220 /// assert (LOW asserts, HIGH clears). The shared-WRAM convergence
2221 /// (`pump_comms`'s separate `drain_vs_dual_wram_writes` step) runs on
2222 /// BOTH consoles every poll, independent of this bit-1 signal.
2223 pub const fn take_vs_mainsub_edge(&mut self) -> Option<bool> {
2224 if self.vs_4016_bit1_dirty {
2225 self.vs_4016_bit1_dirty = false;
2226 Some(self.vs_4016_bit1)
2227 } else {
2228 None
2229 }
2230 }
2231
2232 /// v2.0.0 beta.5 (Vs. `DualSystem`): provision the mapper-99 shared 2 KiB
2233 /// WRAM window (`$6000-$7FFF`). Wrapper-only; no-op on other boards.
2234 pub fn enable_vs_dual_wram(&mut self) {
2235 self.mapper.enable_vs_dual_wram();
2236 }
2237
2238 /// v2.0.0 beta.5 (Vs. `DualSystem`): mark the mapper as the SUB
2239 /// console's instance (banks the second PRG half + upper CHR pages —
2240 /// the two CPUs run different programs). Wrapper-only cabinet wiring.
2241 pub fn set_vs_dual_sub(&mut self) {
2242 self.mapper.set_vs_dual_sub();
2243 }
2244
2245 /// v2.0.0 beta.5 (Vs. `DualSystem`): drain this console's shared-WRAM
2246 /// write log for the wrapper to replay into the partner console (the
2247 /// fully-shared MAME model). Empty off-board. Allocates a fresh `Vec`
2248 /// each call — fine for diagnostics/tests, NOT used by the hot
2249 /// `pump_comms` path (see [`Self::drain_vs_dual_wram_writes`]).
2250 pub fn take_vs_dual_wram_writes(&mut self) -> alloc::vec::Vec<(u16, u8)> {
2251 self.mapper.take_vs_dual_wram_writes()
2252 }
2253
2254 /// v2.0.0 beta.5 (Vs. `DualSystem`): drain this console's shared-WRAM
2255 /// write log into a caller-owned, reusable `dst` buffer — the
2256 /// hot-path counterpart of [`Self::take_vs_dual_wram_writes`], used by
2257 /// `VsDualSystem::pump_comms` (called after every stepped instruction)
2258 /// to avoid allocating a fresh `Vec` on every call.
2259 pub fn drain_vs_dual_wram_writes(&mut self, dst: &mut alloc::vec::Vec<(u16, u8)>) {
2260 self.mapper.drain_vs_dual_wram_writes(dst);
2261 }
2262
2263 /// v2.0.0 beta.5 (Vs. `DualSystem`): replay one partner-console write
2264 /// into this console's shared-WRAM copy (no re-log).
2265 pub fn apply_vs_dual_wram_write(&mut self, offset: u16, value: u8) {
2266 self.mapper.apply_vs_dual_wram_write(offset, value);
2267 }
2268
2269 /// v2.0.0 beta.5 (Vs. `DualSystem`): take the shared-WRAM copy (wrapper
2270 /// snapshot-restore normalization). `None` off-board.
2271 pub fn take_vs_dual_wram(&mut self) -> Option<alloc::boxed::Box<[u8]>> {
2272 self.mapper.take_vs_dual_wram()
2273 }
2274
2275 /// v2.0.0 beta.5 (Vs. `DualSystem`): install a shared-WRAM copy (the
2276 /// other half of the restore normalization).
2277 pub fn set_vs_dual_wram(&mut self, wram: alloc::boxed::Box<[u8]>) {
2278 self.mapper.set_vs_dual_wram(wram);
2279 }
2280
2281 /// True when the running cart is Vs. System hardware (NES 2.0 console type).
2282 #[must_use]
2283 pub fn is_vs_system(&self) -> bool {
2284 self.cart.console_type == rustynes_mappers::ConsoleType::VsSystem
2285 }
2286
2287 /// True when the cart's header marks a Vs. `DualSystem` board (two CPUs /
2288 /// two PPUs). Detection only — the dual-console emulation is a documented
2289 /// v2.0 deferral (`docs/audit/vs-dualsystem-design-2026-06-11.md`); this
2290 /// lets the frontend surface a clear note instead of a black screen.
2291 #[must_use]
2292 pub const fn is_vs_dual_system(&self) -> bool {
2293 self.cart.vs_dual_system
2294 }
2295
2296 /// Overlay the Vs. System `$4016` upper bits (service, DIP 1/2, coins) onto
2297 /// the standard controller read. No-op on non-Vs. carts, so the standard
2298 /// `$4016` read is byte-identical.
2299 ///
2300 /// Layout (nesdev "Vs. System" §`$4016` read): `PCCD DS0B` — bit 0 = right
2301 /// stick (already in `base`), bit 2 = service, bit 3 = DIP switch 1, bit 4 =
2302 /// DIP switch 2, bit 5 = coin #1, bit 6 = coin #2, bit 7 = primary CPU.
2303 /// Bit 7 is `0` on a single console / the `DualSystem` MAIN half and `0x80`
2304 /// on the `DualSystem` SUB half (Mesen2 `IsVsMainConsole() ? 0 : 0x80` —
2305 /// the identity bit the `DualSystem` ROM polls; v2.0.0 beta.5).
2306 fn vs_overlay_4016(&self, base: u8) -> u8 {
2307 if !self.is_vs_system() {
2308 return base;
2309 }
2310 // Keep only bit 0 (controller D0) + bit 1 (D1, always 0 here); the Vs.
2311 // bus drives bits 2-7 from the panel, not from open bus.
2312 let mut v = base & 0x01;
2313 if self.vs_service {
2314 v |= 0x04;
2315 }
2316 // DIP switch 1 -> bit 3, switch 2 -> bit 4.
2317 v |= (self.vs_dip & 0x01) << 3;
2318 v |= ((self.vs_dip >> 1) & 0x01) << 4;
2319 // Coin acceptors -> bits 5/6.
2320 v |= (self.vs_coin & 0x03) << 5;
2321 // v2.0.0 beta.5: the DualSystem main/sub identity bit.
2322 if self.vs_is_sub {
2323 v |= 0x80;
2324 }
2325 v
2326 }
2327
2328 /// Overlay the Vs. System `$4017` upper bits (DIP 3-8) onto the standard
2329 /// controller read. No-op on non-Vs. carts.
2330 ///
2331 /// Layout (nesdev "Vs. System" §`$4017` read): `DDDD DD0B` — bit 0 = left
2332 /// stick (already in `base`), bits 2-7 = DIP switches 3 through 8.
2333 fn vs_overlay_4017(&self, base: u8) -> u8 {
2334 if !self.is_vs_system() {
2335 return base;
2336 }
2337 // DIP switches 3..=8 occupy bits 2..=7 (switch 3 = DIP bit 2 -> $4017
2338 // bit 2, switch 8 = DIP bit 7 -> $4017 bit 7); a 1:1 mapping.
2339 (base & 0x01) | (self.vs_dip & 0xFC)
2340 }
2341
2342 /// Mapper debug info for the debugger UI: the mapper's own bank/IRQ state
2343 /// ENRICHED (v1.5.0 "Lens" Workstream I8) with the cartridge-level metadata
2344 /// the bus owns — submapper, accuracy tier, ROM/RAM sizes, battery, the IRQ
2345 /// mechanism, and the expansion-audio chip. Output-only; these enrichment
2346 /// fields are filled here rather than in each of the 100+ mappers, and they
2347 /// default to empty (so a mapper's own `debug_info()` is unchanged).
2348 #[must_use]
2349 pub fn mapper_debug_info(&self) -> rustynes_mappers::MapperDebugInfo {
2350 let mut info = self.mapper.debug_info();
2351 let cart = &self.cart;
2352 info.submapper = cart.submapper;
2353 info.tier = rustynes_mappers::mapper_tier(cart.mapper_id, cart.submapper)
2354 .map_or("", rustynes_mappers::MapperTier::name);
2355 info.prg_rom_size = cart.prg_rom.len();
2356 info.chr_rom_size = cart.chr_rom.len();
2357 info.prg_ram_size = cart.prg_ram_size as usize;
2358 info.chr_ram_size = cart.chr_ram_size as usize;
2359 info.has_battery = cart.has_battery;
2360 // IRQ mechanism: named per the documented per-mapper IRQ family table
2361 // (docs/mappers.md). MMC3/RAMBO use PPU A12; MMC5 uses scanline
2362 // detection; the VRC/FME-7/N163 families tick on the CPU-cycle hook.
2363 info.irq_kind = match cart.mapper_id {
2364 4 | 64 | 118 | 119 | 206 => "PPU A12 counter (MMC3-style)",
2365 5 => "PPU scanline (MMC5)",
2366 // CPU-cycle-clocked IRQ counters surface via the caps hook.
2367 _ if self.mapper_caps.cpu_cycle_hook => "CPU cycle (VRC / FME-7 / N163)",
2368 _ => "",
2369 };
2370 info.expansion_audio = if self.mapper_caps.audio {
2371 Some(match cart.mapper_id {
2372 5 => "MMC5",
2373 19 | 210 => "Namco 163",
2374 20 => "FDS",
2375 24 | 26 => "VRC6",
2376 69 => "Sunsoft 5B",
2377 85 => "VRC7 (OPLL)",
2378 _ => "Expansion audio",
2379 })
2380 } else {
2381 None
2382 };
2383 info
2384 }
2385
2386 /// The cached per-cycle mapper capability flags (see
2387 /// [`rustynes_mappers::MapperCaps`]). `caps.audio` reflects whether the
2388 /// loaded mapper has on-cart expansion audio with the `mapper-audio` feature
2389 /// compiled in — used by the frontend to surface expansion-channel mixing
2390 /// controls only for boards that actually have them.
2391 #[must_use]
2392 pub const fn mapper_caps(&self) -> rustynes_mappers::MapperCaps {
2393 self.mapper_caps
2394 }
2395
2396 /// Borrow CPU RAM (2 KiB).
2397 #[must_use]
2398 pub fn ram_bytes(&self) -> &[u8] {
2399 &*self.ram
2400 }
2401
2402 /// Borrow both controllers as a slice.
2403 #[must_use]
2404 pub const fn controllers_ref(&self) -> &[Controller; 2] {
2405 &self.controllers
2406 }
2407
2408 /// Borrow the Four Score players 3 & 4 (save-state).
2409 #[must_use]
2410 pub const fn controllers34_ref(&self) -> &[Controller; 2] {
2411 &self.controllers34
2412 }
2413
2414 /// Enable/disable the Four Score 4-player adapter. Off by default; while
2415 /// off, `$4016`/`$4017` behave exactly as the standard two controllers
2416 /// (byte-identical reads — determinism + save-states unaffected).
2417 pub const fn set_four_score(&mut self, enabled: bool) {
2418 self.four_score = enabled;
2419 }
2420
2421 /// Whether the Four Score adapter is currently enabled.
2422 #[must_use]
2423 pub const fn four_score(&self) -> bool {
2424 self.four_score
2425 }
2426
2427 // --- Famicom Disk System disk control (delegates to the mapper) ---
2428
2429 /// Number of disk sides in the inserted FDS image (0 for cartridge builds).
2430 #[must_use]
2431 pub fn disk_side_count(&self) -> usize {
2432 self.mapper.disk_side_count()
2433 }
2434
2435 /// The currently inserted FDS disk side, or `None` when ejected (or for a
2436 /// cartridge build).
2437 #[must_use]
2438 pub fn inserted_disk_side(&self) -> Option<usize> {
2439 self.mapper.inserted_disk_side()
2440 }
2441
2442 /// Insert FDS side `i` (`Some`) or eject (`None`). No-op on cartridge builds.
2443 pub fn set_disk_side(&mut self, side: Option<usize>) {
2444 self.mapper.set_disk_side(side);
2445 }
2446
2447 /// Number of selectable NSF songs (0 for cartridge / disk builds).
2448 #[must_use]
2449 pub fn nsf_song_count(&self) -> u8 {
2450 self.mapper.nsf_song_count()
2451 }
2452
2453 /// The currently-selected 0-based NSF song (0 for cartridge / disk builds).
2454 #[must_use]
2455 pub fn nsf_current_song(&self) -> u8 {
2456 self.mapper.nsf_current_song()
2457 }
2458
2459 /// Select a 0-based NSF song. Returns `true` if this is an NSF build (so the
2460 /// caller re-runs the reset that re-enters the driver's `init`).
2461 pub fn nsf_set_song(&mut self, song: u8) -> bool {
2462 self.mapper.nsf_set_song(song)
2463 }
2464
2465 /// Start recording the diagnostic FDS read-stream trace (off by default;
2466 /// observation-only). No-op on cartridge builds.
2467 pub fn enable_fds_trace(&mut self) {
2468 self.mapper.enable_fds_trace();
2469 }
2470
2471 /// Drain the accumulated FDS read-stream trace records (empty for cartridge
2472 /// builds / when tracing was never enabled).
2473 pub fn take_fds_trace(&mut self) -> Vec<rustynes_mappers::FdsTraceRec> {
2474 self.mapper.take_fds_trace()
2475 }
2476
2477 /// Re-serialize the (possibly-modified) FDS disk image to its byte layout
2478 /// for host persistence. Empty for cartridge builds.
2479 #[must_use]
2480 pub fn disk_image_bytes(&self) -> Vec<u8> {
2481 self.mapper.disk_image_bytes()
2482 }
2483
2484 /// Whether the FDS disk image has unsaved writes.
2485 #[must_use]
2486 pub fn disk_is_dirty(&self) -> bool {
2487 self.mapper.disk_is_dirty()
2488 }
2489
2490 /// Clear the FDS disk dirty flag (after the host persists the image).
2491 pub fn clear_disk_dirty(&mut self) {
2492 self.mapper.clear_disk_dirty();
2493 }
2494
2495 /// Mark the inserted FDS disk read-only (`true`) or writable (`false`).
2496 pub fn set_disk_write_protected(&mut self, protected: bool) {
2497 self.mapper.set_disk_write_protected(protected);
2498 }
2499
2500 /// Commit a controller-strobe write to all controllers, resetting the
2501 /// Four Score read sequence + reloading its signature when enabled.
2502 const fn commit_controller_strobe(&mut self, value: u8) {
2503 self.controllers[0].write_strobe(value);
2504 self.controllers[1].write_strobe(value);
2505 // Forward the strobe to any attached overlay device (only the Vaus
2506 // latches on it; the Zapper ignores it). Done unconditionally — the
2507 // standard controllers above are still strobed, so detaching a device
2508 // returns to byte-identical behavior.
2509 if let Some(d) = &mut self.expansion_device[0] {
2510 d.write_strobe(value);
2511 }
2512 if let Some(d) = &mut self.expansion_device[1] {
2513 d.write_strobe(value);
2514 }
2515 if self.four_score {
2516 self.controllers34[0].write_strobe(value);
2517 self.controllers34[1].write_strobe(value);
2518 // Reset the 24-read sequence + reload the adapter signature
2519 // (port 0 = 0x08, port 1 = 0x04, shifted out LSB-first).
2520 self.four_score_idx = [0, 0];
2521 self.four_score_sig = [0x08, 0x04];
2522 }
2523 }
2524
2525 /// Read the D0 controller bit for `port` (0 = `$4016`, 1 = `$4017`),
2526 /// advancing the shift register. Four Score off → just
2527 /// `controllers[port].read()`; on → the multiplexed 24-read sequence
2528 /// (primary pad → secondary pad → signature → 1s).
2529 fn read_port(&mut self, port: usize) -> u8 {
2530 // v1.6.0 Workstream A3 (`TAStudio` lag log): any read of $4016/$4017
2531 // counts as the game polling input this frame. Output-only; gated.
2532 #[cfg(feature = "debug-hooks")]
2533 {
2534 self.controller_polled = true;
2535 }
2536 // A non-standard overlay device takes over the port entirely: it
2537 // returns its own bit-positioned byte (Vaus = bits 3/4, Zapper =
2538 // bits 3/4) instead of the standard D0 shift-register bit. The
2539 // standard controller is still strobed (in `commit_controller_strobe`)
2540 // so detaching the device restores byte-identical behavior.
2541 // A3 (v2.2.3, opt-in): serve the Zapper's light bit from the
2542 // beam-relative model. `read_at_scanline` takes `&self` and the PPU is a
2543 // different field, so these are disjoint borrows. Off by default, so the
2544 // shipped path below is byte-identical.
2545 if self.zapper_temporal_light
2546 && let Some(crate::input_device::InputDevice::Zapper(z)) = &self.expansion_device[port]
2547 {
2548 // `scanline()` is `i16` but is non-negative on every current region
2549 // (visible 0..=239, then post-render / vblank up to the pre-render
2550 // line — 261 NTSC / 311 PAL, NOT -1), so `try_from` always succeeds
2551 // and this resolves to `read_at_scanline`, which already yields
2552 // no-light for the pre-render line (`prerender - y >= HOLD` for every
2553 // visible aim). The `Err` arm is a total-conversion fallback: if a
2554 // future convention ever produced a negative scanline (a -1
2555 // pre-render), the correct answer is "no light yet" —
2556 // `read_before_visible` — rather than the row-0 fold a bare
2557 // `unwrap_or(0)` would give.
2558 return match u16::try_from(self.ppu.scanline()) {
2559 Ok(sl) => z.read_at_scanline(self.ppu.framebuffer(), sl),
2560 Err(_) => z.read_before_visible(),
2561 };
2562 }
2563 if let Some(d) = &mut self.expansion_device[port] {
2564 return d.read();
2565 }
2566 if !self.four_score || self.controllers[port].strobe {
2567 return self.controllers[port].read();
2568 }
2569 let idx = self.four_score_idx[port];
2570 let bit = if idx < 8 {
2571 self.controllers[port].read()
2572 } else if idx < 16 {
2573 self.controllers34[port].read()
2574 } else if idx < 24 {
2575 let b = self.four_score_sig[port] & 1;
2576 self.four_score_sig[port] = (self.four_score_sig[port] >> 1) | 0x80;
2577 b
2578 } else {
2579 1
2580 };
2581 if idx < 24 {
2582 self.four_score_idx[port] += 1;
2583 }
2584 bit
2585 }
2586
2587 /// Side-effect-free companion to [`Self::read_port`] (debugger peek).
2588 fn peek_port(&self, port: usize) -> u8 {
2589 // Mirror the temporal-Zapper branch in `read_port` so a debugger peek
2590 // shows the same `$4016`/`$4017` byte the CPU would receive. Without
2591 // this, with `zapper_temporal_light` on, `peek_port` fell through to the
2592 // overlay's frame-granular `peek()` and could disagree with the live
2593 // read. `peek` is non-mutating and all of `scanline()` / `framebuffer()`
2594 // / `read_at_scanline` / `read_before_visible` take `&self`, so this is a
2595 // pure read; it costs `peek_port` its `const` (try_from/match are not
2596 // const here), which nothing relied on. Off by default → byte-identical.
2597 if self.zapper_temporal_light
2598 && let Some(crate::input_device::InputDevice::Zapper(z)) = &self.expansion_device[port]
2599 {
2600 return u16::try_from(self.ppu.scanline()).map_or_else(
2601 |_| z.read_before_visible(),
2602 |sl| z.read_at_scanline(self.ppu.framebuffer(), sl),
2603 );
2604 }
2605 if let Some(d) = &self.expansion_device[port] {
2606 return d.peek();
2607 }
2608 if !self.four_score || self.controllers[port].strobe {
2609 return self.controllers[port].peek();
2610 }
2611 let idx = self.four_score_idx[port];
2612 if idx < 8 {
2613 self.controllers[port].peek()
2614 } else if idx < 16 {
2615 self.controllers34[port].peek()
2616 } else if idx < 24 {
2617 self.four_score_sig[port] & 1
2618 } else {
2619 1
2620 }
2621 }
2622
2623 /// Bus-side bookkeeping snapshot used by `bus_snapshot::encode_bus`.
2624 #[must_use]
2625 pub const fn bus_misc_state(&self) -> crate::bus_snapshot::BusMiscState {
2626 crate::bus_snapshot::BusMiscState {
2627 dma_pending: self.dma_pending,
2628 dma_cycles_owed: self.dma_cycles_owed,
2629 dma_byte: self.dma_byte,
2630 dma_idx: self.dma_idx,
2631 dma_page: self.dma_page,
2632 dma_halt_addr: self.dma_halt_addr,
2633 last_nmi_level: self.last_nmi_level,
2634 nmi_edge_latch: self.nmi_edge_latch,
2635 open_bus: self.open_bus,
2636 last_read_addr: self.last_read_addr,
2637 deferred_dma_replay_addr: self.deferred_dma_replay_addr,
2638 in_dmc_dma: self.in_dmc_dma,
2639 controller_write_pending: self.controller_write_pending,
2640 controller_write_value: self.controller_write_value,
2641 four_score: self.four_score,
2642 four_score_idx: self.four_score_idx,
2643 four_score_sig: self.four_score_sig,
2644 // W3-Stage-4 (2026-06-10): the unified-engine OAM state + the
2645 // DMC halt latch. Always present in the ferry struct (zeros when
2646 // the engine feature is off) so the BUS section layout is
2647 // identical across feature builds.
2648 dmc_halt: self.dmc_halt,
2649 uni_oam_active: self.uni_oam_active,
2650 uni_oam_halt: self.uni_oam_halt,
2651 uni_oam_aligned: self.uni_oam_aligned,
2652 uni_oam_addr: self.uni_oam_addr,
2653 ppu_clock: self.ppu_clock,
2654 dma_mc_consumed: self.dma_mc_consumed,
2655 }
2656 }
2657
2658 /// Apply a previously-snapshotted bus bookkeeping state.
2659 pub const fn set_bus_misc_state(&mut self, s: crate::bus_snapshot::BusMiscState) {
2660 self.dma_pending = s.dma_pending;
2661 self.dma_cycles_owed = s.dma_cycles_owed;
2662 self.dma_byte = s.dma_byte;
2663 self.dma_idx = s.dma_idx;
2664 self.dma_page = s.dma_page;
2665 self.dma_halt_addr = s.dma_halt_addr;
2666 self.last_nmi_level = s.last_nmi_level;
2667 self.nmi_edge_latch = s.nmi_edge_latch;
2668 self.open_bus = s.open_bus;
2669 self.last_read_addr = s.last_read_addr;
2670 self.deferred_dma_replay_addr = s.deferred_dma_replay_addr;
2671 self.in_dmc_dma = s.in_dmc_dma;
2672 self.controller_write_pending = s.controller_write_pending;
2673 self.controller_write_value = s.controller_write_value;
2674 self.four_score = s.four_score;
2675 self.four_score_idx = s.four_score_idx;
2676 self.four_score_sig = s.four_score_sig;
2677 // W3-Stage-4 (2026-06-10): the unified engine's OAM state + the DMC
2678 // halt latch are now serialized (trailing-default-zero in the BUS
2679 // section), replacing the Stage-1 clear-on-restore. Snapshots are
2680 // taken at instruction boundaries where the engine is idle, so for
2681 // every legitimately produced blob these decode to the same inactive
2682 // state the clear imposed -- but a restored blob now reproduces them
2683 // EXACTLY instead of by assumption.
2684 {
2685 self.dmc_halt = s.dmc_halt;
2686 }
2687 {
2688 self.uni_oam_active = s.uni_oam_active;
2689 self.uni_oam_halt = s.uni_oam_halt;
2690 self.uni_oam_aligned = s.uni_oam_aligned;
2691 self.uni_oam_addr = s.uni_oam_addr;
2692 }
2693 // The R1 substrate master-clock pair (see `BusMiscState::ppu_clock`):
2694 // pre-Stage-4 blobs decode these as 0, which together with the CPU
2695 // v1-blob `master_clock` upconvert keeps the pair coherent.
2696 {
2697 self.ppu_clock = s.ppu_clock;
2698 self.dma_mc_consumed = s.dma_mc_consumed;
2699 }
2700 }
2701
2702 /// Set the cumulative CPU cycle counter (used by save-state restore).
2703 pub const fn set_cycle(&mut self, cycle: u64) {
2704 self.cycle = cycle;
2705 }
2706
2707 /// Overwrite the 2 KiB CPU RAM.
2708 ///
2709 /// # Errors
2710 ///
2711 /// Returns [`SnapshotError::SectionInvalid`] if `bytes.len() != 2048`.
2712 pub fn set_ram_bytes(&mut self, bytes: &[u8]) -> Result<(), SnapshotError> {
2713 if bytes.len() != self.ram.len() {
2714 return Err(SnapshotError::SectionInvalid {
2715 tag: "BUS ".into(),
2716 reason: format!("ram length {} != {}", bytes.len(), self.ram.len()),
2717 });
2718 }
2719 self.ram.copy_from_slice(bytes);
2720 Ok(())
2721 }
2722
2723 /// Overwrite both controllers' state.
2724 pub const fn set_controllers(&mut self, controllers: [Controller; 2]) {
2725 self.controllers = controllers;
2726 }
2727
2728 /// Overwrite the Four Score players 3 & 4 (save-state restore).
2729 pub const fn set_controllers34(&mut self, controllers: [Controller; 2]) {
2730 self.controllers34 = controllers;
2731 }
2732
2733 /// Write a byte directly into CPU work RAM (`$0000-$1FFF`, mirrored every
2734 /// `$800`). Used by the frontend's raw RAM cheats (GameShark-style),
2735 /// applied caller-side *after* [`crate::Nes::run_frame`] so the core run
2736 /// loop stays pure (the determinism contract is unperturbed for the
2737 /// no-cheat path). No-op for addresses outside system RAM.
2738 pub fn poke_ram(&mut self, addr: u16, value: u8) {
2739 if addr < 0x2000 {
2740 self.ram[(addr & 0x07FF) as usize] = value;
2741 }
2742 }
2743
2744 /// Encode the entire bus + chip state into a `.rns` snapshot.
2745 ///
2746 /// Returns the bytes the caller should persist via
2747 /// `frontend::save_state` (or feed into the rewind ring).
2748 ///
2749 /// The output is bit-deterministic: same `(seed, ROM, input sequence)`
2750 /// produces identical bytes.
2751 #[must_use]
2752 pub fn snapshot(&self, rom_hash_tag: [u8; save_state::ROM_HASH_TAG_LEN]) -> Vec<u8> {
2753 let mut out = Vec::with_capacity(0x4_0000);
2754 self.snapshot_into(&mut out, rom_hash_tag);
2755 out
2756 }
2757
2758 /// v2.8.0 Phase 3 — [`Self::snapshot`] into a caller-owned buffer
2759 /// (cleared first; capacity reused across calls). The per-call
2760 /// allocation of the ~250 KiB blob matters to per-frame consumers
2761 /// (run-ahead, the netplay save-state ring, rewind).
2762 pub fn snapshot_into(
2763 &self,
2764 out: &mut Vec<u8>,
2765 rom_hash_tag: [u8; save_state::ROM_HASH_TAG_LEN],
2766 ) {
2767 self.snapshot_into_with(out, rom_hash_tag, false);
2768 }
2769
2770 /// v2.3.3 — [`Self::snapshot_into`] with the PPU encoded slim (no
2771 /// framebuffer). See `rustynes_ppu::PPU_SNAPSHOT_SLIM_FLAG`.
2772 pub fn snapshot_into_slim(
2773 &self,
2774 out: &mut Vec<u8>,
2775 rom_hash_tag: [u8; save_state::ROM_HASH_TAG_LEN],
2776 ) {
2777 self.snapshot_into_with(out, rom_hash_tag, true);
2778 }
2779
2780 fn snapshot_into_with(
2781 &self,
2782 out: &mut Vec<u8>,
2783 rom_hash_tag: [u8; save_state::ROM_HASH_TAG_LEN],
2784 slim: bool,
2785 ) {
2786 out.clear();
2787 save_state::write_header(out, rom_hash_tag);
2788
2789 // BUS section.
2790 let bus_body = crate::bus_snapshot::encode_bus(self);
2791 save_state::write_section(
2792 out,
2793 save_state::tag::BUS,
2794 crate::bus_snapshot::BUS_SECTION_VERSION,
2795 &bus_body,
2796 );
2797
2798 // CPU is owned by the surrounding `Nes` facade — but the bus is
2799 // the canonical owner of the persistable state, so the public
2800 // `snapshot` lives there. The CPU section is appended by
2801 // `Nes::snapshot` because the CPU isn't reachable from inside
2802 // the bus without violating the dependency graph. We stub
2803 // section emission here; `Nes::snapshot` will re-call this and
2804 // splice the CPU bytes in.
2805
2806 // PPU section.
2807 let ppu_body = if slim {
2808 self.ppu.snapshot_slim()
2809 } else {
2810 self.ppu.snapshot()
2811 };
2812 save_state::write_section(
2813 out,
2814 save_state::tag::PPU,
2815 rustynes_ppu::PPU_SNAPSHOT_VERSION,
2816 &ppu_body,
2817 );
2818
2819 // APU section.
2820 let apu_body = self.apu.snapshot();
2821 save_state::write_section(
2822 out,
2823 save_state::tag::APU,
2824 rustynes_apu::APU_SNAPSHOT_VERSION,
2825 &apu_body,
2826 );
2827
2828 // MAP section (mapper-resident state).
2829 let map_body = self.mapper.save_state();
2830 save_state::write_section(out, save_state::tag::MAP, 1, &map_body);
2831 }
2832
2833 /// Apply a previously snapshotted blob *to the bus and chips*. The CPU
2834 /// is restored separately by [`crate::Nes::restore`].
2835 ///
2836 /// # Errors
2837 ///
2838 /// Returns [`SnapshotError`] for unknown sections, version mismatches,
2839 /// or malformed bodies.
2840 pub fn restore(&mut self, data: &[u8]) -> Result<(), SnapshotError> {
2841 let (header, body_off) = save_state::parse_header(data)?;
2842 let _ = header; // currently informational
2843 let mut saw_bus = false;
2844 let mut saw_ppu = false;
2845 let mut saw_apu = false;
2846 let mut saw_map = false;
2847 for s in save_state::SectionIter::new(&data[body_off..]) {
2848 let s = s?;
2849 match s.tag {
2850 save_state::tag::BUS => {
2851 if s.version != crate::bus_snapshot::BUS_SECTION_VERSION {
2852 return Err(SnapshotError::VersionMismatch {
2853 tag: save_state::tag_string(s.tag),
2854 file_version: s.version,
2855 chip_supports: crate::bus_snapshot::BUS_SECTION_VERSION,
2856 });
2857 }
2858 crate::bus_snapshot::decode_bus(self, s.body)?;
2859 saw_bus = true;
2860 }
2861 save_state::tag::PPU => {
2862 if s.version != rustynes_ppu::PPU_SNAPSHOT_VERSION {
2863 return Err(SnapshotError::VersionMismatch {
2864 tag: save_state::tag_string(s.tag),
2865 file_version: s.version,
2866 chip_supports: rustynes_ppu::PPU_SNAPSHOT_VERSION,
2867 });
2868 }
2869 self.ppu.restore(s.body).map_err(|e: PpuSnapshotError| {
2870 SnapshotError::SectionInvalid {
2871 tag: save_state::tag_string(s.tag),
2872 reason: format!("{e}"),
2873 }
2874 })?;
2875 saw_ppu = true;
2876 }
2877 save_state::tag::APU => {
2878 if s.version != rustynes_apu::APU_SNAPSHOT_VERSION {
2879 return Err(SnapshotError::VersionMismatch {
2880 tag: save_state::tag_string(s.tag),
2881 file_version: s.version,
2882 chip_supports: rustynes_apu::APU_SNAPSHOT_VERSION,
2883 });
2884 }
2885 self.apu.restore(s.body).map_err(|e: ApuSnapshotError| {
2886 SnapshotError::SectionInvalid {
2887 tag: save_state::tag_string(s.tag),
2888 reason: format!("{e}"),
2889 }
2890 })?;
2891 saw_apu = true;
2892 }
2893 save_state::tag::MAP => {
2894 self.mapper.load_state(s.body).map_err(|e: MapperError| {
2895 SnapshotError::SectionInvalid {
2896 tag: save_state::tag_string(s.tag),
2897 reason: format!("{e}"),
2898 }
2899 })?;
2900 saw_map = true;
2901 }
2902 save_state::tag::CPU => {
2903 // Skipped — restored by the surrounding `Nes` facade.
2904 }
2905 _other => {
2906 // Unknown tags are forward-compatible: skip silently
2907 // so cross-version files load when they include
2908 // sections this build doesn't know about (e.g. a
2909 // future "DBG " debugger section).
2910 }
2911 }
2912 }
2913 // BUS is mandatory; chip sections are mandatory too because
2914 // they round-trip the entire emulator state.
2915 if !saw_bus {
2916 return Err(SnapshotError::MissingSection("BUS ".into()));
2917 }
2918 if !saw_ppu {
2919 return Err(SnapshotError::MissingSection("PPU ".into()));
2920 }
2921 if !saw_apu {
2922 return Err(SnapshotError::MissingSection("APU ".into()));
2923 }
2924 if !saw_map {
2925 return Err(SnapshotError::MissingSection("MAP ".into()));
2926 }
2927 // RW-0 fix: under R1, `dmc_driven_externally` is NOT serialized (it is
2928 // build configuration, not emulated state), so after `apu.restore` it
2929 // reverts to the `Apu::new` default (`false`), which STOPS `put_cycle`
2930 // toggling and disables the interleaved DMC DMA service — a latent R1
2931 // save-state correctness bug. Re-apply the R1 drive here exactly as
2932 // `new`/`reset`/`power_cycle` do.
2933 //
2934 // W3-Stage-4 (2026-06-10, the RW-3 follow-through): the APU snapshot
2935 // now DOES carry the exact `put_cycle` / `parity_seed` phase in its
2936 // Stage-4 tail, so re-seed the boot alignment ONLY for pre-Stage-4
2937 // blobs that lack the tail (`snapshot_restored_parity` is false) —
2938 // otherwise the boot seed would overwrite the restored mid-state
2939 // parity that the counter-collapse end-flip reads at the next access
2940 // point.
2941 {
2942 self.apu.set_dmc_driven_externally(true);
2943 if !self.apu.snapshot_restored_parity() {
2944 self.apu.seed_apu_alignment(0);
2945 }
2946 }
2947 Ok(())
2948 }
2949
2950 const fn ppu_region(&self) -> PpuRegion {
2951 match self.cart.region {
2952 rustynes_mappers::Region::Pal => PpuRegion::Pal,
2953 rustynes_mappers::Region::Dendy => PpuRegion::Dendy,
2954 _ => PpuRegion::Ntsc,
2955 }
2956 }
2957
2958 const fn apu_region(&self) -> ApuRegion {
2959 match self.cart.region {
2960 rustynes_mappers::Region::Pal => ApuRegion::Pal,
2961 rustynes_mappers::Region::Dendy => ApuRegion::Dendy,
2962 _ => ApuRegion::Ntsc,
2963 }
2964 }
2965
2966 /// Drive the PPU forward 3 dots and account for one CPU cycle of
2967 /// bookkeeping (mapper-cycle hook, DMA progress, NMI edge sample,
2968 /// APU tick).
2969 #[allow(clippy::too_many_lines)] // Session-21 added per-cycle DMC + bus-access snapshots; splitting the trace push into a helper would force the bus to recompute `trace_*_pre_tick` values across function boundaries.
2970 pub(crate) fn tick_one_cpu_cycle(&mut self) {
2971 // Tick PPU 3 dots in NTSC. PAL would be 3.2 (5 dots per 16 PPU dots);
2972 // we approximate as 3 for now and gate region accuracy behind a
2973 // future Phase 2 follow-up.
2974 //
2975 // Sample the PPU /NMI line state *between every dot* so a glitched
2976 // edge that goes low->high then back to low within a single CPU
2977 // cycle (e.g. PPUCTRL.7 set during pre-render dot 0, then VBL
2978 // cleared at dot 1 within the same CPU cycle) is still latched.
2979 //
2980 // When the `irq-timing-trace` cargo feature is enabled, capture
2981 // per-cycle (cpu_cycle, ppu_scanline, ppu_dot, a12_events, IRQ
2982 // lines sampled at TWO points within the cycle, NMI line) into
2983 // the bus's trace buffer.
2984 //
2985 // Phase A of the C1 plan (`docs/adr/0002-irq-timing-coordination.md`)
2986 // takes TWO IRQ snapshots per CPU cycle so the M2-low → M2-high
2987 // asymmetry the coordinated change is designed to model is
2988 // observable in the trace data:
2989 //
2990 // * M2-low snapshot: taken AFTER PPU sub-dot 0 has ticked.
2991 // This catches any A12 transition / APU IRQ assertion that
2992 // happened on the cycle's first PPU dot, but before sub-dots
2993 // 1 and 2 have run.
2994 // * M2-high snapshot: taken AFTER PPU sub-dot 2 has ticked,
2995 // i.e. at the end-of-3-PPU-dots boundary, BEFORE
2996 // `notify_cpu_cycle` / `tick_with_external` run. This is
2997 // the historical query point the pre-Phase-B2
2998 // `Bus::poll_irq` impl used when called from
2999 // `Cpu::idle_tick` after `bus.on_cpu_cycle()` returned.
3000 //
3001 // The conventional names map to silicon's φ1 / φ2 halves of the
3002 // 6502 cycle. The exact sub-dot placement is conventional, not
3003 // canonical — what matters is that the bus records IRQ state at
3004 // TWO distinct points within the cycle so downstream phases can
3005 // diff them.
3006 //
3007 // Phase B2 of the C1 IRQ-timing rework: the M2-low and M2-high
3008 // snapshots are now stored on `self` unconditionally (not gated
3009 // on the `irq-timing-trace` feature). The trace fixture's
3010 // `_at_low` / `_at_high` columns read from these snapshots
3011 // rather than re-querying the mapper / APU, removing the
3012 // duplicate `mapper.irq_pending()` call that Phase A introduced
3013 // inside the cycle. The production `Bus::poll_irq` /
3014 // `Bus::poll_irq_at_phase` paths on `LockstepBus` also read
3015 // from these snapshots — see the `impl Bus for LockstepBus`
3016 // block below.
3017 // Session-24 / Phase 3 (Controller Strobing): commit any
3018 // pending controller-strobe write at the START of this CPU
3019 // cycle (M2-low boundary). Mirrors Mesen2's
3020 // `NesConsole::ProcessCpuClock` → `NesControlManager::ProcessWrites`
3021 // call site (`Core/NES/NesConsole.cpp` line 72). See
3022 // `docs/audit/session-24-phase3-controller-strobing-2026-05-23.md`.
3023 if self.controller_write_pending > 0 {
3024 self.controller_write_pending -= 1;
3025 if self.controller_write_pending == 0 {
3026 let value = self.controller_write_value;
3027 // The strobe line is shared between both controllers.
3028 self.commit_controller_strobe(value);
3029 }
3030 }
3031 #[cfg(feature = "irq-timing-trace")]
3032 let (trace_scanline_start, trace_dot_start, trace_frame_start) =
3033 (self.ppu.scanline(), self.ppu.dot(), self.ppu.frame());
3034 // Session-21 (Sprint 1 iteration 2 prereq): snapshot the DMC
3035 // scheduler's "pre-tick" state (mirrors `_at_low` for the IRQ
3036 // columns). These read BEFORE `apu.tick_with_external` runs at
3037 // the bottom of this method.
3038 #[cfg(feature = "irq-timing-trace")]
3039 let trace_dmc_dma_pending_pre = self.apu.dmc_dma_pending();
3040 // M2-phase tracking (Phase B1 of the C1 IRQ-timing rework):
3041 // each CPU cycle begins in `M2Phase::Low`, transitions to
3042 // `M2Phase::High` after sub-dot 1 has ticked (the M2-rising
3043 // boundary), and resets to `Low` at end-of-cycle.
3044 self.m2_phase = M2Phase::Low;
3045 #[cfg(not(feature = "irq-timing-trace"))]
3046 for sub_dot in 0..3u8 {
3047 let mut adapter = PpuBusAdapter {
3048 mapper: self.mapper.as_mut(),
3049 nt_override: self.nt_mirroring_override,
3050 sub_dot,
3051 };
3052 self.ppu.tick(&mut adapter);
3053 self.sample_nmi_edge();
3054 if sub_dot == 0 {
3055 // M2-low IRQ snapshot.
3056 self.irq_snapshot_mapper_at_low = self.mapper.irq_pending();
3057 self.irq_snapshot_apu_at_low = self.apu.irq_line();
3058 }
3059 if sub_dot == 1 {
3060 self.m2_phase = M2Phase::High;
3061 }
3062 }
3063 #[cfg(feature = "irq-timing-trace")]
3064 for sub_dot in 0..3u8 {
3065 let mut adapter = PpuBusAdapter {
3066 mapper: self.mapper.as_mut(),
3067 nt_override: self.nt_mirroring_override,
3068 sub_dot,
3069 trace_a12_latest: if self.irq_trace.is_some() {
3070 Some(&mut self.trace_a12_latest)
3071 } else {
3072 None
3073 },
3074 };
3075 self.ppu.tick(&mut adapter);
3076 self.sample_nmi_edge();
3077 // The `is_some()` guard is load-bearing beyond the borrow below:
3078 // it keeps `take()` -- which CLEARS `trace_a12_latest` -- from
3079 // running when tracing is off. Short-circuit evaluation in the
3080 // let-chain preserves that exactly.
3081 if self.irq_trace.is_some()
3082 && let Some(level) = self.trace_a12_latest.take()
3083 {
3084 if let Some(t) = self.irq_trace.as_mut() {
3085 t.notify_a12_count = t.notify_a12_count.saturating_add(1);
3086 }
3087 // The PPU already filters to transitions only; every
3088 // `notify_a12` call IS a level change. Record it.
3089 self.trace_a12_scratch.push(A12Event { sub_dot, level });
3090 self.trace_last_a12 = level;
3091 }
3092 if sub_dot == 0 {
3093 // M2-low IRQ snapshot: taken AFTER sub-dot 0 has ticked
3094 // so it reflects the dot's mapper-side effects (e.g. an
3095 // A12 rise on sub-dot 0 that just clocked the MMC3 IRQ
3096 // counter). Sub-dots 1 and 2 have not yet run.
3097 self.irq_snapshot_mapper_at_low = self.mapper.irq_pending();
3098 self.irq_snapshot_apu_at_low = self.apu.irq_line();
3099 }
3100 if sub_dot == 1 {
3101 self.m2_phase = M2Phase::High;
3102 }
3103 }
3104
3105 // Phase-A-compatible end-of-3-PPU-dots snapshot — taken BEFORE
3106 // `notify_cpu_cycle` / `tick_with_external` advance the mapper
3107 // and APU. Only used by the trace fixture's `_at_high` column
3108 // so the Phase A baseline CSV files stay byte-identical across
3109 // Phases B2+. The production `Bus::poll_irq{,_at_phase}` path
3110 // reads from `irq_snapshot_*_at_high` below, taken AFTER those
3111 // advance, so the CPU's IRQ sample point is unchanged.
3112 #[cfg(feature = "irq-timing-trace")]
3113 let trace_mapper_at_high_pre_tick = self.mapper.irq_pending();
3114 #[cfg(feature = "irq-timing-trace")]
3115 let trace_apu_at_high_pre_tick = self.apu.irq_line();
3116
3117 // End-of-cycle: the bus advances to the next CPU cycle, which
3118 // (re)starts in `M2Phase::Low`. Reset BEFORE the cycle counter
3119 // increment so any future read of `current_m2_phase()` from
3120 // inside `notify_cpu_cycle` / `tick_with_external` sees the new
3121 // cycle's phase rather than the previous cycle's tail.
3122 self.m2_phase = M2Phase::Low;
3123 self.cycle = self.cycle.wrapping_add(1);
3124 self.ppu.on_cpu_cycle();
3125 self.mapper.notify_cpu_cycle();
3126 // Sample the mapper's audio extension AFTER notify_cpu_cycle has
3127 // advanced its oscillators. `Mapper::mix_audio` returns i32 (widened
3128 // from i16 in v2.2.3 so the Sunsoft 5B's ~3.6x full-volume level is
3129 // representable); we scale to approximately the same [-0.5, 0.5] range
3130 // as the APU mixer's own output. Mappers without on-cart audio return
3131 // 0, which scales to 0.0 -- a no-op for the standard cartridges.
3132 //
3133 // `as f32` rather than `f32::from`: there is no lossless From<i32> for
3134 // f32. The cast is exact for every value any board actually produces
3135 // (|sample| well under 2^24, where f32 is still integer-exact); the
3136 // widening exists to raise a ~32k ceiling to ~16.7M, not to use it.
3137 #[allow(clippy::cast_precision_loss)]
3138 let mapper_sample = self.mapper.mix_audio() as f32 / 65536.0;
3139 self.apu.tick_with_external(mapper_sample);
3140 // Fan-out the APU frame-counter events to any on-cart audio
3141 // extension that shares the 2A03 frame-counter cadence (MMC5).
3142 // Default no-op for all other mappers.
3143 let ev = self.apu.last_frame_events();
3144 self.mapper.notify_frame_event(MapperFrameEvents {
3145 quarter: ev.quarter,
3146 half: ev.half,
3147 });
3148
3149 // M2-high IRQ snapshot: at the VERY END of `tick_one_cpu_cycle`,
3150 // AFTER `notify_cpu_cycle` / `tick_with_external` /
3151 // `notify_frame_event` have run. This matches the historical
3152 // `mapper.irq_pending() || apu.irq_line()` query point that
3153 // `Cpu::idle_tick` saw when it called `bus.poll_irq()` after
3154 // `bus.on_cpu_cycle()` returned — so the production
3155 // `Bus::poll_irq` / `poll_irq_at_phase(M2Phase::High)` paths
3156 // stay semantically identical to the pre-Phase-B2 direct query
3157 // of `mapper.irq_pending() || apu.irq_line()`.
3158 self.irq_snapshot_mapper_at_high = self.mapper.irq_pending();
3159 self.irq_snapshot_apu_at_high = self.apu.irq_line();
3160
3161 #[cfg(feature = "irq-timing-trace")]
3162 if self.irq_trace.is_some() {
3163 let events = core::mem::take(&mut self.trace_a12_scratch);
3164 let events_len = events.len();
3165 // Session-21: snapshot the DMC scheduler "post-tick" state
3166 // and consume the per-cycle bus-access tracker. These are
3167 // taken AFTER `apu.tick_with_external` has run for this
3168 // cycle, so they reflect the end-of-cycle scheduler shape
3169 // that the next CPU cycle's bus access will observe.
3170 let bus_access = core::mem::replace(&mut self.trace_bus_access, BusAccess::Idle);
3171 let bus_addr = core::mem::take(&mut self.trace_bus_addr);
3172 let bus_data = core::mem::take(&mut self.trace_bus_data);
3173 let rec = CycleRecord {
3174 // `cpu_cycle` here refers to the cycle we JUST ticked.
3175 // `self.cycle` was incremented above, so subtract 1.
3176 cpu_cycle: self.cycle.wrapping_sub(1),
3177 pc: self.trace_last_pc,
3178 ppu_scanline: trace_scanline_start,
3179 ppu_dot: trace_dot_start,
3180 ppu_frame: trace_frame_start,
3181 irq_pending_mapper_at_low: self.irq_snapshot_mapper_at_low,
3182 irq_pending_apu_at_low: self.irq_snapshot_apu_at_low,
3183 // Trace's `_at_high` columns retain the Phase A
3184 // pre-tick_with_external semantics so the committed
3185 // baseline CSVs in
3186 // `crates/rustynes-test-harness/golden/irq_trace/` stay
3187 // byte-identical. Production `poll_irq` reads from
3188 // the post-tick `irq_snapshot_*_at_high` fields above
3189 // instead.
3190 irq_pending_mapper_at_high: trace_mapper_at_high_pre_tick,
3191 irq_pending_apu_at_high: trace_apu_at_high_pre_tick,
3192 nmi_line: self.ppu.nmi_line(),
3193 a12_events: events,
3194 // --- Session-21 DMC + bus-access columns ---
3195 dmc_dma_pending_pre: trace_dmc_dma_pending_pre,
3196 dmc_dma_pending_post: self.apu.dmc_dma_pending(),
3197 dmc_dma_short_post: self.apu.dmc_dma_short(),
3198 dmc_abort_pending_post: self.apu.dmc_abort_pending(),
3199 dmc_abort_delay_post: self.apu.dmc_abort_delay(),
3200 dmc_dma_cooldown_post: self.apu.dmc_dma_cooldown(),
3201 dmc_dma_delay_post: self.apu.dmc_dma_delay(),
3202 apu_phase_post: self.apu.apu_phase(),
3203 in_dmc_dma: self.in_dmc_dma,
3204 dma_cycles_owed: self.dma_cycles_owed,
3205 bus_access,
3206 bus_addr,
3207 bus_data,
3208 put_cycle_post: self.apu.put_cycle(),
3209 dmc_timer_post: self.apu.dmc_timer(),
3210 dmc_bits_remaining_post: self.apu.dmc_bits_remaining(),
3211 dmc_silence_post: self.apu.dmc_silence(),
3212 dmc_buffer_full_post: self.apu.dmc_buffer_full(),
3213 };
3214 if let Some(t) = self.irq_trace.as_mut() {
3215 if events_len > 0 {
3216 t.records_with_a12_count = t.records_with_a12_count.saturating_add(1);
3217 }
3218 t.push(rec);
3219 }
3220 }
3221 // v2.0 R1 DMA-coherence (Phase 3): under `mc-r1-substrate` this fn is
3222 // reached ONLY from the bus-side DMA path — the normal R1 cycle runs
3223 // the PPU via `run_ppu_to` + does its per-cycle work in `cpu_clock`,
3224 // which does NOT call this. Each DMA cycle ticked the real PPU by 3
3225 // dots without advancing `master_clock`/`ppu_clock`. Bump `ppu_clock`
3226 // so the next `run_ppu_to` does not RE-tick those dots, and
3227 // `dma_mc_consumed` so `Cpu::end_cycle` folds the DMA span into
3228 // `master_clock` — keeping the CPU<->PPU phase coherent across DMA
3229 // (the v2.0-R1 regression this prevents). Mirrors `dma_tick_one_cycle`
3230 // on `refactor/v2.0-master-clock`.
3231 {
3232 let (cpu_div, ppu_div) = self.region_dividers();
3233 // The PPU was physically ticked 3 dots by this DMA cycle, so
3234 // `ppu_clock` advances by exactly `3 * ppu_divider` mc — keeping the
3235 // boundary check in `run_ppu_to` from re-ticking those dots.
3236 self.ppu_clock = self.ppu_clock.wrapping_add(u64::from(ppu_div) * 3);
3237 // `master_clock` (via `dma_mc_consumed`) advances by the region's
3238 // true CPU-cycle span (`cpu_divider`). On NTSC/Dendy this equals
3239 // `3 * ppu_divider` (12/15), so the path is byte-identical; on PAL
3240 // (16 vs 15) the 1-mc/cycle deficit accumulates and the next
3241 // `run_ppu_to` ticks the catch-up dot, yielding the correct 3.2:1
3242 // average across the DMA span.
3243 self.dma_mc_consumed = self.dma_mc_consumed.wrapping_add(u64::from(cpu_div));
3244 }
3245 }
3246
3247 /// Capture the PPU /NMI line transition (false → true) into the edge
3248 /// latch consumed by [`Bus::poll_nmi`]. Idempotent within a "still
3249 /// asserted" window: only the rising edge latches.
3250 const fn sample_nmi_edge(&mut self) {
3251 let level = self.ppu.nmi_line();
3252 if level && !self.last_nmi_level {
3253 self.nmi_edge_latch = true;
3254 }
3255 self.last_nmi_level = level;
3256 }
3257
3258 /// OAM-DMA source fetch (Session-26 / Sprint 2 iter 4).
3259 ///
3260 /// The 2A03 has three internal address buses (6502, OAM DMA, DMC
3261 /// DMA), but only the 6502 bus asserts the APU/controller chip
3262 /// select. During OAM DMA the 6502 is halted, so its bus is parked
3263 /// at `self.dma_halt_addr` (last CPU read address). The OAM DMA
3264 /// engine drives the EXTERNAL address bus with `src_addr`, but the
3265 /// APU registers' `CHIP_SELECT` is gated on `6502_addr ∈ $4000-$401F`,
3266 /// not on the DMA's source page.
3267 ///
3268 /// Consequence: if the 6502 bus is parked outside `$4000-$401F`
3269 /// and the OAM DMA reads a source address inside that range, the
3270 /// APU/controllers are silent — the read returns the open-bus
3271 /// latch and triggers no register side-effects (no `apu.read_status()`,
3272 /// no controller shift, etc.). The DMC DMA helper already implements
3273 /// the equivalent gate (`dmc_dma_read` lines 1329-1356).
3274 ///
3275 /// `AccuracyCoin` `APU Register Activation` Test 4 (asm:8091-8109)
3276 /// exercises this: `LDA #$40; STA $4014` runs an OAM DMA from page
3277 /// `$40` while CPU code lives in PRG ROM. Without this gate, the
3278 /// DMA's `$4015` read clears the frame-counter IRQ flag, failing
3279 /// the subsequent `LDA $4015 / AND #$40 / BEQ FAIL` check.
3280 ///
3281 /// The Test 5/6 conflict-path semantics (where the 6502 bus IS in
3282 /// `$4000-$401F` because the test uses `JSR $3FFE` + the BRK trick)
3283 /// need additional modelling — deferred. Two components, established by
3284 /// the 2026-06-05 investigation (`docs/audit/`):
3285 /// 1. **Active-window mirror decode.** When the 6502 bus is parked in
3286 /// `$4000-$401F`, an OAM DMA reading page `$40` reads the readable
3287 /// registers (`$4015`/`$4016`/`$4017`) AND their `$20`-byte mirrors:
3288 /// the 2A03 decodes on the low 5 address bits, so `$4020-$40FF` mirror
3289 /// `$4000-$401F` (`$4035` -> `$4015`) with side-effects (`$4015` clears
3290 /// the frame IRQ flag; `$4016`/`$4017` advance the controller shift).
3291 /// The fix is to mask `src` to `0x4000 | (src & 0x1F)` here when active.
3292 /// 2. **Upstream coupling (the actual blocker).** This is NOT independently
3293 /// reachable: Test 6's OAM-copy is all-zeros in this emulator because the
3294 /// page-`$40` register-read OAM DMA does not fire as the test intends — it
3295 /// depends on Test 5's `[DMC DMA! Overwrite data bus with $40]` trick
3296 /// landing cycle-exactly so `STA $4014` reads `$40` and the 6502 bus is
3297 /// parked in `$40xx` during the DMA. That is the deferred DMC-DMA-timing /
3298 /// data-bus axis. So component 1 is correct hardware behavior but inert
3299 /// until that axis lands — do NOT add it speculatively (it touches the
3300 /// default build and cannot be verified against the test in isolation).
3301 fn raw_oam_dma_read(&mut self, src_addr: u16) -> u8 {
3302 if (self.dma_halt_addr & 0xFFE0) != 0x4000 && (src_addr & 0xFFE0) == 0x4000 {
3303 // APU/controllers inactive: return the floating-bus latch
3304 // without firing any register side-effects. The latch
3305 // itself is NOT updated — DMA reads of the inactive
3306 // register window don't drive the external data bus
3307 // (the chip is silent).
3308 return self.open_bus;
3309 }
3310 // W3-Stage-4 (`mc-r1-oam-dma-reg-window`): the ACTIVE-window arm —
3311 // the 6502 bus is parked in `$4000-$401F`, so the APU/controller
3312 // chip select is asserted for EVERY OAM-DMA source read and the
3313 // readable registers decode at `$4000 | (src & $1F)` (the `$20`-byte
3314 // mirrors AccuracyCoin `APU Register Activation` Tests 5-7 bracket).
3315 if (self.dma_halt_addr & 0xFFE0) == 0x4000 {
3316 return self.oam_dma_read_reg_active(src_addr);
3317 }
3318 self.raw_cpu_read(src_addr)
3319 }
3320
3321 /// W3-Stage-4 (`mc-r1-oam-dma-reg-window`): one OAM-DMA source read with
3322 /// the 2A03 register window ACTIVE (the halted 6502 address bus is parked
3323 /// in `$4000-$401F`, e.g. the `AccuracyCoin` `APU Register Activation`
3324 /// Test 5/7 `JSR $3FFE` + BRK choreography parks it at `$4001`).
3325 ///
3326 /// Direct port of the `TriCNES` `Fetch` addressBus-window block
3327 /// (`Emulator.cs:9252-9311`):
3328 ///
3329 /// * The normal external decode of `src_addr` runs first (RAM / PPU /
3330 /// cartridge / floating), tracking whether the region DRIVES the data
3331 /// pins (`dataPinsAreNotFloating`).
3332 /// * `Reg == $15` (`$4015` mirror): returns the APU status on the
3333 /// INTERNAL bus — the frame-IRQ flag is cleared (the side effect Test 4
3334 /// brackets from the inactive side), bit 5 comes from the internal-bus
3335 /// latch (Test 7's `$24` = triangle + bit 5 of the previous page-2
3336 /// fetch), and the data bus / open-bus latch is NOT driven ("reading
3337 /// from `$4015` can not affect the databus"). The status value still
3338 /// reaches OAM because the DMA PUT half writes (and drives the bus
3339 /// with) the byte — see [`Self::oam_dma_put`].
3340 /// * `Reg == $16`/`$17` (`$4016`/`$4017` mirrors): the controller shift
3341 /// register is clocked; the value is `bit | (open_bus & $E0)` when the
3342 /// source region floats (Test 5's page-`$50` chain: `$41`, `$40`, then
3343 /// `$01`/`$00` after the `$4015` value decays bit 6 off the bus), but
3344 /// when the source DRIVES the pins the external byte wins the bus
3345 /// conflict and the controller bits are invisible (Test 7's page-`$02`
3346 /// variant — "it does not appear to have read the controllers...
3347 /// but they are still getting clocked").
3348 /// * Everything else: the external fetch value (floating sources return
3349 /// the open-bus latch untouched).
3350 ///
3351 /// The end of the Test-5 chain leaves `$00` on the bus, so the resumed
3352 /// opcode fetch at `$4001` (open bus) executes BRK — the value path is
3353 /// load-bearing for the test's own control flow: any divergence here is
3354 /// what wedged the Stage-3 attempt (runaway execution instead of BRK).
3355 fn oam_dma_read_reg_active(&mut self, src_addr: u16) -> u8 {
3356 // Does the external decode of `src_addr` drive the data pins?
3357 // (TriCNES `dataPinsAreNotFloating` after the normal decode.)
3358 let drives = match src_addr {
3359 // RAM and the PPU registers always drive (write-only PPU regs
3360 // return the PPU-bus latch — still driven).
3361 0x0000..=0x3FFF => true,
3362 // The `$4000-$401F` window itself: the APU drives the INTERNAL
3363 // bus only; the external pins float. (The register overlay
3364 // below is the single decode — skip the external fetch so the
3365 // readable registers don't double-fire.)
3366 0x4000..=0x401F => false,
3367 // Cartridge space: mapper-dependent.
3368 _ => !self.mapper.cpu_read_unmapped(src_addr),
3369 };
3370 let external = if (src_addr & 0xFFE0) == 0x4000 {
3371 self.last_read_addr = src_addr;
3372 self.open_bus
3373 } else {
3374 // Normal external fetch (side effects included — a PPU-register
3375 // source behaves exactly as TriCNES's normal decode does).
3376 // Floating sources early-return the open-bus latch untouched.
3377 self.raw_cpu_read(src_addr)
3378 };
3379 match src_addr & 0x1F {
3380 0x15 => {
3381 // `$4015` mirror: internal-bus read, external bus untouched.
3382 // Mirrors the normal-CPU `$4015` composition in
3383 // `raw_cpu_read` (status bits + internal-bus bit 5).
3384 let status = self.apu.read_status();
3385 (status & 0xDF) | (self.internal_data_bus & 0x20)
3386 }
3387 reg @ (0x16 | 0x17) => {
3388 let port = usize::from(reg - 0x16);
3389 let bit = self.read_port(port);
3390 if drives {
3391 // Bus conflict: the externally-driven byte wins; the
3392 // controller was still clocked (`read_port` above).
3393 external
3394 } else {
3395 let v = (self.open_bus & 0xE0) | bit;
3396 self.open_bus = v;
3397 v
3398 }
3399 }
3400 _ => external,
3401 }
3402 }
3403
3404 /// OAM-DMA PUT half: write the latched byte to OAM.
3405 ///
3406 /// W3-Stage-4 (`mc-r1-oam-dma-reg-window`): when the halted 6502 bus is
3407 /// parked in `$4000-$401F`, the put (a `$2004` write) DRIVES the external
3408 /// data bus with the byte — `TriCNES` `OAMDMA_Put` ->
3409 /// `Store(OAM_InternalBus, 0x2004)`, where every `Store` puts the value
3410 /// on `dataBus`. This is how the `$4015`-mirror value (which cannot drive
3411 /// the bus on its read) reaches the open-bus latch for the NEXT mirror
3412 /// read's `& $E0` merge, and how the Test-5 chain decays to `$00` so the
3413 /// resumed `$4001` fetch executes BRK. On real silicon every OAM put
3414 /// drives the bus; the model is deliberately scoped to the parked-window
3415 /// case so all other OAM DMAs stay byte-identical to the floor.
3416 fn oam_dma_put(&mut self) {
3417 self.ppu.oam_dma_write(self.dma_byte);
3418 if (self.dma_halt_addr & 0xFFE0) == 0x4000 {
3419 self.open_bus = self.dma_byte;
3420 }
3421 }
3422
3423 /// Session-21: set the bus-access tracker for an upcoming DMA cycle.
3424 /// `tick_one_cpu_cycle` consumes this when it pushes the record.
3425 /// No-op (no field even exists) when the trace feature is disabled.
3426 #[cfg(feature = "irq-timing-trace")]
3427 const fn set_trace_dma_access(&mut self, access: BusAccess, addr: u16, data: u8) {
3428 self.trace_bus_access = access;
3429 self.trace_bus_addr = addr;
3430 self.trace_bus_data = data;
3431 }
3432
3433 const fn capture_deferred_dma_replay(&mut self) {
3434 self.deferred_dma_replay_addr = match self.open_bus {
3435 0x02 => 0x2002,
3436 0x07 => 0x2007,
3437 0x15 => 0x4015,
3438 0x16 => 0x4016,
3439 0x17 => 0x4017,
3440 _ => 0,
3441 };
3442 }
3443
3444 /// Re-execute the side-effect of the most recent CPU read for the
3445 /// 2A03 DMC-DMA readout bug. Replays side effects of reads from
3446 /// `$2002`, `$2007`, `$4015`, `$4016` and `$4017`. Per `AccuracyCoin`
3447 /// "APU Registers and DMA tests" — sub-tests check that the DMC
3448 /// DMA halt cycles re-trigger the cached read's side effects on
3449 /// real silicon.
3450 fn replay_dma_noop_read(&mut self, addr: u16) {
3451 if matches!(self.apu_region(), ApuRegion::Pal) {
3452 return;
3453 }
3454 match addr {
3455 0x2002 => {
3456 let mut adapter = PpuBusAdapter {
3457 mapper: self.mapper.as_mut(),
3458 nt_override: self.nt_mirroring_override,
3459 sub_dot: 2,
3460 #[cfg(feature = "irq-timing-trace")]
3461 trace_a12_latest: None,
3462 };
3463 let _ = self.ppu.cpu_read_register(2, &mut adapter);
3464 }
3465 0x2007 => {
3466 let mut adapter = PpuBusAdapter {
3467 mapper: self.mapper.as_mut(),
3468 nt_override: self.nt_mirroring_override,
3469 // CPU register replay (e.g. $2007 read-bug): treated as
3470 // M2-high (sub_dot 2) since the 6502 drives its bus
3471 // during φ2.
3472 sub_dot: 2,
3473 #[cfg(feature = "irq-timing-trace")]
3474 trace_a12_latest: None,
3475 };
3476 let _ = self.ppu.cpu_read_register(7, &mut adapter);
3477 }
3478 0x4015 => {
3479 let _ = self.apu.read_status();
3480 self.apu.clear_frame_irq_immediate_for_dma();
3481 }
3482 0x4016 => {
3483 let _ = self.controllers[0].read();
3484 }
3485 0x4017 => {
3486 let _ = self.controllers[1].read();
3487 }
3488 _ => {}
3489 }
3490 }
3491
3492 /// Read the DMC sample byte and model the 2A03 register-conflict path
3493 /// where 6502 core address bits 15..=5 remain from the halted CPU read
3494 /// while DMA supplies address bits 4..=0.
3495 fn dmc_dma_read(&mut self, addr: u16, halted_addr: u16) -> u8 {
3496 let sample = self.raw_cpu_read(addr);
3497 if matches!(self.apu_region(), ApuRegion::Pal) || (halted_addr & 0xFFE0) != 0x4000 {
3498 return sample;
3499 }
3500
3501 let conflict_addr = 0x4000 | (addr & 0x001F);
3502 match conflict_addr {
3503 0x4015 => {
3504 let _ = self.apu.read_status();
3505 sample
3506 }
3507 0x4016 => {
3508 // Keep the DMC-conflict $4016 composition consistent with the
3509 // normal controller read (line ~3890): D2 carries the Famicom
3510 // built-in microphone. Default-off (mic released) leaves `mic`
3511 // = 0, so the returned byte is byte-identical to prior releases.
3512 let mic = u8::from(self.famicom_mic) << 2;
3513 let v = (sample & 0xE0) | self.controllers[0].read() | mic;
3514 self.open_bus = v;
3515 v
3516 }
3517 0x4017 => {
3518 let v = (sample & 0xE0) | self.controllers[1].read();
3519 self.open_bus = v;
3520 v
3521 }
3522 _ => sample,
3523 }
3524 }
3525
3526 /// v2.0 interleaved-DMA Phase B: perform ONE cycle of an interleaved DMC
3527 /// DMA (`TriCNES` `_6502` DMC-only path: `DMCDMA_Halted`/`Put`/`Get`). Called
3528 /// once per R1 cycle from `Cpu::read1` while `apu.dmc_dma_pending()`, at the
3529 /// access-point of the cycle (after `start_cycle`, before `end_cycle`). The
3530 /// CPU drives the cycle timing; this does only the DMA bus access + advances
3531 /// the halt/get state. The GET always lands on a get cycle (`!put_cycle`),
3532 /// so the 3-vs-4-cycle span is EMERGENT from the `put_cycle` parity at arm
3533 /// time (divergence-A self-consistency), not main's fixed `short?2:3`.
3534 #[allow(clippy::too_many_lines)]
3535 fn dmc_dma_step_impl(&mut self, halted_addr: u16) {
3536 if !self.in_dmc_dma {
3537 // First cycle of this DMA span: latch halt + the open-bus replay.
3538 self.in_dmc_dma = true;
3539 self.dmc_halt = true;
3540 self.capture_deferred_dma_replay();
3541 }
3542 // get = read cycle (TriCNES `!APU_PutCycle`); put = write cycle.
3543 let get_cycle = !self.apu.put_cycle();
3544 if get_cycle && !self.dmc_halt {
3545 // The GET: fetch the sample (with the `$4000` open-bus conflict the
3546 // DMA cluster brackets) + deliver to the DMC.
3547 let addr = self.apu.dmc_dma_addr();
3548 let byte = self.dmc_dma_read(addr, halted_addr);
3549 #[cfg(feature = "irq-timing-trace")]
3550 self.set_trace_dma_access(BusAccess::DmaRead, addr, byte);
3551 // v1.4.0 Workstream D (D2) — DMC-DMA event-breakpoint tap (the GET
3552 // cycle that fetches a sample). Output-only.
3553 #[cfg(feature = "debug-hooks")]
3554 self.record_event_break(EventBpKind::DmcDma, addr);
3555 self.apu.complete_dmc_dma(byte);
3556 self.in_dmc_dma = false;
3557 // Program M (M-2): this step performed the GET (steals an OAM slot).
3558 {
3559 self.dmc_step_was_get = true;
3560 }
3561 } else {
3562 // Program M (M-2): this step was a halt/dummy/align (overlaps OAM).
3563 {
3564 self.dmc_step_was_get = false;
3565 }
3566 // Halt / alignment / put cycle: re-read the halted CPU address bus
3567 // (TriCNES `Fetch(addressBus)`).
3568 self.replay_dma_noop_read(halted_addr);
3569 // Tag the halt re-read so the trace shows the DMC DMA's $4015
3570 // (etc.) re-read landing — the side-effect cycle the $4015
3571 // frame-IRQ-clear diagnostic correlates against.
3572 #[cfg(feature = "irq-timing-trace")]
3573 self.set_trace_dma_access(BusAccess::DmaRead, halted_addr, self.open_bus);
3574 if get_cycle {
3575 // A get cycle clears the halt ("halts clear after a get cycle").
3576 self.dmc_halt = false;
3577 }
3578 }
3579 }
3580
3581 /// W3-Stage-1 (`mc-r1-dma-unified`): clear the unified engine's transient
3582 /// OAM-DMA state (reset / power-cycle / snapshot-restore).
3583 const fn unified_dma_clear(&mut self) {
3584 self.uni_oam_active = false;
3585 self.uni_oam_halt = false;
3586 self.uni_oam_aligned = false;
3587 self.uni_oam_addr = 0;
3588 }
3589
3590 /// W3-Stage-1 (`mc-r1-dma-unified`): ONE cycle of the unified DMC/OAM DMA
3591 /// engine — a direct port of the `TriCNES` `_6502` per-cycle DMA dispatch
3592 /// table (`crates/rustynes-test-harness/golden/tricnes/tricnes-harness-src/
3593 /// Emulator.cs` ~4233-4357), the SINGLE driver that standalone DMC,
3594 /// standalone OAM, and the DMC-during-OAM overlap all ride — AT FLOOR
3595 /// PARITY for this stage (the structural-equivalence proof; Stage 2 flips
3596 /// the one engine to the breakthrough parity).
3597 ///
3598 /// Floor-parity mapping (the structural truth Stage 2 collapses): the
3599 /// floor's two drivers run on OPPOSITE halves of the shared cycle counter
3600 /// (`put_cycle == (self.cycle & 1 == 0)` at the access point):
3601 ///
3602 /// * the DMC engine's GET half is `!put_cycle` (ODD bus cycles) — the
3603 /// emergent `dmc_dma_step_impl` span (halt latched on entry, cleared at
3604 /// the end of the first odd cycle, GET on the next odd) is preserved
3605 /// exactly: entry-on-even = span 4, entry-on-odd = span 3;
3606 /// * the OAM engine's READ half is `put_cycle` (EVEN bus cycles) — the
3607 /// floor's `oam_dma_step` latches 514 (halt + align + 512) when its
3608 /// first serviced cycle is even (`self.cycle & 1 == 0`) and its reads
3609 /// always land on even cycles; the emergent `uni_oam_halt` (`TriCNES`
3610 /// `OAMDMA_Halt`, set only when the first serviced cycle is the read
3611 /// half) reproduces the same 514/513 split with no owed-cycle counter.
3612 ///
3613 /// Each engine's halt clears at the end of ITS OWN get half — `TriCNES`
3614 /// "both halt cycles get cleared after a get cycle", split across the
3615 /// floor's two parities (Stage 2 merges them onto one). The post-GET
3616 /// realign is EMERGENT: a DMC GET stalls OAM for the slot AND forces
3617 /// `uni_oam_aligned = false` (`TriCNES` `DMCDMA_Get` ->
3618 /// `OAMDMA_Aligned = false`), so the in-flight byte is re-read.
3619 ///
3620 /// ONE bus slot per cycle. When a halted DMC overlaps an advancing OAM
3621 /// cycle, the held CPU read's side-effect replay still fires alongside —
3622 /// the lockstep `service_dmc_dma_during_oam` noop-body model (the in-tree
3623 /// overlap spec that passes the whole abort cluster on the default build).
3624 #[allow(clippy::too_many_lines)] // the cfg-split floor + merged dispatches
3625 fn unified_dma_cycle_impl(&mut self, halted_addr: u16) {
3626 // Cycle-half label at the access point (post `cpu_clock`, the APU
3627 // counter has flipped): even bus cycle == `put_cycle` at floor parity.
3628 // W3-Stage-2 (`mc-r1-dma-unified-collapse`): under the put_cycle
3629 // END-flip (the counter-collapse breakthrough parity) the access-point
3630 // read is the references' in-cycle `APU_PutCycle` label DIRECTLY —
3631 // TriCNES also flips at end-of-cycle — so the dispatch runs the single
3632 // TriCNES labeling: `get = !APU_PutCycle`. The floor's split halves
3633 // (DMC GET = odd / OAM READ = even) merge onto this one label.
3634 let get = !self.apu.put_cycle();
3635
3636 // The two activation-time roles, derived per parity model:
3637 // * `oam_halt_on_first` — TriCNES `FirstCycleOfOAMDMA`: halt when the
3638 // first serviced cycle lands on the OAM READ half (floor: even; the
3639 // merged labeling: the GET half). Half-swap x parity-flip = the SAME
3640 // absolute cycles, so standalone OAM timing is invariant.
3641 // * `dmc_noop_half` — the half a LOAD may not ENTER on (the span-3
3642 // load-get-entry rule: a load enters on its get half).
3643 let (oam_halt_on_first, dmc_noop_half) = (get, !get);
3644
3645 // --- OAM activation (TriCNES `$4014` -> FirstCycleOfOAMDMA) ---
3646 // The first serviced cycle after the `$4014` write latches the page +
3647 // the parked CPU address; `uni_oam_halt` is set only when this first
3648 // cycle lands on the OAM read half (floor: even -> the 514 case).
3649 // Latching here, regardless of any in-flight DMC, natively absorbs the
3650 // Stage-0 `$4014`-write-to-first-OAM-cycle gap (lockstep `drain_dma`
3651 // latches OAM BEFORE its DMC-pending check).
3652 if let Some(page) = self.dma_pending.take() {
3653 self.dma_page = page;
3654 self.uni_oam_addr = 0;
3655 self.uni_oam_aligned = false;
3656 self.uni_oam_active = true;
3657 self.uni_oam_halt = oam_halt_on_first;
3658 self.dma_halt_addr = halted_addr;
3659 }
3660
3661 // --- DMC activation (the floor `dmc_dma_step_impl` first-cycle latch)
3662 // A LOAD may not ENTER on the DMC noop half: the floor's
3663 // `dmc_dma_defer_load_entry` while-gate defers exactly the entries
3664 // whose access-point parity is the noop half, so a load enters on its
3665 // get half = span 3 (`mc-r1-dmc-load-get-entry`). The same defer is
3666 // re-derived here for cycles the loop runs anyway because OAM is
3667 // active.
3668 // W3-Stage-3 (`mc-r1-dmc-delayed-4015`): a pending DMC whose APPLIED
3669 // status is false may not ACTIVATE either (the loop can still be
3670 // running for an active OAM; TriCNES's stale `DoDMCDMA` similarly
3671 // never re-enters the halt-latch path — `DMCDMA_Halt` was latched at
3672 // the original activation).
3673 let dmc_serviceable = self.apu.dmc_dma_serviceable();
3674 if self.apu.dmc_dma_pending() && dmc_serviceable && !self.in_dmc_dma {
3675 let defer_load = self.apu.dmc_dma_is_load() && dmc_noop_half;
3676 if !defer_load {
3677 self.in_dmc_dma = true;
3678 self.dmc_halt = true;
3679 self.capture_deferred_dma_replay();
3680 }
3681 }
3682
3683 // --- Dispatch: ONE bus slot per cycle (floor parity: split halves) ---
3684
3685 // --- Dispatch: ONE bus slot per cycle (W3-Stage-2: the references'
3686 // single get/put labeling — the literal TriCNES `_6502` table) ---
3687 if get {
3688 // GET half: DMC GET (priority) > OAM READ > halted reads.
3689 if self.in_dmc_dma && !self.dmc_halt {
3690 // THE DMC GET: owns the bus slot (with the `$4000` open-bus
3691 // conflict the DMA cluster brackets); a sharing OAM is STALLED
3692 // for the slot AND loses alignment (TriCNES `DMCDMA_Get` ->
3693 // `OAMDMA_Aligned = false`, the emergent post-GET realign).
3694 let addr = self.apu.dmc_dma_addr();
3695 let byte = self.dmc_dma_read(addr, halted_addr);
3696 #[cfg(feature = "irq-timing-trace")]
3697 self.set_trace_dma_access(BusAccess::DmaRead, addr, byte);
3698 self.apu.complete_dmc_dma(byte);
3699 self.in_dmc_dma = false;
3700 if self.uni_oam_active {
3701 self.uni_oam_aligned = false;
3702 }
3703 } else if self.uni_oam_active && !self.uni_oam_halt {
3704 // A halted DMC shares this OAM-READ cycle: on `Rp2A03G` the held
3705 // CPU read's side-effect replay fires first (lockstep noop-body
3706 // order: `replay_dma_noop_read` THEN the OAM slot). This extra
3707 // parked-address re-read — a *halted* DMC squeezing a side-effect
3708 // into an OAM-owned read cycle — is v2.1.7's "unexpected DMA"
3709 // extra read, and it is revision-gated: `Rp2A03G` (default)
3710 // performs it, `Rp2A03H` OMITS it (opt-in later-die model —
3711 // unverified direction; see ADR 0033). Suppression is
3712 // deterministic and cannot desync the transfer:
3713 // `replay_dma_noop_read` only re-triggers a *register's*
3714 // side-effect (a `$2007` buffer advance / `$4016`-`$4017` shift /
3715 // `$4015` IRQ-clear); it ticks no time and advances no DMA
3716 // counter, so the OAM/DMC data path and cycle length are
3717 // identical on both arms.
3718 //
3719 // HONEST RESIDUAL (ADR 0033): on this ported engine the branch
3720 // FIRES (measured ~75× in a synthetic DMC+OAM+`$2007`-loop probe)
3721 // but `replay_dma_noop_read(halted_addr)` is a no-op every time,
3722 // because `halted_addr` during a DMC+OAM overlap is always the
3723 // post-`$4014` *instruction fetch* in PRG (OAM DMA drains on the
3724 // next opcode read, not on a register operand read), never a
3725 // `$2002/$2007/$4015/$4016/$4017` address. So `Rp2A03G` and
3726 // `Rp2A03H` are, in practice, byte-identical on every public
3727 // oracle and every constructible scenario — the die-revision
3728 // extra read is unobservable here, not merely unverified. The
3729 // gate is kept at its mechanism-correct location so it becomes
3730 // live immediately if the parked-address model ever exposes a
3731 // register during the overlap; it never perturbs the default
3732 // (`Rp2A03G`) path.
3733 if self.in_dmc_dma && self.cpu_2a03_revision.has_unexpected_dma_extra_read() {
3734 self.replay_dma_noop_read(halted_addr);
3735 }
3736 // OAM GET: the OAM engine owns the bus slot.
3737 let src = (u16::from(self.dma_page) << 8) | self.uni_oam_addr;
3738 self.dma_byte = self.raw_oam_dma_read(src);
3739 self.uni_oam_aligned = true;
3740 #[cfg(feature = "irq-timing-trace")]
3741 self.set_trace_dma_access(BusAccess::DmaRead, src, self.dma_byte);
3742 } else if self.in_dmc_dma {
3743 // DMC halted get: re-read the parked CPU address (TriCNES
3744 // `Fetch(addressBus)`). Covers the both-halted shared cycle
3745 // too (ONE re-read — TriCNES `DMCDMA_Halted`).
3746 self.replay_dma_noop_read(halted_addr);
3747 #[cfg(feature = "irq-timing-trace")]
3748 self.set_trace_dma_access(BusAccess::DmaRead, halted_addr, self.open_bus);
3749 } else {
3750 // OAM halt cycle alone: the parked address stays on the bus
3751 // (the floor `oam_dma_step` halt branch — no side-effect
3752 // replay).
3753 #[cfg(feature = "irq-timing-trace")]
3754 self.set_trace_dma_access(BusAccess::DmaRead, self.dma_halt_addr, self.open_bus);
3755 }
3756 // TriCNES: BOTH halt cycles get cleared after a get cycle.
3757 self.dmc_halt = false;
3758 self.uni_oam_halt = false;
3759 } else {
3760 // PUT half: OAM WRITE/align; a waiting/halted DMC replays the held
3761 // CPU read's side-effect alongside (TriCNES `DMCDMA_Put` /
3762 // `DMCDMA_Halted` — both `Fetch(addressBus)`).
3763 if self.in_dmc_dma {
3764 self.replay_dma_noop_read(halted_addr);
3765 #[cfg(feature = "irq-timing-trace")]
3766 self.set_trace_dma_access(BusAccess::DmaRead, halted_addr, self.open_bus);
3767 }
3768 if self.uni_oam_active && !self.uni_oam_halt {
3769 if self.uni_oam_aligned {
3770 // OAM PUT: write the latched byte to OAM ($2004).
3771 // `uni_oam_aligned` stays set through the transfer
3772 // (TriCNES: only `DMCDMA_Get` and completion clear it).
3773 self.oam_dma_put();
3774 #[cfg(feature = "irq-timing-trace")]
3775 self.set_trace_dma_access(BusAccess::DmaWrite, 0x2004, self.dma_byte);
3776 self.uni_oam_addr += 1;
3777 if self.uni_oam_addr == 256 {
3778 // The DMA completes on the 256th write.
3779 self.uni_oam_active = false;
3780 self.uni_oam_aligned = false;
3781 }
3782 } else {
3783 // OAM alignment dummy: the parked address stays on the
3784 // bus (the floor `oam_dma_step` align branch — no
3785 // side-effect replay).
3786 #[cfg(feature = "irq-timing-trace")]
3787 if !self.in_dmc_dma {
3788 self.set_trace_dma_access(
3789 BusAccess::DmaRead,
3790 self.dma_halt_addr,
3791 self.open_bus,
3792 );
3793 }
3794 }
3795 }
3796 // (An OAM halt can never land on the PUT half under the merged
3797 // labeling — `uni_oam_halt` is set only on a GET first cycle and
3798 // clears at the end of that same GET half.)
3799 }
3800 }
3801
3802 /// Raw CPU read that does **not** advance time — used by the OAM DMA
3803 /// engine and DMC DMA fetches. Time was already advanced by the
3804 /// surrounding `tick_one_cpu_cycle` (or the DMA stall).
3805 pub(crate) fn raw_cpu_read(&mut self, addr: u16) -> u8 {
3806 // $4015 special case: reading from the APU status port reads
3807 // 2A03 internal state but does NOT drive the data bus (per
3808 // nesdev "Open bus behavior" + AccuracyCoin `CPU Behavior ::
3809 // Open Bus` Test 7). The CPU still receives the APU status,
3810 // but the open-bus latch stays at its prior value, so a
3811 // subsequent open-bus-region read returns the *previous*
3812 // floating-bus value rather than the APU status.
3813 if addr == 0x4015 {
3814 // $4015 read returns the APU status (internal silicon
3815 // state) and does NOT drive the external data bus, so
3816 // `self.open_bus` stays at its prior value (per nesdev
3817 // "Open bus behavior" + AccuracyCoin `CPU Behavior ::
3818 // Open Bus` Test 7).
3819 //
3820 // Bit 5 of $4015 is documented as open-bus on silicon.
3821 // With the Phase 1a internal-vs-external bus split, we
3822 // expose this from the INTERNAL data bus (CPU-only, NOT
3823 // polluted by DMC DMA fetches). This satisfies BOTH:
3824 // * Open Bus Test 9 — bit 5 returns the bus latch value
3825 // * Internal Data Bus Test 2 — DMC DMA does NOT change
3826 // bit 5 because DMC drives only the external bus.
3827 //
3828 // The pre-2026-05-23 conflated `open_bus` model could
3829 // not honour both tests simultaneously: empirically (per
3830 // CLAUDE.md Phase D3 audit), OR-ing `open_bus & 0x20`
3831 // into the read flipped Test 9 PASS but tripped Test 2
3832 // to FAIL — net-zero swap. With the internal-bus
3833 // separation, the trade-off is resolved.
3834 let status = self.apu.read_status();
3835 let v = (status & 0xDF) | (self.internal_data_bus & 0x20);
3836 self.last_read_addr = addr;
3837 return v;
3838 }
3839 let v = match addr {
3840 0x0000..=0x1FFF => self.ram[(addr & 0x07FF) as usize],
3841 0x2000..=0x3FFF => self.ppu_register_read(addr),
3842 0x4000..=0x4014 | 0x4018..=0x401F => self.open_bus,
3843 0x4015 => unreachable!("handled above"),
3844 // Controllers drive D0 (and D1 on Famicom expansion port,
3845 // unused here). Bits 5-7 are open bus — the bus latch's
3846 // upper 3 bits show through. Bit 4 is the secondary
3847 // controller D1 (also open bus on stock NES). Per nesdev
3848 // "Standard controller" + AccuracyCoin `CPU Behavior ::
3849 // Open Bus` Test 6.
3850 0x4016 => {
3851 let mic = u8::from(self.famicom_mic) << 2;
3852 let base = (self.open_bus & 0xE0) | self.read_port(0) | mic;
3853 self.vs_overlay_4016(base)
3854 }
3855 0x4017 => {
3856 let base = (self.open_bus & 0xE0) | self.read_port(1);
3857 self.vs_overlay_4017(base)
3858 }
3859 0x4020..=0xFFFF => {
3860 if self.mapper.cpu_read_unmapped(addr) {
3861 // Unmapped read: bus stays at the floating-latch
3862 // value (per nesdev "Open bus behavior"). Don't
3863 // overwrite `open_bus` — return early.
3864 self.last_read_addr = addr;
3865 return self.open_bus;
3866 }
3867 // The Game Genie physically substitutes the byte on the
3868 // cartridge bus, so the (possibly substituted) value is what
3869 // the CPU sees AND what latches onto `open_bus` below.
3870 let raw = self.mapper.cpu_read(addr);
3871 self.apply_genie(addr, raw)
3872 }
3873 };
3874 self.last_read_addr = addr;
3875 self.open_bus = v;
3876 // Mirror the read onto the internal data bus, but ONLY when
3877 // this is a CPU-initiated access. DMC DMA fetches drive
3878 // only the EXTERNAL (`open_bus`) bus per nesdev's two-bus
3879 // 2A03 model and per AccuracyCoin's `CPU Behavior 2 ::
3880 // Internal Data Bus` Test 2 ("This DMC DMA does not update
3881 // the external data bus. Only the internal one." — the
3882 // upstream comment treats "internal" as the OPPOSITE of
3883 // what we call internal here; per the test sequence the
3884 // INTERNAL_data_bus is what `$4015` bit-5 returns, and DMC
3885 // DMA must NOT pollute it). The `in_dmc_dma` guard is set
3886 // by `service_dmc_dma` before invoking `dmc_dma_read` →
3887 // `raw_cpu_read`; we skip the internal-bus mirror in that
3888 // path so the internal latch retains its prior CPU-driven
3889 // value across DMC halts. Phase 1 of `linked-puzzling-sutherland`.
3890 if !self.in_dmc_dma {
3891 self.internal_data_bus = v;
3892 }
3893 v
3894 }
3895
3896 /// PPU register read with side effects.
3897 fn ppu_register_read(&mut self, addr: u16) -> u8 {
3898 let reg = (addr & 7) as u8;
3899 let mut adapter = PpuBusAdapter {
3900 mapper: self.mapper.as_mut(),
3901 nt_override: self.nt_mirroring_override,
3902 // CPU bus access happens during φ2 → sub_dot 2 (M2-high).
3903 sub_dot: 2,
3904 #[cfg(feature = "irq-timing-trace")]
3905 trace_a12_latest: None,
3906 };
3907 self.ppu.cpu_read_register(reg, &mut adapter)
3908 }
3909
3910 /// PPU register write with side effects.
3911 fn ppu_register_write(&mut self, addr: u16, value: u8) {
3912 let reg = (addr & 7) as u8;
3913 let mut adapter = PpuBusAdapter {
3914 mapper: self.mapper.as_mut(),
3915 nt_override: self.nt_mirroring_override,
3916 // CPU bus access happens during φ2 → sub_dot 2 (M2-high).
3917 sub_dot: 2,
3918 #[cfg(feature = "irq-timing-trace")]
3919 trace_a12_latest: None,
3920 };
3921 self.ppu.cpu_write_register(reg, value, &mut adapter);
3922 }
3923}
3924
3925/// v1.1.0 beta.1 (T-110-B4) — translate a `$2000-$3EFF` PPU address to a
3926/// CIRAM offset under an explicit mirroring (the per-game override path),
3927/// mirroring the `Mapper::nametable_address` default impl.
3928#[allow(clippy::cast_possible_truncation)] // physical_bank is always 0 or 1.
3929const fn override_nt_addr(m: rustynes_mappers::Mirroring, addr: u16) -> u16 {
3930 const NT: u16 = 0x0400;
3931 let table = ((addr.wrapping_sub(0x2000)) / NT) & 0x03;
3932 let local = addr & (NT - 1);
3933 (m.physical_bank(table as u8) as u16) * NT + local
3934}
3935
3936/// Adapter that exposes the [`PpuBus`] interface over a `&mut dyn Mapper`.
3937struct PpuBusAdapter<'a> {
3938 mapper: &'a mut dyn Mapper,
3939 /// v1.1.0 beta.1 (T-110-B4) — the bus's per-game mirroring override, copied
3940 /// in at construction. When `Some`, `nametable_address` uses it instead of
3941 /// the mapper's mirroring.
3942 nt_override: Option<rustynes_mappers::Mirroring>,
3943 /// Current PPU sub-dot of the host CPU cycle (0, 1, or 2). Set by
3944 /// the bus's tick loop before each `Ppu::tick` call so that
3945 /// `notify_a12_at_sub_dot` (C1 step B4-successor M2-phase plumbing)
3946 /// can forward the sub-dot to the mapper for cycle-precise IRQ
3947 /// propagation modeling. Sub-dots 0 / 1 are M2-low (φ1) and 2 is
3948 /// M2-high (φ2) per our convention.
3949 sub_dot: u8,
3950 /// When the IRQ-timing trace feature is enabled, the most recent A12
3951 /// level passed through `notify_a12` is mirrored here so the bus's
3952 /// per-sub-dot trace loop can pick it up. `None` when tracing is
3953 /// off (the standard hot path).
3954 #[cfg(feature = "irq-timing-trace")]
3955 trace_a12_latest: Option<&'a mut Option<bool>>,
3956}
3957
3958impl PpuBus for PpuBusAdapter<'_> {
3959 fn ppu_read(&mut self, addr: u16) -> u8 {
3960 self.mapper.ppu_read(addr & 0x1FFF)
3961 }
3962 fn ppu_read_sprite(&mut self, addr: u16) -> u8 {
3963 self.mapper.ppu_read_sprite(addr & 0x1FFF)
3964 }
3965 fn chr_phys(&self, addr: u16) -> Option<u32> {
3966 self.mapper.chr_phys(addr & 0x1FFF)
3967 }
3968 fn ppu_write(&mut self, addr: u16, value: u8) {
3969 self.mapper.ppu_write(addr & 0x1FFF, value);
3970 }
3971 fn peek_nametable(&mut self, addr: u16) -> Option<u8> {
3972 self.mapper.nametable_fetch(addr)
3973 }
3974 fn write_nametable(&mut self, addr: u16, value: u8) -> bool {
3975 self.mapper.nametable_write(addr, value)
3976 }
3977 fn peek_ex_attribute(&mut self, v: u16) -> Option<PpuExAttribute> {
3978 self.mapper.peek_ex_attribute(v).map(|ex| PpuExAttribute {
3979 palette: ex.palette,
3980 chr_bank: ex.chr_bank,
3981 })
3982 }
3983 fn bg_split_state(&mut self, scanline_y: u16, coarse_x: u16) -> Option<PpuBgSplitState> {
3984 self.mapper
3985 .bg_split_state(scanline_y, coarse_x)
3986 .map(|s| PpuBgSplitState {
3987 nt_addr: s.nt_addr,
3988 at_addr: s.at_addr,
3989 fine_y: s.fine_y,
3990 chr_bank: s.chr_bank,
3991 })
3992 }
3993 fn notify_a12(&mut self, level: bool) {
3994 // C1 step B4 successor: forward the current sub-dot to the
3995 // mapper so MMC3 can apply the M2-phase-aware IRQ-output
3996 // propagation delay required by `mmc3_test_2/4-scanline_timing`
3997 // sub-test #3. Non-MMC3 mappers' default
3998 // `notify_a12_at_sub_dot` impl falls back to plain `notify_a12`,
3999 // so this thread-through is invisible to NROM / UxROM / etc.
4000 self.mapper.notify_a12_at_sub_dot(level, self.sub_dot);
4001 #[cfg(feature = "irq-timing-trace")]
4002 if let Some(slot) = self.trace_a12_latest.as_deref_mut() {
4003 *slot = Some(level);
4004 }
4005 }
4006 fn notify_scanline_start(&mut self) {
4007 self.mapper.notify_scanline_start();
4008 }
4009 fn notify_vblank(&mut self) {
4010 self.mapper.notify_vblank();
4011 }
4012 fn nametable_address(&self, addr: u16) -> u16 {
4013 resolve_nt_addr(self.nt_override, &*self.mapper, addr)
4014 }
4015}
4016
4017/// Resolve a nametable address to a physical CIRAM offset, honouring the
4018/// per-game mirroring override when one is set.
4019///
4020/// Factored out of [`PpuBusAdapter::nametable_address`] (v2.3.2 "Lucid") so
4021/// [`LockstepBus::resolve_nametable_address`] can answer the same question
4022/// without constructing an adapter. One definition, so the fetch path and the
4023/// provenance panel cannot drift apart on a board with an override.
4024fn resolve_nt_addr(
4025 nt_override: Option<rustynes_mappers::Mirroring>,
4026 mapper: &dyn Mapper,
4027 addr: u16,
4028) -> u16 {
4029 nt_override.map_or_else(
4030 || mapper.nametable_address(addr),
4031 |m| override_nt_addr(m, addr),
4032 )
4033}
4034
4035impl LockstepBus {
4036 /// Read-only nametable-address resolution for the pixel-provenance panel.
4037 ///
4038 /// Shares [`resolve_nt_addr`] with the PPU's own fetch path, so a board with
4039 /// a per-game mirroring override reports the offset its fetches really use.
4040 #[cfg(feature = "debug-hooks")]
4041 pub(crate) fn resolve_nametable_address(&self, addr: u16) -> u16 {
4042 resolve_nt_addr(self.nt_mirroring_override, &*self.mapper, addr)
4043 }
4044}
4045
4046/// v2.0 master-clock R1 substrate helpers (Phase 1). Compiled only under
4047/// `mc-r1-substrate`; used by the clean `Bus` contract overrides below.
4048impl LockstepBus {
4049 /// `(cpu_divider, ppu_divider)` in master clocks for the cartridge region
4050 /// (NTSC 12/4, PAL 16/5, Dendy 15/5). Drives the R1 `run_ppu_to` dot loop.
4051 /// Reads the values cached at construction (region is immutable after
4052 /// parse), so the hot R1 paths avoid a per-cycle `match`.
4053 const fn region_dividers(&self) -> (u8, u8) {
4054 (self.cpu_div_cached, self.ppu_div_cached)
4055 }
4056
4057 /// Tick the APU + frame counter once and fan frame events out to on-cart
4058 /// audio (the per-CPU-cycle APU advance extracted from
4059 /// `tick_one_cpu_cycle`, for the R1 `cpu_clock`).
4060 ///
4061 /// v2.8.0 Phase 4 — the mapper dispatches are gated on the cached
4062 /// capability flags: boards without on-cart audio would return 0 from
4063 /// the default `mix_audio` (0.0 after the f32 conversion — identical),
4064 /// and boards without the frame hook have the default no-op. Skipping
4065 /// both saves two virtual calls + an f32 divide per CPU cycle.
4066 fn apu_advance_one(&mut self) {
4067 #[allow(clippy::cast_precision_loss)] // see `mix_audio`'s call site above
4068 let mapper_sample = if self.mapper_caps.audio {
4069 self.mapper.mix_audio() as f32 / 65536.0
4070 } else {
4071 0.0
4072 };
4073 // v2.0.0 beta.1 (A1 one-clock collapse): hand the APU the canonical
4074 // bus cycle counter (incremented earlier in this same `cpu_clock`)
4075 // instead of letting it keep an independent `+= 1` mirror (the
4076 // one-clock collapse, promoted to the only path in v2.0.0 beta.4).
4077 self.apu.set_canonical_cycle(self.cycle);
4078 self.apu.tick_with_external(mapper_sample);
4079 if self.mapper_caps.frame_event_hook {
4080 let ev = self.apu.last_frame_events();
4081 self.mapper.notify_frame_event(MapperFrameEvents {
4082 quarter: ev.quarter,
4083 half: ev.half,
4084 });
4085 }
4086 }
4087}
4088
4089impl Bus for LockstepBus {
4090 fn cpu_read(&mut self, addr: u16) -> u8 {
4091 if self.deferred_dma_replay_addr != 0
4092 && self.open_bus == (self.deferred_dma_replay_addr >> 8) as u8
4093 {
4094 if self.deferred_dma_replay_addr == addr {
4095 self.replay_dma_noop_read(addr);
4096 }
4097 self.deferred_dma_replay_addr = 0;
4098 }
4099 let value = self.raw_cpu_read(addr);
4100 // v1.1.0 beta.3 (T-110-E2) — Lua onRead access tap. Output-only, gated.
4101 #[cfg(feature = "debug-hooks")]
4102 if self.access_logging && self.accesses.len() < ACCESS_CAP {
4103 self.accesses.push(AccessRec {
4104 write: false,
4105 addr,
4106 value,
4107 });
4108 }
4109 // v1.5.0 Workstream A2 — event-viewer read tap: the graphical PPU Event
4110 // Viewer needs PPU-register READS (`$2002` status polls, `$2007` data
4111 // fetches) plotted alongside writes. Only the `$2000-$3FFF` PPU window is
4112 // captured (the dense APU/RAM/PRG read stream would swamp the timeline);
4113 // writes across PPU/APU/mapper are captured in `cpu_write`. Output-only,
4114 // gated, bounded by `EVENT_CAP` — determinism-neutral.
4115 #[cfg(feature = "debug-hooks")]
4116 if self.event_logging && matches!(addr, 0x2000..=0x3FFF) && self.events.len() < EVENT_CAP {
4117 self.events.push(EventRec {
4118 kind: EventKind::PpuRead,
4119 scanline: self.ppu.scanline(),
4120 dot: self.ppu.dot(),
4121 addr,
4122 value,
4123 });
4124 }
4125 // v1.4.0 Workstream D (D2) — event-breakpoint read taps. Output-only.
4126 // The `mask == 0` early-out in `record_event_break` keeps the default
4127 // path cheap; the sprite-0-hit category is observed where games detect
4128 // it: a `$2002` read returning bit 6 set.
4129 #[cfg(feature = "debug-hooks")]
4130 if self.event_bp_mask != 0 {
4131 match addr {
4132 0x2002 if value & 0x40 != 0 => {
4133 self.record_event_break(EventBpKind::Sprite0Hit, addr);
4134 }
4135 0x2000..=0x3FFF => self.record_event_break(EventBpKind::PpuRead, addr),
4136 0x4000..=0x4017 => self.record_event_break(EventBpKind::ApuRead, addr),
4137 0x4020..=0xFFFF => self.record_event_break(EventBpKind::MapperRead, addr),
4138 _ => {}
4139 }
4140 }
4141 #[cfg(feature = "irq-timing-trace")]
4142 {
4143 // Session-21: record the CPU-initiated read at the bus-access
4144 // tracker. `tick_one_cpu_cycle` was already called by the
4145 // CPU's `read1`/`idle_tick` path (post `bus.on_cpu_cycle()`),
4146 // but the order in `Cpu::read1` is `bus.cpu_read(addr)` then
4147 // `idle_tick(bus)` → `bus.on_cpu_cycle()` → record-push.
4148 // So writing the tracker here populates the record that the
4149 // about-to-fire `tick_one_cpu_cycle` will consume.
4150 self.trace_bus_access = BusAccess::Read;
4151 self.trace_bus_addr = addr;
4152 self.trace_bus_data = value;
4153 }
4154 value
4155 }
4156
4157 fn cpu_write(&mut self, addr: u16, value: u8) {
4158 self.open_bus = value;
4159 // Mirror the CPU-initiated write onto the internal data bus.
4160 // Symmetric with `raw_cpu_read`'s mirror — DMC DMA does not
4161 // perform writes, so internal-vs-external divergence only
4162 // arises across DMC read halts. (No `in_dmc_dma` guard
4163 // here because DMC DMA never invokes `cpu_write`.)
4164 self.internal_data_bus = value;
4165 // v1.1.0 beta.3 (T-110-E2) — Lua onWrite access tap. Output-only, gated.
4166 #[cfg(feature = "debug-hooks")]
4167 if self.access_logging && self.accesses.len() < ACCESS_CAP {
4168 self.accesses.push(AccessRec {
4169 write: true,
4170 addr,
4171 value,
4172 });
4173 }
4174 // v1.1.0 beta.2 (T-110-C3) — event-viewer tap: classify the write +
4175 // record it with the current PPU position. Output-only, gated.
4176 #[cfg(feature = "debug-hooks")]
4177 if self.event_logging {
4178 let kind = match addr {
4179 0x2000..=0x3FFF => Some(EventKind::PpuWrite),
4180 // The whole `$4000-$4017` APU / I/O window (Copilot #43): this
4181 // now also captures `$4014` OAM DMA and `$4016` controller
4182 // strobe, which the legend's "$4000-4017" already advertises.
4183 0x4000..=0x4017 => Some(EventKind::ApuWrite),
4184 0x4020..=0xFFFF => Some(EventKind::MapperWrite),
4185 _ => None,
4186 };
4187 if let Some(kind) = kind
4188 && self.events.len() < EVENT_CAP
4189 {
4190 self.events.push(EventRec {
4191 kind,
4192 scanline: self.ppu.scanline(),
4193 dot: self.ppu.dot(),
4194 addr,
4195 value,
4196 });
4197 }
4198 }
4199 // v1.4.0 Workstream D (D2) — event-breakpoint write taps. Output-only.
4200 // `$4014` is the OAM-DMA trigger; the rest classify by window.
4201 #[cfg(feature = "debug-hooks")]
4202 if self.event_bp_mask != 0 {
4203 match addr {
4204 0x2000..=0x3FFF => self.record_event_break(EventBpKind::PpuWrite, addr),
4205 REG_OAM_DMA => self.record_event_break(EventBpKind::OamDma, addr),
4206 0x4000..=0x4017 => self.record_event_break(EventBpKind::ApuWrite, addr),
4207 0x4020..=0xFFFF => self.record_event_break(EventBpKind::MapperWrite, addr),
4208 _ => {}
4209 }
4210 }
4211 match addr {
4212 0x0000..=0x1FFF => self.ram[(addr & 0x07FF) as usize] = value,
4213 0x2000..=0x3FFF => self.ppu_register_write(addr, value),
4214 REG_OAM_DMA => {
4215 // v2.3.2 "Lucid" — freeze THIS instruction (the `STA $4014`) as
4216 // the cause of the burst before it is armed. The 513/514 DMA
4217 // cycles are stolen from the instructions that follow, so by the
4218 // time the first OAM byte lands the live attribution context has
4219 // moved on to whichever instruction is being halted.
4220 #[cfg(feature = "debug-hooks")]
4221 self.ppu.latch_dma_attrib_context();
4222 // v2.3.7 "Overtone" — `$4014` sits inside the `$4000-$4017`
4223 // window the audio-provenance table reserves a slot for, but the
4224 // arm below routes only `$4000-$4013 | $4015 | $4017` to
4225 // `Apu::write_register`, where attribution is recorded. Record it
4226 // here so the reserved slot is actually populated; nothing is
4227 // dispatched to the APU, so the DMA behaviour is unchanged.
4228 #[cfg(feature = "debug-hooks")]
4229 self.apu
4230 .record_bus_handled_register_write(REG_OAM_DMA, value);
4231 self.dma_pending = Some(value);
4232 }
4233 0x4000..=0x4013 | 0x4015 | 0x4017 => self.apu.write_register(addr, value),
4234 0x4016 => {
4235 // v2.3.7 "Overtone" — same as `$4014` above: inside the
4236 // provenance window, never routed to `Apu::write_register`, so
4237 // attribute it here. The strobe itself is still buffered and
4238 // committed by the code below; this only records the cause.
4239 #[cfg(feature = "debug-hooks")]
4240 self.apu.record_bus_handled_register_write(0x4016, value);
4241 // Session-24 / Phase 3 (Controller Strobing): the
4242 // controllers' OUT pins are only updated at the start
4243 // of M2-low (PUT) cycles. Buffer the write and
4244 // commit at the next M2-low boundary inside
4245 // `tick_one_cpu_cycle`. Mirrors Mesen2's
4246 // `NesControlManager::WriteRam` (Core/NES/
4247 // NesControlManager.cpp lines 252-273).
4248 //
4249 // Parity convention: in `RustyNES` the bus enters each
4250 // CPU cycle at `M2Phase::Low` and transitions to
4251 // `M2Phase::High` after PPU sub-dot 1. The cycle
4252 // counter advances at end-of-cycle. So a CPU write
4253 // executed during cycle `self.cycle` lands at the END
4254 // of that cycle's M2-high half. The NEXT cycle
4255 // (`self.cycle + 1`) starts at M2-low — which is the
4256 // commit boundary. In Mesen2's master-clock terms,
4257 // odd master clocks mean "one cycle from PUT" and
4258 // even mean "two cycles from PUT"; the corresponding
4259 // `RustyNES` rule is: if `self.cycle` is odd at write
4260 // time, pending = 1 (commit at next cycle); if even,
4261 // pending = 2 (commit at cycle-after-next). This
4262 // collapses the AccuracyCoin Test 4 1-cycle DEC
4263 // `$4016` strobe pulse (both writes target the SAME
4264 // commit cycle; the second overwrites the first; no
4265 // edge is observed → no latch). See
4266 // `docs/audit/session-24-phase3-controller-strobing-2026-05-23.md`.
4267 self.controller_write_value = value;
4268 // Parity convention: in `RustyNES` the CPU `cpu_write` runs
4269 // INSIDE `tick_one_cpu_cycle` AFTER `self.cycle` has
4270 // been incremented to the post-cycle value (see
4271 // `tick_one_cpu_cycle` flow). The committed commit
4272 // cycle MUST land on an M2-low boundary (PUT cycle).
4273 // In `RustyNES` every CPU cycle starts at M2-low and
4274 // transitions to M2-high after sub-dot 1, so every
4275 // cycle has an M2-low half — but only cycles where
4276 // the COMMITTED strobe value is observable AT the
4277 // beginning of the cycle qualify as the deferred-
4278 // write commit target.
4279 //
4280 // The empirical calibration from the Phase 3 oracle:
4281 // Mesen2 PUT cycles correspond to ODD `cpu.cycleCount`
4282 // (per `NesCpu.cpp:400` `bool getCycle = (CycleCount &
4283 // 0x01) == 0;` — get cycles are even, put cycles are
4284 // odd). Our `self.cycle` parity at the moment of
4285 // `cpu_write` differs from Mesen2's by an offset
4286 // (Mesen2's cycle count includes the boot/reset
4287 // sequence differently); empirically, our EVEN cycles
4288 // correspond to Mesen2's PUT cycles in the
4289 // `controller-strobing.nes` Test 3 vs Test 4
4290 // discrimination. Hence: even `self.cycle` → pending
4291 // = 1 (commit next cycle); odd `self.cycle` → pending
4292 // = 2 (commit cycle-after-next).
4293 self.controller_write_pending = if (self.cycle & 1) == 0 { 1 } else { 2 };
4294 // Vs. System (mapper 99): the CHR bank select is bit 2 of the
4295 // value written to $4016 (shared with the controller strobe).
4296 // Forward every $4016 write to the mapper; only mapper 99
4297 // consumes it — every other mapper's `cpu_write` ignores the
4298 // $4016 address (their match arms only cover $8000-$FFFF /
4299 // $4020-$7FFF), so this is byte-for-byte a no-op on all
4300 // non-Vs. carts.
4301 self.mapper.cpu_write(0x4016, value);
4302 // v2.0.0 beta.5 (Vs. DualSystem): report the bit-1 (main/sub
4303 // comms signal) LEVEL on EVERY $4016 write for the wrapper
4304 // to poll. Deliberately not edge-filtered: the wrapper seeds
4305 // the reset-time levels itself (Mesen2's
4306 // `UpdateMainSubBit(main ? 0x00 : 0x02)`), so a bus-side
4307 // edge filter starting from a `false` latch would swallow a
4308 // genuine seeded-HIGH → written-LOW transition (Balloon
4309 // Fight's reset writes `$4016 = $00` on both consoles) and
4310 // deadlock the boot handshake. Applying an unchanged level
4311 // is idempotent in the wrapper. The latch is only consulted
4312 // by the DualSystem wrapper; single-console behavior is
4313 // untouched (two dead field writes on non-Vs carts, no
4314 // reads).
4315 self.vs_4016_bit1 = (value & 0x02) != 0;
4316 self.vs_4016_bit1_dirty = true;
4317 }
4318 0x4018..=0x401F => {}
4319 0x4020..=0xFFFF => self.mapper.cpu_write(addr, value),
4320 }
4321 #[cfg(feature = "irq-timing-trace")]
4322 {
4323 // Session-21: record the CPU-initiated write at the bus-access
4324 // tracker for the same reason `cpu_read` does above.
4325 self.trace_bus_access = BusAccess::Write;
4326 self.trace_bus_addr = addr;
4327 self.trace_bus_data = value;
4328 }
4329 }
4330
4331 fn poll_nmi(&mut self) -> bool {
4332 let edge = self.nmi_edge_latch;
4333 self.nmi_edge_latch = false;
4334 edge
4335 }
4336
4337 fn poll_irq(&mut self) -> bool {
4338 // Phase B2 of the C1 IRQ-timing rework: read the M2-high
4339 // snapshot captured at end-of-3-PPU-dots inside
4340 // `tick_one_cpu_cycle`. Semantically identical to the prior
4341 // `mapper.irq_pending() || apu.irq_line()` query for every
4342 // workspace test ROM (verified: 500 strict + 6 ignored
4343 // unchanged; trace baselines byte-identical). Phase B4 will
4344 // make the snapshot's value depend on the M2 phase via the
4345 // MMC3 sub_dot-aware A12 filter — this method becomes the
4346 // single point where the production CPU IRQ sample crosses
4347 // into the bus, and from there into the mapper.
4348 self.irq_snapshot_mapper_at_high || self.irq_snapshot_apu_at_high
4349 }
4350
4351 fn poll_irq_at_phase(&mut self, phase: M2Phase) -> bool {
4352 match phase {
4353 M2Phase::Low => self.irq_snapshot_mapper_at_low || self.irq_snapshot_apu_at_low,
4354 M2Phase::High => self.irq_snapshot_mapper_at_high || self.irq_snapshot_apu_at_high,
4355 }
4356 }
4357
4358 fn on_cpu_cycle(&mut self) {
4359 self.tick_one_cpu_cycle();
4360 }
4361
4362 fn internal_data_bus(&self) -> u8 {
4363 // Phase 1 of `linked-puzzling-sutherland` v1.0.0-final brief:
4364 // expose the internal CPU data bus latch separately from the
4365 // external `open_bus`. Mirrored from every CPU read / write;
4366 // NOT updated by DMC DMA fetches. See the field documentation
4367 // on [`LockstepBus::internal_data_bus`] and the trait method
4368 // documentation on [`Bus::internal_data_bus`].
4369 self.internal_data_bus
4370 }
4371
4372 fn cycle_count(&self) -> u64 {
4373 // Cumulative bus-side cycle counter, including DMC DMA cycles
4374 // that the CPU's own `Cpu::cycles` field does not count. Used
4375 // by the SH* unstable-store family to detect DMA interrupting
4376 // their dummy-read cycle per Mesen2's `SyaSxaAxa` algorithm.
4377 self.cycle
4378 }
4379
4380 fn notify_irq_service(&mut self, vector: u16, is_nmi: bool) {
4381 // v1.2.0 (T-110-E1) — Lua onNmi/onIrq interrupt-service tap. This is the
4382 // committed-service commit point (same as the IRQ trace below), NOT the
4383 // speculative poll_nmi/poll_irq sampler. Output-only, gated; no-op when
4384 // `debug-hooks` is off (the log slot only exists feature-gated).
4385 //
4386 // The reliable NMI/IRQ discriminator here is the COMMITTED `vector`
4387 // ($FFFA = NMI, $FFFE = IRQ/BRK), not the `is_nmi` arg: the unified
4388 // dispatch always enters `service_interrupt` with the IRQ vector and
4389 // resolves the NMI *hijack* internally (so the `is_nmi` arg reads
4390 // `false` on a hijacked NMI). Classifying by the vector the CPU actually
4391 // fetched reports exactly the service that committed.
4392 #[cfg(feature = "debug-hooks")]
4393 if self.interrupt_logging && self.interrupts.len() < INTERRUPT_CAP {
4394 let _ = is_nmi;
4395 self.interrupts.push(InterruptRec {
4396 is_nmi: vector == 0xFFFA,
4397 vector,
4398 });
4399 }
4400 // v1.4.0 Workstream D (D2) — NMI/IRQ event-breakpoint tap. Classified by
4401 // the COMMITTED vector (same discriminator the interrupt log uses).
4402 #[cfg(feature = "debug-hooks")]
4403 if self.event_bp_mask != 0 {
4404 let kind = if vector == 0xFFFA {
4405 EventBpKind::Nmi
4406 } else {
4407 EventBpKind::Irq
4408 };
4409 self.record_event_break(kind, vector);
4410 }
4411 // Phase 1.2 of Track C1 attempt 14: emit a [`ServiceEvent`] into
4412 // the IRQ trace if the trace is armed. Production builds with
4413 // the `irq-timing-trace` feature OFF compile this down to a
4414 // no-op (the trace slot only exists feature-gated).
4415 #[cfg(feature = "irq-timing-trace")]
4416 if let Some(trace) = self.irq_trace.as_mut() {
4417 let frame_start = self.ppu.frame();
4418 let scanline_start = self.ppu.scanline();
4419 let dot_start = self.ppu.dot();
4420 let kind = if is_nmi {
4421 crate::irq_trace::ServiceKind::Nmi
4422 } else {
4423 crate::irq_trace::ServiceKind::Irq
4424 };
4425 // `self.cycle` is the count of cycles already consumed; the
4426 // service-vector fetch is the cycle the CPU is ABOUT to
4427 // emit, so reporting `self.cycle` (== the next cycle index)
4428 // matches Mesen2's `cpu.cycleCount` at the moment its
4429 // `emu.eventType.irq` callback fires (its cycle count is
4430 // sampled at the start of the service cycle).
4431 trace.push_service(crate::irq_trace::ServiceEvent {
4432 cpu_cycle: self.cycle,
4433 ppu_scanline: scanline_start,
4434 ppu_dot: dot_start,
4435 ppu_frame: frame_start,
4436 kind,
4437 vector,
4438 });
4439 } else {
4440 let _ = (vector, is_nmi);
4441 }
4442 // Suppress unused-variable warnings when the feature is off.
4443 #[cfg(not(feature = "irq-timing-trace"))]
4444 {
4445 let _ = (vector, is_nmi);
4446 }
4447 }
4448
4449 // ============================================================
4450 // v2.0 master-clock R1 substrate — production overrides (Phase 1).
4451 // Compiled only under `mc-r1-substrate`; consulted by the R1 CPU loop
4452 // (Phases 2+). NOT exercised on the default build, so default behaviour
4453 // is byte-identical. Ported from refactor/v2.0-master-clock with the
4454 // trace + S1/S2 (mc-apu-subcycle / r4-cpu-dma) wiring stripped.
4455 // ============================================================
4456
4457 /// Pure address-space read under R1 (the DMA drain happens in
4458 /// [`Bus::cpu_clock`]; Phase 3 will split the drain out of `cpu_read`).
4459 /// Phase 1 delegates to the legacy path so the contract compiles.
4460 fn read(&mut self, addr: u16) -> u8 {
4461 self.cpu_read(addr)
4462 }
4463
4464 fn write(&mut self, addr: u16, value: u8) {
4465 self.cpu_write(addr, value);
4466 }
4467
4468 /// R1 master clocks per CPU cycle for the cartridge region (NTSC 12 / PAL
4469 /// 16 / Dendy 15) — the `cpu_divider` half of `region_dividers`.
4470 /// Drives the CPU loop's `master_clock` advance + read/write split so the
4471 /// CPU<->PPU phase is 3:1 NTSC, 3.2:1 PAL, 3:1 Dendy.
4472 fn cpu_divider(&self) -> u64 {
4473 u64::from(self.cpu_div_cached)
4474 }
4475
4476 /// R1 double catch-up: tick whole PPU dots while
4477 /// `ppu_clock + ppu_divider <= target`.
4478 ///
4479 /// R1c-3 (`mmc3-m2-phase-irq`, default-off): when the feature is
4480 /// enabled, `sub_dot` is seeded from the REAL M2-phase of this catch-up
4481 /// call (`0` = pre-access / M2-low, called from `Cpu::start_cycle`
4482 /// before the bus access; `2` = post-access / M2-high, called from
4483 /// `Cpu::end_cycle` after it) instead of always restarting at `0`. Prior
4484 /// to this experiment `sub_dot` was a call-LOCAL counter that reset to
4485 /// zero on every invocation of this function — since `run_ppu_to` is
4486 /// called twice per CPU cycle (once per half) and each half typically
4487 /// ticks at most one PPU dot, the value threaded to
4488 /// `Mapper::notify_a12_at_sub_dot` was almost always `0` regardless of
4489 /// which half of the cycle actually produced the A12 transition. That
4490 /// meant the M2-phase plumbing ADR-0002 describes ("sub-dot 0/1 is
4491 /// M2-low, 2 is M2-high") was never actually true on the live R1
4492 /// (non-DMA) scheduler path — only on the legacy `tick_one_cpu_cycle`
4493 /// DMA-burst path, which genuinely walks all 3 dots of a cycle in one
4494 /// call with a persistent counter. This experiment closes that gap so
4495 /// MMC3's (default-off) M2-phase-aware IRQ-visibility pipeline can be
4496 /// evaluated against real phase data on the promoted core. See
4497 /// `docs/adr/0002-irq-timing-coordination.md` and
4498 /// `docs/audit/r1r2-per-dot-scheduler-attempt-2026-07-02.md`.
4499 ///
4500 /// When the feature is OFF this compiles to the exact prior
4501 /// call-local-counter behavior (`sub_dot` always starts at `0`) —
4502 /// byte-identical default build, per the project's additive/off-by-
4503 /// default convention.
4504 fn run_ppu_to(&mut self, target: u64, is_post_access: bool) {
4505 let ppu_div = u64::from(self.ppu_div_cached);
4506 // Seed the real M2-phase into `sub_dot` (0 = pre-access/M2-low catch-up,
4507 // 2 = post-access/M2-high catch-up) for the `mmc3-m2-phase-irq` deferral
4508 // AND for the v2.1.5 F5.0 `mmc3-a12-phase-probe` observational tally.
4509 // The probe only counts, so the emulated timeline stays byte-identical
4510 // even with its feature on. See ADR 0002.
4511 #[cfg(any(feature = "mmc3-m2-phase-irq", feature = "mmc3-a12-phase-probe"))]
4512 let mut sub_dot = if is_post_access { 2u8 } else { 0u8 };
4513 #[cfg(not(any(feature = "mmc3-m2-phase-irq", feature = "mmc3-a12-phase-probe")))]
4514 let (mut sub_dot, _) = (0u8, is_post_access);
4515 while self.ppu_clock + ppu_div <= target {
4516 let mut adapter = PpuBusAdapter {
4517 mapper: self.mapper.as_mut(),
4518 nt_override: self.nt_mirroring_override,
4519 sub_dot,
4520 #[cfg(feature = "irq-timing-trace")]
4521 trace_a12_latest: None,
4522 };
4523 self.ppu.tick(&mut adapter);
4524 self.sample_nmi_edge();
4525 self.ppu_clock += ppu_div;
4526 sub_dot = sub_dot.wrapping_add(1);
4527 }
4528 }
4529
4530 /// R1: one CPU cycle of bus-side work (NO PPU advance — that lives in
4531 /// [`Bus::run_ppu_to`]). Controller strobe + bus-side DMA drain + cycle
4532 /// counter + per-cycle PPU/mapper hooks + APU tick. DMA stays bus-side
4533 /// (the pivot's working `service_dmc_dma`); Phase 3 wires the
4534 /// `dma_mc_consumed` coherence accounting.
4535 fn cpu_clock(&mut self) {
4536 // Diagnostic: snapshot the APU IRQ line (frame-counter | DMC) BEFORE
4537 // `apu_advance_one` runs the frame counter, so `trace_end_cycle` can
4538 // expose the within-cycle frame-counter SET (low=0 -> high=1) vs the
4539 // DMA `$4015` CLEAR (low=1 -> high=0) ordering. Only meaningful under
4540 // the trace feature; the field is otherwise unused on the R1 path.
4541 #[cfg(feature = "irq-timing-trace")]
4542 {
4543 self.irq_snapshot_apu_at_low = self.apu.irq_line();
4544 self.trace_r1_scanline_start = self.ppu.scanline();
4545 self.trace_r1_dot_start = self.ppu.dot();
4546 self.trace_r1_frame_start = self.ppu.frame();
4547 }
4548 if self.controller_write_pending > 0 {
4549 self.controller_write_pending -= 1;
4550 if self.controller_write_pending == 0 {
4551 let value = self.controller_write_value;
4552 self.commit_controller_strobe(value);
4553 }
4554 }
4555 self.cycle = self.cycle.wrapping_add(1);
4556 self.ppu.on_cpu_cycle();
4557 // v2.8.0 Phase 4 — skip the virtual dispatch on boards whose
4558 // `notify_cpu_cycle` is the default no-op (capability-flag cache).
4559 if self.mapper_caps.cpu_cycle_hook {
4560 self.mapper.notify_cpu_cycle();
4561 }
4562 // F-2: `apu_advance_one` (start) ticks the whole APU EXCEPT the DMC
4563 // byte-timer (gated out by `dmc_driven_externally`); the DMC is ticked
4564 // at end-of-cycle by `cpu_clock_apu_dmc`.
4565 self.apu_advance_one();
4566 // (W2 $2007 Stress) The deferred $2007 render-buffer reload is now
4567 // PPU-dot-scheduled and consumed inside `Ppu::tick` — the prior
4568 // per-CPU-cycle `apply_pending_render_buffer` hook here was quantized
4569 // to 3-dot steps and structurally aliased mod 3 against the test's
4570 // 1-dot-per-iteration clockslide.
4571 }
4572
4573 // RA-1 (mc-r1-apu-unified-clock): the DMC byte-timer is now clocked at cycle
4574 // START (in `Apu::tick_with_external` via `apu_advance_one` in `cpu_clock`),
4575 // unified with the rest of the APU and advancing through the DMC DMA span,
4576 // matching Mesen's `ProcessCpuClock` at `StartCpuCycle`. So the END-of-cycle
4577 // DMC tick is a no-op here.
4578 fn cpu_clock_apu_dmc(&mut self) {
4579 // v2.0 Program M (M-1): clock the DMC byte-timer + arm the reload HERE at
4580 // end-of-cycle (after the CPU's bus access), the references' within-cycle
4581 // order. When the flag is OFF the byte-timer stays at cycle-start (above,
4582 // in `tick_with_external`) and this is a no-op -> floor byte-identical.
4583 // Runs BEFORE `promote_dmc_pending_next` so a reload armed at end-of-cycle
4584 // N latches `_next` and is promoted by this SAME call -> serviced N+1
4585 // (the floor service cadence), the byte-timer position being the only
4586 // shift (vs promote-before, which adds a full +1 service cycle and
4587 // over-shifts every DMA).
4588 self.apu.dmc_tick_end();
4589 // Visibility-delay: promote a reload latched this cycle at END (after the
4590 // CPU's bus access) so the NEXT cycle's DMA loop first-services it (put).
4591 self.apu.promote_dmc_pending_next();
4592 }
4593
4594 fn take_dma_mc_consumed(&mut self) -> u64 {
4595 core::mem::take(&mut self.dma_mc_consumed)
4596 }
4597
4598 fn irq_level(&self) -> bool {
4599 // Bound BEFORE the expression rather than as an inline `#[cfg]` block
4600 // inside it. The two forms compile identically -- the default build
4601 // still emits nothing named `inject_`, which is ADR 0038's structural
4602 // gate -- but a `cfg` block in the middle of a boolean chain is hard to
4603 // read, and this chain is the wire-OR of every /IRQ source.
4604 #[cfg(feature = "cosim-interrupt-inject")]
4605 let injected = self.inject_irq;
4606 #[cfg(not(feature = "cosim-interrupt-inject"))]
4607 let injected = false;
4608
4609 // v2.8.0 Phase 4 — boards without an IRQ source have the default
4610 // `irq_pending() == false`; skip the per-cycle virtual call.
4611 // v2.0.0 beta.5 — `vs_external_irq` is the DualSystem partner
4612 // console's `$4016` bit-1 signal (always `false` on a single
4613 // console, so the default path is unchanged).
4614 (self.mapper_caps.irq_source && self.mapper.irq_pending())
4615 || self.apu.irq_line()
4616 || self.vs_external_irq
4617 // v2.5.1 (ADR 0038). Level-sensitive and OR'd, exactly like
4618 // `vs_external_irq` beside it -- which is the precedent: an external
4619 // IRQ source already joins the wire-OR here, and this is the same
4620 // shape with a different driver.
4621 || injected
4622 }
4623
4624 fn nmi_level(&self) -> bool {
4625 // v2.5.1 (ADR 0038). Injected here and NOT in `poll_nmi`, because
4626 // `poll_nmi` is not the path the production CPU uses: it samples this
4627 // LEVEL every cycle and edge-detects it itself (`nmi_first_tick` ->
4628 // `pending_nmi` -> `armed_nmi`).
4629 //
4630 // The first implementation injected at `poll_nmi`, which looks like the
4631 // right function and is dead for this path. The rung-2 sweep found it on
4632 // its first real run -- the DUT took the injected NMI and the oracle did
4633 // not -- which is exactly the defect class a co-simulation exists to
4634 // catch, arriving in the harness rather than in the RTL.
4635 //
4636 // A LEVEL, not a latch: the CPU does its own edge detection, so
4637 // consuming it here would make an injected NMI behave unlike a PPU one.
4638 #[cfg(feature = "cosim-interrupt-inject")]
4639 if self.inject_nmi {
4640 return true;
4641 }
4642 self.ppu.nmi_line()
4643 }
4644
4645 fn dmc_dma_pending(&self) -> bool {
4646 self.apu.dmc_dma_pending()
4647 }
4648
4649 fn dmc_dma_defer_load_entry(&self) -> bool {
4650 {
4651 // The while-gate runs PRE-cycle (before `start_cycle`'s APU tick).
4652 // Floor: the start-flip means the pre-cycle `!put_cycle` predicts
4653 // an access-point parity on the DMC noop half (defer it).
4654 // W3-Stage-2 (`mc-r1-dma-unified-collapse`): the flip moved to
4655 // end-of-cycle, so the pre-cycle value IS the upcoming
4656 // access-point label — the noop half is now the PUT half, so the
4657 // defer condition INVERTS to `put_cycle` (pre-cycle reads are
4658 // flip-invariant in value; the predicted half changes).
4659 let lands_on_noop_half = self.apu.put_cycle();
4660 self.apu.dmc_dma_pending()
4661 && self.apu.dmc_dma_is_load()
4662 && lands_on_noop_half
4663 && !self.in_dmc_dma
4664 }
4665 }
4666
4667 fn dmc_dma_step(&mut self, halted_addr: u16) {
4668 self.dmc_dma_step_impl(halted_addr);
4669 }
4670
4671 fn dmc_dma_step_idle(&mut self) {
4672 // Internal-cycle DMC halt: re-read the held (last CPU read) address.
4673 let halted = self.last_read_addr;
4674 self.dmc_dma_step_impl(halted);
4675 }
4676
4677 // Stage-D: OAM DMA is pending (a `$4014` write awaits its first read cycle)
4678 // or in flight. The CPU `read1` loop drives it one cycle at a time.
4679 fn oam_dma_pending(&self) -> bool {
4680 self.dma_pending.is_some() || self.dma_cycles_owed > 0
4681 }
4682
4683 // Stage-D: one CPU-driven OAM DMA cycle. First call latches the pending
4684 // `$4014` page + the 513/514 alignment count; subsequent calls run one
4685 // halt/align/read/write cycle. Does NOT advance time — the surrounding
4686 // `start_cycle`/`end_cycle` (and their `cpu_clock`/`run_ppu_to`/φ2 sample)
4687 // do, so each OAM cycle is interrupt-sampled like a normal CPU cycle (the
4688 // surface the bus burst bypassed). Mirrors `clock_oam_dma_cycle` minus the
4689 // `tick_one_cpu_cycle`.
4690 fn oam_dma_step(&mut self, halted_addr: u16) {
4691 if let Some(page) = self.dma_pending.take() {
4692 self.dma_page = page;
4693 self.dma_idx = 0;
4694 self.dma_halt_addr = halted_addr;
4695 let extra: u32 = if self.cycle & 1 == 0 { 514 } else { 513 };
4696 self.dma_cycles_owed = extra;
4697 self.dma_total = extra;
4698 }
4699 if self.dma_cycles_owed == 0 {
4700 return;
4701 }
4702 let total = self.dma_total;
4703 let alignment = if total == 514 { 2 } else { 1 };
4704 let consumed = total - self.dma_cycles_owed;
4705 if consumed < alignment {
4706 // Halt / alignment cycle: the held CPU address stays on the bus.
4707 #[cfg(feature = "irq-timing-trace")]
4708 self.set_trace_dma_access(BusAccess::DmaRead, self.dma_halt_addr, self.open_bus);
4709 } else {
4710 let xfer_idx = consumed - alignment; // 0..512
4711 if xfer_idx & 1 == 0 {
4712 let src_addr =
4713 (u16::from(self.dma_page) << 8) | u16::try_from(xfer_idx >> 1).unwrap_or(0);
4714 self.dma_byte = self.raw_oam_dma_read(src_addr);
4715 } else {
4716 self.oam_dma_put();
4717 }
4718 }
4719 self.dma_cycles_owed -= 1;
4720 if self.dma_cycles_owed == 0 {
4721 self.dma_total = 0;
4722 }
4723 }
4724
4725 // W3-Stage-1 (`mc-r1-dma-unified`): the unified engine's pending query.
4726 // Folds the floor's load-get-entry defer (the standalone DMC loop's
4727 // pre-flip while-gate: a deferred load alone does NOT hold the CPU — the
4728 // real read runs and the load enters on the next cycle, its get half) with
4729 // the OAM pending/in-flight state. The engine re-derives the same defer at
4730 // the access point for cycles the loop runs anyway because OAM is active.
4731 fn unified_dma_pending(&self) -> bool {
4732 let dmc = self.apu.dmc_dma_pending() && !Bus::dmc_dma_defer_load_entry(self);
4733 // W3-Stage-3 (`mc-r1-dmc-delayed-4015`): the TriCNES `_6502` line-4218
4734 // service gate — `DoDMCDMA && (APU_Status_DMC || implicit-abort)`. A
4735 // pending (or halted in-flight) DMC DMA whose APPLIED status dropped
4736 // is NOT serviced: the loop exits and the CPU resumes mid-DMA — the
4737 // emergent explicit abort. The engine's transient state (`in_dmc_dma`
4738 // / `dmc_halt` / the APU pending flag) persists, like TriCNES's stale
4739 // `DoDMCDMA`/`DMCDMA_Halt`, and resumes if the status re-applies.
4740 let dmc = dmc && self.apu.dmc_dma_serviceable();
4741 dmc || self.dma_pending.is_some() || self.uni_oam_active
4742 }
4743
4744 // W3-Stage-1: one unified-engine cycle at a CPU read (the preempted
4745 // instruction/operand read supplies the parked 6502 address).
4746 fn unified_dma_cycle(&mut self, halted_addr: u16) {
4747 self.unified_dma_cycle_impl(halted_addr);
4748 }
4749
4750 // W3-Stage-1: one unified-engine cycle at a CPU internal cycle — the bus
4751 // supplies its held (last-read) address, like `dmc_dma_step_idle`.
4752 fn unified_dma_cycle_idle(&mut self) {
4753 let halted = self.last_read_addr;
4754 self.unified_dma_cycle_impl(halted);
4755 }
4756
4757 // Program M (M-2): an OAM DMA is started + still owes cycles (in flight),
4758 // distinct from `oam_dma_pending` (which also covers a not-yet-started write).
4759 fn oam_dma_in_flight(&self) -> bool {
4760 self.dma_cycles_owed > 0
4761 }
4762
4763 // W3-Stage-0 (`mc-r1-counter-collapse`): a pending DMC DMA may overlap an OAM
4764 // DMA that is in flight OR still pending its first cycle. The collapse flag's
4765 // end-of-cycle byte-timer shift can surface the DMC arm in the one-iteration
4766 // gap between the `$4014` write and OAM's start-latch; lockstep `drain_dma`
4767 // latches OAM BEFORE its DMC-pending check, so the same arm overlaps OAM's
4768 // halt/alignment cycles there (the traced DMC+OAM Loop1 idx[6]/idx[7] events
4769 // with owed_at_begin == the FULL 514/513). Routing it to the standalone
4770 // `dmc_dma_step` instead pays a full unshared reload span = the idx[7] `03`.
4771 // Without the collapse flag the arm cannot surface in that gap, so the
4772 // original in-flight-only condition is preserved (audit-state invariant).
4773 fn oam_dma_overlap_ready(&self) -> bool {
4774 self.dma_cycles_owed > 0 || self.dma_pending.is_some()
4775 }
4776
4777 // Program M (M-2): whether the most recent `dmc_dma_step` did the GET.
4778 fn dmc_dma_last_was_get(&self) -> bool {
4779 self.dmc_step_was_get
4780 }
4781
4782 // Program M (M-2): advance ONE in-flight OAM cycle shared with a DMC halt
4783 // cycle. Mirrors the transfer/alignment body of `oam_dma_step` MINUS the
4784 // pending-latch (OAM is already in flight) and MINUS the time tick (the
4785 // surrounding start_cycle/end_cycle owns it). This is the per-cycle analogue
4786 // of lockstep `service_dmc_dma_during_oam` calling `clock_oam_dma_cycle` on
4787 // the DMC halt/dummy/align cycles, which is what produces the test's `02/01`
4788 // "DMC appears to take only 2/1 cycles" sweep entries.
4789 fn oam_dma_overlap_cycle(&mut self) {
4790 if self.dma_cycles_owed == 0 {
4791 return;
4792 }
4793 let total = self.dma_total;
4794 let alignment = if total == 514 { 2 } else { 1 };
4795 let consumed = total - self.dma_cycles_owed;
4796 if consumed >= alignment {
4797 let xfer_idx = consumed - alignment; // 0..512
4798 if xfer_idx & 1 == 0 {
4799 let src_addr =
4800 (u16::from(self.dma_page) << 8) | u16::try_from(xfer_idx >> 1).unwrap_or(0);
4801 self.dma_byte = self.raw_oam_dma_read(src_addr);
4802 } else {
4803 self.oam_dma_put();
4804 }
4805 }
4806 self.dma_cycles_owed -= 1;
4807 if self.dma_cycles_owed == 0 {
4808 self.dma_total = 0;
4809 }
4810 }
4811
4812 // Program M (M-2, exact): begin ONE DMC-DMA-during-OAM event. Direct port of
4813 // lockstep `service_dmc_dma_during_oam`'s prologue (bus.rs ~2067): latch the
4814 // halt + the open-bus replay and return the UNCONDITIONAL halt/dummy/align
4815 // noop count (`dmc_dma_short() ? 2 : 3`) — NOT parity-gated. This replaces the
4816 // prior per-cycle "share-on-every-non-GET" heuristic (which OVER-GLUED the
4817 // looping reloads, reading 2C=44 runaway at the test's `02` positions) with
4818 // lockstep's exact noop/GET/realign accounting bound to ONE DMC DMA.
4819 fn dmc_overlap_begin(&mut self, halted_addr: u16) -> u32 {
4820 // W3-Stage-0 (`mc-r1-counter-collapse` boundary-start): when this event
4821 // STARTS a pending (not-yet-latched) `$4014` OAM DMA, the OAM halt
4822 // address is the CPU read this DMA pair is preempting — the same value
4823 // `oam_dma_step` would have latched. The owed/total latch itself is
4824 // deferred to the first `dmc_overlap_noop_cycle` (inside the cycle's
4825 // `start_cycle`, so the 514/513 `self.cycle & 1` parity matches the
4826 // position `oam_dma_step` would have evaluated it).
4827 if self.dma_pending.is_some() {
4828 self.dma_halt_addr = halted_addr;
4829 }
4830 self.in_dmc_dma = true;
4831 self.dmc_step_was_get = false;
4832 self.capture_deferred_dma_replay();
4833 if self.apu.dmc_dma_short() { 2 } else { 3 }
4834 }
4835
4836 // Program M (M-2, exact): one DMC halt/dummy/align cycle overlapping OAM.
4837 // Mirrors lockstep's noop-loop body: replay the held CPU read's side-effect,
4838 // then (if OAM still owes) advance one OAM slot — the 6502 is RDY-halted but
4839 // the OAM engine keeps its bus slot. The time tick is owned by the CPU's
4840 // surrounding start_cycle/end_cycle.
4841 fn dmc_overlap_noop_cycle(&mut self) {
4842 // W3-Stage-0 (`mc-r1-counter-collapse` boundary-start): latch a pending
4843 // `$4014` OAM DMA on the first shared halt cycle, mirroring
4844 // `oam_dma_step`'s start block at the same within-cycle position (after
4845 // `start_cycle`'s `cpu_clock` increments `self.cycle`, so the 514/513
4846 // parity choice is identical to the no-DMC counterfactual). The latched
4847 // OAM then consumes its halt/alignment/transfer slots through
4848 // `oam_dma_overlap_cycle` below, exactly like lockstep's
4849 // `service_dmc_dma_during_oam` after `drain_dma` started the OAM.
4850 if let Some(page) = self.dma_pending.take() {
4851 self.dma_page = page;
4852 self.dma_idx = 0;
4853 let extra: u32 = if self.cycle & 1 == 0 { 514 } else { 513 };
4854 self.dma_cycles_owed = extra;
4855 self.dma_total = extra;
4856 }
4857 let halted_addr = self.dma_halt_addr;
4858 self.replay_dma_noop_read(halted_addr);
4859 if self.dma_cycles_owed > 0 {
4860 self.oam_dma_overlap_cycle();
4861 } else {
4862 #[cfg(feature = "irq-timing-trace")]
4863 self.set_trace_dma_access(BusAccess::DmaRead, halted_addr, self.open_bus);
4864 }
4865 }
4866
4867 // Program M (M-2, exact): the DMC GET cycle. Mirrors lockstep's get block +
4868 // the R1 `dmc_dma_step` GET (bus.rs ~2530): fetch the sample (with the
4869 // `$4000` open-bus conflict the cluster brackets), deliver it, and clear the
4870 // DMC-DMA pending state. OAM is STALLED — it does NOT advance on the GET.
4871 fn dmc_overlap_get_cycle(&mut self) {
4872 let halted_addr = self.dma_halt_addr;
4873 let addr = self.apu.dmc_dma_addr();
4874 let byte = self.dmc_dma_read(addr, halted_addr);
4875 #[cfg(feature = "irq-timing-trace")]
4876 self.set_trace_dma_access(BusAccess::DmaRead, addr, byte);
4877 self.apu.complete_dmc_dma(byte);
4878 self.in_dmc_dma = false;
4879 self.dmc_step_was_get = true;
4880 }
4881
4882 // Program M (M-2, exact): the post-GET realign stall. Mirrors lockstep's
4883 // `if dma_cycles_owed > 0 { tick }` after the GET — ONE extra OAM-stalled
4884 // cycle (OAM does NOT advance; the parked CPU address stays on the bus) so
4885 // the next OAM read resumes on a later get. The cycle the prior per-cycle
4886 // scaffold was MISSING.
4887 fn dmc_overlap_realign_cycle(&mut self) {
4888 #[cfg(feature = "irq-timing-trace")]
4889 {
4890 let halted_addr = self.dma_halt_addr;
4891 self.set_trace_dma_access(BusAccess::DmaRead, halted_addr, self.open_bus);
4892 }
4893 }
4894
4895 fn dmc_abort_pending(&self) -> bool {
4896 self.apu.dmc_abort_pending()
4897 }
4898
4899 fn dmc_abort_is_get_cycle(&self) -> bool {
4900 // get = read half (TriCNES `!APU_PutCycle`); the 1-cycle abort DMA can
4901 // only land its halt on a get cycle.
4902 !self.apu.put_cycle()
4903 }
4904
4905 fn dmc_abort_halt_step(&mut self, halted_addr: u16) {
4906 // 1-cycle abort DMA (Y=1): one halt re-read of the held CPU address (the
4907 // DMASync `$4000` the spin polls — drives the open-bus conflict), then
4908 // cancel the reload + the abort. The surrounding `read1` start/end_cycle
4909 // advances the clock, so CalculateDMADuration measures exactly 1 cycle.
4910 self.replay_dma_noop_read(halted_addr);
4911 #[cfg(feature = "irq-timing-trace")]
4912 self.set_trace_dma_access(BusAccess::DmaRead, halted_addr, self.open_bus);
4913 self.apu.cancel_dmc_dma();
4914 }
4915
4916 fn dmc_abort_cancel(&mut self) {
4917 // Y=0: the abort matured on a put/write cycle — no DMA occurs. Clear the
4918 // reload + the abort with no halt cycle consumed.
4919 self.apu.cancel_dmc_dma();
4920 }
4921
4922 #[cfg(not(feature = "irq-timing-trace"))]
4923 fn trace_end_cycle(&mut self) {}
4924
4925 /// v2.0 R1c-1 diagnostic: record this instruction's `(pc, cpu_cycle)` into
4926 /// the per-instruction trace ring (default + R1 both; not mc-r1-gated).
4927 #[cfg(feature = "cpu-instr-cycle-trace")]
4928 fn trace_instr(&mut self, pc: u16, cpu_cycle: u64) {
4929 instr_trace::record(pc, cpu_cycle);
4930 // Latch the PC so the per-cycle `CycleRecord` push can stamp every
4931 // cycle (including DMA-insertion cycles, which hold this PC) with the
4932 // instruction currently executing — the TriCNES cross-diff landmark.
4933 #[cfg(feature = "irq-timing-trace")]
4934 {
4935 self.trace_last_pc = pc;
4936 }
4937 }
4938
4939 /// R1-path per-cycle trace push (mirrors the `tick_one_cpu_cycle`
4940 /// `CycleRecord` build for the legacy path). `irq_pending_apu_at_low` was
4941 /// snapshotted at cycle-start in `cpu_clock` (before `apu_advance_one`);
4942 /// `_at_high` is read here at end-of-cycle (after the access + DMC tick), so
4943 /// a record where low=0/high=1 is a frame-counter SET this cycle and
4944 /// low=1/high=0 is a DMA `$4015` CLEAR this cycle — the ordering signal the
4945 /// `DMA + $4015` diagnostic needs.
4946 #[cfg(feature = "irq-timing-trace")]
4947 fn trace_end_cycle(&mut self) {
4948 if self.irq_trace.is_none() {
4949 return;
4950 }
4951 let events = core::mem::take(&mut self.trace_a12_scratch);
4952 let bus_access = core::mem::replace(&mut self.trace_bus_access, BusAccess::Idle);
4953 let bus_addr = core::mem::take(&mut self.trace_bus_addr);
4954 let bus_data = core::mem::take(&mut self.trace_bus_data);
4955 let mapper_irq = self.mapper.irq_pending();
4956 let rec = CycleRecord {
4957 cpu_cycle: self.cycle.wrapping_sub(1),
4958 pc: self.trace_last_pc,
4959 ppu_scanline: self.trace_r1_scanline_start,
4960 ppu_dot: self.trace_r1_dot_start,
4961 ppu_frame: self.trace_r1_frame_start,
4962 irq_pending_mapper_at_low: mapper_irq,
4963 irq_pending_apu_at_low: self.irq_snapshot_apu_at_low,
4964 irq_pending_mapper_at_high: mapper_irq,
4965 irq_pending_apu_at_high: self.apu.irq_line(),
4966 nmi_line: self.ppu.nmi_line(),
4967 a12_events: events,
4968 dmc_dma_pending_pre: false,
4969 dmc_dma_pending_post: self.apu.dmc_dma_pending(),
4970 dmc_dma_short_post: self.apu.dmc_dma_short(),
4971 dmc_abort_pending_post: self.apu.dmc_abort_pending(),
4972 dmc_abort_delay_post: self.apu.dmc_abort_delay(),
4973 dmc_dma_cooldown_post: self.apu.dmc_dma_cooldown(),
4974 dmc_dma_delay_post: self.apu.dmc_dma_delay(),
4975 apu_phase_post: self.apu.apu_phase(),
4976 in_dmc_dma: self.in_dmc_dma,
4977 dma_cycles_owed: self.dma_cycles_owed,
4978 bus_access,
4979 bus_addr,
4980 bus_data,
4981 put_cycle_post: self.apu.put_cycle(),
4982 dmc_timer_post: self.apu.dmc_timer(),
4983 dmc_bits_remaining_post: self.apu.dmc_bits_remaining(),
4984 dmc_silence_post: self.apu.dmc_silence(),
4985 dmc_buffer_full_post: self.apu.dmc_buffer_full(),
4986 };
4987 if let Some(t) = self.irq_trace.as_mut() {
4988 t.push(rec);
4989 }
4990 }
4991}
4992
4993#[cfg(test)]
4994mod four_score_tests {
4995 use super::*;
4996 use crate::controller::Buttons;
4997
4998 /// Minimal NROM (16-byte iNES header + 16 KiB PRG + 8 KiB CHR). Enough to
4999 /// construct a `LockstepBus`; these tests never run the CPU.
5000 fn test_bus() -> LockstepBus {
5001 let mut rom = Vec::with_capacity(16 + 0x4000 + 0x2000);
5002 rom.extend_from_slice(b"NES\x1A");
5003 rom.push(1); // 16 KiB PRG
5004 rom.push(1); // 8 KiB CHR
5005 rom.extend_from_slice(&[0u8; 10]);
5006 rom.extend_from_slice(&[0u8; 0x4000]);
5007 rom.extend_from_slice(&[0u8; 0x2000]);
5008 LockstepBus::new(&rom).expect("synthetic NROM parses")
5009 }
5010
5011 fn strobe(bus: &mut LockstepBus) {
5012 bus.commit_controller_strobe(1);
5013 bus.commit_controller_strobe(0);
5014 }
5015
5016 #[test]
5017 fn famicom_microphone_drives_4016_bit2() {
5018 let mut bus = test_bus();
5019 // Default: mic released -> $4016 bit 2 clear (byte-identical stock read).
5020 assert!(!bus.microphone());
5021 assert_eq!(bus.peek_cpu(0x4016) & 0x04, 0x00, "mic off -> D2 clear");
5022 // Press the mic: $4016 bit 2 reads 1.
5023 bus.set_microphone(true);
5024 assert!(bus.microphone());
5025 assert_eq!(bus.peek_cpu(0x4016) & 0x04, 0x04, "mic on -> D2 set");
5026 // $4017 is unaffected (the Famicom mic is a $4016-only signal).
5027 assert_eq!(bus.peek_cpu(0x4017) & 0x04, 0x00, "mic never touches $4017");
5028 // Release restores the stock read.
5029 bus.set_microphone(false);
5030 assert_eq!(
5031 bus.peek_cpu(0x4016) & 0x04,
5032 0x00,
5033 "mic released -> D2 clear"
5034 );
5035 }
5036
5037 #[test]
5038 fn four_score_off_reads_like_standard_controller() {
5039 let mut bus = test_bus();
5040 assert!(!bus.four_score());
5041 bus.set_buttons(0, Buttons::A);
5042 strobe(&mut bus);
5043 // A, then 7 zeros, then 1s — exactly the standard pad.
5044 assert_eq!(bus.read_port(0), 1);
5045 for _ in 0..7 {
5046 assert_eq!(bus.read_port(0), 0);
5047 }
5048 for _ in 0..3 {
5049 assert_eq!(bus.read_port(0), 1);
5050 }
5051 }
5052
5053 #[test]
5054 fn four_score_multiplexes_four_pads_and_signature() {
5055 let mut bus = test_bus();
5056 bus.set_four_score(true);
5057 bus.set_buttons(0, Buttons::A); // pad 1
5058 bus.set_buttons(2, Buttons::B); // pad 3
5059 bus.set_buttons(1, Buttons::SELECT); // pad 2
5060 bus.set_buttons(3, Buttons::START); // pad 4
5061 strobe(&mut bus);
5062
5063 // Port 0 ($4016): pad1 (A) | pad3 (B) | signature 0x08 (LSB-first) | 1.
5064 let p0: Vec<u8> = (0..25).map(|_| bus.read_port(0)).collect();
5065 assert_eq!(&p0[0..8], &[1, 0, 0, 0, 0, 0, 0, 0], "pad 1: A");
5066 assert_eq!(&p0[8..16], &[0, 1, 0, 0, 0, 0, 0, 0], "pad 3: B");
5067 assert_eq!(&p0[16..24], &[0, 0, 0, 1, 0, 0, 0, 0], "signature 0x08");
5068 assert_eq!(p0[24], 1, "past 24 reads -> 1");
5069
5070 // Port 1 ($4017): pad2 (Select) | pad4 (Start) | signature 0x04 | 1.
5071 let p1: Vec<u8> = (0..25).map(|_| bus.read_port(1)).collect();
5072 assert_eq!(&p1[0..8], &[0, 0, 1, 0, 0, 0, 0, 0], "pad 2: Select");
5073 assert_eq!(&p1[8..16], &[0, 0, 0, 1, 0, 0, 0, 0], "pad 4: Start");
5074 assert_eq!(&p1[16..24], &[0, 0, 1, 0, 0, 0, 0, 0], "signature 0x04");
5075 assert_eq!(p1[24], 1);
5076 }
5077
5078 #[test]
5079 fn four_score_state_round_trips_through_save_state() {
5080 let mut bus = test_bus();
5081 bus.set_four_score(true);
5082 bus.set_buttons(2, Buttons::B | Buttons::A); // pad 3
5083 bus.set_buttons(3, Buttons::START); // pad 4
5084 strobe(&mut bus);
5085 let _ = bus.read_port(0); // advance idx[0] off zero
5086 let blob = crate::bus_snapshot::encode_bus(&bus);
5087
5088 let mut restored = test_bus();
5089 crate::bus_snapshot::decode_bus(&mut restored, &blob).unwrap();
5090 assert!(restored.four_score());
5091 assert_eq!(restored.controller(2).buttons(), Buttons::B | Buttons::A);
5092 assert_eq!(restored.controller(3).buttons(), Buttons::START);
5093 }
5094
5095 #[test]
5096 fn override_nt_addr_maps_per_mirroring() {
5097 use rustynes_mappers::Mirroring;
5098 // Logical tables $2000/$2400/$2800/$2C00, offset 0.
5099 // Horizontal: tables 0/1 -> bank 0, 2/3 -> bank 1.
5100 assert_eq!(override_nt_addr(Mirroring::Horizontal, 0x2000), 0x000);
5101 assert_eq!(override_nt_addr(Mirroring::Horizontal, 0x2400), 0x000);
5102 assert_eq!(override_nt_addr(Mirroring::Horizontal, 0x2800), 0x400);
5103 assert_eq!(override_nt_addr(Mirroring::Horizontal, 0x2C00), 0x400);
5104 // Vertical: tables 0/2 -> bank 0, 1/3 -> bank 1.
5105 assert_eq!(override_nt_addr(Mirroring::Vertical, 0x2000), 0x000);
5106 assert_eq!(override_nt_addr(Mirroring::Vertical, 0x2400), 0x400);
5107 assert_eq!(override_nt_addr(Mirroring::Vertical, 0x2800), 0x000);
5108 assert_eq!(override_nt_addr(Mirroring::Vertical, 0x2C00), 0x400);
5109 // Local offset preserved.
5110 assert_eq!(override_nt_addr(Mirroring::Vertical, 0x2456), 0x456);
5111 }
5112
5113 #[test]
5114 fn mirroring_override_round_trips_through_save_state() {
5115 use rustynes_mappers::Mirroring;
5116 let mut bus = test_bus();
5117 assert_eq!(bus.mirroring_override(), None, "default is no override");
5118 bus.set_mirroring_override(Some(Mirroring::Vertical));
5119 let blob = crate::bus_snapshot::encode_bus(&bus);
5120 let mut restored = test_bus();
5121 crate::bus_snapshot::decode_bus(&mut restored, &blob).unwrap();
5122 assert_eq!(restored.mirroring_override(), Some(Mirroring::Vertical));
5123 }
5124
5125 #[test]
5126 fn pre_v1_7_0_save_state_decodes_with_four_score_off() {
5127 // A v1.7.0 blob carries 11 trailing Four Score bytes (1 flag + 6
5128 // controllers34 + 2 idx + 2 sig); the W3-Stage-4 tail appends 22
5129 // more (dmc_halt + 3 uni_oam flags + uni_oam_addr u16 + ppu_clock
5130 // u64 + dma_mc_consumed u64); the v2.1.0 tail appends 2 more (one
5131 // expansion-device tag byte per port, both `None`); the v1.1.0 beta.1
5132 // tail appends 1 more (the nametable mirroring-override tag, `None`).
5133 // Truncating all 36 simulates a pre-v1.7.0 save, which must still load
5134 // with the adapter off (and no expansion device / override).
5135 let mut bus = test_bus();
5136 bus.set_four_score(true);
5137 let blob = crate::bus_snapshot::encode_bus(&bus);
5138 let old = &blob[..blob.len() - 36];
5139 let mut restored = test_bus();
5140 restored.set_four_score(true); // prove decode actively turns it off
5141 crate::bus_snapshot::decode_bus(&mut restored, old).unwrap();
5142 assert!(!restored.four_score());
5143 }
5144
5145 #[test]
5146 fn expansion_device_state_round_trips_through_save_state() {
5147 use crate::input_device::{InputDevice, VausState, ZapperState};
5148 let mut bus = test_bus();
5149 // Vaus on port 0, Zapper on port 1, with distinctive non-default state.
5150 bus.set_expansion_device(0, Some(InputDevice::Vaus(VausState::new())));
5151 bus.set_paddle(0, 0x3C, true);
5152 bus.set_expansion_device(1, Some(InputDevice::Zapper(ZapperState::new())));
5153 bus.set_zapper(1, 100, 50, true);
5154 let blob = crate::bus_snapshot::encode_bus(&bus);
5155
5156 let mut restored = test_bus();
5157 crate::bus_snapshot::decode_bus(&mut restored, &blob).unwrap();
5158 match restored.expansion_device(0) {
5159 Some(InputDevice::Vaus(v)) => {
5160 assert_eq!(v.position_raw(), 0x3C);
5161 assert!(v.fire_raw());
5162 }
5163 other => panic!("port 0 should be a Vaus, got {other:?}"),
5164 }
5165 match restored.expansion_device(1) {
5166 Some(InputDevice::Zapper(z)) => {
5167 assert_eq!(z.x_raw(), 100);
5168 assert_eq!(z.y_raw(), 50);
5169 assert!(z.trigger_raw());
5170 }
5171 other => panic!("port 1 should be a Zapper, got {other:?}"),
5172 }
5173 }
5174
5175 /// With the beam-relative Zapper model on, a debugger peek of `$4017` must
5176 /// return the SAME light contribution the CPU read produces — at the
5177 /// pre-render line and at a visible line — and must not advance device
5178 /// state.
5179 ///
5180 /// Regression pin for the `peek_port` parity fix: before it, `peek_port`
5181 /// fell through to the overlay's frame-granular `peek()` and could report a
5182 /// different light bit than `read_port` at the same instant. (This is the
5183 /// real defect the fix addressed; the separate `read_before_visible`
5184 /// conversion fallback is defensive, since `scanline()` is non-negative on
5185 /// every current region — pre-render is line 261 NTSC / 311 PAL, not -1.)
5186 #[test]
5187 fn temporal_zapper_debugger_peek_matches_cpu_read() {
5188 use crate::input_device::{InputDevice, ZapperState};
5189
5190 // $4017 bit 3 is the (inverted) light bit; the open-bus upper bits differ
5191 // between the read and peek paths, so compare only the device bit.
5192 const LIGHT: u8 = 0b0000_1000;
5193
5194 // The two models are constructed to DISAGREE, so the test fails if
5195 // `peek_port` does not mirror `read_port`'s temporal branch:
5196 // * frame model (`peek()` -> `ZapperState::read()`) reads `light_seen`,
5197 // which we force TRUE via `from_parts` -> reports light;
5198 // * temporal model (`read_at_scanline`) reads the current scanline. A
5199 // fresh bus sits on the pre-render line (261 NTSC), past the
5200 // photodiode hold window -> reports NO light.
5201 // So a peek that (wrongly) fell through to the frame `peek()` would
5202 // return light while the CPU read returns none. No framebuffer or
5203 // scanline poke is needed — the injected `light_seen` supplies the
5204 // divergence, and the default dark framebuffer keeps the temporal path
5205 // at no-light on every line anyway.
5206 let mut bus = test_bus();
5207 // from_parts(x, y, trigger, light_seen): trigger + light_seen both true.
5208 let zapper = ZapperState::from_parts(128, 12, true, true);
5209 bus.set_expansion_device(1, Some(InputDevice::Zapper(zapper)));
5210 bus.set_zapper_temporal_light(true);
5211
5212 assert!(
5213 bus.ppu.scanline() > 239,
5214 "fresh PPU is on the pre-render line"
5215 );
5216 let cpu = bus.read_port(1) & LIGHT;
5217 let peek = bus.peek_port(1) & LIGHT;
5218 assert_eq!(cpu, LIGHT, "temporal read at pre-render reports NO light");
5219 assert_eq!(
5220 peek, cpu,
5221 "debugger peek must match the CPU read, not the frame `peek()` \
5222 (which would report light from the injected light_seen)",
5223 );
5224
5225 // The peek must be side-effect-free: repeating it does not change the
5226 // answer (guards a regression where a peek routes through mutating state).
5227 assert_eq!(bus.peek_port(1) & LIGHT, peek);
5228 assert_eq!(bus.peek_port(1) & LIGHT, peek);
5229 }
5230
5231 #[test]
5232 fn pre_v2_1_0_save_state_decodes_with_no_expansion_device() {
5233 // A pre-v2.1.0 blob lacks the 2 trailing device-tag bytes (one None
5234 // tag per port); a pre-v1.1.0 blob also lacks the mirroring-override
5235 // tag. With nothing attached the encoder writes `[0, 0]` + `[0]`, so
5236 // truncating those 3 trailing bytes reproduces an older save — which
5237 // must still load with both ports unplugged and no override.
5238 let bus = test_bus();
5239 let blob = crate::bus_snapshot::encode_bus(&bus);
5240 let old = &blob[..blob.len() - 3];
5241 let mut restored = test_bus();
5242 crate::bus_snapshot::decode_bus(&mut restored, old).unwrap();
5243 assert!(restored.expansion_device(0).is_none());
5244 assert!(restored.expansion_device(1).is_none());
5245 assert_eq!(restored.mirroring_override(), None);
5246 }
5247
5248 #[test]
5249 fn power_pad_state_round_trips_through_save_state() {
5250 use crate::input_device::{InputDevice, PowerPadState};
5251 let mut bus = test_bus();
5252 bus.set_expansion_device(1, Some(InputDevice::PowerPad(PowerPadState::new())));
5253 bus.set_power_pad(1, 0b1010_0101_0011);
5254 let blob = crate::bus_snapshot::encode_bus(&bus);
5255 let mut restored = test_bus();
5256 crate::bus_snapshot::decode_bus(&mut restored, &blob).unwrap();
5257 match restored.expansion_device(1) {
5258 Some(InputDevice::PowerPad(p)) => {
5259 assert_eq!(p.buttons_raw(), 0b1010_0101_0011);
5260 }
5261 other => panic!("expected a Power Pad on port 1, got {other:?}"),
5262 }
5263 }
5264
5265 #[test]
5266 fn snes_mouse_state_round_trips_through_save_state() {
5267 use crate::input_device::{InputDevice, SnesMouseState};
5268 let mut bus = test_bus();
5269 bus.set_expansion_device(0, Some(InputDevice::SnesMouse(SnesMouseState::new())));
5270 bus.set_snes_mouse(0, -7, 9, true, false, 2);
5271 let blob = crate::bus_snapshot::encode_bus(&bus);
5272 let mut restored = test_bus();
5273 crate::bus_snapshot::decode_bus(&mut restored, &blob).unwrap();
5274 match restored.expansion_device(0) {
5275 Some(InputDevice::SnesMouse(m)) => {
5276 assert_eq!(m.dx_raw(), -7);
5277 assert_eq!(m.dy_raw(), 9);
5278 assert!(m.left_raw());
5279 assert!(!m.right_raw());
5280 assert_eq!(m.sensitivity_raw(), 2);
5281 }
5282 other => panic!("expected a SNES mouse on port 0, got {other:?}"),
5283 }
5284 }
5285
5286 #[test]
5287 fn family_keyboard_state_round_trips_through_save_state() {
5288 use crate::input_device::{FamilyKeyboardState, InputDevice};
5289 let mut bus = test_bus();
5290 bus.set_expansion_device(
5291 1,
5292 Some(InputDevice::FamilyKeyboard(FamilyKeyboardState::new())),
5293 );
5294 let keys = [0x01, 0x10, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00];
5295 bus.set_family_keyboard(1, keys);
5296 let blob = crate::bus_snapshot::encode_bus(&bus);
5297 let mut restored = test_bus();
5298 crate::bus_snapshot::decode_bus(&mut restored, &blob).unwrap();
5299 match restored.expansion_device(1) {
5300 Some(InputDevice::FamilyKeyboard(k)) => {
5301 assert_eq!(k.keys_raw(), keys);
5302 }
5303 other => panic!("expected a Family BASIC keyboard on port 1, got {other:?}"),
5304 }
5305 }
5306}