gb/mmio/ppu.rs
1//! Display and PPU: control, palettes, scroll, and video memory.
2
3use bitfield_struct::{bitenum, bitfield};
4use voladdress::{Safe, Unsafe, VolAddress, VolBlock, VolGrid2d};
5
6/// One 8x8 2bpp tile: 16 bytes, two bytes per row.
7pub type Tile = [u8; 16];
8
9/// Sprite attribute byte (`OamEntry::attr`).
10///
11/// | Bit | Field | Access | Meaning |
12/// |-----|-------|--------|---------|
13/// | 7 | `priority` | R/W | Set draws the sprite behind BG colors 1-3. |
14/// | 6 | `y_flip` | R/W | Mirror vertically. |
15/// | 5 | `x_flip` | R/W | Mirror horizontally. |
16/// | 4 | `dmg_palette` | R/W | DMG palette: clear = OBP0, set = OBP1. |
17/// | 3 | `cgb_bank` | R/W | CGB tile VRAM bank. |
18/// | 2-0 | `cgb_palette` | R/W | CGB object palette (0-7). |
19#[bitfield(u8)]
20#[derive(PartialEq, Eq)]
21pub struct OamAttr {
22 /// CGB object palette (0-7).
23 #[bits(3)]
24 pub cgb_palette: u8,
25 /// CGB tile VRAM bank.
26 pub cgb_bank: bool,
27 /// DMG palette: `false` = OBP0, `true` = OBP1.
28 pub dmg_palette: bool,
29 /// Mirror horizontally.
30 pub x_flip: bool,
31 /// Mirror vertically.
32 pub y_flip: bool,
33 /// Priority: `true` draws the sprite behind BG colors 1-3.
34 pub priority: bool,
35}
36
37/// A single OAM sprite entry (4 bytes).
38///
39/// | Byte | Field | Meaning |
40/// |------|-------|---------|
41/// | 0 | `y` | Screen Y position, offset by 16. |
42/// | 1 | `x` | Screen X position, offset by 8. |
43/// | 2 | `tile` | Tile index into the sprite tile data. |
44/// | 3 | `attr` | Attribute flags (`OamAttr`). |
45#[derive(Clone, Copy)]
46#[repr(C)]
47pub struct OamEntry {
48 /// Screen Y position, offset by 16 (0 and >= 160 hide the sprite).
49 pub y: u8,
50 /// Screen X position, offset by 8 (0 and >= 168 hide the sprite).
51 pub x: u8,
52 /// Tile index into the sprite tile data.
53 pub tile: u8,
54 /// Attribute flags: palette, flip, priority, and CGB bank/palette.
55 pub attr: OamAttr,
56}
57
58/// LCD control (`LCDC`). Each field's name describes the meaning when *set*.
59///
60/// | Bit | Field | Access | Meaning |
61/// |-----|-------|--------|---------|
62/// | 7 | `lcd_enable` | R/W | LCD and PPU enable. |
63/// | 6 | `window_tilemap_high` | R/W | Window tile map at 0x9C00 instead of 0x9800. |
64/// | 5 | `window_enable` | R/W | Window enable. |
65/// | 4 | `tiledata_8000` | R/W | BG/window tile data from the 0x8000 area instead of 0x8800. |
66/// | 3 | `bg_tilemap_high` | R/W | BG tile map at 0x9C00 instead of 0x9800. |
67/// | 2 | `obj_tall` | R/W | Object size 8x16 instead of 8x8. |
68/// | 1 | `obj_enable` | R/W | Object (sprite) rendering enable. |
69/// | 0 | `bg_window_enable` | R/W | BG and window enable (DMG); BG/window master priority (CGB). |
70#[bitfield(u8)]
71#[derive(PartialEq, Eq)]
72pub struct Lcdc {
73 /// BG and window enable (DMG); BG/window master priority (CGB).
74 pub bg_window_enable: bool,
75 /// Object (sprite) rendering enable.
76 pub obj_enable: bool,
77 /// Object size is 8x16 instead of 8x8.
78 pub obj_tall: bool,
79 /// BG tile map at `0x9C00` instead of `0x9800`.
80 pub bg_tilemap_high: bool,
81 /// BG/window tile data uses the `0x8000` (unsigned) instead of `0x8800` area.
82 pub tiledata_8000: bool,
83 /// Window enable.
84 pub window_enable: bool,
85 /// Window tile map at `0x9C00` instead of `0x9800`.
86 pub window_tilemap_high: bool,
87 /// LCD and PPU enable.
88 pub lcd_enable: bool,
89}
90
91/// Current PPU mode (`STAT` bits 1-0).
92#[bitenum(all = false)]
93#[repr(u8)]
94#[derive(Clone, Copy, Debug, PartialEq, Eq)]
95pub enum PpuMode {
96 /// Mode 0: horizontal blank.
97 HBlank = 0,
98 /// Mode 1: vertical blank.
99 VBlank = 1,
100 /// Mode 2: OAM scan.
101 OamScan = 2,
102 /// Mode 3: drawing pixels.
103 #[fallback]
104 Drawing = 3,
105}
106
107/// LCD status (`STAT`).
108///
109/// | Bit | Field | Access | Meaning |
110/// |-----|-------|--------|---------|
111/// | 7 | — | | Unused. |
112/// | 6 | `lyc_int` | R/W | Raise the STAT interrupt on LYC=LY. |
113/// | 5 | `mode2_int` | R/W | Raise the STAT interrupt on mode 2 (OAM scan). |
114/// | 4 | `mode1_int` | R/W | Raise the STAT interrupt on mode 1 (VBlank). |
115/// | 3 | `mode0_int` | R/W | Raise the STAT interrupt on mode 0 (HBlank). |
116/// | 2 | `lyc_eq_ly` | RO | Set while LY equals LYC. |
117/// | 1-0 | `mode` | RO | Current PPU mode. |
118#[bitfield(u8)]
119#[derive(PartialEq, Eq)]
120pub struct Stat {
121 /// Current PPU mode (read-only).
122 #[bits(2, access = RO)]
123 pub mode: PpuMode,
124 /// Set while `LY` equals `LYC` (read-only).
125 #[bits(1, access = RO)]
126 pub lyc_eq_ly: bool,
127 /// Raise the STAT interrupt on mode 0 (HBlank).
128 pub mode0_int: bool,
129 /// Raise the STAT interrupt on mode 1 (VBlank).
130 pub mode1_int: bool,
131 /// Raise the STAT interrupt on mode 2 (OAM scan).
132 pub mode2_int: bool,
133 /// Raise the STAT interrupt on the LYC=LY condition.
134 pub lyc_int: bool,
135 #[bits(1)]
136 __: u8,
137}
138
139/// One of the four DMG gray shades.
140#[bitenum(all = false)]
141#[repr(u8)]
142#[derive(Clone, Copy, Debug, PartialEq, Eq)]
143pub enum Shade {
144 /// Lightest.
145 White = 0,
146 /// Lighter gray.
147 LightGray = 1,
148 /// Darker gray.
149 DarkGray = 2,
150 /// Darkest.
151 #[fallback]
152 Black = 3,
153}
154
155/// A DMG palette (`BGP` / `OBP0` / `OBP1`): one shade per color index. Object
156/// palettes ignore color index 0 (transparent).
157///
158/// | Bit | Field | Access | Meaning |
159/// |-----|-------|--------|---------|
160/// | 7-6 | `id3` | R/W | Shade for color index 3. |
161/// | 5-4 | `id2` | R/W | Shade for color index 2. |
162/// | 3-2 | `id1` | R/W | Shade for color index 1. |
163/// | 1-0 | `id0` | R/W | Shade for color index 0. |
164#[bitfield(u8)]
165#[derive(PartialEq, Eq)]
166pub struct Palette {
167 /// Shade for color index 0.
168 #[bits(2)]
169 pub id0: Shade,
170 /// Shade for color index 1.
171 #[bits(2)]
172 pub id1: Shade,
173 /// Shade for color index 2.
174 #[bits(2)]
175 pub id2: Shade,
176 /// Shade for color index 3.
177 #[bits(2)]
178 pub id3: Shade,
179}
180
181// ── Registers ───────────────────────────────────────────────────────────────
182
183/// LCD control: display/window/sprite/background enables and tile sources.
184///
185/// # Safety
186///
187/// Writing is `unsafe`: clearing the `lcd_enable` bit (display off) outside
188/// VBlank can damage the panel and desyncs the PPU.
189pub const LCDC: VolAddress<Lcdc, Safe, Unsafe> = unsafe { VolAddress::new(0xFF40) };
190/// LCD status: PPU mode and STAT interrupt sources.
191pub const STAT: VolAddress<Stat, Safe, Safe> = unsafe { VolAddress::new(0xFF41) };
192/// Background scroll Y.
193pub const SCY: VolAddress<u8, Safe, Safe> = unsafe { VolAddress::new(0xFF42) };
194/// Background scroll X.
195pub const SCX: VolAddress<u8, Safe, Safe> = unsafe { VolAddress::new(0xFF43) };
196/// Current scanline. Read-only.
197pub const LY: VolAddress<u8, Safe, ()> = unsafe { VolAddress::new(0xFF44) };
198/// Scanline compare value for the LYC=LY STAT interrupt.
199pub const LYC: VolAddress<u8, Safe, Safe> = unsafe { VolAddress::new(0xFF45) };
200/// OAM DMA: writing a high byte copies 160 bytes from `xx00` into OAM.
201///
202/// # Safety
203///
204/// Writing is `unsafe`: during the transfer the CPU may only access HRAM, so it
205/// must be started from a wait routine running there.
206pub const DMA: VolAddress<u8, Safe, Unsafe> = unsafe { VolAddress::new(0xFF46) };
207/// Background palette (DMG).
208pub const BGP: VolAddress<Palette, Safe, Safe> = unsafe { VolAddress::new(0xFF47) };
209/// Object palette 0 (DMG).
210pub const OBP0: VolAddress<Palette, Safe, Safe> = unsafe { VolAddress::new(0xFF48) };
211/// Object palette 1 (DMG).
212pub const OBP1: VolAddress<Palette, Safe, Safe> = unsafe { VolAddress::new(0xFF49) };
213/// Window Y position.
214pub const WY: VolAddress<u8, Safe, Safe> = unsafe { VolAddress::new(0xFF4A) };
215/// Window X position plus 7.
216pub const WX: VolAddress<u8, Safe, Safe> = unsafe { VolAddress::new(0xFF4B) };
217
218// ── Video memory ────────────────────────────────────────────────────────────
219
220/// Tile data: 384 tiles at `0x8000..0x97FF` (shared by BG and sprites on DMG).
221pub const VRAM_TILES: VolBlock<Tile, Safe, Safe, 384> =
222 unsafe { VolBlock::new(0x8000) };
223/// Tile map 0: a 32x32 grid of tile indices at `0x9800`, indexed `(x, y)`.
224pub const TILEMAP_0: VolGrid2d<u8, Safe, Safe, 32, 32> =
225 unsafe { VolGrid2d::new(0x9800) };
226/// Tile map 1: a 32x32 grid of tile indices at `0x9C00`, indexed `(x, y)`.
227pub const TILEMAP_1: VolGrid2d<u8, Safe, Safe, 32, 32> =
228 unsafe { VolGrid2d::new(0x9C00) };
229/// Object attribute memory: 40 sprite entries at `0xFE00..=0xFE9F`.
230pub const OAM: VolBlock<OamEntry, Safe, Safe, 40> =
231 unsafe { VolBlock::new(0xFE00) };
232
233const _: () = {
234 assert!(Lcdc::new().with_lcd_enable(true).into_bits() == 0b1000_0000);
235 assert!(Lcdc::new().with_bg_window_enable(true).into_bits() == 0b0000_0001);
236 assert!(matches!(Stat::from_bits(0b0000_0011).mode(), PpuMode::Drawing));
237 assert!(Stat::new().with_lyc_int(true).into_bits() == 0b0100_0000);
238 assert!(Palette::new().with_id3(Shade::Black).into_bits() == 0b1100_0000);
239 assert!(Palette::new().with_id0(Shade::Black).into_bits() == 0b0000_0011);
240 assert!(OamAttr::new().with_priority(true).into_bits() == 0b1000_0000);
241 assert!(OamAttr::new().with_y_flip(true).into_bits() == 0b0100_0000);
242 assert!(OamAttr::new().with_x_flip(true).into_bits() == 0b0010_0000);
243 assert!(OamAttr::new().with_cgb_palette(0x7).into_bits() == 0b0000_0111);
244};