1//! PPU (pixel/picture processing unit) emulation code. 2//! 3//! In NTSC, the PPU constantly cycles through 262 scanlines: 240 visible scanlines where the PPU is 4//! actively rendering pixels, a 21-scanline vertical blanking period where the PPU is idle, and a 5//! pre-render scanline where the PPU fetches data that is needed to render the first visible scanline. 6//! 7//! PAL is (mostly) the same except the vertical blanking period lasts for 70 scanlines instead of 20, 8//! for a total of 312 scanlines. 9 10use crate::api::NesEmulatorConfig; 11use crate::bus; 12use crate::bus::{PpuBus, PpuRegisters, PpuTrackedRegister, PpuWriteToggle}; 13use bincode::{Decode, Encode}; 14use jgenesis_common::frontend::TimingMode; 15use jgenesis_common::num::GetBit; 16use std::array; 17use std::fmt::{Display, Formatter}; 18use std::ops::RangeInclusive; 19 20pub const SCREEN_WIDTH: u16 = 256; 21pub const MAX_SCREEN_HEIGHT: u16 = 240; 22 23pub const DOTS_PER_SCANLINE: u16 = 341; 24// Set/reset flags on dot 2 instead of 1 to resolve some CPU/PPU alignment issues that affect NMI 25// timing 26const VBLANK_FLAG_SET_DOT: u16 = 2; 27const RENDERING_DOTS: RangeInclusive<u16> = 1..=256; 28const SPRITE_EVALUATION_DOTS: RangeInclusive<u16> = 65..=256; 29const BG_TILE_PRE_FETCH_DOTS: RangeInclusive<u16> = 321..=336; 30const RESET_VERTICAL_POS_DOTS: RangeInclusive<u16> = 280..=304; 31const INC_VERTICAL_POS_DOT: u16 = 256; 32const RESET_HORIZONTAL_POS_DOT: u16 = 257; 33const FIRST_SPRITE_TILE_FETCH_DOT: u16 = 257; 34 35const VISIBLE_SCANLINES: RangeInclusive<u16> = 0..=239; 36const FIRST_VBLANK_SCANLINE: u16 = 241; 37const NTSC_VBLANK_SCANLINES: RangeInclusive<u16> = 241..=260; 38const NTSC_ALL_IDLE_SCANLINES: RangeInclusive<u16> = 240..=260; 39const NTSC_PRE_RENDER_SCANLINE: u16 = 261; 40const PAL_VBLANK_SCANLINES: RangeInclusive<u16> = 241..=310; 41const PAL_ALL_IDLE_SCANLINES: RangeInclusive<u16> = 240..=310; 42const PAL_PRE_RENDER_SCANLINE: u16 = 311; 43 44const BLACK_NES_COLOR: u8 = 0x0F; 45 46#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode)] 47pub struct ColorEmphasis(u8); 48 49impl ColorEmphasis { 50 pub const NONE: Self = Self(0); 51 52 pub fn new(red: bool, green: bool, blue: bool) -> Self { 53 let emphasis_bits = u8::from(red) | (u8::from(green) << 1) | (u8::from(blue) << 2); 54 Self(emphasis_bits) 55 } 56 57 pub fn get_current(bus: &PpuBus<'_>, timing_mode: TimingMode) -> Self { 58 let ppu_registers = bus.get_ppu_registers(); 59 Self::new( 60 ppu_registers.emphasize_red(timing_mode), 61 ppu_registers.emphasize_green(timing_mode), 62 ppu_registers.emphasize_blue(), 63 ) 64 } 65 66 pub fn red(self) -> bool { 67 self.0.bit(0) 68 } 69 70 pub fn green(self) -> bool { 71 self.0.bit(1) 72 } 73 74 pub fn blue(self) -> bool { 75 self.0.bit(2) 76 } 77} 78 79impl From<ColorEmphasis> for u8 { 80 fn from(value: ColorEmphasis) -> Self { 81 value.0 82 } 83} 84 85impl Display for ColorEmphasis { 86 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 87 write!(f, "ColorEmphasis[R={}, G={}, B={}]", (*self).red(), (*self).green(), (*self).blue()) 88 } 89} 90 91pub type FrameBuffer = [[(u8, ColorEmphasis); SCREEN_WIDTH as usize]; MAX_SCREEN_HEIGHT as usize]; 92 93trait TimingModePpuExt { 94 fn vblank_scanlines(self) -> RangeInclusive<u16>; 95 96 fn all_idle_scanlines(self) -> RangeInclusive<u16>; 97 98 fn pre_render_scanline(self) -> u16; 99} 100 101impl TimingModePpuExt for TimingMode { 102 fn vblank_scanlines(self) -> RangeInclusive<u16> { 103 match self { 104 Self::Ntsc => NTSC_VBLANK_SCANLINES, 105 Self::Pal => PAL_VBLANK_SCANLINES, 106 } 107 } 108 109 fn all_idle_scanlines(self) -> RangeInclusive<u16> { 110 match self { 111 Self::Ntsc => NTSC_ALL_IDLE_SCANLINES, 112 Self::Pal => PAL_ALL_IDLE_SCANLINES, 113 } 114 } 115 116 fn pre_render_scanline(self) -> u16 { 117 match self { 118 Self::Ntsc => NTSC_PRE_RENDER_SCANLINE, 119 Self::Pal => PAL_PRE_RENDER_SCANLINE, 120 } 121 } 122} 123 124#[derive(Debug, Clone, Encode, Decode)] 125struct InternalRegisters { 126 // v register (15-bit) 127 vram_address: u16, 128 // t register (15-bit) 129 temp_vram_address: u16, 130 // x register (3-bit) 131 fine_x_scroll: u8, 132} 133 134impl InternalRegisters { 135 fn new() -> Self { 136 Self { vram_address: 0, temp_vram_address: 0, fine_x_scroll: 0 } 137 } 138 139 fn fine_y(&self) -> u16 { 140 self.vram_address >> 12 141 } 142 143 fn fine_x(&self) -> u8 { 144 self.fine_x_scroll 145 } 146 147 fn coarse_y(&self) -> u16 { 148 (self.vram_address >> 5) & 0x001F 149 } 150 151 fn coarse_x(&self) -> u16 { 152 self.vram_address & 0x001F 153 } 154 155 fn nametable_bits(&self) -> u16 { 156 self.vram_address & 0x0C00 157 } 158} 159 160#[derive(Debug, Clone, Encode, Decode)] 161struct BgBuffers { 162 pattern_table_low: u16, 163 pattern_table_high: u16, 164 palette_indices: u32, 165 next_nametable_byte: u8, 166 next_palette_indices: u16, 167 next_pattern_table_low: u8, 168 next_pattern_table_high: u8, 169} 170 171impl BgBuffers { 172 fn new() -> Self { 173 Self { 174 pattern_table_low: 0, 175 pattern_table_high: 0, 176 palette_indices: 0, 177 next_nametable_byte: 0, 178 next_palette_indices: 0, 179 next_pattern_table_low: 0, 180 next_pattern_table_high: 0, 181 } 182 } 183 184 fn reload(&mut self) { 185 self.pattern_table_low |= u16::from(self.next_pattern_table_low); 186 self.pattern_table_high |= u16::from(self.next_pattern_table_high); 187 self.palette_indices |= u32::from(self.next_palette_indices); 188 } 189 190 fn shift(&mut self) { 191 self.pattern_table_low <<= 1; 192 self.pattern_table_high <<= 1; 193 self.palette_indices <<= 2; 194 } 195 196 fn get_palette_index(&self, fine_x_scroll: u8) -> u8 { 197 ((self.palette_indices & (0xC0000000 >> (2 * fine_x_scroll))) >> (30 - 2 * fine_x_scroll)) 198 as u8 199 } 200} 201 202#[derive(Debug, Clone, Copy, Encode, Decode)] 203struct SpriteBufferData { 204 y_position: u8, 205 x_position: u8, 206 attributes: u8, 207 tile_index: u8, 208} 209 210impl Default for SpriteBufferData { 211 fn default() -> Self { 212 Self { y_position: 0xFF, x_position: 0xFF, attributes: 0xFF, tile_index: 0xFF } 213 } 214} 215 216const SPRITE_BUFFER_LEN: usize = 64; 217 218#[derive(Debug, Clone, Encode, Decode)] 219struct SpriteBuffers { 220 sprites: [SpriteBufferData; SPRITE_BUFFER_LEN], 221 pattern_table_low: [u8; SPRITE_BUFFER_LEN], 222 pattern_table_high: [u8; SPRITE_BUFFER_LEN], 223 buffer_len: u8, 224 sprite_0_buffered: bool, 225} 226 227impl SpriteBuffers { 228 fn new() -> Self { 229 Self { 230 sprites: array::from_fn(|_| SpriteBufferData::default()), 231 pattern_table_low: array::from_fn(|_| 0), 232 pattern_table_high: array::from_fn(|_| 0), 233 buffer_len: 0, 234 sprite_0_buffered: false, 235 } 236 } 237} 238 239#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)] 240enum SpriteEvaluationState { 241 ScanningOam { primary_oam_index: u8 }, 242 CopyingOam { primary_oam_index: u8, byte_index: u8 }, 243 CheckingForOverflow { oam_index: u8, oam_offset: u8, skip_bytes_remaining: u8 }, 244 Done { oam_index: u8 }, 245} 246 247#[derive(Debug, Clone, Encode, Decode)] 248struct SpriteEvaluationData { 249 secondary_oam: [u8; SPRITE_BUFFER_LEN * 4], 250 sprites_found: u8, 251 sprite_0_found: bool, 252 state: SpriteEvaluationState, 253} 254 255impl SpriteEvaluationData { 256 fn new() -> Self { 257 Self { 258 secondary_oam: [0xFF; SPRITE_BUFFER_LEN * 4], 259 sprites_found: 0, 260 sprite_0_found: false, 261 state: SpriteEvaluationState::ScanningOam { primary_oam_index: 0 }, 262 } 263 } 264 265 fn update_sprite_buffers(&self, buffers: &mut SpriteBuffers) { 266 buffers.sprites.fill(SpriteBufferData::default()); 267 268 for (i, &[y_position, tile_index, attributes_byte, x_position]) in self 269 .secondary_oam 270 .as_chunks::<4>() 271 .0 272 .iter() 273 .take(self.sprites_found as usize) 274 .enumerate() 275 { 276 buffers.sprites[i] = SpriteBufferData { 277 y_position, 278 x_position, 279 attributes: attributes_byte, 280 tile_index, 281 }; 282 } 283 284 buffers.pattern_table_low.fill(0); 285 buffers.pattern_table_high.fill(0); 286 buffers.buffer_len = self.sprites_found; 287 buffers.sprite_0_buffered = self.sprite_0_found; 288 } 289} 290 291#[derive(Debug, Clone, Copy, Encode, Decode)] 292struct SpriteData { 293 color_id: u8, 294 is_sprite_0: bool, 295 attributes: u8, 296} 297 298impl SpriteData { 299 // Color 0 is transparent and will never display 300 const NONE: Self = Self { color_id: 0, is_sprite_0: false, attributes: 0x00 }; 301} 302 303type SpriteLineBuffer = [SpriteData; SCREEN_WIDTH as usize]; 304 305#[derive(Debug, Clone, Encode, Decode)] 306pub struct PpuState { 307 timing_mode: TimingMode, 308 pub ntsc_crop_vertical_overscan: bool, 309 frame_buffer: Box<FrameBuffer>, 310 registers: InternalRegisters, 311 bg_buffers: BgBuffers, 312 sprite_buffers: SpriteBuffers, 313 sprite_evaluation_data: SpriteEvaluationData, 314 sprite_line_buffer: SpriteLineBuffer, 315 scanline: u16, 316 dot: u16, 317 odd_frame: bool, 318 sprite_0_hit_delay: u8, 319 first_frame: bool, 320 cycle_counter: u64, 321 frame_start_cycles: u64, 322} 323 324impl PpuState { 325 pub fn new(timing_mode: TimingMode, ntsc_crop_vertical_overscan: bool) -> Self { 326 Self { 327 timing_mode, 328 ntsc_crop_vertical_overscan, 329 frame_buffer: vec![ 330 [(0, ColorEmphasis::default()); SCREEN_WIDTH as usize]; 331 MAX_SCREEN_HEIGHT as usize 332 ] 333 .into_boxed_slice() 334 .try_into() 335 .unwrap(), 336 registers: InternalRegisters::new(), 337 bg_buffers: BgBuffers::new(), 338 sprite_buffers: SpriteBuffers::new(), 339 sprite_evaluation_data: SpriteEvaluationData::new(), 340 sprite_line_buffer: array::from_fn(|_| SpriteData::NONE), 341 scanline: timing_mode.pre_render_scanline(), 342 dot: 0, 343 odd_frame: false, 344 sprite_0_hit_delay: 0, 345 first_frame: true, 346 cycle_counter: 0, 347 frame_start_cycles: 0, 348 } 349 } 350 351 /// Return whether the PPU is currently in the vertical blanking period. 352 /// 353 /// While the PPU's first idle scanline is scanline 240, this method will not return true 354 /// until scanline 241 in order to align with when the PPU sets the VBlank flag in PPUSTATUS. 355 pub fn in_vblank(&self) -> bool { 356 self.timing_mode.vblank_scanlines().contains(&self.scanline) 357 } 358 359 /// Retrieve a reference the PPU's frame buffer. 360 /// 361 /// The frame buffer is a 256x240 grid storing 6-bit NES colors. These colors 362 /// do not map directly to RGB; some sort of palette is needed to convert these colors to RGB 363 /// colors that are appropriate for display. 364 pub fn frame_buffer(&self) -> &FrameBuffer { 365 &self.frame_buffer 366 } 367 368 pub fn frame_start_cycles(&self) -> u64 { 369 self.frame_start_cycles 370 } 371 372 fn set_in_frame_buffer( 373 &mut self, 374 y: u16, 375 x: u16, 376 pixel: u8, 377 color_emphasis: ColorEmphasis, 378 bus: &mut PpuBus<'_>, 379 ) { 380 self.frame_buffer[y as usize][x as usize] = (pixel, color_emphasis); 381 382 let display_mode = 383 if !self.ntsc_crop_vertical_overscan { TimingMode::Pal } else { self.timing_mode }; 384 bus.handle_pixel_rendered(pixel, x, y, display_mode); 385 } 386} 387 388pub fn render_pal_black_border(state: &mut PpuState) { 389 // Clear top scanline 390 for (color, emphasis) in &mut state.frame_buffer[0] { 391 *color = BLACK_NES_COLOR; 392 *emphasis = ColorEmphasis::default(); 393 } 394 395 // Clear leftmost two columns and rightmost two columns 396 for col in [0, 1, (SCREEN_WIDTH - 2) as usize, (SCREEN_WIDTH - 1) as usize] { 397 for row in 1..MAX_SCREEN_HEIGHT as usize { 398 state.frame_buffer[row][col] = (BLACK_NES_COLOR, ColorEmphasis::default()); 399 } 400 } 401} 402 403/// Run the PPU for one PPU cycle. Pixels will be written to `PpuState`'s frame buffer as appropriate. 404pub fn tick(state: &mut PpuState, bus: &mut PpuBus<'_>, config: &NesEmulatorConfig) { 405 let rendering_enabled = 406 bus.get_ppu_registers().bg_enabled() || bus.get_ppu_registers().sprites_enabled(); 407 408 process_register_updates(state, bus, rendering_enabled); 409 410 if state.scanline == state.timing_mode.pre_render_scanline() && state.dot == VBLANK_FLAG_SET_DOT 411 { 412 // Clear per-frame flags at the start of the pre-render scanline 413 let ppu_registers = bus.get_ppu_registers_mut(); 414 ppu_registers.set_vblank_flag(false); 415 ppu_registers.set_sprite_0_hit(false); 416 ppu_registers.set_sprite_overflow(false); 417 if !state.first_frame { 418 ppu_registers.clear_reset_flag(); 419 } 420 state.first_frame = false; 421 } else if state.scanline == FIRST_VBLANK_SCANLINE && state.dot == VBLANK_FLAG_SET_DOT { 422 bus.get_ppu_registers_mut().set_vblank_flag(true); 423 } 424 425 let color_mask = get_color_mask(bus.get_ppu_registers()); 426 if rendering_enabled { 427 process_scanline(state, bus, config.remove_sprite_limit); 428 } else { 429 bus.get_ppu_registers_mut().set_oam_open_bus(None); 430 431 if VISIBLE_SCANLINES.contains(&state.scanline) && RENDERING_DOTS.contains(&state.dot) { 432 // When rendering is disabled, the PPU normally always outputs the backdrop color (index 0), 433 // but if the current VRAM address is in the palette RAM range ($3F00-$3FFF) then it will 434 // use the color at the current palette RAM address instead. 435 // Micro Machines depends on this for correct rendering, as do certain test roms (e.g. full_palette.nes) 436 let vram_addr = state.registers.vram_address & 0x3FFF; 437 let palette_ram_addr = if (0x3F00..=0x3FFF).contains(&vram_addr) { 438 vram_addr & bus::PALETTE_RAM_MASK 439 } else { 440 0 441 }; 442 let backdrop_color = bus.get_palette_ram()[palette_ram_addr as usize] & color_mask; 443 444 let color_emphasis = ColorEmphasis::get_current(bus, state.timing_mode); 445 state.set_in_frame_buffer( 446 state.scanline, 447 state.dot - 1, 448 backdrop_color, 449 color_emphasis, 450 bus, 451 ); 452 } 453 } 454 455 // Copy v register to where the CPU can see it 456 if !rendering_enabled || state.timing_mode.all_idle_scanlines().contains(&state.scanline) { 457 bus.set_bus_address(state.registers.vram_address & 0x3FFF); 458 } 459 460 state.cycle_counter += 1; 461 462 state.dot += 1; 463 if state.dot == DOTS_PER_SCANLINE { 464 state.scanline += 1; 465 state.dot = 0; 466 467 if state.scanline == state.timing_mode.pre_render_scanline() + 1 { 468 state.scanline = 0; 469 470 if state.timing_mode == TimingMode::Ntsc && state.odd_frame && rendering_enabled { 471 // In NTSC, skip the idle cycle in the first visible scanline on odd frames 472 state.dot = 1; 473 } 474 state.odd_frame = !state.odd_frame; 475 476 state.frame_start_cycles = state.cycle_counter; 477 } 478 } 479} 480 481fn get_color_mask(registers: &PpuRegisters) -> u8 { 482 // NES colors are 6 bits normally, and greyscale mode masks out the lower 4 bits 483 if registers.greyscale() { 0x30 } else { 0x3F } 484} 485 486/// Reset the PPU, as if the console's reset button was pressed. 487/// 488/// This resets all PPU state except for the internal v register, and also clears most of the 489/// memory-mapped PPU registers. 490pub fn reset(state: &mut PpuState, bus: &mut PpuBus<'_>) { 491 let vram_address = state.registers.vram_address; 492 *state = PpuState::new(state.timing_mode, state.ntsc_crop_vertical_overscan); 493 state.registers.vram_address = vram_address; 494 495 bus.reset(); 496} 497 498fn process_scanline(state: &mut PpuState, bus: &mut PpuBus<'_>, remove_sprite_limit: bool) { 499 let scanline = state.scanline; 500 let dot = state.dot; 501 let timing_mode = state.timing_mode; 502 503 log::trace!("Rendering at scanline {scanline} dot {dot}"); 504 505 if state.sprite_0_hit_delay != 0 { 506 // If sprite 0 hit triggered 2 cycles ago, set the flag in PPUSTATUS 507 state.sprite_0_hit_delay -= 1; 508 if state.sprite_0_hit_delay == 0 { 509 bus.get_ppu_registers_mut().set_sprite_0_hit(true); 510 } 511 } 512 513 match (timing_mode, scanline) { 514 (_, 0..=239) | (TimingMode::Ntsc, 261) | (TimingMode::Pal, 311) => { 515 let is_pre_render_scanline = scanline == timing_mode.pre_render_scanline(); 516 517 if is_pre_render_scanline && RESET_VERTICAL_POS_DOTS.contains(&dot) { 518 // Repeatedly reset vertical position during the pre-render scanline 519 reset_vertical_pos(&mut state.registers); 520 } 521 522 if !is_pre_render_scanline && dot == 1 { 523 // Clear sprite evaluation data at the beginning of each visible scanline 524 state.sprite_evaluation_data = SpriteEvaluationData::new(); 525 } 526 527 // Render scanlines 528 #[allow(clippy::match_same_arms)] 529 match dot { 530 0 => { 531 // Idle cycle 532 533 bus.get_ppu_registers_mut() 534 .set_oam_open_bus(Some(state.sprite_evaluation_data.secondary_oam[0])); 535 } 536 1..=256 => { 537 // Rendering + sprite evaluation cycles 538 539 if !is_pre_render_scanline { 540 render_pixel(state, bus); 541 } 542 543 if dot > 1 && (dot - 1).trailing_zeros() >= 3 { 544 // Increment horizontal position on cycles 9, 17, 25, .. 545 // before fetching BG tile data 546 increment_horizontal_pos(&mut state.registers); 547 } 548 549 // Start fetching data for the next tile cycle if appropriate 550 fetch_bg_tile_data(state, bus); 551 552 // Evaluate sprites on odd cycles during 65-256 553 if !is_pre_render_scanline 554 && SPRITE_EVALUATION_DOTS.contains(&dot) 555 && dot.bit(0) 556 { 557 evaluate_sprites(state, bus, remove_sprite_limit); 558 559 if remove_sprite_limit && dot == 255 { 560 finish_sprite_evaluation_no_limit(state, bus); 561 } 562 } 563 564 if !SPRITE_EVALUATION_DOTS.contains(&dot) { 565 // OAMDATA always reads $FF during cycles 1-64 566 bus.get_ppu_registers_mut().set_oam_open_bus(Some(0xFF)); 567 } 568 569 if !is_pre_render_scanline && dot == INC_VERTICAL_POS_DOT { 570 // Increment effective vertical position at the end of the rendering phase 571 increment_vertical_pos(&mut state.registers); 572 } 573 } 574 257..=320 => { 575 // Cycles for fetching sprite data for the next scanline 576 577 if dot == RESET_HORIZONTAL_POS_DOT { 578 // Reset horizontal position immediately after the rendering phase 579 reset_horizontal_pos(&mut state.registers); 580 581 // Fill sprite buffers with sprite data for the next scanline 582 state 583 .sprite_evaluation_data 584 .update_sprite_buffers(&mut state.sprite_buffers); 585 } 586 587 fetch_sprite_tile_data(bus, scanline, dot, &mut state.sprite_buffers); 588 589 // OAMADDR is repeatedly reset to 0 during these dots on rendering scanlines. 590 // Ghostbusters II depends on this due to sometimes running OAM DMAs that don't 591 // finish before the pre-render scanline begins, and it never explicitly writes 592 // to OAMADDR after boot 593 bus.get_ppu_registers_mut().set_oam_addr(0); 594 595 if dot == 320 { 596 if remove_sprite_limit { 597 finish_sprite_tile_fetches( 598 bus, 599 scanline, 600 dot, 601 &mut state.sprite_buffers, 602 ); 603 } 604 fill_sprite_line_buffer( 605 &state.sprite_buffers, 606 &mut state.sprite_line_buffer, 607 ); 608 } 609 } 610 321..=336 => { 611 // Cycles for fetching BG tile data for the first 2 tiles of the next scanline 612 613 fetch_bg_tile_data(state, bus); 614 state.bg_buffers.shift(); 615 616 if dot.trailing_zeros() >= 3 { 617 // Increment horizontal position and reload buffers at the end of each tile 618 // (dots 328 and 336) 619 increment_horizontal_pos(&mut state.registers); 620 state.bg_buffers.reload(); 621 } 622 623 bus.get_ppu_registers_mut() 624 .set_oam_open_bus(Some(state.sprite_evaluation_data.secondary_oam[0])); 625 } 626 337 | 339 => { 627 // Idle cycles that do spurious reads 628 // At least one mapper depends on these reads happening (MMC5) 629 fetch_nametable_byte(&state.registers, bus); 630 631 bus.get_ppu_registers_mut() 632 .set_oam_open_bus(Some(state.sprite_evaluation_data.secondary_oam[0])); 633 } 634 338 | 340 => { 635 // Truly idle cycles at the end of each scanline 636 637 bus.get_ppu_registers_mut() 638 .set_oam_open_bus(Some(state.sprite_evaluation_data.secondary_oam[0])); 639 } 640 _ => panic!("invalid dot: {dot}"), 641 } 642 } 643 (TimingMode::Ntsc, 240..=260) | (TimingMode::Pal, 240..=310) => { 644 // PPU idle scanlines 645 646 bus.get_ppu_registers_mut().set_oam_open_bus(None); 647 } 648 _ => panic!("invalid scanline: {scanline}"), 649 } 650} 651 652fn process_register_updates(state: &mut PpuState, bus: &mut PpuBus<'_>, rendering_enabled: bool) { 653 match bus.get_ppu_registers_mut().take_last_accessed_register() { 654 Some(PpuTrackedRegister::PPUCTRL) => { 655 let ppu_ctrl = bus.get_ppu_registers().ppu_ctrl(); 656 log::trace!( 657 "PPU: {ppu_ctrl:02X} written to PPUCTRL on scanline {}, dot {}", 658 state.scanline, 659 state.dot 660 ); 661 662 // Set nametable bits 663 state.registers.temp_vram_address = 664 (state.registers.temp_vram_address & 0xF3FF) | (u16::from(ppu_ctrl & 0x03) << 10); 665 } 666 Some(PpuTrackedRegister::PPUSCROLL) => { 667 let value = bus.get_ppu_registers().get_ppu_open_bus_value(); 668 log::trace!( 669 "PPU: {value:02X} written to PPUSCROLL, write_toggle={:?} on scanline {}, dot {}", 670 bus.get_ppu_registers().get_write_toggle(), 671 state.scanline, 672 state.dot, 673 ); 674 675 match bus.get_ppu_registers().get_write_toggle() { 676 PpuWriteToggle::Second => { 677 // Write was with w=0, set coarse X and fine X 678 state.registers.temp_vram_address = 679 (state.registers.temp_vram_address & 0xFFE0) | u16::from(value >> 3); 680 state.registers.fine_x_scroll = value & 0x07; 681 } 682 PpuWriteToggle::First => { 683 // Write was with w=1, set coarse Y and fine Y 684 state.registers.temp_vram_address = (state.registers.temp_vram_address 685 & 0x0C1F) 686 | (u16::from(value & 0x07) << 12) 687 | (u16::from(value & 0xF8) << 2); 688 } 689 } 690 } 691 Some(PpuTrackedRegister::PPUADDR) => { 692 let value = bus.get_ppu_registers().get_ppu_open_bus_value(); 693 log::trace!( 694 "PPU: {value:02X} written to PPUADDR, write_toggle={:?} on scanline {}, dot {}", 695 bus.get_ppu_registers().get_write_toggle(), 696 state.scanline, 697 state.dot 698 ); 699 700 match bus.get_ppu_registers().get_write_toggle() { 701 PpuWriteToggle::Second => { 702 // Write was with w=0, set bits 13-8 and clear bit 14 703 state.registers.temp_vram_address = (state.registers.temp_vram_address 704 & 0x00FF) 705 | (u16::from(value & 0x3F) << 8); 706 } 707 PpuWriteToggle::First => { 708 // Write was with w=1, set bits 7-0 and copy from t to v 709 state.registers.temp_vram_address = 710 (state.registers.temp_vram_address & 0xFF00) | u16::from(value); 711 state.registers.vram_address = state.registers.temp_vram_address; 712 } 713 } 714 } 715 Some(PpuTrackedRegister::PPUDATA) => { 716 if rendering_enabled 717 && (VISIBLE_SCANLINES.contains(&state.scanline) 718 || state.scanline == state.timing_mode.pre_render_scanline()) 719 { 720 // Accessing PPUDATA during rendering causes a coarse X increment + Y increment 721 log::trace!( 722 "PPU: PPUDATA was accessed during rendering (scanline {} / dot {}), incrementing coarse X and Y in v register", 723 state.scanline, 724 state.dot 725 ); 726 727 increment_horizontal_pos(&mut state.registers); 728 increment_vertical_pos(&mut state.registers); 729 } else { 730 log::trace!( 731 "PPU: PPUDATA was accessed on scanline {} / dot {}, incrementing internal v register by {}", 732 state.scanline, 733 state.dot, 734 bus.get_ppu_registers().ppu_data_addr_increment() 735 ); 736 737 // Any time the CPU accesses PPUDATA outside of rendering, increment VRAM address by 738 // 1 or 32 based on PPUCTRL 739 state.registers.vram_address = state 740 .registers 741 .vram_address 742 .wrapping_add(bus.get_ppu_registers().ppu_data_addr_increment()); 743 } 744 } 745 None => {} 746 } 747} 748 749fn increment_horizontal_pos(registers: &mut InternalRegisters) { 750 // Increment coarse X 751 let coarse_x = registers.coarse_x(); 752 if coarse_x == 0x001F { 753 // Clear coarse X 754 registers.vram_address &= !0x001F; 755 756 // Wrap nametable horizontally 757 registers.vram_address ^= 0x0400; 758 } else { 759 registers.vram_address += 1; 760 } 761} 762 763fn increment_vertical_pos(registers: &mut InternalRegisters) { 764 let fine_y = registers.fine_y(); 765 if fine_y < 7 { 766 // Increment fine Y 767 registers.vram_address = ((fine_y + 1) << 12) | (registers.vram_address & 0x0FFF); 768 } else { 769 let coarse_y = registers.coarse_y(); 770 if coarse_y == 29 { 771 // Clear fine Y and coarse Y 772 registers.vram_address &= 0x0C1F; 773 774 // Wrap nametable vertically 775 registers.vram_address ^= 0x0800; 776 } else if coarse_y == 31 { 777 // Clear fine Y and coarse Y, don't wrap nametable 778 registers.vram_address &= 0x0C1F; 779 } else { 780 // Clear fine Y and increment coarse Y 781 registers.vram_address = (registers.vram_address & 0x0C1F) | ((coarse_y + 1) << 5); 782 } 783 } 784} 785 786fn reset_horizontal_pos(registers: &mut InternalRegisters) { 787 // Copy coarse X and nametable horizontal bit from t to v 788 registers.vram_address = 789 (registers.vram_address & 0xFBE0) | (registers.temp_vram_address & 0x041F); 790} 791 792fn reset_vertical_pos(registers: &mut InternalRegisters) { 793 // Copy fine Y, coarse Y, and nametable vertical bit from t to v 794 registers.vram_address = 795 (registers.vram_address & 0x041F) | (registers.temp_vram_address & 0xFBE0); 796} 797 798fn render_pixel(state: &mut PpuState, bus: &mut PpuBus<'_>) { 799 let pixel = (state.dot - 1) as u8; 800 801 let tile_cycle_offset = pixel & 0x07; 802 if state.dot > 1 && tile_cycle_offset == 0 { 803 // Reload the BG buffers on cycles 9, 17, 25, ... 804 state.bg_buffers.reload(); 805 } 806 807 let ppu_registers = bus.get_ppu_registers(); 808 let bg_enabled = ppu_registers.bg_enabled(); 809 let sprites_enabled = ppu_registers.sprites_enabled(); 810 let left_edge_bg_enabled = ppu_registers.left_edge_bg_enabled(); 811 let left_edge_sprites_enabled = ppu_registers.left_edge_sprites_enabled(); 812 813 // Get next BG pixel color ID 814 let bg_color_id = if bg_enabled && (pixel >= 8 || left_edge_bg_enabled) { 815 get_bg_color_id( 816 state.bg_buffers.pattern_table_low, 817 state.bg_buffers.pattern_table_high, 818 state.registers.fine_x(), 819 ) 820 } else { 821 0 822 }; 823 let bg_palette_index = state.bg_buffers.get_palette_index(state.registers.fine_x()); 824 state.bg_buffers.shift(); 825 826 // Find the first overlapping sprite by OAM index, if any; use transparent if none found 827 let sprite = if state.scanline != 0 828 && state.scanline != state.timing_mode.pre_render_scanline() 829 && sprites_enabled 830 && (pixel >= 8 || left_edge_sprites_enabled) 831 { 832 state.sprite_line_buffer[pixel as usize] 833 } else { 834 SpriteData::NONE 835 }; 836 837 if sprite.is_sprite_0 && bg_color_id != 0 && sprite.color_id != 0 && pixel < 255 { 838 // Set sprite 0 hit when a non-transparent sprite pixel overlaps a non-transparent BG pixel 839 // at x < 255. 840 // Set the actual flag in PPUSTATUS on a 2-PPU-cycle delay to avoid some CPU/PPU alignment 841 // issues. 842 if state.sprite_0_hit_delay == 0 { 843 state.sprite_0_hit_delay = 2; 844 } 845 } 846 847 let sprite_bg_priority = sprite.attributes.bit(5); 848 let sprite_palette_index = sprite.attributes & 0x03; 849 850 // Determine whether to show BG pixel color, sprite pixel color, or backdrop color 851 let palette_ram = bus.get_palette_ram(); 852 let backdrop_color = palette_ram[0]; 853 let pixel_color = if sprite.color_id != 0 && (bg_color_id == 0 || !sprite_bg_priority) { 854 let palette_addr = 0x10 | (sprite_palette_index << 2) | sprite.color_id; 855 palette_ram[palette_addr as usize] 856 } else if bg_color_id != 0 { 857 let palette_addr = (bg_palette_index << 2) | bg_color_id; 858 palette_ram[palette_addr as usize] 859 } else { 860 backdrop_color 861 }; 862 863 let pixel_color = pixel_color & get_color_mask(bus.get_ppu_registers()); 864 let color_emphasis = ColorEmphasis::get_current(bus, state.timing_mode); 865 866 // Render the pixel to the frame buffer 867 state.set_in_frame_buffer(state.scanline, pixel.into(), pixel_color, color_emphasis, bus); 868} 869 870fn fetch_bg_tile_data(state: &mut PpuState, bus: &mut PpuBus<'_>) { 871 debug_assert!( 872 RENDERING_DOTS.contains(&state.dot) || BG_TILE_PRE_FETCH_DOTS.contains(&state.dot) 873 ); 874 875 let tile_cycle_offset = (state.dot - 1) & 0x07; 876 let bg_pattern_table_address = bus.get_ppu_registers().bg_pattern_table_address(); 877 878 // These offsets are not cycle accurate, but for some reason these timings cause the MMC3 IRQ 879 // tests to pass 880 match tile_cycle_offset { 881 0 => { 882 state.bg_buffers.next_nametable_byte = fetch_nametable_byte(&state.registers, bus); 883 } 884 1 => { 885 let next_palette_index = u16::from(fetch_palette_index(&state.registers, bus)); 886 state.bg_buffers.next_palette_indices = 887 (0..8).map(|i| next_palette_index << (2 * i)).reduce(|a, b| a | b).unwrap(); 888 } 889 2 => { 890 state.bg_buffers.next_pattern_table_low = fetch_bg_pattern_table_byte( 891 bg_pattern_table_address, 892 state.bg_buffers.next_nametable_byte, 893 state.registers.fine_y(), 894 PatternTableByte::Low, 895 bus, 896 ); 897 } 898 4 => { 899 state.bg_buffers.next_pattern_table_high = fetch_bg_pattern_table_byte( 900 bg_pattern_table_address, 901 state.bg_buffers.next_nametable_byte, 902 state.registers.fine_y(), 903 PatternTableByte::High, 904 bus, 905 ); 906 } 907 _ => {} 908 } 909} 910 911fn fetch_sprite_tile_data( 912 bus: &mut PpuBus<'_>, 913 scanline: u16, 914 dot: u16, 915 sprite_buffers: &mut SpriteBuffers, 916) { 917 debug_assert!(dot >= FIRST_SPRITE_TILE_FETCH_DOT); 918 919 let sprite_pattern_table_address = bus.get_ppu_registers().sprite_pattern_table_address(); 920 let double_height_sprites = bus.get_ppu_registers().double_height_sprites(); 921 922 // 8 cycles per sprite 923 let sprite_index = sprite_fetch_index(dot); 924 925 let SpriteBufferData { y_position, attributes, tile_index, .. } = 926 sprite_buffers.sprites[sprite_index as usize]; 927 928 // This is not completely accurate but it's close enough 929 // In reality, during cycles 1-4 the value will be Y position, tile index, attributes, and X position in that order 930 // During cycles 5-8 it will stay X position 931 // Once past the end of the sprite buffer, the value will be sprite 63's Y position once, then $FF for the rest of this period 932 if sprite_index < sprite_buffers.buffer_len { 933 bus.get_ppu_registers_mut() 934 .set_oam_open_bus(Some(sprite_buffers.sprites[sprite_index as usize].x_position)); 935 } else { 936 bus.get_ppu_registers_mut().set_oam_open_bus(Some(0xFF)); 937 } 938 939 // These offsets are not cycle accurate, but for some reason these timings cause the MMC3 IRQ 940 // tests to pass 941 let tile_cycle_offset = (dot - 1) & 0x07; 942 match tile_cycle_offset { 943 0 | 1 => { 944 // Spurious nametable fetch 945 // Address doesn't matter, but needs to vary based on sprite to avoid triggering 946 // MMC5 scanline counter 947 bus.read_address(0x2000 + u16::from(sprite_index) + 1); 948 } 949 2 => { 950 if sprite_index < sprite_buffers.buffer_len { 951 let pattern_table_low = fetch_sprite_pattern_table_byte( 952 sprite_pattern_table_address, 953 double_height_sprites, 954 y_position, 955 attributes, 956 tile_index, 957 scanline as u8, 958 PatternTableByte::Low, 959 bus, 960 ); 961 sprite_buffers.pattern_table_low[sprite_index as usize] = pattern_table_low; 962 } else { 963 // Spurious read 964 fetch_sprite_pattern_table_byte( 965 sprite_pattern_table_address, 966 double_height_sprites, 967 0xFF, 968 0xFF, 969 0xFF, 970 0xFF, 971 PatternTableByte::Low, 972 bus, 973 ); 974 } 975 } 976 4 => { 977 if sprite_index < sprite_buffers.buffer_len { 978 let pattern_table_high = fetch_sprite_pattern_table_byte( 979 sprite_pattern_table_address, 980 double_height_sprites, 981 y_position, 982 attributes, 983 tile_index, 984 scanline as u8, 985 PatternTableByte::High, 986 bus, 987 ); 988 sprite_buffers.pattern_table_high[sprite_index as usize] = pattern_table_high; 989 } else { 990 // Spurious read 991 fetch_sprite_pattern_table_byte( 992 sprite_pattern_table_address, 993 double_height_sprites, 994 0xFF, 995 0xFF, 996 0xFF, 997 0xFF, 998 PatternTableByte::High, 999 bus, 1000 ); 1001 } 1002 } 1003 _ => {} 1004 } 1005} 1006 1007fn sprite_fetch_index(dot: u16) -> u8 { 1008 ((dot - FIRST_SPRITE_TILE_FETCH_DOT) >> 3) as u8 1009} 1010 1011fn finish_sprite_tile_fetches( 1012 bus: &mut PpuBus<'_>, 1013 scanline: u16, 1014 dot: u16, 1015 sprite_buffers: &mut SpriteBuffers, 1016) { 1017 let mut dot = dot + 1; 1018 1019 while sprite_fetch_index(dot) < sprite_buffers.buffer_len { 1020 fetch_sprite_tile_data(bus, scanline, dot, sprite_buffers); 1021 dot += 1; 1022 } 1023} 1024 1025const SPRITE_PER_SCANLINE_LIMIT: u8 = 8; 1026 1027fn evaluate_sprites(state: &mut PpuState, bus: &mut PpuBus<'_>, remove_sprite_limit: bool) { 1028 let sprite_height = if bus.get_ppu_registers().double_height_sprites() { 16 } else { 8 }; 1029 1030 let evaluation_data = &mut state.sprite_evaluation_data; 1031 let oam = bus.get_oam(); 1032 evaluation_data.state = match evaluation_data.state { 1033 SpriteEvaluationState::ScanningOam { primary_oam_index } => { 1034 debug_assert!( 1035 primary_oam_index < 64 1036 && (remove_sprite_limit 1037 || evaluation_data.sprites_found < SPRITE_PER_SCANLINE_LIMIT) 1038 ); 1039 1040 let y_position = oam[(primary_oam_index << 2) as usize]; 1041 1042 bus.get_ppu_registers_mut().set_oam_open_bus(Some(y_position)); 1043 1044 evaluation_data.secondary_oam[(evaluation_data.sprites_found << 2) as usize] = 1045 y_position; 1046 1047 if (y_position..y_position.saturating_add(sprite_height)) 1048 .contains(&(state.scanline as u8)) 1049 { 1050 if primary_oam_index == 0 { 1051 evaluation_data.sprite_0_found = true; 1052 } 1053 1054 SpriteEvaluationState::CopyingOam { primary_oam_index, byte_index: 1 } 1055 } else if primary_oam_index < 63 { 1056 SpriteEvaluationState::ScanningOam { primary_oam_index: primary_oam_index + 1 } 1057 } else { 1058 SpriteEvaluationState::Done { oam_index: primary_oam_index } 1059 } 1060 } 1061 SpriteEvaluationState::CopyingOam { primary_oam_index, byte_index } => { 1062 debug_assert!(primary_oam_index < 64 && byte_index < 4); 1063 1064 let next_byte = oam[((primary_oam_index << 2) | byte_index) as usize]; 1065 evaluation_data.secondary_oam 1066 [((evaluation_data.sprites_found << 2) | byte_index) as usize] = next_byte; 1067 1068 bus.get_ppu_registers_mut().set_oam_open_bus(Some(next_byte)); 1069 1070 if byte_index < 3 { 1071 SpriteEvaluationState::CopyingOam { primary_oam_index, byte_index: byte_index + 1 } 1072 } else { 1073 evaluation_data.sprites_found += 1; 1074 1075 let next_oam_index = primary_oam_index + 1; 1076 if next_oam_index == 64 { 1077 SpriteEvaluationState::Done { oam_index: primary_oam_index } 1078 } else if !remove_sprite_limit 1079 && evaluation_data.sprites_found == SPRITE_PER_SCANLINE_LIMIT 1080 { 1081 SpriteEvaluationState::CheckingForOverflow { 1082 oam_index: next_oam_index, 1083 oam_offset: 0, 1084 skip_bytes_remaining: 0, 1085 } 1086 } else { 1087 // This isn't completely accurate because of the buggy nature of how the 1088 // PPU checks for sprite overflow on actual hardware, but it seems to work 1089 // well enough to not break games that depend on this flag 1090 if remove_sprite_limit 1091 && evaluation_data.sprites_found == SPRITE_PER_SCANLINE_LIMIT + 1 1092 { 1093 bus.get_ppu_registers_mut().set_sprite_overflow(true); 1094 } 1095 1096 SpriteEvaluationState::ScanningOam { primary_oam_index: next_oam_index } 1097 } 1098 } 1099 } 1100 SpriteEvaluationState::CheckingForOverflow { 1101 oam_index, 1102 oam_offset, 1103 skip_bytes_remaining, 1104 } => { 1105 if skip_bytes_remaining > 0 { 1106 let dummy_read = oam[((oam_index << 2) | oam_offset) as usize]; 1107 bus.get_ppu_registers_mut().set_oam_open_bus(Some(dummy_read)); 1108 1109 SpriteEvaluationState::CheckingForOverflow { 1110 oam_index, 1111 oam_offset: (oam_offset + 1) & 0x03, 1112 skip_bytes_remaining: skip_bytes_remaining - 1, 1113 } 1114 } else { 1115 let y_position = oam[((oam_index << 2) | oam_offset) as usize]; 1116 1117 bus.get_ppu_registers_mut().set_oam_open_bus(Some(y_position)); 1118 1119 if (y_position..y_position.saturating_add(sprite_height)) 1120 .contains(&(state.scanline as u8)) 1121 { 1122 bus.get_ppu_registers_mut().set_sprite_overflow(true); 1123 1124 SpriteEvaluationState::Done { oam_index } 1125 } else if oam_index < 63 { 1126 // Yes, increment both index and offset; this is replicating a hardware bug that 1127 // makes the sprite overflow flag essentially useless 1128 SpriteEvaluationState::CheckingForOverflow { 1129 oam_index: oam_index + 1, 1130 oam_offset: (oam_offset + 1) & 0x03, 1131 skip_bytes_remaining: 0, 1132 } 1133 } else { 1134 SpriteEvaluationState::Done { oam_index } 1135 } 1136 } 1137 } 1138 SpriteEvaluationState::Done { oam_index } => { 1139 let dummy_read = oam[(oam_index << 2) as usize]; 1140 bus.get_ppu_registers_mut().set_oam_open_bus(Some(dummy_read)); 1141 1142 SpriteEvaluationState::Done { oam_index } 1143 } 1144 }; 1145} 1146 1147fn finish_sprite_evaluation_no_limit(state: &mut PpuState, bus: &mut PpuBus<'_>) { 1148 while !matches!(&state.sprite_evaluation_data.state, SpriteEvaluationState::Done { .. }) { 1149 evaluate_sprites(state, bus, true); 1150 } 1151} 1152 1153fn fill_sprite_line_buffer(sprite_buffers: &SpriteBuffers, line_buffer: &mut SpriteLineBuffer) { 1154 line_buffer.fill(SpriteData::NONE); 1155 1156 for i in 0..sprite_buffers.buffer_len { 1157 let SpriteBufferData { x_position: x_pos, attributes, .. } = 1158 sprite_buffers.sprites[i as usize]; 1159 1160 let sprite_flip_x = attributes.bit(6); 1161 let is_sprite_0 = i == 0 && sprite_buffers.sprite_0_buffered; 1162 1163 for x in x_pos..=x_pos.saturating_add(7) { 1164 if line_buffer[x as usize].color_id != 0 { 1165 // There is already a non-transparent sprite pixel in this position with a lower OAM index 1166 continue; 1167 } 1168 1169 // Determine sprite pixel color ID 1170 let sprite_fine_x = if sprite_flip_x { 7 - (x - x_pos) } else { x - x_pos }; 1171 let color_id = get_color_id( 1172 sprite_buffers.pattern_table_low[i as usize], 1173 sprite_buffers.pattern_table_high[i as usize], 1174 sprite_fine_x, 1175 ); 1176 1177 if color_id == 0 { 1178 // Sprite pixel is transparent 1179 continue; 1180 } 1181 1182 line_buffer[x as usize] = SpriteData { color_id, is_sprite_0, attributes }; 1183 } 1184 } 1185} 1186 1187fn fetch_nametable_byte(registers: &InternalRegisters, bus: &mut PpuBus<'_>) -> u8 { 1188 bus.read_address(0x2000 | (registers.vram_address & 0x0FFF)) 1189} 1190 1191fn fetch_palette_index(registers: &InternalRegisters, bus: &mut PpuBus<'_>) -> u8 { 1192 let coarse_y = registers.coarse_y(); 1193 let coarse_x = registers.coarse_x(); 1194 let nametable_bits = registers.nametable_bits(); 1195 let attributes_byte = 1196 bus.read_address(0x23C0 | nametable_bits | ((coarse_y & 0x001C) << 1) | (coarse_x >> 2)); 1197 1198 match (coarse_x & 0x0002, coarse_y & 0x0002) { 1199 (0x0000, 0x0000) => attributes_byte & 0x03, 1200 (0x0002, 0x0000) => (attributes_byte >> 2) & 0x03, 1201 (0x0000, 0x0002) => (attributes_byte >> 4) & 0x03, 1202 (0x0002, 0x0002) => (attributes_byte >> 6) & 0x03, 1203 _ => unreachable!("masking with 0x0002 should always produce either 0 or 0x0002"), 1204 } 1205} 1206 1207#[derive(Debug, Clone, Copy, PartialEq, Eq)] 1208enum PatternTableByte { 1209 Low, 1210 High, 1211} 1212 1213fn fetch_bg_pattern_table_byte( 1214 bg_pattern_table_address: u16, 1215 nametable_byte: u8, 1216 fine_y_scroll: u16, 1217 byte: PatternTableByte, 1218 bus: &mut PpuBus<'_>, 1219) -> u8 { 1220 let offset = match byte { 1221 PatternTableByte::Low => 0x0000, 1222 PatternTableByte::High => 0x0008, 1223 }; 1224 1225 bus.read_address( 1226 bg_pattern_table_address | (u16::from(nametable_byte) << 4) | offset | fine_y_scroll, 1227 ) 1228} 1229 1230#[allow(clippy::too_many_arguments)] 1231fn fetch_sprite_pattern_table_byte( 1232 sprite_pattern_table_address: u16, 1233 double_height_sprites: bool, 1234 y_position: u8, 1235 attributes: u8, 1236 tile_index: u8, 1237 scanline: u8, 1238 byte: PatternTableByte, 1239 bus: &mut PpuBus<'_>, 1240) -> u8 { 1241 let offset = match byte { 1242 PatternTableByte::Low => 0x0000, 1243 PatternTableByte::High => 0x0008, 1244 }; 1245 1246 let flip_y = attributes.bit(7); 1247 let (sprite_pattern_table_address, tile_index, fine_y_scroll) = if double_height_sprites { 1248 let sprite_pattern_table_address = u16::from(tile_index & 0x01) << 12; 1249 let fine_y_scroll = if flip_y { 1250 15 - scanline.saturating_sub(y_position) 1251 } else { 1252 scanline.saturating_sub(y_position) 1253 }; 1254 let tile_index = (tile_index & 0xFE) | u8::from(fine_y_scroll >= 8); 1255 (sprite_pattern_table_address, tile_index, fine_y_scroll & 0x07) 1256 } else { 1257 let fine_y_scroll = if flip_y { 1258 7 - scanline.saturating_sub(y_position) 1259 } else { 1260 scanline.saturating_sub(y_position) 1261 }; 1262 (sprite_pattern_table_address, tile_index, fine_y_scroll) 1263 }; 1264 1265 bus.read_address( 1266 sprite_pattern_table_address 1267 | (u16::from(tile_index) << 4) 1268 | offset 1269 | u16::from(fine_y_scroll), 1270 ) 1271} 1272 1273fn get_bg_color_id(pattern_table_low: u16, pattern_table_high: u16, fine_x: u8) -> u8 { 1274 get_color_id((pattern_table_low >> 8) as u8, (pattern_table_high >> 8) as u8, fine_x) 1275} 1276 1277fn get_color_id(pattern_table_low: u8, pattern_table_high: u8, fine_x: u8) -> u8 { 1278 debug_assert!(fine_x < 8, "fine_x must be less than 8: {fine_x}"); 1279 1280 let shift = 7 - fine_x; 1281 let mask = 1 << shift; 1282 ((pattern_table_low & mask) >> shift) | (((pattern_table_high & mask) >> shift) << 1) 1283} 1284 1285#[cfg(test)] 1286mod tests { 1287 use super::*; 1288 1289 #[test] 1290 fn color_id() { 1291 assert_eq!(0, get_color_id(0, 0, 0)); 1292 1293 assert_eq!(1, get_color_id(0x80, 0, 0)); 1294 assert_eq!(2, get_color_id(0, 0x80, 0)); 1295 assert_eq!(3, get_color_id(0x80, 0x80, 0)); 1296 1297 assert_eq!(0, get_color_id(0x80, 0x80, 1)); 1298 1299 assert_eq!(3, get_color_id(0x10, 0x10, 3)); 1300 1301 assert_eq!(3, get_color_id(0x01, 0x01, 7)); 1302 } 1303}