fix: PPU timings are more accurate
This commit is contained in:
parent
c6dd3a7bd5
commit
8e47afa1bc
17 changed files with 300 additions and 175 deletions
|
@ -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
|
||||
|
|
12
.github/workflows/action.yml
vendored
12
.github/workflows/action.yml
vendored
|
@ -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
|
||||
|
|
|
@ -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,18 +118,16 @@ 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;
|
||||
|
@ -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],
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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!(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
"
|
|
@ -0,0 +1 @@
|
|||
"
|
|
@ -0,0 +1 @@
|
|||
"
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")]
|
||||
|
@ -69,8 +66,8 @@ fn real_main() -> Result<(), MeowGBError> {
|
|||
}
|
||||
|
||||
Some(std::fs::read(rom)?)
|
||||
},
|
||||
None => None
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
let mut gameboy = WrappedGameboy::new(Gameboy::new(std::io::stdout(), rom));
|
||||
|
|
|
@ -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))
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
3
tests.md
3
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)
|
||||
|
|
Loading…
Reference in a new issue