From e72b11f9468e0f955347ffc65666a7e4d15136cd Mon Sep 17 00:00:00 2001 From: Rekai Musuka Date: Thu, 9 Dec 2021 05:21:05 -0400 Subject: [PATCH] feat: add more debug tools to gui --- src/cpu.rs | 22 +++----- src/emu.rs | 12 ++-- src/gui.rs | 47 ++++++++++++++- src/instruction.rs | 138 ++++++++++++++++++++++----------------------- src/lib.rs | 2 + src/main.rs | 15 ++++- src/ppu.rs | 28 +++++++++ 7 files changed, 172 insertions(+), 92 deletions(-) diff --git a/src/cpu.rs b/src/cpu.rs index d0eb567..5b2c82f 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -73,15 +73,12 @@ impl Cpu { /// /// If opcode == 0xCB, then decoding costs 4 cycles. /// Otherwise, decoding is free - pub(crate) fn decode(&mut self, mut opcode: u8) -> Instruction { - let instr = if opcode == 0xCB { - opcode = self.fetch(); - Instruction::decode(opcode, true) + pub(crate) fn decode(&mut self, opcode: u8) -> Instruction { + if opcode == 0xCB { + Instruction::decode(self.fetch(), true) } else { Instruction::decode(opcode, false) - }; - - instr.unwrap_or_else(|| panic!("{:#04X} is an invalid instruction", opcode)) + } } /// Execute an [Instruction]. @@ -366,15 +363,12 @@ impl Cpu { } fn _dbg_instr(&self) -> Instruction { - let mut byte = self.read_byte(self.reg.pc); - let instr = if byte == 0xCB { - byte = self.read_byte(self.reg.pc + 1); - Instruction::decode(byte, true) + let byte = self.read_byte(self.reg.pc); + if byte == 0xCB { + Instruction::decode(self.read_byte(self.reg.pc + 1), true) } else { Instruction::decode(byte, false) - }; - - instr.unwrap_or_else(|| panic!("{:#04X} is an invalid instruction", byte)) + } } } diff --git a/src/emu.rs b/src/emu.rs index 56ac6a4..89f08ec 100644 --- a/src/emu.rs +++ b/src/emu.rs @@ -18,7 +18,13 @@ pub const CYCLES_IN_FRAME: Cycle = 456 * 154; // 456 Cycles times 154 scanlines pub(crate) const SM83_CLOCK_SPEED: u64 = 0x40_0000; // Hz which is 4.194304Mhz const DEFAULT_TITLE: &str = "Game Boy Screen"; -pub fn run_frame(cpu: &mut Cpu, gamepad: &mut Gilrs, key: KeyboardInput) -> Cycle { +#[inline] +pub fn run_frame(cpu: &mut Cpu, gamepad: &mut Gilrs, key: KeyboardInput) { + run(cpu, gamepad, key, CYCLES_IN_FRAME) +} + +#[inline] +pub fn run(cpu: &mut Cpu, gamepad: &mut Gilrs, key: KeyboardInput, cycles: Cycle) { let mut elapsed = 0; if let Some(event) = gamepad.next_event() { @@ -26,11 +32,9 @@ pub fn run_frame(cpu: &mut Cpu, gamepad: &mut Gilrs, key: KeyboardInput) -> Cycl } crate::joypad::handle_keyboard_input(&mut cpu.bus.joyp, key); - while elapsed < CYCLES_IN_FRAME { + while elapsed < cycles { elapsed += cpu.step(); } - - elapsed } pub fn save_and_exit(cpu: &Cpu, control_flow: &mut ControlFlow) { diff --git a/src/gui.rs b/src/gui.rs index 59a18cb..75c2d4d 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -26,6 +26,7 @@ pub struct GuiState { /// When true, egui winit should exit the application pub quit: bool, pub title: String, + pub mode: EmuMode, } impl GuiState { @@ -33,10 +34,19 @@ impl GuiState { Self { title, quit: Default::default(), + mode: EmuMode::Running, } } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EmuMode { + Running, + StepFrame, + Stopped, + Step, +} + /// To avoid using an [Option] to keep track of user input from winit, /// we can use a "default" value. However, in order for this to work the chosen "default" /// value must be an **unused** key, so that it is ignored by the emulator. @@ -223,7 +233,7 @@ pub fn execute_render_pass( #[inline] pub fn draw_egui(cpu: &Cpu, app: &mut GuiState, ctx: &CtxRef, texture_id: TextureId) { - use crate::{cpu, instruction}; + use crate::{cpu, instruction, ppu}; fn selectable_text(ui: &mut egui::Ui, mut text: &str) -> egui::Response { ui.add(egui::TextEdit::multiline(&mut text).code_editor()) @@ -247,6 +257,17 @@ pub fn draw_egui(cpu: &Cpu, app: &mut GuiState, ctx: &CtxRef, texture_id: Textur selectable_text(ui, &instruction::dbg::tmp_disasm(cpu, 20)); }); + egui::Window::new("Settings").show(ctx, |ui| { + egui::ComboBox::from_label("Emulation Mode") + .selected_text(format!("{:?}", app.mode)) + .show_ui(ui, |ui| { + ui.selectable_value(&mut app.mode, EmuMode::Running, "Running"); + ui.selectable_value(&mut app.mode, EmuMode::Stopped, "Stopped"); + ui.selectable_value(&mut app.mode, EmuMode::StepFrame, "Step Frame"); + ui.selectable_value(&mut app.mode, EmuMode::Step, "Step"); + }) + }); + egui::Window::new("GB Info").show(ctx, |ui| { ui.horizontal(|ui| { ui.vertical(|ui| { @@ -262,10 +283,21 @@ pub fn draw_egui(cpu: &Cpu, app: &mut GuiState, ctx: &CtxRef, texture_id: Textur }); ui.vertical(|ui| { + let ppu = &cpu.bus.ppu; + ui.heading("PPU"); - ui.monospace("LY: ?"); - ui.monospace("SCX: ?"); + ui.monospace(format!("LY: {}", ppu::dbg::ly(ppu))); + ui.horizontal(|ui| { + ui.monospace(format!("SCX: {}", ppu::dbg::scx(ppu))); + ui.monospace(format!("SCY: {}", ppu::dbg::scy(ppu))); + }); + ui.horizontal(|ui| { + ui.monospace(format!("WX: {}", ppu::dbg::wx(ppu))); + ui.monospace(format!("WY: {}", ppu::dbg::wy(ppu))); + }); + + ui.monospace(format!("Mode: {:?}", ppu::dbg::mode(ppu))) }); }); @@ -312,3 +344,12 @@ pub fn draw_egui(cpu: &Cpu, app: &mut GuiState, ctx: &CtxRef, texture_id: Textur }) }); } + +pub mod kbd { + use winit::event::{ElementState, KeyboardInput, VirtualKeyCode}; + + pub fn space_released(input: &KeyboardInput) -> bool { + let keycode = input.virtual_keycode; + matches!(input.state, ElementState::Released if keycode == Some(VirtualKeyCode::Space)) + } +} diff --git a/src/instruction.rs b/src/instruction.rs index 93e4dd5..caa6cef 100644 --- a/src/instruction.rs +++ b/src/instruction.rs @@ -58,6 +58,7 @@ pub(crate) enum Instruction { BIT(u8, Register), RES(u8, Register), SET(u8, Register), + Invalid, } impl std::fmt::Debug for Instruction { @@ -109,6 +110,7 @@ impl std::fmt::Debug for Instruction { BIT(b, r) => write!(f, "BIT {}, {:?}", b, r), RES(b, r) => write!(f, "RES {}, {:?}", b, r), SET(b, r) => write!(f, "SET {}, {:?}", b, r), + Invalid => f.write_str("???"), } } } @@ -1294,6 +1296,7 @@ impl Instruction { } } } + Instruction::Invalid => panic!("Attempted to execute invalid instruction"), } } @@ -1538,7 +1541,7 @@ impl Instruction { } impl Instruction { - pub(crate) fn decode(byte: u8, prefixed: bool) -> Option { + pub(crate) fn decode(byte: u8, prefixed: bool) -> Self { if prefixed { Self::prefixed(byte) } else { @@ -1546,139 +1549,132 @@ impl Instruction { } } - fn unprefixed(byte: u8) -> Option { + fn unprefixed(byte: u8) -> Self { use Instruction::*; match byte { // NOP - 0o000 => Some(NOP), + 0o000 => NOP, // LD (u16), SP - 0o010 => Some(LD(LDTarget::IndirectImmediateWord, LDSource::SP)), + 0o010 => LD(LDTarget::IndirectImmediateWord, LDSource::SP), // STOP - 0o020 => Some(STOP), + 0o020 => STOP, // JR i8 - 0o030 => Some(JR(JpCond::Always)), + 0o030 => JR(JpCond::Always), // JR cond i8 - 0o040 | 0o050 | 0o060 | 0o070 => Some(JR(jump_cond((byte >> 3) & 0x03))), + 0o040 | 0o050 | 0o060 | 0o070 => JR(jump_cond((byte >> 3) & 0x03)), // LD r16, u16 - 0o001 | 0o021 | 0o041 | 0o061 => Some(LD( + 0o001 | 0o021 | 0o041 | 0o061 => LD( LDTarget::Group1(group1((byte >> 4) & 0x03)), LDSource::ImmediateWord, - )), + ), // ADD HL, r16 - 0o011 | 0o031 | 0o051 | 0o071 => Some(ADD( - AddTarget::HL, - AddSource::Group1(group1((byte >> 4) & 0x03)), - )), + 0o011 | 0o031 | 0o051 | 0o071 => { + ADD(AddTarget::HL, AddSource::Group1(group1((byte >> 4) & 0x03))) + } // LD (r16), A - 0o002 | 0o022 | 0o042 | 0o062 => Some(LD( + 0o002 | 0o022 | 0o042 | 0o062 => LD( LDTarget::IndirectGroup2(group2((byte >> 4) & 0x03)), LDSource::A, - )), + ), // LD A, (r16) - 0o012 | 0o032 | 0o052 | 0o072 => Some(LD( + 0o012 | 0o032 | 0o052 | 0o072 => LD( LDTarget::A, LDSource::IndirectGroup2(group2((byte >> 4) & 0x03)), - )), + ), // INC r16 - 0o003 | 0o023 | 0o043 | 0o063 => { - Some(INC(AllRegisters::Group1(group1((byte >> 4) & 0x03)))) - } + 0o003 | 0o023 | 0o043 | 0o063 => INC(AllRegisters::Group1(group1((byte >> 4) & 0x03))), // DEC r16 - 0o013 | 0o033 | 0o053 | 0o073 => { - Some(DEC(AllRegisters::Group1(group1((byte >> 4) & 0x03)))) - } + 0o013 | 0o033 | 0o053 | 0o073 => DEC(AllRegisters::Group1(group1((byte >> 4) & 0x03))), // INC r8 0o004 | 0o014 | 0o024 | 0o034 | 0o044 | 0o054 | 0o064 | 0o074 => { - Some(INC(AllRegisters::Register(register((byte >> 3) & 0x07)))) + INC(AllRegisters::Register(register((byte >> 3) & 0x07))) } // DEC r8 0o005 | 0o015 | 0o025 | 0o035 | 0o045 | 0o055 | 0o065 | 0o075 => { - Some(DEC(AllRegisters::Register(register((byte >> 3) & 0x07)))) + DEC(AllRegisters::Register(register((byte >> 3) & 0x07))) } // LD r8, u8 - 0o006 | 0o016 | 0o026 | 0o036 | 0o046 | 0o056 | 0o066 | 0o076 => Some(LD( + 0o006 | 0o016 | 0o026 | 0o036 | 0o046 | 0o056 | 0o066 | 0o076 => LD( LDTarget::Register(register((byte >> 3) & 0x07)), LDSource::ImmediateByte, - )), + ), // RLCA, RRCA, RLA, RRA, DAA, CPL, SCF, and CCF 0o007 | 0o017 | 0o027 | 0o037 | 0o047 | 0o057 | 0o067 | 0o077 => { - Some(flag_instr((byte >> 3) & 0x07)) + flag_instr((byte >> 3) & 0x07) } // HALT - 0o166 => Some(HALT), + 0o166 => HALT, // LD r8, r8 - 0o100..=0o177 => Some(LD( + 0o100..=0o177 => LD( LDTarget::Register(register((byte >> 3) & 0x07)), LDSource::Register(register(byte & 0x07)), - )), + ), // ADD, ADC, SUB, SBC, AND, XOR, OR, and CP - 0o200..=0o277 => Some(alu_reg_instr((byte >> 3) & 0x07, byte & 0x07)), + 0o200..=0o277 => alu_reg_instr((byte >> 3) & 0x07, byte & 0x07), // RET cond - 0o300 | 0o310 | 0o320 | 0o330 => Some(RET(jump_cond((byte >> 3) & 0x03))), + 0o300 | 0o310 | 0o320 | 0o330 => RET(jump_cond((byte >> 3) & 0x03)), // LD (0xFF00 + u8), A - 0o340 => Some(LD(LDTarget::IoWithImmediateOffset, LDSource::A)), + 0o340 => LD(LDTarget::IoWithImmediateOffset, LDSource::A), // ADD SP, i8 - 0o350 => Some(ADD(AddTarget::SP, AddSource::ImmediateSignedByte)), + 0o350 => ADD(AddTarget::SP, AddSource::ImmediateSignedByte), // LD A, (0xFF00 + u8) - 0o360 => Some(LD(LDTarget::A, LDSource::IoWithImmediateOffset)), + 0o360 => LD(LDTarget::A, LDSource::IoWithImmediateOffset), // LD HL, SP + i8 - 0o370 => Some(LDHL), + 0o370 => LDHL, // POP r16 - 0o301 | 0o321 | 0o341 | 0o361 => Some(POP(group3((byte >> 4) & 0x03))), + 0o301 | 0o321 | 0o341 | 0o361 => POP(group3((byte >> 4) & 0x03)), // RET - 0o311 => Some(RET(JpCond::Always)), + 0o311 => RET(JpCond::Always), // RETI - 0o331 => Some(RETI), + 0o331 => RETI, // JP HL - 0o351 => Some(JP(JpCond::Always, JpLoc::HL)), + 0o351 => JP(JpCond::Always, JpLoc::HL), // LD SP, HL - 0o371 => Some(LD(LDTarget::SP, LDSource::HL)), + 0o371 => LD(LDTarget::SP, LDSource::HL), // JP cond u16 0o302 | 0o312 | 0o322 | 0o332 => { - Some(JP(jump_cond((byte >> 3) & 0x03), JpLoc::ImmediateWord)) + JP(jump_cond((byte >> 3) & 0x03), JpLoc::ImmediateWord) } // LD (0xFF00 + C), A - 0o342 => Some(LD(LDTarget::IoWithC, LDSource::A)), + 0o342 => LD(LDTarget::IoWithC, LDSource::A), // LD (u16), A - 0o352 => Some(LD(LDTarget::IndirectImmediateWord, LDSource::A)), + 0o352 => LD(LDTarget::IndirectImmediateWord, LDSource::A), // LD A, (0xFF00 + C) - 0o362 => Some(LD(LDTarget::A, LDSource::IoWithC)), + 0o362 => LD(LDTarget::A, LDSource::IoWithC), // LD A, (u16) - 0o372 => Some(LD(LDTarget::A, LDSource::IndirectImmediateWord)), + 0o372 => LD(LDTarget::A, LDSource::IndirectImmediateWord), // JP u16 - 0o303 => Some(JP(JpCond::Always, JpLoc::ImmediateWord)), + 0o303 => JP(JpCond::Always, JpLoc::ImmediateWord), // DI - 0o363 => Some(DI), + 0o363 => DI, // EI - 0o373 => Some(EI), + 0o373 => EI, // CALL cond u16 - 0o304 | 0o314 | 0o324 | 0o334 => Some(CALL(jump_cond((byte >> 3) & 0x03))), + 0o304 | 0o314 | 0o324 | 0o334 => CALL(jump_cond((byte >> 3) & 0x03)), // PUSH r16 - 0o305 | 0o325 | 0o345 | 0o365 => Some(PUSH(group3((byte >> 4) & 0x03))), - 0o315 => Some(CALL(JpCond::Always)), + 0o305 | 0o325 | 0o345 | 0o365 => PUSH(group3((byte >> 4) & 0x03)), + 0o315 => CALL(JpCond::Always), 0o306 | 0o316 | 0o326 | 0o336 | 0o346 | 0o356 | 0o366 | 0o376 => { - Some(alu_imm_instr((byte >> 3) & 0x07)) + alu_imm_instr((byte >> 3) & 0x07) } - 0o307 | 0o317 | 0o327 | 0o337 | 0o347 | 0o357 | 0o367 | 0o377 => { - Some(RST(byte & 0b00111000)) - } - _ => None, // 0xCB is 0o313 + 0o307 | 0o317 | 0o327 | 0o337 | 0o347 | 0o357 | 0o367 | 0o377 => RST(byte & 0b00111000), + _ => Invalid, // 0xCB is 0o313 } } - fn prefixed(byte: u8) -> Option { + fn prefixed(byte: u8) -> Self { use Instruction::*; match byte { // RLC, RRC, RL, RR, SLA, SRA, SWAP and SRL - 0o000..=0o077 => Some(prefix_alu((byte >> 3) & 0x07, byte & 0x07)), + 0o000..=0o077 => prefix_alu((byte >> 3) & 0x07, byte & 0x07), // BIT bit, r8 - 0o100..=0o177 => Some(BIT((byte >> 3) & 0x07, register(byte & 0x07))), + 0o100..=0o177 => BIT((byte >> 3) & 0x07, register(byte & 0x07)), // RES bit, r8 - 0o200..=0o277 => Some(RES((byte >> 3) & 0x07, register(byte & 0x07))), + 0o200..=0o277 => RES((byte >> 3) & 0x07, register(byte & 0x07)), // SET bit, r8 - 0o300..=0o377 => Some(SET((byte >> 3) & 0x07, register(byte & 0x07))), + 0o300..=0o377 => SET((byte >> 3) & 0x07, register(byte & 0x07)), } } } @@ -2163,7 +2159,7 @@ pub(crate) mod dbg { use super::{AllRegisters, BusIo, Cpu, Instruction, RegisterPair}; pub(crate) fn tmp_disasm(cpu: &Cpu, limit: u8) -> String { - let mut asm = String::new(); + let mut sm83_asm = String::new(); let mut pc = cpu.register_pair(RegisterPair::PC); for _ in 0..limit { @@ -2179,15 +2175,17 @@ pub(crate) mod dbg { Instruction::unprefixed(opcode) }; - if let Some(instr) = maybe_instr { - let instr_asm = format!("{:04X} {:?}\n", pc - 1, instr); - asm.push_str(&instr_asm); - - pc += delta::pc_inc_count(instr) + match maybe_instr { + Instruction::Invalid => {} + instr => { + let asm = format!("{:04X} {:?}\n", pc - 1, instr); + sm83_asm.push_str(&asm); + pc += delta::pc_inc_count(instr) + } } } - asm + sm83_asm } fn disasm(cpu: &Cpu, pc: u16, instr: Instruction) -> String { diff --git a/src/lib.rs b/src/lib.rs index 913df40..014deff 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ +#![allow(clippy::derivable_impls)] // Will remove this if bitfield-rs allows default impls + pub use apu::gen::init as spsc_init; pub type Cycle = u64; diff --git a/src/main.rs b/src/main.rs index 7661a87..5dd658b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ use std::time::Instant; use clap::{crate_authors, crate_description, crate_name, crate_version, App, Arg}; use egui_wgpu_backend::RenderPass; +use gb::gui::EmuMode; use gb::{emu, gui}; use gilrs::Gilrs; use gui::GuiState; @@ -129,7 +130,19 @@ fn main() { emu::save_and_exit(&cpu, control_flow); } - emu::run_frame(&mut cpu, &mut gamepad, last_key); + match app.mode { + EmuMode::Running => emu::run_frame(&mut cpu, &mut gamepad, last_key), + EmuMode::StepFrame if gui::kbd::space_released(&last_key) => { + emu::run_frame(&mut cpu, &mut gamepad, last_key) + } + EmuMode::Step if gui::kbd::space_released(&last_key) => { + emu::run(&mut cpu, &mut gamepad, last_key, 4); + } + _ => {} + }; + + // Input has been consumed, reset it + last_key = gui::unused_key(); window.request_redraw(); } diff --git a/src/ppu.rs b/src/ppu.rs index d27d71c..bf91176 100644 --- a/src/ppu.rs +++ b/src/ppu.rs @@ -910,3 +910,31 @@ struct WindowStatus { /// drawing from the window tile map is true enabled: bool, } + +pub(crate) mod dbg { + use super::{Ppu, PpuMode}; + + pub(crate) fn ly(ppu: &Ppu) -> u8 { + ppu.pos.line_y + } + + pub(crate) fn scx(ppu: &Ppu) -> u8 { + ppu.pos.scroll_x + } + + pub(crate) fn scy(ppu: &Ppu) -> u8 { + ppu.pos.scroll_y + } + + pub(crate) fn mode(ppu: &Ppu) -> PpuMode { + ppu.stat.mode() + } + + pub(crate) fn wx(ppu: &Ppu) -> i16 { + ppu.pos.window_x as i16 + } + + pub(crate) fn wy(ppu: &Ppu) -> i16 { + ppu.pos.window_y as i16 + } +}