fifo.rsannotatedfifo.rssource623 lines · 22.3 KB · raw
1use crate::HardwareMode;
2use crate::cgb::{CgbRegisters, ObjPriority};
3use crate::ppu::registers::{CgbPaletteRam, Registers, TileDataArea};
4use crate::ppu::{MAX_SPRITES_PER_LINE, PpuFrameBuffer, SCREEN_WIDTH, SpriteData, Vram};
5use bincode::{Decode, Encode};
6use jgenesis_common::num::GetBit;
7use std::array;
8use std::collections::VecDeque;
9
10const MAX_FIFO_X: u8 = SCREEN_WIDTH as u8 + 8;
11
12#[derive(Debug, Clone, Copy, Encode, Decode)]
13struct BgPixel {
14    color: u8,
15    palette: u8,
16    high_priority: bool,
17}
18
19#[derive(Debug, Clone, Copy, Encode, Decode)]
20struct SpritePixel {
21    color: u8,
22    palette: u8,
23    low_priority: bool,
24    oam_index: u8,
25}
26
27impl SpritePixel {
28    const TRANSPARENT: Self = Self { color: 0, palette: 0, low_priority: true, oam_index: 255 };
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)]
32enum BgLayer {
33    Background,
34    Window { window_line: u8 },
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)]
38struct RenderingBgTileFields {
39    bg_layer: BgLayer,
40    dots_remaining: u8,
41    screen_x: u8,
42    fetcher_x: u8,
43    latched_scx: u8,
44    latched_scy: u8,
45    // Whether or not a sprite fetch was delayed by a BG fetch in the current BG tile
46    sprite_fetch_delayed: bool,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)]
50enum FifoState {
51    // Fetching the offscreen first tile
52    InitialBgFetch { dots_remaining: u8 },
53    // Rendering a background tile
54    RenderingBgTile(RenderingBgTileFields),
55    // Fetching a sprite tile
56    SpriteFetch { dots_remaining: u8, previous_bg_fields: RenderingBgTileFields },
57    // Fetching the first window tile
58    InitialWindowFetch { dots_remaining: u8, screen_x: u8, window_line: u8 },
59}
60
61#[derive(Debug, Clone, Encode, Decode)]
62pub struct PixelFifo {
63    hardware_mode: HardwareMode,
64    bg: VecDeque<BgPixel>,
65    sprites: VecDeque<SpritePixel>,
66    y: u8,
67    window_y_triggered: bool,
68    window_line_counter: u8,
69    window_triggered_in_frame: bool,
70    scanned_sprites: VecDeque<SpriteData>,
71    state: FifoState,
72}
73
74impl PixelFifo {
75    pub fn new(hardware_mode: HardwareMode) -> Self {
76        Self {
77            hardware_mode,
78            bg: VecDeque::with_capacity(16),
79            sprites: VecDeque::with_capacity(16),
80            y: 0,
81            window_y_triggered: false,
82            window_line_counter: 0,
83            window_triggered_in_frame: false,
84            scanned_sprites: VecDeque::with_capacity(MAX_SPRITES_PER_LINE),
85            state: FifoState::InitialBgFetch { dots_remaining: 6 },
86        }
87    }
88
89    pub fn reset_window_state(&mut self) {
90        self.window_y_triggered = false;
91        self.window_line_counter = 0;
92        self.window_triggered_in_frame = false;
93    }
94
95    pub fn check_window_y(&mut self, scanline: u8, registers: &Registers) {
96        self.window_y_triggered |= registers.window_y == scanline && registers.window_enabled;
97    }
98
99    pub fn start_new_line(&mut self, scanline: u8, registers: &Registers, sprites: &[SpriteData]) {
100        self.bg.clear();
101        self.sprites.clear();
102        self.y = scanline;
103
104        self.check_window_y(scanline, registers);
105
106        self.scanned_sprites.clear();
107        self.scanned_sprites.extend(sprites);
108
109        // Initial BG tile fetch always takes 6 cycles
110        self.state = FifoState::InitialBgFetch { dots_remaining: 6 };
111
112        // TODO check WY here
113    }
114
115    pub fn tick(
116        &mut self,
117        vram: &Vram,
118        registers: &Registers,
119        cgb_registers: CgbRegisters,
120        bg_palette_ram: &CgbPaletteRam,
121        sprite_palette_ram: &CgbPaletteRam,
122        frame_buffer: Option<&mut PpuFrameBuffer>,
123    ) {
124        match self.state {
125            FifoState::InitialBgFetch { dots_remaining } => {
126                self.handle_initial_bg_fetch(dots_remaining, vram, registers);
127            }
128            FifoState::RenderingBgTile(fields) => {
129                self.handle_rendering_bg_tile(
130                    fields,
131                    vram,
132                    registers,
133                    cgb_registers,
134                    bg_palette_ram,
135                    sprite_palette_ram,
136                    frame_buffer,
137                );
138            }
139            FifoState::SpriteFetch { dots_remaining, previous_bg_fields } => {
140                self.handle_sprite_fetch(dots_remaining, previous_bg_fields);
141            }
142            FifoState::InitialWindowFetch { dots_remaining, screen_x, window_line } => {
143                self.handle_initial_window_fetch(dots_remaining, screen_x, window_line);
144            }
145        }
146    }
147
148    fn handle_initial_bg_fetch(&mut self, dots_remaining: u8, vram: &Vram, registers: &Registers) {
149        if dots_remaining == 6 {
150            // Do the initial tile fetch
151            let bg_tile_row = fetch_bg_tile(
152                self.hardware_mode,
153                0,
154                self.y,
155                vram,
156                registers,
157                registers.bg_x_scroll,
158                registers.bg_y_scroll,
159            );
160            for color in bg_tile_row.pixels {
161                self.bg.push_back(BgPixel {
162                    color,
163                    palette: bg_tile_row.palette,
164                    high_priority: bg_tile_row.high_priority,
165                });
166            }
167        }
168
169        if dots_remaining != 1 {
170            self.state = FifoState::InitialBgFetch { dots_remaining: dots_remaining - 1 };
171            return;
172        }
173
174        // TODO check if WX=0 here and emulate WX=0 hardware glitches
175
176        // DMG hardware glitch: render a single pixel of color BGP[0] if the following are all true:
177        //   The window triggered earlier in the current frame
178        //   The window is currently disabled
179        //   WX & 7 == 7 - (SCX & 7)
180        // This is needed for correct alignment on the title screen of Star Trek: 25th Anniversary
181        // Reference: https://github.com/LIJI32/SameBoy/issues/278
182        if self.hardware_mode == HardwareMode::Dmg
183            && self.window_triggered_in_frame
184            && !registers.window_enabled
185            && registers.window_x & 7 == 7 - (registers.bg_x_scroll & 7)
186        {
187            self.bg.push_back(BgPixel { color: 0, palette: 0, high_priority: false });
188        }
189
190        // The first 8 pixels are always discarded, and (SCX % 8) additional pixels are discarded to handle fine X scrolling
191        // Simulate this by using fine X scroll to move the screen position backwards
192        let fine_x_scroll = registers.bg_x_scroll % 8;
193        self.state = FifoState::RenderingBgTile(RenderingBgTileFields {
194            bg_layer: BgLayer::Background,
195            dots_remaining: 8,
196            screen_x: 0_u8.wrapping_sub(fine_x_scroll),
197            fetcher_x: 0,
198            sprite_fetch_delayed: false,
199            latched_scx: registers.bg_x_scroll,
200            latched_scy: registers.bg_y_scroll,
201        });
202    }
203
204    #[allow(clippy::too_many_arguments)]
205    fn handle_rendering_bg_tile(
206        &mut self,
207        mut fields: RenderingBgTileFields,
208        vram: &Vram,
209        registers: &Registers,
210        cgb_registers: CgbRegisters,
211        bg_palette_ram: &CgbPaletteRam,
212        sprite_palette_ram: &CgbPaletteRam,
213        frame_buffer: Option<&mut PpuFrameBuffer>,
214    ) {
215        if let Some(sprite) =
216            self.scanned_sprites.pop_front_if(|sprite| sprite.x == fields.screen_x)
217        {
218            // A sprite starts on this position. Go ahead and do the actual tile fetch immediately
219            // TODO GBC always fetches sprite tiles even when sprites are disabled
220            if registers.sprites_enabled {
221                self.fetch_next_sprite_tile(sprite, vram, registers, cgb_registers.obj_priority);
222
223                // Sprite fetches take at minimum 6 cycles, and the fetch may be delayed by an additional 1-5 cycles if it
224                // needs to wait for a BG fetch to finish
225                let sprite_fetch_cycles =
226                    if !fields.sprite_fetch_delayed && (4..9).contains(&fields.dots_remaining) {
227                        fields.sprite_fetch_delayed = true;
228                        6 + fields.dots_remaining - 3
229                    } else {
230                        6
231                    };
232
233                log::trace!(
234                    "Sprite encountered at X {}, delaying by {sprite_fetch_cycles} cycles",
235                    fields.screen_x
236                );
237
238                // Subtract 1 to account for the current tick
239                self.state = FifoState::SpriteFetch {
240                    dots_remaining: sprite_fetch_cycles - 1,
241                    previous_bg_fields: fields,
242                };
243
244                return;
245            }
246
247            // Sprites are disabled; pop all sprites at the current position
248            while self.scanned_sprites.front().is_some_and(|sprite| sprite.x == fields.screen_x) {
249                self.scanned_sprites.pop_front();
250            }
251        }
252
253        if self.window_y_triggered
254            && registers.window_enabled
255            && registers.window_x <= (SCREEN_WIDTH + 7) as u8
256            && registers.window_x.saturating_add(1) == fields.screen_x
257            && fields.bg_layer == BgLayer::Background
258        {
259            log::trace!("Window started at X {}", fields.screen_x);
260
261            // Window triggered; clear the BG FIFO and fetch window tile 0
262            self.bg.clear();
263            let first_window_tile =
264                fetch_window_tile(self.hardware_mode, 0, self.window_line_counter, vram, registers);
265            self.push_bg_tile_row(first_window_tile);
266
267            self.state = FifoState::InitialWindowFetch {
268                // Wait for 5 cycles instead of 6 to account for the current tick
269                dots_remaining: 5,
270                screen_x: fields.screen_x,
271                window_line: self.window_line_counter,
272            };
273            self.window_line_counter += 1;
274            self.window_triggered_in_frame = true;
275
276            return;
277        }
278
279        if fields.dots_remaining == 4 {
280            // Latch SCY/SCX two dots before tile fetch
281            // Some demos depend on this, e.g. Demotronic
282            fields.latched_scx = registers.bg_x_scroll;
283            fields.latched_scy = registers.bg_y_scroll;
284        }
285
286        if fields.dots_remaining == 2 {
287            log::trace!("Fetching BG tile at X {}", fields.screen_x);
288
289            // Fetch next BG/window tile
290            match fields.bg_layer {
291                BgLayer::Background => {
292                    let bg_tile_row = fetch_bg_tile(
293                        self.hardware_mode,
294                        fields.fetcher_x,
295                        self.y,
296                        vram,
297                        registers,
298                        fields.latched_scx,
299                        fields.latched_scy,
300                    );
301                    self.push_bg_tile_row(bg_tile_row);
302                }
303                BgLayer::Window { window_line } => {
304                    let window_tile_row = fetch_window_tile(
305                        self.hardware_mode,
306                        fields.fetcher_x,
307                        window_line,
308                        vram,
309                        registers,
310                    );
311                    self.push_bg_tile_row(window_tile_row);
312                }
313            }
314
315            fields.fetcher_x = fields.fetcher_x.wrapping_add(1);
316        }
317
318        let bg_pixel = self.bg.pop_front().expect("BG FIFO should never be empty while rendering");
319
320        let mut sprite_pixel = self.sprites.pop_front().unwrap_or(SpritePixel::TRANSPARENT);
321        if !registers.sprites_enabled {
322            sprite_pixel = SpritePixel::TRANSPARENT;
323        }
324
325        if (8..MAX_FIFO_X).contains(&fields.screen_x) {
326            let color = if sprite_pixel.color != 0
327                && !(bg_pixel.color != 0
328                    && registers.bg_enabled
329                    && (sprite_pixel.low_priority || bg_pixel.high_priority))
330            {
331                // Render sprite pixel
332                let read_dmg_color = |pixel: SpritePixel| {
333                    registers.sprite_palettes[pixel.palette as usize][pixel.color as usize]
334                };
335
336                match (self.hardware_mode, cgb_registers.dmg_compatibility) {
337                    (HardwareMode::Dmg, _) => read_dmg_color(sprite_pixel).into(),
338                    (HardwareMode::Cgb, false) => {
339                        sprite_palette_ram.read_color(sprite_pixel.palette, sprite_pixel.color)
340                    }
341                    (HardwareMode::Cgb, true) => {
342                        let dmg_color = read_dmg_color(sprite_pixel);
343                        sprite_palette_ram.read_color(sprite_pixel.palette, dmg_color)
344                    }
345                }
346            } else if registers.bg_enabled
347                || (self.hardware_mode == HardwareMode::Cgb && !cgb_registers.dmg_compatibility)
348            {
349                // Render BG pixel
350                match (self.hardware_mode, cgb_registers.dmg_compatibility) {
351                    (HardwareMode::Dmg, _) => registers.bg_palette[bg_pixel.color as usize].into(),
352                    (HardwareMode::Cgb, false) => {
353                        bg_palette_ram.read_color(bg_pixel.palette, bg_pixel.color)
354                    }
355                    (HardwareMode::Cgb, true) => {
356                        let dmg_color = registers.bg_palette[bg_pixel.color as usize];
357                        bg_palette_ram.read_color(0, dmg_color)
358                    }
359                }
360            } else {
361                // In DMG mode, if BG is disabled and sprite pixel is transparent, always display
362                // BG color 0
363                let dmg_color = registers.bg_palette[0];
364                match self.hardware_mode {
365                    HardwareMode::Dmg => dmg_color.into(),
366                    HardwareMode::Cgb => bg_palette_ram.read_color(0, dmg_color),
367                }
368            };
369
370            if let Some(frame_buffer) = frame_buffer {
371                let frame_buffer_idx =
372                    (self.y as usize) * SCREEN_WIDTH + (fields.screen_x - 8) as usize;
373                frame_buffer[frame_buffer_idx] = color;
374            }
375        }
376        fields.screen_x = fields.screen_x.wrapping_add(1);
377
378        if fields.dots_remaining == 1 {
379            fields.dots_remaining = 8;
380            fields.sprite_fetch_delayed = false;
381        } else {
382            fields.dots_remaining -= 1;
383        }
384
385        self.state = FifoState::RenderingBgTile(fields);
386    }
387
388    fn push_bg_tile_row(&mut self, bg_tile_row: BgTileRow) {
389        for color in bg_tile_row.pixels {
390            self.bg.push_back(BgPixel {
391                color,
392                palette: bg_tile_row.palette,
393                high_priority: bg_tile_row.high_priority,
394            });
395        }
396    }
397
398    fn fetch_next_sprite_tile(
399        &mut self,
400        sprite: SpriteData,
401        vram: &Vram,
402        registers: &Registers,
403        cgb_obj_priority: ObjPriority,
404    ) {
405        let sprite_tile = fetch_sprite_tile(sprite, self.y, vram, registers.double_height_sprites);
406
407        while self.sprites.len() < 8 {
408            self.sprites.push_back(SpritePixel::TRANSPARENT);
409        }
410
411        for (i, color) in sprite_tile.into_iter().enumerate() {
412            if color == 0 {
413                // Don't add transparent pixels to the FIFO
414                continue;
415            }
416
417            // Replace any transparent pixels in the FIFO
418            // If in CGB mode and OPRI=0, also replace any non-transparent pixels from sprites with a higher OAM index
419            if self.sprites[i].color == 0
420                || (self.hardware_mode == HardwareMode::Cgb
421                    && cgb_obj_priority == ObjPriority::OamIndex
422                    && sprite.oam_index < self.sprites[i].oam_index)
423            {
424                self.sprites[i] = SpritePixel {
425                    color,
426                    palette: sprite.palette,
427                    low_priority: sprite.low_priority,
428                    oam_index: sprite.oam_index,
429                };
430            }
431        }
432    }
433
434    fn handle_sprite_fetch(
435        &mut self,
436        dots_remaining: u8,
437        previous_bg_fields: RenderingBgTileFields,
438    ) {
439        self.state = if dots_remaining == 1 {
440            FifoState::RenderingBgTile(previous_bg_fields)
441        } else {
442            FifoState::SpriteFetch { dots_remaining: dots_remaining - 1, previous_bg_fields }
443        };
444    }
445
446    fn handle_initial_window_fetch(&mut self, dots_remaining: u8, screen_x: u8, window_line: u8) {
447        self.state = if dots_remaining == 1 {
448            FifoState::RenderingBgTile(RenderingBgTileFields {
449                bg_layer: BgLayer::Window { window_line },
450                dots_remaining: 8,
451                screen_x,
452                // Start at tile 1 since tile 0 has already been fetched
453                fetcher_x: 1,
454                sprite_fetch_delayed: false,
455                // Values don't matter because the window does not use SCY/SCX during tile fetch
456                latched_scx: 0,
457                latched_scy: 0,
458            })
459        } else {
460            FifoState::InitialWindowFetch {
461                dots_remaining: dots_remaining - 1,
462                screen_x,
463                window_line,
464            }
465        };
466    }
467
468    pub fn done_with_line(&self) -> bool {
469        match self.state {
470            FifoState::InitialBgFetch { .. }
471            | FifoState::SpriteFetch { .. }
472            | FifoState::InitialWindowFetch { .. } => false,
473            FifoState::RenderingBgTile(fields) => fields.screen_x == MAX_FIFO_X,
474        }
475    }
476}
477
478#[derive(Debug, Clone, Copy)]
479pub(super) struct BgTileAttributes {
480    pub(super) high_priority: bool,
481    pub(super) vertical_flip: bool,
482    pub(super) horizontal_flip: bool,
483    pub(super) vram_bank: u8,
484    pub(super) palette: u8,
485}
486
487impl From<u8> for BgTileAttributes {
488    fn from(value: u8) -> Self {
489        Self {
490            high_priority: value.bit(7),
491            vertical_flip: value.bit(6),
492            horizontal_flip: value.bit(5),
493            vram_bank: value.bit(3).into(),
494            palette: value & 0x07,
495        }
496    }
497}
498
499#[derive(Debug, Clone, Copy)]
500struct BgTileRow {
501    pixels: [u8; 8],
502    palette: u8,
503    high_priority: bool,
504}
505
506fn fetch_bg_tile(
507    hardware_mode: HardwareMode,
508    fetcher_x: u8,
509    y: u8,
510    vram: &Vram,
511    registers: &Registers,
512    bg_x_scroll: u8,
513    bg_y_scroll: u8,
514) -> BgTileRow {
515    if hardware_mode == HardwareMode::Dmg && !registers.bg_enabled {
516        // On DMG, all BG pixels are transparent if BG is disabled
517        return BgTileRow { pixels: [0; 8], palette: 0, high_priority: false };
518    }
519
520    let coarse_x_scroll = bg_x_scroll / 8;
521    let tile_map_x: u16 = (fetcher_x.wrapping_add(coarse_x_scroll) % 32).into();
522
523    let bg_y: u16 = y.wrapping_add(bg_y_scroll).into();
524    let tile_map_y = bg_y / 8;
525
526    let tile_map_addr = registers.bg_tile_map_addr | (tile_map_y << 5) | tile_map_x;
527    let tile_number = vram[tile_map_addr as usize];
528
529    // No need to check for CGB here; the CPU can't write to VRAM bank 1 in DMG mode, and an attributes byte of 0 is
530    // equivalent to DMG functionality
531    let attributes_map_addr = 0x2000 | tile_map_addr;
532    let attributes = BgTileAttributes::from(vram[attributes_map_addr as usize]);
533
534    let bank_addr = u16::from(attributes.vram_bank) << 13;
535    let tile_row = if attributes.vertical_flip { 7 - (bg_y % 8) } else { bg_y % 8 };
536    let tile_addr =
537        bank_addr | registers.bg_tile_data_area.tile_address(tile_number) | (tile_row << 1);
538    let tile_data_lsb = vram[tile_addr as usize];
539    let tile_data_msb = vram[(tile_addr + 1) as usize];
540
541    let pixels = tile_data_to_pixels(tile_data_lsb, tile_data_msb, attributes.horizontal_flip);
542
543    BgTileRow { pixels, palette: attributes.palette, high_priority: attributes.high_priority }
544}
545
546fn fetch_window_tile(
547    hardware_mode: HardwareMode,
548    fetcher_x: u8,
549    window_line: u8,
550    vram: &Vram,
551    registers: &Registers,
552) -> BgTileRow {
553    if (hardware_mode == HardwareMode::Dmg && !registers.bg_enabled) || !registers.window_enabled {
554        // All BG pixels are transparent if BG is disabled
555        return BgTileRow { pixels: [0; 8], palette: 0, high_priority: false };
556    }
557
558    let tile_map_x: u16 = fetcher_x.into();
559    let tile_map_y: u16 = (window_line / 8).into();
560
561    let tile_map_addr = registers.window_tile_map_addr | (tile_map_y << 5) | tile_map_x;
562    let tile_number = vram[tile_map_addr as usize];
563
564    let attributes_addr = 0x2000 | tile_map_addr;
565    let attributes = BgTileAttributes::from(vram[attributes_addr as usize]);
566
567    let bank_addr = u16::from(attributes.vram_bank) << 13;
568    let tile_row: u16 = if attributes.vertical_flip {
569        (7 - (window_line % 8)).into()
570    } else {
571        (window_line % 8).into()
572    };
573    let tile_addr =
574        bank_addr | registers.bg_tile_data_area.tile_address(tile_number) | (tile_row << 1);
575    let tile_data_lsb = vram[tile_addr as usize];
576    let tile_data_msb = vram[(tile_addr + 1) as usize];
577
578    let pixels = tile_data_to_pixels(tile_data_lsb, tile_data_msb, attributes.horizontal_flip);
579
580    BgTileRow { pixels, palette: attributes.palette, high_priority: attributes.high_priority }
581}
582
583fn fetch_sprite_tile(
584    sprite: SpriteData,
585    y: u8,
586    vram: &Vram,
587    double_height_sprites: bool,
588) -> [u8; 8] {
589    let sprite_row = y.wrapping_sub(sprite.y.wrapping_add(16));
590
591    let tile_number = if double_height_sprites {
592        // In double height sprite mode, the lowest bit of tile number is ignored, and which tile gets used depends
593        // on sprite row and vertical flip
594        let base_tile_number = sprite.tile_number & !0x01;
595        let lower_tile = sprite_row.bit(3) ^ sprite.vertical_flip;
596        base_tile_number | u8::from(lower_tile)
597    } else {
598        sprite.tile_number
599    };
600
601    let tile_row = if sprite.vertical_flip { 7 - (sprite_row & 0x07) } else { sprite_row & 0x07 };
602
603    let bank_addr = u16::from(sprite.vram_bank) << 13;
604    let tile_addr =
605        bank_addr | TileDataArea::SPRITES.tile_address(tile_number) | u16::from(tile_row << 1);
606    let tile_data_lsb = vram[tile_addr as usize];
607    let tile_data_msb = vram[(tile_addr + 1) as usize];
608
609    tile_data_to_pixels(tile_data_lsb, tile_data_msb, sprite.horizontal_flip)
610}
611
612fn tile_data_to_pixels(tile_data_lsb: u8, tile_data_msb: u8, horizontal_flip: bool) -> [u8; 8] {
613    if horizontal_flip {
614        array::from_fn(|i| {
615            u8::from(tile_data_lsb.bit(i as u8)) | (u8::from(tile_data_msb.bit(i as u8)) << 1)
616        })
617    } else {
618        array::from_fn(|i| {
619            let pixel_idx = 7 - i as u8;
620            u8::from(tile_data_lsb.bit(pixel_idx)) | (u8::from(tile_data_msb.bit(pixel_idx)) << 1)
621        })
622    }
623}