fix: PPU timings are more accurate

This commit is contained in:
EliseZeroTwo 2024-01-25 10:41:36 -07:00
parent c6dd3a7bd5
commit 8e47afa1bc
Signed by: elise
GPG key ID: FA8F56FFFE6E8B3E
17 changed files with 300 additions and 175 deletions

View file

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

View file

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

View file

@ -104,13 +104,11 @@ impl<S: SerialWriter> Gameboy<S> {
}
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<S: SerialWriter> Gameboy<S> {
}
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<S: SerialWriter> Gameboy<S> {
// 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<S: SerialWriter> Gameboy<S> {
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<S: SerialWriter> Gameboy<S> {
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<S: SerialWriter> Gameboy<S> {
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<S: SerialWriter> Gameboy<S> {
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],

View file

@ -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,
};

View file

@ -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 {

View file

@ -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<PPUMode>,
sprite_buffer: [Option<OAMEntry>; 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<OAMEntry> = 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<OAMEntry> = 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!(),
}
}

View file

@ -12,7 +12,8 @@ pub fn setup_test_emulator<const ROM_LENGTH: usize>(
(&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

View file

@ -123,18 +123,15 @@ fn generate_output<const FRAMEBUFFER: bool>(
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<const FRAMEBUFFER: bool>(
rom: &Path,
maximum_m_cycles: u64,
@ -155,7 +152,12 @@ fn run_test<const FRAMEBUFFER: bool>(
};
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::<false>(args.rom.as_path(), maximum_m_cycles, expected_serial.as_path()) {
match run_test::<false>(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::<false>(args.rom.as_path(), m_cycles, expected_serial.as_path()) {
match generate_output::<false>(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::<true>(args.rom.as_path(), maximum_m_cycles, expected_framebuffer.as_path()) {
match run_test::<true>(
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::<true>(args.rom.as_path(), m_cycles, expected_framebuffer.as_path()) {
}
Operation::GenerateOutputFramebuffer { m_cycles, expected_framebuffer } => {
match generate_output::<true>(
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);
}
}
},
}
}
}

View file

@ -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));

View file

@ -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))

View file

@ -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)