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    /// v3.1.0 (`T-SPRITE-LIMIT`) — whether a CHR read through
46    /// [`Self::ppu_read`] / [`Self::ppu_read_sprite`] has no effect beyond
47    /// returning the byte. The "disable sprite limit" option makes extra,
48    /// display-only pattern reads, and only where this is `true`, so the
49    /// option can never change emulation. Default `true`; the core forwards the
50    /// mapper's answer (`Mapper::chr_reads_are_pure`).
51    fn chr_reads_are_pure(&self) -> bool {
52        true
53    }
54
55    /// Write a byte at `addr`.
56    fn ppu_write(&mut self, addr: u16, value: u8);
57
58    /// Whether `$3000-$3EFF` is independent cartridge RAM rather than a
59    /// mirror of `$2000-$2EFF` (the mapper's `nametable_unfolded`). When it
60    /// is, the PPU passes those addresses unfolded to
61    /// [`Self::peek_nametable`] and [`Self::write_nametable`]. Default:
62    /// `false`.
63    fn nametable_unfolded(&self) -> bool {
64        false
65    }
66
67    /// Optionally synthesize a nametable byte for `addr` ($2000-$3EFF).
68    ///
69    /// When the bus returns `Some(v)`, the PPU uses `v` directly and skips
70    /// its CIRAM read. MMC5 uses this for fill mode and ExRAM-as-nametable.
71    /// Default returns `None`.
72    fn peek_nametable(&mut self, _addr: u16) -> Option<u8> {
73        None
74    }
75
76    /// Optionally absorb a nametable write directly into mapper storage.
77    ///
78    /// Returns `true` if consumed; PPU then skips its CIRAM write. Default
79    /// returns `false`.
80    fn write_nametable(&mut self, _addr: u16, _value: u8) -> bool {
81        false
82    }
83
84    /// Optional per-tile extended attribute + CHR-bank override for the BG
85    /// tile currently being fetched (loopy-v passed in `v`). MMC5 in `$5104`
86    /// mode 01 (`ExGrafix`) returns `Some(...)` here. Default returns `None`.
87    fn peek_ex_attribute(&mut self, _v: u16) -> Option<ExAttribute> {
88        None
89    }
90
91    /// Optional vertical split-screen override for the BG fetch group about
92    /// to start at `(scanline_y, coarse_x)`. MMC5 with split enabled
93    /// (`$5200` bit 7) returns `Some(...)` here for tile columns that fall
94    /// within the alt region. Default returns `None`.
95    fn bg_split_state(&mut self, _scanline_y: u16, _coarse_x: u16) -> Option<BgSplitState> {
96        None
97    }
98
99    /// Notification of a PPU A12 line transition (rising or falling). The
100    /// PPU calls this on every transition, with `level = true` for high.
101    /// MMC3 / MMC5 use this internally for IRQ counter clocking.
102    fn notify_a12(&mut self, _level: bool) {}
103
104    /// Notification that the PPU is starting a new rendered scanline (visible
105    /// or pre-render). MMC5 uses this to drive its scanline IRQ counter.
106    /// Default no-op.
107    fn notify_scanline_start(&mut self) {}
108
109    /// Notification that the PPU has entered vertical blank. MMC5 uses this
110    /// to clear its "in-frame" flag. Default no-op.
111    fn notify_vblank(&mut self) {}
112
113    /// Resolve a logical nametable address in `$2000-$3EFF` to a CIRAM offset
114    /// in `0..0x800` under the mapper's currently-selected mirroring.
115    ///
116    /// Default impl uses a vertical-mirroring fallback so this trait remains
117    /// drop-in for ad-hoc test buses; the lockstep bus in `rustynes-core`
118    /// overrides this to delegate to `Mapper::nametable_address`.
119    fn nametable_address(&self, addr: u16) -> u16 {
120        // Default fallback: vertical mirroring (tables 0/2 -> bank 0, 1/3 -> bank 1).
121        let table = ((addr.wrapping_sub(0x2000)) / 0x0400) & 0x03;
122        let local = addr & 0x03FF;
123        ((table & 1) * 0x0400) | local
124    }
125}
126
127/// Re-export of the mapper-side per-tile extended-attribute info.
128///
129/// Lives in `rustynes-mappers` (the canonical owner) and is re-declared here as
130/// a small POD to avoid making `rustynes-ppu` depend on `rustynes-mappers`. The
131/// lockstep bus's `PpuBusAdapter` translates between the two.
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub struct ExAttribute {
134    /// 2-bit palette select for this tile.
135    pub palette: u8,
136    /// 12-bit physical CHR bank (4 KiB units) for this tile.
137    pub chr_bank: u16,
138}
139
140/// Vertical split-screen override (MMC5 `$5200`-`$5202` and equivalents).
141///
142/// Lives in `rustynes-mappers` (the canonical owner) and is re-declared here as
143/// a small POD to avoid making `rustynes-ppu` depend on `rustynes-mappers`. The
144/// lockstep bus's `PpuBusAdapter` translates between the two.
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146pub struct BgSplitState {
147    /// Synthesized nametable byte address for the alt region (`$2000-$3EFF`).
148    pub nt_addr: u16,
149    /// Synthesized attribute byte address for the alt region.
150    pub at_addr: u16,
151    /// Fine-Y (0..=7) for the alt region's logical row.
152    pub fine_y: u8,
153    /// 4 KiB CHR bank index for the alt region's BG pattern fetches.
154    pub chr_bank: u8,
155}