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
This commit is contained in:
parent
4a1a21a08f
commit
90d2da9272
39
src/cpu.rs
39
src/cpu.rs
|
@ -485,3 +485,42 @@ pub(crate) enum ImeState {
|
||||||
Pending,
|
Pending,
|
||||||
Enabled,
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
77
src/gui.rs
77
src/gui.rs
|
@ -11,6 +11,7 @@ use winit::event::{ElementState, KeyboardInput};
|
||||||
use winit::event_loop::EventLoop;
|
use winit::event_loop::EventLoop;
|
||||||
use winit::window::Window;
|
use winit::window::Window;
|
||||||
|
|
||||||
|
use crate::cpu::Cpu;
|
||||||
use crate::{GB_HEIGHT, GB_WIDTH};
|
use crate::{GB_HEIGHT, GB_WIDTH};
|
||||||
|
|
||||||
const EGUI_DIMENSIONS: (usize, usize) = (1280, 720);
|
const EGUI_DIMENSIONS: (usize, usize) = (1280, 720);
|
||||||
|
@ -221,7 +222,13 @@ pub fn execute_render_pass(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[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::TopBottomPanel::top("top_panel").show(ctx, |ui| {
|
||||||
egui::menu::menu(ui, "File", |ui| {
|
egui::menu::menu(ui, "File", |ui| {
|
||||||
if ui.button("Quit").clicked() {
|
if ui.button("Quit").clicked() {
|
||||||
|
@ -234,6 +241,74 @@ pub fn draw_egui(app: &mut GuiState, ctx: &CtxRef, texture_id: TextureId) {
|
||||||
texture_id,
|
texture_id,
|
||||||
[GB_WIDTH as f32 * SCALE, GB_HEIGHT as f32 * SCALE],
|
[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");
|
||||||
|
})
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -150,7 +150,7 @@ fn main() {
|
||||||
|
|
||||||
// Begin to draw Egui components
|
// Begin to draw Egui components
|
||||||
platform.begin_frame();
|
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.
|
// 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_commands) = platform.end_frame(Some(&window));
|
||||||
let paint_jobs = platform.context().tessellate(paint_commands);
|
let paint_jobs = platform.context().tessellate(paint_commands);
|
||||||
|
|
Loading…
Reference in New Issue