#![allow(clippy::unusual_byte_groupings)]
use std::fs::File;
use std::io::{BufWriter, Write};
use std::path::Path;
use super::breakpoints::Breakpoints;
use super::disasm::{disasm_arm, disasm_thumb};
use super::trace::{CpuTrace, TraceEntry};
use crate::gba::cpu::{Arm7tdmi, Bus};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BreakpointHit {
None,
At(u32),
}
pub struct GbaDebuggerController {
breakpoints: Breakpoints,
trace: CpuTrace,
trace_file: Option<BufWriter<File>>,
}
impl Default for GbaDebuggerController {
fn default() -> Self {
Self::new()
}
}
impl GbaDebuggerController {
pub fn new() -> Self {
Self {
breakpoints: Breakpoints::new(),
trace: CpuTrace::default(),
trace_file: None,
}
}
pub fn breakpoints_mut(&mut self) -> &mut Breakpoints {
&mut self.breakpoints
}
pub fn breakpoints(&self) -> &Breakpoints {
&self.breakpoints
}
pub fn add_breakpoint(&mut self, addr: u32) -> bool {
self.breakpoints.insert(addr)
}
pub fn remove_breakpoint(&mut self, addr: u32) -> bool {
self.breakpoints.remove(addr)
}
pub fn trace_mut(&mut self) -> &mut CpuTrace {
&mut self.trace
}
pub fn trace(&self) -> &CpuTrace {
&self.trace
}
pub fn enable_trace(&mut self, enabled: bool) {
self.trace.set_enabled(enabled);
}
pub fn set_trace_file<P: AsRef<Path>>(&mut self, path: P) -> std::io::Result<()> {
let file = File::create(path)?;
self.trace_file = Some(BufWriter::new(file));
Ok(())
}
pub fn clear_trace_file(&mut self) {
if let Some(mut w) = self.trace_file.take() {
let _ = w.flush();
}
}
pub fn step<B: Bus>(&mut self, cpu: &mut Arm7tdmi, bus: &mut B) -> u32 {
let pc = cpu.regs.r[15];
let thumb = cpu.thumb();
let raw_instr = if thumb {
bus.read16(pc & !1) as u32
} else {
bus.read32(pc & !3)
};
let disasm = if thumb {
disasm_thumb(raw_instr as u16, pc)
} else {
disasm_arm(raw_instr, pc)
};
cpu.step(bus);
let new_pc = cpu.regs.r[15];
let entry = TraceEntry {
pc,
instr: raw_instr,
thumb,
disasm,
regs: cpu.regs.r,
cpsr: cpu.regs.cpsr,
cycles: cpu.cycles,
};
if let Some(w) = self.trace_file.as_mut() {
let _ = writeln!(
w,
"{:08X}: {:08X} {}{}",
entry.pc,
entry.instr,
if entry.thumb { "T " } else { "" },
entry.disasm
);
}
self.trace.push(entry);
new_pc
}
pub fn run_until_breakpoint<B: Bus>(
&mut self,
cpu: &mut Arm7tdmi,
bus: &mut B,
max_steps: u32,
) -> BreakpointHit {
for _ in 0..max_steps {
let new_pc = self.step(cpu, bus);
if self.breakpoints.contains(new_pc) {
return BreakpointHit::At(new_pc);
}
}
BreakpointHit::None
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gba::cpu::registers::CpuMode;
use crate::gba::cpu::{Arm7tdmi, RamBus};
fn make_thumb_cpu(words: &[u16]) -> (Arm7tdmi, RamBus) {
let mut cpu = Arm7tdmi::new();
cpu.regs.switch_mode(CpuMode::User);
cpu.regs.set_thumb(true);
cpu.regs.cpsr &= !crate::gba::cpu::registers::FLAG_I;
cpu.regs.cpsr &= !crate::gba::cpu::registers::FLAG_F;
cpu.regs.r[15] = 0;
let mut bus = RamBus::new(0x1000);
for (i, w) in words.iter().enumerate() {
bus.write16(i as u32 * 2, *w);
}
(cpu, bus)
}
#[test]
fn step_records_disassembly_and_register_snapshot() {
let (mut cpu, mut bus) = make_thumb_cpu(&[0b00100_000_00101010u16]);
let mut dbg = GbaDebuggerController::new();
dbg.enable_trace(true);
dbg.step(&mut cpu, &mut bus);
let last = dbg.trace().last().expect("trace entry recorded");
assert_eq!(last.pc, 0);
assert_eq!(last.disasm, "MOV R0, #42");
assert!(last.thumb);
assert_eq!(last.regs[0], 42);
}
#[test]
fn run_until_breakpoint_halts_at_breakpoint_address() {
let (mut cpu, mut bus) =
make_thumb_cpu(&[0x2000u16, 0x2000u16, 0x2000u16, 0x2000u16, 0x2000u16]);
let mut dbg = GbaDebuggerController::new();
dbg.add_breakpoint(0x4);
let hit = dbg.run_until_breakpoint(&mut cpu, &mut bus, 16);
assert_eq!(hit, BreakpointHit::At(0x4));
assert_eq!(cpu.regs.r[15], 0x4);
}
#[test]
fn run_until_breakpoint_returns_none_when_no_breakpoint_hit() {
let (mut cpu, mut bus) = make_thumb_cpu(&[0x2000u16; 4]);
let mut dbg = GbaDebuggerController::new();
let hit = dbg.run_until_breakpoint(&mut cpu, &mut bus, 3);
assert_eq!(hit, BreakpointHit::None);
assert_eq!(cpu.regs.r[15], 6);
}
#[test]
fn trace_disabled_by_default_so_step_does_not_record() {
let (mut cpu, mut bus) = make_thumb_cpu(&[0x2000u16]);
let mut dbg = GbaDebuggerController::new();
dbg.step(&mut cpu, &mut bus);
assert!(dbg.trace().is_empty());
}
#[test]
fn breakpoint_helpers_delegate_to_set() {
let mut dbg = GbaDebuggerController::new();
assert!(dbg.add_breakpoint(0x100));
assert!(dbg.breakpoints().contains(0x100));
assert!(dbg.remove_breakpoint(0x100));
assert!(!dbg.breakpoints().contains(0x100));
}
#[test]
fn trace_file_logging_writes_one_line_per_step() {
let (mut cpu, mut bus) = make_thumb_cpu(&[0x2000u16, 0x2000u16]);
let dir = std::env::temp_dir();
let path = dir.join(format!("neser-gba-trace-{}.log", std::process::id()));
let mut dbg = GbaDebuggerController::new();
dbg.set_trace_file(&path).unwrap();
dbg.step(&mut cpu, &mut bus);
dbg.step(&mut cpu, &mut bus);
dbg.clear_trace_file();
let contents = std::fs::read_to_string(&path).unwrap();
let lines: Vec<&str> = contents.lines().collect();
assert_eq!(lines.len(), 2);
assert!(lines[0].contains("MOV R0, #0"));
assert!(lines[1].contains("MOV R0, #0"));
let _ = std::fs::remove_file(&path);
}
}