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}