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}