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() 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 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) - name: Run test ROM (mooneye-test-suite ppu/stat_irq_blocking)
if: always() 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 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() 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 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) - name: Run test ROM (mooneye-test-suite ppu/stat_irq_blocking)
if: always() 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 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 { macro_rules! pop8 {
() => { () => {{
{ let res = self.debug_read_u8(self.registers.sp);
let res = self.debug_read_u8(self.registers.sp); self.registers.sp = self.registers.sp.overflowing_add(1).0;
self.registers.sp = self.registers.sp.overflowing_add(1).0; res
res }};
}
};
} }
macro_rules! pop16 { macro_rules! pop16 {
@ -120,22 +118,20 @@ impl<S: SerialWriter> Gameboy<S> {
} }
macro_rules! rl { macro_rules! rl {
($reg:ident) => { ($reg:ident) => {{
{ let new_carry = self.registers.$reg >> 7 == 1;
let new_carry = self.registers.$reg >> 7 == 1; self.registers.$reg <<= 1;
self.registers.$reg <<= 1;
if self.registers.get_carry() { if self.registers.get_carry() {
self.registers.$reg |= 1; self.registers.$reg |= 1;
}
self.registers.set_carry(new_carry);
} }
};
self.registers.set_carry(new_carry);
}};
} }
self.registers.sp = 0xFFFE; self.registers.sp = 0xFFFE;
// Clear VRAM // Clear VRAM
self.registers.a = 0; self.registers.a = 0;
let mut address = 0x9FFF; let mut address = 0x9FFF;
@ -160,7 +156,7 @@ impl<S: SerialWriter> Gameboy<S> {
// self.registers.set_hl(hl_content - 1); // self.registers.set_hl(hl_content - 1);
// self.registers.a = 0x77; // self.registers.a = 0x77;
// self.debug_write_u8(hl_content, self.registers.a); // self.debug_write_u8(hl_content, self.registers.a);
// Configure Background Palette // Configure Background Palette
self.registers.a = 0xFC; self.registers.a = 0xFC;
self.debug_write_u8(0xFF47, self.registers.a); self.debug_write_u8(0xFF47, self.registers.a);
@ -171,7 +167,7 @@ impl<S: SerialWriter> Gameboy<S> {
loop { loop {
self.registers.a = self.debug_read_u8(self.registers.get_de()); self.registers.a = self.debug_read_u8(self.registers.get_de());
self.registers.c = self.registers.a; self.registers.c = self.registers.a;
for _ in 0..2 { for _ in 0..2 {
self.registers.b = 4; 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.debug_write_u8(self.registers.get_hl(), self.registers.a);
self.registers.set_hl(self.registers.get_hl() + 2); self.registers.set_hl(self.registers.get_hl() + 2);
} }
self.registers.set_de(self.registers.get_de() + 1); self.registers.set_de(self.registers.get_de() + 1);
self.registers.a = self.registers.e; self.registers.a = self.registers.e;
@ -394,7 +390,7 @@ impl<S: SerialWriter> Gameboy<S> {
0xFF4B => self.ppu.registers.wx = value, 0xFF4B => self.ppu.registers.wx = value,
0xFF4C..=0xFF4E => {} // Unused 0xFF4C..=0xFF4E => {} // Unused
0xFF4F => {} // CGB VRAM Bank Select 0xFF4F => {} // CGB VRAM Bank Select
0xFF50 => {} // BROM lockout 0xFF50 => {} // BROM lockout
0xFF51..=0xFF55 => {} // CGB VRAM DMA 0xFF51..=0xFF55 => {} // CGB VRAM DMA
0xFF56..=0xFF67 => {} // Unused 0xFF56..=0xFF67 => {} // Unused
0xFF68..=0xFF69 => {} // CGB BG/OBJ Palettes 0xFF68..=0xFF69 => {} // CGB BG/OBJ Palettes
@ -474,7 +470,9 @@ impl<S: SerialWriter> Gameboy<S> {
self.registers.mem_op_happened = true; self.registers.mem_op_happened = true;
let value = match self.dma.is_conflict(address) { let value = match self.dma.is_conflict(address) {
true => match 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, 0xFE00..=0xFEFF => 0xFF,
0xFF00..=0xFF7F => self.cpu_read_io(address), 0xFF00..=0xFF7F => self.cpu_read_io(address),
0xFF80..=0xFFFE => self.memory.hram[address as usize - 0xFF80], 0xFF80..=0xFFFE => self.memory.hram[address as usize - 0xFF80],

View file

@ -69,7 +69,12 @@ impl DmaState {
self.restarting = Some((base, false)); 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; let read_address = self.dma_in_progress.unwrap() as usize;
match self.original_base { match self.original_base {
0..=0x7F => match cartridge { 0..=0x7F => match cartridge {
@ -83,7 +88,7 @@ impl DmaState {
}, },
0xC0..=0xDF => memory.wram[read_address - 0xC000], 0xC0..=0xDF => memory.wram[read_address - 0xC000],
0xE0..=0xFD => memory.wram[read_address - 0xE000], 0xE0..=0xFD => memory.wram[read_address - 0xE000],
0xFE..=0xFF => 0xFF 0xFE..=0xFF => 0xFF,
} }
} }
@ -103,9 +108,12 @@ impl DmaState {
None => {} 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 { 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, false => None,
}; };

View file

@ -5,10 +5,7 @@ pub struct Memory {
impl Memory { impl Memory {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self { wram: [0; 0x2000], hram: [0; 0xAF] }
wram: [0; 0x2000],
hram: [0; 0xAF],
}
} }
pub fn get_bootrom_disabled(&self) -> u8 { 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 /// (original SCX, original SCY, drawn pixel count, window drawn, draw only
/// sprites) /// sprites)
BackgroundAndObjectFifo(u8, u8, u8, bool, bool), BackgroundAndObjectFifo(u8, u8, u8, bool, bool),
/// (Cycles left)
WaitWindow(usize),
Finished, Finished,
} }
@ -276,6 +278,7 @@ pub struct PpuRegisters {
cycles_since_stat_mode_0: u64, cycles_since_stat_mode_0: u64,
cycles_since_last_last_mode_start_increment: [u64; 4], cycles_since_last_last_mode_start_increment: [u64; 4],
cycles_since_stat_mode_2: u64, cycles_since_stat_mode_2: u64,
cycles_since_stat_mode_3: u64,
pub lyc: u8, pub lyc: u8,
pub wy: u8, pub wy: u8,
pub wx: u8, pub wx: u8,
@ -297,6 +300,8 @@ pub struct Ppu {
current_dot: u16, current_dot: u16,
dot_target: u16, dot_target: u16,
last_mode: Option<PPUMode>,
sprite_buffer: [Option<OAMEntry>; 10], sprite_buffer: [Option<OAMEntry>; 10],
sprite_count: usize, sprite_count: usize,
@ -313,6 +318,12 @@ pub struct Ppu {
impl Ppu { impl Ppu {
pub fn stop(&mut self) { pub fn stop(&mut self) {
assert_eq!(
self.mode(),
PPUMode::VBlank,
"PPU disabled outside of VBlank: {:?}",
self.mode()
);
self.current_dot = 0; self.current_dot = 0;
self.dot_target = 0; self.dot_target = 0;
self.sprite_buffer = [None; 10]; self.sprite_buffer = [None; 10];
@ -320,6 +331,7 @@ impl Ppu {
self.current_draw_state = None; self.current_draw_state = None;
self.wy_match = false; self.wy_match = false;
self.registers.mode = PPUMode::HBlank; self.registers.mode = PPUMode::HBlank;
self.last_mode = None;
self.first_frame = true; self.first_frame = true;
self.first_line = true; self.first_line = true;
self.total_dots = 0; self.total_dots = 0;
@ -347,6 +359,7 @@ impl Ppu {
ly_lyc: true, ly_lyc: true,
cycles_since_stat_mode_0: 0, cycles_since_stat_mode_0: 0,
cycles_since_stat_mode_2: 0, cycles_since_stat_mode_2: 0,
cycles_since_stat_mode_3: 0,
}, },
vram: [0; 0x2000], vram: [0; 0x2000],
oam: [0; 0xA0], oam: [0; 0xA0],
@ -357,6 +370,7 @@ impl Ppu {
current_dot: 0, current_dot: 0,
dot_target: 0, dot_target: 0,
last_mode: None,
sprite_buffer: [None; 10], sprite_buffer: [None; 10],
sprite_count: 0, sprite_count: 0,
current_draw_state: None, current_draw_state: None,
@ -394,6 +408,8 @@ impl Ppu {
self.registers.cycles_since_stat_mode_0 = 0; self.registers.cycles_since_stat_mode_0 = 0;
} else if stat_int && self.registers.mode == PPUMode::SearchingOAM { } else if stat_int && self.registers.mode == PPUMode::SearchingOAM {
self.registers.cycles_since_stat_mode_2 = 0; 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); interrupts.write_if_lcd_stat(true);
} }
@ -411,13 +427,9 @@ impl Ppu {
} }
pub fn get_stat(&self) -> u8 { 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) (1 << 7)
| self.registers.stat_flags.flag_bits() | self.registers.stat_flags.flag_bits()
| ((ly_eq_lyc as u8) << 2) | ((self.registers.ly_lyc as u8) << 2)
| self.mode().mode_flag() | self.mode().mode_flag()
} }
@ -474,7 +486,7 @@ impl Ppu {
pub fn cpu_read_oam(&self, address: u16) -> u8 { pub fn cpu_read_oam(&self, address: u16) -> u8 {
let decoded_address = address - 0xFE00; 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() { match self.mode() {
PPUMode::HBlank | PPUMode::VBlank => self.oam[decoded_address as usize], PPUMode::HBlank | PPUMode::VBlank => self.oam[decoded_address as usize],
PPUMode::SearchingOAM | PPUMode::TransferringData => 0xFF, PPUMode::SearchingOAM | PPUMode::TransferringData => 0xFF,
@ -486,7 +498,7 @@ impl Ppu {
pub fn cpu_write_oam(&mut self, address: u16, value: u8) { pub fn cpu_write_oam(&mut self, address: u16, value: u8) {
let decoded_address = address - 0xFE00; 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() { match self.mode() {
PPUMode::HBlank | PPUMode::VBlank => self.oam[decoded_address as usize] = value, PPUMode::HBlank | PPUMode::VBlank => self.oam[decoded_address as usize] = value,
PPUMode::SearchingOAM | PPUMode::TransferringData => {} PPUMode::SearchingOAM | PPUMode::TransferringData => {}
@ -528,10 +540,22 @@ impl Ppu {
(self.registers.lcdc >> 7) == 1 (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; self.registers.cycles_since_last_last_mode_start_increment[mode.mode_flag() as usize] = 0;
if mode == PPUMode::HBlank { 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 >= 172);
assert!(self.current_dot <= 289); assert!(self.current_dot <= 289);
self.dot_target = 376 - self.dot_target; self.dot_target = 376 - self.dot_target;
@ -539,9 +563,10 @@ impl Ppu {
assert!(self.dot_target <= 204); assert!(self.dot_target <= 204);
} else if mode == PPUMode::TransferringData { } else if mode == PPUMode::TransferringData {
if !self.first_frame { 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 { } else if self.registers.ly == 0 {
assert_eq!(self.mode(), PPUMode::HBlank); assert_eq!(last_mode, PPUMode::HBlank);
} }
self.current_draw_state = None; self.current_draw_state = None;
self.dot_target = 160 + 12; self.dot_target = 160 + 12;
@ -585,6 +610,12 @@ impl Ppu {
self.registers.cycles_since_last_ly_increment += 1; self.registers.cycles_since_last_ly_increment += 1;
self.registers.cycles_since_stat_mode_0 += 1; self.registers.cycles_since_stat_mode_0 += 1;
self.registers.cycles_since_stat_mode_2 += 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() { match self.mode() {
PPUMode::SearchingOAM => { PPUMode::SearchingOAM => {
self.registers.cycles_since_last_last_mode_start_increment[2] += 1; self.registers.cycles_since_last_last_mode_start_increment[2] += 1;
@ -597,7 +628,7 @@ impl Ppu {
self.sprite_count = 0; 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_item_idx: usize = (self.current_dot as usize / 2) * 4;
let oam_entry = OAMEntry::parse([ let oam_entry = OAMEntry::parse([
@ -626,7 +657,7 @@ impl Ppu {
self.total_dots += 1; self.total_dots += 1;
if self.current_dot == 80 { if self.current_dot == 80 {
self.set_mode(interrupts, PPUMode::TransferringData); self.set_mode_next(PPUMode::TransferringData);
assert_eq!(self.total_dots, 80); assert_eq!(self.total_dots, 80);
} else { } else {
assert!(self.current_dot < 80); assert!(self.current_dot < 80);
@ -636,7 +667,7 @@ impl Ppu {
} }
PPUMode::TransferringData => { PPUMode::TransferringData => {
self.registers.cycles_since_last_last_mode_start_increment[3] += 1; 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_eq!(self.total_dots, 80);
} }
// assert!(self.current_dot < self.dot_target); // assert!(self.current_dot < self.dot_target);
@ -653,13 +684,24 @@ impl Ppu {
self.current_dot += 1; self.current_dot += 1;
self.total_dots += 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 { // assert_eq!(self.total_dots, match self.first_frame && self.first_line {
// true => self.dot_target, // true => self.dot_target,
// false => 80 + self.dot_target // false => 80 + self.dot_target
// }); // });
assert_eq!(self.current_draw_state, Some(LineDrawingState::Finished)); assert_eq!(self.current_draw_state, Some(LineDrawingState::Finished));
self.set_mode(interrupts, PPUMode::HBlank); self.set_mode_next(PPUMode::HBlank);
} }
false false
@ -671,18 +713,19 @@ impl Ppu {
if !self.first_line { if !self.first_line {
assert_ne!(self.dot_target, 0); assert_ne!(self.dot_target, 0);
} }
if self.first_line && self.current_dot == 80 && self.dot_target == 0 { if self.first_line && self.current_dot == 76 && self.dot_target == 0 {
self.set_mode(interrupts, PPUMode::TransferringData); self.set_mode_next(PPUMode::TransferringData);
} else if self.dot_target != 0 && self.current_dot == self.dot_target { } else if self.dot_target != 0 && self.current_dot == self.dot_target {
self.set_scanline(interrupts, self.registers.ly + 1); self.set_scanline(interrupts, self.registers.ly + 1);
assert_eq!( assert_eq!(
self.total_dots, self.total_dots,
456 /* match self.first_frame && self.first_line { match self.first_frame && self.first_line {
* true => 456 - (80 - 64), true => 456 - (80 - 76),
* false => 456, false => 456,
* } */ }
); );
self.total_dots = 0; self.total_dots = 0;
self.first_line = false; self.first_line = false;
@ -691,7 +734,7 @@ impl Ppu {
false => PPUMode::SearchingOAM, false => PPUMode::SearchingOAM,
}; };
self.set_mode(interrupts, next_mode); self.set_mode_next(next_mode);
} }
false false
@ -703,7 +746,7 @@ impl Ppu {
if self.current_dot % 456 == 0 { if self.current_dot % 456 == 0 {
if self.registers.ly >= 153 { if self.registers.ly >= 153 {
self.set_scanline(interrupts, 0); self.set_scanline(interrupts, 0);
self.set_mode(interrupts, PPUMode::SearchingOAM); self.set_mode_next(PPUMode::SearchingOAM);
self.first_frame = false; self.first_frame = false;
true true
} else { } else {
@ -789,120 +832,121 @@ impl Ppu {
mut window_drawn, mut window_drawn,
draw_only_sprites, draw_only_sprites,
) => { ) => {
let wx_match = (drawn_pixels as usize + 7) >= self.registers.wx as usize; if !self.first_frame {
let scrolled_y = self.registers.ly.wrapping_add(scy) as usize; let wx_match = (drawn_pixels as usize + 7) >= self.registers.wx as usize;
let scrolled_x = drawn_pixels.wrapping_add(scx) 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 { let (bg_color_id, bg_color) = match draw_only_sprites {
true => (0, Color::White), true => (0, Color::White),
false => { false => {
let tilemap_idx = scrolled_x / 8 + ((scrolled_y / 8) * 32); let tilemap_idx = scrolled_x / 8 + ((scrolled_y / 8) * 32);
let tilemap_value = self.read_tile_map()[tilemap_idx]; let tilemap_value = self.read_tile_map()[tilemap_idx];
let (mut bg_color_id, mut bg_color) = Self::parse_tile_color( 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(
self.read_bg_win_tile(tilemap_value), self.read_bg_win_tile(tilemap_value),
window_x % 8, scrolled_x % 8,
window_y % 8, scrolled_y % 8,
&self.bgp, &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;
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);
}
} }
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 let x_valid =
for sprite in &sprite_buffer { drawn_pixels < sprite.x && drawn_pixels.wrapping_add(8) >= sprite.x;
let mut sprite_x_idx = let y_valid = self.registers.ly < sprite.y
drawn_pixels.wrapping_sub(sprite.x.wrapping_sub(8)) as usize; && self.registers.ly.wrapping_add(16) >= sprite.y;
let mut sprite_y_idx =
self.registers.ly.wrapping_sub(sprite.y.wrapping_sub(16)) as usize;
if sprite.y_flip() { if !sprite_buffer.iter().any(|existing| existing.x == sprite.x)
let sprite_offset = match self.sprite_height() { && x_valid && y_valid
SpriteHeight::Eight => 7, {
SpriteHeight::Sixteen => 15, 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() { if sprite_y_idx >= 8 {
sprite_x_idx = 7 - sprite_x_idx; sprite_y_idx -= 8;
} assert!(sprite_y_idx < 8);
}
let tile_idx = match self.sprite_height() { let color_id =
SpriteHeight::Eight => sprite.tile_idx, self.read_obj_tile_colour_id(tile_idx, sprite_x_idx, sprite_y_idx);
SpriteHeight::Sixteen => match sprite_y_idx >= 8 { let palette = &self.obp[sprite.palette_number() as usize];
true => sprite.tile_idx | 1, let sprite_color = palette.color_from_2bit(color_id);
false => sprite.tile_idx & 0xFE,
},
};
if sprite_y_idx >= 8 { let sprite_covered = sprite.covered_by_bg_window() && bg_color_id != 0;
sprite_y_idx -= 8;
assert!(sprite_y_idx < 8);
}
let color_id = if color_id != 0 && !sprite_covered {
self.read_obj_tile_colour_id(tile_idx, sprite_x_idx, sprite_y_idx); let [r, g, b, a] = *sprite_color.rgba();
let palette = &self.obp[sprite.palette_number() as usize];
let sprite_color = palette.color_from_2bit(color_id);
let sprite_covered = sprite.covered_by_bg_window() && bg_color_id != 0; self.sprite_framebuffer[framebuffer_offset + 0] = r;
self.sprite_framebuffer[framebuffer_offset + 1] = g;
if color_id != 0 && !sprite_covered { self.sprite_framebuffer[framebuffer_offset + 2] = b;
let [r, g, b, a] = *sprite_color.rgba(); 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 { if window_drawn {
self.window_counter += 1; self.window_counter += 1;
self.dot_target += 6; 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 { } else {
self.current_draw_state = Some(LineDrawingState::BackgroundAndObjectFifo( self.current_draw_state = Some(LineDrawingState::BackgroundAndObjectFifo(
scx, 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!(), 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); (&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 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 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); drop(gameboy);
if FRAMEBUFFER { if FRAMEBUFFER {
std::fs::write(expected, &fb.unwrap()) std::fs::write(expected, &fb.unwrap()).map_err(DmgTestError::OutputFileWrite)?;
.map_err(DmgTestError::OutputFileWrite)?;
} else { } else {
let serial_content = sync_writer.into_inner(); let serial_content = sync_writer.into_inner();
std::fs::write(expected, &serial_content) std::fs::write(expected, &serial_content).map_err(DmgTestError::OutputFileWrite)?;
.map_err(DmgTestError::OutputFileWrite)?;
} }
Ok(instant.elapsed()) Ok(instant.elapsed())
} }
fn run_test<const FRAMEBUFFER: bool>( fn run_test<const FRAMEBUFFER: bool>(
rom: &Path, rom: &Path,
maximum_m_cycles: u64, maximum_m_cycles: u64,
@ -155,7 +152,12 @@ fn run_test<const FRAMEBUFFER: bool>(
}; };
if FRAMEBUFFER { 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(); let sync_writer = SyncWriter::new();
@ -202,7 +204,8 @@ fn main() {
match args.operation { match args.operation {
Operation::TestSerial { maximum_m_cycles, expected_serial } => { 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)) => { Ok((m_cycles, duration)) => {
println!("Success! Ran {} M-Cycles in {}ms", m_cycles, duration.as_millis()); println!("Success! Ran {} M-Cycles in {}ms", m_cycles, duration.as_millis());
} }
@ -213,7 +216,8 @@ fn main() {
} }
} }
Operation::GenerateOutputSerial { m_cycles, expected_serial } => { 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) => { 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()); 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 } => { 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)) => { Ok((m_cycles, duration)) => {
println!("Success! Ran {} M-Cycles in {}ms", m_cycles, duration.as_millis()); println!("Success! Ran {} M-Cycles in {}ms", m_cycles, duration.as_millis());
} }
@ -233,9 +241,13 @@ fn main() {
std::process::exit(1); std::process::exit(1);
} }
} }
}, }
Operation::GenerateOutputFramebuffer { m_cycles, expected_framebuffer } => { Operation::GenerateOutputFramebuffer { m_cycles, expected_framebuffer } => {
match generate_output::<true>(args.rom.as_path(), m_cycles, expected_framebuffer.as_path()) { match generate_output::<true>(
args.rom.as_path(),
m_cycles,
expected_framebuffer.as_path(),
) {
Ok(duration) => { 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()); 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); std::process::exit(1);
} }
} }
}, }
} }
} }

View file

@ -12,10 +12,7 @@ use std::{
use clap::Parser; use clap::Parser;
use config::MeowGBConfig; use config::MeowGBConfig;
use meowgb_core::gameboy::{ use meowgb_core::gameboy::{serial::SerialWriter, Gameboy};
serial::SerialWriter,
Gameboy,
};
use window::events::{EmulatorDebugEvent, EmulatorWindowEvent, GameboyEvent}; use window::events::{EmulatorDebugEvent, EmulatorWindowEvent, GameboyEvent};
#[cfg(feature = "debugger")] #[cfg(feature = "debugger")]
@ -67,10 +64,10 @@ fn real_main() -> Result<(), MeowGBError> {
if !rom.is_file() { if !rom.is_file() {
return Err(MeowGBError::GameNotFound); return Err(MeowGBError::GameNotFound);
} }
Some(std::fs::read(rom)?) Some(std::fs::read(rom)?)
}, }
None => None None => None,
}; };
let mut gameboy = WrappedGameboy::new(Gameboy::new(std::io::stdout(), rom)); let mut gameboy = WrappedGameboy::new(Gameboy::new(std::io::stdout(), rom));

View file

@ -557,6 +557,36 @@ else
echo "Failed: $res" echo "Failed: $res"
fi 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" echo "Running test ROM ./test-roms/mooneye-test-suite/serial-roms/ppu/stat_irq_blocking.gb"
TEST_TOTAL=$((TEST_TOTAL + 1)) 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_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_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_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/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) * 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)