ppu.rsannotatedppu.rssource1303 lines · 46.4 KB · raw

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;
20pub const SCREEN_WIDTH: u16 = 256;
21pub const MAX_SCREEN_HEIGHT: u16 = 240;
22
23pub const DOTS_PER_SCANLINE: u16 = 341;

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.

355    pub fn in_vblank(&self) -> bool {
356        self.timing_mode.vblank_scanlines().contains(&self.scanline)
357    }

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.

364    pub fn frame_buffer(&self) -> &FrameBuffer {
365        &self.frame_buffer
366    }
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}
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}

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.

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}
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}