From a2156ec7f48cfd587a8df3ea52a88510de524855 Mon Sep 17 00:00:00 2001 From: EliseZeroTwo Date: Fri, 12 Jan 2024 12:19:45 -0700 Subject: [PATCH] feat: improve debugger --- .forgejo/workflows/action.yml | 28 ++ .github/workflows/action.yml | 28 ++ meowgb-core/Cargo.toml | 2 + meowgb-core/src/gameboy.rs | 25 +- meowgb-core/src/gameboy/cpu.rs | 7 +- meowgb-core/src/gameboy/dma.rs | 10 +- meowgb-core/src/gameboy/mapper.rs | 6 +- meowgb-core/src/gameboy/ppu.rs | 18 +- meowgb-core/src/lib.rs | 1 + meowgb-core/src/ringbuffer.rs | 21 ++ meowgb-tests/expected_output/di_timing-GS.bin | 1 + .../halt_ime0_nointr_timing.bin | 1 + .../expected_output/halt_ime1_timing.bin | 1 + .../expected_output/halt_ime1_timing2-GS.bin | 1 + meowgb-tests/expected_output/pop_timing.bin | 1 + meowgb-tests/expected_output/push_timing.bin | 1 + meowgb-tests/expected_output/rst_timing.bin | 1 + meowgb/Cargo.toml | 3 + meowgb/src/main.rs | 134 +++++++-- meowgb/src/window.rs | 103 ++++--- meowgb/src/window/events.rs | 9 +- meowgb/src/window/overlay.rs | 280 +++++++++++++++--- run-test-roms.sh | 70 +++++ .../mooneye-test-suite/roms/di_timing-GS.gb | Bin 0 -> 32768 bytes .../roms/halt_ime0_nointr_timing.gb | Bin 0 -> 32768 bytes .../roms/halt_ime1_timing.gb | Bin 0 -> 32768 bytes .../roms/halt_ime1_timing2-GS.gb | Bin 0 -> 32768 bytes .../mooneye-test-suite/roms/pop_timing.gb | Bin 0 -> 32768 bytes .../mooneye-test-suite/roms/push_timing.gb | Bin 0 -> 32768 bytes .../mooneye-test-suite/roms/rst_timing.gb | Bin 0 -> 32768 bytes tests.md | 7 + 31 files changed, 615 insertions(+), 144 deletions(-) create mode 100644 meowgb-tests/expected_output/di_timing-GS.bin create mode 100644 meowgb-tests/expected_output/halt_ime0_nointr_timing.bin create mode 100644 meowgb-tests/expected_output/halt_ime1_timing.bin create mode 100644 meowgb-tests/expected_output/halt_ime1_timing2-GS.bin create mode 100644 meowgb-tests/expected_output/pop_timing.bin create mode 100644 meowgb-tests/expected_output/push_timing.bin create mode 100644 meowgb-tests/expected_output/rst_timing.bin create mode 100644 test-roms/mooneye-test-suite/roms/di_timing-GS.gb create mode 100644 test-roms/mooneye-test-suite/roms/halt_ime0_nointr_timing.gb create mode 100644 test-roms/mooneye-test-suite/roms/halt_ime1_timing.gb create mode 100644 test-roms/mooneye-test-suite/roms/halt_ime1_timing2-GS.gb create mode 100644 test-roms/mooneye-test-suite/roms/pop_timing.gb create mode 100644 test-roms/mooneye-test-suite/roms/push_timing.gb create mode 100644 test-roms/mooneye-test-suite/roms/rst_timing.gb diff --git a/.forgejo/workflows/action.yml b/.forgejo/workflows/action.yml index 9f9f1de..1e27741 100644 --- a/.forgejo/workflows/action.yml +++ b/.forgejo/workflows/action.yml @@ -46,6 +46,10 @@ jobs: if: always() run: ./target/release/meowgb-tests test-roms/mooneye-test-suite/roms/daa.gb test -m 100000000 -s meowgb-tests/expected_output/daa.bin + - name: Run test ROM (mooneye-test-suite di_timing-GS) + if: always() + run: ./target/release/meowgb-tests test-roms/mooneye-test-suite/roms/di_timing-GS.gb test -m 100000000 -s meowgb-tests/expected_output/di_timing-GS.bin + - name: Run test ROM (mooneye-test-suite div_timing) if: always() run: ./target/release/meowgb-tests test-roms/mooneye-test-suite/roms/div_timing.gb test -m 100000000 -s meowgb-tests/expected_output/div_timing.bin @@ -66,6 +70,18 @@ jobs: if: always() run: ./target/release/meowgb-tests test-roms/mooneye-test-suite/roms/halt_ime0_ei.gb test -m 100000000 -s meowgb-tests/expected_output/halt_ime0_ei.bin + - name: Run test ROM (mooneye-test-suite halt_ime0_nointr_timing) + if: always() + run: ./target/release/meowgb-tests test-roms/mooneye-test-suite/roms/halt_ime0_nointr_timing.gb test -m 100000000 -s meowgb-tests/expected_output/halt_ime0_nointr_timing.bin + + - name: Run test ROM (mooneye-test-suite halt_ime1_timing) + if: always() + run: ./target/release/meowgb-tests test-roms/mooneye-test-suite/roms/halt_ime1_timing.gb test -m 100000000 -s meowgb-tests/expected_output/halt_ime1_timing.bin + + - name: Run test ROM (mooneye-test-suite halt_ime1_timing2-GS) + if: always() + run: ./target/release/meowgb-tests test-roms/mooneye-test-suite/roms/halt_ime1_timing2-GS.gb test -m 100000000 -s meowgb-tests/expected_output/halt_ime1_timing2-GS.bin + - name: Run test ROM (mooneye-test-suite intr_1_2_timing-GS) if: always() run: ./target/release/meowgb-tests test-roms/mooneye-test-suite/roms/intr_1_2_timing-GS.gb test -m 100000000 -s meowgb-tests/expected_output/intr_1_2_timing-GS.bin @@ -90,6 +106,14 @@ jobs: if: always() run: ./target/release/meowgb-tests test-roms/mooneye-test-suite/roms/oam_dma_timing.gb test -m 100000000 -s meowgb-tests/expected_output/oam_dma_timing.bin + - name: Run test ROM (mooneye-test-suite pop_timing) + if: always() + run: ./target/release/meowgb-tests test-roms/mooneye-test-suite/roms/pop_timing.gb test -m 100000000 -s meowgb-tests/expected_output/pop_timing.bin + + - name: Run test ROM (mooneye-test-suite push_timing) + if: always() + run: ./target/release/meowgb-tests test-roms/mooneye-test-suite/roms/push_timing.gb test -m 100000000 -s meowgb-tests/expected_output/push_timing.bin + - name: Run test ROM (mooneye-test-suite rapid_di_ei) if: always() run: ./target/release/meowgb-tests test-roms/mooneye-test-suite/roms/rapid_di_ei.gb test -m 100000000 -s meowgb-tests/expected_output/rapid_di_ei.bin @@ -106,6 +130,10 @@ jobs: if: always() run: ./target/release/meowgb-tests test-roms/mooneye-test-suite/roms/reg_read.gb test -m 100000000 -s meowgb-tests/expected_output/reg_read.bin + - name: Run test ROM (mooneye-test-suite rst_timing) + if: always() + run: ./target/release/meowgb-tests test-roms/mooneye-test-suite/roms/rst_timing.gb test -m 100000000 -s meowgb-tests/expected_output/rst_timing.bin + - name: Run test ROM (mooneye-test-suite stat_irq_blocking) if: always() run: ./target/release/meowgb-tests test-roms/mooneye-test-suite/roms/stat_irq_blocking.gb test -m 100000000 -s meowgb-tests/expected_output/stat_irq_blocking.bin diff --git a/.github/workflows/action.yml b/.github/workflows/action.yml index 9f9f1de..1e27741 100644 --- a/.github/workflows/action.yml +++ b/.github/workflows/action.yml @@ -46,6 +46,10 @@ jobs: if: always() run: ./target/release/meowgb-tests test-roms/mooneye-test-suite/roms/daa.gb test -m 100000000 -s meowgb-tests/expected_output/daa.bin + - name: Run test ROM (mooneye-test-suite di_timing-GS) + if: always() + run: ./target/release/meowgb-tests test-roms/mooneye-test-suite/roms/di_timing-GS.gb test -m 100000000 -s meowgb-tests/expected_output/di_timing-GS.bin + - name: Run test ROM (mooneye-test-suite div_timing) if: always() run: ./target/release/meowgb-tests test-roms/mooneye-test-suite/roms/div_timing.gb test -m 100000000 -s meowgb-tests/expected_output/div_timing.bin @@ -66,6 +70,18 @@ jobs: if: always() run: ./target/release/meowgb-tests test-roms/mooneye-test-suite/roms/halt_ime0_ei.gb test -m 100000000 -s meowgb-tests/expected_output/halt_ime0_ei.bin + - name: Run test ROM (mooneye-test-suite halt_ime0_nointr_timing) + if: always() + run: ./target/release/meowgb-tests test-roms/mooneye-test-suite/roms/halt_ime0_nointr_timing.gb test -m 100000000 -s meowgb-tests/expected_output/halt_ime0_nointr_timing.bin + + - name: Run test ROM (mooneye-test-suite halt_ime1_timing) + if: always() + run: ./target/release/meowgb-tests test-roms/mooneye-test-suite/roms/halt_ime1_timing.gb test -m 100000000 -s meowgb-tests/expected_output/halt_ime1_timing.bin + + - name: Run test ROM (mooneye-test-suite halt_ime1_timing2-GS) + if: always() + run: ./target/release/meowgb-tests test-roms/mooneye-test-suite/roms/halt_ime1_timing2-GS.gb test -m 100000000 -s meowgb-tests/expected_output/halt_ime1_timing2-GS.bin + - name: Run test ROM (mooneye-test-suite intr_1_2_timing-GS) if: always() run: ./target/release/meowgb-tests test-roms/mooneye-test-suite/roms/intr_1_2_timing-GS.gb test -m 100000000 -s meowgb-tests/expected_output/intr_1_2_timing-GS.bin @@ -90,6 +106,14 @@ jobs: if: always() run: ./target/release/meowgb-tests test-roms/mooneye-test-suite/roms/oam_dma_timing.gb test -m 100000000 -s meowgb-tests/expected_output/oam_dma_timing.bin + - name: Run test ROM (mooneye-test-suite pop_timing) + if: always() + run: ./target/release/meowgb-tests test-roms/mooneye-test-suite/roms/pop_timing.gb test -m 100000000 -s meowgb-tests/expected_output/pop_timing.bin + + - name: Run test ROM (mooneye-test-suite push_timing) + if: always() + run: ./target/release/meowgb-tests test-roms/mooneye-test-suite/roms/push_timing.gb test -m 100000000 -s meowgb-tests/expected_output/push_timing.bin + - name: Run test ROM (mooneye-test-suite rapid_di_ei) if: always() run: ./target/release/meowgb-tests test-roms/mooneye-test-suite/roms/rapid_di_ei.gb test -m 100000000 -s meowgb-tests/expected_output/rapid_di_ei.bin @@ -106,6 +130,10 @@ jobs: if: always() run: ./target/release/meowgb-tests test-roms/mooneye-test-suite/roms/reg_read.gb test -m 100000000 -s meowgb-tests/expected_output/reg_read.bin + - name: Run test ROM (mooneye-test-suite rst_timing) + if: always() + run: ./target/release/meowgb-tests test-roms/mooneye-test-suite/roms/rst_timing.gb test -m 100000000 -s meowgb-tests/expected_output/rst_timing.bin + - name: Run test ROM (mooneye-test-suite stat_irq_blocking) if: always() run: ./target/release/meowgb-tests test-roms/mooneye-test-suite/roms/stat_irq_blocking.gb test -m 100000000 -s meowgb-tests/expected_output/stat_irq_blocking.bin diff --git a/meowgb-core/Cargo.toml b/meowgb-core/Cargo.toml index ab87a15..3ebd2f7 100644 --- a/meowgb-core/Cargo.toml +++ b/meowgb-core/Cargo.toml @@ -4,6 +4,8 @@ version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +instr-dbg = [] [dependencies] log = "0.4.14" diff --git a/meowgb-core/src/gameboy.rs b/meowgb-core/src/gameboy.rs index 7b76c9f..541b9f1 100644 --- a/meowgb-core/src/gameboy.rs +++ b/meowgb-core/src/gameboy.rs @@ -24,6 +24,8 @@ use self::{ serial::{Serial, SerialWriter}, sound::Sound, }; +#[cfg(feature = "instr-dbg")] +use crate::ringbuffer::RingBuffer; pub struct Gameboy { pub ppu: Ppu, @@ -43,6 +45,12 @@ pub struct Gameboy { pub stop: bool, pub tick_count: u8, + + pub last_read: Option<(u16, u8)>, + pub last_write: Option<(u16, u8)>, + + #[cfg(feature = "instr-dbg")] + pub pc_history: RingBuffer, } impl Gameboy { @@ -66,6 +74,10 @@ impl Gameboy { used_halt_bug: false, stop: false, tick_count: 0, + last_read: None, + last_write: None, + #[cfg(feature = "instr-dbg")] + pc_history: RingBuffer::new(), } } @@ -221,10 +233,7 @@ impl Gameboy { } 0xFF41 => self.ppu.set_stat(&mut self.interrupts, value), 0xFF42 => self.ppu.registers.scy = value, - 0xFF43 => { - // println!("Setting SCX to {} from {}", value, self.ppu.registers.scx); - self.ppu.registers.scx = value; - }, + 0xFF43 => self.ppu.registers.scx = value, 0xFF44 => {} // LY is read only 0xFF45 => self.ppu.set_lyc(&mut self.interrupts, value), 0xFF46 => self.dma.init_request(value), @@ -312,6 +321,10 @@ impl Gameboy { } pub fn cpu_read_u8(&mut self, address: u16) { + self.cpu_read_u8_internal(address, false); + } + + pub fn cpu_read_u8_internal(&mut self, address: u16, is_next_pc: bool) { assert!(!self.registers.mem_op_happened); assert!(self.registers.mem_read_hold.is_none()); self.registers.mem_op_happened = true; @@ -342,12 +355,16 @@ impl Gameboy { 0xFFFF => self.interrupts.interrupt_enable, }, }; + if !is_next_pc { + self.last_read = Some((address, value)); + } self.registers.mem_read_hold = Some(value); } pub fn cpu_write_u8(&mut self, address: u16, value: u8) { assert!(!self.registers.mem_op_happened); self.registers.mem_op_happened = true; + self.last_write = Some((address, value)); match self.ppu.dma_occuring { true => match address { diff --git a/meowgb-core/src/gameboy/cpu.rs b/meowgb-core/src/gameboy/cpu.rs index 0ebe4d2..b68ba2f 100644 --- a/meowgb-core/src/gameboy/cpu.rs +++ b/meowgb-core/src/gameboy/cpu.rs @@ -142,6 +142,8 @@ impl Registers { pub fn tick_cpu(state: &mut Gameboy) { state.registers.mem_op_happened = false; + state.last_read = None; + state.last_write = None; if state.joypad.interrupt_triggered { state.joypad.interrupt_triggered = false; @@ -162,7 +164,6 @@ pub fn tick_cpu(state: &mut Gameboy) { } } - // TODO: Interrupts if state.registers.cycle == 0 && state.interrupts.ime { if state.interrupts.read_ie_vblank() && state.interrupts.read_if_vblank() { state.registers.in_interrupt_vector = Some(0); @@ -244,7 +245,7 @@ pub fn tick_cpu(state: &mut Gameboy) { opcode } None => { - state.cpu_read_u8(state.registers.pc); + state.cpu_read_u8_internal(state.registers.pc, true); return; } }, @@ -533,7 +534,7 @@ pub fn tick_cpu(state: &mut Gameboy) { } if !state.registers.mem_op_happened { - state.cpu_read_u8(state.registers.pc); + state.cpu_read_u8_internal(state.registers.pc, true); } state.registers.cycle = 0; diff --git a/meowgb-core/src/gameboy/dma.rs b/meowgb-core/src/gameboy/dma.rs index b774e35..65fc66b 100644 --- a/meowgb-core/src/gameboy/dma.rs +++ b/meowgb-core/src/gameboy/dma.rs @@ -38,25 +38,25 @@ impl DmaState { if self.remaining_cycles > 0 { let offset = 0xA0 - self.remaining_cycles; + let read_address = ((self.original_base as usize) << 8) | offset as usize; let value = if self.original_base <= 0x7F { match cartridge { - Some(cart) => cart.read_rom_u8((self.base as u16) << 8 | offset as u16), + Some(cart) => cart.read_rom_u8(read_address as u16), None => 0xFF, } } else if self.original_base <= 0x9F { - let address = (((self.original_base as usize) << 8) | offset as usize) - 0x8000; + let address = read_address - 0x8000; ppu.vram[address] } else if self.original_base <= 0xDF { - let address = ((self.original_base as usize) << 8 | offset as usize) - 0xC000; + let address = read_address - 0xC000; memory.wram[address] } else if self.original_base <= 0xFD { - let address = ((self.original_base as usize) << 8 | offset as usize) - 0xE000; + let address = read_address - 0xE000; memory.wram[address] } else { 0xFF }; - ppu.dma_write_oam(offset, value); self.remaining_cycles -= 1; } diff --git a/meowgb-core/src/gameboy/mapper.rs b/meowgb-core/src/gameboy/mapper.rs index a762fc1..1d0931f 100644 --- a/meowgb-core/src/gameboy/mapper.rs +++ b/meowgb-core/src/gameboy/mapper.rs @@ -41,17 +41,15 @@ impl Mapper for NoMBC { fn write_rom_u8(&mut self, _address: u16, _value: u8) {} fn read_eram_u8(&self, address: u16) -> u8 { - let decoded_address = address - 0xA000; match &self.ram { - Some(ram) => ram[decoded_address as usize], + Some(ram) => ram[address as usize], None => 0, } } fn write_eram_u8(&mut self, address: u16, value: u8) { - let decoded_address = address - 0xA000; if let Some(ram) = &mut self.ram { - ram[decoded_address as usize] = value; + ram[address as usize] = value; } } } diff --git a/meowgb-core/src/gameboy/ppu.rs b/meowgb-core/src/gameboy/ppu.rs index 11b4b52..685dfed 100644 --- a/meowgb-core/src/gameboy/ppu.rs +++ b/meowgb-core/src/gameboy/ppu.rs @@ -550,8 +550,10 @@ impl Ppu { } fn set_scanline(&mut self, interrupts: &mut Interrupts, scanline: u8) { - // println!("LY incrementing: {} cycles since last incrementation. cycles since: {:?}", self.registers.cycles_since_last_ly_increment, self.registers.cycles_since_last_last_mode_start_increment.iter().enumerate().map(|(idx, value)| { - // let idx_enum = match idx { + // println!("LY incrementing: {} cycles since last incrementation. cycles since: + // {:?}", self.registers.cycles_since_last_ly_increment, + // self.registers.cycles_since_last_last_mode_start_increment.iter(). + // enumerate().map(|(idx, value)| { let idx_enum = match idx { // 0 => PPUMode::HBlank, // 1 => PPUMode::VBlank, // 2 => PPUMode::SearchingOAM, @@ -638,7 +640,6 @@ impl Ppu { self.total_dots += 1; if self.current_dot == self.dot_target { - // println!("Mode 3 DT: {}", self.dot_target); // assert_eq!(self.total_dots, match self.first_frame && self.first_line { // true => self.dot_target, // false => 80 + self.dot_target @@ -659,15 +660,14 @@ impl Ppu { if self.first_line && self.current_dot == 80 && self.dot_target == 0 { self.set_mode(interrupts, PPUMode::TransferringData); } else if self.dot_target != 0 && self.current_dot == self.dot_target { - // println!("Mode 0 DT: {}", 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, - // } + 456 /* match self.first_frame && self.first_line { + * true => 456 - (80 - 64), + * false => 456, + * } */ ); self.total_dots = 0; self.first_line = false; @@ -729,10 +729,8 @@ impl Ppu { Some(state) => state, None => { let scrolling_delay = self.registers.scx % 8; - // println!("{}", scrolling_delay); self.dot_target += scrolling_delay as u16; if scrolling_delay != 0 { - // println!("Scrolling delay of {:#X} with scx of {}", scrolling_delay, self.registers.scx); LineDrawingState::BackgroundScrolling( scrolling_delay as usize, self.registers.scx, diff --git a/meowgb-core/src/lib.rs b/meowgb-core/src/lib.rs index 06dd4a4..eb33e04 100644 --- a/meowgb-core/src/lib.rs +++ b/meowgb-core/src/lib.rs @@ -1,4 +1,5 @@ pub mod gameboy; +#[cfg(feature = "instr-dbg")] pub mod ringbuffer; /// A helper for writing CPU tests in Rust, the emulator returned by this diff --git a/meowgb-core/src/ringbuffer.rs b/meowgb-core/src/ringbuffer.rs index 8442ce3..41647c5 100644 --- a/meowgb-core/src/ringbuffer.rs +++ b/meowgb-core/src/ringbuffer.rs @@ -68,3 +68,24 @@ fn test_ringbuffer() { &[16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31] ); } + +impl std::fmt::Display + for RingBuffer +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut offset = self.read_ptr; + + f.write_str("[\n")?; + + for idx in 0..self.size { + f.write_fmt(format_args!(" {}: {:#X},\n", idx, self.buffer[offset]))?; + + offset += 1; + offset %= SIZE; + } + + f.write_str("]")?; + + Ok(()) + } +} diff --git a/meowgb-tests/expected_output/di_timing-GS.bin b/meowgb-tests/expected_output/di_timing-GS.bin new file mode 100644 index 0000000..2a02163 --- /dev/null +++ b/meowgb-tests/expected_output/di_timing-GS.bin @@ -0,0 +1 @@ + " \ No newline at end of file diff --git a/meowgb-tests/expected_output/halt_ime0_nointr_timing.bin b/meowgb-tests/expected_output/halt_ime0_nointr_timing.bin new file mode 100644 index 0000000..2a02163 --- /dev/null +++ b/meowgb-tests/expected_output/halt_ime0_nointr_timing.bin @@ -0,0 +1 @@ + " \ No newline at end of file diff --git a/meowgb-tests/expected_output/halt_ime1_timing.bin b/meowgb-tests/expected_output/halt_ime1_timing.bin new file mode 100644 index 0000000..2a02163 --- /dev/null +++ b/meowgb-tests/expected_output/halt_ime1_timing.bin @@ -0,0 +1 @@ + " \ No newline at end of file diff --git a/meowgb-tests/expected_output/halt_ime1_timing2-GS.bin b/meowgb-tests/expected_output/halt_ime1_timing2-GS.bin new file mode 100644 index 0000000..2a02163 --- /dev/null +++ b/meowgb-tests/expected_output/halt_ime1_timing2-GS.bin @@ -0,0 +1 @@ + " \ No newline at end of file diff --git a/meowgb-tests/expected_output/pop_timing.bin b/meowgb-tests/expected_output/pop_timing.bin new file mode 100644 index 0000000..2a02163 --- /dev/null +++ b/meowgb-tests/expected_output/pop_timing.bin @@ -0,0 +1 @@ + " \ No newline at end of file diff --git a/meowgb-tests/expected_output/push_timing.bin b/meowgb-tests/expected_output/push_timing.bin new file mode 100644 index 0000000..2a02163 --- /dev/null +++ b/meowgb-tests/expected_output/push_timing.bin @@ -0,0 +1 @@ + " \ No newline at end of file diff --git a/meowgb-tests/expected_output/rst_timing.bin b/meowgb-tests/expected_output/rst_timing.bin new file mode 100644 index 0000000..2a02163 --- /dev/null +++ b/meowgb-tests/expected_output/rst_timing.bin @@ -0,0 +1 @@ + " \ No newline at end of file diff --git a/meowgb/Cargo.toml b/meowgb/Cargo.toml index dce249b..9381cd1 100644 --- a/meowgb/Cargo.toml +++ b/meowgb/Cargo.toml @@ -4,6 +4,9 @@ version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +debugger = [] +instr-dbg = ["meowgb-core/instr-dbg"] [dependencies] meowgb-core = { path = "../meowgb-core" } diff --git a/meowgb/src/main.rs b/meowgb/src/main.rs index e6a2f29..4421d64 100644 --- a/meowgb/src/main.rs +++ b/meowgb/src/main.rs @@ -17,8 +17,24 @@ use meowgb_core::gameboy::{ serial::SerialWriter, Gameboy, }; -use window::events::{EmulatorWindowEvent, GameboyEvent}; +use window::events::{EmulatorDebugEvent, EmulatorWindowEvent, GameboyEvent}; +#[cfg(feature = "debugger")] +#[derive(Debug, Parser)] +/// DMG Emulator +pub struct CliArgs { + /// bootrom path + #[clap(long)] + pub bootrom: Option, + /// game path + #[clap(long)] + pub rom: Option, + /// start the emulator in debug mode + #[clap(short, long)] + pub debug: bool, +} + +#[cfg(not(feature = "debugger"))] #[derive(Debug, Parser)] /// DMG Emulator pub struct CliArgs { @@ -60,7 +76,13 @@ fn real_main() -> Result<(), MeowGBError> { None => None, }; - let gameboy = Arc::new(RwLock::new(Gameboy::new(bootrom, std::io::stdout()))); + let mut gameboy = WrappedGameboy::new(Gameboy::new(bootrom, std::io::stdout())); + #[cfg(feature = "debugger")] + let dbg = args.debug; + #[cfg(not(feature = "debugger"))] + let dbg = false; + gameboy.debugging = dbg; + let gameboy = Arc::new(RwLock::new(gameboy)); let gameboy_2 = gameboy.clone(); let jh = std::thread::Builder::new() @@ -88,9 +110,21 @@ fn main() { } } +pub struct WrappedGameboy { + pub breakpoints: [[bool; 3]; 0x10000], + pub debugging: bool, + pub gameboy: Gameboy, +} + +impl WrappedGameboy { + pub fn new(gameboy: Gameboy) -> Self { + Self { breakpoints: [[false; 3]; 0x10000], debugging: false, gameboy } + } +} + pub fn run_gameboy( args: CliArgs, - gameboy_arc: Arc>>, + gameboy_arc: Arc>>, rx: Receiver, tx: Sender, ) -> Result<(), MeowGBError> { @@ -103,50 +137,94 @@ pub fn run_gameboy( let rom = std::fs::read(rom.as_path())?; - gameboy.load_cartridge(rom) + gameboy.gameboy.load_cartridge(rom) } drop(gameboy); let mut goal = time::OffsetDateTime::now_utc() + time::Duration::milliseconds(1000 / 60); let mut frame_counter = 0; + let mut debugging_tbf = None; 'outer: loop { - let mut gameboy = gameboy_arc.write().unwrap(); + let mut step = false; + let mut gameboy = gameboy_arc.write().unwrap(); while let Ok(event) = rx.try_recv() { match event { - EmulatorWindowEvent::AToggle => gameboy.joypad.invert_a(), - EmulatorWindowEvent::BToggle => gameboy.joypad.invert_b(), - EmulatorWindowEvent::SelectToggle => gameboy.joypad.invert_select(), - EmulatorWindowEvent::StartToggle => gameboy.joypad.invert_start(), - EmulatorWindowEvent::UpToggle => gameboy.joypad.invert_up(), - EmulatorWindowEvent::DownToggle => gameboy.joypad.invert_down(), - EmulatorWindowEvent::LeftToggle => gameboy.joypad.invert_left(), - EmulatorWindowEvent::RightToggle => gameboy.joypad.invert_right(), - EmulatorWindowEvent::PauseToggle => unimplemented!(), + EmulatorWindowEvent::AToggle => gameboy.gameboy.joypad.invert_a(), + EmulatorWindowEvent::BToggle => gameboy.gameboy.joypad.invert_b(), + EmulatorWindowEvent::SelectToggle => gameboy.gameboy.joypad.invert_select(), + EmulatorWindowEvent::StartToggle => gameboy.gameboy.joypad.invert_start(), + EmulatorWindowEvent::UpToggle => gameboy.gameboy.joypad.invert_up(), + EmulatorWindowEvent::DownToggle => gameboy.gameboy.joypad.invert_down(), + EmulatorWindowEvent::LeftToggle => gameboy.gameboy.joypad.invert_left(), + EmulatorWindowEvent::RightToggle => gameboy.gameboy.joypad.invert_right(), EmulatorWindowEvent::Exit => break 'outer, + EmulatorWindowEvent::Debug(EmulatorDebugEvent::ToggleBreakpoint(addr, breaks)) => { + gameboy.breakpoints[addr as usize] = breaks; + } + EmulatorWindowEvent::Debug(EmulatorDebugEvent::Continue) => { + gameboy.debugging = false; + if let Some(debugging_tbf) = debugging_tbf.take() { + let delta = time::OffsetDateTime::now_utc() - debugging_tbf; + goal += delta; + } + } + EmulatorWindowEvent::Debug(EmulatorDebugEvent::Step) => { + step = true; + } } } - let redraw_needed = gameboy.tick_4(); + if !gameboy.debugging || step { + let needs_redraw = gameboy.gameboy.tick_4(); + let bp_triggered = gameboy + .gameboy + .last_read + .map(|(addr, _)| gameboy.breakpoints[addr as usize][0]) + .unwrap_or_default() + || gameboy + .gameboy + .last_write + .map(|(addr, _)| gameboy.breakpoints[addr as usize][1]) + .unwrap_or_default() + || gameboy.breakpoints[gameboy.gameboy.registers.pc as usize][2]; + gameboy.debugging |= bp_triggered; - drop(gameboy); + if bp_triggered || step { + gameboy.debugging = bp_triggered; - if redraw_needed { - let now = time::OffsetDateTime::now_utc(); - frame_counter += 1; - tx.send(GameboyEvent::Framebuffer(gameboy_arc.read().unwrap().ppu.write_fb())).unwrap(); - let delta = goal - now; - let delta_ms = delta.whole_milliseconds(); - if delta_ms > 0 { - std::thread::sleep(std::time::Duration::from_millis(delta_ms as u64)); + let now = time::OffsetDateTime::now_utc(); + + if let Some(debugging_tbf) = debugging_tbf { + let delta = now - debugging_tbf; + goal += delta; + } + + debugging_tbf = Some(now); } - goal = goal + time::Duration::milliseconds(1000 / 60); - if frame_counter == 60 { - log::debug!("Rendered 60 frames"); - frame_counter = 0; + drop(gameboy); + + if needs_redraw { + let now = time::OffsetDateTime::now_utc(); + frame_counter += 1; + tx.send(GameboyEvent::Framebuffer( + gameboy_arc.read().unwrap().gameboy.ppu.write_fb(), + )) + .unwrap(); + let delta = goal - now; + let delta_ms = delta.whole_milliseconds(); + if delta_ms > 0 { + std::thread::sleep(std::time::Duration::from_millis(delta_ms as u64)); + } + goal = goal + time::Duration::milliseconds(1000 / 60); + + if frame_counter == 60 { + log::debug!("Rendered 60 frames"); + frame_counter = 0; + } } } } diff --git a/meowgb/src/window.rs b/meowgb/src/window.rs index f645fd3..ed571a7 100644 --- a/meowgb/src/window.rs +++ b/meowgb/src/window.rs @@ -1,4 +1,5 @@ pub mod events; +#[cfg(feature = "debugger")] mod overlay; use std::sync::{ @@ -6,7 +7,10 @@ use std::sync::{ Arc, RwLock, }; -use meowgb_core::gameboy::{serial::SerialWriter, Gameboy}; +use events::{EmulatorWindowEvent, GameboyEvent, Keymap}; +use meowgb_core::gameboy::serial::SerialWriter; +#[cfg(feature = "debugger")] +use overlay::Framework; use pixels::{Pixels, SurfaceTexture}; use winit::{ event::Event, @@ -15,11 +19,7 @@ use winit::{ }; use winit_input_helper::WinitInputHelper; -use self::{ - events::{EmulatorWindowEvent, GameboyEvent, Keymap}, - overlay::Framework, -}; -use crate::config::MeowGBConfig; +use crate::{config::MeowGBConfig, WrappedGameboy}; macro_rules! define_keypress { ($input:ident, $config:ident, $keymap:ident, $tx:ident, $key:ident, $event:ident) => { @@ -42,10 +42,13 @@ macro_rules! define_keypress { pub fn run_window( rom_name: &str, config: MeowGBConfig, - gameboy: Arc>>, + gameboy: Arc>>, rx: Receiver, tx: Sender, ) { + #[cfg(not(feature = "debugger"))] + drop(gameboy); + let event_loop = EventLoop::new(); let mut input = WinitInputHelper::new(); @@ -53,26 +56,27 @@ pub fn run_window( WindowBuilder::new().with_title(format!("Meow - {}", rom_name)).build(&event_loop).unwrap() }; - let (mut pixels, mut framework) = { - let window_size = window.inner_size(); - let scale_factor = window.scale_factor() as f32; - let surface_texture = SurfaceTexture::new(window_size.width, window_size.height, &window); - let pixels = Pixels::new( - meowgb_core::gameboy::ppu::FB_WIDTH, - meowgb_core::gameboy::ppu::FB_HEIGHT, - surface_texture, - ) - .unwrap(); - let framework = Framework::new( - &event_loop, - window_size.width, - window_size.height, - scale_factor, - &pixels, - &gameboy.read().unwrap(), - ); - (pixels, framework) - }; + let window_size = window.inner_size(); + #[cfg(feature = "debugger")] + let scale_factor = window.scale_factor() as f32; + let surface_texture = SurfaceTexture::new(window_size.width, window_size.height, &window); + let mut pixels = Pixels::new( + meowgb_core::gameboy::ppu::FB_WIDTH, + meowgb_core::gameboy::ppu::FB_HEIGHT, + surface_texture, + ) + .unwrap(); + + #[cfg(feature = "debugger")] + let mut framework = Framework::new( + &event_loop, + window_size.width, + window_size.height, + scale_factor, + &pixels, + &gameboy.read().unwrap(), + tx.clone(), + ); let mut redraw_happened = true; let mut fb: Option> = None; @@ -87,19 +91,34 @@ pub fn run_window( return; } - if input.key_pressed(config.bindings.pause) { - tx.send(EmulatorWindowEvent::PauseToggle).unwrap(); - } - + // if input.key_pressed(config.bindings.pause) { + // tx.send(EmulatorWindowEvent::PauseToggle).unwrap(); + // } + #[cfg(feature = "debugger")] if let Some(debug_menu) = config.bindings.debug_menu { if input.key_pressed(debug_menu) { - framework.gui.window_open = !framework.gui.window_open; + if !framework.gui.state.any_open() { + if let Some(old_state) = framework.gui.state_restore.take() { + framework.gui.state = old_state; + } + framework.gui.state.window_open = true; + } else { + framework.gui.state_restore = Some(framework.gui.state); + framework.gui.state.close_all(); + } + redraw_happened = true; } } if input.key_pressed(config.bindings.pause) {} + #[cfg(feature = "debugger")] if let Some(scale_factor) = input.scale_factor() { framework.scale_factor(scale_factor); + redraw_happened = true; + } + #[cfg(not(feature = "debugger"))] + { + redraw_happened |= input.scale_factor().is_some(); } define_keypress!(input, config, keymap, tx, a, AToggle); @@ -113,8 +132,9 @@ pub fn run_window( } match event { + #[cfg(feature = "debugger")] Event::WindowEvent { event, .. } => { - framework.handle_event(&event); + redraw_happened |= framework.handle_event(&event); } Event::RedrawRequested(_) => { let frame = pixels.frame_mut(); @@ -130,12 +150,14 @@ pub fn run_window( } } + #[cfg(feature = "debugger")] framework.prepare(&window, &gameboy.read().unwrap()); let render_result = pixels.render_with(|encoder, render_target, context| { // Render the world texture context.scaling_renderer.render(encoder, render_target); + #[cfg(feature = "debugger")] // Render egui framework.render(encoder, render_target, context); @@ -154,22 +176,23 @@ pub fn run_window( if let Some(size) = input.window_resized() { pixels.resize_surface(size.width, size.height).unwrap(); + #[cfg(feature = "debugger")] framework.resize(size.width, size.height); - window.request_redraw(); - redraw_happened = false; + redraw_happened = true; } while let Ok(event) = rx.try_recv() { match event { GameboyEvent::Framebuffer(buf) => { fb = Some(buf); - - if redraw_happened { - window.request_redraw(); - redraw_happened = false; - } + redraw_happened = true; } } } + + if redraw_happened { + window.request_redraw(); + redraw_happened = false; + } }); } diff --git a/meowgb/src/window/events.rs b/meowgb/src/window/events.rs index 7ec3c72..51530bc 100644 --- a/meowgb/src/window/events.rs +++ b/meowgb/src/window/events.rs @@ -12,10 +12,17 @@ pub enum EmulatorWindowEvent { DownToggle, LeftToggle, RightToggle, - PauseToggle, + Debug(EmulatorDebugEvent), Exit, } +#[derive(Debug, Clone, Copy)] +pub enum EmulatorDebugEvent { + Step, + Continue, + ToggleBreakpoint(u16, [bool; 3]), +} + #[derive(Debug)] pub enum GameboyEvent { Framebuffer(Vec), diff --git a/meowgb/src/window/overlay.rs b/meowgb/src/window/overlay.rs index 8356fe1..59ecbe7 100644 --- a/meowgb/src/window/overlay.rs +++ b/meowgb/src/window/overlay.rs @@ -1,11 +1,14 @@ /// Provides an [egui] based overlay for debugigng the emulator whilst it is /// running -use egui::{ClippedPrimitive, Context, TexturesDelta}; +use egui::{ClippedPrimitive, Context, Grid, TexturesDelta}; use egui_wgpu::renderer::{Renderer, ScreenDescriptor}; -use meowgb_core::gameboy::{serial::SerialWriter, Gameboy}; +use meowgb_core::gameboy::serial::SerialWriter; use pixels::{wgpu, PixelsContext}; use winit::{event_loop::EventLoopWindowTarget, window::Window}; +use super::events::{EmulatorDebugEvent, EmulatorWindowEvent}; +use crate::WrappedGameboy; + pub(crate) struct Framework { egui_ctx: Context, egui_state: egui_winit::State, @@ -16,13 +19,55 @@ pub(crate) struct Framework { pub gui: Gui, } -pub struct Gui { +#[derive(Debug, Clone, Copy)] +pub struct GuiWindowState { pub window_open: bool, pub register_window_open: bool, pub ppu_register_window_open: bool, + pub debugger_window_open: bool, + pub wram_window_open: bool, + pub oam_window_open: bool, + pub hram_window_open: bool, +} +impl GuiWindowState { + pub fn close_all(&mut self) { + self.ppu_register_window_open = false; + self.register_window_open = false; + self.window_open = false; + self.debugger_window_open = false; + self.wram_window_open = false; + self.oam_window_open = false; + self.hram_window_open = false; + } + + pub fn any_open(&self) -> bool { + self.window_open + || self.register_window_open + || self.ppu_register_window_open + || self.debugger_window_open + || self.wram_window_open + || self.oam_window_open + || self.hram_window_open + } +} + +pub struct Gui { + pub state: GuiWindowState, + pub state_restore: Option, pub registers: meowgb_core::gameboy::cpu::Registers, pub ppu_registers: meowgb_core::gameboy::ppu::PpuRegisters, + pub wram: [u8; 0x2000], + pub hram: [u8; 0xAF], + // pub vram: [u8; 0x2000], + pub oam: [u8; 0xA0], + pub bp_string: String, + pub bp_read_checkbox: bool, + pub bp_write_checkbox: bool, + pub bp_execute_checkbox: bool, + pub is_debugging: bool, + pub breakpoints: [[bool; 3]; 0x10000], + pub sender: std::sync::mpsc::Sender, } impl Framework { @@ -32,7 +77,8 @@ impl Framework { height: u32, scale_factor: f32, pixels: &pixels::Pixels, - gameboy: &Gameboy, + gameboy: &WrappedGameboy, + sender: std::sync::mpsc::Sender, ) -> Self { let max_texture_size = pixels.device().limits().max_texture_dimension_2d as usize; @@ -44,7 +90,7 @@ impl Framework { ScreenDescriptor { size_in_pixels: [width, height], pixels_per_point: scale_factor }; let renderer = Renderer::new(pixels.device(), pixels.render_texture_format(), None, 1); let textures = TexturesDelta::default(); - let gui = Gui::new(gameboy); + let gui = Gui::new(gameboy, sender); Self { egui_ctx, @@ -57,8 +103,8 @@ impl Framework { } } - pub(crate) fn handle_event(&mut self, event: &winit::event::WindowEvent) { - let _ = self.egui_state.on_event(&self.egui_ctx, event); + pub(crate) fn handle_event(&mut self, event: &winit::event::WindowEvent) -> bool { + self.egui_state.on_event(&self.egui_ctx, event).repaint } pub(crate) fn resize(&mut self, width: u32, height: u32) { @@ -71,9 +117,13 @@ impl Framework { self.screen_descriptor.pixels_per_point = scale_factor as f32; } - pub(crate) fn prepare(&mut self, window: &Window, gameboy: &Gameboy) { - self.gui.registers = gameboy.registers; - self.gui.ppu_registers = gameboy.ppu.registers; + pub(crate) fn prepare(&mut self, window: &Window, gameboy: &WrappedGameboy) { + self.gui.registers = gameboy.gameboy.registers; + self.gui.ppu_registers = gameboy.gameboy.ppu.registers; + self.gui.is_debugging = gameboy.debugging; + self.gui.oam = gameboy.gameboy.ppu.oam; + self.gui.hram = gameboy.gameboy.memory.hram; + self.gui.wram = gameboy.gameboy.memory.wram; // Run the egui frame and create all paint jobs to prepare for rendering. let raw_input = self.egui_state.take_egui_input(window); @@ -126,62 +176,194 @@ impl Framework { } impl Gui { - fn new(gameboy: &Gameboy) -> Self { + fn new( + gameboy: &WrappedGameboy, + sender: std::sync::mpsc::Sender, + ) -> Self { Self { - window_open: false, - register_window_open: false, - ppu_register_window_open: false, - registers: gameboy.registers, - ppu_registers: gameboy.ppu.registers, + state: GuiWindowState { + window_open: gameboy.debugging, + register_window_open: false, + ppu_register_window_open: false, + debugger_window_open: gameboy.debugging, + wram_window_open: false, + oam_window_open: false, + hram_window_open: false, + }, + state_restore: None, + registers: gameboy.gameboy.registers, + ppu_registers: gameboy.gameboy.ppu.registers, + bp_string: String::with_capacity(16), + breakpoints: [[false, false, false]; 0x10000], + bp_read_checkbox: false, + bp_write_checkbox: false, + bp_execute_checkbox: false, + sender, + is_debugging: gameboy.debugging, + wram: gameboy.gameboy.memory.wram, + hram: gameboy.gameboy.memory.hram, + oam: gameboy.gameboy.ppu.oam, } } fn ui(&mut self, ctx: &Context) { - egui::Window::new("MeowGB Debugger").open(&mut self.window_open).show(ctx, |ui| { + egui::Window::new("MeowGB Debugger").open(&mut self.state.window_open).show(ctx, |ui| { + if ui.button("Toggle Debugger Window").clicked() { + self.state.debugger_window_open = !self.state.debugger_window_open; + } + if ui.button("Toggle Register Window").clicked() { - self.register_window_open = !self.register_window_open; + self.state.register_window_open = !self.state.register_window_open; } if ui.button("Toggle PPU Window").clicked() { - self.ppu_register_window_open = !self.ppu_register_window_open; - } - - if ui.button("Toggle OAM Window").clicked() { - self.ppu_register_window_open = !self.ppu_register_window_open; + self.state.ppu_register_window_open = !self.state.ppu_register_window_open; } if ui.button("Toggle BG Tiles Window").clicked() { - self.ppu_register_window_open = !self.ppu_register_window_open; + self.state.ppu_register_window_open = !self.state.ppu_register_window_open; + } + + if ui.button("Toggle WRAM Window").clicked() { + self.state.wram_window_open = !self.state.wram_window_open; + } + + if ui.button("Toggle HRAM Window").clicked() { + self.state.hram_window_open = !self.state.hram_window_open; + } + + if ui.button("Toggle OAM Window").clicked() { + self.state.oam_window_open = !self.state.oam_window_open; } }); - egui::Window::new("Register State").open(&mut self.register_window_open).show(ctx, |ui| { - ui.label(format!("AF: {:04X}", self.registers.get_af())); - ui.label(format!("BC: {:04X}", self.registers.get_bc())); - ui.label(format!("DE: {:04X}", self.registers.get_de())); - ui.label(format!("HL: {:04X}", self.registers.get_hl())); - ui.label(format!("SP: {:04X}", self.registers.get_sp())); - ui.label(format!("PC: {:04X}", self.registers.pc)); + egui::Window::new("Register State").open(&mut self.state.register_window_open).show( + ctx, + |ui| { + ui.label(format!("AF: {:04X}", self.registers.get_af())); + ui.label(format!("BC: {:04X}", self.registers.get_bc())); + ui.label(format!("DE: {:04X}", self.registers.get_de())); + ui.label(format!("HL: {:04X}", self.registers.get_hl())); + ui.label(format!("SP: {:04X}", self.registers.get_sp())); + ui.label(format!("PC: {:04X}", self.registers.pc)); + }, + ); + + egui::Window::new("Debugger").open(&mut self.state.debugger_window_open).show(ctx, |ui| { + if ui.button("Step").clicked() { + let _ = self.sender.send(EmulatorWindowEvent::Debug(EmulatorDebugEvent::Step)); + } + if ui.button("Continue").clicked() { + let _ = self.sender.send(EmulatorWindowEvent::Debug(EmulatorDebugEvent::Continue)); + } + ui.label("Toggle Breakpoint"); + ui.text_edit_singleline(&mut self.bp_string); + self.bp_string.retain(|x| x.is_ascii_hexdigit()); + if let Some((fourth_index, _)) = self.bp_string.char_indices().nth(4) { + self.bp_string.truncate(fourth_index); + } + Grid::new("debugger_bp_select_grid").show(ui, |ui| { + let address = u16::from_str_radix(self.bp_string.as_str(), 16).unwrap_or_default(); + ui.label(format!("({:#X}) ", address)); + let [read, write, execute] = &mut self.breakpoints[address as usize]; + let mut changed = ui.checkbox(read, "Read").clicked(); + changed |= ui.checkbox(write, "Write").clicked(); + changed |= ui.checkbox(execute, "Execute").clicked(); + if changed { + let _ = self.sender.send(EmulatorWindowEvent::Debug( + EmulatorDebugEvent::ToggleBreakpoint( + address, + self.breakpoints[address as usize], + ), + )); + } + }); + + Grid::new("debugger_bp_list_grid").show(ui, |ui| { + ui.heading("Enabled BPs"); + ui.end_row(); + for (idx, [read, write, execute]) in self.breakpoints.iter_mut().enumerate() { + if *read || *write || *execute { + ui.label(format!("{:#X}: ", idx)); + let mut changed = ui.checkbox(read, "Read").clicked(); + changed |= ui.checkbox(write, "Write").clicked(); + changed |= ui.checkbox(execute, "Execute").clicked(); + if changed { + let _ = self.sender.send(EmulatorWindowEvent::Debug( + EmulatorDebugEvent::ToggleBreakpoint( + idx as u16, + [*read, *write, *execute], + ), + )); + } + ui.end_row(); + } + } + }); }); - egui::Window::new("PPU State").open(&mut self.ppu_register_window_open).show(ctx, |ui| { - ui.label(format!("Mode: {:?}", self.ppu_registers.mode)); - ui.label(format!("LCDC: {:02X}", self.ppu_registers.lcdc)); - ui.label(format!( - "Stat: {:02X}", - (1 << 7) - | self.ppu_registers.stat_flags.flag_bits() - | ((match (self.ppu_registers.lcdc >> 7) == 1 { - true => self.ppu_registers.ly == self.ppu_registers.lyc, - false => self.ppu_registers.ly_lyc, - } as u8) << 2) | self.ppu_registers.mode.mode_flag() - )); - ui.label(format!("SCY: {:02X}", self.ppu_registers.scy)); - ui.label(format!("SCX: {:02X}", self.ppu_registers.scx)); - ui.label(format!("LY: {:02X}", self.ppu_registers.ly)); - ui.label(format!("LYC: {:02X}", self.ppu_registers.lyc)); - ui.label(format!("WY: {:02X}", self.ppu_registers.wy)); - ui.label(format!("WX: {:02X}", self.ppu_registers.wx)); + egui::Window::new("PPU State").open(&mut self.state.ppu_register_window_open).show( + ctx, + |ui| { + ui.label(format!("Mode: {:?}", self.ppu_registers.mode)); + ui.label(format!("LCDC: {:02X}", self.ppu_registers.lcdc)); + ui.label(format!( + "Stat: {:02X}", + (1 << 7) + | self.ppu_registers.stat_flags.flag_bits() + | ((match (self.ppu_registers.lcdc >> 7) == 1 { + true => self.ppu_registers.ly == self.ppu_registers.lyc, + false => self.ppu_registers.ly_lyc, + } as u8) << 2) | self.ppu_registers.mode.mode_flag() + )); + ui.label(format!("SCY: {:02X}", self.ppu_registers.scy)); + ui.label(format!("SCX: {:02X}", self.ppu_registers.scx)); + ui.label(format!("LY: {:02X}", self.ppu_registers.ly)); + ui.label(format!("LYC: {:02X}", self.ppu_registers.lyc)); + ui.label(format!("WY: {:02X}", self.ppu_registers.wy)); + ui.label(format!("WX: {:02X}", self.ppu_registers.wx)); + }, + ); + + egui::Window::new("WRAM").vscroll(true).open(&mut self.state.wram_window_open).show(ctx, |ui| { + egui::Grid::new("memory_ov_wram").show(ui, |ui| { + ui.monospace(" 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F"); + for mem_row_idx in 0xC00..(0xE00) { + let row_base = (mem_row_idx * 0x10) - 0xC000; + ui.end_row(); + ui.monospace(format!("{:X}: {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X}", mem_row_idx * 0x10, self.wram[row_base + 0], self.wram[row_base + 1], self.wram[row_base + 2], self.wram[row_base + 3], self.wram[row_base + 4], self.wram[row_base + 5], self.wram[row_base + 6], self.wram[row_base + 7], self.wram[row_base + 8], self.wram[row_base + 9], self.wram[row_base + 10], self.wram[row_base + 11], self.wram[row_base + 12], self.wram[row_base + 13], self.wram[row_base + 14], self.wram[row_base + 15])); + } + }); + }); + + egui::Window::new("HRAM").vscroll(true).open(&mut self.state.hram_window_open).show(ctx, |ui| { + egui::Grid::new("memory_ov_hram").show(ui, |ui| { + ui.label("ROW: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F"); + for mem_row_idx in 0x0..0xA { + let row_base = mem_row_idx * 0x10; + ui.end_row(); + ui.monospace(format!("{:X}: {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X}", 0xFF80 + row_base, self.hram[row_base + 0], self.hram[row_base + 1], self.hram[row_base + 2], self.hram[row_base + 3], self.hram[row_base + 4], self.hram[row_base + 5], self.hram[row_base + 6], self.hram[row_base + 7], self.hram[row_base + 8], self.hram[row_base + 9], self.hram[row_base + 10], self.hram[row_base + 11], self.hram[row_base + 12], self.hram[row_base + 13], self.hram[row_base + 14], self.hram[row_base + 15])); + } + + let row_base = 0xA0; + ui.end_row(); + ui.monospace(format!("FFF0: {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} ??", self.hram[row_base + 0], self.hram[row_base + 1], self.hram[row_base + 2], self.hram[row_base + 3], self.hram[row_base + 4], self.hram[row_base + 5], self.hram[row_base + 6], self.hram[row_base + 7], self.hram[row_base + 8], self.hram[row_base + 9], self.hram[row_base + 10], self.hram[row_base + 11], self.hram[row_base + 12], self.hram[row_base + 13], self.hram[row_base + 14])); + }); + }); + + egui::Window::new("OAM").vscroll(true).open(&mut self.state.oam_window_open).show(ctx, |ui| { + egui::Grid::new("memory_ov_oam").show(ui, |ui| { + ui.label("ROW: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F"); + for mem_row_idx in 0xC00..(0xC0A) { + let row_base = (mem_row_idx * 0x10) - 0xC000; + ui.end_row(); + ui.label(format!("{:X}: {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X}", mem_row_idx * 0x10, self.hram[row_base + 0], self.hram[row_base + 1], self.hram[row_base + 2], self.hram[row_base + 3], self.hram[row_base + 4], self.hram[row_base + 5], self.hram[row_base + 6], self.hram[row_base + 7], self.hram[row_base + 8], self.hram[row_base + 9], self.hram[row_base + 10], self.hram[row_base + 11], self.hram[row_base + 12], self.hram[row_base + 13], self.hram[row_base + 14], self.hram[row_base + 15])); + } + + let row_base = 0xA0; + ui.end_row(); + ui.label(format!("CF00: {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} ??", self.hram[row_base + 0], self.hram[row_base + 1], self.hram[row_base + 2], self.hram[row_base + 3], self.hram[row_base + 4], self.hram[row_base + 5], self.hram[row_base + 6], self.hram[row_base + 7], self.hram[row_base + 8], self.hram[row_base + 9], self.hram[row_base + 10], self.hram[row_base + 11], self.hram[row_base + 12], self.hram[row_base + 13], self.hram[row_base + 14])); + }); }); } } diff --git a/run-test-roms.sh b/run-test-roms.sh index e315aca..7626947 100755 --- a/run-test-roms.sh +++ b/run-test-roms.sh @@ -77,6 +77,16 @@ else echo "Failed: $res" fi +echo "Running test ROM ./test-roms/mooneye-test-suite/roms/di_timing-GS.gb" + +TEST_TOTAL=$((TEST_TOTAL + 1)) + +if res=$(./target/release/meowgb-tests test-roms/mooneye-test-suite/roms/di_timing-GS.gb test -m 100000000 -s meowgb-tests/expected_output/di_timing-GS.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/roms/div_timing.gb" TEST_TOTAL=$((TEST_TOTAL + 1)) @@ -127,6 +137,36 @@ else echo "Failed: $res" fi +echo "Running test ROM ./test-roms/mooneye-test-suite/roms/halt_ime0_nointr_timing.gb" + +TEST_TOTAL=$((TEST_TOTAL + 1)) + +if res=$(./target/release/meowgb-tests test-roms/mooneye-test-suite/roms/halt_ime0_nointr_timing.gb test -m 100000000 -s meowgb-tests/expected_output/halt_ime0_nointr_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/roms/halt_ime1_timing.gb" + +TEST_TOTAL=$((TEST_TOTAL + 1)) + +if res=$(./target/release/meowgb-tests test-roms/mooneye-test-suite/roms/halt_ime1_timing.gb test -m 100000000 -s meowgb-tests/expected_output/halt_ime1_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/roms/halt_ime1_timing2-GS.gb" + +TEST_TOTAL=$((TEST_TOTAL + 1)) + +if res=$(./target/release/meowgb-tests test-roms/mooneye-test-suite/roms/halt_ime1_timing2-GS.gb test -m 100000000 -s meowgb-tests/expected_output/halt_ime1_timing2-GS.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/roms/intr_1_2_timing-GS.gb" TEST_TOTAL=$((TEST_TOTAL + 1)) @@ -187,6 +227,26 @@ else echo "Failed: $res" fi +echo "Running test ROM ./test-roms/mooneye-test-suite/roms/pop_timing.gb" + +TEST_TOTAL=$((TEST_TOTAL + 1)) + +if res=$(./target/release/meowgb-tests test-roms/mooneye-test-suite/roms/pop_timing.gb test -m 100000000 -s meowgb-tests/expected_output/pop_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/roms/push_timing.gb" + +TEST_TOTAL=$((TEST_TOTAL + 1)) + +if res=$(./target/release/meowgb-tests test-roms/mooneye-test-suite/roms/push_timing.gb test -m 100000000 -s meowgb-tests/expected_output/push_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/roms/rapid_di_ei.gb" TEST_TOTAL=$((TEST_TOTAL + 1)) @@ -227,6 +287,16 @@ else echo "Failed: $res" fi +echo "Running test ROM ./test-roms/mooneye-test-suite/roms/rst_timing.gb" + +TEST_TOTAL=$((TEST_TOTAL + 1)) + +if res=$(./target/release/meowgb-tests test-roms/mooneye-test-suite/roms/rst_timing.gb test -m 100000000 -s meowgb-tests/expected_output/rst_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/roms/stat_irq_blocking.gb" TEST_TOTAL=$((TEST_TOTAL + 1)) diff --git a/test-roms/mooneye-test-suite/roms/di_timing-GS.gb b/test-roms/mooneye-test-suite/roms/di_timing-GS.gb new file mode 100644 index 0000000000000000000000000000000000000000..9c3dfd5347c367ecba9ff60687150209e8363409 GIT binary patch literal 32768 zcmeI4UuYav6vppNwsG>OajG>;!r0C(OW20Aq}zZ?7{}RhNG5GCp)?diz_6idU)ls( zWRq;R38LszeaM4_V14VGp^p+2WGQ5ARs=;5TA3HCf|f`SHI>ABaysr>@u?3*`+aa{ z=iGD8z32Syy-QxQJ3Ei2A093Iu}6#2u=3eYlauRvMN{2H@w#Xhdxf^|;9GBtmfPzW z?%uic_0`qYvlACueq6tE`|7pT505$Lr=~8PnK^U#Y|grL=HewWIx3`;W8(0q6Pu0_ z?!?b48~*kcYfC=gv9?yir|-WeO6yAeP36AR(C?Ion({ZS`xGU_X6ZX6{@%X?>{0&_ zuqigUYX#ZCQ=-+Au6qx=P7aN{;#)`A z+pPXIAqTQIP2R`8M@Rq!KmY_l00ck)1V8`;KmY_l00ck)1V8`;KmY_l00ck)1V8`; zKmY_l00ck)1V8`;KmY_l00ck)1V8`;KmY_l00ck)1V8`;KmY_l00ck)1V8`;KmY`u z1Omkx(SM`Ym_^wCKOVHFwbC9)2u-8@lvJ&sdI|+@)URm=4y011+1O~?6fr)&v~=!V zYwMvy_4VX8O-rh+t)ru@jmPu6^iI+=U8fPc&i(WmlcIj|cXpblus z$7|5|R4NDx1)jfHblpNhRU^M`lT9n|_+s(z>(hMHB0s-7j;dB`;F+uSMm7_wuQ!bG zal>HVlrv?@*^e@zDlZo4>3KAscb51&4%wzjry&-L#`}H{ z3=dCF2Z8UCpH^aV#o6q{#K_3NKvs(Uo|ns=JbC_V2IG zS3NIWUnWDhg~a?_m+ww2M%mK-?#4DWG_haTAHKgdip29p_m2DMY1^vmIDLJ+y<`y? zI={Kx*cjO)zSCU4D&;ty7meo|L$@G0zlNdfnT#&Q(L(g}3=aB!Pfsky)nqcnCz&Ka zo$UuWckegZ(T&u!>1nDwnM|d+ySut*Jk85uzSK84IXX(z>78`sNIFfM&K74F?GN>h zjpcHlM<>;F$?ol?(?WMp-DTR&fp8;LxJ4@fJvst1-Y6tdq zJa;5vXpYxEbo$k^Q*Py(Ak&_&d=SjYHY?klYz5ioWiy`1hssQ-%!bNbs1!nFK2%&| zdnNS3m%|g`ac5uXI)8-yr7wfVy6bD(jj6hX@r(R<8)Jj=DX#I)=-m3a94gLYsII(7 zl~})*n zylB(3nlyi)1{x5Ww?JQxLTLzrKsl0nT(0D$fkFcN5+TrrEfU(MMLXY)lhcHPkcU9> zeYmsx&G%yt4v1rHi3|Dyf z*5Jaud%s@1c5R?{u<*BA_wHW&=GxW0+9yLpgMCN)b`A8XqkSWzTt^2-gx|~U%DKGg zm`i!;V~w|$6ZU5B0RPVsF8P4>dIX|^+Y?FHo+DIoJni87)&KggcI{f+GrfHOmm81v zaCX9;wolnJ_Luf)`>cJ&K5w7XitMxA>E-l-w#7aJ9}gG3&i>?^P2|OsSLTA1xnnhi z8=Gs@NVxd`S7M|Vz4^jZrp9*<)VH>_wYc^dzDYO%B!C2v01`j~NB{{S0VIF~kN^@u z0!RP}AOR$R1dsp{Kmter2_OL^fCP{L57` zWUaI(77GO60ZkOQZw~|%B`+@&f{5K{$+F*X z5b^tE8FW#UWymdxiUN8lWEdWgEURkVt$(H)1|0+Z1A#~+7Nhy&ab1ta1cBK@A<$t2 z>K~7<9Ut<+!|e21(*z;A2AVm$UZzt*xw*33-7U+MH_RDkN@oiiwSf<4!JuivdN__o z!wOAvZfXbK( zKRG!!$Kqk-@i?@G0seGnfxo7Kt|(9q9uM<3%}AuBWq3FeF-@?;NL1*G!{OfEwzkH` zFcGjDMo&*`Yg1E01I@1}z7?VSOV>v;fd6W{Ah7kfw6O8OE=dIiJ|B%gejMgXg^owp z!uE4&3UogaT3;})#>Td`-d;98u+S=m_;A?g+qyNIFIz8keZe4{77)r`*Xh~ucwn}$ zzi#Ztjrr6rNl%|&@B*Rv*tw&9(1t>SplJ;awY8uD8K~c$p3Y9tLFk#L>lcV-A zw6e0g8pZ`1UA-*B+^H@}p%Bauaetv+vVVvnJfnDT+nDEF*-{H|iLxWGuMr8a-U<-{sKyJNJ`#(7Jao>krD$(vt zcaadnD_s}Z3Bep`YE{meiaMvR1`0CY!<{o;mD?(BBOg(4mGj9LKqkVaEjO-##Z5+? znf1ic*+7E#+yn+oa3bn_xt;{+_uaTU7WZk?IlZ2^?7+Ql0+S^;8FkLCCqeonH?E$= zeHL}jtS2rz@Q9lr!V-KDbep6PLVTkqd|T`lcKAGqSNNLciSi5MKO2qA(%D3(I$)aZLkG6LW z4C$F~tzg+$X3#oHo^kRVBTtMx6XcP%jM@BAn;*CNV>TbN`3alX<)vb`c*3N{%1Cj& z?rIGyT5&OfL>-JZ4t38um}?yBpLK|=aUgdlxdXaGq!PAa*V&fcMDASDwSVpX$xBvV z&gJ=~yg-gmene&=I{+mIf@oMX-Wat0uHniBgPN zH`+9<=KsM!0z&f^=!+CeLkI-QlGJ6hB`*yW5}23x)RrkB^cU?td%W4Tc@dfrDCgk4 zJLjICd+&G8UDyX_b@lm5V#@P;6TaYVLZaRC{m+Agcee_Kd1Jy!p-9*&NF}>w=Y--1 zcZZiBKK${@&6`90!^J<{efZ$Y7dLP0*FPE=86G$`uxF@G8y^@O7rMFxCZ7Gmp3a`X z7Mpdiv#Pt!CVi8$`D5>6-(gSU_15$yJAL1Nhq-Hm?;ZJY;C-RYOg`Q>yZY#d+fVih z&Z0BxoOb4%v(6dkymQXE=v>eR=d5pbHFf{7&uwNYUN8wtPXven5g-CYfCvx)B0vO) z01+SpM1Tko0U|&IhyW2F0z`la5CI}U1c(3;AOb{y2oM1xKm>>Y5g-CYfCvx)B0vO) z01+SpM1Tko0U|&IhyW2F0z`la5CI}U1c(3;AObIwKs-m_zu0rsIO+dC9&o0)BCUxa zND_<(ndI7GBo@O4Y$Rbx_KURhaQj{cYz zo#AB+1sVt}P}iYHo%cKf^l+GuWRrRk4o-ghXGk|}tT@?BL+uQkkV3*~hBEKKwPn>|YqTqbE z7k-{IGoS~UV12>5T3R|f`uq9%z=Bl>@eK`r|Mui!5{LXAYT|89PH`>>hKJ*cW-Si96D;aLVi9l*4^FLXPQu{h5>qgJyZ+aK{3O$ zy1KR&<^>z>UQuA}sLOIV3@gaa4|7#kR#c!JYB2l#0%J2wy1E7jvv#O~TpepbmhsNQ zH!I8n{;Dd=HB4j~`EyphV&MFXm`D;|A4JvGwza`0oO)J3FPUD&SJ50A7&Ym$3Tz2BI zOo9kc@Ojj|xRC^@_f zk=K02YHG!`RxW6Nvi@SGfBHOmO)JjNE8bKl?c7?qW3OM4bhA0y*)ud^q`$O76%*-U z`xtAJtQ}`9#@ZBX%C-r|bIkEfI-cW>C+2vj9FL)_lxB*jfz?T>Y5g-CYfCvx)B0vO)01+SpM1Tko0U|&IhyW2F0z`la5CI}U w1c(3;AOb{y2oM1xKm>>Y5g-CYfCvx)B0vO)01+SpM1Tko0U|&I{yTwx0IkYj00000 literal 0 HcmV?d00001 diff --git a/test-roms/mooneye-test-suite/roms/halt_ime1_timing2-GS.gb b/test-roms/mooneye-test-suite/roms/halt_ime1_timing2-GS.gb new file mode 100644 index 0000000000000000000000000000000000000000..810c2cfcbe5c6f4c5bfa7b46e4a0ef820a1dddc0 GIT binary patch literal 32768 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