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