feat: add more debug tools to gui

This commit is contained in:
Rekai Nyangadzayi Musuka 2021-12-09 05:21:05 -04:00
parent bbcbbd8ce3
commit e72b11f946
7 changed files with 172 additions and 92 deletions

View File

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

View File

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

View File

@ -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<KeyboardInput>] 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))
}
}

View File

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

View File

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

View File

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

View File

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