From 8e47afa1bc626714b516e64c6df0fa5940eee5ba Mon Sep 17 00:00:00 2001 From: EliseZeroTwo Date: Thu, 25 Jan 2024 10:41:36 -0700 Subject: [PATCH] fix: PPU timings are more accurate --- .forgejo/workflows/action.yml | 12 + .github/workflows/action.yml | 12 + meowgb-core/src/gameboy.rs | 44 ++- meowgb-core/src/gameboy/dma.rs | 16 +- meowgb-core/src/gameboy/memory.rs | 5 +- meowgb-core/src/gameboy/ppu.rs | 298 ++++++++++-------- meowgb-core/src/lib.rs | 3 +- .../ppu/intr_2_mode0_timing.bin | 1 + .../ppu/intr_2_mode3_timing.bin | 1 + .../ppu/intr_2_oam_ok_timing.bin | 1 + meowgb-tests/src/main.rs | 38 ++- meowgb/src/main.rs | 11 +- run-test-roms.sh | 30 ++ .../serial-roms/ppu/intr_2_mode0_timing.gb | Bin 0 -> 32768 bytes .../serial-roms/ppu/intr_2_mode3_timing.gb | Bin 0 -> 32768 bytes .../serial-roms/ppu/intr_2_oam_ok_timing.gb | Bin 0 -> 32768 bytes tests.md | 3 + 17 files changed, 300 insertions(+), 175 deletions(-) create mode 100644 meowgb-tests/expected_output/serial/mooneye-test-suite/ppu/intr_2_mode0_timing.bin create mode 100644 meowgb-tests/expected_output/serial/mooneye-test-suite/ppu/intr_2_mode3_timing.bin create mode 100644 meowgb-tests/expected_output/serial/mooneye-test-suite/ppu/intr_2_oam_ok_timing.bin create mode 100644 test-roms/mooneye-test-suite/serial-roms/ppu/intr_2_mode0_timing.gb create mode 100644 test-roms/mooneye-test-suite/serial-roms/ppu/intr_2_mode3_timing.gb create mode 100644 test-roms/mooneye-test-suite/serial-roms/ppu/intr_2_oam_ok_timing.gb diff --git a/.forgejo/workflows/action.yml b/.forgejo/workflows/action.yml index a9c0d68..fec8908 100644 --- a/.forgejo/workflows/action.yml +++ b/.forgejo/workflows/action.yml @@ -238,6 +238,18 @@ jobs: if: always() run: ./target/release/meowgb-tests test-roms/mooneye-test-suite/serial-roms/ppu/intr_2_0_timing.gb test-serial -m 100000000 -s meowgb-tests/expected_output/serial/mooneye-test-suite/ppu/intr_2_0_timing.bin + - name: Run test ROM (mooneye-test-suite ppu/intr_2_mode0_timing) + if: always() + run: ./target/release/meowgb-tests test-roms/mooneye-test-suite/serial-roms/ppu/intr_2_mode0_timing.gb test-serial -m 100000000 -s meowgb-tests/expected_output/serial/mooneye-test-suite/ppu/intr_2_mode0_timing.bin + + - name: Run test ROM (mooneye-test-suite ppu/intr_2_mode3_timing) + if: always() + run: ./target/release/meowgb-tests test-roms/mooneye-test-suite/serial-roms/ppu/intr_2_mode3_timing.gb test-serial -m 100000000 -s meowgb-tests/expected_output/serial/mooneye-test-suite/ppu/intr_2_mode3_timing.bin + + - name: Run test ROM (mooneye-test-suite ppu/intr_2_oam_ok_timing) + if: always() + run: ./target/release/meowgb-tests test-roms/mooneye-test-suite/serial-roms/ppu/intr_2_oam_ok_timing.gb test-serial -m 100000000 -s meowgb-tests/expected_output/serial/mooneye-test-suite/ppu/intr_2_oam_ok_timing.bin + - name: Run test ROM (mooneye-test-suite ppu/stat_irq_blocking) if: always() run: ./target/release/meowgb-tests test-roms/mooneye-test-suite/serial-roms/ppu/stat_irq_blocking.gb test-serial -m 100000000 -s meowgb-tests/expected_output/serial/mooneye-test-suite/ppu/stat_irq_blocking.bin diff --git a/.github/workflows/action.yml b/.github/workflows/action.yml index a9c0d68..fec8908 100644 --- a/.github/workflows/action.yml +++ b/.github/workflows/action.yml @@ -238,6 +238,18 @@ jobs: if: always() run: ./target/release/meowgb-tests test-roms/mooneye-test-suite/serial-roms/ppu/intr_2_0_timing.gb test-serial -m 100000000 -s meowgb-tests/expected_output/serial/mooneye-test-suite/ppu/intr_2_0_timing.bin + - name: Run test ROM (mooneye-test-suite ppu/intr_2_mode0_timing) + if: always() + run: ./target/release/meowgb-tests test-roms/mooneye-test-suite/serial-roms/ppu/intr_2_mode0_timing.gb test-serial -m 100000000 -s meowgb-tests/expected_output/serial/mooneye-test-suite/ppu/intr_2_mode0_timing.bin + + - name: Run test ROM (mooneye-test-suite ppu/intr_2_mode3_timing) + if: always() + run: ./target/release/meowgb-tests test-roms/mooneye-test-suite/serial-roms/ppu/intr_2_mode3_timing.gb test-serial -m 100000000 -s meowgb-tests/expected_output/serial/mooneye-test-suite/ppu/intr_2_mode3_timing.bin + + - name: Run test ROM (mooneye-test-suite ppu/intr_2_oam_ok_timing) + if: always() + run: ./target/release/meowgb-tests test-roms/mooneye-test-suite/serial-roms/ppu/intr_2_oam_ok_timing.gb test-serial -m 100000000 -s meowgb-tests/expected_output/serial/mooneye-test-suite/ppu/intr_2_oam_ok_timing.bin + - name: Run test ROM (mooneye-test-suite ppu/stat_irq_blocking) if: always() run: ./target/release/meowgb-tests test-roms/mooneye-test-suite/serial-roms/ppu/stat_irq_blocking.gb test-serial -m 100000000 -s meowgb-tests/expected_output/serial/mooneye-test-suite/ppu/stat_irq_blocking.bin diff --git a/meowgb-core/src/gameboy.rs b/meowgb-core/src/gameboy.rs index 841fbef..d71651b 100644 --- a/meowgb-core/src/gameboy.rs +++ b/meowgb-core/src/gameboy.rs @@ -104,13 +104,11 @@ impl Gameboy { } macro_rules! pop8 { - () => { - { - let res = self.debug_read_u8(self.registers.sp); - self.registers.sp = self.registers.sp.overflowing_add(1).0; - res - } - }; + () => {{ + let res = self.debug_read_u8(self.registers.sp); + self.registers.sp = self.registers.sp.overflowing_add(1).0; + res + }}; } macro_rules! pop16 { @@ -120,22 +118,20 @@ impl Gameboy { } macro_rules! rl { - ($reg:ident) => { - { - let new_carry = self.registers.$reg >> 7 == 1; - self.registers.$reg <<= 1; + ($reg:ident) => {{ + let new_carry = self.registers.$reg >> 7 == 1; + self.registers.$reg <<= 1; - if self.registers.get_carry() { - self.registers.$reg |= 1; - } - - self.registers.set_carry(new_carry); + if self.registers.get_carry() { + self.registers.$reg |= 1; } - }; + + self.registers.set_carry(new_carry); + }}; } self.registers.sp = 0xFFFE; - + // Clear VRAM self.registers.a = 0; let mut address = 0x9FFF; @@ -160,7 +156,7 @@ impl Gameboy { // self.registers.set_hl(hl_content - 1); // self.registers.a = 0x77; // self.debug_write_u8(hl_content, self.registers.a); - + // Configure Background Palette self.registers.a = 0xFC; self.debug_write_u8(0xFF47, self.registers.a); @@ -171,7 +167,7 @@ impl Gameboy { loop { self.registers.a = self.debug_read_u8(self.registers.get_de()); - + self.registers.c = self.registers.a; for _ in 0..2 { self.registers.b = 4; @@ -190,7 +186,7 @@ impl Gameboy { self.debug_write_u8(self.registers.get_hl(), self.registers.a); self.registers.set_hl(self.registers.get_hl() + 2); } - + self.registers.set_de(self.registers.get_de() + 1); self.registers.a = self.registers.e; @@ -394,7 +390,7 @@ impl Gameboy { 0xFF4B => self.ppu.registers.wx = value, 0xFF4C..=0xFF4E => {} // Unused 0xFF4F => {} // CGB VRAM Bank Select - 0xFF50 => {} // BROM lockout + 0xFF50 => {} // BROM lockout 0xFF51..=0xFF55 => {} // CGB VRAM DMA 0xFF56..=0xFF67 => {} // Unused 0xFF68..=0xFF69 => {} // CGB BG/OBJ Palettes @@ -474,7 +470,9 @@ impl Gameboy { self.registers.mem_op_happened = true; let value = match self.dma.is_conflict(address) { true => match address { - 0..=0xFDFF => self.dma.read_next_byte(&self.ppu, &self.memory, self.cartridge.as_deref()), + 0..=0xFDFF => { + self.dma.read_next_byte(&self.ppu, &self.memory, self.cartridge.as_deref()) + } 0xFE00..=0xFEFF => 0xFF, 0xFF00..=0xFF7F => self.cpu_read_io(address), 0xFF80..=0xFFFE => self.memory.hram[address as usize - 0xFF80], diff --git a/meowgb-core/src/gameboy/dma.rs b/meowgb-core/src/gameboy/dma.rs index dc69a77..1fa5c79 100644 --- a/meowgb-core/src/gameboy/dma.rs +++ b/meowgb-core/src/gameboy/dma.rs @@ -69,7 +69,12 @@ impl DmaState { self.restarting = Some((base, false)); } - pub fn read_next_byte(&self, ppu: &Ppu, memory: &Memory, cartridge: Option<&GenericCartridge>) -> u8 { + pub fn read_next_byte( + &self, + ppu: &Ppu, + memory: &Memory, + cartridge: Option<&GenericCartridge>, + ) -> u8 { let read_address = self.dma_in_progress.unwrap() as usize; match self.original_base { 0..=0x7F => match cartridge { @@ -83,7 +88,7 @@ impl DmaState { }, 0xC0..=0xDF => memory.wram[read_address - 0xC000], 0xE0..=0xFD => memory.wram[read_address - 0xE000], - 0xFE..=0xFF => 0xFF + 0xFE..=0xFF => 0xFF, } } @@ -103,9 +108,12 @@ impl DmaState { None => {} } - // We do not clear this after running because the "in progress" should remain the entire cycle + // We do not clear this after running because the "in progress" should remain + // the entire cycle self.dma_in_progress = match self.remaining_cycles > 0 { - true => Some(((self.original_base as u16) << 8) | (0xA0 - self.remaining_cycles) as u16), + true => { + Some(((self.original_base as u16) << 8) | (0xA0 - self.remaining_cycles) as u16) + } false => None, }; diff --git a/meowgb-core/src/gameboy/memory.rs b/meowgb-core/src/gameboy/memory.rs index 5a2e44f..befcd71 100644 --- a/meowgb-core/src/gameboy/memory.rs +++ b/meowgb-core/src/gameboy/memory.rs @@ -5,10 +5,7 @@ pub struct Memory { impl Memory { pub fn new() -> Self { - Self { - wram: [0; 0x2000], - hram: [0; 0xAF], - } + Self { wram: [0; 0x2000], hram: [0; 0xAF] } } pub fn get_bootrom_disabled(&self) -> u8 { diff --git a/meowgb-core/src/gameboy/ppu.rs b/meowgb-core/src/gameboy/ppu.rs index 680ace8..842459b 100644 --- a/meowgb-core/src/gameboy/ppu.rs +++ b/meowgb-core/src/gameboy/ppu.rs @@ -35,6 +35,8 @@ enum LineDrawingState { /// (original SCX, original SCY, drawn pixel count, window drawn, draw only /// sprites) BackgroundAndObjectFifo(u8, u8, u8, bool, bool), + /// (Cycles left) + WaitWindow(usize), Finished, } @@ -276,6 +278,7 @@ pub struct PpuRegisters { cycles_since_stat_mode_0: u64, cycles_since_last_last_mode_start_increment: [u64; 4], cycles_since_stat_mode_2: u64, + cycles_since_stat_mode_3: u64, pub lyc: u8, pub wy: u8, pub wx: u8, @@ -297,6 +300,8 @@ pub struct Ppu { current_dot: u16, dot_target: u16, + last_mode: Option, + sprite_buffer: [Option; 10], sprite_count: usize, @@ -313,6 +318,12 @@ pub struct Ppu { impl Ppu { pub fn stop(&mut self) { + assert_eq!( + self.mode(), + PPUMode::VBlank, + "PPU disabled outside of VBlank: {:?}", + self.mode() + ); self.current_dot = 0; self.dot_target = 0; self.sprite_buffer = [None; 10]; @@ -320,6 +331,7 @@ impl Ppu { self.current_draw_state = None; self.wy_match = false; self.registers.mode = PPUMode::HBlank; + self.last_mode = None; self.first_frame = true; self.first_line = true; self.total_dots = 0; @@ -347,6 +359,7 @@ impl Ppu { ly_lyc: true, cycles_since_stat_mode_0: 0, cycles_since_stat_mode_2: 0, + cycles_since_stat_mode_3: 0, }, vram: [0; 0x2000], oam: [0; 0xA0], @@ -357,6 +370,7 @@ impl Ppu { current_dot: 0, dot_target: 0, + last_mode: None, sprite_buffer: [None; 10], sprite_count: 0, current_draw_state: None, @@ -394,6 +408,8 @@ impl Ppu { self.registers.cycles_since_stat_mode_0 = 0; } else if stat_int && self.registers.mode == PPUMode::SearchingOAM { self.registers.cycles_since_stat_mode_2 = 0; + } else if stat_int && self.registers.mode == PPUMode::TransferringData { + self.registers.cycles_since_stat_mode_3 = 0; } interrupts.write_if_lcd_stat(true); } @@ -411,13 +427,9 @@ impl Ppu { } pub fn get_stat(&self) -> u8 { - let ly_eq_lyc = match self.enabled() { - true => self.registers.ly == self.registers.lyc, - false => self.registers.ly_lyc, - }; (1 << 7) | self.registers.stat_flags.flag_bits() - | ((ly_eq_lyc as u8) << 2) + | ((self.registers.ly_lyc as u8) << 2) | self.mode().mode_flag() } @@ -474,7 +486,7 @@ impl Ppu { pub fn cpu_read_oam(&self, address: u16) -> u8 { let decoded_address = address - 0xFE00; - if self.enabled() && !self.first_frame && !OVERRIDE_PPU_MEMORY_ACCESS { + if self.enabled() && !OVERRIDE_PPU_MEMORY_ACCESS { match self.mode() { PPUMode::HBlank | PPUMode::VBlank => self.oam[decoded_address as usize], PPUMode::SearchingOAM | PPUMode::TransferringData => 0xFF, @@ -486,7 +498,7 @@ impl Ppu { pub fn cpu_write_oam(&mut self, address: u16, value: u8) { let decoded_address = address - 0xFE00; - if self.enabled() && !self.first_frame && !OVERRIDE_PPU_MEMORY_ACCESS { + if self.enabled() && !OVERRIDE_PPU_MEMORY_ACCESS { match self.mode() { PPUMode::HBlank | PPUMode::VBlank => self.oam[decoded_address as usize] = value, PPUMode::SearchingOAM | PPUMode::TransferringData => {} @@ -528,10 +540,22 @@ impl Ppu { (self.registers.lcdc >> 7) == 1 } - pub fn set_mode(&mut self, interrupts: &mut Interrupts, mode: PPUMode) { + pub fn set_mode_next(&mut self, mode: PPUMode) { + self.last_mode = Some(self.mode()); + self.registers.mode = mode; + } + + pub fn set_mode_now(&mut self, interrupts: &mut Interrupts, mode: PPUMode) { + let last_mode = self.mode(); + self.registers.mode = mode; + self.update_mode(interrupts, last_mode) + } + + fn update_mode(&mut self, interrupts: &mut Interrupts, last_mode: PPUMode) { + let mode = self.mode(); self.registers.cycles_since_last_last_mode_start_increment[mode.mode_flag() as usize] = 0; if mode == PPUMode::HBlank { - assert_eq!(self.mode(), PPUMode::TransferringData); + assert_eq!(last_mode, PPUMode::TransferringData); assert!(self.current_dot >= 172); assert!(self.current_dot <= 289); self.dot_target = 376 - self.dot_target; @@ -539,9 +563,10 @@ impl Ppu { assert!(self.dot_target <= 204); } else if mode == PPUMode::TransferringData { if !self.first_frame { - assert_eq!(self.mode(), PPUMode::SearchingOAM); + assert_eq!(last_mode, PPUMode::SearchingOAM); + assert_eq!(self.current_dot, 80); } else if self.registers.ly == 0 { - assert_eq!(self.mode(), PPUMode::HBlank); + assert_eq!(last_mode, PPUMode::HBlank); } self.current_draw_state = None; self.dot_target = 160 + 12; @@ -585,6 +610,12 @@ impl Ppu { self.registers.cycles_since_last_ly_increment += 1; self.registers.cycles_since_stat_mode_0 += 1; self.registers.cycles_since_stat_mode_2 += 1; + self.registers.cycles_since_stat_mode_3 += 1; + + if let Some(mode) = self.last_mode.take() { + self.update_mode(interrupts, mode); + } + match self.mode() { PPUMode::SearchingOAM => { self.registers.cycles_since_last_last_mode_start_increment[2] += 1; @@ -597,7 +628,7 @@ impl Ppu { self.sprite_count = 0; } - if self.current_dot % 2 == 0 { + if !self.first_frame && self.current_dot % 2 == 0 { let oam_item_idx: usize = (self.current_dot as usize / 2) * 4; let oam_entry = OAMEntry::parse([ @@ -626,7 +657,7 @@ impl Ppu { self.total_dots += 1; if self.current_dot == 80 { - self.set_mode(interrupts, PPUMode::TransferringData); + self.set_mode_next(PPUMode::TransferringData); assert_eq!(self.total_dots, 80); } else { assert!(self.current_dot < 80); @@ -636,7 +667,7 @@ impl Ppu { } PPUMode::TransferringData => { self.registers.cycles_since_last_last_mode_start_increment[3] += 1; - if !self.first_frame && self.current_dot == 0 { + if !self.first_line && self.current_dot == 0 { assert_eq!(self.total_dots, 80); } // assert!(self.current_dot < self.dot_target); @@ -653,13 +684,24 @@ impl Ppu { self.current_dot += 1; self.total_dots += 1; - if self.current_dot == self.dot_target { + let left = self.current_dot == self.dot_target; + let right = matches!(self.current_draw_state, Some(LineDrawingState::Finished)); + + if left || right { + assert_eq!( + left, + right, + "{}/{} dots remaining in state {:?}", + self.dot_target - self.current_dot, + self.dot_target, + self.current_draw_state + ); // assert_eq!(self.total_dots, match self.first_frame && self.first_line { // true => self.dot_target, // false => 80 + self.dot_target // }); assert_eq!(self.current_draw_state, Some(LineDrawingState::Finished)); - self.set_mode(interrupts, PPUMode::HBlank); + self.set_mode_next(PPUMode::HBlank); } false @@ -671,18 +713,19 @@ impl Ppu { if !self.first_line { assert_ne!(self.dot_target, 0); } - if self.first_line && self.current_dot == 80 && self.dot_target == 0 { - self.set_mode(interrupts, PPUMode::TransferringData); + if self.first_line && self.current_dot == 76 && self.dot_target == 0 { + self.set_mode_next(PPUMode::TransferringData); } else if self.dot_target != 0 && self.current_dot == self.dot_target { self.set_scanline(interrupts, self.registers.ly + 1); assert_eq!( self.total_dots, - 456 /* match self.first_frame && self.first_line { - * true => 456 - (80 - 64), - * false => 456, - * } */ + match self.first_frame && self.first_line { + true => 456 - (80 - 76), + false => 456, + } ); + self.total_dots = 0; self.first_line = false; @@ -691,7 +734,7 @@ impl Ppu { false => PPUMode::SearchingOAM, }; - self.set_mode(interrupts, next_mode); + self.set_mode_next(next_mode); } false @@ -703,7 +746,7 @@ impl Ppu { if self.current_dot % 456 == 0 { if self.registers.ly >= 153 { self.set_scanline(interrupts, 0); - self.set_mode(interrupts, PPUMode::SearchingOAM); + self.set_mode_next(PPUMode::SearchingOAM); self.first_frame = false; true } else { @@ -789,120 +832,121 @@ impl Ppu { mut window_drawn, draw_only_sprites, ) => { - let wx_match = (drawn_pixels as usize + 7) >= self.registers.wx as usize; - let scrolled_y = self.registers.ly.wrapping_add(scy) as usize; - let scrolled_x = drawn_pixels.wrapping_add(scx) as usize; + if !self.first_frame { + let wx_match = (drawn_pixels as usize + 7) >= self.registers.wx as usize; + let scrolled_y = self.registers.ly.wrapping_add(scy) as usize; + let scrolled_x = drawn_pixels.wrapping_add(scx) as usize; - let (bg_color_id, bg_color) = match draw_only_sprites { - true => (0, Color::White), - false => { - let tilemap_idx = scrolled_x / 8 + ((scrolled_y / 8) * 32); - let tilemap_value = self.read_tile_map()[tilemap_idx]; - let (mut bg_color_id, mut bg_color) = Self::parse_tile_color( - self.read_bg_win_tile(tilemap_value), - scrolled_x % 8, - scrolled_y % 8, - &self.bgp, - ); - - if self.window_enabled() && wx_match && self.wy_match { - window_drawn = true; - let window_x = (drawn_pixels as u8) - .wrapping_sub(self.registers.wx.wrapping_sub(7)) - as usize; - let window_y = - self.registers.ly.wrapping_sub(self.registers.wy) as usize; - let tilemap_idx = window_x / 8 + ((self.window_counter / 8) * 32); - let tilemap_value = self.read_window_tile_map()[tilemap_idx]; - let (window_color_id, window_color) = Self::parse_tile_color( + let (bg_color_id, bg_color) = match draw_only_sprites { + true => (0, Color::White), + false => { + let tilemap_idx = scrolled_x / 8 + ((scrolled_y / 8) * 32); + let tilemap_value = self.read_tile_map()[tilemap_idx]; + let (mut bg_color_id, mut bg_color) = Self::parse_tile_color( self.read_bg_win_tile(tilemap_value), - window_x % 8, - window_y % 8, + scrolled_x % 8, + scrolled_y % 8, &self.bgp, ); - bg_color_id = window_color_id; - bg_color = window_color; + + if self.window_enabled() && wx_match && self.wy_match { + window_drawn = true; + let window_x = (drawn_pixels as u8) + .wrapping_sub(self.registers.wx.wrapping_sub(7)) + as usize; + let window_y = + self.registers.ly.wrapping_sub(self.registers.wy) as usize; + let tilemap_idx = window_x / 8 + ((self.window_counter / 8) * 32); + let tilemap_value = self.read_window_tile_map()[tilemap_idx]; + let (window_color_id, window_color) = Self::parse_tile_color( + self.read_bg_win_tile(tilemap_value), + window_x % 8, + window_y % 8, + &self.bgp, + ); + bg_color_id = window_color_id; + bg_color = window_color; + } + + (bg_color_id, bg_color) } + }; - (bg_color_id, bg_color) - } - }; - - let framebuffer_offset = ((self.registers.ly as usize * FB_WIDTH as usize) - + drawn_pixels as usize) - * PIXEL_SIZE; - for (idx, byte) in bg_color.rgba().iter().enumerate() { - self.framebuffer[framebuffer_offset + idx] = *byte; - } - - if (self.registers.lcdc >> 1) & 0b1 == 1 { - let mut sprite_buffer: Vec = Vec::new(); - for sprite_idx in 0..self.sprite_count { - // WARNING: Sprites are not scrolled, they have an absolute position! - let sprite = self.sprite_buffer[sprite_idx] - .as_ref() - .expect("within the sprite count there should be no `None`s"); - - let x_valid = - drawn_pixels < sprite.x && drawn_pixels.wrapping_add(8) >= sprite.x; - let y_valid = self.registers.ly < sprite.y - && self.registers.ly.wrapping_add(16) >= sprite.y; - - if !sprite_buffer.iter().any(|existing| existing.x == sprite.x) - && x_valid && y_valid - { - sprite_buffer.push(*sprite); - } + let framebuffer_offset = ((self.registers.ly as usize * FB_WIDTH as usize) + + drawn_pixels as usize) * PIXEL_SIZE; + for (idx, byte) in bg_color.rgba().iter().enumerate() { + self.framebuffer[framebuffer_offset + idx] = *byte; } - sprite_buffer.sort_by(|l, r| r.x.cmp(&l.x)); + if (self.registers.lcdc >> 1) & 0b1 == 1 { + let mut sprite_buffer: Vec = Vec::new(); + for sprite_idx in 0..self.sprite_count { + // WARNING: Sprites are not scrolled, they have an absolute position! + let sprite = self.sprite_buffer[sprite_idx] + .as_ref() + .expect("within the sprite count there should be no `None`s"); - // TODO: Adjust mode length based on sprites - for sprite in &sprite_buffer { - let mut sprite_x_idx = - drawn_pixels.wrapping_sub(sprite.x.wrapping_sub(8)) as usize; - let mut sprite_y_idx = - self.registers.ly.wrapping_sub(sprite.y.wrapping_sub(16)) as usize; + let x_valid = + drawn_pixels < sprite.x && drawn_pixels.wrapping_add(8) >= sprite.x; + let y_valid = self.registers.ly < sprite.y + && self.registers.ly.wrapping_add(16) >= sprite.y; - if sprite.y_flip() { - let sprite_offset = match self.sprite_height() { - SpriteHeight::Eight => 7, - SpriteHeight::Sixteen => 15, + if !sprite_buffer.iter().any(|existing| existing.x == sprite.x) + && x_valid && y_valid + { + sprite_buffer.push(*sprite); + } + } + + sprite_buffer.sort_by(|l, r| r.x.cmp(&l.x)); + + // TODO: Adjust mode length based on sprites + for sprite in &sprite_buffer { + let mut sprite_x_idx = + drawn_pixels.wrapping_sub(sprite.x.wrapping_sub(8)) as usize; + let mut sprite_y_idx = + self.registers.ly.wrapping_sub(sprite.y.wrapping_sub(16)) as usize; + + if sprite.y_flip() { + let sprite_offset = match self.sprite_height() { + SpriteHeight::Eight => 7, + SpriteHeight::Sixteen => 15, + }; + sprite_y_idx = sprite_offset - sprite_y_idx; + } + + if sprite.x_flip() { + sprite_x_idx = 7 - sprite_x_idx; + } + + let tile_idx = match self.sprite_height() { + SpriteHeight::Eight => sprite.tile_idx, + SpriteHeight::Sixteen => match sprite_y_idx >= 8 { + true => sprite.tile_idx | 1, + false => sprite.tile_idx & 0xFE, + }, }; - sprite_y_idx = sprite_offset - sprite_y_idx; - } - if sprite.x_flip() { - sprite_x_idx = 7 - sprite_x_idx; - } + if sprite_y_idx >= 8 { + sprite_y_idx -= 8; + assert!(sprite_y_idx < 8); + } - let tile_idx = match self.sprite_height() { - SpriteHeight::Eight => sprite.tile_idx, - SpriteHeight::Sixteen => match sprite_y_idx >= 8 { - true => sprite.tile_idx | 1, - false => sprite.tile_idx & 0xFE, - }, - }; + let color_id = + self.read_obj_tile_colour_id(tile_idx, sprite_x_idx, sprite_y_idx); + let palette = &self.obp[sprite.palette_number() as usize]; + let sprite_color = palette.color_from_2bit(color_id); - if sprite_y_idx >= 8 { - sprite_y_idx -= 8; - assert!(sprite_y_idx < 8); - } + let sprite_covered = sprite.covered_by_bg_window() && bg_color_id != 0; - let color_id = - self.read_obj_tile_colour_id(tile_idx, sprite_x_idx, sprite_y_idx); - let palette = &self.obp[sprite.palette_number() as usize]; - let sprite_color = palette.color_from_2bit(color_id); + if color_id != 0 && !sprite_covered { + let [r, g, b, a] = *sprite_color.rgba(); - let sprite_covered = sprite.covered_by_bg_window() && bg_color_id != 0; - - if color_id != 0 && !sprite_covered { - let [r, g, b, a] = *sprite_color.rgba(); - - self.sprite_framebuffer[framebuffer_offset + 0] = r; - self.sprite_framebuffer[framebuffer_offset + 1] = g; - self.sprite_framebuffer[framebuffer_offset + 2] = b; - self.sprite_framebuffer[framebuffer_offset + 3] = a; + self.sprite_framebuffer[framebuffer_offset + 0] = r; + self.sprite_framebuffer[framebuffer_offset + 1] = g; + self.sprite_framebuffer[framebuffer_offset + 2] = b; + self.sprite_framebuffer[framebuffer_offset + 3] = a; + } } } } @@ -912,8 +956,10 @@ impl Ppu { if window_drawn { self.window_counter += 1; self.dot_target += 6; + self.current_draw_state = Some(LineDrawingState::WaitWindow(5)); + } else { + self.current_draw_state = Some(LineDrawingState::Finished); } - self.current_draw_state = Some(LineDrawingState::Finished); } else { self.current_draw_state = Some(LineDrawingState::BackgroundAndObjectFifo( scx, @@ -924,6 +970,12 @@ impl Ppu { )); } } + LineDrawingState::WaitWindow(remaining) => { + self.current_draw_state = Some(match remaining { + 0 => LineDrawingState::Finished, + remaining => LineDrawingState::WaitWindow(remaining - 1), + }) + } LineDrawingState::Finished => unreachable!(), } } diff --git a/meowgb-core/src/lib.rs b/meowgb-core/src/lib.rs index 9983a80..4fca86a 100644 --- a/meowgb-core/src/lib.rs +++ b/meowgb-core/src/lib.rs @@ -12,7 +12,8 @@ pub fn setup_test_emulator( (&mut cartridge.rom[0x100..ROM_LENGTH + 0x100]).copy_from_slice(&test_opcodes); - let mut gameboy = gameboy::Gameboy::new_with_cartridge(std::io::stdout(), Some(Box::new(cartridge))); + let mut gameboy = + gameboy::Gameboy::new_with_cartridge(std::io::stdout(), Some(Box::new(cartridge))); gameboy.tick_4(); // Prefetch instruction assert!(gameboy.registers.mem_read_hold.is_some()); // Assert prefetch happened and opcode is now sitting in the memory bus diff --git a/meowgb-tests/expected_output/serial/mooneye-test-suite/ppu/intr_2_mode0_timing.bin b/meowgb-tests/expected_output/serial/mooneye-test-suite/ppu/intr_2_mode0_timing.bin new file mode 100644 index 0000000..2a02163 --- /dev/null +++ b/meowgb-tests/expected_output/serial/mooneye-test-suite/ppu/intr_2_mode0_timing.bin @@ -0,0 +1 @@ + " \ No newline at end of file diff --git a/meowgb-tests/expected_output/serial/mooneye-test-suite/ppu/intr_2_mode3_timing.bin b/meowgb-tests/expected_output/serial/mooneye-test-suite/ppu/intr_2_mode3_timing.bin new file mode 100644 index 0000000..2a02163 --- /dev/null +++ b/meowgb-tests/expected_output/serial/mooneye-test-suite/ppu/intr_2_mode3_timing.bin @@ -0,0 +1 @@ + " \ No newline at end of file diff --git a/meowgb-tests/expected_output/serial/mooneye-test-suite/ppu/intr_2_oam_ok_timing.bin b/meowgb-tests/expected_output/serial/mooneye-test-suite/ppu/intr_2_oam_ok_timing.bin new file mode 100644 index 0000000..2a02163 --- /dev/null +++ b/meowgb-tests/expected_output/serial/mooneye-test-suite/ppu/intr_2_oam_ok_timing.bin @@ -0,0 +1 @@ + " \ No newline at end of file diff --git a/meowgb-tests/src/main.rs b/meowgb-tests/src/main.rs index a6141e3..80f61f2 100644 --- a/meowgb-tests/src/main.rs +++ b/meowgb-tests/src/main.rs @@ -123,18 +123,15 @@ fn generate_output( drop(gameboy); if FRAMEBUFFER { - std::fs::write(expected, &fb.unwrap()) - .map_err(DmgTestError::OutputFileWrite)?; + std::fs::write(expected, &fb.unwrap()).map_err(DmgTestError::OutputFileWrite)?; } else { let serial_content = sync_writer.into_inner(); - std::fs::write(expected, &serial_content) - .map_err(DmgTestError::OutputFileWrite)?; + std::fs::write(expected, &serial_content).map_err(DmgTestError::OutputFileWrite)?; } Ok(instant.elapsed()) } - fn run_test( rom: &Path, maximum_m_cycles: u64, @@ -155,7 +152,12 @@ fn run_test( }; if FRAMEBUFFER { - assert_eq!(expected.len(), (meowgb_core::gameboy::ppu::FB_WIDTH as usize * meowgb_core::gameboy::ppu::FB_HEIGHT as usize) * meowgb_core::gameboy::ppu::PIXEL_SIZE as usize); + assert_eq!( + expected.len(), + (meowgb_core::gameboy::ppu::FB_WIDTH as usize + * meowgb_core::gameboy::ppu::FB_HEIGHT as usize) + * meowgb_core::gameboy::ppu::PIXEL_SIZE as usize + ); } let sync_writer = SyncWriter::new(); @@ -202,7 +204,8 @@ fn main() { match args.operation { Operation::TestSerial { maximum_m_cycles, expected_serial } => { - match run_test::(args.rom.as_path(), maximum_m_cycles, expected_serial.as_path()) { + match run_test::(args.rom.as_path(), maximum_m_cycles, expected_serial.as_path()) + { Ok((m_cycles, duration)) => { println!("Success! Ran {} M-Cycles in {}ms", m_cycles, duration.as_millis()); } @@ -213,7 +216,8 @@ fn main() { } } Operation::GenerateOutputSerial { m_cycles, expected_serial } => { - match generate_output::(args.rom.as_path(), m_cycles, expected_serial.as_path()) { + match generate_output::(args.rom.as_path(), m_cycles, expected_serial.as_path()) + { Ok(duration) => { println!("Successfully written serial output to {} in {} M-Cycles ({}ms), please verify it is correct", expected_serial.display(), m_cycles, duration.as_millis()); } @@ -224,7 +228,11 @@ fn main() { } } Operation::TestFramebuffer { maximum_m_cycles, expected_framebuffer } => { - match run_test::(args.rom.as_path(), maximum_m_cycles, expected_framebuffer.as_path()) { + match run_test::( + args.rom.as_path(), + maximum_m_cycles, + expected_framebuffer.as_path(), + ) { Ok((m_cycles, duration)) => { println!("Success! Ran {} M-Cycles in {}ms", m_cycles, duration.as_millis()); } @@ -233,9 +241,13 @@ fn main() { std::process::exit(1); } } - }, - Operation::GenerateOutputFramebuffer { m_cycles, expected_framebuffer } => { - match generate_output::(args.rom.as_path(), m_cycles, expected_framebuffer.as_path()) { + } + Operation::GenerateOutputFramebuffer { m_cycles, expected_framebuffer } => { + match generate_output::( + args.rom.as_path(), + m_cycles, + expected_framebuffer.as_path(), + ) { Ok(duration) => { println!("Successfully written framebuffer output to {} in {} M-Cycles ({}ms), please verify it is correct", expected_framebuffer.display(), m_cycles, duration.as_millis()); } @@ -244,6 +256,6 @@ fn main() { std::process::exit(1); } } - }, + } } } diff --git a/meowgb/src/main.rs b/meowgb/src/main.rs index 3c5dd82..bf01bcb 100644 --- a/meowgb/src/main.rs +++ b/meowgb/src/main.rs @@ -12,10 +12,7 @@ use std::{ use clap::Parser; use config::MeowGBConfig; -use meowgb_core::gameboy::{ - serial::SerialWriter, - Gameboy, -}; +use meowgb_core::gameboy::{serial::SerialWriter, Gameboy}; use window::events::{EmulatorDebugEvent, EmulatorWindowEvent, GameboyEvent}; #[cfg(feature = "debugger")] @@ -67,10 +64,10 @@ fn real_main() -> Result<(), MeowGBError> { if !rom.is_file() { return Err(MeowGBError::GameNotFound); } - + Some(std::fs::read(rom)?) - }, - None => None + } + None => None, }; let mut gameboy = WrappedGameboy::new(Gameboy::new(std::io::stdout(), rom)); diff --git a/run-test-roms.sh b/run-test-roms.sh index ad893c5..ea74c25 100755 --- a/run-test-roms.sh +++ b/run-test-roms.sh @@ -557,6 +557,36 @@ else echo "Failed: $res" fi +echo "Running test ROM ./test-roms/mooneye-test-suite/serial-roms/ppu/intr_2_mode0_timing.gb" + +TEST_TOTAL=$((TEST_TOTAL + 1)) + +if res=$(./target/release/meowgb-tests test-roms/mooneye-test-suite/serial-roms/ppu/intr_2_mode0_timing.gb test-serial -m 100000000 -s meowgb-tests/expected_output/serial/mooneye-test-suite/ppu/intr_2_mode0_timing.bin 2>&1 > /dev/null) ; then + TEST_SUCCESS=$((TEST_SUCCESS + 1)) +else + echo "Failed: $res" +fi + +echo "Running test ROM ./test-roms/mooneye-test-suite/serial-roms/ppu/intr_2_mode3_timing.gb" + +TEST_TOTAL=$((TEST_TOTAL + 1)) + +if res=$(./target/release/meowgb-tests test-roms/mooneye-test-suite/serial-roms/ppu/intr_2_mode3_timing.gb test-serial -m 100000000 -s meowgb-tests/expected_output/serial/mooneye-test-suite/ppu/intr_2_mode3_timing.bin 2>&1 > /dev/null) ; then + TEST_SUCCESS=$((TEST_SUCCESS + 1)) +else + echo "Failed: $res" +fi + +echo "Running test ROM ./test-roms/mooneye-test-suite/serial-roms/ppu/intr_2_oam_ok_timing.gb" + +TEST_TOTAL=$((TEST_TOTAL + 1)) + +if res=$(./target/release/meowgb-tests test-roms/mooneye-test-suite/serial-roms/ppu/intr_2_oam_ok_timing.gb test-serial -m 100000000 -s meowgb-tests/expected_output/serial/mooneye-test-suite/ppu/intr_2_oam_ok_timing.bin 2>&1 > /dev/null) ; then + TEST_SUCCESS=$((TEST_SUCCESS + 1)) +else + echo "Failed: $res" +fi + echo "Running test ROM ./test-roms/mooneye-test-suite/serial-roms/ppu/stat_irq_blocking.gb" TEST_TOTAL=$((TEST_TOTAL + 1)) diff --git a/test-roms/mooneye-test-suite/serial-roms/ppu/intr_2_mode0_timing.gb b/test-roms/mooneye-test-suite/serial-roms/ppu/intr_2_mode0_timing.gb new file mode 100644 index 0000000000000000000000000000000000000000..6a05419bcbfbf564645cabda348e4eb1d8800f9e GIT binary patch literal 32768 zcmeI4U1$_n6oBtewsA~ioEQz0bgHw<64o?Ii~*N)JDr`3agzybum(#SFio07X`2KS zN{pK|skKeC{yape5U6iLUqYc&L=eiN&9ZLsr9werUJQL`Wr|3xE!o~PH@BOp4_gqy zat@iFbI;E`_nUJAdC1LXpO5?^q@GWr7o3G8+dV)2*57|~9VsptA;(DxSx3aOEz>ik z^v=z}#k+TZzHs&GK<{AbuQ%`Bx$xE1D?7E1hK2_Fj`VFC=ut=eMn*|zC!ylmNw(eI zw}aS@xpY>2Oz-E^$G%8b6YOk862K*YDJ0&+VuAX&{l7-?z;_<(n9kn&>H0%j(7Ziu zpR{M}FYHtH8T+(-&OWPo>@&XUYHNZUSAVZ59z@Pa!Ir$Yir00|%gB!C2v z01`j~NB{{S0VIF~kN^@u0!RP}AOR$R1dsp{Kmter2_OL^fCP{L5{^GsQ>?XJb46x zxGaQ-BG`gdR{I2l@i@C+TogBN4h9vas3;tUi0t|6Df=HV60zgVAU_&hjS`x*m@U z0=I|5pu-HzKap5HKjedp+u6IO2|{iSEOTzXTxWy|3uU>xTb3Dbs2M6{)q=)s-~(DH zWSX!ZLRd7c&@|`fW@o#)R8>)ADuDp20{q9uOcO4+^Y}?BrWp!xJ6O3r9@q7;F`Y_0 z4)&bRc*FdYlXG)C9#)=6z}+ywpM6>2uW6tw3Uq_l%l%C=8f|YM9*#y$6YMY(6Sm?= zq_?-DqopN61?+~=)3bMPYin~e%daT@Wntf!t&e2@|CM%u9@4?=?d^O%uuD=&iQmuS zj~#t}4{*4=R`Evcj))xxFZUJHZ zb)D@UuNSI?@9V~{U0clTlJw;M1uqbmkMBD+2KR7S5Hzj1xv>#6AOro|)6>-jItbg- zZ2bZiO*0Jc&vp##0^Yx}EJ>k|M1@5Y($LV>W||ERUN2isCtSB7vDM2m)XsEC3WuSB-25hn+W+36 z5BuIHsrmiR^Z^<|d8M0zTOpXxTcb*xshD%}O0XpJEu0kNQ^`ho6Mcz+t6WUq05TPl zw%oX89yb|tX7Y(+)j)!`-2?_ta6IOGkxzp3yKY<)kNYI%oXRIIH*v3s9BZ9ML?m~%Ftxa4_@o+RYi-Fp3A z=9U#&n>k=*-mo$=mXCayf|1vJdN#eZ^6aCP@8g%$(!zq$S1QEK>#8?x_sgPY?1}9^ zFfgQNzP3VDW0^tg2))MXb(CIldQH$v-Y{l+j@X`Y+jG?R#BI-n?a}3>a<_RRw8yGw zbG_ literal 0 HcmV?d00001 diff --git a/test-roms/mooneye-test-suite/serial-roms/ppu/intr_2_mode3_timing.gb b/test-roms/mooneye-test-suite/serial-roms/ppu/intr_2_mode3_timing.gb new file mode 100644 index 0000000000000000000000000000000000000000..96c5ff6587be4223e8d41a2ec39bca5e0f0b08bd GIT binary patch literal 32768 zcmeI4UuYCZ7{F&Y*SIDzZj6Ray4Aho2xl54#(+n9-QI21c*zDXSc4@Em?ll4v`vBu zCC1B{)Y>Nge^96psBc1FLP07b2<6b`IIs9pp`fsRF!<2Q7Li(8a-DA{)4SA{JlH_X z_u=mCH{ZXR-+UwZ;AFEeM;?i(my>9fvyf!F@cl3S{dd-p;(`%!oRpAtL@L`dJwrlyr6yD#A{(t@i2; zVms#IIqf-ppVyvyBUxRvvzeO$^(Z8s#6rPhf%dH39mwhLJlQdwefYzzr!;)dp0-cg zGxllwlzrAdW1qLr>4JUMJDp80&|GKW?6C*g_rkw85&7p=@at2xrr)~H1zZTm=t@!#CARqhWghXjxS5 zAd)1@;33Ow9DY4iU>xi%F3a2RZg z;`bX={C-6NU6K?9a!Zn`f*uMPhR35QnwD_;pX-LfYJh(r5RJy;EPo;q4#(r7$nBvJ z=umdRIXOGa<6-5A1iTFc{MpU|e_aP%RiPU^9`0|N(P(@7@NhJ0nqY@YOxTJek>1{p zj+T}P6|fsdPtV@Ht*yrY%Oxc~AB#VB4Ccy& z)njYn`#CiQx}OT`FPK+LOGigle1ZU=U6V2;(0Pv$Nyz zz-(cE-PpBjizmibajCa!p<~Xzeq*b z4TJl$69cD!_phSJaxf@UVbO#%G_hKM+eS3X9Y&z4}4Eg@RSXWn1k6}QkhQpvYHbS?+ z85CWnwYBy2P#0`$^@;*>XSythLNJ3|eW+DkT~)>G(1W?ZUtnyCN@r((f6fj)kgu~I zkY#pe;cr%`0{*Hh%XL&_nepeWc*ns0mr#);K0k=6uWxIEU)c4WfL=D4Wmm!M?Hf2W zGTL`|gn&-}d;@pHIMdex+vr6xk&%(U!=s0W2LCo2(eZ164J>vaz4b=xfA7$TeeaXh z+#c{GyH;>* z=75!X!^+H9Uh+i>MqcxVv+2eByN~j}$FFFmg$1RrR7jiGRd3qvQzYHk6Wf1aU?`mV z$_iGEWd^My^cknmQToK`GeI9^!d>keGw9%%SN;2XmRj?iU@R%N*$4Np3;6L~719>?Yf?TlF$Ktp926NnW;!3a-pA z76l4?%2PTE#Q`XZd3G06`ew=&^eJ1MdlOEg_Iqk!{bEs2yuIdjoiF8;C(B4!aMmPm zS`!Of9&ZVq7`p3EdC8YjVOG`+C00S6;0Awx52D z`-+ukj(P9$Td8j|G3yrI@3dO~h{8HZ00|%gB!C2v01`j~NB{{S0VIF~kN^@u0!RP} zAOR$R1dsp{Kmter2_OL^fCP{L5r{0JVH7;2o=wEvgO;C zwh`Mg7tg8>>HVDgFn!UgB#WA0XVRqtc;znn#2a78s1MuzX(SJP{ob~z%$@J9-={Ur z*;Dojd)hu_pR~`|r|on0SDIU{jv@1ImmB-_7Hw^G+Ul#al8t94w-Qe|df76Ua+S-POA`#OBJIus{ ztvDR+>1l6oZVpocyJ2*9@7dGR($vKAD~fMP*!N}YV;R7Ixm}1guXaEh!K>v1kcXomf z!uB*+9$M>hKP-b!%N6d~~L<3iFqx_Jkon;n1D`yzJTLgoT?>YTU|C`w<0V`3^*vR>XmU!veD7t%L? zOogN@H?E1tO+=mPT;f#tn@TJ-ss~bjJ!~(XHtvH&pulIK6Xhh&d)1;u1wsxrfS1hpDb#| z?&!Y#{eyb?b1PUenjWwY(`$@gN9Yx!*Eqf8b)&ZDui1pWeu_uLu) literal 0 HcmV?d00001 diff --git a/tests.md b/tests.md index 56fbda9..529443a 100644 --- a/tests.md +++ b/tests.md @@ -75,6 +75,9 @@ * ppu/intr_1_2_timing-GS.gb - [ROM](./test-roms/mooneye-test-suite/serial-roms/ppu/intr_1_2_timing-GS.gb) - [Expected Serial Output](./meowgb-tests/expected_output/serial/mooneye-test-suite/ppu/intr_1_2_timing-GS.bin) * ppu/intr_2_0_timing.gb - [ROM](./test-roms/mooneye-test-suite/serial-roms/ppu/intr_2_0_timing.gb) - [Expected Serial Output](./meowgb-tests/expected_output/serial/mooneye-test-suite/ppu/intr_2_0_timing.bin) +* ppu/intr_2_mode0_timing.gb - [ROM](./test-roms/mooneye-test-suite/serial-roms/ppu/intr_2_mode0_timing.gb) - [Expected Serial Output](./meowgb-tests/expected_output/serial/mooneye-test-suite/ppu/intr_2_mode0_timing.bin) +* ppu/intr_2_mode3_timing.gb - [ROM](./test-roms/mooneye-test-suite/serial-roms/ppu/intr_2_mode3_timing.gb) - [Expected Serial Output](./meowgb-tests/expected_output/serial/mooneye-test-suite/ppu/intr_2_mode3_timing.bin) +* ppu/intr_2_oam_ok_timing.gb - [ROM](./test-roms/mooneye-test-suite/serial-roms/ppu/intr_2_oam_ok_timing.gb) - [Expected Serial Output](./meowgb-tests/expected_output/serial/mooneye-test-suite/ppu/intr_2_oam_ok_timing.bin) * ppu/stat_irq_blocking.gb - [ROM](./test-roms/mooneye-test-suite/serial-roms/ppu/stat_irq_blocking.gb) - [Expected Serial Output](./meowgb-tests/expected_output/serial/mooneye-test-suite/ppu/stat_irq_blocking.bin) * ppu/stat_lyc_onoff.gb - [ROM](./test-roms/mooneye-test-suite/serial-roms/ppu/stat_lyc_onoff.gb) - [Expected Serial Output](./meowgb-tests/expected_output/serial/mooneye-test-suite/ppu/stat_lyc_onoff.bin) * ppu/vblank_stat_intr-GS.gb - [ROM](./test-roms/mooneye-test-suite/serial-roms/ppu/vblank_stat_intr-GS.gb) - [Expected Serial Output](./meowgb-tests/expected_output/serial/mooneye-test-suite/ppu/vblank_stat_intr-GS.bin)