ppu.rsannotatedppu.rssource2038 lines · 70.5 KB · raw

GBA PPU (picture processing unit)

3mod debug;
4mod registers;
6use crate::dma::DmaState;
7use crate::interrupts::{InterruptRegisters, InterruptType};
8use crate::ppu::registers::{
9    AffineOverflowBehavior, BgMode, BitsPerPixel, BlendMode, ObjVramMapDimensions, Registers,
10    Window, WindowEnabled,
11};
12use crate::scheduler::{Scheduler, SchedulerEvent};
13use bincode::{Decode, Encode};
14use jgenesis_common::boxedarray::{BoxedByteArray, BoxedColorArray, BoxedWordArray};
15use jgenesis_common::frontend::{Color, FrameSize};
16use jgenesis_common::num::{GetBit, U16Ext};
17use std::ops::Range;
18use std::{array, cmp, iter, mem};
19
20const VRAM_LOW_LEN: usize = 64 * 1024;
21const VRAM_HIGH_LEN: usize = 32 * 1024;
22const VRAM_LEN: usize = VRAM_LOW_LEN + VRAM_HIGH_LEN;
23const VRAM_ADDR_MASK: usize = (128 * 1024) - 1;
24
25const PALETTE_RAM_LEN_HALFWORDS: usize = 1024 / 2;
26
27const OAM_LEN_HALFWORDS: usize = 1024 / 2;
28
29pub const SCREEN_HEIGHT: u32 = 160;
30pub const SCREEN_WIDTH: u32 = 240;
31pub const FRAME_BUFFER_LEN: usize = (SCREEN_HEIGHT as usize) * (SCREEN_WIDTH as usize);
32pub const FRAME_SIZE: FrameSize = FrameSize { width: SCREEN_WIDTH, height: SCREEN_HEIGHT };
33
34pub const LINES_PER_FRAME: u32 = 228;
35pub const DOTS_PER_LINE: u32 = 1232;

VBlank flag is not set on the last line of the frame because of sprite processing for line 0

38const VBLANK_LINES: Range<u32> = 160..227;
39const VBLANK_IRQ_DOT: u32 = 1;
40const V_COUNTER_IRQ_DOT: u32 = 2;
41const HBLANK_START_DOT: u32 = 1006;
42const HBLANK_IRQ_DOT: u32 = 1008;
44#[derive(Debug, Clone, Encode, Decode)]
45struct GbaFrameBuffer(BoxedWordArray<FRAME_BUFFER_LEN>);
46
47impl GbaFrameBuffer {
48    fn new() -> Self {
49        Self(BoxedWordArray::new())
50    }
51
52    fn set(&mut self, line: u32, pixel: u32, color: u16) {
53        let frame_buffer_addr = (line * SCREEN_WIDTH + pixel) as usize;
54        self.0[frame_buffer_addr] = color;
55    }
56}
57
58#[derive(Debug, Clone, Encode, Decode)]
59struct RgbaFrameBuffer(BoxedColorArray<FRAME_BUFFER_LEN>);
60
61impl RgbaFrameBuffer {
62    fn new() -> Self {
63        Self(BoxedColorArray::new())
64    }
65
66    fn copy_from(&mut self, frame_buffer: &GbaFrameBuffer) {
67        let mut address = 0;
68
69        for _ in 0..SCREEN_HEIGHT {
70            for _ in 0..SCREEN_WIDTH {
71                let gba_color = frame_buffer.0[address];
72                self.0[address] = gba_color_to_rgb8(gba_color);
73                address += 1;
74            }
75        }
76    }
77}
78
79#[derive(Debug, Clone, Copy, Default, Encode, Decode)]
80struct BgAffineLatch {
81    x: [i32; 2],
82    y: [i32; 2],
83    x_written: [bool; 2],
84    y_written: [bool; 2],
85}
86
87impl BgAffineLatch {
88    // Called once per line during active display
89    fn update_reference_points(&mut self, registers: &Registers, bg_enabled_latency: [u8; 4]) {
90        for (i, (x, y)) in iter::zip(&mut self.x, &mut self.y).enumerate() {
91            // Only increment latched X/Y if they haven't been written within the last scanline
92            // e.g. Iridion 3D (game over screen), Star Wars Episode II (text scroll)
93            //
94            // Also, only increment latches when corresponding BG is enabled (e.g. Pinball Tycoon)
95            let bg_enabled = registers.bg_enabled[i + 2] && bg_enabled_latency[i + 2] == 0;
96
97            if mem::take(&mut self.x_written[i]) {
98                *x = registers.bg_affine_parameters[i].reference_x;
99            } else if bg_enabled {
100                *x += registers.bg_affine_parameters[i].b;
101            }
102
103            if mem::take(&mut self.y_written[i]) {
104                *y = registers.bg_affine_parameters[i].reference_y;
105            } else if bg_enabled {
106                *y += registers.bg_affine_parameters[i].d;
107            }
108        }
109    }
110}
111
112#[derive(Debug, Clone, Default, Encode, Decode)]
113struct MosaicState {
114    bg_v_counter: u8,
115    bg_text_line: u32,
116    bg_affine: BgAffineLatch,
117    obj_v_counter: u8,
118    obj_line: u32,
119}
120
121#[derive(Debug, Clone, Encode, Decode)]
122struct State {
123    scanline: u32,
124    dot: u32,
125    frame_complete: bool,
126    bg_affine_latch: BgAffineLatch,
127    mosaic: MosaicState,
128    bg_enabled_latency: [u8; 4],
129    obj_enabled_latency: u8,
130    forced_blanking_latency: u8,
131    window_y_active: [bool; 2],
132    video_capture_latch: bool,
133}
134
135impl State {
136    fn new(skip_bios_animation: bool) -> Self {
137        let (scanline, dot) = if skip_bios_animation {
138            // These are probably not accurate, but currently match where the BIOS hands over control
139            // in this emulator
140            (126, 827)
141        } else {
142            (0, 0)
143        };
144
145        Self {
146            scanline,
147            dot,
148            frame_complete: false,
149            bg_affine_latch: BgAffineLatch::default(),
150            mosaic: MosaicState::default(),
151            bg_enabled_latency: [0; 4],
152            obj_enabled_latency: 0,
153            forced_blanking_latency: 0,
154            window_y_active: [false; 2],
155            video_capture_latch: false,
156        }
157    }
158
159    // Should be called at the start of each line
160    fn update_mosaic_v_state(&mut self, registers: &Registers) {
161        // BG V mosaic
162        if self.scanline == 0 || self.mosaic.bg_v_counter == registers.bg_mosaic_v_size {
163            self.mosaic.bg_v_counter = 0;
164            self.mosaic.bg_text_line = self.scanline;
165            self.mosaic.bg_affine = self.bg_affine_latch;
166        } else {
167            self.mosaic.bg_v_counter = (self.mosaic.bg_v_counter + 1) & 0xF;
168        }
169
170        // OBJ V mosaic
171        if self.scanline == LINES_PER_FRAME - 1 {
172            self.mosaic.obj_v_counter = 0;
173            self.mosaic.obj_line = 0;
174        } else if self.mosaic.obj_v_counter == registers.obj_mosaic_v_size {
175            self.mosaic.obj_v_counter = 0;
176            self.mosaic.obj_line = self.scanline + 1;
177        } else {
178            self.mosaic.obj_v_counter = (self.mosaic.obj_v_counter + 1) & 0xF;
179        }
180    }
181}
182
183#[derive(Debug, Clone, Copy, Default, Encode, Decode)]
184struct Pixel(u16);
185
186impl Pixel {
187    const TRANSPARENT: Self = Self(0);
188
189    fn transparent(self) -> bool {
190        !self.0.bit(15)
191    }
192
193    fn red(self) -> u16 {
194        self.0 & 0x1F
195    }
196
197    fn green(self) -> u16 {
198        (self.0 >> 5) & 0x1F
199    }
200
201    fn blue(self) -> u16 {
202        (self.0 >> 10) & 0x1F
203    }
204
205    fn new_opaque(color: u16) -> Self {
206        Self(color | 0x8000)
207    }
208
209    fn new_opaque_rgb(r: u16, g: u16, b: u16) -> Self {
210        Self(0x8000 | r | (g << 5) | (b << 10))
211    }
212
213    fn new_transparent(color: u16) -> Self {
214        Self(color & 0x7FFF)
215    }
216}
217
218#[derive(Debug, Clone, Copy, Default, Encode, Decode)]
219struct ObjPixel {
220    color: Pixel,
221    priority: u8,
222    mosaic: bool,
223    semi_transparent: bool,
224}
225
226#[derive(Debug, Clone, Encode, Decode)]
227struct Buffers {
228    bg_pixels: [[Pixel; SCREEN_WIDTH as usize]; 4],
229    obj_pixels: [ObjPixel; SCREEN_WIDTH as usize],
230    obj_window: [bool; SCREEN_WIDTH as usize],
231}
232
233impl Buffers {
234    fn new() -> Self {
235        Self {
236            bg_pixels: array::from_fn(|_| array::from_fn(|_| Pixel::default())),
237            obj_pixels: array::from_fn(|_| ObjPixel::default()),
238            obj_window: array::from_fn(|_| false),
239        }
240    }
241}
242
243#[derive(Debug, Clone, Copy, PartialEq, Eq)]
244enum Layer {
245    Bg0,
246    Bg1,
247    Bg2,
248    Bg3,
249    Obj,
250    Backdrop,
251    None,
252}
253
254impl Layer {
255    const BG: [Self; 4] = [Self::Bg0, Self::Bg1, Self::Bg2, Self::Bg3];
256
257    fn is_1st_target_enabled(self, registers: &Registers) -> bool {
258        match self {
259            Self::Bg0 => registers.bg_blend_1st_target[0],
260            Self::Bg1 => registers.bg_blend_1st_target[1],
261            Self::Bg2 => registers.bg_blend_1st_target[2],
262            Self::Bg3 => registers.bg_blend_1st_target[3],
263            Self::Obj => registers.obj_blend_1st_target,
264            Self::Backdrop => registers.backdrop_blend_1st_target,
265            Self::None => false,
266        }
267    }
268
269    fn is_2nd_target_enabled(self, registers: &Registers) -> bool {
270        match self {
271            Self::Bg0 => registers.bg_blend_2nd_target[0],
272            Self::Bg1 => registers.bg_blend_2nd_target[1],
273            Self::Bg2 => registers.bg_blend_2nd_target[2],
274            Self::Bg3 => registers.bg_blend_2nd_target[3],
275            Self::Obj => registers.obj_blend_2nd_target,
276            Self::Backdrop => registers.backdrop_blend_2nd_target,
277            Self::None => false,
278        }
279    }
280}
281
282#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode)]
283enum SpriteMode {
284    #[default]
285    Normal,
286    SemiTransparent,
287    ObjWindow,
288    Invalid,
289}
290
291impl SpriteMode {
292    fn from_bits(bits: u16) -> Self {
293        match bits & 3 {
294            0 => Self::Normal,
295            1 => Self::SemiTransparent,
296            2 => Self::ObjWindow,
297            3 => Self::Invalid,
298            _ => unreachable!("value & 3 is always <= 3"),
299        }
300    }
301}
302
303#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode)]
304enum SpriteSize {
305    #[default]
306    Zero,
307    One,
308    Two,
309    Three,
310}
311
312impl SpriteSize {
313    fn from_bits(bits: u16) -> Self {
314        match bits & 3 {
315            0 => Self::Zero,
316            1 => Self::One,
317            2 => Self::Two,
318            3 => Self::Three,
319            _ => unreachable!("value & 3 is always <= 3"),
320        }
321    }
322}
323
324#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode)]
325enum SpriteShape {
326    #[default]
327    Square,
328    HorizontalRect,
329    VerticalRect,
330    Invalid,
331}
332
333impl SpriteShape {
334    fn from_bits(bits: u16) -> Self {
335        match bits & 3 {
336            0 => Self::Square,
337            1 => Self::HorizontalRect,
338            2 => Self::VerticalRect,
339            3 => Self::Invalid,
340            _ => unreachable!("value & 3 is always <= 3"),
341        }
342    }
343
344    #[allow(clippy::match_same_arms)]
345    fn size_pixels(self, size: SpriteSize) -> (u32, u32) {
346        use SpriteShape::{HorizontalRect, Invalid, Square, VerticalRect};
347        use SpriteSize::{One, Three, Two, Zero};
348
349        match (self, size) {
350            (Square, Zero) => (8, 8),
351            (Square, One) => (16, 16),
352            (Square, Two) => (32, 32),
353            (Square, Three) => (64, 64),
354            (HorizontalRect, Zero) => (16, 8),
355            (HorizontalRect, One) => (32, 8),
356            (HorizontalRect, Two) => (32, 16),
357            (HorizontalRect, Three) => (64, 32),
358            (VerticalRect, Zero) => (8, 16),
359            (VerticalRect, One) => (8, 32),
360            (VerticalRect, Two) => (16, 32),
361            (VerticalRect, Three) => (32, 64),
362            (Invalid, _) => {
363                // TODO ???
364                (8, 8)
365            }
366        }
367    }
368}
369
370#[derive(Debug, Clone, Default, Encode, Decode)]
371struct OamEntry {
372    x: u32,
373    y: u32,
374    tile_number: u32,
375    affine: bool,
376    affine_double_size: bool,
377    affine_parameter_group: u16,
378    disabled: bool,
379    mode: SpriteMode,
380    mosaic: bool,
381    bpp: BitsPerPixel,
382    shape: SpriteShape,
383    size: SpriteSize,
384    h_flip: bool,
385    v_flip: bool,
386    priority: u8,
387    palette: u16,
388}
389
390impl OamEntry {
391    fn parse(attributes: [u16; 3]) -> Self {
392        // First halfword
393        // Bit 9 means double size for affine sprites and disabled for non-affine
394        let y: u32 = (attributes[0] & 0xFF).into();
395        let affine = attributes[0].bit(8);
396        let affine_double_size = affine && attributes[0].bit(9);
397        let disabled = !affine && attributes[0].bit(9);
398        let mode = SpriteMode::from_bits(attributes[0] >> 10);
399        let mosaic = attributes[0].bit(12);
400        let bpp = BitsPerPixel::from_bit(attributes[0].bit(13));
401        let shape = SpriteShape::from_bits(attributes[0] >> 14);
402
403        // Second halfword
404        // Bits 9-13 are parameter group for affine sprites and H/V flip for non-affine
405        let x: u32 = (attributes[1] & 0x1FF).into();
406        let affine_parameter_group = (attributes[1] >> 9) & 0x1F;
407        let h_flip = !affine && attributes[1].bit(12);
408        let v_flip = !affine && attributes[1].bit(13);
409        let size = SpriteSize::from_bits(attributes[1] >> 14);
410
411        // Third halfword
412        let tile_number: u32 = (attributes[2] & 0x3FF).into();
413        let priority = ((attributes[2] >> 10) & 3) as u8;
414        let palette = attributes[2] >> 12;
415
416        Self {
417            x,
418            y,
419            tile_number,
420            affine,
421            affine_double_size,
422            affine_parameter_group,
423            disabled,
424            mode,
425            mosaic,
426            bpp,
427            shape,
428            size,
429            h_flip,
430            v_flip,
431            priority,
432            palette,
433        }
434    }
435}
436
437#[derive(Debug, Clone, Copy)]
438struct MergePixel {
439    color: Pixel,
440    layer: Layer,
441    priority: u8,
442    semi_transparent: bool,
443}
444
445impl MergePixel {
446    fn bg(bg: usize, color: Pixel, priority: u8) -> Self {
447        Self { color, layer: Layer::BG[bg], priority, semi_transparent: false }
448    }
449
450    fn obj(pixel: ObjPixel) -> Self {
451        Self {
452            color: pixel.color,
453            layer: Layer::Obj,
454            priority: pixel.priority,
455            semi_transparent: pixel.semi_transparent,
456        }
457    }
458
459    fn backdrop(color: Pixel) -> Self {
460        Self { color, layer: Layer::Backdrop, priority: u8::MAX, semi_transparent: false }
461    }
462
463    fn none() -> Self {
464        Self {
465            color: Pixel::TRANSPARENT,
466            layer: Layer::None,
467            priority: u8::MAX,
468            semi_transparent: false,
469        }
470    }
471}
472
473#[derive(Debug, Clone, Encode, Decode)]
474pub struct Ppu {
475    frame_buffer: GbaFrameBuffer,
476    ready_frame_buffer: RgbaFrameBuffer,
477    vram: BoxedByteArray<VRAM_LEN>,
478    palette_ram: BoxedWordArray<PALETTE_RAM_LEN_HALFWORDS>,
479    oam: BoxedWordArray<OAM_LEN_HALFWORDS>,
480    oam_parsed: Box<[OamEntry; 128]>,
481    registers: Registers,
482    state: State,
483    buffers: Box<Buffers>,
484    cycles: u64,
485    next_event_cycles: u64,
486}
487
488impl Ppu {
489    pub fn new(skip_bios_animation: bool) -> Self {
490        Self {
491            frame_buffer: GbaFrameBuffer::new(),
492            ready_frame_buffer: RgbaFrameBuffer::new(),
493            vram: BoxedByteArray::new(),
494            palette_ram: BoxedWordArray::new(),
495            oam: BoxedWordArray::new(),
496            oam_parsed: Box::new(array::from_fn(|_| OamEntry::default())),
497            registers: Registers::new(),
498            state: State::new(skip_bios_animation),
499            buffers: Box::new(Buffers::new()),
500            cycles: 0,
501            next_event_cycles: 0,
502        }
503    }
504
505    pub fn step_to(&mut self, cycles: u64, dma: &mut DmaState, scheduler: &mut Scheduler) {
506        if cycles <= self.cycles {
507            return;
508        }
509
510        if cycles < self.next_event_cycles {
511            self.state.dot += (cycles - self.cycles) as u32;
512            self.cycles = cycles;
513            return;
514        }
515
516        self.tick(cycles, dma);
517        scheduler.insert_or_update(SchedulerEvent::PpuEvent, self.next_event_cycles);
518    }
519
520    fn tick(&mut self, cycles: u64, dma: &mut DmaState) {
521        type EventFn = fn(&mut Ppu, &mut DmaState, u64);
522
523        fn render_line(ppu: &mut Ppu, _: &mut DmaState, _: u64) {
524            ppu.render_current_line();
525            ppu.render_next_sprite_line();
526        }
527
528        fn hblank_start(ppu: &mut Ppu, dma: &mut DmaState, cycles: u64) {
529            if ppu.state.scanline < SCREEN_HEIGHT {
530                dma.notify_hblank_start(cycles);
531            }
532        }
533
534        fn end_of_line(ppu: &mut Ppu, dma: &mut DmaState, cycles: u64) {
535            ppu.state.dot = 0;
536
537            ppu.state.scanline += 1;
538            match ppu.state.scanline {
539                LINES_PER_FRAME => {
540                    ppu.state.scanline = 0;
541
542                    // Force reference point re-latch
543                    ppu.state.bg_affine_latch.x_written = [true; 2];
544                    ppu.state.bg_affine_latch.y_written = [true; 2];
545                }
546                SCREEN_HEIGHT => {
547                    dma.notify_vblank_start(cycles);
548
549                    ppu.state.frame_complete = true;
550                    ppu.ready_frame_buffer.copy_from(&ppu.frame_buffer);
551                }
552                162 => {
553                    if ppu.state.video_capture_latch {
554                        dma.end_video_capture();
555                    }
556                    ppu.state.video_capture_latch = dma.video_capture_active();
557                }
558                _ => {}
559            }
560
561            if ppu.state.scanline < SCREEN_HEIGHT {
562                ppu.state
563                    .bg_affine_latch
564                    .update_reference_points(&ppu.registers, ppu.state.bg_enabled_latency);
565            }
566
567            if ppu.state.video_capture_latch && (2..162).contains(&ppu.state.scanline) {
568                // Video capture DMA triggers at dot 3 (then begins at dot 5 due to 2-cycle DMA latency)
569                dma.notify_video_capture(cycles + 3);
570            }
571
572            ppu.state.update_mosaic_v_state(&ppu.registers);
573
574            for latency in &mut ppu.state.bg_enabled_latency {
575                *latency = latency.saturating_sub(1);
576            }
577            ppu.state.obj_enabled_latency = ppu.state.obj_enabled_latency.saturating_sub(1);
578            ppu.state.forced_blanking_latency = ppu.state.forced_blanking_latency.saturating_sub(1);
579
580            for (i, window_y_active) in ppu.state.window_y_active.iter_mut().enumerate() {
581                *window_y_active |= ppu.state.scanline == ppu.registers.window_y1[i];
582                *window_y_active &= ppu.state.scanline != ppu.registers.window_y2[i];
583            }
584        }
585
586        // Arbitrary dot around the middle of the line
587        const RENDER_DOT: u32 = 526;
588
589        const LINE_EVENTS: &[(u32, EventFn)] = &[
590            (RENDER_DOT, render_line),
591            (HBLANK_START_DOT + 1, hblank_start),
592            (DOTS_PER_LINE, end_of_line),
593        ];
594
595        if cycles <= self.cycles {
596            return;
597        }
598
599        let mut elapsed_cycles = (cycles - self.cycles) as u32;
600        let mut event_idx = 0;
601        while elapsed_cycles != 0 {
602            while self.state.dot >= LINE_EVENTS[event_idx].0 {
603                event_idx += 1;
604            }
605
606            let change = cmp::min(elapsed_cycles, LINE_EVENTS[event_idx].0 - self.state.dot);
607            self.state.dot += change;
608            elapsed_cycles -= change;
609            self.cycles += u64::from(change);
610
611            if self.state.dot == LINE_EVENTS[event_idx].0 {
612                (LINE_EVENTS[event_idx].1)(self, dma, self.cycles);
613
614                event_idx += 1;
615                if event_idx == LINE_EVENTS.len() {
616                    event_idx = 0;
617                }
618            }
619        }
620
621        self.next_event_cycles = self.cycles + u64::from(LINE_EVENTS[event_idx].0 - self.state.dot);
622    }
623
624    fn render_current_line(&mut self) {
625        if self.state.scanline >= SCREEN_HEIGHT {
626            return;
627        }
628
629        if self.registers.forced_blanking || self.state.forced_blanking_latency != 0 {
630            self.clear_current_line();
631            return;
632        }
633
634        self.render_bg_layers();
635
636        self.merge_layers();
637    }
638
639    fn clear_current_line(&mut self) {
640        const WHITE: u16 = 0x7FFF;
641
642        for pixel in 0..SCREEN_WIDTH {
643            self.frame_buffer.set(self.state.scanline, pixel, WHITE);
644        }
645    }
646
647    #[allow(clippy::match_same_arms)]
648    fn render_bg_layers(&mut self) {
649        match self.registers.bg_mode {
650            BgMode::Zero => {
651                // BG0-3 in text mode
652                for bg in 0..4 {
653                    self.render_text_bg(bg);
654                }
655            }
656            BgMode::One => {
657                // BG0-1 in text mode, BG2 in affine mode
658                for bg in 0..2 {
659                    self.render_text_bg(bg);
660                }
661                self.render_affine_bg(2, self.affine_sample_tile_map(2));
662            }
663            BgMode::Two => {
664                // BG2-3 in affine mode
665                for bg in 2..4 {
666                    self.render_affine_bg(bg, self.affine_sample_tile_map(bg));
667                }
668            }
669            BgMode::Three => {
670                // Bitmap mode: 240x160, 15bpp, single frame buffer
671                self.render_affine_bg(2, Self::affine_sample_mode_3);
672            }
673            BgMode::Four => {
674                // Bitmap mode: 240x160, 8bpp, two frame buffers
675                self.render_affine_bg(2, self.affine_sample_mode_4());
676            }
677            BgMode::Five => {
678                // Bitmap mode: 160x128, 15bpp, two frame buffers
679                self.render_affine_bg(2, self.affine_sample_mode_5());
680            }
681            BgMode::Invalid(_) => {}
682        }
683    }
684
685    fn render_text_bg(&mut self, bg: usize) {
686        self.buffers.bg_pixels[bg].fill(Pixel::TRANSPARENT);
687
688        if !self.registers.bg_enabled[bg] {
689            return;
690        }
691
692        let bg_control = &self.registers.bg_control[bg];
693
694        let width_tiles = bg_control.size.text_width_tiles();
695        let width_screens = width_tiles / 32;
696        let height_tiles = bg_control.size.text_height_tiles();
697
698        let h_scroll = self.registers.bg_h_scroll[bg];
699        let fine_h_scroll = h_scroll % 8;
700        let coarse_h_scroll = h_scroll / 8;
701
702        let scanline =
703            if bg_control.mosaic { self.state.mosaic.bg_text_line } else { self.state.scanline };
704        let v_scroll = self.registers.bg_v_scroll[bg];
705        let scrolled_line = scanline + v_scroll;
706
707        let (tile_map_row, screen_map_row) = {
708            let tile_map_row = (scrolled_line / 8) & (height_tiles - 1);
709            let screen_map_row = tile_map_row / 32;
710            (tile_map_row % 32, screen_map_row)
711        };
712        let tile_row = scrolled_line % 8;
713
714        let tile_size_bytes = bg_control.bpp.tile_size_bytes();
715
716        let end_tile = if fine_h_scroll != 0 { SCREEN_WIDTH / 8 + 1 } else { SCREEN_WIDTH / 8 };
717
718        for tile_idx in 0..end_tile {
719            let base_pixel = (8 * tile_idx) as i32 - fine_h_scroll as i32;
720
721            let (tile_map_col, screen_map_col) = {
722                let tile_map_col = (tile_idx + coarse_h_scroll) & (width_tiles - 1);
723                let screen_map_col = tile_map_col / 32;
724                (tile_map_col % 32, screen_map_col)
725            };
726
727            let screen_idx = screen_map_row * width_screens + screen_map_col;
728            let screen_addr = bg_control.tile_map_addr + screen_idx * 2 * 32 * 32;
729
730            let tile_map_addr = screen_addr + 2 * (tile_map_row * 32 + tile_map_col);
731            let tile_map_entry = if tile_map_addr <= 0xFFFF {
732                u16::from_le_bytes([
733                    self.vram[tile_map_addr as usize],
734                    self.vram[(tile_map_addr + 1) as usize],
735                ])
736            } else {
737                // TODO should read VRAM open bus?
738                0
739            };
740
741            let tile_number: u32 = (tile_map_entry & 0x3FF).into();
742            let h_flip = tile_map_entry.bit(10);
743            let v_flip = tile_map_entry.bit(11);
744            let palette = match bg_control.bpp {
745                BitsPerPixel::Four => tile_map_entry >> 12,
746                BitsPerPixel::Eight => 0,
747            };
748
749            let tile_base_addr = bg_control.tile_data_addr + tile_number * tile_size_bytes;
750            let tile_row = if v_flip { 7 - tile_row } else { tile_row };
751
752            match bg_control.bpp {
753                BitsPerPixel::Four => {
754                    let tile_row_addr = tile_base_addr + tile_row * 4;
755
756                    for pixel_idx in 0..8 {
757                        let pixel = pixel_idx as i32 + base_pixel;
758                        if !(0..SCREEN_WIDTH as i32).contains(&pixel) {
759                            continue;
760                        }
761                        let pixel = pixel as usize;
762
763                        let tile_col = if h_flip { 7 - pixel_idx } else { pixel_idx };
764                        let tile_addr = tile_row_addr + (tile_col >> 1);
765
766                        let tile_byte = if tile_addr <= 0xFFFF {
767                            self.vram[tile_addr as usize]
768                        } else {
769                            // TODO should read VRAM open bus?
770                            0
771                        };
772
773                        let color_id = (tile_byte >> (4 * (tile_col & 1))) & 0xF;
774                        if color_id == 0 {
775                            // Transparent pixel
776                            continue;
777                        }
778
779                        let palette_ram_addr = 16 * palette + u16::from(color_id);
780                        let color = self.palette_ram[palette_ram_addr as usize];
781                        self.buffers.bg_pixels[bg][pixel] = Pixel::new_opaque(color);
782                    }
783                }
784                BitsPerPixel::Eight => {
785                    let tile_row_addr = tile_base_addr + tile_row * 8;
786
787                    for pixel_idx in 0..8 {
788                        let pixel = pixel_idx as i32 + base_pixel;
789                        if !(0..SCREEN_WIDTH as i32).contains(&pixel) {
790                            continue;
791                        }
792                        let pixel = pixel as usize;
793
794                        let tile_col = if h_flip { 7 - pixel_idx } else { pixel_idx };
795                        let tile_addr = tile_row_addr + tile_col;
796
797                        let color_id = if tile_addr <= 0xFFFF {
798                            self.vram[tile_addr as usize]
799                        } else {
800                            // TODO should read VRAM open bus?
801                            0
802                        };
803
804                        if color_id == 0 {
805                            // Transparent pixel
806                            continue;
807                        }
808
809                        let color = self.palette_ram[color_id as usize];
810                        self.buffers.bg_pixels[bg][pixel] = Pixel::new_opaque(color);
811                    }
812                }
813            }
814        }
815
816        self.apply_bg_h_mosaic(bg);
817    }
818
819    fn render_affine_bg(&mut self, bg: usize, sample_fn: impl Fn(&Self, i32, i32) -> Pixel) {
820        assert!(bg == 2 || bg == 3);
821
822        self.buffers.bg_pixels[bg].fill(Pixel::TRANSPARENT);
823
824        if !self.registers.bg_enabled[bg] {
825            return;
826        }
827
828        let bg_control = &self.registers.bg_control[bg];
829
830        let dx = self.registers.bg_affine_parameters[bg - 2].a;
831        let dy = self.registers.bg_affine_parameters[bg - 2].c;
832
833        let bg_affine_latch = if bg_control.mosaic {
834            self.state.mosaic.bg_affine
835        } else {
836            self.state.bg_affine_latch
837        };
838        let mut x = bg_affine_latch.x[bg - 2];
839        let mut y = bg_affine_latch.y[bg - 2];
840
841        for pixel in 0..SCREEN_WIDTH {
842            // Affine coordinates are in 1/256 pixel units - convert to pixel
843            let x_pixel = x >> 8;
844            let y_pixel = y >> 8;
845
846            self.buffers.bg_pixels[bg][pixel as usize] = sample_fn(self, x_pixel, y_pixel);
847
848            x += dx;
849            y += dy;
850        }
851
852        self.apply_bg_h_mosaic(bg);
853    }
854
855    fn affine_sample_tile_map(&self, bg: usize) -> impl Fn(&Self, i32, i32) -> Pixel + 'static {
856        let bg_control = &self.registers.bg_control[bg];
857
858        let dimension_tiles = bg_control.size.affine_dimension_tiles();
859        let dimension_pixels = (8 * dimension_tiles) as i32;
860
861        let base_tile_map_addr = bg_control.tile_map_addr;
862        let base_tile_data_addr = bg_control.tile_data_addr;
863        let affine_overflow = bg_control.affine_overflow;
864
865        move |ppu, mut x, mut y| {
866            if !(0..dimension_pixels).contains(&x) || !(0..dimension_pixels).contains(&y) {
867                match affine_overflow {
868                    AffineOverflowBehavior::Transparent => return Pixel::TRANSPARENT,
869                    AffineOverflowBehavior::Wrap => {
870                        x &= dimension_pixels - 1;
871                        y &= dimension_pixels - 1;
872                    }
873                }
874            }
875
876            let x = x as u32;
877            let y = y as u32;
878
879            let tile_map_row = y / 8;
880            let tile_row = y % 8;
881
882            let tile_map_col = x / 8;
883            let tile_col = x % 8;
884
885            let tile_map_addr = base_tile_map_addr + tile_map_row * dimension_tiles + tile_map_col;
886            let tile_number = if tile_map_addr <= 0xFFFF {
887                ppu.vram[tile_map_addr as usize]
888            } else {
889                // TODO should be VRAM open bus?
890                0
891            };
892            let tile_number: u32 = tile_number.into();
893
894            // Affine tiles are always 8bpp
895            let tile_base_addr = base_tile_data_addr + 64 * tile_number;
896
897            // Tile data address will never exceed $FFFF because tile numbers are 8-bit and tile
898            // data base address is in 16KB steps
899            assert!(tile_base_addr <= 0x10000 - 64);
900
901            let tile_row_addr = tile_base_addr + 8 * tile_row;
902            let tile_addr = tile_row_addr + tile_col;
903            let color_id = ppu.vram[tile_addr as usize];
904
905            if color_id == 0 {
906                return Pixel::TRANSPARENT;
907            }
908
909            let color = ppu.palette_ram[color_id as usize];
910            Pixel::new_opaque(color)
911        }
912    }
913
914    fn affine_sample_mode_3(&self, x: i32, y: i32) -> Pixel {
915        if !(0..SCREEN_WIDTH as i32).contains(&x) || !(0..SCREEN_HEIGHT as i32).contains(&y) {
916            return Pixel::TRANSPARENT;
917        }
918
919        let x = x as u32;
920        let y = y as u32;
921
922        let pixel_addr = (2 * (y * SCREEN_WIDTH + x)) as usize;
923        let color = u16::from_le_bytes([self.vram[pixel_addr], self.vram[pixel_addr + 1]]);
924        Pixel::new_opaque(color)
925    }
926
927    fn affine_sample_mode_4(&self) -> impl Fn(&Self, i32, i32) -> Pixel + 'static {
928        let fb_addr = self.registers.bitmap_frame_buffer.vram_address();
929
930        move |ppu, x, y| {
931            if !(0..SCREEN_WIDTH as i32).contains(&x) || !(0..SCREEN_HEIGHT as i32).contains(&y) {
932                return Pixel::TRANSPARENT;
933            }
934
935            let x = x as u32;
936            let y = y as u32;
937
938            let pixel_addr = (fb_addr + y * SCREEN_WIDTH + x) as usize;
939            let color_id = ppu.vram[pixel_addr];
940
941            if color_id == 0 {
942                return Pixel::TRANSPARENT;
943            }
944
945            let color = ppu.palette_ram[color_id as usize];
946            Pixel::new_opaque(color)
947        }
948    }
949
950    fn affine_sample_mode_5(&self) -> impl Fn(&Self, i32, i32) -> Pixel + 'static {
951        const MODE_5_WIDTH: u32 = 160;
952        const MODE_5_HEIGHT: u32 = 128;
953
954        let fb_addr = self.registers.bitmap_frame_buffer.vram_address();
955
956        move |ppu, x, y| {
957            if !(0..MODE_5_WIDTH as i32).contains(&x) || !(0..MODE_5_HEIGHT as i32).contains(&y) {
958                return Pixel::TRANSPARENT;
959            }
960
961            let x = x as u32;
962            let y = y as u32;
963
964            let pixel_addr = (fb_addr + 2 * (y * MODE_5_WIDTH + x)) as usize;
965            let color = u16::from_le_bytes([ppu.vram[pixel_addr], ppu.vram[pixel_addr + 1]]);
966            Pixel::new_opaque(color)
967        }
968    }
969
970    fn apply_bg_h_mosaic(&mut self, bg: usize) {
971        if !self.registers.bg_control[bg].mosaic {
972            return;
973        }
974
975        let mut h_counter = 0;
976        let mut color_latch = self.buffers.bg_pixels[bg][0];
977        for pixel in 1..SCREEN_WIDTH {
978            if h_counter == self.registers.bg_mosaic_h_size {
979                h_counter = 0;
980                color_latch = self.buffers.bg_pixels[bg][pixel as usize];
981            } else {
982                h_counter += 1;
983                self.buffers.bg_pixels[bg][pixel as usize] = color_latch;
984            }
985        }
986    }
987
988    fn merge_layers(&mut self) {
989        let backdrop_color = Pixel::new_transparent(self.palette_ram[0]);
990
991        // Alpha blending coefficients
992        let eva: u16 = cmp::min(16, self.registers.blend_alpha_a).into();
993        let evb: u16 = cmp::min(16, self.registers.blend_alpha_b).into();
994
995        // Brightness increase/decrease coefficient
996        let evy: u16 = cmp::min(16, self.registers.blend_brightness).into();
997
998        let bg_enabled: [bool; 4] = array::from_fn(|bg| {
999            self.registers.bg_enabled[bg]
1000                && self.registers.bg_mode.bg_active_in_mode(bg)
1001                && self.state.bg_enabled_latency[bg] == 0
1002        });
1003
1004        let obj_window_enabled = self.registers.obj_window_enabled;
1005        let any_window_enabled = self.registers.window_enabled[0]
1006            || self.registers.window_enabled[1]
1007            || obj_window_enabled;
1008
1009        let mut window_x_active: [bool; 2] =
1010            array::from_fn(|i| self.registers.window_x1[i] > self.registers.window_x2[i]);
1011
1012        let window_y_active: [bool; 2] =
1013            array::from_fn(|i| self.registers.window_enabled[i] && self.state.window_y_active[i]);
1014
1015        let obj_enabled = self.registers.obj_enabled && self.state.obj_enabled_latency == 0;
1016
1017        let mut obj_mosaic_h_counter = self.registers.obj_mosaic_h_size;
1018        let mut obj_mosaic_latch = ObjPixel::default();
1019
1020        for pixel in 0..SCREEN_WIDTH {
1021            for (i, window_x_active) in window_x_active.iter_mut().enumerate() {
1022                *window_x_active |= pixel == self.registers.window_x1[i];
1023                *window_x_active &= pixel != self.registers.window_x2[i];
1024            }
1025
1026            let window_layers_enabled = if any_window_enabled {
1027                let window = if window_y_active[0] && window_x_active[0] {
1028                    Window::Inside0
1029                } else if window_y_active[1] && window_x_active[1] {
1030                    Window::Inside1
1031                } else if obj_window_enabled && self.buffers.obj_window[pixel as usize] {
1032                    Window::InsideObj
1033                } else {
1034                    Window::Outside
1035                };
1036                self.registers.window_layers_enabled(window)
1037            } else {
1038                WindowEnabled::ALL
1039            };
1040
1041            let mut first_pixel = MergePixel::backdrop(backdrop_color);
1042            let mut second_pixel = MergePixel::none();
1043
1044            // When priority value is equal, layer priority is OBJ > BG0 > BG1 > BG2 > BG3
1045            // Process layers in that order
1046
1047            if obj_enabled {
1048                let obj_pixel = self.buffers.obj_pixels[pixel as usize];
1049
1050                if obj_mosaic_h_counter == self.registers.obj_mosaic_h_size {
1051                    obj_mosaic_h_counter = 0;
1052                    obj_mosaic_latch = obj_pixel;
1053                } else {
1054                    obj_mosaic_h_counter += 1;
1055
1056                    // Update the mosaic latch if either the current or latched pixel is not
1057                    // mosaic-enabled, or if the current pixel has priority over latched pixel
1058                    // e.g. sprite-hmosaic test ROM
1059                    if !obj_mosaic_latch.mosaic
1060                        || !obj_pixel.mosaic
1061                        || obj_pixel.priority < obj_mosaic_latch.priority
1062                    {
1063                        obj_mosaic_latch = obj_pixel;
1064                    }
1065                }
1066
1067                if window_layers_enabled.obj && !obj_mosaic_latch.color.transparent() {
1068                    second_pixel = first_pixel;
1069                    first_pixel = MergePixel::obj(obj_mosaic_latch);
1070                }
1071            }
1072
1073            for (bg, enabled) in bg_enabled.into_iter().enumerate() {
1074                if !enabled || !window_layers_enabled.bg[bg] {
1075                    continue;
1076                }
1077
1078                let bg_pixel = self.buffers.bg_pixels[bg][pixel as usize];
1079                if bg_pixel.transparent() {
1080                    continue;
1081                }
1082
1083                let priority = self.registers.bg_control[bg].priority;
1084                if priority < first_pixel.priority {
1085                    second_pixel = first_pixel;
1086                    first_pixel = MergePixel::bg(bg, bg_pixel, priority);
1087                } else if priority < second_pixel.priority {
1088                    second_pixel = MergePixel::bg(bg, bg_pixel, priority);
1089                }
1090            }
1091
1092            let mut blend_color = first_pixel.color;
1093
1094            // Semi-transparency takes priority over normal blending effect if the condition is true
1095            if first_pixel.semi_transparent
1096                && second_pixel.layer.is_2nd_target_enabled(&self.registers)
1097            {
1098                blend_color = alpha_blend(first_pixel.color, second_pixel.color, eva, evb);
1099            } else if window_layers_enabled.blend
1100                && first_pixel.layer.is_1st_target_enabled(&self.registers)
1101            {
1102                match self.registers.blend_mode {
1103                    BlendMode::AlphaBlending => {
1104                        if second_pixel.layer.is_2nd_target_enabled(&self.registers) {
1105                            blend_color =
1106                                alpha_blend(first_pixel.color, second_pixel.color, eva, evb);
1107                        }
1108                    }
1109                    BlendMode::BrightnessIncrease => {
1110                        blend_color = adjust_brightness::<true>(first_pixel.color, evy);
1111                    }
1112                    BlendMode::BrightnessDecrease => {
1113                        blend_color = adjust_brightness::<false>(first_pixel.color, evy);
1114                    }
1115                    BlendMode::None => {}
1116                }
1117            }
1118
1119            self.frame_buffer.set(self.state.scanline, pixel, blend_color.0);
1120        }
1121
1122        if self.registers.green_swap {
1123            self.green_swap_line();
1124        }
1125    }
1126
1127    fn green_swap_line(&mut self) {
1128        let scanline_addr = (self.state.scanline * SCREEN_WIDTH) as usize;
1129        for [c0, c1] in self.frame_buffer.0[scanline_addr..scanline_addr + SCREEN_WIDTH as usize]
1130            .as_chunks_mut::<2>()
1131            .0
1132        {
1133            let g0 = *c0 & (0x1F << 5);
1134            let g1 = *c1 & (0x1F << 5);
1135
1136            *c0 = (*c0 & !(0x1F << 5)) | g1;
1137            *c1 = (*c1 & !(0x1F << 5)) | g0;
1138        }
1139    }
1140
1141    #[allow(clippy::many_single_char_names)]
1142    fn render_next_sprite_line(&mut self) {
1143        if self.registers.forced_blanking
1144            || (self.state.scanline >= SCREEN_HEIGHT && self.state.scanline != LINES_PER_FRAME - 1)
1145        {
1146            return;
1147        }
1148
1149        let is_bitmap_mode = self.registers.bg_mode.is_bitmap();
1150
1151        self.buffers.obj_pixels.fill(ObjPixel::default());
1152        self.buffers.obj_window.fill(false);
1153
1154        let target_line =
1155            if self.state.scanline == LINES_PER_FRAME - 1 { 0 } else { self.state.scanline + 1 };
1156
1157        // One memory access every 2 cycles
1158        // When OAM is free during HBlank, sprite rendering runs from dots 40 to 1006 (HBlank start)
1159        let mut memory_accesses = if self.registers.oam_free_during_hblank {
1160            // -3 based on Sprite_Last_VRAM_Access_Free test ROM
1161            (HBLANK_START_DOT - 40) / 2 - 3
1162        } else {
1163            DOTS_PER_LINE / 2
1164        };
1165
1166        'outer: for oam_idx in 0..128 {
1167            // 32-bit OAM read of first two attribute words
1168            memory_accesses -= 1;
1169            if memory_accesses == 0 {
1170                break 'outer;
1171            }
1172
1173            let oam_entry = &self.oam_parsed[oam_idx as usize];
1174
1175            if oam_entry.disabled {
1176                continue;
1177            }
1178
1179            let (sprite_width, sprite_height) = oam_entry.shape.size_pixels(oam_entry.size);
1180            let display_width = sprite_width << u8::from(oam_entry.affine_double_size);
1181            let display_height = sprite_height << u8::from(oam_entry.affine_double_size);
1182
1183            if oam_entry.x >= SCREEN_WIDTH && oam_entry.x + display_width <= 512 {
1184                // Sprite is fully horizontally offscreen
1185                continue;
1186            }
1187
1188            if oam_entry.y + display_height > 256 && target_line > 128 {
1189                // 128px tall sprites with Y>128 never display on lines >128
1190                continue;
1191            }
1192
1193            let sprite_y = {
1194                let mut sprite_y = target_line.wrapping_sub(oam_entry.y) & 0xFF;
1195                if sprite_y >= display_height {
1196                    // Sprite does not overlap this scanline
1197                    continue;
1198                }
1199
1200                if oam_entry.mosaic {
1201                    let mosaic_line = self.state.mosaic.obj_line;
1202                    sprite_y = mosaic_line.wrapping_sub(oam_entry.y) & 0xFF;
1203                    if sprite_y >= display_height {
1204                        // If mosaic moves the Y coordinate out of bounds, clamp to 0
1205                        // e.g. Castlevania: Aria of Sorrow, Shrek 2, sprite-vmosaic test ROM
1206                        sprite_y = 0;
1207                    }
1208                }
1209
1210                sprite_y
1211            };
1212
1213            // 16-bit OAM read of third attribute word
1214            memory_accesses -= 1;
1215            if memory_accesses == 0 {
1216                break 'outer;
1217            }
1218
1219            let oam_entry = oam_entry.clone();
1220
1221            if oam_entry.affine {
1222                // 4 OAM reads for the affine parameters plus 1 idle slot at the beginning of VRAM fetch
1223                memory_accesses = memory_accesses.saturating_sub(5);
1224                if memory_accesses == 0 {
1225                    break 'outer;
1226                }
1227
1228                let group_base_addr = 16 * oam_entry.affine_parameter_group as usize;
1229                let [a, b, c, d] = [
1230                    self.oam[group_base_addr + 3],
1231                    self.oam[group_base_addr + 7],
1232                    self.oam[group_base_addr + 11],
1233                    self.oam[group_base_addr + 15],
1234                ]
1235                .map(|p| i32::from(p as i16));
1236
1237                let half_sprite_width = (sprite_width / 2) as i32;
1238                let half_sprite_height = (sprite_height / 2) as i32;
1239                let half_display_width = (display_width / 2) as i32;
1240                let half_display_height = (display_height / 2) as i32;
1241
1242                let y_offset = (sprite_y as i32) - half_display_height;
1243                let x_offset = -half_display_width;
1244
1245                let mut x = a * x_offset + b * y_offset - a;
1246                let mut y = c * x_offset + d * y_offset - c;
1247
1248                let start_x = if oam_entry.x < SCREEN_WIDTH { 0 } else { 512 - oam_entry.x };
1249
1250                for sprite_x in 0..display_width {
1251                    x += a;
1252                    y += c;
1253
1254                    if sprite_x < start_x {
1255                        // Pixel is offscreen to left of frame; PPU skips VRAM fetch
1256                        continue;
1257                    }
1258
1259                    // 1 VRAM read per pixel for affine sprites
1260                    memory_accesses -= 1;
1261                    if memory_accesses == 0 {
1262                        break 'outer;
1263                    }
1264
1265                    let pixel = (oam_entry.x + sprite_x) & 0x1FF;
1266                    if !(0..SCREEN_WIDTH).contains(&pixel) {
1267                        // Pixel is offscreen to right of frame; PPU does _not_ skip VRAM fetch
1268                        // Famicom Mini - Metroid depends on this
1269                        continue;
1270                    }
1271
1272                    let sample_x = (x >> 8) + half_sprite_width;
1273                    let sample_y = (y >> 8) + half_sprite_height;
1274
1275                    if !(0..sprite_width as i32).contains(&sample_x)
1276                        || !(0..sprite_height as i32).contains(&sample_y)
1277                    {
1278                        // Sampling point is out of bounds; pixel is transparent
1279                        continue;
1280                    }
1281
1282                    self.render_sprite_pixel(
1283                        pixel,
1284                        &oam_entry,
1285                        sample_x as u32,
1286                        sample_y as u32,
1287                        sprite_width,
1288                        is_bitmap_mode,
1289                    );
1290                }
1291            } else {
1292                // Non-affine sprite
1293                let sample_y =
1294                    if oam_entry.v_flip { sprite_height - 1 - sprite_y } else { sprite_y };
1295
1296                // Skip over VRAM fetches for pixels that are offscreen to left of frame, aligned to a 2-pixel boundary
1297                let start_x = if oam_entry.x < SCREEN_WIDTH { 0 } else { (512 - oam_entry.x) & !1 };
1298
1299                for sprite_x in start_x..sprite_width {
1300                    // 1 VRAM read per 2 pixels for non-affine sprites
1301                    if sprite_x & 1 == 0 {
1302                        memory_accesses -= 1;
1303                        if memory_accesses == 0 {
1304                            break 'outer;
1305                        }
1306                    }
1307
1308                    let pixel = (oam_entry.x + sprite_x) & 0x1FF;
1309                    if !(0..SCREEN_WIDTH).contains(&pixel) {
1310                        // Sprite pixel is offscreen
1311                        continue;
1312                    }
1313
1314                    let sample_x =
1315                        if oam_entry.h_flip { sprite_width - 1 - sprite_x } else { sprite_x };
1316
1317                    self.render_sprite_pixel(
1318                        pixel,
1319                        &oam_entry,
1320                        sample_x,
1321                        sample_y,
1322                        sprite_width,
1323                        is_bitmap_mode,
1324                    );
1325                }
1326            }
1327
1328            // Next 2 OAM reads overlap with VRAM reads from the previous sprite
1329            memory_accesses += 2;
1330        }
1331    }
1332
1333    fn render_sprite_pixel(
1334        &mut self,
1335        pixel: u32,
1336        oam_entry: &OamEntry,
1337        sample_x: u32,
1338        sample_y: u32,
1339        sprite_width: u32,
1340        is_bitmap_mode: bool,
1341    ) {
1342        let map_step = match oam_entry.bpp {
1343            BitsPerPixel::Four => 1,
1344            BitsPerPixel::Eight => 2,
1345        };
1346
1347        let sprite_tile_x = sample_x / 8;
1348        let sprite_tile_y = sample_y / 8;
1349
1350        let tile_number = match self.registers.obj_vram_map_dimensions {
1351            ObjVramMapDimensions::Two => {
1352                let map_col = (oam_entry.tile_number + sprite_tile_x * map_step) & 0x1F;
1353                let map_row = (oam_entry.tile_number + sprite_tile_y * 32) & 0x3FF & !0x1F;
1354                map_row | map_col
1355            }
1356            ObjVramMapDimensions::One => {
1357                let sprite_width_tiles = sprite_width / 8;
1358                let map_row_width = map_step * sprite_width_tiles;
1359                (oam_entry.tile_number + sprite_tile_y * map_row_width + sprite_tile_x * map_step)
1360                    & 0x3FF
1361            }
1362        };
1363
1364        if is_bitmap_mode && tile_number < 512 {
1365            // Sprite tile numbers 0-511 are not usable in bitmap modes; tiles are fully transparent
1366            return;
1367        }
1368
1369        let tile_col = sample_x % 8;
1370        let tile_row = sample_y % 8;
1371        let tile_base_addr = 0x10000 | (tile_number * 32);
1372
1373        let color_id = match oam_entry.bpp {
1374            BitsPerPixel::Four => {
1375                let tile_addr = tile_base_addr + 4 * tile_row + (tile_col >> 1);
1376                let tile_byte = self.vram[tile_addr as usize];
1377                (tile_byte >> (4 * (tile_col & 1))) & 0xF
1378            }
1379            BitsPerPixel::Eight => {
1380                let tile_addr = tile_base_addr + 8 * tile_row + tile_col;
1381                if tile_addr <= 0x17FFF {
1382                    self.vram[tile_addr as usize]
1383                } else {
1384                    // TODO what should this do? can happen when using an odd tile number
1385                    0
1386                }
1387            }
1388        };
1389
1390        if oam_entry.mode == SpriteMode::ObjWindow && color_id != 0 {
1391            // Opaque OBJ window pixel; mark OBJ window and don't update any other buffers
1392            self.buffers.obj_window[pixel as usize] = true;
1393            return;
1394        }
1395
1396        let buffer_pixel = &mut self.buffers.obj_pixels[pixel as usize];
1397
1398        if oam_entry.priority >= buffer_pixel.priority && !buffer_pixel.color.transparent() {
1399            // Existing opaque pixel with the same or lower priority; do nothing
1400            return;
1401        }
1402
1403        // Always update priority and mosaic flags here because of a hardware bug:
1404        // A transparent pixel that overlaps an opaque pixel from a sprite with lower OAM index and
1405        // higher priority will overwrite the priority and mosaic flags
1406        buffer_pixel.priority = oam_entry.priority;
1407        buffer_pixel.mosaic = oam_entry.mosaic;
1408
1409        if color_id == 0 {
1410            // Transparent pixel; don't update color or semi-transparency flag
1411            return;
1412        }
1413
1414        let palette = match oam_entry.bpp {
1415            BitsPerPixel::Four => oam_entry.palette,
1416            BitsPerPixel::Eight => 0,
1417        };
1418        let palette_ram_addr = 0x100 | (16 * palette + u16::from(color_id));
1419        let color = self.palette_ram[palette_ram_addr as usize];
1420
1421        buffer_pixel.color = Pixel::new_opaque(color);
1422        buffer_pixel.semi_transparent = oam_entry.mode == SpriteMode::SemiTransparent;
1423    }
1424
1425    pub fn frame_complete(&self) -> bool {
1426        self.state.frame_complete
1427    }
1428
1429    pub fn clear_frame_complete(&mut self) {
1430        self.state.frame_complete = false;
1431    }
1432
1433    pub fn frame_buffer(&self) -> &[Color] {
1434        self.ready_frame_buffer.0.as_slice()
1435    }
1436
1437    fn mask_vram_address(address: u32) -> usize {
1438        let vram_addr = (address as usize) & VRAM_ADDR_MASK & !1;
1439        if vram_addr & 0x10000 != 0 { 0x10000 | (vram_addr & 0x7FFF) } else { vram_addr }
1440    }
1441
1442    fn should_ignore_vram_access(&self, address: u32) -> bool {
1443        // When the PPU is in a bitmap mode, accesses to mirrored VRAM at $18000-$1C000 do not work (vram-mirror test ROM)
1444        // Reads always return 0 (or open bus?) and writes are discarded
1445        self.registers.bg_mode.is_bitmap() && (0x18000..0x1C000).contains(&(address & 0x1FFFF))
1446    }
1447
1448    pub fn read_vram(&self, address: u32) -> u16 {
1449        if self.should_ignore_vram_access(address) {
1450            return 0;
1451        }
1452
1453        let vram_addr = Self::mask_vram_address(address);
1454        u16::from_le_bytes(self.vram[vram_addr..vram_addr + 2].try_into().unwrap())
1455    }
1456
1457    pub fn write_vram(&mut self, address: u32, value: u16) {
1458        if self.should_ignore_vram_access(address) {
1459            return;
1460        }
1461
1462        let vram_addr = Self::mask_vram_address(address);
1463        self.vram[vram_addr..vram_addr + 2].copy_from_slice(&value.to_le_bytes());
1464    }
1465
1466    pub fn write_vram_byte(&mut self, address: u32, value: u8) {
1467        let in_obj_vram = if self.registers.bg_mode.is_bitmap() {
1468            // $14000-$17FFF
1469            address & 0x10000 != 0 && address & 0x04000 != 0
1470        } else {
1471            // $10000-$17FFF
1472            address & 0x10000 != 0
1473        };
1474
1475        if in_obj_vram {
1476            // 8-bit writes to OBJ VRAM are ignored
1477            return;
1478        }
1479
1480        // 8-bit writes to BG VRAM duplicate the byte
1481        self.write_vram(address & !1, u16::from_le_bytes([value; 2]));
1482    }
1483
1484    pub fn read_palette_ram(&self, address: u32) -> u16 {
1485        let palette_ram_addr = ((address >> 1) as usize) & (PALETTE_RAM_LEN_HALFWORDS - 1);
1486        self.palette_ram[palette_ram_addr]
1487    }
1488
1489    pub fn write_palette_ram(&mut self, address: u32, value: u16) {
1490        let palette_ram_addr = ((address >> 1) as usize) & (PALETTE_RAM_LEN_HALFWORDS - 1);
1491        self.palette_ram[palette_ram_addr] = value;
1492    }
1493
1494    pub fn read_oam(&self, address: u32) -> u16 {
1495        let oam_addr = ((address >> 1) as usize) & (OAM_LEN_HALFWORDS - 1);
1496        self.oam[oam_addr]
1497    }
1498
1499    pub fn write_oam(&mut self, address: u32, value: u16) {
1500        let oam_addr = ((address >> 1) as usize) & (OAM_LEN_HALFWORDS - 1);
1501        self.oam[oam_addr] = value;
1502
1503        if address & 3 != 3 {
1504            let oam_idx = oam_addr >> 2;
1505            self.oam_parsed[oam_idx] = OamEntry::parse([
1506                self.oam[4 * oam_idx],
1507                self.oam[4 * oam_idx + 1],
1508                self.oam[4 * oam_idx + 2],
1509            ]);
1510        }
1511    }
1512
1513    // TODO this is not accurate
1514    // PPU usually performs 1 palette RAM access per 4 cycles, but it will perform a second access
1515    // if it needs to for alpha blending.
1516    // In modes 3 and 5, it also skips accesses where the layer is BG2 (direct color bitmap)
1517    pub fn palette_ram_in_use(&self) -> bool {
1518        const RENDER_START_DOT: u32 = 46;
1519
1520        !self.registers.forced_blanking
1521            && self.state.scanline < SCREEN_HEIGHT
1522            && (RENDER_START_DOT..HBLANK_START_DOT).contains(&self.state.dot)
1523            && self.state.dot.is_multiple_of(4)
1524    }
1525
1526    pub fn vram_in_use(&self, address: u32) -> bool {
1527        if self.registers.forced_blanking || self.state.forced_blanking_latency != 0 {
1528            return false;
1529        }
1530
1531        let sprite_vram_start = if self.registers.bg_mode.is_bitmap() { 0x14000 } else { 0x10000 };
1532        if address & 0x1FFFF < sprite_vram_start {
1533            self.bg_vram_in_use()
1534        } else {
1535            self.sprite_vram_in_use()
1536        }
1537    }
1538
1539    fn bg_vram_in_use(&self) -> bool {
1540        const FETCH_START_DOT: u32 = 30;
1541
1542        if self.state.scanline >= SCREEN_HEIGHT || self.state.dot >= HBLANK_START_DOT {
1543            // No BG fetching during VBlank or HBlank
1544            return false;
1545        }
1546
1547        let is_bg_enabled =
1548            |bg: usize| self.registers.bg_enabled[bg] && self.state.bg_enabled_latency[bg] == 0;
1549
1550        // Text BG access pattern (32-cycle batches):
1551        //   4bpp: M--- T--- ---- ---- ---- T--- ---- ----
1552        //   8bpp: M--- T--- ---- T--- ---- T--- ---- T---
1553        // In mode 0, slots constantly cycle: BG0, BG1, BG2, BG3, BG0, BG1, etc.
1554        // In mode 1, the BG2 and BG3 slots are both used for affine BG2
1555        let check_text_bg = |bg: usize| {
1556            if !is_bg_enabled(bg) {
1557                return false;
1558            }
1559
1560            // Fetching starts earlier if BG is using fine horizontal scrolling
1561            let start_dot =
1562                FETCH_START_DOT + (bg as u32) - 4 * (self.registers.bg_h_scroll[bg] % 8);
1563            if self.state.dot < start_dot {
1564                return false;
1565            }
1566
1567            let offset = (self.state.dot - start_dot) % 32;
1568            if !offset.is_multiple_of(4) {
1569                return false;
1570            }
1571
1572            // Check slots used for both 4bpp and 8bpp accesses
1573            let slot = offset / 4;
1574            if slot == 0 || slot == 1 || slot == 5 {
1575                return true;
1576            }
1577
1578            // If 8bpp, additionally check slots used for only 8bpp accesses
1579            self.registers.bg_control[bg].bpp == BitsPerPixel::Eight && (slot == 3 || slot == 7)
1580        };
1581
1582        // Affine BGs access during every cycle if enabled
1583        // In mode 1, alternates between 2 cycles of BG0/1 (text), 2 cycles of BG2, 2 cycles of BG0/1, etc.
1584        // In mode 2, alternates between 2 cycles of BG3, 2 cycles of BG2, 2 cycles of BG3, etc.
1585        let check_affine_bg = |bg: usize| is_bg_enabled(bg) && self.state.dot >= FETCH_START_DOT;
1586
1587        let offset = self.state.dot % 4;
1588        match self.registers.bg_mode {
1589            BgMode::Zero => check_text_bg(offset as usize),
1590            BgMode::One => {
1591                if offset < 2 {
1592                    check_text_bg(offset as usize)
1593                } else {
1594                    check_affine_bg(2)
1595                }
1596            }
1597            BgMode::Two => check_affine_bg(2 + usize::from(offset < 2)),
1598            _ => {
1599                // Bitmap modes fetch from VRAM every 4th cycle
1600                offset == 3 && self.state.dot >= FETCH_START_DOT
1601            }
1602        }
1603    }
1604
1605    // TODO this is not entirely accurate - some even cycles only perform an OAM access, and some
1606    // even cycles don't perform an access at all (e.g. for affine sprites)
1607    fn sprite_vram_in_use(&self) -> bool {
1608        const FETCH_START_DOT: u32 = 40;
1609
1610        if !self.registers.obj_enabled || self.state.obj_enabled_latency != 0 {
1611            return false;
1612        }
1613
1614        if !self.state.dot.is_multiple_of(2) {
1615            // Sprite hardware only accesses VRAM/OAM on even cycles
1616            return false;
1617        }
1618
1619        if (SCREEN_HEIGHT..LINES_PER_FRAME - 1).contains(&self.state.scanline) {
1620            // VBlank lines; sprite hardware is idle
1621            return false;
1622        }
1623
1624        if (self.state.scanline == SCREEN_HEIGHT - 1 && self.state.dot >= FETCH_START_DOT)
1625            || (self.state.scanline == LINES_PER_FRAME - 1 && self.state.dot < FETCH_START_DOT)
1626        {
1627            // Too late in the last line (159) or too early in the first line (227)
1628            return false;
1629        }
1630
1631        let interval = if self.registers.oam_free_during_hblank {
1632            FETCH_START_DOT..HBLANK_START_DOT
1633        } else {
1634            0..DOTS_PER_LINE
1635        };
1636
1637        interval.contains(&self.state.dot)
1638    }
1639
1640    pub fn read_register(
1641        &mut self,
1642        address: u32,
1643        cycles: u64,
1644        dma: &mut DmaState,
1645        scheduler: &mut Scheduler,
1646    ) -> Option<u16> {
1647        self.step_to(cycles, dma, scheduler);
1648
1649        log::trace!("PPU register read {address:08X}");
1650
1651        let value = match address {
1652            0x4000000 => self.registers.read_dispcnt(),
1653            0x4000002 => self.registers.read_green_swap(),
1654            0x4000004 => self.read_dispstat(),
1655            0x4000006 => self.state.scanline as u16,
1656            0x4000008..=0x400000E => {
1657                let bg = (address & 7) >> 1;
1658                self.registers.read_bgcnt(bg as usize)
1659            }
1660            0x4000048 => self.registers.read_winin(),
1661            0x400004A => self.registers.read_winout(),
1662            0x4000050 => self.registers.read_bldcnt(),
1663            0x4000052 => self.registers.read_bldalpha(),
1664            _ => {
1665                log::debug!("Unhandled PPU register read {address:08X}");
1666                return None;
1667            }
1668        };
1669
1670        Some(value)
1671    }
1672
1673    // $4000004: DISPSTAT (Display status)
1674    fn read_dispstat(&self) -> u16 {
1675        let in_vblank = self.in_vblank();
1676        let in_hblank = self.in_hblank();
1677        let v_counter_match = self.v_counter_for_dispstat() == self.registers.v_counter_match;
1678
1679        u16::from(in_vblank)
1680            | (u16::from(in_hblank) << 1)
1681            | (u16::from(v_counter_match) << 2)
1682            | (u16::from(self.registers.vblank_irq_enabled) << 3)
1683            | (u16::from(self.registers.hblank_irq_enabled) << 4)
1684            | (u16::from(self.registers.v_counter_irq_enabled) << 5)
1685            | (u16::from(self.registers.v_counter_match) << 8)
1686    }
1687
1688    // $4000004: DISPSTAT (Display status)
1689    fn write_dispstat(
1690        &mut self,
1691        value: u16,
1692        cycles: u64,
1693        interrupts: &mut InterruptRegisters,
1694        scheduler: &mut Scheduler,
1695    ) {
1696        let prev_vblank_enabled = self.registers.vblank_irq_enabled;
1697        let prev_hblank_enabled = self.registers.hblank_irq_enabled;
1698        let prev_v_count_enabled = self.registers.v_counter_irq_enabled;
1699        let prev_v_counter_match = self.registers.v_counter_match;
1700
1701        let v_counter = self.v_counter_for_dispstat();
1702        let prev_v_match = self.registers.v_counter_match == v_counter;
1703
1704        self.registers.write_dispstat(value);
1705
1706        // Changing VCOUNT match mid-line can trigger VCOUNT match IRQs
1707        // e.g. lyc_midline and window_midframe test ROMs
1708        // TODO is it right that this only happens if VCOUNT enabled status doesn't change?
1709        if prev_v_count_enabled
1710            && self.registers.v_counter_irq_enabled
1711            && !prev_v_match
1712            && self.registers.v_counter_match == v_counter
1713        {
1714            interrupts.set_flag(InterruptType::VCounter, cycles + 1);
1715        }
1716
1717        if prev_vblank_enabled != self.registers.vblank_irq_enabled {
1718            self.schedule_vblank_irq(scheduler);
1719        }
1720
1721        if prev_hblank_enabled != self.registers.hblank_irq_enabled {
1722            self.schedule_hblank_irq(scheduler);
1723        }
1724
1725        if prev_v_count_enabled != self.registers.v_counter_irq_enabled
1726            || prev_v_counter_match != self.registers.v_counter_match
1727        {
1728            self.schedule_v_counter_irq(scheduler);
1729        }
1730    }
1731
1732    fn in_vblank(&self) -> bool {
1733        VBLANK_LINES.contains(&self.state.scanline)
1734    }
1735
1736    fn in_hblank(&self) -> bool {
1737        (HBLANK_START_DOT + 1..DOTS_PER_LINE).contains(&self.state.dot)
1738    }
1739
1740    fn v_counter_for_dispstat(&self) -> u8 {
1741        // V counter match bit in DISPSTAT changes at dot 1
1742        let line = if self.state.dot == 0 {
1743            if self.state.scanline == 0 { LINES_PER_FRAME - 1 } else { self.state.scanline - 1 }
1744        } else {
1745            self.state.scanline
1746        };
1747
1748        line as u8
1749    }
1750
1751    #[allow(clippy::match_same_arms)]
1752    pub fn write_register(
1753        &mut self,
1754        address: u32,
1755        value: u16,
1756        cycles: u64,
1757        dma: &mut DmaState,
1758        interrupts: &mut InterruptRegisters,
1759        scheduler: &mut Scheduler,
1760    ) {
1761        log::debug!(
1762            "PPU register write {address:08X} {value:04X} (line {} dot {})",
1763            self.state.scanline,
1764            self.state.dot
1765        );
1766
1767        self.step_to(cycles, dma, scheduler);
1768
1769        match address {
1770            0x4000000 => self.registers.write_dispcnt(value, &mut self.state),
1771            0x4000002 => self.registers.write_green_swap(value),
1772            0x4000004 => self.write_dispstat(value, cycles, interrupts, scheduler),
1773            0x4000006 => {} // High halfword of word-size writes to DISPSTAT
1774            0x4000008..=0x400000E => {
1775                // BGxCNT
1776                let bg = (address & 7) >> 1;
1777                self.registers.write_bgcnt(bg as usize, value);
1778            }
1779            0x4000010..=0x400001E => {
1780                // BGxHOFS / BGxVOFS
1781                let bg = (address & 0xF) >> 2;
1782                if !address.bit(1) {
1783                    self.registers.write_bghofs(bg as usize, value);
1784                } else {
1785                    self.registers.write_bgvofs(bg as usize, value);
1786                }
1787            }
1788            0x4000020..=0x400003E => self.registers.write_bg_affine_register(
1789                address,
1790                value,
1791                &mut self.state.bg_affine_latch,
1792            ),
1793            0x4000040 => self.registers.write_winh(0, value),
1794            0x4000042 => self.registers.write_winh(1, value),
1795            0x4000044 => self.registers.write_winv(0, value),
1796            0x4000046 => self.registers.write_winv(1, value),
1797            0x4000048 => self.registers.write_winin(value),
1798            0x400004A => self.registers.write_winout(value),
1799            0x400004C => self.registers.write_mosaic(value),
1800            0x400004E => {} // High halfword of word-size writes to MOSAIC
1801            0x4000050 => self.registers.write_bldcnt(value),
1802            0x4000052 => self.registers.write_bldalpha(value),
1803            0x4000054 => self.registers.write_bldy(value),
1804            0x4000056 => {} // High halfword of word-size writes to BLDY
1805            _ => {
1806                log::debug!("Unhandled PPU register write {address:08X} {value:04X}");
1807            }
1808        }
1809    }
1810
1811    pub fn write_register_byte(
1812        &mut self,
1813        address: u32,
1814        value: u8,
1815        cycles: u64,
1816        dma: &mut DmaState,
1817        interrupts: &mut InterruptRegisters,
1818        scheduler: &mut Scheduler,
1819    ) {
1820        trait U16Ext {
1821            fn set_byte(&mut self, i: bool, value: u8);
1822        }
1823
1824        impl U16Ext for u16 {
1825            fn set_byte(&mut self, i: bool, value: u8) {
1826                if !i {
1827                    self.set_lsb(value);
1828                } else {
1829                    self.set_msb(value);
1830                }
1831            }
1832        }
1833
1834        self.step_to(cycles, dma, scheduler);
1835
1836        // TODO BGxHOFS, BGxVOFS, MOSAIC, blend registers
1837        match address {
1838            0x4000000..=0x4000005
1839            | 0x4000008..=0x400000F
1840            | 0x4000048..=0x400004B
1841            | 0x4000050..=0x4000053 => {
1842                // R/W registers: DISPCNT, green swap, DISPSTAT, BGxCNT, WININ, WINOUT, BLDCNT, BLDALPHA
1843                let Some(mut halfword) = self.read_register(address & !1, cycles, dma, scheduler)
1844                else {
1845                    return;
1846                };
1847                halfword.set_byte(address.bit(0), value);
1848                self.write_register(address & !1, halfword, cycles, dma, interrupts, scheduler);
1849            }
1850            0x4000010..=0x400001F => {
1851                // BGxHOFS / BGxVOFS
1852                let bg = ((address >> 2) & 3) as usize;
1853                if !address.bit(1) {
1854                    let mut hofs = self.registers.bg_h_scroll[bg] as u16;
1855                    hofs.set_byte(address.bit(0), value);
1856                    self.registers.write_bghofs(bg, hofs);
1857                } else {
1858                    let mut vofs = self.registers.bg_v_scroll[bg] as u16;
1859                    vofs.set_byte(address.bit(0), value);
1860                    self.registers.write_bgvofs(bg, vofs);
1861                }
1862            }
1863            0x4000020..=0x400003F => {
1864                // BG affine registers
1865                let mut halfword = self.registers.read_bg_affine_register(address & !1);
1866                halfword.set_byte(address.bit(0), value);
1867                self.registers.write_bg_affine_register(
1868                    address & !1,
1869                    halfword,
1870                    &mut self.state.bg_affine_latch,
1871                );
1872            }
1873            0x4000040 => self.registers.write_winh_low(0, value),
1874            0x4000041 => self.registers.write_winh_high(0, value),
1875            0x4000042 => self.registers.write_winh_low(1, value),
1876            0x4000043 => self.registers.write_winh_high(1, value),
1877            0x4000044 => self.registers.write_winv_low(0, value),
1878            0x4000045 => self.registers.write_winv_high(0, value),
1879            0x4000046 => self.registers.write_winv_low(1, value),
1880            0x4000047 => self.registers.write_winv_high(1, value),
1881            0x400004C => self.registers.write_bg_mosaic(value),
1882            0x400004D => self.registers.write_obj_mosaic(value),
1883            0x4000054 => self.registers.write_bldy(value.into()), // BLDY is only a 5-bit register
1884            _ => {
1885                log::debug!("Unexpected PPU byte register write {address:08X} {value:02X}");
1886            }
1887        }
1888    }
1889
1890    fn schedule_vblank_irq(&self, scheduler: &mut Scheduler) {
1891        if !self.registers.vblank_irq_enabled {
1892            scheduler.remove(SchedulerEvent::VBlankIrq);
1893            return;
1894        }
1895
1896        let irq_cycles = self.cycles + self.cycles_until_dot(SCREEN_HEIGHT, VBLANK_IRQ_DOT);
1897        scheduler.insert_or_update(SchedulerEvent::VBlankIrq, irq_cycles);
1898    }
1899
1900    pub fn schedule_next_vblank_irq(&self, scheduler: &mut Scheduler, last_irq_cycles: u64) {
1901        debug_assert!(self.registers.vblank_irq_enabled);
1902
1903        let next_irq_cycles = last_irq_cycles + u64::from(DOTS_PER_LINE * LINES_PER_FRAME);
1904        scheduler.insert_or_update(SchedulerEvent::VBlankIrq, next_irq_cycles);
1905    }
1906
1907    fn schedule_hblank_irq(&self, scheduler: &mut Scheduler) {
1908        if !self.registers.hblank_irq_enabled {
1909            scheduler.remove(SchedulerEvent::HBlankIrq);
1910            return;
1911        }
1912
1913        let cycles_until_irq = if self.state.dot >= HBLANK_IRQ_DOT {
1914            DOTS_PER_LINE + HBLANK_IRQ_DOT - self.state.dot
1915        } else {
1916            HBLANK_IRQ_DOT - self.state.dot
1917        };
1918        scheduler
1919            .insert_or_update(SchedulerEvent::HBlankIrq, self.cycles + u64::from(cycles_until_irq));
1920    }
1921
1922    pub fn schedule_next_hblank_irq(&self, scheduler: &mut Scheduler, last_irq_cycles: u64) {
1923        debug_assert!(self.registers.hblank_irq_enabled);
1924
1925        let next_irq_cycles = last_irq_cycles + u64::from(DOTS_PER_LINE);
1926        scheduler.insert_or_update(SchedulerEvent::HBlankIrq, next_irq_cycles);
1927    }
1928
1929    fn schedule_v_counter_irq(&self, scheduler: &mut Scheduler) {
1930        if !self.registers.v_counter_irq_enabled
1931            || self.registers.v_counter_match >= LINES_PER_FRAME as u8
1932        {
1933            scheduler.remove(SchedulerEvent::VCounterIrq);
1934            return;
1935        }
1936
1937        let irq_cycles = self.cycles
1938            + self.cycles_until_dot(self.registers.v_counter_match.into(), V_COUNTER_IRQ_DOT);
1939        scheduler.insert_or_update(SchedulerEvent::VCounterIrq, irq_cycles);
1940    }
1941
1942    pub fn schedule_next_v_counter_irq(&self, scheduler: &mut Scheduler, last_irq_cycles: u64) {
1943        debug_assert!(self.registers.v_counter_irq_enabled);
1944
1945        let next_irq_cycles = last_irq_cycles + u64::from(DOTS_PER_LINE * LINES_PER_FRAME);
1946        scheduler.insert_or_update(SchedulerEvent::VCounterIrq, next_irq_cycles);
1947    }
1948
1949    fn cycles_until_dot(&self, line: u32, dot: u32) -> u64 {
1950        let cycles = if self.state.dot < dot {
1951            let full_lines = if self.state.scanline > line {
1952                LINES_PER_FRAME + line - self.state.scanline
1953            } else {
1954                line - self.state.scanline
1955            };
1956            dot - self.state.dot + full_lines * DOTS_PER_LINE
1957        } else {
1958            let full_lines = if self.state.scanline >= line {
1959                LINES_PER_FRAME - 1 + line - self.state.scanline
1960            } else {
1961                line - self.state.scanline - 1
1962            };
1963            DOTS_PER_LINE + dot - self.state.dot + full_lines * DOTS_PER_LINE
1964        };
1965
1966        cycles.into()
1967    }
1968}
1969
1970fn alpha_blend(first: Pixel, second: Pixel, eva: u16, evb: u16) -> Pixel {
1971    let alpha_blend_component =
1972        |first: u16, second: u16| cmp::min(31, (eva * first + evb * second) >> 4);
1973
1974    let r = alpha_blend_component(first.red(), second.red());
1975    let g = alpha_blend_component(first.green(), second.green());
1976    let b = alpha_blend_component(first.blue(), second.blue());
1977
1978    Pixel::new_opaque_rgb(r, g, b)
1979}
1980
1981fn adjust_brightness<const INCREASE: bool>(color: Pixel, evy: u16) -> Pixel {
1982    let adjust_component = |component: u16| {
1983        if INCREASE {
1984            component + ((evy * (31 - component)) >> 4)
1985        } else {
1986            component - ((evy * component) >> 4)
1987        }
1988    };
1989
1990    let r = adjust_component(color.red());
1991    let g = adjust_component(color.green());
1992    let b = adjust_component(color.blue());
1993
1994    Pixel::new_opaque_rgb(r, g, b)
1995}
1996
1997fn gba_color_to_rgb8(gba_color: u16) -> Color {
1998    const RGB_5_TO_8: &[u8; 32] = &[
1999        0, 8, 16, 25, 33, 41, 49, 58, 66, 74, 82, 90, 99, 107, 115, 123, 132, 140, 148, 156, 165,
2000        173, 181, 189, 197, 206, 214, 222, 230, 239, 247, 255,
2001    ];
2002
2003    let r = gba_color & 0x1F;
2004    let g = (gba_color >> 5) & 0x1F;
2005    let b = (gba_color >> 10) & 0x1F;
2006
2007    Color::rgb(RGB_5_TO_8[r as usize], RGB_5_TO_8[g as usize], RGB_5_TO_8[b as usize])
2008}
2009
2010#[cfg(test)]
2011mod tests {
2012    use super::*;
2013
2014    #[test]
2015    fn cycles_until_dot() {
2016        let mut ppu = Ppu::new(false);
2017
2018        ppu.state.scanline = 7;
2019        ppu.state.dot = 1;
2020        assert_eq!(ppu.cycles_until_dot(7, 1), u64::from(DOTS_PER_LINE * LINES_PER_FRAME));
2021
2022        ppu.state.scanline = 5;
2023        ppu.state.dot = 0;
2024        assert_eq!(ppu.cycles_until_dot(7, 1), u64::from(2 * DOTS_PER_LINE + 1));
2025        ppu.state.dot = 1;
2026        assert_eq!(ppu.cycles_until_dot(7, 1), u64::from(2 * DOTS_PER_LINE));
2027        ppu.state.dot = 2;
2028        assert_eq!(ppu.cycles_until_dot(7, 1), u64::from(2 * DOTS_PER_LINE - 1));
2029
2030        ppu.state.scanline = 10;
2031        ppu.state.dot = 0;
2032        assert_eq!(ppu.cycles_until_dot(7, 1), u64::from(225 * DOTS_PER_LINE + 1));
2033        ppu.state.dot = 1;
2034        assert_eq!(ppu.cycles_until_dot(7, 1), u64::from(225 * DOTS_PER_LINE));
2035        ppu.state.dot = 2;
2036        assert_eq!(ppu.cycles_until_dot(7, 1), u64::from(225 * DOTS_PER_LINE - 1));
2037    }
2038}