1use crate::HardwareMode; 2use crate::cgb::{CgbRegisters, ObjPriority}; 3use crate::ppu::registers::{CgbPaletteRam, Registers, TileDataArea}; 4use crate::ppu::{MAX_SPRITES_PER_LINE, PpuFrameBuffer, SCREEN_WIDTH, SpriteData, Vram}; 5use bincode::{Decode, Encode}; 6use jgenesis_common::num::GetBit; 7use std::array; 8use std::collections::VecDeque; 9 10const MAX_FIFO_X: u8 = SCREEN_WIDTH as u8 + 8; 11 12#[derive(Debug, Clone, Copy, Encode, Decode)] 13struct BgPixel { 14 color: u8, 15 palette: u8, 16 high_priority: bool, 17} 18 19#[derive(Debug, Clone, Copy, Encode, Decode)] 20struct SpritePixel { 21 color: u8, 22 palette: u8, 23 low_priority: bool, 24 oam_index: u8, 25} 26 27impl SpritePixel { 28 const TRANSPARENT: Self = Self { color: 0, palette: 0, low_priority: true, oam_index: 255 }; 29} 30 31#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)] 32enum BgLayer { 33 Background, 34 Window { window_line: u8 }, 35} 36 37#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)] 38struct RenderingBgTileFields { 39 bg_layer: BgLayer, 40 dots_remaining: u8, 41 screen_x: u8, 42 fetcher_x: u8, 43 latched_scx: u8, 44 latched_scy: u8, 45 // Whether or not a sprite fetch was delayed by a BG fetch in the current BG tile 46 sprite_fetch_delayed: bool, 47} 48 49#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)] 50enum FifoState { 51 // Fetching the offscreen first tile 52 InitialBgFetch { dots_remaining: u8 }, 53 // Rendering a background tile 54 RenderingBgTile(RenderingBgTileFields), 55 // Fetching a sprite tile 56 SpriteFetch { dots_remaining: u8, previous_bg_fields: RenderingBgTileFields }, 57 // Fetching the first window tile 58 InitialWindowFetch { dots_remaining: u8, screen_x: u8, window_line: u8 }, 59} 60 61#[derive(Debug, Clone, Encode, Decode)] 62pub struct PixelFifo { 63 hardware_mode: HardwareMode, 64 bg: VecDeque<BgPixel>, 65 sprites: VecDeque<SpritePixel>, 66 y: u8, 67 window_y_triggered: bool, 68 window_line_counter: u8, 69 window_triggered_in_frame: bool, 70 scanned_sprites: VecDeque<SpriteData>, 71 state: FifoState, 72} 73 74impl PixelFifo { 75 pub fn new(hardware_mode: HardwareMode) -> Self { 76 Self { 77 hardware_mode, 78 bg: VecDeque::with_capacity(16), 79 sprites: VecDeque::with_capacity(16), 80 y: 0, 81 window_y_triggered: false, 82 window_line_counter: 0, 83 window_triggered_in_frame: false, 84 scanned_sprites: VecDeque::with_capacity(MAX_SPRITES_PER_LINE), 85 state: FifoState::InitialBgFetch { dots_remaining: 6 }, 86 } 87 } 88 89 pub fn reset_window_state(&mut self) { 90 self.window_y_triggered = false; 91 self.window_line_counter = 0; 92 self.window_triggered_in_frame = false; 93 } 94 95 pub fn check_window_y(&mut self, scanline: u8, registers: &Registers) { 96 self.window_y_triggered |= registers.window_y == scanline && registers.window_enabled; 97 } 98 99 pub fn start_new_line(&mut self, scanline: u8, registers: &Registers, sprites: &[SpriteData]) { 100 self.bg.clear(); 101 self.sprites.clear(); 102 self.y = scanline; 103 104 self.check_window_y(scanline, registers); 105 106 self.scanned_sprites.clear(); 107 self.scanned_sprites.extend(sprites); 108 109 // Initial BG tile fetch always takes 6 cycles 110 self.state = FifoState::InitialBgFetch { dots_remaining: 6 }; 111 112 // TODO check WY here 113 } 114 115 pub fn tick( 116 &mut self, 117 vram: &Vram, 118 registers: &Registers, 119 cgb_registers: CgbRegisters, 120 bg_palette_ram: &CgbPaletteRam, 121 sprite_palette_ram: &CgbPaletteRam, 122 frame_buffer: Option<&mut PpuFrameBuffer>, 123 ) { 124 match self.state { 125 FifoState::InitialBgFetch { dots_remaining } => { 126 self.handle_initial_bg_fetch(dots_remaining, vram, registers); 127 } 128 FifoState::RenderingBgTile(fields) => { 129 self.handle_rendering_bg_tile( 130 fields, 131 vram, 132 registers, 133 cgb_registers, 134 bg_palette_ram, 135 sprite_palette_ram, 136 frame_buffer, 137 ); 138 } 139 FifoState::SpriteFetch { dots_remaining, previous_bg_fields } => { 140 self.handle_sprite_fetch(dots_remaining, previous_bg_fields); 141 } 142 FifoState::InitialWindowFetch { dots_remaining, screen_x, window_line } => { 143 self.handle_initial_window_fetch(dots_remaining, screen_x, window_line); 144 } 145 } 146 } 147 148 fn handle_initial_bg_fetch(&mut self, dots_remaining: u8, vram: &Vram, registers: &Registers) { 149 if dots_remaining == 6 { 150 // Do the initial tile fetch 151 let bg_tile_row = fetch_bg_tile( 152 self.hardware_mode, 153 0, 154 self.y, 155 vram, 156 registers, 157 registers.bg_x_scroll, 158 registers.bg_y_scroll, 159 ); 160 for color in bg_tile_row.pixels { 161 self.bg.push_back(BgPixel { 162 color, 163 palette: bg_tile_row.palette, 164 high_priority: bg_tile_row.high_priority, 165 }); 166 } 167 } 168 169 if dots_remaining != 1 { 170 self.state = FifoState::InitialBgFetch { dots_remaining: dots_remaining - 1 }; 171 return; 172 } 173 174 // TODO check if WX=0 here and emulate WX=0 hardware glitches 175 176 // DMG hardware glitch: render a single pixel of color BGP[0] if the following are all true: 177 // The window triggered earlier in the current frame 178 // The window is currently disabled 179 // WX & 7 == 7 - (SCX & 7) 180 // This is needed for correct alignment on the title screen of Star Trek: 25th Anniversary 181 // Reference: https://github.com/LIJI32/SameBoy/issues/278 182 if self.hardware_mode == HardwareMode::Dmg 183 && self.window_triggered_in_frame 184 && !registers.window_enabled 185 && registers.window_x & 7 == 7 - (registers.bg_x_scroll & 7) 186 { 187 self.bg.push_back(BgPixel { color: 0, palette: 0, high_priority: false }); 188 } 189 190 // The first 8 pixels are always discarded, and (SCX % 8) additional pixels are discarded to handle fine X scrolling 191 // Simulate this by using fine X scroll to move the screen position backwards 192 let fine_x_scroll = registers.bg_x_scroll % 8; 193 self.state = FifoState::RenderingBgTile(RenderingBgTileFields { 194 bg_layer: BgLayer::Background, 195 dots_remaining: 8, 196 screen_x: 0_u8.wrapping_sub(fine_x_scroll), 197 fetcher_x: 0, 198 sprite_fetch_delayed: false, 199 latched_scx: registers.bg_x_scroll, 200 latched_scy: registers.bg_y_scroll, 201 }); 202 } 203 204 #[allow(clippy::too_many_arguments)] 205 fn handle_rendering_bg_tile( 206 &mut self, 207 mut fields: RenderingBgTileFields, 208 vram: &Vram, 209 registers: &Registers, 210 cgb_registers: CgbRegisters, 211 bg_palette_ram: &CgbPaletteRam, 212 sprite_palette_ram: &CgbPaletteRam, 213 frame_buffer: Option<&mut PpuFrameBuffer>, 214 ) { 215 if let Some(sprite) = 216 self.scanned_sprites.pop_front_if(|sprite| sprite.x == fields.screen_x) 217 { 218 // A sprite starts on this position. Go ahead and do the actual tile fetch immediately 219 // TODO GBC always fetches sprite tiles even when sprites are disabled 220 if registers.sprites_enabled { 221 self.fetch_next_sprite_tile(sprite, vram, registers, cgb_registers.obj_priority); 222 223 // Sprite fetches take at minimum 6 cycles, and the fetch may be delayed by an additional 1-5 cycles if it 224 // needs to wait for a BG fetch to finish 225 let sprite_fetch_cycles = 226 if !fields.sprite_fetch_delayed && (4..9).contains(&fields.dots_remaining) { 227 fields.sprite_fetch_delayed = true; 228 6 + fields.dots_remaining - 3 229 } else { 230 6 231 }; 232 233 log::trace!( 234 "Sprite encountered at X {}, delaying by {sprite_fetch_cycles} cycles", 235 fields.screen_x 236 ); 237 238 // Subtract 1 to account for the current tick 239 self.state = FifoState::SpriteFetch { 240 dots_remaining: sprite_fetch_cycles - 1, 241 previous_bg_fields: fields, 242 }; 243 244 return; 245 } 246 247 // Sprites are disabled; pop all sprites at the current position 248 while self.scanned_sprites.front().is_some_and(|sprite| sprite.x == fields.screen_x) { 249 self.scanned_sprites.pop_front(); 250 } 251 } 252 253 if self.window_y_triggered 254 && registers.window_enabled 255 && registers.window_x <= (SCREEN_WIDTH + 7) as u8 256 && registers.window_x.saturating_add(1) == fields.screen_x 257 && fields.bg_layer == BgLayer::Background 258 { 259 log::trace!("Window started at X {}", fields.screen_x); 260 261 // Window triggered; clear the BG FIFO and fetch window tile 0 262 self.bg.clear(); 263 let first_window_tile = 264 fetch_window_tile(self.hardware_mode, 0, self.window_line_counter, vram, registers); 265 self.push_bg_tile_row(first_window_tile); 266 267 self.state = FifoState::InitialWindowFetch { 268 // Wait for 5 cycles instead of 6 to account for the current tick 269 dots_remaining: 5, 270 screen_x: fields.screen_x, 271 window_line: self.window_line_counter, 272 }; 273 self.window_line_counter += 1; 274 self.window_triggered_in_frame = true; 275 276 return; 277 } 278 279 if fields.dots_remaining == 4 { 280 // Latch SCY/SCX two dots before tile fetch 281 // Some demos depend on this, e.g. Demotronic 282 fields.latched_scx = registers.bg_x_scroll; 283 fields.latched_scy = registers.bg_y_scroll; 284 } 285 286 if fields.dots_remaining == 2 { 287 log::trace!("Fetching BG tile at X {}", fields.screen_x); 288 289 // Fetch next BG/window tile 290 match fields.bg_layer { 291 BgLayer::Background => { 292 let bg_tile_row = fetch_bg_tile( 293 self.hardware_mode, 294 fields.fetcher_x, 295 self.y, 296 vram, 297 registers, 298 fields.latched_scx, 299 fields.latched_scy, 300 ); 301 self.push_bg_tile_row(bg_tile_row); 302 } 303 BgLayer::Window { window_line } => { 304 let window_tile_row = fetch_window_tile( 305 self.hardware_mode, 306 fields.fetcher_x, 307 window_line, 308 vram, 309 registers, 310 ); 311 self.push_bg_tile_row(window_tile_row); 312 } 313 } 314 315 fields.fetcher_x = fields.fetcher_x.wrapping_add(1); 316 } 317 318 let bg_pixel = self.bg.pop_front().expect("BG FIFO should never be empty while rendering"); 319 320 let mut sprite_pixel = self.sprites.pop_front().unwrap_or(SpritePixel::TRANSPARENT); 321 if !registers.sprites_enabled { 322 sprite_pixel = SpritePixel::TRANSPARENT; 323 } 324 325 if (8..MAX_FIFO_X).contains(&fields.screen_x) { 326 let color = if sprite_pixel.color != 0 327 && !(bg_pixel.color != 0 328 && registers.bg_enabled 329 && (sprite_pixel.low_priority || bg_pixel.high_priority)) 330 { 331 // Render sprite pixel 332 let read_dmg_color = |pixel: SpritePixel| { 333 registers.sprite_palettes[pixel.palette as usize][pixel.color as usize] 334 }; 335 336 match (self.hardware_mode, cgb_registers.dmg_compatibility) { 337 (HardwareMode::Dmg, _) => read_dmg_color(sprite_pixel).into(), 338 (HardwareMode::Cgb, false) => { 339 sprite_palette_ram.read_color(sprite_pixel.palette, sprite_pixel.color) 340 } 341 (HardwareMode::Cgb, true) => { 342 let dmg_color = read_dmg_color(sprite_pixel); 343 sprite_palette_ram.read_color(sprite_pixel.palette, dmg_color) 344 } 345 } 346 } else if registers.bg_enabled 347 || (self.hardware_mode == HardwareMode::Cgb && !cgb_registers.dmg_compatibility) 348 { 349 // Render BG pixel 350 match (self.hardware_mode, cgb_registers.dmg_compatibility) { 351 (HardwareMode::Dmg, _) => registers.bg_palette[bg_pixel.color as usize].into(), 352 (HardwareMode::Cgb, false) => { 353 bg_palette_ram.read_color(bg_pixel.palette, bg_pixel.color) 354 } 355 (HardwareMode::Cgb, true) => { 356 let dmg_color = registers.bg_palette[bg_pixel.color as usize]; 357 bg_palette_ram.read_color(0, dmg_color) 358 } 359 } 360 } else { 361 // In DMG mode, if BG is disabled and sprite pixel is transparent, always display 362 // BG color 0 363 let dmg_color = registers.bg_palette[0]; 364 match self.hardware_mode { 365 HardwareMode::Dmg => dmg_color.into(), 366 HardwareMode::Cgb => bg_palette_ram.read_color(0, dmg_color), 367 } 368 }; 369 370 if let Some(frame_buffer) = frame_buffer { 371 let frame_buffer_idx = 372 (self.y as usize) * SCREEN_WIDTH + (fields.screen_x - 8) as usize; 373 frame_buffer[frame_buffer_idx] = color; 374 } 375 } 376 fields.screen_x = fields.screen_x.wrapping_add(1); 377 378 if fields.dots_remaining == 1 { 379 fields.dots_remaining = 8; 380 fields.sprite_fetch_delayed = false; 381 } else { 382 fields.dots_remaining -= 1; 383 } 384 385 self.state = FifoState::RenderingBgTile(fields); 386 } 387 388 fn push_bg_tile_row(&mut self, bg_tile_row: BgTileRow) { 389 for color in bg_tile_row.pixels { 390 self.bg.push_back(BgPixel { 391 color, 392 palette: bg_tile_row.palette, 393 high_priority: bg_tile_row.high_priority, 394 }); 395 } 396 } 397 398 fn fetch_next_sprite_tile( 399 &mut self, 400 sprite: SpriteData, 401 vram: &Vram, 402 registers: &Registers, 403 cgb_obj_priority: ObjPriority, 404 ) { 405 let sprite_tile = fetch_sprite_tile(sprite, self.y, vram, registers.double_height_sprites); 406 407 while self.sprites.len() < 8 { 408 self.sprites.push_back(SpritePixel::TRANSPARENT); 409 } 410 411 for (i, color) in sprite_tile.into_iter().enumerate() { 412 if color == 0 { 413 // Don't add transparent pixels to the FIFO 414 continue; 415 } 416 417 // Replace any transparent pixels in the FIFO 418 // If in CGB mode and OPRI=0, also replace any non-transparent pixels from sprites with a higher OAM index 419 if self.sprites[i].color == 0 420 || (self.hardware_mode == HardwareMode::Cgb 421 && cgb_obj_priority == ObjPriority::OamIndex 422 && sprite.oam_index < self.sprites[i].oam_index) 423 { 424 self.sprites[i] = SpritePixel { 425 color, 426 palette: sprite.palette, 427 low_priority: sprite.low_priority, 428 oam_index: sprite.oam_index, 429 }; 430 } 431 } 432 } 433 434 fn handle_sprite_fetch( 435 &mut self, 436 dots_remaining: u8, 437 previous_bg_fields: RenderingBgTileFields, 438 ) { 439 self.state = if dots_remaining == 1 { 440 FifoState::RenderingBgTile(previous_bg_fields) 441 } else { 442 FifoState::SpriteFetch { dots_remaining: dots_remaining - 1, previous_bg_fields } 443 }; 444 } 445 446 fn handle_initial_window_fetch(&mut self, dots_remaining: u8, screen_x: u8, window_line: u8) { 447 self.state = if dots_remaining == 1 { 448 FifoState::RenderingBgTile(RenderingBgTileFields { 449 bg_layer: BgLayer::Window { window_line }, 450 dots_remaining: 8, 451 screen_x, 452 // Start at tile 1 since tile 0 has already been fetched 453 fetcher_x: 1, 454 sprite_fetch_delayed: false, 455 // Values don't matter because the window does not use SCY/SCX during tile fetch 456 latched_scx: 0, 457 latched_scy: 0, 458 }) 459 } else { 460 FifoState::InitialWindowFetch { 461 dots_remaining: dots_remaining - 1, 462 screen_x, 463 window_line, 464 } 465 }; 466 } 467 468 pub fn done_with_line(&self) -> bool { 469 match self.state { 470 FifoState::InitialBgFetch { .. } 471 | FifoState::SpriteFetch { .. } 472 | FifoState::InitialWindowFetch { .. } => false, 473 FifoState::RenderingBgTile(fields) => fields.screen_x == MAX_FIFO_X, 474 } 475 } 476} 477 478#[derive(Debug, Clone, Copy)] 479pub(super) struct BgTileAttributes { 480 pub(super) high_priority: bool, 481 pub(super) vertical_flip: bool, 482 pub(super) horizontal_flip: bool, 483 pub(super) vram_bank: u8, 484 pub(super) palette: u8, 485} 486 487impl From<u8> for BgTileAttributes { 488 fn from(value: u8) -> Self { 489 Self { 490 high_priority: value.bit(7), 491 vertical_flip: value.bit(6), 492 horizontal_flip: value.bit(5), 493 vram_bank: value.bit(3).into(), 494 palette: value & 0x07, 495 } 496 } 497} 498 499#[derive(Debug, Clone, Copy)] 500struct BgTileRow { 501 pixels: [u8; 8], 502 palette: u8, 503 high_priority: bool, 504} 505 506fn fetch_bg_tile( 507 hardware_mode: HardwareMode, 508 fetcher_x: u8, 509 y: u8, 510 vram: &Vram, 511 registers: &Registers, 512 bg_x_scroll: u8, 513 bg_y_scroll: u8, 514) -> BgTileRow { 515 if hardware_mode == HardwareMode::Dmg && !registers.bg_enabled { 516 // On DMG, all BG pixels are transparent if BG is disabled 517 return BgTileRow { pixels: [0; 8], palette: 0, high_priority: false }; 518 } 519 520 let coarse_x_scroll = bg_x_scroll / 8; 521 let tile_map_x: u16 = (fetcher_x.wrapping_add(coarse_x_scroll) % 32).into(); 522 523 let bg_y: u16 = y.wrapping_add(bg_y_scroll).into(); 524 let tile_map_y = bg_y / 8; 525 526 let tile_map_addr = registers.bg_tile_map_addr | (tile_map_y << 5) | tile_map_x; 527 let tile_number = vram[tile_map_addr as usize]; 528 529 // No need to check for CGB here; the CPU can't write to VRAM bank 1 in DMG mode, and an attributes byte of 0 is 530 // equivalent to DMG functionality 531 let attributes_map_addr = 0x2000 | tile_map_addr; 532 let attributes = BgTileAttributes::from(vram[attributes_map_addr as usize]); 533 534 let bank_addr = u16::from(attributes.vram_bank) << 13; 535 let tile_row = if attributes.vertical_flip { 7 - (bg_y % 8) } else { bg_y % 8 }; 536 let tile_addr = 537 bank_addr | registers.bg_tile_data_area.tile_address(tile_number) | (tile_row << 1); 538 let tile_data_lsb = vram[tile_addr as usize]; 539 let tile_data_msb = vram[(tile_addr + 1) as usize]; 540 541 let pixels = tile_data_to_pixels(tile_data_lsb, tile_data_msb, attributes.horizontal_flip); 542 543 BgTileRow { pixels, palette: attributes.palette, high_priority: attributes.high_priority } 544} 545 546fn fetch_window_tile( 547 hardware_mode: HardwareMode, 548 fetcher_x: u8, 549 window_line: u8, 550 vram: &Vram, 551 registers: &Registers, 552) -> BgTileRow { 553 if (hardware_mode == HardwareMode::Dmg && !registers.bg_enabled) || !registers.window_enabled { 554 // All BG pixels are transparent if BG is disabled 555 return BgTileRow { pixels: [0; 8], palette: 0, high_priority: false }; 556 } 557 558 let tile_map_x: u16 = fetcher_x.into(); 559 let tile_map_y: u16 = (window_line / 8).into(); 560 561 let tile_map_addr = registers.window_tile_map_addr | (tile_map_y << 5) | tile_map_x; 562 let tile_number = vram[tile_map_addr as usize]; 563 564 let attributes_addr = 0x2000 | tile_map_addr; 565 let attributes = BgTileAttributes::from(vram[attributes_addr as usize]); 566 567 let bank_addr = u16::from(attributes.vram_bank) << 13; 568 let tile_row: u16 = if attributes.vertical_flip { 569 (7 - (window_line % 8)).into() 570 } else { 571 (window_line % 8).into() 572 }; 573 let tile_addr = 574 bank_addr | registers.bg_tile_data_area.tile_address(tile_number) | (tile_row << 1); 575 let tile_data_lsb = vram[tile_addr as usize]; 576 let tile_data_msb = vram[(tile_addr + 1) as usize]; 577 578 let pixels = tile_data_to_pixels(tile_data_lsb, tile_data_msb, attributes.horizontal_flip); 579 580 BgTileRow { pixels, palette: attributes.palette, high_priority: attributes.high_priority } 581} 582 583fn fetch_sprite_tile( 584 sprite: SpriteData, 585 y: u8, 586 vram: &Vram, 587 double_height_sprites: bool, 588) -> [u8; 8] { 589 let sprite_row = y.wrapping_sub(sprite.y.wrapping_add(16)); 590 591 let tile_number = if double_height_sprites { 592 // In double height sprite mode, the lowest bit of tile number is ignored, and which tile gets used depends 593 // on sprite row and vertical flip 594 let base_tile_number = sprite.tile_number & !0x01; 595 let lower_tile = sprite_row.bit(3) ^ sprite.vertical_flip; 596 base_tile_number | u8::from(lower_tile) 597 } else { 598 sprite.tile_number 599 }; 600 601 let tile_row = if sprite.vertical_flip { 7 - (sprite_row & 0x07) } else { sprite_row & 0x07 }; 602 603 let bank_addr = u16::from(sprite.vram_bank) << 13; 604 let tile_addr = 605 bank_addr | TileDataArea::SPRITES.tile_address(tile_number) | u16::from(tile_row << 1); 606 let tile_data_lsb = vram[tile_addr as usize]; 607 let tile_data_msb = vram[(tile_addr + 1) as usize]; 608 609 tile_data_to_pixels(tile_data_lsb, tile_data_msb, sprite.horizontal_flip) 610} 611 612fn tile_data_to_pixels(tile_data_lsb: u8, tile_data_msb: u8, horizontal_flip: bool) -> [u8; 8] { 613 if horizontal_flip { 614 array::from_fn(|i| { 615 u8::from(tile_data_lsb.bit(i as u8)) | (u8::from(tile_data_msb.bit(i as u8)) << 1) 616 }) 617 } else { 618 array::from_fn(|i| { 619 let pixel_idx = 7 - i as u8; 620 u8::from(tile_data_lsb.bit(pixel_idx)) | (u8::from(tile_data_msb.bit(pixel_idx)) << 1) 621 }) 622 } 623}