jevsnes.git / third-party / rust / jgenesis / backend / gb-core / src / ppu / registers.rs
1use crate::cgb::CpuSpeed;
2use crate::ppu::{PpuMode, State};
3use bincode::{Decode, Encode};
4use jgenesis_common::num::GetBit;
5use std::fmt::{Display, Formatter};
6use std::ops::Index;
7use std::{array, iter};
8
9pub const TILE_MAP_AREA_0: u16 = 0x1800;
10pub const TILE_MAP_AREA_1: u16 = 0x1C00;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode)]
13pub enum TileDataArea {
14    // $8800-$97FF
15    #[default]
16    Zero,
17    // $8000-$8FFF
18    One,
19}
20
21impl TileDataArea {
22    // Sprites always use $8000-$8FFF
23    pub const SPRITES: Self = Self::One;
24
25    pub fn tile_address(self, tile_number: u8) -> u16 {
26        // 16 bytes per tile
27        match self {
28            Self::Zero => {
29                // Treat tile number as a signed integer so that 128-255 map to $8800-$8FFF
30                let relative_tile_addr = (tile_number as i8 as u16) << 4;
31                0x1000_u16.wrapping_add(relative_tile_addr)
32            }
33            Self::One => u16::from(tile_number) << 4,
34        }
35    }
36
37    fn from_bit(bit: bool) -> Self {
38        if bit { Self::One } else { Self::Zero }
39    }
40
41    fn to_bit(self) -> bool {
42        self == Self::One
43    }
44}
45
46impl Display for TileDataArea {
47    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
48        match self {
49            Self::Zero => write!(f, "$8800-$97FF"),
50            Self::One => write!(f, "$8000-$8FFF"),
51        }
52    }
53}
54
55#[derive(Debug, Clone, Default, Encode, Decode)]
56pub struct Registers {
57    // LCDC: LCD control
58    pub ppu_enabled: bool,
59    pub bg_enabled: bool,
60    pub window_enabled: bool,
61    pub sprites_enabled: bool,
62    pub bg_tile_map_addr: u16,
63    pub window_tile_map_addr: u16,
64    pub bg_tile_data_area: TileDataArea,
65    pub double_height_sprites: bool,
66    // STAT: LCD status
67    pub lyc_interrupt_enabled: bool,
68    pub mode_2_interrupt_enabled: bool,
69    pub mode_1_interrupt_enabled: bool,
70    pub mode_0_interrupt_enabled: bool,
71    // LYC: LY compare
72    pub ly_compare: u8,
73    // SCX/SCY: Background X/Y position
74    pub bg_x_scroll: u8,
75    pub bg_y_scroll: u8,
76    // WX/WY: Window X/Y position
77    pub window_x: u8,
78    pub window_y: u8,
79    // BGP: Background palette
80    pub bg_palette: [u8; 4],
81    // OBP0/OBP1: Sprite palettes
82    pub sprite_palettes: [[u8; 4]; 2],
83    // VBK: VRAM bank
84    pub vram_bank: u8,
85}
86
87impl Registers {
88    pub fn new(boot_rom_present: bool) -> Self {
89        Self {
90            ppu_enabled: !boot_rom_present,
91            bg_enabled: true,
92            window_enabled: false,
93            sprites_enabled: false,
94            bg_tile_map_addr: TILE_MAP_AREA_0,
95            window_tile_map_addr: TILE_MAP_AREA_0,
96            bg_tile_data_area: TileDataArea::One,
97            double_height_sprites: false,
98            lyc_interrupt_enabled: false,
99            mode_2_interrupt_enabled: false,
100            mode_1_interrupt_enabled: false,
101            mode_0_interrupt_enabled: false,
102            ly_compare: 0,
103            bg_x_scroll: 0,
104            bg_y_scroll: 0,
105            window_x: 0,
106            window_y: 0,
107            // Power-on value is $FC / 0b11_11_11_00
108            bg_palette: [0, 3, 3, 3],
109            sprite_palettes: [[0; 4]; 2],
110            vram_bank: 0,
111        }
112    }
113
114    pub fn write_lcdc(&mut self, value: u8) {
115        self.ppu_enabled = value.bit(7);
116        self.window_tile_map_addr = if value.bit(6) { TILE_MAP_AREA_1 } else { TILE_MAP_AREA_0 };
117        self.window_enabled = value.bit(5);
118        self.bg_tile_data_area = TileDataArea::from_bit(value.bit(4));
119        self.bg_tile_map_addr = if value.bit(3) { TILE_MAP_AREA_1 } else { TILE_MAP_AREA_0 };
120        self.double_height_sprites = value.bit(2);
121        self.sprites_enabled = value.bit(1);
122        self.bg_enabled = value.bit(0);
123
124        log::trace!("LCDC write: {value:02X}");
125        log::trace!("  PPU enabled: {}", self.ppu_enabled);
126        log::trace!("  BG/window enabled: {}", self.bg_enabled);
127        log::trace!("  Window enabled: {}", self.window_enabled);
128        log::trace!("  Sprites enabled: {}", self.sprites_enabled);
129        log::trace!("  BG tile map address: ${:04X}", self.bg_tile_map_addr);
130        log::trace!("  Window tile map address: ${:04X}", self.window_tile_map_addr);
131        log::trace!("  BG tile data area: {}", self.bg_tile_data_area);
132        log::trace!("  Double height sprites: {}", self.double_height_sprites);
133    }
134
135    pub fn read_lcdc(&self) -> u8 {
136        (u8::from(self.ppu_enabled) << 7)
137            | (u8::from(self.window_tile_map_addr == TILE_MAP_AREA_1) << 6)
138            | (u8::from(self.window_enabled) << 5)
139            | (u8::from(self.bg_tile_data_area.to_bit()) << 4)
140            | (u8::from(self.bg_tile_map_addr == TILE_MAP_AREA_1) << 3)
141            | (u8::from(self.double_height_sprites) << 2)
142            | (u8::from(self.sprites_enabled) << 1)
143            | u8::from(self.bg_enabled)
144    }
145
146    pub fn write_stat(&mut self, value: u8) {
147        self.lyc_interrupt_enabled = value.bit(6);
148        self.mode_2_interrupt_enabled = value.bit(5);
149        self.mode_1_interrupt_enabled = value.bit(4);
150        self.mode_0_interrupt_enabled = value.bit(3);
151
152        log::trace!("STAT write: {value:02X}");
153        log::trace!("  LY=LYC interrupt enabled: {}", self.lyc_interrupt_enabled);
154        log::trace!("  Mode 2 (OAM scan) interrupt enabled: {}", self.mode_2_interrupt_enabled);
155        log::trace!("  Mode 1 (VBlank) interrupt enabled: {}", self.mode_1_interrupt_enabled);
156        log::trace!("  Mode 0 (HBlank) interrupt enabled: {}", self.mode_0_interrupt_enabled);
157    }
158
159    pub fn read_stat(&self, state: &State, speed: CpuSpeed) -> u8 {
160        let ly_lyc_bit = if self.ppu_enabled {
161            state.ly_for_compare(speed) == self.ly_compare
162        } else {
163            state.frozen_ly_lyc_bit
164        };
165
166        // Have the CPU read mode 0->2 and mode 1->2 transitions one M-cycle late to account for the
167        // read occurring mid-M-cycle on actual hardware.
168        // This is needed for the demo Space Waste to not crash at the first effect, as it does lots
169        // of mid-frame CGB palette writes gated by STAT mode checks and it needs to finish all of
170        // them within a certain number of cycles
171        let mode_for_read = if state.mode == PpuMode::ScanningOam && state.dot == 0 {
172            if state.scanline == 0 { PpuMode::VBlank } else { PpuMode::HBlank }
173        } else {
174            state.mode
175        };
176
177        0x80 | (u8::from(self.lyc_interrupt_enabled) << 6)
178            | (u8::from(self.mode_2_interrupt_enabled) << 5)
179            | (u8::from(self.mode_1_interrupt_enabled) << 4)
180            | (u8::from(self.mode_0_interrupt_enabled) << 3)
181            | (u8::from(ly_lyc_bit) << 2)
182            | mode_for_read.to_bits()
183    }
184
185    pub fn write_lyc(&mut self, value: u8) {
186        self.ly_compare = value;
187
188        log::trace!("LYC write: {value:02X}");
189    }
190
191    pub fn write_scx(&mut self, value: u8) {
192        self.bg_x_scroll = value;
193
194        log::trace!("SCX write: {value:02X}");
195    }
196
197    pub fn write_scy(&mut self, value: u8) {
198        self.bg_y_scroll = value;
199
200        log::trace!("SCY write: {value:02X}");
201    }
202
203    pub fn write_wx(&mut self, value: u8) {
204        self.window_x = value;
205
206        log::trace!("WX write: {value:02X}");
207    }
208
209    pub fn write_wy(&mut self, value: u8) {
210        self.window_y = value;
211
212        log::trace!("WY write: {value:02X}");
213    }
214
215    pub fn write_bgp(&mut self, value: u8) {
216        self.bg_palette = parse_dmg_palette(value);
217
218        log::trace!("BGP write: {value:02X}");
219    }
220
221    pub fn read_bgp(&self) -> u8 {
222        read_dmg_palette(self.bg_palette)
223    }
224
225    pub fn write_obp0(&mut self, value: u8) {
226        self.sprite_palettes[0] = parse_dmg_palette(value);
227
228        log::trace!("OBP0 write: {value:02X}");
229    }
230
231    pub fn write_obp1(&mut self, value: u8) {
232        self.sprite_palettes[1] = parse_dmg_palette(value);
233
234        log::trace!("OBP1 write: {value:02X}");
235    }
236
237    pub fn read_obp0(&self) -> u8 {
238        read_dmg_palette(self.sprite_palettes[0])
239    }
240
241    pub fn read_obp1(&self) -> u8 {
242        read_dmg_palette(self.sprite_palettes[1])
243    }
244
245    pub fn write_vbk(&mut self, value: u8) {
246        self.vram_bank = value & 0x01;
247
248        log::trace!("VBK write: VRAM bank = {}", self.vram_bank);
249    }
250
251    pub fn read_vbk(&self) -> u8 {
252        0xFE | self.vram_bank
253    }
254}
255
256fn parse_dmg_palette(value: u8) -> [u8; 4] {
257    array::from_fn(|palette| (value >> (2 * palette)) & 0x3)
258}
259
260fn read_dmg_palette(palette: [u8; 4]) -> u8 {
261    palette.into_iter().enumerate().map(|(i, color)| color << (2 * i)).reduce(|a, b| a | b).unwrap()
262}
263
264const PALETTE_RAM_LEN: usize = 64;
265
266#[derive(Debug, Clone, Encode, Decode)]
267pub struct CgbPaletteRam {
268    ram: Box<[u8; PALETTE_RAM_LEN]>,
269    data_port_address: u8,
270    data_port_auto_increment: bool,
271}
272
273impl Index<usize> for CgbPaletteRam {
274    type Output = u8;
275
276    fn index(&self, index: usize) -> &Self::Output {
277        &self.ram[index]
278    }
279}
280
281impl CgbPaletteRam {
282    fn new(initial_ram: Box<[u8; PALETTE_RAM_LEN]>) -> Self {
283        Self { ram: initial_ram, data_port_address: 0, data_port_auto_increment: true }
284    }
285
286    pub fn new_bg() -> Self {
287        // BG palette RAM should be intialized to all white (0xFFFF)
288        Self::new(vec![0xFF; PALETTE_RAM_LEN].into_boxed_slice().try_into().unwrap())
289    }
290
291    pub fn new_obj() -> Self {
292        // OBJ palette RAM contents are undefined at power-on and even after boot ROM runs
293        let initial_ram: Vec<u8> = iter::repeat_with(rand::random).take(PALETTE_RAM_LEN).collect();
294        Self::new(initial_ram.into_boxed_slice().try_into().unwrap())
295    }
296
297    pub fn read_data_port_address(&self) -> u8 {
298        0x40 | (u8::from(self.data_port_auto_increment) << 7) | self.data_port_address
299    }
300
301    pub fn write_data_port_address(&mut self, value: u8) {
302        self.data_port_address = value & 0x3F;
303        self.data_port_auto_increment = value.bit(7);
304    }
305
306    pub fn read_data_port(&self, cpu_can_access_vram: bool) -> u8 {
307        if cpu_can_access_vram { self.ram[self.data_port_address as usize] } else { 0xFF }
308    }
309
310    pub fn write_data_port(&mut self, value: u8, cpu_can_access_vram: bool) {
311        if cpu_can_access_vram {
312            self.ram[self.data_port_address as usize] = value;
313            log::trace!("CGB palette RAM write: {:02X}, {value:02X}", self.data_port_address);
314        }
315
316        // Auto-increment is always applied, even if the CPU can't access VRAM
317        if self.data_port_auto_increment {
318            self.data_port_address = (self.data_port_address + 1) & 0x3F;
319        }
320    }
321
322    pub fn read_color(&self, palette: u8, color: u8) -> u16 {
323        let addr = 2 * ((palette << 2) | color);
324        let lsb = self.ram[addr as usize];
325        let msb = self.ram[(addr + 1) as usize];
326        u16::from_le_bytes([lsb, msb]) & 0x7FFF
327    }
328}