PPU (pixel/picture processing unit) emulation code.
In NTSC, the PPU constantly cycles through 262 scanlines: 240 visible scanlines where the PPU is actively rendering pixels, a 21-scanline vertical blanking period where the PPU is idle, and a pre-render scanline where the PPU fetches data that is needed to render the first visible scanline.
PAL is (mostly) the same except the vertical blanking period lasts for 70 scanlines instead of 20, for a total of 312 scanlines.
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;
Set/reset flags on dot 2 instead of 1 to resolve some CPU/PPU alignment issues that affect NMI 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;
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 }
Return whether the PPU is currently in the vertical blanking period.
While the PPU's first idle scanline is scanline 240, this method will not return true until scanline 241 in order to align with when the PPU sets the VBlank flag in PPUSTATUS.
Retrieve a reference the PPU's frame buffer.
The frame buffer is a 256x240 grid storing 6-bit NES colors. These colors do not map directly to RGB; some sort of palette is needed to convert these colors to RGB colors that are appropriate for display.
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}
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}
Reset the PPU, as if the console's reset button was pressed.
This resets all PPU state except for the internal v register, and also clears most of the memory-mapped PPU registers.
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}