diff --git a/build.rs b/build.rs index aeb1a79..57b8731 100644 --- a/build.rs +++ b/build.rs @@ -23,7 +23,9 @@ fn main() -> Result<(), Box> { .define("VB_LITTLE_ENDIAN", None) .define("VB_SIGNED_PROPAGATE", None) .define("VB_DIV_GENERIC", None) + .define("VB_DIRECT_EXCEPTION", "on_exception") .define("VB_DIRECT_EXECUTE", "on_execute") + .define("VB_DIRECT_FETCH", "on_fetch") .define("VB_DIRECT_FRAME", "on_frame") .define("VB_DIRECT_READ", "on_read") .define("VB_DIRECT_WRITE", "on_write") diff --git a/src/emulator.rs b/src/emulator.rs index d73b276..4ea5860 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -18,7 +18,7 @@ use tracing::{error, warn}; use crate::{ audio::Audio, - emulator::cart::Cart, + emulator::{cart::Cart, profiler::Profiler}, graphics::TextureSink, memory::{MemoryRange, MemoryRegion}, }; @@ -27,6 +27,7 @@ pub use shrooms_vb_core::{VBKey, VBRegister, VBWatchpointType}; mod address_set; mod cart; +mod profiler; mod shrooms_vb_core; #[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)] @@ -69,6 +70,7 @@ pub struct EmulatorBuilder { audio_on: Arc<[AtomicBool; 2]>, linked: Arc, start_paused: bool, + monitor_events: bool, } impl EmulatorBuilder { @@ -85,6 +87,7 @@ impl EmulatorBuilder { audio_on: Arc::new([AtomicBool::new(true), AtomicBool::new(true)]), linked: Arc::new(AtomicBool::new(false)), start_paused: false, + monitor_events: false, }; let client = EmulatorClient { queue, @@ -110,6 +113,13 @@ impl EmulatorBuilder { } } + pub fn monitor_events(self, monitor_events: bool) -> Self { + Self { + monitor_events, + ..self + } + } + pub fn build(self) -> Result { let mut emulator = Emulator::new( self.commands, @@ -124,6 +134,9 @@ impl EmulatorBuilder { if self.start_paused { emulator.pause_sims()?; } + if self.monitor_events { + emulator.monitor_events(SimId::Player1)?; + } Ok(emulator) } } @@ -137,6 +150,7 @@ pub struct Emulator { state: Arc>, audio_on: Arc<[AtomicBool; 2]>, linked: Arc, + profilers: [Profiler; 2], renderers: HashMap, messages: HashMap>, debuggers: HashMap, @@ -164,6 +178,7 @@ impl Emulator { state, audio_on, linked, + profilers: [Profiler::new(), Profiler::new()], renderers: HashMap::new(), messages: HashMap::new(), debuggers: HashMap::new(), @@ -213,6 +228,7 @@ impl Emulator { } let sim = &mut self.sims[index]; sim.reset(); + sim.monitor_events(self.profilers[sim_id.to_index()].is_enabled()); if let Some(cart) = new_cart { sim.load_cart(cart.rom.clone(), cart.sram.clone())?; self.carts[index] = Some(cart); @@ -383,6 +399,11 @@ impl Emulator { self.watched_regions.insert(range, region); } + fn monitor_events(&mut self, sim_id: SimId) -> Result<()> { + self.profilers[sim_id.to_index()].enable(true); + self.reset_sim(sim_id, None) + } + pub fn run(&mut self) { loop { let idle = self.tick(); @@ -438,12 +459,20 @@ impl Emulator { let p1_running = running && p1_state == SimState::Ready; let p2_running = running && p2_state == SimState::Ready; let mut idle = !p1_running && !p2_running; - if p1_running && p2_running { - Sim::emulate_many(&mut self.sims); - } else if p1_running { - self.sims[SimId::Player1.to_index()].emulate(); - } else if p2_running { - self.sims[SimId::Player2.to_index()].emulate(); + + let cycles = self.emulate(p1_running, p2_running); + + // if we're profiling, track events + for ((sim, profiler), running) in self + .sims + .iter_mut() + .zip(self.profilers.iter_mut()) + .zip([p1_running, p2_running]) + { + if !running || !profiler.is_enabled() { + continue; + } + profiler.track(cycles, sim.take_sim_event()); } if state == EmulatorState::Stepping { @@ -470,7 +499,7 @@ impl Emulator { let Some(sim) = self.sims.get_mut(sim_id.to_index()) else { continue; }; - if let Some(reason) = sim.stop_reason() { + if let Some(reason) = sim.take_stop_reason() { let stop_reason = match reason { StopReason::Stepped => DebugStopReason::Trace, StopReason::Watchpoint(watch, address) => { @@ -529,6 +558,19 @@ impl Emulator { idle } + fn emulate(&mut self, p1_running: bool, p2_running: bool) -> u32 { + const MAX_CYCLES: u32 = 20_000_000; + let mut cycles = MAX_CYCLES; + if p1_running && p2_running { + Sim::emulate_many(&mut self.sims, &mut cycles); + } else if p1_running { + self.sims[SimId::Player1.to_index()].emulate(&mut cycles); + } else if p2_running { + self.sims[SimId::Player2.to_index()].emulate(&mut cycles); + } + MAX_CYCLES - cycles + } + fn handle_command(&mut self, command: EmulatorCommand) { match command { EmulatorCommand::ConnectToSim(sim_id, renderer, messages) => { diff --git a/src/emulator/profiler.rs b/src/emulator/profiler.rs new file mode 100644 index 0000000..0674d68 --- /dev/null +++ b/src/emulator/profiler.rs @@ -0,0 +1,23 @@ +use crate::emulator::shrooms_vb_core::SimEvent; + +pub struct Profiler { + enabled: bool, +} + +impl Profiler { + pub fn new() -> Self { + Self { enabled: false } + } + + pub fn enable(&mut self, enabled: bool) { + self.enabled = enabled; + } + + pub fn is_enabled(&self) -> bool { + self.enabled + } + + pub fn track(&mut self, cycles: u32, event: Option) { + println!("profiler {cycles} {event:0x?}"); + } +} diff --git a/src/emulator/shrooms_vb_core.rs b/src/emulator/shrooms_vb_core.rs index 772bdf8..212d10c 100644 --- a/src/emulator/shrooms_vb_core.rs +++ b/src/emulator/shrooms_vb_core.rs @@ -71,8 +71,16 @@ pub enum VBWatchpointType { Access, } +type OnException = extern "C" fn(sim: *mut VB, cause: *mut u16) -> c_int; type OnExecute = extern "C" fn(sim: *mut VB, address: u32, code: *const u16, length: c_int) -> c_int; +type OnFetch = extern "C" fn( + sim: *mut VB, + fetch: c_int, + address: u32, + value: *mut i32, + cycles: *mut u32, +) -> c_int; type OnFrame = extern "C" fn(sim: *mut VB) -> c_int; type OnRead = extern "C" fn( sim: *mut VB, @@ -135,8 +143,15 @@ unsafe extern "C" { fn vb_set_cart_ram(sim: *mut VB, sram: *mut c_void, size: u32) -> c_int; #[link_name = "vbSetCartROM"] fn vb_set_cart_rom(sim: *mut VB, rom: *mut c_void, size: u32) -> c_int; + #[link_name = "vbSetExceptionCallback"] + fn vb_set_exception_callback( + sim: *mut VB, + callback: Option, + ) -> Option; #[link_name = "vbSetExecuteCallback"] fn vb_set_execute_callback(sim: *mut VB, callback: Option) -> Option; + #[link_name = "vbSetFetchCallback"] + fn vb_set_fetch_callback(sim: *mut VB, callback: Option) -> Option; #[link_name = "vbSetFrameCallback"] fn vb_set_frame_callback(sim: *mut VB, callback: Option) -> Option; #[link_name = "vbSetKeys"] @@ -180,11 +195,21 @@ extern "C" fn on_frame(sim: *mut VB) -> c_int { } #[unsafe(no_mangle)] -extern "C" fn on_execute(sim: *mut VB, address: u32, _code: *const u16, _length: c_int) -> c_int { +extern "C" fn on_execute(sim: *mut VB, address: u32, code: *const u16, length: c_int) -> c_int { // SAFETY: the *mut VB owns its userdata. // There is no way for the userdata to be null or otherwise invalid. let data: &mut VBState = unsafe { &mut *vb_get_user_data(sim).cast() }; + if data.monitor.enabled { + // SAFETY: length is the length of code, in elements + let code = unsafe { slice::from_raw_parts(code, length as usize) }; + if data.monitor.detect_event(sim, address, code) { + // Something interesting will happen after this instruction is run. + // The on_fetch callback will fire when it does. + unsafe { vb_set_fetch_callback(sim, Some(on_fetch)) }; + } + } + let mut stopped = data.stop_reason.is_some(); if data.step_from.is_some_and(|s| s != address) { data.step_from = None; @@ -199,6 +224,35 @@ extern "C" fn on_execute(sim: *mut VB, address: u32, _code: *const u16, _length: if stopped { 1 } else { 0 } } +#[unsafe(no_mangle)] +extern "C" fn on_fetch( + sim: *mut VB, + _fetch: c_int, + _address: u32, + _value: *mut i32, + _cycles: *mut u32, +) -> c_int { + // SAFETY: the *mut VB owns its userdata. + // There is no way for the userdata to be null or otherwise invalid. + let data: &mut VBState = unsafe { &mut *vb_get_user_data(sim).cast() }; + data.monitor.event = data.monitor.queued_event.take(); + unsafe { vb_set_exception_callback(sim, Some(on_exception)) }; + unsafe { vb_set_fetch_callback(sim, None) }; + 1 +} + +#[unsafe(no_mangle)] +extern "C" fn on_exception(sim: *mut VB, cause: *mut u16) -> c_int { + // SAFETY: the *mut VB owns its userdata. + // There is no way for the userdata to be null or otherwise invalid. + let data: &mut VBState = unsafe { &mut *vb_get_user_data(sim).cast() }; + data.monitor.event = data.monitor.queued_event.take(); + data.monitor.queued_event = Some(SimEvent::Interrupt(unsafe { *cause })); + unsafe { vb_set_exception_callback(sim, None) }; + unsafe { vb_set_fetch_callback(sim, Some(on_fetch)) }; + if data.monitor.event.is_some() { 1 } else { 0 } +} + #[unsafe(no_mangle)] extern "C" fn on_read( sim: *mut VB, @@ -260,6 +314,83 @@ extern "C" fn on_write( 0 } +#[allow(dead_code)] +#[derive(Debug)] +pub enum SimEvent { + Call(u32), + Return, + Interrupt(u16), + Reti, +} + +struct EventMonitor { + enabled: bool, + event: Option, + queued_event: Option, +} + +impl EventMonitor { + fn new() -> Self { + Self { + enabled: false, + event: None, + queued_event: None, + } + } + + fn detect_event(&mut self, sim: *mut VB, address: u32, code: &[u16]) -> bool { + self.queued_event = self.do_detect_event(sim, address, code); + self.queued_event.is_some() + } + + fn do_detect_event(&self, sim: *mut VB, address: u32, code: &[u16]) -> Option { + const JAL_OPCODE: u16 = 0b101011; + const JMP_OPCODE: u16 = 0b000110; + const RETI_OPCODE: u16 = 0b011001; + + const fn format_i_reg_1(code: &[u16]) -> u8 { + (code[0] & 0x1f) as u8 + } + + const fn format_iv_disp(code: &[u16]) -> i32 { + let value = ((code[0] & 0x3ff) as i32) << 16 | (code[1] as i32); + value << 6 >> 6 + } + + let opcode = code[0] >> 10; + + if opcode == JAL_OPCODE { + let disp = format_iv_disp(code); + if disp != 4 { + // JAL .+4 is how programs get r31 to a known value for indirect calls + // (which we detect later.) + // Any other JAL is a function call. + return Some(SimEvent::Call(address.wrapping_add_signed(disp))); + } + } + + if opcode == JMP_OPCODE { + let jmp_reg = format_i_reg_1(code); + if jmp_reg == 31 { + // JMP[r31] is a return + return Some(SimEvent::Return); + } + let r31 = unsafe { vb_get_program_register(sim, 31) }; + if r31 as u32 == address.wrapping_add(2) { + // JMP anywhere else, if r31 points to after the JMP, is an indirect call + let target = unsafe { vb_get_program_register(sim, jmp_reg as u32) }; + return Some(SimEvent::Call(target as u32)); + } + } + + if opcode == RETI_OPCODE { + return Some(SimEvent::Reti); + } + + None + } +} + const AUDIO_CAPACITY_SAMPLES: usize = 834 * 4; const AUDIO_CAPACITY_FLOATS: usize = AUDIO_CAPACITY_SAMPLES * 2; pub const EXPECTED_FRAME_SIZE: usize = 834 * 2; @@ -267,6 +398,7 @@ pub const EXPECTED_FRAME_SIZE: usize = 834 * 2; struct VBState { frame_seen: bool, stop_reason: Option, + monitor: EventMonitor, step_from: Option, breakpoints: Vec, read_watchpoints: AddressSet, @@ -277,6 +409,7 @@ struct VBState { impl VBState { fn needs_execute_callback(&self) -> bool { self.step_from.is_some() + || self.monitor.enabled || !self.breakpoints.is_empty() || !self.read_watchpoints.is_empty() || !self.write_watchpoints.is_empty() @@ -311,6 +444,7 @@ impl Sim { let state = VBState { frame_seen: false, stop_reason: None, + monitor: EventMonitor::new(), step_from: None, breakpoints: vec![], read_watchpoints: AddressSet::new(), @@ -332,6 +466,23 @@ impl Sim { unsafe { vb_reset(self.sim) }; } + pub fn monitor_events(&mut self, enabled: bool) { + let state = self.get_state(); + state.monitor.enabled = enabled; + state.monitor.event = None; + state.monitor.queued_event = None; + if enabled { + unsafe { vb_set_execute_callback(self.sim, Some(on_execute)) }; + unsafe { vb_set_exception_callback(self.sim, Some(on_exception)) }; + } else { + if !state.needs_execute_callback() { + unsafe { vb_set_execute_callback(self.sim, None) }; + } + unsafe { vb_set_exception_callback(self.sim, None) }; + unsafe { vb_set_fetch_callback(self.sim, None) }; + } + } + pub fn load_cart(&mut self, mut rom: Vec, mut sram: Vec) -> Result<()> { self.unload_cart(); @@ -394,16 +545,14 @@ impl Sim { unsafe { vb_set_peer(self.sim, ptr::null_mut()) }; } - pub fn emulate(&mut self) { - let mut cycles = 20_000_000; - unsafe { vb_emulate(self.sim, &mut cycles) }; + pub fn emulate(&mut self, cycles: &mut u32) { + unsafe { vb_emulate(self.sim, cycles) }; } - pub fn emulate_many(sims: &mut [Sim]) { - let mut cycles = 20_000_000; + pub fn emulate_many(sims: &mut [Sim], cycles: &mut u32) { let count = sims.len() as c_uint; let sims = sims.as_mut_ptr().cast(); - unsafe { vb_emulate_ex(sims, count, &mut cycles) }; + unsafe { vb_emulate_ex(sims, count, cycles) }; } pub fn read_pixels(&mut self, buffers: &mut [u8]) -> bool { @@ -632,7 +781,7 @@ impl Sim { Some(string) } - pub fn stop_reason(&mut self) -> Option { + pub fn take_stop_reason(&mut self) -> Option { let data = self.get_state(); let reason = data.stop_reason.take(); if !data.needs_execute_callback() { @@ -641,6 +790,11 @@ impl Sim { reason } + pub fn take_sim_event(&mut self) -> Option { + let data = self.get_state(); + data.monitor.event.take() + } + fn get_state(&mut self) -> &mut VBState { // SAFETY: the *mut VB owns its userdata. // There is no way for the userdata to be null or otherwise invalid. diff --git a/src/main.rs b/src/main.rs index a9b0b46..a899c03 100644 --- a/src/main.rs +++ b/src/main.rs @@ -31,6 +31,9 @@ struct Args { /// Start a GDB/LLDB debug server on this port. #[arg(short, long)] debug_port: Option, + /// Enable profiling a game + #[arg(short, long)] + profile: bool, } fn init_logger() { @@ -106,6 +109,12 @@ fn main() -> Result<()> { } builder = builder.start_paused(true); } + if args.profile { + if args.rom.is_none() { + bail!("to start profiling, please select a game."); + } + builder = builder.monitor_events(true) + } ThreadBuilder::default() .name("Emulator".to_owned())