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}