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,
|
||||
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::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");
|
||||
})
|
||||
})
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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
|
||||
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);
|
||||
|
|
Loading…
Reference in New Issue