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}