Skip to main content

rustynes_ppu/
bus.rs

1//! PPU-side bus trait.
2//!
3//! The PPU owns its CIRAM (2 KiB nametable VRAM in real hardware), OAM, and
4//! palette RAM. CHR-ROM / CHR-RAM / nametable mirroring all go through the
5//! mapper's PPU port — modeled here as the `PpuBus` trait. Mappers also
6//! receive A12 transition notifications via `notify_a12` so MMC3 / MMC5 can
7//! drive their IRQ counters.
8//!
9//! Per `docs/ppu-2c02.md` §Interfaces.
10
11/// Bus interface the PPU sees.
12///
13/// In production the lockstep bus in `rustynes-core` routes:
14///
15/// - CHR reads/writes (`$0000-$1FFF`) → mapper.
16/// - Nametable reads/writes (`$2000-$3EFF`) → PPU's own CIRAM, with the
17///   mapper-supplied mirroring offset via [`PpuBus::nametable_address`].
18/// - A12 transitions → mapper.
19///
20/// In tests, a small in-memory [`PpuBus`] impl owns 8 KiB of CHR-RAM and a
21/// dummy mirroring map.
22pub trait PpuBus {
23    /// Read a byte at `addr`. The PPU passes addresses in the full
24    /// `$0000-$3FFF` window; the bus is responsible for routing CHR
25    /// (`$0000-$1FFF`) and nametables (`$2000-$3EFF`) appropriately.
26    fn ppu_read(&mut self, addr: u16) -> u8;
27
28    /// Read a byte from the pattern-table window (`$0000-$1FFF`) on behalf
29    /// of a *sprite* tile fetch. MMC5 in 8x16 sprite mode uses a different
30    /// CHR bank set (`$5120-$5127`) for sprite fetches than for BG; other
31    /// mappers default to the same path as [`Self::ppu_read`].
32    fn ppu_read_sprite(&mut self, addr: u16) -> u8 {
33        self.ppu_read(addr)
34    }
35
36    /// HD-pack tile identity: the ABSOLUTE post-banking offset into CHR-ROM for a
37    /// pattern-space address (`Some(offset)`), or `None` when CHR is RAM (or the
38    /// mapper doesn't expose it). `tile_index = offset / 16` keys Mesen CHR-ROM
39    /// `<tile>` replacements; `None` routes to the CHR-RAM content-hash path.
40    /// Default `None`; only consulted on the HD-pack fetch path.
41    fn chr_phys(&self, _addr: u16) -> Option<u32> {
42        None
43    }
44
45    /// Write a byte at `addr`.
46    fn ppu_write(&mut self, addr: u16, value: u8);
47
48    /// Optionally synthesize a nametable byte for `addr` ($2000-$3EFF).
49    ///
50    /// When the bus returns `Some(v)`, the PPU uses `v` directly and skips
51    /// its CIRAM read. MMC5 uses this for fill mode and ExRAM-as-nametable.
52    /// Default returns `None`.
53    fn peek_nametable(&mut self, _addr: u16) -> Option<u8> {
54        None
55    }
56
57    /// Optionally absorb a nametable write directly into mapper storage.
58    ///
59    /// Returns `true` if consumed; PPU then skips its CIRAM write. Default
60    /// returns `false`.
61    fn write_nametable(&mut self, _addr: u16, _value: u8) -> bool {
62        false
63    }
64
65    /// Optional per-tile extended attribute + CHR-bank override for the BG
66    /// tile currently being fetched (loopy-v passed in `v`). MMC5 in `$5104`
67    /// mode 01 (`ExGrafix`) returns `Some(...)` here. Default returns `None`.
68    fn peek_ex_attribute(&mut self, _v: u16) -> Option<ExAttribute> {
69        None
70    }
71
72    /// Optional vertical split-screen override for the BG fetch group about
73    /// to start at `(scanline_y, coarse_x)`. MMC5 with split enabled
74    /// (`$5200` bit 7) returns `Some(...)` here for tile columns that fall
75    /// within the alt region. Default returns `None`.
76    fn bg_split_state(&mut self, _scanline_y: u16, _coarse_x: u16) -> Option<BgSplitState> {
77        None
78    }
79
80    /// Notification of a PPU A12 line transition (rising or falling). The
81    /// PPU calls this on every transition, with `level = true` for high.
82    /// MMC3 / MMC5 use this internally for IRQ counter clocking.
83    fn notify_a12(&mut self, _level: bool) {}
84
85    /// Notification that the PPU is starting a new rendered scanline (visible
86    /// or pre-render). MMC5 uses this to drive its scanline IRQ counter.
87    /// Default no-op.
88    fn notify_scanline_start(&mut self) {}
89
90    /// Notification that the PPU has entered vertical blank. MMC5 uses this
91    /// to clear its "in-frame" flag. Default no-op.
92    fn notify_vblank(&mut self) {}
93
94    /// Resolve a logical nametable address in `$2000-$3EFF` to a CIRAM offset
95    /// in `0..0x800` under the mapper's currently-selected mirroring.
96    ///
97    /// Default impl uses a vertical-mirroring fallback so this trait remains
98    /// drop-in for ad-hoc test buses; the lockstep bus in `rustynes-core`
99    /// overrides this to delegate to `Mapper::nametable_address`.
100    fn nametable_address(&self, addr: u16) -> u16 {
101        // Default fallback: vertical mirroring (tables 0/2 -> bank 0, 1/3 -> bank 1).
102        let table = ((addr.wrapping_sub(0x2000)) / 0x0400) & 0x03;
103        let local = addr & 0x03FF;
104        ((table & 1) * 0x0400) | local
105    }
106}
107
108/// Re-export of the mapper-side per-tile extended-attribute info.
109///
110/// Lives in `rustynes-mappers` (the canonical owner) and is re-declared here as
111/// a small POD to avoid making `rustynes-ppu` depend on `rustynes-mappers`. The
112/// lockstep bus's `PpuBusAdapter` translates between the two.
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub struct ExAttribute {
115    /// 2-bit palette select for this tile.
116    pub palette: u8,
117    /// 12-bit physical CHR bank (4 KiB units) for this tile.
118    pub chr_bank: u16,
119}
120
121/// Vertical split-screen override (MMC5 `$5200`-`$5202` and equivalents).
122///
123/// Lives in `rustynes-mappers` (the canonical owner) and is re-declared here as
124/// a small POD to avoid making `rustynes-ppu` depend on `rustynes-mappers`. The
125/// lockstep bus's `PpuBusAdapter` translates between the two.
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub struct BgSplitState {
128    /// Synthesized nametable byte address for the alt region (`$2000-$3EFF`).
129    pub nt_addr: u16,
130    /// Synthesized attribute byte address for the alt region.
131    pub at_addr: u16,
132    /// Fine-Y (0..=7) for the alt region's logical row.
133    pub fine_y: u8,
134    /// 4 KiB CHR bank index for the alt region's BG pattern fetches.
135    pub chr_bank: u8,
136}