ppu.rsannotatedppu.rssource1303 lines · 46.4 KB · raw
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}