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}