Skip to main content

rustynes_core/
bus.rs

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