pub mod breakpoint; pub mod trace; use std::collections::HashMap; use xenia_cpu::context::PpcContext; use xenia_memory::MemoryAccess; pub use breakpoint::Breakpoint; pub use trace::TraceEntry; /// The debugger. Hooks into every instruction step for observation. pub struct Debugger { pub breakpoints: HashMap, pub trace_log: Vec, pub trace_enabled: bool, pub max_trace_entries: usize, pub paused: bool, pub step_mode: StepMode, break_pending: bool, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum StepMode { /// Run freely until breakpoint or pause Run, /// Execute one instruction then pause StepInto, /// Run but break after current function returns (when LR changes) StepOver { return_addr: u32 }, } impl Debugger { pub fn new() -> Self { Self { breakpoints: HashMap::new(), trace_log: Vec::new(), trace_enabled: true, max_trace_entries: 100_000, paused: true, // Start paused for debugging step_mode: StepMode::StepInto, break_pending: false, } } /// Tier-3 perf: single branch that the hot interpreter loop checks /// before dispatching to [`pre_step`]/[`post_step`]. When the /// debugger is in "cold run" mode (not paused, no breakpoints, /// `StepMode::Run`, in-memory trace off), both hooks become dead /// code and we can skip the HashMap lookup + step-mode match + Vec /// maintenance entirely. The compiler reliably branch-predicts the /// stable branch direction across millions of instructions. #[inline] pub fn wants_hooks(&self) -> bool { self.trace_enabled || self.paused || self.break_pending || !matches!(self.step_mode, StepMode::Run) || !self.breakpoints.is_empty() } /// Called before each instruction executes. pub fn pre_step(&mut self, ctx: &PpcContext, _mem: &dyn MemoryAccess) { // Check breakpoints if let Some(bp) = self.breakpoints.get(&ctx.pc) && bp.enabled { self.break_pending = true; tracing::info!("Breakpoint hit at {:#010x}", ctx.pc); } } /// Called after each instruction executes. pub fn post_step(&mut self, ctx: &PpcContext, _mem: &dyn MemoryAccess) { // Log to trace if self.trace_enabled { if self.trace_log.len() >= self.max_trace_entries { self.trace_log.remove(0); } self.trace_log.push(TraceEntry { pc: ctx.pc, cycle: ctx.cycle_count, gpr_snapshot: [ctx.gpr[0], ctx.gpr[1], ctx.gpr[3], ctx.gpr[4]], lr: ctx.lr, }); } // Handle step mode match self.step_mode { StepMode::StepInto => { self.break_pending = true; } StepMode::StepOver { return_addr } => { if ctx.pc == return_addr { self.break_pending = true; } } StepMode::Run => {} } } /// Should we break execution? pub fn should_break(&self) -> bool { self.break_pending || self.paused } /// Add a breakpoint at the given address. pub fn add_breakpoint(&mut self, addr: u32) { self.breakpoints.insert(addr, Breakpoint { addr, enabled: true, condition: None }); } /// Remove a breakpoint. pub fn remove_breakpoint(&mut self, addr: u32) { self.breakpoints.remove(&addr); } /// Continue execution. pub fn continue_execution(&mut self) { self.paused = false; self.break_pending = false; self.step_mode = StepMode::Run; } /// Step one instruction. pub fn step_into(&mut self) { self.paused = false; self.break_pending = false; self.step_mode = StepMode::StepInto; } /// Clear break state after handling. pub fn acknowledge_break(&mut self) { self.break_pending = false; self.paused = true; } } impl Default for Debugger { fn default() -> Self { Self::new() } }