Skip to main content

rustynes_cpu/
bus.rs

1//! CPU `Bus` trait.
2//!
3//! Per `docs/cpu-6502.md` §Interfaces. The trait is the whole surface the
4//! 6502 core sees: address-fanout reads/writes, the per-cycle hooks the
5//! one-clock scheduler calls in each half of a CPU cycle (`run_ppu_to`,
6//! `cpu_clock`, `cpu_clock_apu_dmc`), the live /IRQ and /NMI line levels the
7//! CPU edge-detects itself, and the unified DMC/OAM DMA engine's per-cycle
8//! entry points. Every method other than `cpu_read` / `cpu_write` has a
9//! default, so a test bus implements only what it models.
10//!
11//! v2.9.8 removed the 18 methods deprecated at v2.7.5 (ADR 0042): the
12//! `poll_nmi` / `poll_irq` family, the pre-v2.0.0 per-phase hooks
13//! (`cpu_cycle_phi1` / `cpu_cycle_phi2`), `internal_data_bus`, and the
14//! per-engine DMC / OAM / overlap DMA hooks the unified engine replaced. None
15//! had a caller after the v2.0.0 one-clock scheduler.
16
17/// Address-space bus seen by the CPU.
18///
19/// The CPU borrows `&mut Bus` for the duration of an instruction; the bus
20/// fans the access out to RAM, PPU registers, APU registers, controllers,
21/// and the cartridge's mapper.
22pub trait Bus {
23    /// Read a byte at `addr`.
24    fn cpu_read(&mut self, addr: u16) -> u8;
25
26    /// Write `value` to `addr`.
27    fn cpu_write(&mut self, addr: u16, value: u8);
28
29    /// Called once per CPU cycle consumed. Used by the scheduler to advance
30    /// the PPU/APU in lockstep (Phase 2+) and by the test harness to count
31    /// cycles for golden-log compare.
32    fn on_cpu_cycle(&mut self) {}
33
34    /// Notify the bus that the CPU is about to perform an interrupt
35    /// vector fetch from `vector` (`$FFFE` for IRQ/BRK, `$FFFA` for NMI,
36    /// or `$FFFA` if an IRQ/BRK service sequence was hijacked by an NMI
37    /// edge during cycles 1..=5 of the service sequence).  `is_nmi` is
38    /// `true` for an NMI service entry and `false` for an IRQ or BRK
39    /// service entry (so the bus can distinguish hijack from a clean
40    /// NMI even when the vector is the same).
41    ///
42    /// Default impl is a no-op; production buses with the
43    /// `irq-timing-trace` feature override this to emit a
44    /// [`ServiceEvent`] into the IRQ trace fixture.  Phase 1.2 of
45    /// Track C1 attempt 14 added this method to close the schema gap
46    /// with Mesen2's `emu.eventType.irq` / `emu.eventType.nmi` oracle.
47    ///
48    /// [`ServiceEvent`]: # "see rustynes_core::irq_trace::ServiceEvent"
49    fn notify_irq_service(&mut self, vector: u16, is_nmi: bool) {
50        let _ = vector;
51        let _ = is_nmi;
52    }
53
54    /// Cumulative bus-side cycle counter.
55    ///
56    /// On the production `SystemBus`, this is `self.cycle` —
57    /// the total number of CPU cycles the bus has ticked, INCLUDING
58    /// DMC DMA halt + dummy + alignment + transfer cycles. Every one of
59    /// them, DMA or not, runs through `Cpu::start_cycle`, which calls the
60    /// bus's per-cycle `cpu_clock` (the only place `self.cycle` advances) and
61    /// then copies this count into `Cpu::cycles`. (Until v2.9.8 this said
62    /// the DMA cycles advanced through `bus.tick_one_cpu_cycle()` and that
63    /// `Cpu::cycles` missed them; that path was removed at v2.9.8, ADR 0042,
64    /// and the copy has made the two counts agree since the v2.0.0 one-clock
65    /// scheduler.)
66    ///
67    /// Used by the SH* unstable-store family (`SHA / SHX / SHY /
68    /// SHS / TAS`) to detect when DMC DMA interrupted the
69    /// instruction's dummy-read cycle: per Mesen2 `NesCpu.h`
70    /// `SyaSxaAxa` (lines 716-745), if the dummy read consumed
71    /// more than 1 bus cycle, a DMA fired, and the value written
72    /// is `valueReg` un-ANDed with the H+1 byte (the DMA pulled
73    /// the bus low / corrupted the latch).  Mesen2 detects this
74    /// via `_state.CycleCount - cyc > 1` after the dummy read;
75    /// we mirror via `bus.cycle_count() - before > 1`.
76    ///
77    /// Default impl returns `0` for legacy / test bus stubs.
78    fn cycle_count(&self) -> u64 {
79        0
80    }
81
82    // ================================================================
83    // The one-clock scheduler's contract (ADR 0002 / ADR 0029).
84    //
85    // `Cpu::start_cycle` / `Cpu::end_cycle` call these around every access.
86    // The defaults delegate to `cpu_read` / `cpu_write` / `on_cpu_cycle`, so
87    // a simple test bus keeps working without modelling the split; the
88    // production `SystemBus` overrides them with the real master-clock
89    // catch-up. History: `docs/audit/v2.0-master-clock-r1-port-plan-2026-06-03.md`.
90    // ================================================================
91
92    /// Pure address-space read (no per-cycle work). Under R1 the cycle work
93    /// is done by [`Bus::run_ppu_to`] + [`Bus::cpu_clock`], which the CPU
94    /// calls around the access. Default delegates to [`Bus::cpu_read`].
95    fn read(&mut self, addr: u16) -> u8 {
96        self.cpu_read(addr)
97    }
98
99    /// Pure address-space write. Default delegates to [`Bus::cpu_write`].
100    fn write(&mut self, addr: u16, value: u8) {
101        self.cpu_write(addr, value);
102    }
103
104    /// Master clocks per CPU cycle for the cartridge region: NTSC 12, PAL 16,
105    /// Dendy 15 (the master-clock unit is shared with [`Bus::run_ppu_to`]'s
106    /// `ppu_divider`, so per CPU cycle the PPU advances `cpu_divider /
107    /// ppu_divider` dots — 3:1 NTSC, 3.2:1 PAL, 3:1 Dendy). The R1 CPU loop
108    /// advances `master_clock` and derives its read/write split off this. The
109    /// default (12) keeps test stubs + the non-regioned path on NTSC; the
110    /// `SystemBus` overrides from the cartridge region.
111    fn cpu_divider(&self) -> u64 {
112        12
113    }
114
115    /// Catch the PPU up to `target` master clocks (Mesen `NesPpu::Run` /
116    /// `TetaNES` `clock_to`). Ticks whole PPU dots while
117    /// `ppu_clock + ppu_divider <= target`. Called by the R1 CPU loop in
118    /// BOTH halves of each access (the double catch-up). Default no-op.
119    ///
120    /// `is_post_access` distinguishes WHICH half of the CPU cycle this
121    /// catch-up belongs to: `false` for the pre-access half (called from
122    /// `Cpu::start_cycle`, before the bus access — mirrors Mesen's
123    /// `StartCpuCycle`), `true` for the post-access half (called from
124    /// `Cpu::end_cycle`, after the bus access — mirrors `EndCpuCycle`).
125    /// R1c-3 (v2.0.0's `mmc3-m2-phase-irq`, removed at v2.9.9; the
126    /// `mmc3-a12-phase-probe` feature still uses it): `SystemBus`
127    /// forwards this as the real M2-phase label on the `PpuBusAdapter` it
128    /// constructs, replacing the previously call-local (and therefore
129    /// almost-always-zero) `sub_dot` counter with a value that actually
130    /// distinguishes the pre-access (M2-low, φ1) and post-access
131    /// (M2-high, φ2) halves for any A12 transition ticked during this
132    /// catch-up. See `docs/adr/0002-irq-timing-coordination.md` and
133    /// `docs/audit/r1r2-per-dot-scheduler-attempt-2026-07-02.md`.
134    fn run_ppu_to(&mut self, target: u64, is_post_access: bool) {
135        let _ = (target, is_post_access);
136    }
137
138    /// One CPU cycle of bus-side work (Mesen `ProcessCpuClock`): APU +
139    /// frame counter + per-cycle mapper hook + bus-side DMA drain + cycle
140    /// counter. The PPU advance is in [`Bus::run_ppu_to`], not here. Default
141    /// delegates to [`Bus::on_cpu_cycle`] (legacy combined per-cycle work).
142    fn cpu_clock(&mut self) {
143        self.on_cpu_cycle();
144    }
145
146    /// F-2: tick ONLY the DMC byte-timer + DMA arm, at END of cycle (called
147    /// from `Cpu::end_cycle` after the access + PPU catch-up). This places the
148    /// DMC fire-phase at main's end-of-cycle position (so `DMASync`'s `$4000`
149    /// open-bus conflict lands), while the rest of the APU (incl. the IRQ line)
150    /// stays on the cycle-start `cpu_clock` tick (so the C1 φ2 IRQ sample is
151    /// unchanged). Default no-op. Pairs with `Apu::set_dmc_driven_externally`.
152    fn cpu_clock_apu_dmc(&mut self) {}
153
154    /// Live IRQ line level (mapper IRQ OR APU frame-counter/DMC IRQ). The
155    /// CPU does the I-flag mask + one-cycle `prev_run_irq` delay itself.
156    /// Default `false`; the production bus overrides this.
157    fn irq_level(&self) -> bool {
158        false
159    }
160
161    /// Live /NMI line level (PPU-driven). The CPU does its own edge detect +
162    /// one-cycle `prev_need_nmi` delay. Default `false` (test stubs).
163    fn nmi_level(&self) -> bool {
164        false
165    }
166
167    /// `mc-r1-dmc-load-get-entry`: defer a LOAD whose first-service would be a PUT
168    /// cycle by 1 CPU cycle so it enters on a GET (span-3 hardware load). Gates BOTH
169    /// the read1 loop AND the `idle_tick` loop (`DMASync`'s load fires during NOPs=idle).
170    fn dmc_dma_defer_load_entry(&self) -> bool {
171        false
172    }
173
174    /// W3-Stage-1 (`mc-r1-dma-unified`): is ANY DMA work pending for the
175    /// unified DMC/OAM engine — a serviceable DMC DMA (pending and not a
176    /// load deferred to its get-cycle entry, the `mc-r1-dmc-load-get-entry`
177    /// rule), a `$4014` OAM DMA awaiting its first cycle, or an OAM transfer
178    /// still in flight? The ONE `Cpu::read1`/`idle_tick` DMA loop spins on
179    /// this, running one [`Bus::unified_dma_cycle`] per CPU cycle (each a
180    /// full R1 cycle: `start_cycle` -> dispatch -> `end_cycle`, so every DMA
181    /// cycle keeps the φ2 IRQ sample — the C1-safe shape). Default `false`.
182    fn unified_dma_pending(&self) -> bool {
183        false
184    }
185
186    /// W3-Stage-1 (`mc-r1-dma-unified`): ONE cycle of the unified DMC/OAM DMA
187    /// engine — a direct port of the `TriCNES` `_6502` per-cycle DMA dispatch
188    /// (recorded as a derivation in the `// Provenance:` header of
189    /// `rustynes-core/src/bus.rs`, where the engine lives; v2.9.9, NC-17)
190    /// table (the SINGLE driver standalone DMC, standalone OAM, and the
191    /// overlap all ride), at FLOOR parity for this stage. `halted_addr` is
192    /// the CPU read the DMA is preempting (the parked 6502 address bus).
193    /// Does NOT advance time — the surrounding `start_cycle`/`end_cycle` do.
194    /// Default no-op.
195    fn unified_dma_cycle(&mut self, halted_addr: u16) {
196        let _ = halted_addr;
197    }
198
199    /// W3-Stage-1 (`mc-r1-dma-unified`): one unified-engine DMA cycle during
200    /// a CPU INTERNAL cycle (no instruction read; the bus supplies its held
201    /// last-read address). Default no-op.
202    fn unified_dma_cycle_idle(&mut self) {}
203
204    /// accuracycoin-100 Phase 2 (`mc-r1-dmc-abort-cancel`): is a 1-byte
205    /// non-looping implicit DMC-DMA abort matured and awaiting service? The CPU
206    /// consults this at the top of `read1`/`write1`. Default `false`.
207    fn dmc_abort_pending(&self) -> bool {
208        false
209    }
210
211    /// accuracycoin-100 Phase 2: is the upcoming cycle a GET (read) cycle for
212    /// the DMC DMA (`!put_cycle`)? On a get cycle the matured abort runs as a
213    /// 1-cycle DMA (Y=1); on a put cycle (or any CPU write) it does NOT occur
214    /// (Y=0, "the abort will not land on a write cycle"). Default `false`.
215    fn dmc_abort_is_get_cycle(&self) -> bool {
216        false
217    }
218
219    /// accuracycoin-100 Phase 2: service the matured abort as a 1-cycle DMA
220    /// (Y=1) — one halt re-read of `halted_addr`, then clear the abort + the
221    /// pending reload. Called by `read1` only on a get cycle. Default no-op.
222    fn dmc_abort_halt_step(&mut self, halted_addr: u16) {
223        let _ = halted_addr;
224    }
225
226    /// accuracycoin-100 Phase 2: cancel the matured abort with NO halt cycle
227    /// (Y=0) — the abort lands on a write/put cycle so the DMA does not occur.
228    /// Clears the abort + the pending reload. Default no-op.
229    fn dmc_abort_cancel(&mut self) {}
230
231    /// Diagnostic-only hook fired once per R1 CPU cycle from `Cpu::end_cycle`
232    /// (after `handle_interrupts`), so the `irq-timing-trace` tooling can
233    /// record a `CycleRecord` for each CPU cycle. (Until v2.9.8 this said the
234    /// R1 path bypassed a `tick_one_cpu_cycle` push; that method was removed
235    /// at v2.9.8, ADR 0042, and this hook is now the trace's per-cycle point.)
236    /// Default no-op; the production
237    /// bus overrides it only under the `irq-timing-trace` feature, so non-trace
238    /// R1 builds compile this to an empty call.
239    fn trace_end_cycle(&mut self) {}
240
241    /// Diagnostic-only hook fired once per CPU INSTRUCTION from `Cpu::step`
242    /// (at the opcode fetch), with the instruction's `pc` and the cumulative
243    /// CPU cycle count. Lets the `cpu-instr-cycle-trace` tooling diff R1 vs
244    /// default per-instruction to pin the cumulative cycle-count divergence
245    /// (the R1c-1 odd-cycle source). Default no-op.
246    #[cfg(feature = "cpu-instr-cycle-trace")]
247    fn trace_instr(&mut self, _pc: u16, _cpu_cycle: u64) {}
248}