Skip to main content

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}