pub trait Bus {
Show 22 methods
// Required methods
fn cpu_read(&mut self, addr: u16) -> u8;
fn cpu_write(&mut self, addr: u16, value: u8);
// Provided methods
fn on_cpu_cycle(&mut self) { ... }
fn notify_irq_service(&mut self, vector: u16, is_nmi: bool) { ... }
fn cycle_count(&self) -> u64 { ... }
fn read(&mut self, addr: u16) -> u8 { ... }
fn write(&mut self, addr: u16, value: u8) { ... }
fn cpu_divider(&self) -> u64 { ... }
fn run_ppu_to(&mut self, target: u64, is_post_access: bool) { ... }
fn cpu_clock(&mut self) { ... }
fn cpu_clock_apu_dmc(&mut self) { ... }
fn irq_level(&self) -> bool { ... }
fn nmi_level(&self) -> bool { ... }
fn dmc_dma_defer_load_entry(&self) -> bool { ... }
fn unified_dma_pending(&self) -> bool { ... }
fn unified_dma_cycle(&mut self, halted_addr: u16) { ... }
fn unified_dma_cycle_idle(&mut self) { ... }
fn dmc_abort_pending(&self) -> bool { ... }
fn dmc_abort_is_get_cycle(&self) -> bool { ... }
fn dmc_abort_halt_step(&mut self, halted_addr: u16) { ... }
fn dmc_abort_cancel(&mut self) { ... }
fn trace_end_cycle(&mut self) { ... }
}Expand description
Address-space bus seen by the CPU.
The CPU borrows &mut Bus for the duration of an instruction; the bus
fans the access out to RAM, PPU registers, APU registers, controllers,
and the cartridge’s mapper.
Required Methods§
Provided Methods§
Sourcefn on_cpu_cycle(&mut self)
fn on_cpu_cycle(&mut self)
Called once per CPU cycle consumed. Used by the scheduler to advance the PPU/APU in lockstep (Phase 2+) and by the test harness to count cycles for golden-log compare.
Sourcefn notify_irq_service(&mut self, vector: u16, is_nmi: bool)
fn notify_irq_service(&mut self, vector: u16, is_nmi: bool)
Notify the bus that the CPU is about to perform an interrupt
vector fetch from vector ($FFFE for IRQ/BRK, $FFFA for NMI,
or $FFFA if an IRQ/BRK service sequence was hijacked by an NMI
edge during cycles 1..=5 of the service sequence). is_nmi is
true for an NMI service entry and false for an IRQ or BRK
service entry (so the bus can distinguish hijack from a clean
NMI even when the vector is the same).
Default impl is a no-op; production buses with the
irq-timing-trace feature override this to emit a
ServiceEvent into the IRQ trace fixture. Phase 1.2 of
Track C1 attempt 14 added this method to close the schema gap
with Mesen2’s emu.eventType.irq / emu.eventType.nmi oracle.
Sourcefn cycle_count(&self) -> u64
fn cycle_count(&self) -> u64
Cumulative bus-side cycle counter.
On the production SystemBus, this is self.cycle —
the total number of CPU cycles the bus has ticked, INCLUDING
DMC DMA halt + dummy + alignment + transfer cycles. Every one of
them, DMA or not, runs through Cpu::start_cycle, which calls the
bus’s per-cycle cpu_clock (the only place self.cycle advances) and
then copies this count into Cpu::cycles. (Until v2.9.8 this said
the DMA cycles advanced through bus.tick_one_cpu_cycle() and that
Cpu::cycles missed them; that path was removed at v2.9.8, ADR 0042,
and the copy has made the two counts agree since the v2.0.0 one-clock
scheduler.)
Used by the SH* unstable-store family (SHA / SHX / SHY / SHS / TAS) to detect when DMC DMA interrupted the
instruction’s dummy-read cycle: per Mesen2 NesCpu.h
SyaSxaAxa (lines 716-745), if the dummy read consumed
more than 1 bus cycle, a DMA fired, and the value written
is valueReg un-ANDed with the H+1 byte (the DMA pulled
the bus low / corrupted the latch). Mesen2 detects this
via _state.CycleCount - cyc > 1 after the dummy read;
we mirror via bus.cycle_count() - before > 1.
Default impl returns 0 for legacy / test bus stubs.
Sourcefn read(&mut self, addr: u16) -> u8
fn read(&mut self, addr: u16) -> u8
Pure address-space read (no per-cycle work). Under R1 the cycle work
is done by Bus::run_ppu_to + Bus::cpu_clock, which the CPU
calls around the access. Default delegates to Bus::cpu_read.
Sourcefn write(&mut self, addr: u16, value: u8)
fn write(&mut self, addr: u16, value: u8)
Pure address-space write. Default delegates to Bus::cpu_write.
Sourcefn cpu_divider(&self) -> u64
fn cpu_divider(&self) -> u64
Master clocks per CPU cycle for the cartridge region: NTSC 12, PAL 16,
Dendy 15 (the master-clock unit is shared with Bus::run_ppu_to’s
ppu_divider, so per CPU cycle the PPU advances cpu_divider / ppu_divider dots — 3:1 NTSC, 3.2:1 PAL, 3:1 Dendy). The R1 CPU loop
advances master_clock and derives its read/write split off this. The
default (12) keeps test stubs + the non-regioned path on NTSC; the
SystemBus overrides from the cartridge region.
Sourcefn run_ppu_to(&mut self, target: u64, is_post_access: bool)
fn run_ppu_to(&mut self, target: u64, is_post_access: bool)
Catch the PPU up to target master clocks (Mesen NesPpu::Run /
TetaNES clock_to). Ticks whole PPU dots while
ppu_clock + ppu_divider <= target. Called by the R1 CPU loop in
BOTH halves of each access (the double catch-up). Default no-op.
is_post_access distinguishes WHICH half of the CPU cycle this
catch-up belongs to: false for the pre-access half (called from
Cpu::start_cycle, before the bus access — mirrors Mesen’s
StartCpuCycle), true for the post-access half (called from
Cpu::end_cycle, after the bus access — mirrors EndCpuCycle).
R1c-3 (v2.0.0’s mmc3-m2-phase-irq, removed at v2.9.9; the
mmc3-a12-phase-probe feature still uses it): SystemBus
forwards this as the real M2-phase label on the PpuBusAdapter it
constructs, replacing the previously call-local (and therefore
almost-always-zero) sub_dot counter with a value that actually
distinguishes the pre-access (M2-low, φ1) and post-access
(M2-high, φ2) halves for any A12 transition ticked during this
catch-up. See docs/adr/0002-irq-timing-coordination.md and
docs/audit/r1r2-per-dot-scheduler-attempt-2026-07-02.md.
Sourcefn cpu_clock(&mut self)
fn cpu_clock(&mut self)
One CPU cycle of bus-side work (Mesen ProcessCpuClock): APU +
frame counter + per-cycle mapper hook + bus-side DMA drain + cycle
counter. The PPU advance is in Bus::run_ppu_to, not here. Default
delegates to Bus::on_cpu_cycle (legacy combined per-cycle work).
Sourcefn cpu_clock_apu_dmc(&mut self)
fn cpu_clock_apu_dmc(&mut self)
F-2: tick ONLY the DMC byte-timer + DMA arm, at END of cycle (called
from Cpu::end_cycle after the access + PPU catch-up). This places the
DMC fire-phase at main’s end-of-cycle position (so DMASync’s $4000
open-bus conflict lands), while the rest of the APU (incl. the IRQ line)
stays on the cycle-start cpu_clock tick (so the C1 φ2 IRQ sample is
unchanged). Default no-op. Pairs with Apu::set_dmc_driven_externally.
Sourcefn irq_level(&self) -> bool
fn irq_level(&self) -> bool
Live IRQ line level (mapper IRQ OR APU frame-counter/DMC IRQ). The
CPU does the I-flag mask + one-cycle prev_run_irq delay itself.
Default false; the production bus overrides this.
Sourcefn nmi_level(&self) -> bool
fn nmi_level(&self) -> bool
Live /NMI line level (PPU-driven). The CPU does its own edge detect +
one-cycle prev_need_nmi delay. Default false (test stubs).
Sourcefn dmc_dma_defer_load_entry(&self) -> bool
fn dmc_dma_defer_load_entry(&self) -> bool
mc-r1-dmc-load-get-entry: defer a LOAD whose first-service would be a PUT
cycle by 1 CPU cycle so it enters on a GET (span-3 hardware load). Gates BOTH
the read1 loop AND the idle_tick loop (DMASync’s load fires during NOPs=idle).
Sourcefn unified_dma_pending(&self) -> bool
fn unified_dma_pending(&self) -> bool
W3-Stage-1 (mc-r1-dma-unified): is ANY DMA work pending for the
unified DMC/OAM engine — a serviceable DMC DMA (pending and not a
load deferred to its get-cycle entry, the mc-r1-dmc-load-get-entry
rule), a $4014 OAM DMA awaiting its first cycle, or an OAM transfer
still in flight? The ONE Cpu::read1/idle_tick DMA loop spins on
this, running one Bus::unified_dma_cycle per CPU cycle (each a
full R1 cycle: start_cycle -> dispatch -> end_cycle, so every DMA
cycle keeps the φ2 IRQ sample — the C1-safe shape). Default false.
Sourcefn unified_dma_cycle(&mut self, halted_addr: u16)
fn unified_dma_cycle(&mut self, halted_addr: u16)
W3-Stage-1 (mc-r1-dma-unified): ONE cycle of the unified DMC/OAM DMA
engine — a direct port of the TriCNES _6502 per-cycle DMA dispatch
(recorded as a derivation in the // Provenance: header of
rustynes-core/src/bus.rs, where the engine lives; v2.9.9, NC-17)
table (the SINGLE driver standalone DMC, standalone OAM, and the
overlap all ride), at FLOOR parity for this stage. halted_addr is
the CPU read the DMA is preempting (the parked 6502 address bus).
Does NOT advance time — the surrounding start_cycle/end_cycle do.
Default no-op.
Sourcefn unified_dma_cycle_idle(&mut self)
fn unified_dma_cycle_idle(&mut self)
W3-Stage-1 (mc-r1-dma-unified): one unified-engine DMA cycle during
a CPU INTERNAL cycle (no instruction read; the bus supplies its held
last-read address). Default no-op.
Sourcefn dmc_abort_pending(&self) -> bool
fn dmc_abort_pending(&self) -> bool
accuracycoin-100 Phase 2 (mc-r1-dmc-abort-cancel): is a 1-byte
non-looping implicit DMC-DMA abort matured and awaiting service? The CPU
consults this at the top of read1/write1. Default false.
Sourcefn dmc_abort_is_get_cycle(&self) -> bool
fn dmc_abort_is_get_cycle(&self) -> bool
accuracycoin-100 Phase 2: is the upcoming cycle a GET (read) cycle for
the DMC DMA (!put_cycle)? On a get cycle the matured abort runs as a
1-cycle DMA (Y=1); on a put cycle (or any CPU write) it does NOT occur
(Y=0, “the abort will not land on a write cycle”). Default false.
Sourcefn dmc_abort_halt_step(&mut self, halted_addr: u16)
fn dmc_abort_halt_step(&mut self, halted_addr: u16)
accuracycoin-100 Phase 2: service the matured abort as a 1-cycle DMA
(Y=1) — one halt re-read of halted_addr, then clear the abort + the
pending reload. Called by read1 only on a get cycle. Default no-op.
Sourcefn dmc_abort_cancel(&mut self)
fn dmc_abort_cancel(&mut self)
accuracycoin-100 Phase 2: cancel the matured abort with NO halt cycle (Y=0) — the abort lands on a write/put cycle so the DMA does not occur. Clears the abort + the pending reload. Default no-op.
Sourcefn trace_end_cycle(&mut self)
fn trace_end_cycle(&mut self)
Diagnostic-only hook fired once per R1 CPU cycle from Cpu::end_cycle
(after handle_interrupts), so the irq-timing-trace tooling can
record a CycleRecord for each CPU cycle. (Until v2.9.8 this said the
R1 path bypassed a tick_one_cpu_cycle push; that method was removed
at v2.9.8, ADR 0042, and this hook is now the trace’s per-cycle point.)
Default no-op; the production
bus overrides it only under the irq-timing-trace feature, so non-trace
R1 builds compile this to an empty call.
Dyn Compatibility§
This trait is dyn compatible.
In older versions of Rust, dyn compatibility was called "object safety".