From 90d2da9272cf003bb39643005b2659b945ee953f Mon Sep 17 00:00:00 2001 From: Rekai Musuka Date: Tue, 30 Nov 2021 10:23:06 -0400 Subject: [PATCH] chore(gui): implement basic disassembler Also implement CPU and Interrupt debug information. Of note: 1. IE and IRQ status boxes are slightly misaligned 2. Whenever the disassembler accidentally reads into game data rather than executable code the emulator crashes * Thus I should turn Instruction decoding into a Result<> rather than panic on failure --- src/cpu.rs | 39 ++++++++++++ src/gui.rs | 77 +++++++++++++++++++++++- src/instruction.rs | 146 +++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 2 +- 4 files changed, 262 insertions(+), 2 deletions(-) diff --git a/src/cpu.rs b/src/cpu.rs index b753cf6..63bd934 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -485,3 +485,42 @@ pub(crate) enum ImeState { Pending, Enabled, } + +pub(crate) mod dbg { + use super::{Cpu, ImeState, RegisterPair}; + + pub(crate) fn flags(cpu: &Cpu) -> u8 { + cpu.flags.into() + } + + pub(crate) fn af(cpu: &Cpu) -> u16 { + cpu.register_pair(RegisterPair::AF) + } + + pub(crate) fn bc(cpu: &Cpu) -> u16 { + cpu.register_pair(RegisterPair::BC) + } + + pub(crate) fn de(cpu: &Cpu) -> u16 { + cpu.register_pair(RegisterPair::DE) + } + + pub(crate) fn hl(cpu: &Cpu) -> u16 { + cpu.register_pair(RegisterPair::HL) + } + + pub(crate) fn sp(cpu: &Cpu) -> u16 { + cpu.register_pair(RegisterPair::SP) + } + + pub(crate) fn pc(cpu: &Cpu) -> u16 { + cpu.register_pair(RegisterPair::PC) + } + + pub(crate) fn ime(cpu: &Cpu) -> bool { + match cpu.ime { + ImeState::Enabled => true, + _ => false, + } + } +} diff --git a/src/gui.rs b/src/gui.rs index 1a09df1..59a18cb 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -11,6 +11,7 @@ use winit::event::{ElementState, KeyboardInput}; use winit::event_loop::EventLoop; use winit::window::Window; +use crate::cpu::Cpu; use crate::{GB_HEIGHT, GB_WIDTH}; const EGUI_DIMENSIONS: (usize, usize) = (1280, 720); @@ -221,7 +222,13 @@ pub fn execute_render_pass( } #[inline] -pub fn draw_egui(app: &mut GuiState, ctx: &CtxRef, texture_id: TextureId) { +pub fn draw_egui(cpu: &Cpu, app: &mut GuiState, ctx: &CtxRef, texture_id: TextureId) { + use crate::{cpu, instruction}; + + fn selectable_text(ui: &mut egui::Ui, mut text: &str) -> egui::Response { + ui.add(egui::TextEdit::multiline(&mut text).code_editor()) + } + egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { egui::menu::menu(ui, "File", |ui| { if ui.button("Quit").clicked() { @@ -234,6 +241,74 @@ pub fn draw_egui(app: &mut GuiState, ctx: &CtxRef, texture_id: TextureId) { texture_id, [GB_WIDTH as f32 * SCALE, GB_HEIGHT as f32 * SCALE], ); + }); + + egui::Window::new("Disassembly").show(ctx, |ui| { + selectable_text(ui, &instruction::dbg::tmp_disasm(cpu, 20)); + }); + + egui::Window::new("GB Info").show(ctx, |ui| { + ui.horizontal(|ui| { + ui.vertical(|ui| { + ui.heading("CPU"); + + ui.monospace(format!("AF: {:#06X}", cpu::dbg::af(cpu))); + ui.monospace(format!("BC: {:#06X}", cpu::dbg::bc(cpu))); + ui.monospace(format!("DE: {:#06X}", cpu::dbg::de(cpu))); + ui.monospace(format!("HL: {:#06X}", cpu::dbg::hl(cpu))); + ui.add_space(10.0); + ui.monospace(format!("SP: {:#06X}", cpu::dbg::sp(cpu))); + ui.monospace(format!("PC: {:#06X}", cpu::dbg::pc(cpu))); + }); + + ui.vertical(|ui| { + ui.heading("PPU"); + + ui.monospace("LY: ?"); + ui.monospace("SCX: ?"); + }); + }); + + ui.add_space(10.0); + + let _ = ui.selectable_label(cpu::dbg::ime(cpu), "IME"); + ui.horizontal(|ui| { + let irq = cpu.int_request(); + + ui.label("IRQ:"); + let _ = ui.selectable_label(irq & 0b01 == 0x01, "VBlank"); + let _ = ui.selectable_label(irq >> 1 & 0x01 == 0x01, "LCD STAT"); + let _ = ui.selectable_label(irq >> 2 & 0x01 == 0x01, "Timer"); + let _ = ui.selectable_label(irq >> 3 & 0x01 == 0x01, "Serial"); + let _ = ui.selectable_label(irq >> 4 & 0x01 == 0x01, "Joypad"); + }); + + ui.horizontal(|ui| { + let ie = cpu.int_enable(); + + let r_len = ctx.fonts().glyph_width(egui::TextStyle::Body, 'R'); + let e_len = ctx.fonts().glyph_width(egui::TextStyle::Body, 'E'); + let q_len = ctx.fonts().glyph_width(egui::TextStyle::Body, 'Q'); + + ui.label("IE:"); + ui.add_space(q_len - (e_len - r_len)); + let _ = ui.selectable_label(ie & 0b01 == 0x01, "VBlank"); + let _ = ui.selectable_label(ie >> 1 & 0x01 == 0x01, "LCD STAT"); + let _ = ui.selectable_label(ie >> 2 & 0x01 == 0x01, "Timer"); + let _ = ui.selectable_label(ie >> 3 & 0x01 == 0x01, "Serial"); + let _ = ui.selectable_label(ie >> 4 & 0x01 == 0x01, "Joypad"); + }); + + ui.add_space(10.0); + + ui.horizontal(|ui| { + let flags = cpu::dbg::flags(cpu); + + let _ = ui.selectable_label((flags >> 7 & 0x01) == 0x01, "Zero"); + let _ = ui.selectable_label((flags >> 6 & 0x01) == 0x01, "Negative"); + let _ = ui.selectable_label((flags >> 5 & 0x01) == 0x01, "Half-Carry"); + let _ = ui.selectable_label((flags >> 4 & 0x01) == 0x01, "Carry"); + }) }) }); } diff --git a/src/instruction.rs b/src/instruction.rs index 0b4f51e..a5bbf27 100644 --- a/src/instruction.rs +++ b/src/instruction.rs @@ -2150,3 +2150,149 @@ mod table { } } } + +pub(crate) mod dbg { + use super::add::{Source as AddSource, Target as AddTarget}; + use super::jump::JumpCondition; + use super::load::{Source as LDSource, Target as LDTarget}; + use super::{AllRegisters, BusIo, Cpu, Instruction, RegisterPair}; + + pub(crate) fn tmp_disasm(cpu: &Cpu, limit: u8) -> String { + let mut asm = String::new(); + let mut pc = cpu.register_pair(RegisterPair::PC); + + for _ in 0..limit { + let opcode = cpu.read_byte(pc); + pc += 1; + + let instr = if opcode == 0xCB { + let opcode = cpu.read_byte(pc); + pc += 1; + + Instruction::prefixed(opcode) + } else { + Instruction::unprefixed(opcode) + }; + + let instr_asm = format!("{:04X} {:?}\n", pc - 1, instr); + asm.push_str(&instr_asm); + + pc += delta::pc_inc_count(instr) + } + + asm + } + + fn disasm(cpu: &Cpu, pc: u16, instr: Instruction) -> String { + use Instruction::*; + + let imm_byte = cpu.read_byte(pc + 1); + let imm_word = (cpu.read_byte(pc + 2) as u16) << 8 | imm_byte as u16; + + match instr { + NOP => format!("NOP"), + LD(LDTarget::IndirectImmediateWord, LDSource::SP) => { + format!("LD ({:#06X}), SP", imm_word) + } + STOP => format!("STOP"), + JR(JumpCondition::Always) => format!("JR {}", imm_byte as i8), + JR(cond) => format!("JR {:?} {}", cond, imm_byte as i8), + LD(LDTarget::Group1(rp), LDSource::ImmediateWord) => { + format!("LD {:?} {:#06X}", rp, imm_word) + } + ADD(AddTarget::HL, AddSource::Group1(rp)) => format!("ADD HL, {:?}", rp), + LD(LDTarget::IndirectGroup2(rp), LDSource::A) => format!("LD ({:?}), A", rp), + LD(LDTarget::A, LDSource::IndirectGroup2(rp)) => format!("LD A, ({:?})", rp), + + _ => todo!(), + } + } + + mod delta { + use super::super::add::{Source as AddSource, Target as AddTarget}; + use super::super::alu::Source as AluSource; + use super::super::jump::{JumpCondition, JumpLocation}; + use super::super::load::{Source as LDSource, Target as LDTarget}; + use super::super::{AllRegisters, Instruction}; + + pub(super) fn pc_inc_count(instr: Instruction) -> u16 { + use Instruction::*; + + match instr { + // Unprefixed + NOP => 0, + LD(LDTarget::IndirectImmediateWord, LDSource::SP) => 2, + STOP => 0, + JR(_) => 1, + LD(LDTarget::Group1(_), LDSource::ImmediateWord) => 2, + ADD(AddTarget::HL, AddSource::Group1(_)) => 0, + LD(LDTarget::IndirectGroup2(_), LDSource::A) => 0, + LD(LDTarget::A, LDSource::IndirectGroup2(_)) => 0, + INC(AllRegisters::Group1(_)) => 0, + DEC(AllRegisters::Group1(_)) => 0, + INC(AllRegisters::Register(_)) => 0, + DEC(AllRegisters::Register(_)) => 0, + LD(LDTarget::Register(_), LDSource::ImmediateByte) => 1, + RLCA => 0, + RRCA => 0, + RLA => 0, + RRA => 0, + DAA => 0, + CPL => 0, + SCF => 0, + CCF => 0, + HALT => 0, + LD(LDTarget::Register(_), LDSource::Register(_)) => 0, + ADD(AddTarget::A, AddSource::Register(_)) => 0, + ADC(AluSource::Register(_)) => 0, + SUB(AluSource::Register(_)) => 0, + SBC(AluSource::Register(_)) => 0, + AND(AluSource::Register(_)) => 0, + XOR(AluSource::Register(_)) => 0, + OR(AluSource::Register(_)) => 0, + CP(AluSource::Register(_)) => 0, + RET(_) => 0, + LD(LDTarget::IoWithImmediateOffset, LDSource::A) => 1, + ADD(AddTarget::SP, AddSource::ImmediateSignedByte) => 1, + LD(LDTarget::A, LDSource::IoWithImmediateOffset) => 1, + LDHL => 1, + POP(_) => 0, + RETI => 0, + JP(JumpCondition::Always, JumpLocation::HL) => 0, + LD(LDTarget::SP, LDSource::HL) => 0, + JP(_, JumpLocation::ImmediateWord) => 2, + LD(LDTarget::IoWithC, LDSource::A) => 0, + LD(LDTarget::IndirectImmediateWord, LDSource::A) => 2, + LD(LDTarget::A, LDSource::IoWithC) => 0, + LD(LDTarget::A, LDSource::IndirectImmediateWord) => 2, + DI => 0, + EI => 0, + CALL(_) => 0, + PUSH(_) => 0, + ADD(AddTarget::A, AddSource::ImmediateByte) => 1, + ADC(AluSource::ImmediateByte) => 1, + SUB(AluSource::ImmediateByte) => 1, + SBC(AluSource::ImmediateByte) => 1, + AND(AluSource::ImmediateByte) => 1, + XOR(AluSource::ImmediateByte) => 1, + OR(AluSource::ImmediateByte) => 1, + CP(AluSource::ImmediateByte) => 1, + RST(_) => 0, + + // Prefixed + RLC(_) => 0, + RRC(_) => 0, + RL(_) => 0, + RR(_) => 0, + SLA(_) => 0, + SRA(_) => 0, + SWAP(_) => 0, + SRL(_) => 0, + BIT(_, _) => 0, + RES(_, _) => 0, + SET(_, _) => 0, + _ => unreachable!("{:?} is an illegal instruction", instr), + } + } + } +} diff --git a/src/main.rs b/src/main.rs index 4b5b238..7661a87 100644 --- a/src/main.rs +++ b/src/main.rs @@ -150,7 +150,7 @@ fn main() { // Begin to draw Egui components platform.begin_frame(); - gui::draw_egui(&mut app, &platform.context(), texture_id); + gui::draw_egui(&cpu, &mut app, &platform.context(), texture_id); // End the UI frame. We could now handle the output and draw the UI with the backend. let (_, paint_commands) = platform.end_frame(Some(&window)); let paint_jobs = platform.context().tessellate(paint_commands);