rustysnes_core/dma_bus.rs
1//! The narrow bus interface the DMA controller drives.
2//!
3//! The DMA controller moves bytes between the **A-bus** (the 24-bit CPU address space — WRAM,
4//! ROM, SRAM) and the **B-bus** (the `$2100-$21FF` PPU/APU register window, addressed by its
5//! low byte). Keeping this a trait decouples [`crate::dma::Dma`] from the concrete [`crate::Bus`]
6//! so the transfer logic is unit-testable in isolation. The A-bus invalid-region rules (DMA
7//! cannot touch `$2100-$21FF`, `$4000-$43FF` via the A-bus) and the WRAM↔WRAM exclusion are
8//! enforced by the concrete `Bus` impl, which returns open bus / drops the write there.
9
10/// Read/write split across the SNES A-bus (24-bit) and B-bus (`$21xx`) for the DMA controller.
11pub trait DmaBus {
12 /// Read a byte from the A-bus (24-bit CPU address). Invalid A-bus regions return open bus.
13 fn read_a(&mut self, addr: u32) -> u8;
14
15 /// Write a byte to the A-bus (24-bit CPU address). Invalid regions are dropped.
16 fn write_a(&mut self, addr: u32, val: u8);
17
18 /// Read a byte from the B-bus register `$2100 | addr`.
19 fn read_b(&mut self, addr: u8) -> u8;
20
21 /// Write a byte to the B-bus register `$2100 | addr`.
22 fn write_b(&mut self, addr: u8, val: u8);
23
24 /// Advance the master clock by `clocks` *during* a transfer, so time-dependent state that
25 /// gates the transfer — chiefly the PPU scanline that decides VRAM/CGRAM/OAM accessibility
26 /// (`$2118`/`$2119` land only in force-blank or V-blank) — tracks the transfer in lockstep,
27 /// exactly as the real bus keeps scanning while a DMA runs (ares `Channel::step` →
28 /// `cpu.step`). A big GP-DMA that starts late in active display and crosses into V-blank must
29 /// have its V-blank portion actually reach VRAM; freezing the scanline for the whole burst
30 /// would drop every write (the Star Fox framebuffer-transfer bug). Default no-op so the
31 /// controller stays unit-testable against an isolated bus with no clock.
32 fn step(&mut self, _clocks: u32) {}
33
34 /// The PPU scanline the bus is currently scanning. On real hardware HDMA preempts a running
35 /// GP-DMA at the *start of every scanline*; a long GP-DMA that spans scanline boundaries must
36 /// therefore let those HDMA transfers interleave (Star Fox force-blanks its framebuffer DMA
37 /// via an HDMA-driven `$2100` write on the very lines the DMA is filling). [`crate::dma::Dma`]
38 /// drives that interleave itself from inside `run_gp` because the bus's own per-tick HDMA path
39 /// is dormant while the bus has lent out its controller. Default `u16::MAX` so a clockless
40 /// unit-test bus never trips the interleave.
41 fn scanline(&self) -> u16 {
42 u16::MAX
43 }
44
45 /// The last visible scanline HDMA services this frame (`visible_height`), so the in-GP-DMA
46 /// interleave knows the visible-line window. Default `0` (unit-test buses run no HDMA).
47 fn visible_height(&self) -> u16 {
48 0
49 }
50
51 /// The scanline the bus last serviced HDMA for, so the in-GP-DMA interleave resumes from the
52 /// bus's own bookkeeping and hands it back afterwards (no line runs twice, none is skipped).
53 /// Default `u16::MAX`.
54 fn hdma_last_line(&self) -> u16 {
55 u16::MAX
56 }
57
58 /// Record that HDMA has now been serviced for `line`, syncing the bus's bookkeeping with the
59 /// interleave driven from inside `run_gp`. Default no-op.
60 fn set_hdma_last_line(&mut self, _line: u16) {}
61}