From 11c17cb246c0a577e29d5660a55f613897a8fcf3 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Thu, 7 Aug 2025 23:49:29 -0400 Subject: [PATCH 01/15] Start implementing profiling --- build.rs | 2 + src/emulator.rs | 58 +++++++++-- src/emulator/profiler.rs | 23 +++++ src/emulator/shrooms_vb_core.rs | 170 ++++++++++++++++++++++++++++++-- src/main.rs | 9 ++ 5 files changed, 246 insertions(+), 16 deletions(-) create mode 100644 src/emulator/profiler.rs 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 85ac5f1..e01d879 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()) From 38b34c9cf958e65203cf53fead582162bdf0a7e2 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sat, 9 Aug 2025 22:28:45 -0400 Subject: [PATCH 02/15] Run elf files --- Cargo.lock | 7 +++++++ Cargo.toml | 1 + src/emulator/cart.rs | 18 ++++++++++++++++++ 3 files changed, 26 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 26fdcf1..fef6576 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1046,6 +1046,12 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "elf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55dd888a213fc57e957abf2aa305ee3e8a28dbe05687a251f33b637cd46b0070" + [[package]] name = "emath" version = "0.32.1" @@ -1833,6 +1839,7 @@ dependencies = [ "egui-wgpu", "egui-winit", "egui_extras", + "elf", "fixed", "gilrs", "hex", diff --git a/Cargo.toml b/Cargo.toml index a151041..5647ec3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ egui_extras = { version = "0.32", features = ["image"] } egui-notify = "0.20" egui-winit = "0.32" egui-wgpu = { version = "0.32", features = ["winit"] } +elf = "0.8" fixed = { version = "1.28", features = ["num-traits"] } gilrs = { version = "0.11", features = ["serde-serialize"] } hex = "0.4" diff --git a/src/emulator/cart.rs b/src/emulator/cart.rs index 79f53c3..8aec193 100644 --- a/src/emulator/cart.rs +++ b/src/emulator/cart.rs @@ -18,6 +18,7 @@ pub struct Cart { impl Cart { pub fn load(file_path: &Path, sim_id: SimId) -> Result { let rom = fs::read(file_path)?; + let rom = try_parse_elf(&rom).unwrap_or(rom); let mut sram_file = File::options() .read(true) @@ -55,6 +56,23 @@ impl Cart { } } +fn try_parse_elf(data: &[u8]) -> Option> { + let parsed = elf::ElfBytes::::minimal_parse(data).ok()?; + let mut bytes = vec![]; + let mut pstart = None; + for phdr in parsed.segments()? { + if phdr.p_filesz == 0 { + continue; + } + let start = pstart.unwrap_or(phdr.p_paddr); + pstart = Some(start); + bytes.resize((phdr.p_paddr - start) as usize, 0); + let data = parsed.segment_data(&phdr).ok()?; + bytes.extend_from_slice(data); + } + Some(bytes) +} + fn sram_path(file_path: &Path, sim_id: SimId) -> PathBuf { match sim_id { SimId::Player1 => file_path.with_extension("p1.sram"), From 3bfdcc93665bbd81e18e8a60528f2d02d31c42ad Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sat, 9 Aug 2025 23:45:34 -0400 Subject: [PATCH 03/15] Start profiler UI/thread --- src/app.rs | 17 ++++++++- src/emulator.rs | 79 ++++++++++++++++++++++++++++------------ src/emulator/profiler.rs | 23 ------------ src/main.rs | 13 ++++--- src/profiler.rs | 79 ++++++++++++++++++++++++++++++++++++++++ src/window.rs | 2 + src/window/game.rs | 5 +++ src/window/profile.rs | 54 +++++++++++++++++++++++++++ 8 files changed, 218 insertions(+), 54 deletions(-) delete mode 100644 src/emulator/profiler.rs create mode 100644 src/profiler.rs create mode 100644 src/window/profile.rs diff --git a/src/app.rs b/src/app.rs index 2c299d2..b1f7245 100644 --- a/src/app.rs +++ b/src/app.rs @@ -24,8 +24,8 @@ use crate::{ persistence::Persistence, window::{ AboutWindow, AppWindow, BgMapWindow, CharacterDataWindow, FrameBufferWindow, GameWindow, - GdbServerWindow, HotkeysWindow, InputWindow, ObjectWindow, RegisterWindow, TerminalWindow, - WorldWindow, + GdbServerWindow, HotkeysWindow, InputWindow, ObjectWindow, ProfileWindow, RegisterWindow, + TerminalWindow, WorldWindow, }, }; @@ -54,6 +54,7 @@ pub struct Application { viewports: HashMap, focused: Option, init_debug_port: Option, + init_profiling: bool, } impl Application { @@ -61,6 +62,7 @@ impl Application { client: EmulatorClient, proxy: EventLoopProxy, debug_port: Option, + profiling: bool, ) -> Self { let wgpu = WgpuState::new(); let icon = load_icon().ok().map(Arc::new); @@ -89,6 +91,7 @@ impl Application { viewports: HashMap::new(), focused: None, init_debug_port: debug_port, + init_profiling: profiling, } } @@ -113,6 +116,11 @@ impl ApplicationHandler for Application { server.launch(port); self.open(event_loop, Box::new(server)); } + if self.init_profiling { + let mut profiler = ProfileWindow::new(SimId::Player1, self.client.clone()); + profiler.launch(); + self.open(event_loop, Box::new(profiler)); + } let app = GameWindow::new( self.client.clone(), self.proxy.clone(), @@ -247,6 +255,10 @@ impl ApplicationHandler for Application { let terminal = TerminalWindow::new(sim_id, &self.client); self.open(event_loop, Box::new(terminal)); } + UserEvent::OpenProfiler(sim_id) => { + let profile = ProfileWindow::new(sim_id, self.client.clone()); + self.open(event_loop, Box::new(profile)); + } UserEvent::OpenDebugger(sim_id) => { let debugger = GdbServerWindow::new(sim_id, self.client.clone(), self.proxy.clone()); @@ -521,6 +533,7 @@ pub enum UserEvent { OpenFrameBuffers(SimId), OpenRegisters(SimId), OpenTerminal(SimId), + OpenProfiler(SimId), OpenDebugger(SimId), OpenInput, OpenHotkeys, diff --git a/src/emulator.rs b/src/emulator.rs index e01d879..1bcc2d3 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -18,7 +18,7 @@ use tracing::{error, warn}; use crate::{ audio::Audio, - emulator::{cart::Cart, profiler::Profiler}, + emulator::{cart::Cart, shrooms_vb_core::SimEvent}, graphics::TextureSink, memory::{MemoryRange, MemoryRegion}, }; @@ -27,7 +27,6 @@ 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)] @@ -70,7 +69,6 @@ pub struct EmulatorBuilder { audio_on: Arc<[AtomicBool; 2]>, linked: Arc, start_paused: bool, - monitor_events: bool, } impl EmulatorBuilder { @@ -87,7 +85,6 @@ 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, @@ -113,13 +110,6 @@ 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, @@ -134,9 +124,6 @@ impl EmulatorBuilder { if self.start_paused { emulator.pause_sims()?; } - if self.monitor_events { - emulator.monitor_events(SimId::Player1)?; - } Ok(emulator) } } @@ -150,7 +137,7 @@ pub struct Emulator { state: Arc>, audio_on: Arc<[AtomicBool; 2]>, linked: Arc, - profilers: [Profiler; 2], + profilers: [Option; 2], renderers: HashMap, messages: HashMap>, debuggers: HashMap, @@ -178,7 +165,7 @@ impl Emulator { state, audio_on, linked, - profilers: [Profiler::new(), Profiler::new()], + profilers: [None, None], renderers: HashMap::new(), messages: HashMap::new(), debuggers: HashMap::new(), @@ -228,12 +215,29 @@ 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); self.sim_state[index].store(SimState::Ready, Ordering::Release); } + let mut profiling = false; + if let Some(profiler) = self.profilers[sim_id.to_index()].as_ref() { + if let Some(cart) = self.carts[index].as_ref() { + if profiler + .send(ProfileEvent::Start { + file_path: cart.file_path.clone(), + }) + .is_ok() + { + sim.monitor_events(true); + profiling = true; + } + } + } + if !profiling { + sim.monitor_events(false); + } + if self.sim_state[index].load(Ordering::Acquire) == SimState::Ready { self.resume_sims(); } @@ -331,6 +335,11 @@ impl Emulator { Ok(()) } + fn start_profiling(&mut self, sim_id: SimId, sender: ProfileSender) -> Result<()> { + self.profilers[sim_id.to_index()] = Some(sender); + self.reset_sim(sim_id, None) + } + fn start_debugging(&mut self, sim_id: SimId, sender: DebugSender) { if self.sim_state[sim_id.to_index()].load(Ordering::Acquire) != SimState::Ready { // Can't debug unless a game is connected @@ -399,11 +408,6 @@ 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(); @@ -469,10 +473,20 @@ impl Emulator { .zip(self.profilers.iter_mut()) .zip([p1_running, p2_running]) { - if !running || !profiler.is_enabled() { + if !running { continue; } - profiler.track(cycles, sim.take_sim_event()); + if let Some(p) = profiler { + if p.send(ProfileEvent::Update { + cycles, + event: sim.take_sim_event(), + }) + .is_err() + { + sim.monitor_events(false); + *profiler = None; + } + } } if state == EmulatorState::Stepping { @@ -614,6 +628,11 @@ impl Emulator { self.report_error(SimId::Player1, format!("Error setting speed: {error}")); } } + EmulatorCommand::StartProfiling(sim_id, profiler) => { + if let Err(error) = self.start_profiling(sim_id, profiler) { + self.report_error(SimId::Player1, format!("Error enaling profiler: {error}")); + } + } EmulatorCommand::StartDebugging(sim_id, debugger) => { self.start_debugging(sim_id, debugger); } @@ -751,6 +770,7 @@ pub enum EmulatorCommand { Resume, FrameAdvance, SetSpeed(f64), + StartProfiling(SimId, ProfileSender), StartDebugging(SimId, DebugSender), StopDebugging(SimId), DebugInterrupt(SimId), @@ -792,6 +812,7 @@ pub enum EmulatorState { Debugging, } +type ProfileSender = tokio::sync::mpsc::UnboundedSender; type DebugSender = tokio::sync::mpsc::UnboundedSender; #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -815,6 +836,16 @@ pub enum DebugEvent { Stopped(DebugStopReason), } +pub enum ProfileEvent { + Start { + file_path: PathBuf, + }, + Update { + cycles: u32, + event: Option, + }, +} + #[derive(Clone)] pub struct EmulatorClient { queue: mpsc::Sender, diff --git a/src/emulator/profiler.rs b/src/emulator/profiler.rs deleted file mode 100644 index 0674d68..0000000 --- a/src/emulator/profiler.rs +++ /dev/null @@ -1,23 +0,0 @@ -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/main.rs b/src/main.rs index a899c03..04f823c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,6 +22,7 @@ mod images; mod input; mod memory; mod persistence; +mod profiler; mod window; #[derive(Parser)] @@ -110,10 +111,7 @@ 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) + builder = builder.start_paused(true) } ThreadBuilder::default() @@ -133,6 +131,11 @@ fn main() -> Result<()> { let event_loop = EventLoop::with_user_event().build().unwrap(); event_loop.set_control_flow(ControlFlow::Poll); let proxy = event_loop.create_proxy(); - event_loop.run_app(&mut Application::new(client, proxy, args.debug_port))?; + event_loop.run_app(&mut Application::new( + client, + proxy, + args.debug_port, + args.profile, + ))?; Ok(()) } diff --git a/src/profiler.rs b/src/profiler.rs new file mode 100644 index 0000000..6004f44 --- /dev/null +++ b/src/profiler.rs @@ -0,0 +1,79 @@ +use std::{ + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, + thread, +}; + +use tokio::{select, sync::mpsc}; + +use crate::emulator::{EmulatorClient, EmulatorCommand, ProfileEvent, SimId}; + +pub struct Profiler { + sim_id: SimId, + client: EmulatorClient, + running: Arc, + killer: Option>, +} + +impl Profiler { + pub fn new(sim_id: SimId, client: EmulatorClient) -> Self { + Self { + sim_id, + client, + running: Arc::new(AtomicBool::new(false)), + killer: None, + } + } + + pub fn started(&self) -> bool { + self.running.load(Ordering::Relaxed) + } + + pub fn start(&mut self) { + let sim_id = self.sim_id; + let client = self.client.clone(); + let running = self.running.clone(); + let (tx, rx) = oneshot::channel(); + self.killer = Some(tx); + thread::spawn(move || { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(async move { + select! { + _ = run_profile(sim_id, client, running.clone()) => {} + _ = rx => { + running.store(false, Ordering::Relaxed); + } + } + }) + }); + } + + pub fn stop(&mut self) { + if let Some(killer) = self.killer.take() { + let _ = killer.send(()); + } + } +} + +async fn run_profile(sim_id: SimId, client: EmulatorClient, running: Arc) { + let (profile_sync, mut profile_source) = mpsc::unbounded_channel(); + client.send_command(EmulatorCommand::StartProfiling(sim_id, profile_sync)); + + running.store(true, Ordering::Relaxed); + while let Some(event) = profile_source.recv().await { + match event { + ProfileEvent::Start { file_path } => { + println!("profiling {}", file_path.display()); + } + ProfileEvent::Update { cycles, event } => { + println!("update {cycles} {event:#x?}"); + } + } + } + running.store(false, Ordering::Release); +} diff --git a/src/window.rs b/src/window.rs index 502fae4..237f262 100644 --- a/src/window.rs +++ b/src/window.rs @@ -4,6 +4,7 @@ pub use game::GameWindow; pub use gdb::GdbServerWindow; pub use hotkeys::HotkeysWindow; pub use input::InputWindow; +pub use profile::ProfileWindow; pub use terminal::TerminalWindow; pub use vip::{ BgMapWindow, CharacterDataWindow, FrameBufferWindow, ObjectWindow, RegisterWindow, WorldWindow, @@ -18,6 +19,7 @@ mod game_screen; mod gdb; mod hotkeys; mod input; +mod profile; mod terminal; mod utils; mod vip; diff --git a/src/window/game.rs b/src/window/game.rs index c71abd0..3dc9dda 100644 --- a/src/window/game.rs +++ b/src/window/game.rs @@ -228,6 +228,11 @@ impl GameWindow { .send_event(UserEvent::OpenTerminal(self.sim_id)) .unwrap(); } + if ui.button("Profiler").clicked() { + self.proxy + .send_event(UserEvent::OpenProfiler(self.sim_id)) + .unwrap(); + } if ui.button("GDB Server").clicked() { self.proxy .send_event(UserEvent::OpenDebugger(self.sim_id)) diff --git a/src/window/profile.rs b/src/window/profile.rs new file mode 100644 index 0000000..41de435 --- /dev/null +++ b/src/window/profile.rs @@ -0,0 +1,54 @@ +use egui::{CentralPanel, ViewportBuilder, ViewportId}; + +use crate::{ + emulator::{EmulatorClient, SimId}, + profiler::Profiler, + window::AppWindow, +}; + +pub struct ProfileWindow { + sim_id: SimId, + profiler: Profiler, +} + +impl ProfileWindow { + pub fn new(sim_id: SimId, client: EmulatorClient) -> Self { + Self { + sim_id, + profiler: Profiler::new(sim_id, client), + } + } + + pub fn launch(&mut self) { + self.profiler.start(); + } +} + +impl AppWindow for ProfileWindow { + fn viewport_id(&self) -> ViewportId { + ViewportId::from_hash_of(format!("Profile-{}", self.sim_id)) + } + + fn sim_id(&self) -> SimId { + self.sim_id + } + + fn initial_viewport(&self) -> ViewportBuilder { + ViewportBuilder::default() + .with_title(format!("Profiler ({})", self.sim_id)) + .with_inner_size((300.0, 200.0)) + } + + fn show(&mut self, ctx: &egui::Context) { + CentralPanel::default().show(ctx, |ui| { + let mut started = self.profiler.started(); + if ui.checkbox(&mut started, "Profiling enabled?").changed() { + if started { + self.profiler.start(); + } else { + self.profiler.stop(); + } + } + }); + } +} From 7f819d080f8a92dc65301ee8d8ce0694e5df973e Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Mon, 11 Aug 2025 19:55:55 -0400 Subject: [PATCH 04/15] Begin implementing profiling sessions --- Cargo.lock | 953 +++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 1 + src/emulator.rs | 4 +- src/profiler.rs | 103 +++++- 4 files changed, 1055 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fef6576..86a4786 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -34,6 +34,7 @@ version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ + "fallible-iterator", "gimli", ] @@ -66,6 +67,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "alsa" version = "0.9.1" @@ -273,6 +289,21 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-compression" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddb939d66e4ae03cee6091612804ba446b12878410cfa17f785f4dd67d4014e8" +dependencies = [ + "brotli", + "flate2", + "futures-core", + "futures-io", + "memchr", + "pin-project-lite", + "tokio", +] + [[package]] name = "async-executor" version = "1.13.2" @@ -453,6 +484,18 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "binary-merge" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597bb81c80a54b6a4381b23faba8d7774b144c94cbd1d6fe3f1329bd776554ab" + [[package]] name = "bindgen" version = "0.72.0" @@ -538,6 +581,27 @@ dependencies = [ "piper", ] +[[package]] +name = "brotli" +version = "8.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9991eea70ea4f293524138648e41ee89b0b2b12ddef3b255effa43c8056e0e0d" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bumpalo" version = "3.19.0" @@ -564,6 +628,12 @@ dependencies = [ "syn", ] +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "byteorder-lite" version = "0.1.0" @@ -576,6 +646,18 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +[[package]] +name = "cab" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "171228650e6721d5acc0868a462cd864f49ac5f64e4a42cde270406e64e404d2" +dependencies = [ + "byteorder", + "flate2", + "lzxd", + "time", +] + [[package]] name = "calloop" version = "0.13.0" @@ -828,6 +910,30 @@ dependencies = [ "windows 0.54.0", ] +[[package]] +name = "cpp_demangle" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96e58d342ad113c2b878f16d5d034c03be492ae460cdbc02b7f0f2284d310c7d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crc" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" version = "1.5.0" @@ -864,6 +970,24 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" +[[package]] +name = "debugid" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "uuid", +] + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + [[package]] name = "directories" version = "6.0.0" @@ -873,6 +997,15 @@ dependencies = [ "dirs-sys", ] +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + [[package]] name = "dirs-sys" version = "0.5.0" @@ -1052,6 +1185,15 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55dd888a213fc57e957abf2aa305ee3e8a28dbe05687a251f33b637cd46b0070" +[[package]] +name = "elsa" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9abf33c656a7256451ebb7d0082c5a471820c31269e49d807c538c252352186e" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "emath" version = "0.32.1" @@ -1188,6 +1330,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + [[package]] name = "fastrand" version = "2.3.0" @@ -1223,6 +1371,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", + "libz-rs-sys", "miniz_oxide", ] @@ -1274,6 +1423,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs4" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8640e34b88f7652208ce9e88b1a37a2ae95227d84abec377ccd3c5cfeb141ed4" +dependencies = [ + "rustix 1.0.8", + "windows-sys 0.59.0", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -1319,6 +1478,12 @@ dependencies = [ "syn", ] +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + [[package]] name = "futures-task" version = "0.3.31" @@ -1334,6 +1499,7 @@ dependencies = [ "futures-core", "futures-io", "futures-macro", + "futures-sink", "futures-task", "memchr", "pin-project-lite", @@ -1358,8 +1524,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -1369,9 +1537,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", ] [[package]] @@ -1415,6 +1585,10 @@ name = "gimli" version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +dependencies = [ + "fallible-iterator", + "stable_deref_trait", +] [[package]] name = "gl_generator" @@ -1549,6 +1723,106 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.0", + "tokio", + "tower-service", + "tracing", +] + [[package]] name = "icu_collections" version = "2.0.0" @@ -1699,6 +1973,15 @@ dependencies = [ "libc", ] +[[package]] +name = "inplace-vec-builder" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf64c2edc8226891a71f127587a2861b132d2b942310843814d5001d99a1d307" +dependencies = [ + "smallvec", +] + [[package]] name = "io-kit-sys" version = "0.4.1" @@ -1720,6 +2003,22 @@ dependencies = [ "libc", ] +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -1860,6 +2159,7 @@ dependencies = [ "tracing", "tracing-subscriber", "wgpu", + "wholesym", "windows 0.61.3", "winit", "winresource", @@ -1908,6 +2208,48 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "libz-rs-sys" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "172a788537a2221661b480fee8dc5f96c580eb34fa88764d3205dc356c7e4221" +dependencies = [ + "zlib-rs", +] + +[[package]] +name = "linear-map" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfae20f6b19ad527b550c223fddc3077a547fc70cda94b9b566575423fd303ee" + +[[package]] +name = "linux-perf-data" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f85f35725e15ad6c62b9db73f3d62439094e616a2f83500f7bcdc01ae5b84d8" +dependencies = [ + "byteorder", + "linear-map", + "linux-perf-event-reader", + "memchr", + "prost", + "prost-derive", + "thiserror 2.0.12", +] + +[[package]] +name = "linux-perf-event-reader" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa8fc7e83909ea3b9e2784591655637d3401f2f16014f9d8d6e23ccd138e665f" +dependencies = [ + "bitflags 2.9.1", + "byteorder", + "memchr", + "thiserror 2.0.12", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -1948,6 +2290,28 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc", +] + +[[package]] +name = "lzxd" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b29dffab797218e12e4df08ef5d15ab9efca2504038b1b32b9b32fc844b39c9" + [[package]] name = "mach2" version = "0.4.3" @@ -1957,6 +2321,17 @@ dependencies = [ "libc", ] +[[package]] +name = "macho-unwind-info" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4bdc8b0ce69932332cf76d24af69c3a155242af95c226b2ab6c2e371ed1149" +dependencies = [ + "thiserror 2.0.12", + "zerocopy", + "zerocopy-derive", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -1975,6 +2350,12 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "maybe-owned" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" + [[package]] name = "memchr" version = "2.7.5" @@ -2059,6 +2440,16 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "msvc-demangler" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbeff6bd154a309b2ada5639b2661ca6ae4599b34e8487dc276d2cd637da2d76" +dependencies = [ + "bitflags 2.9.1", + "itoa", +] + [[package]] name = "naga" version = "25.0.1" @@ -2197,6 +2588,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-derive" version = "0.4.2" @@ -2535,7 +2932,9 @@ version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ + "flate2", "memchr", + "ruzstd", ] [[package]] @@ -2663,6 +3062,31 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pdb-addr2line" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a2e496e34cd96a1bb26f681e5adb11c98f1e5378e294e60c06c0cf04c526ba" +dependencies = [ + "bitflags 2.9.1", + "elsa", + "maybe-owned", + "pdb2", + "range-collections", + "thiserror 2.0.12", +] + +[[package]] +name = "pdb2" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266b8733697ccc3aa405a9b2cf485a42746ad1cf73d3b0497b79b24f9e874a71" +dependencies = [ + "fallible-iterator", + "scroll", + "uuid", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -2810,6 +3234,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -2858,6 +3288,28 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "quick-xml" version = "0.37.5" @@ -2867,6 +3319,61 @@ dependencies = [ "memchr", ] +[[package]] +name = "quinn" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.1", + "rustls", + "socket2 0.5.10", + "thiserror 2.0.12", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" +dependencies = [ + "bytes", + "getrandom 0.3.3", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash 2.1.1", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.12", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.5.10", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "quote" version = "1.0.40" @@ -2932,6 +3439,24 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d6831663a5098ea164f89cff59c6284e95f4e3c76ce9848d4529f5ccca9bde" +[[package]] +name = "range-collections" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "861706ea9c4aded7584c5cd1d241cec2ea7f5f50999f236c22b65409a1f1a0d0" +dependencies = [ + "binary-merge", + "inplace-vec-builder", + "ref-cast", + "smallvec", +] + +[[package]] +name = "rangemap" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93e7e49bb0bf967717f7bd674458b3d6b0c5f48ec7e3038166026a69fc22223" + [[package]] name = "raw-window-handle" version = "0.6.2" @@ -2976,6 +3501,26 @@ dependencies = [ "thiserror 2.0.16", ] +[[package]] +name = "ref-cast" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "regex" version = "1.11.1" @@ -3026,6 +3571,48 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" +[[package]] +name = "reqwest" +version = "0.12.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" +dependencies = [ + "async-compression", + "base64", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots", +] + [[package]] name = "rfd" version = "0.15.4" @@ -3050,6 +3637,20 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rtrb" version = "0.3.2" @@ -3126,12 +3727,56 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "rustls" +version = "0.23.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "ruzstd" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fad02996bfc73da3e301efe90b1837be9ed8f4a462b6ed410aa35d00381de89f" +dependencies = [ + "twox-hash", +] + [[package]] name = "ryu" version = "1.0.20" @@ -3147,6 +3792,46 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "samply-symbols" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97e73d38bb04a373dba1260af91d4b0010e84cecd92d20b8e9949a910d5b9cbb" +dependencies = [ + "addr2line", + "bitflags 2.9.1", + "cpp_demangle", + "crc32fast", + "debugid", + "elsa", + "flate2", + "gimli", + "linux-perf-data", + "lzma-rs", + "macho-unwind-info", + "memchr", + "msvc-demangler", + "nom", + "object", + "pdb-addr2line", + "rangemap", + "rustc-demangle", + "scala-native-demangle", + "srcsrv", + "thiserror 2.0.12", + "uuid", + "yoke", + "yoke-derive", + "zerocopy", + "zerocopy-derive", +] + +[[package]] +name = "scala-native-demangle" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a4416eddc0eaf31e04aa4039bd3db4288ea1ba613955d86cf9c310049c5d1e2" + [[package]] name = "scoped-tls" version = "1.0.1" @@ -3159,6 +3844,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scroll" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" + [[package]] name = "sctk-adwaita" version = "0.10.1" @@ -3224,6 +3915,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -3326,6 +4029,16 @@ dependencies = [ "serde", ] +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "socket2" version = "0.6.0" @@ -3345,6 +4058,16 @@ dependencies = [ "bitflags 2.9.3", ] +[[package]] +name = "srcsrv" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cd3e3828fb4dd5ba0e7091777edb6c3db3cd2d6fc10547b29b40f6949a29be" +dependencies = [ + "memchr", + "thiserror 2.0.12", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -3397,6 +4120,30 @@ dependencies = [ "syn", ] +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "symsrv" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c73f1f7b9bc1423e6b5c6b4ed3840f266f8291b270520c0fb0501bad3a0aa7" +dependencies = [ + "async-compression", + "cab", + "dirs", + "fs4", + "futures-util", + "http", + "reqwest", + "scopeguard", + "thiserror 2.0.12", + "tokio", +] + [[package]] name = "syn" version = "2.0.106" @@ -3408,6 +4155,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + [[package]] name = "synstructure" version = "0.13.2" @@ -3515,6 +4271,25 @@ dependencies = [ "weezl", ] +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + [[package]] name = "tiny-skia" version = "0.11.4" @@ -3550,6 +4325,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.47.1" @@ -3563,7 +4353,7 @@ dependencies = [ "mio", "pin-project-lite", "slab", - "socket2", + "socket2 0.6.0", "tokio-macros", "windows-sys 0.59.0", ] @@ -3579,6 +4369,29 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" version = "0.8.23" @@ -3620,6 +4433,51 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.9.1", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + [[package]] name = "tracing" version = "0.1.41" @@ -3691,12 +4549,28 @@ dependencies = [ "strength_reduce", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "ttf-parser" version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +[[package]] +name = "twox-hash" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" +dependencies = [ + "cfg-if", + "static_assertions", +] + [[package]] name = "type-map" version = "0.5.1" @@ -3747,6 +4621,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.6" @@ -3815,6 +4695,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -3901,6 +4790,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wayland-backend" version = "0.3.11" @@ -4046,6 +4948,15 @@ dependencies = [ "web-sys", ] +[[package]] +name = "webpki-roots" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "weezl" version = "0.1.10" @@ -4199,6 +5110,34 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wholesym" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ea291707798a4b15f75d46418b6f7c5a044cce8d55e2f18584ccebcdb8b4354" +dependencies = [ + "async-compression", + "bytes", + "core-foundation 0.10.1", + "core-foundation-sys", + "debugid", + "flate2", + "fs4", + "futures-util", + "http", + "libc", + "memmap2", + "reqwest", + "samply-symbols", + "scopeguard", + "symsrv", + "thiserror 2.0.12", + "tokio", + "uuid", + "yoke", + "yoke-derive", +] + [[package]] name = "winapi" version = "0.3.9" @@ -5012,6 +5951,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + [[package]] name = "zerotrie" version = "0.2.2" @@ -5045,6 +5990,12 @@ dependencies = [ "syn", ] +[[package]] +name = "zlib-rs" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a" + [[package]] name = "zvariant" version = "5.6.0" diff --git a/Cargo.toml b/Cargo.toml index 5647ec3..7c4c10d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ tokio = { version = "1", features = ["io-util", "macros", "net", "rt", "sync", " tracing = { version = "0.1", features = ["release_max_level_info"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] } wgpu = "25" +wholesym = "0.8" winit = { version = "0.30", features = ["serde"] } [target.'cfg(windows)'.dependencies] diff --git a/src/emulator.rs b/src/emulator.rs index 1bcc2d3..0ba327f 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -18,12 +18,12 @@ use tracing::{error, warn}; use crate::{ audio::Audio, - emulator::{cart::Cart, shrooms_vb_core::SimEvent}, + emulator::cart::Cart, graphics::TextureSink, memory::{MemoryRange, MemoryRegion}, }; use shrooms_vb_core::{EXPECTED_FRAME_SIZE, Sim, StopReason}; -pub use shrooms_vb_core::{VBKey, VBRegister, VBWatchpointType}; +pub use shrooms_vb_core::{SimEvent, VBKey, VBRegister, VBWatchpointType}; mod address_set; mod cart; diff --git a/src/profiler.rs b/src/profiler.rs index 6004f44..20195a8 100644 --- a/src/profiler.rs +++ b/src/profiler.rs @@ -1,4 +1,6 @@ use std::{ + collections::HashMap, + path::PathBuf, sync::{ Arc, atomic::{AtomicBool, Ordering}, @@ -7,8 +9,9 @@ use std::{ }; use tokio::{select, sync::mpsc}; +use wholesym::{SymbolManager, SymbolMap}; -use crate::emulator::{EmulatorClient, EmulatorCommand, ProfileEvent, SimId}; +use crate::emulator::{EmulatorClient, EmulatorCommand, ProfileEvent, SimEvent, SimId}; pub struct Profiler { sim_id: SimId, @@ -65,15 +68,109 @@ async fn run_profile(sim_id: SimId, client: EmulatorClient, running: Arc { - println!("profiling {}", file_path.display()); + session = Some(ProfileSession::new(file_path).await); } ProfileEvent::Update { cycles, event } => { - println!("update {cycles} {event:#x?}"); + if let Some(session) = &mut session { + session.track_elapsed_cycles(cycles); + if let Some(event) = event { + session.track_event(event); + } + } } } } running.store(false, Ordering::Release); } + +struct ProfileSession { + symbol_map: SymbolMap, + call_stacks: HashMap>, + context_stack: Vec, +} + +struct StackFrame { + #[expect(dead_code)] + address: Option, + cycles: u64, +} + +impl ProfileSession { + async fn new(file_path: PathBuf) -> Self { + let symbol_manager = SymbolManager::with_config(Default::default()); + let symbol_map = symbol_manager + .load_symbol_map_for_binary_at_path(&file_path, None) + .await + .expect("cannae load symbols"); + let mut call_stacks = HashMap::new(); + call_stacks.insert( + 0, + vec![StackFrame { + address: None, + cycles: 0, + }], + ); + Self { + symbol_map, + call_stacks, + context_stack: vec![], + } + } + + fn track_elapsed_cycles(&mut self, cycles: u32) { + let code = self.context_stack.last().copied().unwrap_or(0); + let Some(stack) = self.call_stacks.get_mut(&code) else { + panic!("missing stack {code:04x}"); + }; + for frame in stack { + frame.cycles += cycles as u64; + } + } + + fn track_event(&mut self, event: SimEvent) { + match event { + SimEvent::Interrupt(code) => { + self.context_stack.push(code); + if self.call_stacks.insert(code, vec![]).is_some() { + panic!("{code:04x} fired twice"); + } + } + SimEvent::Reti => { + let Some(code) = self.context_stack.pop() else { + panic!("reti when not in interrupt"); + }; + if self.call_stacks.remove(&code).is_none() { + panic!("{code:04x} popped but never called") + } + } + SimEvent::Call(addr) => { + let code = self.context_stack.last().copied().unwrap_or(0); + let Some(stack) = self.call_stacks.get_mut(&code) else { + panic!("missing stack {code:04x}"); + }; + let name = self + .symbol_map + .lookup_sync(wholesym::LookupAddress::Svma(addr as u64)); + println!("depth {}: {:?}", stack.len(), name); + stack.push(StackFrame { + address: Some(addr), + cycles: 0, + }); + } + SimEvent::Return => { + let code = self.context_stack.last().copied().unwrap_or(0); + let Some(stack) = self.call_stacks.get_mut(&code) else { + panic!("missing stack {code:04x}"); + }; + if stack.pop().is_none() { + panic!("returned from {code:04x} but stack was empty"); + } + } + } + } +} From 8aab90f0246f97b7033391f6b3628135f226f035 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Mon, 11 Aug 2025 21:00:45 -0400 Subject: [PATCH 05/15] Track HALT --- src/emulator/shrooms_vb_core.rs | 30 ++++++++++-- src/profiler.rs | 84 +++++++++++++++++++++------------ 2 files changed, 80 insertions(+), 34 deletions(-) diff --git a/src/emulator/shrooms_vb_core.rs b/src/emulator/shrooms_vb_core.rs index 212d10c..0edd327 100644 --- a/src/emulator/shrooms_vb_core.rs +++ b/src/emulator/shrooms_vb_core.rs @@ -210,7 +210,7 @@ extern "C" fn on_execute(sim: *mut VB, address: u32, code: *const u16, length: c } } - let mut stopped = data.stop_reason.is_some(); + let mut stopped = data.stop_reason.is_some() || data.monitor.event.is_some(); if data.step_from.is_some_and(|s| s != address) { data.step_from = None; data.stop_reason = Some(StopReason::Stepped); @@ -247,7 +247,13 @@ extern "C" fn on_exception(sim: *mut VB, cause: *mut u16) -> c_int { // 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 })); + let cause = unsafe { *cause }; + let pc = if cause == 0xff70 { + 0xffffff60 + } else { + (cause & 0xfff0) as u32 | 0xffff0000 + }; + data.monitor.queued_event = Some(SimEvent::Interrupt(cause, pc)); 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 } @@ -319,7 +325,8 @@ extern "C" fn on_write( pub enum SimEvent { Call(u32), Return, - Interrupt(u16), + Halt, + Interrupt(u16, u32), Reti, } @@ -327,6 +334,7 @@ struct EventMonitor { enabled: bool, event: Option, queued_event: Option, + just_halted: bool, } impl EventMonitor { @@ -335,6 +343,7 @@ impl EventMonitor { enabled: false, event: None, queued_event: None, + just_halted: false, } } @@ -343,7 +352,8 @@ impl EventMonitor { self.queued_event.is_some() } - fn do_detect_event(&self, sim: *mut VB, address: u32, code: &[u16]) -> Option { + fn do_detect_event(&mut self, sim: *mut VB, address: u32, code: &[u16]) -> Option { + const HALT_OPCODE: u16 = 0b011010; const JAL_OPCODE: u16 = 0b101011; const JMP_OPCODE: u16 = 0b000110; const RETI_OPCODE: u16 = 0b011001; @@ -359,6 +369,18 @@ impl EventMonitor { let opcode = code[0] >> 10; + if opcode == HALT_OPCODE { + if !self.just_halted { + self.just_halted = true; + self.event = Some(SimEvent::Halt); + } else { + self.just_halted = false; + } + // Don't _return_ an event, we want to emit this right away. + // If the CPU is halting, no other callbacks will run for a long time. + return None; + } + if opcode == JAL_OPCODE { let disp = format_iv_disp(code); if disp != 4 { diff --git a/src/profiler.rs b/src/profiler.rs index 20195a8..dd27a47 100644 --- a/src/profiler.rs +++ b/src/profiler.rs @@ -96,10 +96,11 @@ struct ProfileSession { struct StackFrame { #[expect(dead_code)] - address: Option, + address: u32, cycles: u64, } +const RESET_CODE: u16 = 0xfff0; impl ProfileSession { async fn new(file_path: PathBuf) -> Self { let symbol_manager = SymbolManager::with_config(Default::default()); @@ -109,22 +110,24 @@ impl ProfileSession { .expect("cannae load symbols"); let mut call_stacks = HashMap::new(); call_stacks.insert( - 0, + RESET_CODE, vec![StackFrame { - address: None, + address: 0xfffffff0, cycles: 0, }], ); Self { symbol_map, call_stacks, - context_stack: vec![], + context_stack: vec![RESET_CODE], } } fn track_elapsed_cycles(&mut self, cycles: u32) { - let code = self.context_stack.last().copied().unwrap_or(0); - let Some(stack) = self.call_stacks.get_mut(&code) else { + let Some(code) = self.context_stack.last() else { + return; // program is halted, CPU is idle + }; + let Some(stack) = self.call_stacks.get_mut(code) else { panic!("missing stack {code:04x}"); }; for frame in stack { @@ -134,42 +137,63 @@ impl ProfileSession { fn track_event(&mut self, event: SimEvent) { match event { - SimEvent::Interrupt(code) => { - self.context_stack.push(code); - if self.call_stacks.insert(code, vec![]).is_some() { - panic!("{code:04x} fired twice"); - } - } - SimEvent::Reti => { - let Some(code) = self.context_stack.pop() else { - panic!("reti when not in interrupt"); + SimEvent::Call(address) => { + let Some(code) = self.context_stack.last() else { + panic!("How did we call anything when we're halted?"); }; - if self.call_stacks.remove(&code).is_none() { - panic!("{code:04x} popped but never called") - } - } - SimEvent::Call(addr) => { - let code = self.context_stack.last().copied().unwrap_or(0); - let Some(stack) = self.call_stacks.get_mut(&code) else { + let Some(stack) = self.call_stacks.get_mut(code) else { panic!("missing stack {code:04x}"); }; let name = self .symbol_map - .lookup_sync(wholesym::LookupAddress::Svma(addr as u64)); - println!("depth {}: {:?}", stack.len(), name); - stack.push(StackFrame { - address: Some(addr), - cycles: 0, - }); + .lookup_sync(wholesym::LookupAddress::Svma(address as u64)); + println!("depth {}: {:x?}", stack.len(), name); + stack.push(StackFrame { address, cycles: 0 }); } SimEvent::Return => { - let code = self.context_stack.last().copied().unwrap_or(0); - let Some(stack) = self.call_stacks.get_mut(&code) else { + let Some(code) = self.context_stack.last() else { + panic!("how did we return when we're halted?"); + }; + let Some(stack) = self.call_stacks.get_mut(code) else { panic!("missing stack {code:04x}"); }; if stack.pop().is_none() { panic!("returned from {code:04x} but stack was empty"); } + if stack.is_empty() { + panic!("returned to oblivion"); + } + } + SimEvent::Halt => { + let Some(RESET_CODE) = self.context_stack.pop() else { + panic!("halted when not in an interrupt"); + }; + } + SimEvent::Interrupt(code, address) => { + // if the CPU was halted before, wake it up now + if self.context_stack.is_empty() { + self.context_stack.push(RESET_CODE); + } + + self.context_stack.push(code); + if self + .call_stacks + .insert(code, vec![StackFrame { address, cycles: 0 }]) + .is_some() + { + panic!("{code:04x} fired twice"); + } + } + SimEvent::Reti => { + let Some(code) = self.context_stack.pop() else { + panic!("RETI when halted"); + }; + if code == RESET_CODE { + panic!("RETI when not in interrupt"); + } + if self.call_stacks.remove(&code).is_none() { + panic!("{code:04x} popped but never called"); + } } } } From ce15d22ab1d897ea2130013e8bfcc83c125e3e76 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Mon, 11 Aug 2025 22:51:05 -0400 Subject: [PATCH 06/15] UI for recording --- src/app.rs | 10 +- src/profiler.rs | 255 +++++++++++++++++++++++++++++++++--------- src/window/profile.rs | 88 +++++++++++++-- 3 files changed, 287 insertions(+), 66 deletions(-) diff --git a/src/app.rs b/src/app.rs index b1f7245..0ad3ee1 100644 --- a/src/app.rs +++ b/src/app.rs @@ -116,11 +116,6 @@ impl ApplicationHandler for Application { server.launch(port); self.open(event_loop, Box::new(server)); } - if self.init_profiling { - let mut profiler = ProfileWindow::new(SimId::Player1, self.client.clone()); - profiler.launch(); - self.open(event_loop, Box::new(profiler)); - } let app = GameWindow::new( self.client.clone(), self.proxy.clone(), @@ -129,6 +124,11 @@ impl ApplicationHandler for Application { SimId::Player1, ); self.open(event_loop, Box::new(app)); + if self.init_profiling { + let mut profiler = ProfileWindow::new(SimId::Player1, self.client.clone()); + profiler.launch(); + self.open(event_loop, Box::new(profiler)); + } } fn window_event( diff --git a/src/profiler.rs b/src/profiler.rs index dd27a47..a9ccb22 100644 --- a/src/profiler.rs +++ b/src/profiler.rs @@ -1,13 +1,11 @@ use std::{ collections::HashMap, path::PathBuf, - sync::{ - Arc, - atomic::{AtomicBool, Ordering}, - }, + sync::{Arc, Mutex}, thread, }; +use anyhow::{Result, bail}; use tokio::{select, sync::mpsc}; use wholesym::{SymbolManager, SymbolMap}; @@ -16,7 +14,8 @@ use crate::emulator::{EmulatorClient, EmulatorCommand, ProfileEvent, SimEvent, S pub struct Profiler { sim_id: SimId, client: EmulatorClient, - running: Arc, + status: Arc>, + action: Option>, killer: Option>, } @@ -25,21 +24,24 @@ impl Profiler { Self { sim_id, client, - running: Arc::new(AtomicBool::new(false)), + status: Arc::new(Mutex::new(ProfilerStatus::Disabled)), + action: None, killer: None, } } - pub fn started(&self) -> bool { - self.running.load(Ordering::Relaxed) + pub fn status(&self) -> ProfilerStatus { + self.status.lock().unwrap().clone() } - pub fn start(&mut self) { + pub fn enable(&mut self) { let sim_id = self.sim_id; let client = self.client.clone(); - let running = self.running.clone(); - let (tx, rx) = oneshot::channel(); - self.killer = Some(tx); + let status = self.status.clone(); + let (action_tx, action_rx) = mpsc::unbounded_channel(); + self.action = Some(action_tx); + let (killer_tx, killer_rx) = oneshot::channel(); + self.killer = Some(killer_tx); thread::spawn(move || { tokio::runtime::Builder::new_current_thread() .enable_all() @@ -47,48 +49,194 @@ impl Profiler { .unwrap() .block_on(async move { select! { - _ = run_profile(sim_id, client, running.clone()) => {} - _ = rx => { - running.store(false, Ordering::Relaxed); + _ = run_profile(sim_id, client, status.clone(), action_rx) => {} + _ = killer_rx => { + *status.lock().unwrap() = ProfilerStatus::Disabled; } } }) }); } - pub fn stop(&mut self) { + pub fn disable(&mut self) { if let Some(killer) = self.killer.take() { let _ = killer.send(()); } } + + pub fn start_recording(&mut self) { + if let Some(action) = &self.action { + let _ = action.send(RecordingAction::Start); + } + } + + pub fn finish_recording(&mut self) -> oneshot::Receiver> { + let (tx, rx) = oneshot::channel(); + if let Some(action) = &self.action { + let _ = action.send(RecordingAction::Finish(tx)); + } + rx + } + + pub fn cancel_recording(&mut self) { + if let Some(action) = &self.action { + let _ = action.send(RecordingAction::Cancel); + } + } } -async fn run_profile(sim_id: SimId, client: EmulatorClient, running: Arc) { +impl Drop for Profiler { + fn drop(&mut self) { + self.disable(); + } +} + +async fn run_profile( + sim_id: SimId, + client: EmulatorClient, + status: Arc>, + mut action_source: mpsc::UnboundedReceiver, +) { let (profile_sync, mut profile_source) = mpsc::unbounded_channel(); client.send_command(EmulatorCommand::StartProfiling(sim_id, profile_sync)); - running.store(true, Ordering::Relaxed); + *status.lock().unwrap() = ProfilerStatus::Enabled; - let mut session = None; - while let Some(event) = profile_source.recv().await { - match event { - ProfileEvent::Start { file_path } => { - session = Some(ProfileSession::new(file_path).await); - } - ProfileEvent::Update { cycles, event } => { - if let Some(session) = &mut session { - session.track_elapsed_cycles(cycles); - if let Some(event) = event { - session.track_event(event); - } + let mut session = ProfilerSession::new(); + loop { + select! { + maybe_event = profile_source.recv() => { + let Some(event) = maybe_event else { + break; // emulator thread disconnected + }; + if let Err(error) = handle_event(event, &mut session).await { + *status.lock().unwrap() = ProfilerStatus::Error(error.to_string()); + return; } } + maybe_action = action_source.recv() => { + let Some(action) = maybe_action else { + break; // ui thread disconnected + }; + handle_action(action, &mut session, &status); + } } } - running.store(false, Ordering::Release); + + *status.lock().unwrap() = ProfilerStatus::Disabled; } -struct ProfileSession { +async fn handle_event(event: ProfileEvent, session: &mut ProfilerSession) -> Result<()> { + match event { + ProfileEvent::Start { file_path } => session.start_profiling(file_path).await, + ProfileEvent::Update { cycles, event } => { + session.track_elapsed_cycles(cycles)?; + if let Some(event) = event { + session.track_event(event)?; + } + Ok(()) + } + } +} + +fn handle_action( + action: RecordingAction, + session: &mut ProfilerSession, + status: &Mutex, +) { + match action { + RecordingAction::Start => { + session.start_recording(); + *status.lock().unwrap() = ProfilerStatus::Recording; + } + RecordingAction::Finish(rx) => { + if let Some(bytes) = session.finish_recording() { + let _ = rx.send(bytes); + } + *status.lock().unwrap() = ProfilerStatus::Enabled; + } + RecordingAction::Cancel => { + session.cancel_recording(); + *status.lock().unwrap() = ProfilerStatus::Enabled; + } + } +} + +#[derive(Clone)] +pub enum ProfilerStatus { + Disabled, + Enabled, + Recording, + Error(String), +} + +impl ProfilerStatus { + pub fn enabled(&self) -> bool { + matches!(self, Self::Enabled | Self::Recording) + } +} + +enum RecordingAction { + Start, + Finish(oneshot::Sender>), + Cancel, +} + +struct Recording {} + +impl Recording { + fn new() -> Self { + Self {} + } +} + +struct ProfilerSession { + program: Option, + recording: Option, +} + +impl ProfilerSession { + fn new() -> Self { + Self { + program: None, + recording: None, + } + } + + async fn start_profiling(&mut self, file_path: PathBuf) -> Result<()> { + self.program = Some(ProgramState::new(file_path).await?); + self.recording = None; + Ok(()) + } + + fn track_elapsed_cycles(&mut self, cycles: u32) -> Result<()> { + if let Some(program) = &mut self.program { + program.track_elapsed_cycles(cycles)?; + } + Ok(()) + } + + fn track_event(&mut self, event: SimEvent) -> Result<()> { + if let Some(program) = &mut self.program { + program.track_event(event)?; + } + Ok(()) + } + + fn start_recording(&mut self) { + self.recording = Some(Recording::new()); + } + + fn finish_recording(&mut self) -> Option> { + self.recording.take().map(|_| vec![]) + } + + fn cancel_recording(&mut self) { + self.recording.take(); + } +} + +struct ProgramState { symbol_map: SymbolMap, call_stacks: HashMap>, context_stack: Vec, @@ -101,13 +249,12 @@ struct StackFrame { } const RESET_CODE: u16 = 0xfff0; -impl ProfileSession { - async fn new(file_path: PathBuf) -> Self { +impl ProgramState { + async fn new(file_path: PathBuf) -> Result { let symbol_manager = SymbolManager::with_config(Default::default()); let symbol_map = symbol_manager .load_symbol_map_for_binary_at_path(&file_path, None) - .await - .expect("cannae load symbols"); + .await?; let mut call_stacks = HashMap::new(); call_stacks.insert( RESET_CODE, @@ -116,33 +263,34 @@ impl ProfileSession { cycles: 0, }], ); - Self { + Ok(Self { symbol_map, call_stacks, context_stack: vec![RESET_CODE], - } + }) } - fn track_elapsed_cycles(&mut self, cycles: u32) { + fn track_elapsed_cycles(&mut self, cycles: u32) -> Result<()> { let Some(code) = self.context_stack.last() else { - return; // program is halted, CPU is idle + return Ok(()); // program is halted, CPU is idle }; let Some(stack) = self.call_stacks.get_mut(code) else { - panic!("missing stack {code:04x}"); + bail!("missing stack {code:04x}"); }; for frame in stack { frame.cycles += cycles as u64; } + Ok(()) } - fn track_event(&mut self, event: SimEvent) { + fn track_event(&mut self, event: SimEvent) -> Result<()> { match event { SimEvent::Call(address) => { let Some(code) = self.context_stack.last() else { - panic!("How did we call anything when we're halted?"); + bail!("How did we call anything when we're halted?"); }; let Some(stack) = self.call_stacks.get_mut(code) else { - panic!("missing stack {code:04x}"); + bail!("missing stack {code:04x}"); }; let name = self .symbol_map @@ -152,21 +300,21 @@ impl ProfileSession { } SimEvent::Return => { let Some(code) = self.context_stack.last() else { - panic!("how did we return when we're halted?"); + bail!("how did we return when we're halted?"); }; let Some(stack) = self.call_stacks.get_mut(code) else { - panic!("missing stack {code:04x}"); + bail!("missing stack {code:04x}"); }; if stack.pop().is_none() { - panic!("returned from {code:04x} but stack was empty"); + bail!("returned from {code:04x} but stack was empty"); } if stack.is_empty() { - panic!("returned to oblivion"); + bail!("returned to oblivion"); } } SimEvent::Halt => { let Some(RESET_CODE) = self.context_stack.pop() else { - panic!("halted when not in an interrupt"); + bail!("halted when not in an interrupt"); }; } SimEvent::Interrupt(code, address) => { @@ -181,20 +329,21 @@ impl ProfileSession { .insert(code, vec![StackFrame { address, cycles: 0 }]) .is_some() { - panic!("{code:04x} fired twice"); + bail!("{code:04x} fired twice"); } } SimEvent::Reti => { let Some(code) = self.context_stack.pop() else { - panic!("RETI when halted"); + bail!("RETI when halted"); }; if code == RESET_CODE { - panic!("RETI when not in interrupt"); + bail!("RETI when not in interrupt"); } if self.call_stacks.remove(&code).is_none() { - panic!("{code:04x} popped but never called"); + bail!("{code:04x} popped but never called"); } } } + Ok(()) } } diff --git a/src/window/profile.rs b/src/window/profile.rs index 41de435..0a2d1a1 100644 --- a/src/window/profile.rs +++ b/src/window/profile.rs @@ -1,14 +1,19 @@ -use egui::{CentralPanel, ViewportBuilder, ViewportId}; +use std::{fs, time::Duration}; + +use anyhow::Result; +use egui::{Button, CentralPanel, Checkbox, ViewportBuilder, ViewportId}; +use egui_notify::{Anchor, Toast, Toasts}; use crate::{ emulator::{EmulatorClient, SimId}, - profiler::Profiler, + profiler::{Profiler, ProfilerStatus}, window::AppWindow, }; pub struct ProfileWindow { sim_id: SimId, profiler: Profiler, + toasts: Toasts, } impl ProfileWindow { @@ -16,11 +21,47 @@ impl ProfileWindow { Self { sim_id, profiler: Profiler::new(sim_id, client), + toasts: Toasts::new() + .with_anchor(Anchor::BottomLeft) + .with_margin((10.0, 10.0).into()) + .reverse(true), } } pub fn launch(&mut self) { - self.profiler.start(); + self.profiler.enable(); + } + + fn finish_recording(&mut self) { + match self.try_finish_recording() { + Ok(Some(path)) => { + let mut toast = Toast::info(format!("Saved to {path}")); + toast.duration(Some(Duration::from_secs(5))); + self.toasts.add(toast); + } + Ok(None) => {} + Err(error) => { + let mut toast = Toast::error(format!("{error:#}")); + toast.duration(Some(Duration::from_secs(5))); + self.toasts.add(toast); + } + } + } + + fn try_finish_recording(&mut self) -> Result> { + let bytes_receiver = self.profiler.finish_recording(); + let file = rfd::FileDialog::new() + .add_filter("Profiler files", &["json"]) + .set_file_name("profile.json") + .save_file(); + if let Some(path) = file { + let bytes = pollster::block_on(bytes_receiver)?; + fs::write(&path, bytes)?; + Ok(Some(path.display().to_string())) + } else { + self.profiler.cancel_recording(); + Ok(None) + } } } @@ -40,15 +81,46 @@ impl AppWindow for ProfileWindow { } fn show(&mut self, ctx: &egui::Context) { + let status = self.profiler.status(); + let recording = matches!(status, ProfilerStatus::Recording); CentralPanel::default().show(ctx, |ui| { - let mut started = self.profiler.started(); - if ui.checkbox(&mut started, "Profiling enabled?").changed() { - if started { - self.profiler.start(); + let mut enabled = status.enabled(); + let enabled_checkbox = Checkbox::new(&mut enabled, "Profiling enabled?"); + if ui.add_enabled(!recording, enabled_checkbox).changed() { + if enabled { + self.profiler.enable(); } else { - self.profiler.stop(); + self.profiler.disable(); } } + + ui.horizontal(|ui| { + if !recording { + let record_button = Button::new("Record"); + let can_record = matches!(status, ProfilerStatus::Enabled); + if ui.add_enabled(can_record, record_button).clicked() { + self.profiler.start_recording(); + } + } else { + if ui.button("Finish recording").clicked() { + self.finish_recording(); + } + if ui.button("Cancel recording").clicked() { + self.profiler.cancel_recording(); + } + } + }); + + match &status { + ProfilerStatus::Recording => { + ui.label("Recording..."); + } + ProfilerStatus::Error(message) => { + ui.label(message); + } + _ => {} + } }); + self.toasts.show(ctx); } } From ed06004a60a16d091521ab1493d480fc2a8d592e Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Wed, 13 Aug 2025 23:58:40 -0400 Subject: [PATCH 07/15] Actually produce profiler files --- Cargo.lock | 25 +++++++ Cargo.toml | 2 + src/profiler.rs | 140 ++++---------------------------------- src/profiler/recording.rs | 114 +++++++++++++++++++++++++++++++ src/profiler/state.rs | 104 ++++++++++++++++++++++++++++ src/profiler/symbols.rs | 67 ++++++++++++++++++ 6 files changed, 325 insertions(+), 127 deletions(-) create mode 100644 src/profiler/recording.rs create mode 100644 src/profiler/state.rs create mode 100644 src/profiler/symbols.rs diff --git a/Cargo.lock b/Cargo.lock index 86a4786..74bf0f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1507,6 +1507,20 @@ dependencies = [ "slab", ] +[[package]] +name = "fxprof-processed-profile" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25234f20a3ec0a962a61770cfe39ecf03cb529a6e474ad8cff025ed497eda557" +dependencies = [ + "bitflags 2.9.1", + "debugid", + "rustc-hash 2.1.1", + "serde", + "serde_derive", + "serde_json", +] + [[package]] name = "gethostname" version = "0.4.3" @@ -2140,10 +2154,12 @@ dependencies = [ "egui_extras", "elf", "fixed", + "fxprof-processed-profile", "gilrs", "hex", "image", "itertools 0.14.0", + "normpath", "num-derive", "num-traits", "oneshot", @@ -2569,6 +2585,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "normpath" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8911957c4b1549ac0dc74e30db9c8b0e66ddcd6d7acc33098f4c63a64a6d7ed" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" diff --git a/Cargo.toml b/Cargo.toml index 7c4c10d..0fb4fa6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,11 +22,13 @@ egui-notify = "0.20" egui-winit = "0.32" egui-wgpu = { version = "0.32", features = ["winit"] } elf = "0.8" +fxprof-processed-profile = "0.8" fixed = { version = "1.28", features = ["num-traits"] } gilrs = { version = "0.11", features = ["serde-serialize"] } hex = "0.4" image = { version = "0.25", default-features = false, features = ["png"] } itertools = "0.14" +normpath = "1" num-derive = "0.4" num-traits = "0.2" oneshot = "0.1" diff --git a/src/profiler.rs b/src/profiler.rs index a9ccb22..62ebac1 100644 --- a/src/profiler.rs +++ b/src/profiler.rs @@ -1,15 +1,19 @@ use std::{ - collections::HashMap, path::PathBuf, sync::{Arc, Mutex}, thread, }; -use anyhow::{Result, bail}; +use anyhow::Result; use tokio::{select, sync::mpsc}; -use wholesym::{SymbolManager, SymbolMap}; use crate::emulator::{EmulatorClient, EmulatorCommand, ProfileEvent, SimEvent, SimId}; +use recording::Recording; +use state::ProgramState; + +mod recording; +mod state; +mod symbols; pub struct Profiler { sim_id: SimId, @@ -182,14 +186,6 @@ enum RecordingAction { Cancel, } -struct Recording {} - -impl Recording { - fn new() -> Self { - Self {} - } -} - struct ProfilerSession { program: Option, recording: Option, @@ -210,8 +206,8 @@ impl ProfilerSession { } fn track_elapsed_cycles(&mut self, cycles: u32) -> Result<()> { - if let Some(program) = &mut self.program { - program.track_elapsed_cycles(cycles)?; + if let (Some(state), Some(recording)) = (&self.program, &mut self.recording) { + recording.track_elapsed_cycles(state, cycles); } Ok(()) } @@ -224,126 +220,16 @@ impl ProfilerSession { } fn start_recording(&mut self) { - self.recording = Some(Recording::new()); + if let Some(program) = &self.program { + self.recording = Some(Recording::new(program)); + } } fn finish_recording(&mut self) -> Option> { - self.recording.take().map(|_| vec![]) + self.recording.take().map(|r| r.finish()) } fn cancel_recording(&mut self) { self.recording.take(); } } - -struct ProgramState { - symbol_map: SymbolMap, - call_stacks: HashMap>, - context_stack: Vec, -} - -struct StackFrame { - #[expect(dead_code)] - address: u32, - cycles: u64, -} - -const RESET_CODE: u16 = 0xfff0; -impl ProgramState { - async fn new(file_path: PathBuf) -> Result { - let symbol_manager = SymbolManager::with_config(Default::default()); - let symbol_map = symbol_manager - .load_symbol_map_for_binary_at_path(&file_path, None) - .await?; - let mut call_stacks = HashMap::new(); - call_stacks.insert( - RESET_CODE, - vec![StackFrame { - address: 0xfffffff0, - cycles: 0, - }], - ); - Ok(Self { - symbol_map, - call_stacks, - context_stack: vec![RESET_CODE], - }) - } - - fn track_elapsed_cycles(&mut self, cycles: u32) -> Result<()> { - let Some(code) = self.context_stack.last() else { - return Ok(()); // program is halted, CPU is idle - }; - let Some(stack) = self.call_stacks.get_mut(code) else { - bail!("missing stack {code:04x}"); - }; - for frame in stack { - frame.cycles += cycles as u64; - } - Ok(()) - } - - fn track_event(&mut self, event: SimEvent) -> Result<()> { - match event { - SimEvent::Call(address) => { - let Some(code) = self.context_stack.last() else { - bail!("How did we call anything when we're halted?"); - }; - let Some(stack) = self.call_stacks.get_mut(code) else { - bail!("missing stack {code:04x}"); - }; - let name = self - .symbol_map - .lookup_sync(wholesym::LookupAddress::Svma(address as u64)); - println!("depth {}: {:x?}", stack.len(), name); - stack.push(StackFrame { address, cycles: 0 }); - } - SimEvent::Return => { - let Some(code) = self.context_stack.last() else { - bail!("how did we return when we're halted?"); - }; - let Some(stack) = self.call_stacks.get_mut(code) else { - bail!("missing stack {code:04x}"); - }; - if stack.pop().is_none() { - bail!("returned from {code:04x} but stack was empty"); - } - if stack.is_empty() { - bail!("returned to oblivion"); - } - } - SimEvent::Halt => { - let Some(RESET_CODE) = self.context_stack.pop() else { - bail!("halted when not in an interrupt"); - }; - } - SimEvent::Interrupt(code, address) => { - // if the CPU was halted before, wake it up now - if self.context_stack.is_empty() { - self.context_stack.push(RESET_CODE); - } - - self.context_stack.push(code); - if self - .call_stacks - .insert(code, vec![StackFrame { address, cycles: 0 }]) - .is_some() - { - bail!("{code:04x} fired twice"); - } - } - SimEvent::Reti => { - let Some(code) = self.context_stack.pop() else { - bail!("RETI when halted"); - }; - if code == RESET_CODE { - bail!("RETI when not in interrupt"); - } - if self.call_stacks.remove(&code).is_none() { - bail!("{code:04x} popped but never called"); - } - } - } - Ok(()) - } -} diff --git a/src/profiler/recording.rs b/src/profiler/recording.rs new file mode 100644 index 0000000..d4836eb --- /dev/null +++ b/src/profiler/recording.rs @@ -0,0 +1,114 @@ +use std::collections::HashMap; + +use fxprof_processed_profile::{ + CategoryHandle, CpuDelta, Frame, FrameFlags, FrameInfo, ProcessHandle, Profile, + ReferenceTimestamp, SamplingInterval, StackHandle, ThreadHandle, Timestamp, +}; + +use crate::profiler::state::{ProgramState, RESET_CODE, StackFrame}; + +pub struct Recording { + profile: Profile, + process: ProcessHandle, + threads: HashMap, + now: u64, +} + +impl Recording { + pub fn new(state: &ProgramState) -> Self { + let symbol_file = state.symbol_file(); + + let name = &symbol_file.name(); + let reference_timestamp = ReferenceTimestamp::from_millis_since_unix_epoch(0.0); + let interval = SamplingInterval::from_hz(20_000_000.0); + let mut profile = Profile::new(name, reference_timestamp, interval); + + let process = profile.add_process(name, 1, Timestamp::from_nanos_since_reference(0)); + + let lib = profile.add_lib(symbol_file.library_info().clone()); + profile.add_lib_mapping(process, lib, 0x00000000, 0xffffffff, 0); + + let mut me = Self { + profile, + process, + threads: HashMap::new(), + now: 0, + }; + me.track_elapsed_cycles(state, 0); + me + } + + pub fn track_elapsed_cycles(&mut self, state: &ProgramState, cycles: u32) { + self.now += cycles as u64; + let timestamp = Timestamp::from_nanos_since_reference(self.now * 50); + let weight = 1; + + let active_code = if let Some((code, frames)) = state.current_stack() { + let thread = *self.threads.entry(code).or_insert_with(|| { + let process = self.process; + let tid = code as u32; + let start_time = Timestamp::from_nanos_since_reference(self.now * 50); + let is_main = code == RESET_CODE; + let thread = self.profile.add_thread(process, tid, start_time, is_main); + self.profile + .set_thread_name(thread, &thread_name_for_code(code)); + thread + }); + + let stack = self.handle_for_stack(thread, frames); + let cpu_delta = CpuDelta::from_nanos((self.now - cycles as u64) * 50); + self.profile + .add_sample(thread, timestamp, stack, cpu_delta, weight); + Some(code) + } else { + None + }; + for (code, thread) in &self.threads { + if active_code == Some(*code) { + continue; + } + self.profile + .add_sample_same_stack_zero_cpu(*thread, timestamp, weight); + } + } + + pub fn finish(self) -> Vec { + serde_json::to_vec(&self.profile).expect("could not serialize profile") + } + + fn handle_for_stack( + &mut self, + thread: ThreadHandle, + frames: &[StackFrame], + ) -> Option { + self.profile.intern_stack_frames( + thread, + frames.iter().map(|f| FrameInfo { + frame: Frame::InstructionPointer(f.address as u64), + category_pair: CategoryHandle::OTHER.into(), + flags: FrameFlags::empty(), + }), + ) + } +} + +fn thread_name_for_code(code: u16) -> std::borrow::Cow<'static, str> { + match code { + RESET_CODE => "Main".into(), + 0xffd0 => "Duplexed exception".into(), + 0xfe40 => "VIP interrupt".into(), + 0xfe30 => "Communication interrupt".into(), + 0xfe20 => "Game pak interrupt".into(), + 0xfe10 => "Timer interrupt".into(), + 0xfe00 => "Game pad interrupt".into(), + 0xffc0 => "Address trap".into(), + 0xffa0..0xffc0 => format!("Trap (vector {})", code - 0xffa0).into(), + 0xff90 => "Illegal opcode exception".into(), + 0xff80 => "Zero division exception".into(), + 0xff60 => "Floating-point reserved operand exception".into(), + 0xff70 => "Floating-point invalid operation exception".into(), + 0xff68 => "Floating-point zero division exception".into(), + 0xff64 => "Floating-point overflow exception".into(), + other => format!("Unrecognized handler (0x{other:04x})").into(), + } +} diff --git a/src/profiler/state.rs b/src/profiler/state.rs new file mode 100644 index 0000000..40bdc72 --- /dev/null +++ b/src/profiler/state.rs @@ -0,0 +1,104 @@ +use std::{collections::HashMap, path::PathBuf}; + +use anyhow::{Result, bail}; + +use crate::{emulator::SimEvent, profiler::symbols::SymbolFile}; + +pub struct ProgramState { + symbol_file: SymbolFile, + call_stacks: HashMap>, + context_stack: Vec, +} + +pub struct StackFrame { + pub address: u32, +} + +pub const RESET_CODE: u16 = 0xfff0; +impl ProgramState { + pub async fn new(file_path: PathBuf) -> Result { + let symbol_file = SymbolFile::load(&file_path).await?; + let mut call_stacks = HashMap::new(); + call_stacks.insert( + RESET_CODE, + vec![StackFrame { + address: 0xfffffff0, + }], + ); + Ok(Self { + symbol_file, + call_stacks, + context_stack: vec![RESET_CODE], + }) + } + + pub fn symbol_file(&self) -> &SymbolFile { + &self.symbol_file + } + + pub fn current_stack(&self) -> Option<(u16, &[StackFrame])> { + let code = self.context_stack.last()?; + let call_stack = self.call_stacks.get(code)?; + Some((*code, call_stack)) + } + + pub fn track_event(&mut self, event: SimEvent) -> Result<()> { + match event { + SimEvent::Call(address) => { + let Some(code) = self.context_stack.last() else { + bail!("How did we call anything when we're halted?"); + }; + let Some(stack) = self.call_stacks.get_mut(code) else { + bail!("missing stack {code:04x}"); + }; + stack.push(StackFrame { address }); + } + SimEvent::Return => { + let Some(code) = self.context_stack.last() else { + bail!("how did we return when we're halted?"); + }; + let Some(stack) = self.call_stacks.get_mut(code) else { + bail!("missing stack {code:04x}"); + }; + if stack.pop().is_none() { + bail!("returned from {code:04x} but stack was empty"); + } + if stack.is_empty() { + bail!("returned to oblivion"); + } + } + SimEvent::Halt => { + let Some(RESET_CODE) = self.context_stack.pop() else { + bail!("halted when not in an interrupt"); + }; + } + SimEvent::Interrupt(code, address) => { + // if the CPU was halted before, wake it up now + if self.context_stack.is_empty() { + self.context_stack.push(RESET_CODE); + } + + self.context_stack.push(code); + if self + .call_stacks + .insert(code, vec![StackFrame { address }]) + .is_some() + { + bail!("{code:04x} fired twice"); + } + } + SimEvent::Reti => { + let Some(code) = self.context_stack.pop() else { + bail!("RETI when halted"); + }; + if code == RESET_CODE { + bail!("RETI when not in interrupt"); + } + if self.call_stacks.remove(&code).is_none() { + bail!("{code:04x} popped but never called"); + } + } + } + Ok(()) + } +} diff --git a/src/profiler/symbols.rs b/src/profiler/symbols.rs new file mode 100644 index 0000000..fd84211 --- /dev/null +++ b/src/profiler/symbols.rs @@ -0,0 +1,67 @@ +use std::{path::Path, sync::Arc}; + +use anyhow::Result; +use fxprof_processed_profile::{LibraryInfo, Symbol, SymbolTable}; +use wholesym::{SymbolManager, samply_symbols::demangle_any}; + +pub struct SymbolFile { + library_info: LibraryInfo, +} + +impl SymbolFile { + pub async fn load(file_path: &Path) -> Result { + let normalized = normpath::PathExt::normalize(file_path)?; + let library_info = + SymbolManager::library_info_for_binary_at_path(normalized.as_path(), None).await?; + + let symbol_manager = SymbolManager::with_config(Default::default()); + let symbol_map = symbol_manager + .load_symbol_map_for_binary_at_path(normalized.as_path(), None) + .await?; + + let name = library_info + .name + .or_else(|| { + normalized + .file_name() + .map(|n| n.to_string_lossy().into_owned()) + }) + .unwrap_or("game".to_string()); + let debug_name = library_info.debug_name.unwrap_or_else(|| name.clone()); + let path = library_info + .path + .unwrap_or_else(|| normalized.into_os_string().to_string_lossy().into_owned()); + let debug_path = library_info.debug_path.unwrap_or_else(|| path.clone()); + let debug_id = library_info.debug_id.unwrap_or_default(); + let code_id = library_info.code_id.map(|id| id.to_string()); + let arch = library_info.arch; + let symbols = symbol_map + .iter_symbols() + .map(|(address, name)| Symbol { + address: address + 0x07000000, + size: None, + name: demangle_any(&name), + }) + .collect(); + Ok(Self { + library_info: LibraryInfo { + name, + debug_name, + path, + debug_path, + debug_id, + code_id, + arch, + symbol_table: Some(Arc::new(SymbolTable::new(symbols))), + }, + }) + } + + pub fn name(&self) -> &str { + &self.library_info.name + } + + pub fn library_info(&self) -> &LibraryInfo { + &self.library_info + } +} From 5e23df4723c57d75708afa991a69df9045aad409 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Fri, 15 Aug 2025 23:15:43 -0400 Subject: [PATCH 08/15] Add markers to event stream --- src/emulator/shrooms_vb_core.rs | 6 +- src/profiler.rs | 18 ++++- src/profiler/recording.rs | 39 ++++++++++- src/profiler/state.rs | 116 +++++++++++++++++--------------- 4 files changed, 116 insertions(+), 63 deletions(-) diff --git a/src/emulator/shrooms_vb_core.rs b/src/emulator/shrooms_vb_core.rs index 0edd327..56684b6 100644 --- a/src/emulator/shrooms_vb_core.rs +++ b/src/emulator/shrooms_vb_core.rs @@ -1,4 +1,4 @@ -use std::{ffi::c_void, ptr, slice}; +use std::{borrow::Cow, ffi::c_void, ptr, slice}; use anyhow::{Result, anyhow}; use bitflags::bitflags; @@ -191,6 +191,9 @@ extern "C" fn on_frame(sim: *mut VB) -> c_int { // 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.frame_seen = true; + if data.monitor.enabled { + data.monitor.event = Some(SimEvent::Marker(Cow::Borrowed("Frame Drawn"))); + } 1 } @@ -328,6 +331,7 @@ pub enum SimEvent { Halt, Interrupt(u16, u32), Reti, + Marker(Cow<'static, str>), } struct EventMonitor { diff --git a/src/profiler.rs b/src/profiler.rs index 62ebac1..d818b0d 100644 --- a/src/profiler.rs +++ b/src/profiler.rs @@ -213,10 +213,22 @@ impl ProfilerSession { } fn track_event(&mut self, event: SimEvent) -> Result<()> { - if let Some(program) = &mut self.program { - program.track_event(event)?; + let Some(program) = &mut self.program else { + return Ok(()); + }; + match event { + SimEvent::Call(address) => program.track_call(address), + SimEvent::Return => program.track_return(), + SimEvent::Halt => program.track_halt(), + SimEvent::Interrupt(code, address) => program.track_interrupt(code, address), + SimEvent::Reti => program.track_reti(), + SimEvent::Marker(name) => { + if let Some(recording) = &mut self.recording { + recording.track_marker(name); + }; + Ok(()) + } } - Ok(()) } fn start_recording(&mut self) { diff --git a/src/profiler/recording.rs b/src/profiler/recording.rs index d4836eb..0b1b778 100644 --- a/src/profiler/recording.rs +++ b/src/profiler/recording.rs @@ -1,8 +1,9 @@ -use std::collections::HashMap; +use std::{borrow::Cow, collections::HashMap}; use fxprof_processed_profile::{ - CategoryHandle, CpuDelta, Frame, FrameFlags, FrameInfo, ProcessHandle, Profile, - ReferenceTimestamp, SamplingInterval, StackHandle, ThreadHandle, Timestamp, + CategoryHandle, CpuDelta, Frame, FrameFlags, FrameInfo, MarkerTiming, ProcessHandle, Profile, + ReferenceTimestamp, SamplingInterval, StackHandle, StaticSchemaMarker, StringHandle, + ThreadHandle, Timestamp, }; use crate::profiler::state::{ProgramState, RESET_CODE, StackFrame}; @@ -72,6 +73,15 @@ impl Recording { } } + pub fn track_marker(&mut self, name: Cow<'static, str>) { + let Some(thread) = self.threads.get(&RESET_CODE) else { + return; + }; + let timing = MarkerTiming::Instant(Timestamp::from_nanos_since_reference(self.now * 50)); + let marker = SimpleMarker(name); + self.profile.add_marker(*thread, timing, marker); + } + pub fn finish(self) -> Vec { serde_json::to_vec(&self.profile).expect("could not serialize profile") } @@ -92,6 +102,29 @@ impl Recording { } } +struct SimpleMarker(Cow<'static, str>); + +impl StaticSchemaMarker for SimpleMarker { + const UNIQUE_MARKER_TYPE_NAME: &'static str = "Simple"; + const FIELDS: &'static [fxprof_processed_profile::StaticSchemaMarkerField] = &[]; + + fn name(&self, profile: &mut Profile) -> StringHandle { + profile.intern_string(&self.0) + } + + fn category(&self, _profile: &mut Profile) -> CategoryHandle { + CategoryHandle::OTHER + } + + fn string_field_value(&self, _field_index: u32) -> StringHandle { + unreachable!() + } + + fn number_field_value(&self, _field_index: u32) -> f64 { + unreachable!() + } +} + fn thread_name_for_code(code: u16) -> std::borrow::Cow<'static, str> { match code { RESET_CODE => "Main".into(), diff --git a/src/profiler/state.rs b/src/profiler/state.rs index 40bdc72..d2da976 100644 --- a/src/profiler/state.rs +++ b/src/profiler/state.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, path::PathBuf}; use anyhow::{Result, bail}; -use crate::{emulator::SimEvent, profiler::symbols::SymbolFile}; +use crate::profiler::symbols::SymbolFile; pub struct ProgramState { symbol_file: SymbolFile, @@ -42,62 +42,66 @@ impl ProgramState { Some((*code, call_stack)) } - pub fn track_event(&mut self, event: SimEvent) -> Result<()> { - match event { - SimEvent::Call(address) => { - let Some(code) = self.context_stack.last() else { - bail!("How did we call anything when we're halted?"); - }; - let Some(stack) = self.call_stacks.get_mut(code) else { - bail!("missing stack {code:04x}"); - }; - stack.push(StackFrame { address }); - } - SimEvent::Return => { - let Some(code) = self.context_stack.last() else { - bail!("how did we return when we're halted?"); - }; - let Some(stack) = self.call_stacks.get_mut(code) else { - bail!("missing stack {code:04x}"); - }; - if stack.pop().is_none() { - bail!("returned from {code:04x} but stack was empty"); - } - if stack.is_empty() { - bail!("returned to oblivion"); - } - } - SimEvent::Halt => { - let Some(RESET_CODE) = self.context_stack.pop() else { - bail!("halted when not in an interrupt"); - }; - } - SimEvent::Interrupt(code, address) => { - // if the CPU was halted before, wake it up now - if self.context_stack.is_empty() { - self.context_stack.push(RESET_CODE); - } + pub fn track_call(&mut self, address: u32) -> Result<()> { + let Some(code) = self.context_stack.last() else { + bail!("How did we call anything when we're halted?"); + }; + let Some(stack) = self.call_stacks.get_mut(code) else { + bail!("missing stack {code:04x}"); + }; + stack.push(StackFrame { address }); + Ok(()) + } - self.context_stack.push(code); - if self - .call_stacks - .insert(code, vec![StackFrame { address }]) - .is_some() - { - bail!("{code:04x} fired twice"); - } - } - SimEvent::Reti => { - let Some(code) = self.context_stack.pop() else { - bail!("RETI when halted"); - }; - if code == RESET_CODE { - bail!("RETI when not in interrupt"); - } - if self.call_stacks.remove(&code).is_none() { - bail!("{code:04x} popped but never called"); - } - } + pub fn track_return(&mut self) -> Result<()> { + let Some(code) = self.context_stack.last() else { + bail!("how did we return when we're halted?"); + }; + let Some(stack) = self.call_stacks.get_mut(code) else { + bail!("missing stack {code:04x}"); + }; + if stack.pop().is_none() { + bail!("returned from {code:04x} but stack was empty"); + } + if stack.is_empty() { + bail!("returned to oblivion"); + } + Ok(()) + } + + pub fn track_halt(&mut self) -> Result<()> { + let Some(RESET_CODE) = self.context_stack.pop() else { + bail!("halted when not in an interrupt"); + }; + Ok(()) + } + + pub fn track_interrupt(&mut self, code: u16, address: u32) -> Result<()> { + // if the CPU was halted before, wake it up now + if self.context_stack.is_empty() { + self.context_stack.push(RESET_CODE); + } + + self.context_stack.push(code); + if self + .call_stacks + .insert(code, vec![StackFrame { address }]) + .is_some() + { + bail!("{code:04x} fired twice"); + } + Ok(()) + } + + pub fn track_reti(&mut self) -> Result<()> { + let Some(code) = self.context_stack.pop() else { + bail!("RETI when halted"); + }; + if code == RESET_CODE { + bail!("RETI when not in interrupt"); + } + if self.call_stacks.remove(&code).is_none() { + bail!("{code:04x} popped but never called"); } Ok(()) } From 2936960cc95959225597fcd739576c7cc0879da7 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Tue, 19 Aug 2025 22:08:46 -0400 Subject: [PATCH 09/15] Restart recording when restarting game --- src/profiler.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/profiler.rs b/src/profiler.rs index d818b0d..1d9359d 100644 --- a/src/profiler.rs +++ b/src/profiler.rs @@ -200,8 +200,14 @@ impl ProfilerSession { } async fn start_profiling(&mut self, file_path: PathBuf) -> Result<()> { - self.program = Some(ProgramState::new(file_path).await?); - self.recording = None; + let program = ProgramState::new(file_path).await?; + let recording = if self.recording.is_some() { + Some(Recording::new(&program)) + } else { + None + }; + self.program = Some(program); + self.recording = recording; Ok(()) } From 4785e5e11be3b9093b3e2dd307ffc9d6a884124d Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Tue, 19 Aug 2025 22:37:06 -0400 Subject: [PATCH 10/15] Profile without ELF data --- src/profiler.rs | 12 +++++------- src/profiler/recording.rs | 14 +++++++------- src/profiler/state.rs | 29 ++++++++++++++++++++++------- 3 files changed, 34 insertions(+), 21 deletions(-) diff --git a/src/profiler.rs b/src/profiler.rs index 1d9359d..9705e8e 100644 --- a/src/profiler.rs +++ b/src/profiler.rs @@ -134,13 +134,13 @@ async fn handle_event(event: ProfileEvent, session: &mut ProfilerSession) -> Res match event { ProfileEvent::Start { file_path } => session.start_profiling(file_path).await, ProfileEvent::Update { cycles, event } => { - session.track_elapsed_cycles(cycles)?; + session.track_elapsed_cycles(cycles); if let Some(event) = event { session.track_event(event)?; } - Ok(()) } } + Ok(()) } fn handle_action( @@ -199,8 +199,8 @@ impl ProfilerSession { } } - async fn start_profiling(&mut self, file_path: PathBuf) -> Result<()> { - let program = ProgramState::new(file_path).await?; + async fn start_profiling(&mut self, file_path: PathBuf) { + let program = ProgramState::new(file_path).await; let recording = if self.recording.is_some() { Some(Recording::new(&program)) } else { @@ -208,14 +208,12 @@ impl ProfilerSession { }; self.program = Some(program); self.recording = recording; - Ok(()) } - fn track_elapsed_cycles(&mut self, cycles: u32) -> Result<()> { + fn track_elapsed_cycles(&mut self, cycles: u32) { if let (Some(state), Some(recording)) = (&self.program, &mut self.recording) { recording.track_elapsed_cycles(state, cycles); } - Ok(()) } fn track_event(&mut self, event: SimEvent) -> Result<()> { diff --git a/src/profiler/recording.rs b/src/profiler/recording.rs index 0b1b778..2513330 100644 --- a/src/profiler/recording.rs +++ b/src/profiler/recording.rs @@ -17,17 +17,17 @@ pub struct Recording { impl Recording { pub fn new(state: &ProgramState) -> Self { - let symbol_file = state.symbol_file(); - - let name = &symbol_file.name(); let reference_timestamp = ReferenceTimestamp::from_millis_since_unix_epoch(0.0); let interval = SamplingInterval::from_hz(20_000_000.0); - let mut profile = Profile::new(name, reference_timestamp, interval); + let mut profile = Profile::new(state.name(), reference_timestamp, interval); - let process = profile.add_process(name, 1, Timestamp::from_nanos_since_reference(0)); + let process = + profile.add_process(state.name(), 1, Timestamp::from_nanos_since_reference(0)); - let lib = profile.add_lib(symbol_file.library_info().clone()); - profile.add_lib_mapping(process, lib, 0x00000000, 0xffffffff, 0); + if let Some(symbol_file) = state.symbol_file() { + let lib = profile.add_lib(symbol_file.library_info().clone()); + profile.add_lib_mapping(process, lib, 0x00000000, 0xffffffff, 0); + } let mut me = Self { profile, diff --git a/src/profiler/state.rs b/src/profiler/state.rs index d2da976..5eb39e2 100644 --- a/src/profiler/state.rs +++ b/src/profiler/state.rs @@ -5,7 +5,8 @@ use anyhow::{Result, bail}; use crate::profiler::symbols::SymbolFile; pub struct ProgramState { - symbol_file: SymbolFile, + name: String, + symbol_file: Option, call_stacks: HashMap>, context_stack: Vec, } @@ -16,8 +17,17 @@ pub struct StackFrame { pub const RESET_CODE: u16 = 0xfff0; impl ProgramState { - pub async fn new(file_path: PathBuf) -> Result { - let symbol_file = SymbolFile::load(&file_path).await?; + pub async fn new(file_path: PathBuf) -> Self { + let symbol_file = SymbolFile::load(&file_path).await.ok(); + let name = symbol_file + .as_ref() + .map(|f| f.name().to_string()) + .or_else(|| { + file_path + .file_stem() + .map(|s| s.to_string_lossy().into_owned()) + }) + .unwrap_or_else(|| "game".to_string()); let mut call_stacks = HashMap::new(); call_stacks.insert( RESET_CODE, @@ -25,15 +35,20 @@ impl ProgramState { address: 0xfffffff0, }], ); - Ok(Self { + Self { + name, symbol_file, call_stacks, context_stack: vec![RESET_CODE], - }) + } } - pub fn symbol_file(&self) -> &SymbolFile { - &self.symbol_file + pub fn name(&self) -> &str { + &self.name + } + + pub fn symbol_file(&self) -> Option<&SymbolFile> { + self.symbol_file.as_ref() } pub fn current_stack(&self) -> Option<(u16, &[StackFrame])> { From e5abd48337da2e4696644ef9f3dc55a1e1f0fa88 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Wed, 27 Aug 2025 23:02:04 -0400 Subject: [PATCH 11/15] Update deps --- Cargo.lock | 181 ++++++++++++++++++++++++++++------------------------- 1 file changed, 97 insertions(+), 84 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 74bf0f1..daed724 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -84,9 +84,9 @@ dependencies = [ [[package]] name = "alsa" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" +checksum = "bdc00893e7a970727e9304671b2c88577b4cfe53dc64019fdfdf9683573a09c4" dependencies = [ "alsa-sys", "bitflags 2.9.3", @@ -198,9 +198,9 @@ checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" [[package]] name = "arboard" -version = "3.6.0" +version = "3.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55f533f8e0af236ffe5eb979b99381df3258853f00ba2e44b6e1955292c75227" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" dependencies = [ "clipboard-win", "image", @@ -212,7 +212,7 @@ dependencies = [ "objc2-foundation 0.3.1", "parking_lot", "percent-encoding", - "windows-sys 0.59.0", + "windows-sys 0.60.2", "x11rb", ] @@ -291,11 +291,13 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.27" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddb939d66e4ae03cee6091612804ba446b12878410cfa17f785f4dd67d4014e8" +checksum = "6448dfb3960f0b038e88c781ead1e7eb7929dfc3a71a1336ec9086c00f6d1e75" dependencies = [ "brotli", + "compression-codecs", + "compression-core", "flate2", "futures-core", "futures-io", @@ -306,9 +308,9 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.13.2" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb812ffb58524bdd10860d7d974e2f01cc0950c2438a74ee5ec2e2280c6c4ffa" +checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" dependencies = [ "async-task", "concurrent-queue", @@ -583,9 +585,9 @@ dependencies = [ [[package]] name = "brotli" -version = "8.0.1" +version = "8.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9991eea70ea4f293524138648e41ee89b0b2b12ddef3b255effa43c8056e0e0d" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -735,9 +737,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.45" +version = "4.5.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318" +checksum = "2c5e4fcf9c21d2e544ca1ee9d8552de13019a42aa7dbf32747fa7aaf1df76e57" dependencies = [ "clap_builder", "clap_derive", @@ -745,9 +747,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.44" +version = "4.5.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" +checksum = "fecb53a0e6fcfb055f686001bc2e2592fa527efaf38dbe81a6a9563562e57d41" dependencies = [ "anstream", "anstyle", @@ -809,6 +811,26 @@ dependencies = [ "memchr", ] +[[package]] +name = "compression-codecs" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46cc6539bf1c592cff488b9f253b30bc0ec50d15407c2cf45e27bd8f308d5905" +dependencies = [ + "brotli", + "compression-core", + "flate2", + "futures-core", + "memchr", + "pin-project-lite", +] + +[[package]] +name = "compression-core" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2957e823c15bde7ecf1e8b64e537aa03a6be5fda0e2334e99887669e75b12e01" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -1513,7 +1535,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25234f20a3ec0a962a61770cfe39ecf03cb529a6e474ad8cff025ed497eda557" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "debugid", "rustc-hash 2.1.1", "serde", @@ -1779,18 +1801,20 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", + "futures-core", "http", "http-body", "httparse", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -1831,7 +1855,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.0", + "socket2", "tokio", "tower-service", "tracing", @@ -2087,9 +2111,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "jobserver" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ "getrandom 0.3.3", "libc", @@ -2251,7 +2275,7 @@ dependencies = [ "memchr", "prost", "prost-derive", - "thiserror 2.0.12", + "thiserror 2.0.16", ] [[package]] @@ -2260,10 +2284,10 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa8fc7e83909ea3b9e2784591655637d3401f2f16014f9d8d6e23ccd138e665f" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "byteorder", "memchr", - "thiserror 2.0.12", + "thiserror 2.0.16", ] [[package]] @@ -2343,7 +2367,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb4bdc8b0ce69932332cf76d24af69c3a155242af95c226b2ab6c2e371ed1149" dependencies = [ - "thiserror 2.0.12", + "thiserror 2.0.16", "zerocopy", "zerocopy-derive", ] @@ -2462,7 +2486,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbeff6bd154a309b2ada5639b2661ca6ae4599b34e8487dc276d2cd637da2d76" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "itoa", ] @@ -3093,12 +3117,12 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2a2e496e34cd96a1bb26f681e5adb11c98f1e5378e294e60c06c0cf04c526ba" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "elsa", "maybe-owned", "pdb2", "range-collections", - "thiserror 2.0.12", + "thiserror 2.0.16", ] [[package]] @@ -3346,9 +3370,9 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ "bytes", "cfg_aliases", @@ -3357,8 +3381,8 @@ dependencies = [ "quinn-udp", "rustc-hash 2.1.1", "rustls", - "socket2 0.5.10", - "thiserror 2.0.12", + "socket2", + "thiserror 2.0.16", "tokio", "tracing", "web-time", @@ -3366,9 +3390,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.12" +version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ "bytes", "getrandom 0.3.3", @@ -3379,7 +3403,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.12", + "thiserror 2.0.16", "tinyvec", "tracing", "web-time", @@ -3387,16 +3411,16 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.13" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -3548,14 +3572,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.1" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", + "regex-automata 0.4.10", + "regex-syntax 0.8.6", ] [[package]] @@ -3569,13 +3593,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax 0.8.6", ] [[package]] @@ -3586,9 +3610,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" [[package]] name = "renderdoc-sys" @@ -3598,9 +3622,9 @@ checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" [[package]] name = "reqwest" -version = "0.12.22" +version = "0.12.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" dependencies = [ "async-compression", "base64", @@ -3824,7 +3848,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97e73d38bb04a373dba1260af91d4b0010e84cecd92d20b8e9949a910d5b9cbb" dependencies = [ "addr2line", - "bitflags 2.9.1", + "bitflags 2.9.3", "cpp_demangle", "crc32fast", "debugid", @@ -3843,7 +3867,7 @@ dependencies = [ "rustc-demangle", "scala-native-demangle", "srcsrv", - "thiserror 2.0.12", + "thiserror 2.0.16", "uuid", "yoke", "yoke-derive", @@ -4054,16 +4078,6 @@ dependencies = [ "serde", ] -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "socket2" version = "0.6.0" @@ -4090,7 +4104,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85cd3e3828fb4dd5ba0e7091777edb6c3db3cd2d6fc10547b29b40f6949a29be" dependencies = [ "memchr", - "thiserror 2.0.12", + "thiserror 2.0.16", ] [[package]] @@ -4165,7 +4179,7 @@ dependencies = [ "http", "reqwest", "scopeguard", - "thiserror 2.0.12", + "thiserror 2.0.16", "tokio", ] @@ -4352,9 +4366,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] @@ -4378,7 +4392,7 @@ dependencies = [ "mio", "pin-project-lite", "slab", - "socket2 0.6.0", + "socket2", "tokio-macros", "windows-sys 0.59.0", ] @@ -4479,7 +4493,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "bytes", "futures-util", "http", @@ -4654,9 +4668,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.6" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "137a3c834eaf7139b73688502f3f1141a0337c5d8e4d9b536f9b8c796e26a7c4" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", @@ -5156,7 +5170,7 @@ dependencies = [ "samply-symbols", "scopeguard", "symsrv", - "thiserror 2.0.12", + "thiserror 2.0.16", "tokio", "uuid", "yoke", @@ -5877,9 +5891,9 @@ dependencies = [ [[package]] name = "zbus" -version = "5.9.0" +version = "5.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb4f9a464286d42851d18a605f7193b8febaf5b0919d71c6399b7b26e5b0aad" +checksum = "67a073be99ace1adc48af593701c8015cd9817df372e14a1a6b0ee8f8bf043be" dependencies = [ "async-broadcast", "async-executor", @@ -5901,7 +5915,7 @@ dependencies = [ "serde_repr", "tracing", "uds_windows", - "windows-sys 0.59.0", + "windows-sys 0.60.2", "winnow", "zbus_macros", "zbus_names", @@ -5910,9 +5924,9 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.9.0" +version = "5.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef9859f68ee0c4ee2e8cde84737c78e3f4c54f946f2a38645d0d4c7a95327659" +checksum = "0e80cd713a45a49859dcb648053f63265f4f2851b6420d47a958e5697c68b131" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -6023,9 +6037,9 @@ checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a" [[package]] name = "zvariant" -version = "5.6.0" +version = "5.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91b3680bb339216abd84714172b5138a4edac677e641ef17e1d8cb1b3ca6e6f" +checksum = "999dd3be73c52b1fccd109a4a81e4fcd20fab1d3599c8121b38d04e1419498db" dependencies = [ "endi", "enumflags2", @@ -6038,9 +6052,9 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "5.6.0" +version = "5.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a8c68501be459a8dbfffbe5d792acdd23b4959940fc87785fb013b32edbc208" +checksum = "6643fd0b26a46d226bd90d3f07c1b5321fe9bb7f04673cb37ac6d6883885b68e" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -6051,14 +6065,13 @@ dependencies = [ [[package]] name = "zvariant_utils" -version = "3.2.0" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16edfee43e5d7b553b77872d99bc36afdda75c223ca7ad5e3fbecd82ca5fc34" +checksum = "c6949d142f89f6916deca2232cf26a8afacf2b9fdc35ce766105e104478be599" dependencies = [ "proc-macro2", "quote", "serde", - "static_assertions", "syn", "winnow", ] From 04c92c1454b38f2a62747e66b1e8ad2f80deb47d Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sat, 30 Aug 2025 13:02:38 -0400 Subject: [PATCH 12/15] Parse ELF exclusively on emulation thread --- Cargo.lock | 42 ++++++++++++++------ Cargo.toml | 2 +- src/emulator.rs | 37 +++++++++-------- src/emulator/cart.rs | 43 +++++++++++++++----- src/emulator/game_info.rs | 83 +++++++++++++++++++++++++++++++++++++++ src/profiler.rs | 10 ++--- src/profiler/recording.rs | 6 +-- src/profiler/state.rs | 29 +++++--------- src/profiler/symbols.rs | 67 ------------------------------- 9 files changed, 182 insertions(+), 137 deletions(-) create mode 100644 src/emulator/game_info.rs delete mode 100644 src/profiler/symbols.rs diff --git a/Cargo.lock b/Cargo.lock index daed724..fb7bbe3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -481,7 +481,7 @@ dependencies = [ "cfg-if", "libc", "miniz_oxide", - "object", + "object 0.36.7", "rustc-demangle", "windows-targets 0.52.6", ] @@ -1201,12 +1201,6 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" -[[package]] -name = "elf" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55dd888a213fc57e957abf2aa305ee3e8a28dbe05687a251f33b637cd46b0070" - [[package]] name = "elsa" version = "1.11.2" @@ -2176,7 +2170,6 @@ dependencies = [ "egui-wgpu", "egui-winit", "egui_extras", - "elf", "fixed", "fxprof-processed-profile", "gilrs", @@ -2186,6 +2179,7 @@ dependencies = [ "normpath", "num-derive", "num-traits", + "object 0.37.3", "oneshot", "pollster", "rand 0.9.2", @@ -2983,7 +2977,18 @@ checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "flate2", "memchr", - "ruzstd", + "ruzstd 0.7.3", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "flate2", + "memchr", + "ruzstd 0.8.1", ] [[package]] @@ -3823,7 +3828,16 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fad02996bfc73da3e301efe90b1837be9ed8f4a462b6ed410aa35d00381de89f" dependencies = [ - "twox-hash", + "twox-hash 1.6.3", +] + +[[package]] +name = "ruzstd" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640bec8aad418d7d03c72ea2de10d5c646a598f9883c7babc160d91e3c1b26c" +dependencies = [ + "twox-hash 2.1.1", ] [[package]] @@ -3861,7 +3875,7 @@ dependencies = [ "memchr", "msvc-demangler", "nom", - "object", + "object 0.36.7", "pdb-addr2line", "rangemap", "rustc-demangle", @@ -4610,6 +4624,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "twox-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b907da542cbced5261bd3256de1b3a1bf340a3d37f93425a07362a1d687de56" + [[package]] name = "type-map" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index 0fb4fa6..449e0ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,6 @@ egui_extras = { version = "0.32", features = ["image"] } egui-notify = "0.20" egui-winit = "0.32" egui-wgpu = { version = "0.32", features = ["winit"] } -elf = "0.8" fxprof-processed-profile = "0.8" fixed = { version = "1.28", features = ["num-traits"] } gilrs = { version = "0.11", features = ["serde-serialize"] } @@ -31,6 +30,7 @@ itertools = "0.14" normpath = "1" num-derive = "0.4" num-traits = "0.2" +object = "0.37" oneshot = "0.1" pollster = "0.4" rand = "0.9" diff --git a/src/emulator.rs b/src/emulator.rs index 0ba327f..2c31bde 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -22,11 +22,13 @@ use crate::{ graphics::TextureSink, memory::{MemoryRange, MemoryRegion}, }; +pub use game_info::GameInfo; use shrooms_vb_core::{EXPECTED_FRAME_SIZE, Sim, StopReason}; pub use shrooms_vb_core::{SimEvent, VBKey, VBRegister, VBWatchpointType}; mod address_set; mod cart; +mod game_info; mod shrooms_vb_core; #[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)] @@ -221,18 +223,16 @@ impl Emulator { self.sim_state[index].store(SimState::Ready, Ordering::Release); } let mut profiling = false; - if let Some(profiler) = self.profilers[sim_id.to_index()].as_ref() { - if let Some(cart) = self.carts[index].as_ref() { - if profiler - .send(ProfileEvent::Start { - file_path: cart.file_path.clone(), - }) - .is_ok() - { - sim.monitor_events(true); - profiling = true; - } - } + if let Some(profiler) = self.profilers[sim_id.to_index()].as_ref() + && let Some(cart) = self.carts[index].as_ref() + && profiler + .send(ProfileEvent::Start { + info: cart.info.clone(), + }) + .is_ok() + { + sim.monitor_events(true); + profiling = true; } if !profiling { sim.monitor_events(false); @@ -476,16 +476,15 @@ impl Emulator { if !running { continue; } - if let Some(p) = profiler { - if p.send(ProfileEvent::Update { + if let Some(p) = profiler + && p.send(ProfileEvent::Update { cycles, event: sim.take_sim_event(), }) .is_err() - { - sim.monitor_events(false); - *profiler = None; - } + { + sim.monitor_events(false); + *profiler = None; } } @@ -838,7 +837,7 @@ pub enum DebugEvent { pub enum ProfileEvent { Start { - file_path: PathBuf, + info: Arc, }, Update { cycles: u32, diff --git a/src/emulator/cart.rs b/src/emulator/cart.rs index 8aec193..efc7730 100644 --- a/src/emulator/cart.rs +++ b/src/emulator/cart.rs @@ -4,21 +4,24 @@ use std::{ fs::{self, File}, io::{Read, Seek as _, SeekFrom, Write as _}, path::{Path, PathBuf}, + sync::Arc, }; -use crate::emulator::SimId; +use crate::emulator::{SimId, game_info::GameInfo}; pub struct Cart { pub file_path: PathBuf, pub rom: Vec, sram_file: File, pub sram: Vec, + pub info: Arc, } impl Cart { pub fn load(file_path: &Path, sim_id: SimId) -> Result { let rom = fs::read(file_path)?; - let rom = try_parse_elf(&rom).unwrap_or(rom); + let (rom, info) = + try_parse_elf(file_path, &rom).unwrap_or_else(|| (rom, GameInfo::empty(file_path))); let mut sram_file = File::options() .read(true) @@ -46,6 +49,7 @@ impl Cart { rom, sram_file, sram, + info: Arc::new(info), }) } @@ -56,18 +60,39 @@ impl Cart { } } -fn try_parse_elf(data: &[u8]) -> Option> { - let parsed = elf::ElfBytes::::minimal_parse(data).ok()?; +fn try_parse_elf(file_path: &Path, data: &[u8]) -> Option<(Vec, GameInfo)> { + use object::read::elf::FileHeader; + let program = match object::FileKind::parse(data).ok()? { + object::FileKind::Elf32 => { + let header = object::elf::FileHeader32::parse(data).ok()?; + parse_elf_program(header, data)? + } + object::FileKind::Elf64 => { + let header = object::elf::FileHeader64::parse(data).ok()?; + parse_elf_program(header, data)? + } + _ => return None, + }; + let info = GameInfo::new(file_path, data).unwrap_or_else(|_| GameInfo::empty(file_path)); + Some((program, info)) +} + +fn parse_elf_program>( + header: &Elf, + data: &[u8], +) -> Option> { + use object::read::elf::ProgramHeader; + let endian = header.endian().ok()?; let mut bytes = vec![]; let mut pstart = None; - for phdr in parsed.segments()? { - if phdr.p_filesz == 0 { + for phdr in header.program_headers(endian, data).ok()? { + if phdr.p_filesz(endian).into() == 0 { continue; } - let start = pstart.unwrap_or(phdr.p_paddr); + let start = pstart.unwrap_or(phdr.p_paddr(endian).into()); pstart = Some(start); - bytes.resize((phdr.p_paddr - start) as usize, 0); - let data = parsed.segment_data(&phdr).ok()?; + bytes.resize((phdr.p_paddr(endian).into() - start) as usize, 0); + let data = phdr.data(endian, data).ok()?; bytes.extend_from_slice(data); } Some(bytes) diff --git a/src/emulator/game_info.rs b/src/emulator/game_info.rs new file mode 100644 index 0000000..0ff679f --- /dev/null +++ b/src/emulator/game_info.rs @@ -0,0 +1,83 @@ +use std::{path::Path, sync::Arc}; + +use anyhow::Result; +use fxprof_processed_profile::{LibraryInfo, Symbol, SymbolTable, debugid::DebugId}; +use object::{Object, ObjectSymbol}; +use wholesym::samply_symbols::{DebugIdExt, demangle_any}; + +#[derive(Debug)] +pub struct GameInfo { + library_info: LibraryInfo, +} + +impl GameInfo { + pub fn new(file_path: &Path, input: &[u8]) -> Result { + let file = object::File::parse(input)?; + + let (name, path) = name_and_path(file_path); + let debug_id = file + .build_id()? + .map(|id| DebugId::from_identifier(id, true)) + .unwrap_or_default(); + let code_id = file.build_id()?.map(hex::encode); + let mut symbols = vec![]; + for sym in file.symbols() { + symbols.push(Symbol { + address: sym.address() as u32, + size: Some(sym.size() as u32), + name: demangle_any(sym.name()?), + }); + } + + let library_info = LibraryInfo { + name: name.clone(), + debug_name: name, + path: path.clone(), + debug_path: path, + debug_id, + code_id, + arch: None, + symbol_table: Some(Arc::new(SymbolTable::new(symbols))), + }; + + Ok(Self { library_info }) + } + + pub fn empty(file_path: &Path) -> Self { + let (name, path) = name_and_path(file_path); + let library_info = LibraryInfo { + name: name.clone(), + debug_name: name, + path: path.clone(), + debug_path: path, + debug_id: DebugId::default(), + code_id: None, + arch: None, + symbol_table: None, + }; + Self { library_info } + } + + pub fn name(&self) -> &str { + &self.library_info.name + } + + pub fn library_info(&self) -> &LibraryInfo { + &self.library_info + } +} + +fn name_and_path(file_path: &Path) -> (String, String) { + let normalized = normpath::PathExt::normalize(file_path); + let path = normalized + .as_ref() + .map(|n| n.as_path()) + .unwrap_or(file_path); + + let name = match path.file_stem() { + Some(s) => s.to_string_lossy().into_owned(), + None => "game".to_string(), + }; + let path = path.to_string_lossy().into_owned(); + (name, path) +} diff --git a/src/profiler.rs b/src/profiler.rs index 9705e8e..fe16404 100644 --- a/src/profiler.rs +++ b/src/profiler.rs @@ -1,5 +1,4 @@ use std::{ - path::PathBuf, sync::{Arc, Mutex}, thread, }; @@ -7,13 +6,12 @@ use std::{ use anyhow::Result; use tokio::{select, sync::mpsc}; -use crate::emulator::{EmulatorClient, EmulatorCommand, ProfileEvent, SimEvent, SimId}; +use crate::emulator::{EmulatorClient, EmulatorCommand, GameInfo, ProfileEvent, SimEvent, SimId}; use recording::Recording; use state::ProgramState; mod recording; mod state; -mod symbols; pub struct Profiler { sim_id: SimId, @@ -132,7 +130,7 @@ async fn run_profile( async fn handle_event(event: ProfileEvent, session: &mut ProfilerSession) -> Result<()> { match event { - ProfileEvent::Start { file_path } => session.start_profiling(file_path).await, + ProfileEvent::Start { info } => session.start_profiling(info).await, ProfileEvent::Update { cycles, event } => { session.track_elapsed_cycles(cycles); if let Some(event) = event { @@ -199,8 +197,8 @@ impl ProfilerSession { } } - async fn start_profiling(&mut self, file_path: PathBuf) { - let program = ProgramState::new(file_path).await; + async fn start_profiling(&mut self, info: Arc) { + let program = ProgramState::new(info).await; let recording = if self.recording.is_some() { Some(Recording::new(&program)) } else { diff --git a/src/profiler/recording.rs b/src/profiler/recording.rs index 2513330..0723be9 100644 --- a/src/profiler/recording.rs +++ b/src/profiler/recording.rs @@ -24,10 +24,8 @@ impl Recording { let process = profile.add_process(state.name(), 1, Timestamp::from_nanos_since_reference(0)); - if let Some(symbol_file) = state.symbol_file() { - let lib = profile.add_lib(symbol_file.library_info().clone()); - profile.add_lib_mapping(process, lib, 0x00000000, 0xffffffff, 0); - } + let lib = profile.add_lib(state.library_info().clone()); + profile.add_lib_mapping(process, lib, 0x00000000, 0xffffffff, 0); let mut me = Self { profile, diff --git a/src/profiler/state.rs b/src/profiler/state.rs index 5eb39e2..ed7e9e8 100644 --- a/src/profiler/state.rs +++ b/src/profiler/state.rs @@ -1,12 +1,12 @@ -use std::{collections::HashMap, path::PathBuf}; +use std::{collections::HashMap, sync::Arc}; use anyhow::{Result, bail}; +use fxprof_processed_profile::LibraryInfo; -use crate::profiler::symbols::SymbolFile; +use crate::emulator::GameInfo; pub struct ProgramState { - name: String, - symbol_file: Option, + info: Arc, call_stacks: HashMap>, context_stack: Vec, } @@ -17,17 +17,7 @@ pub struct StackFrame { pub const RESET_CODE: u16 = 0xfff0; impl ProgramState { - pub async fn new(file_path: PathBuf) -> Self { - let symbol_file = SymbolFile::load(&file_path).await.ok(); - let name = symbol_file - .as_ref() - .map(|f| f.name().to_string()) - .or_else(|| { - file_path - .file_stem() - .map(|s| s.to_string_lossy().into_owned()) - }) - .unwrap_or_else(|| "game".to_string()); + pub async fn new(info: Arc) -> Self { let mut call_stacks = HashMap::new(); call_stacks.insert( RESET_CODE, @@ -36,19 +26,18 @@ impl ProgramState { }], ); Self { - name, - symbol_file, + info, call_stacks, context_stack: vec![RESET_CODE], } } pub fn name(&self) -> &str { - &self.name + self.info.name() } - pub fn symbol_file(&self) -> Option<&SymbolFile> { - self.symbol_file.as_ref() + pub fn library_info(&self) -> &LibraryInfo { + self.info.library_info() } pub fn current_stack(&self) -> Option<(u16, &[StackFrame])> { diff --git a/src/profiler/symbols.rs b/src/profiler/symbols.rs deleted file mode 100644 index fd84211..0000000 --- a/src/profiler/symbols.rs +++ /dev/null @@ -1,67 +0,0 @@ -use std::{path::Path, sync::Arc}; - -use anyhow::Result; -use fxprof_processed_profile::{LibraryInfo, Symbol, SymbolTable}; -use wholesym::{SymbolManager, samply_symbols::demangle_any}; - -pub struct SymbolFile { - library_info: LibraryInfo, -} - -impl SymbolFile { - pub async fn load(file_path: &Path) -> Result { - let normalized = normpath::PathExt::normalize(file_path)?; - let library_info = - SymbolManager::library_info_for_binary_at_path(normalized.as_path(), None).await?; - - let symbol_manager = SymbolManager::with_config(Default::default()); - let symbol_map = symbol_manager - .load_symbol_map_for_binary_at_path(normalized.as_path(), None) - .await?; - - let name = library_info - .name - .or_else(|| { - normalized - .file_name() - .map(|n| n.to_string_lossy().into_owned()) - }) - .unwrap_or("game".to_string()); - let debug_name = library_info.debug_name.unwrap_or_else(|| name.clone()); - let path = library_info - .path - .unwrap_or_else(|| normalized.into_os_string().to_string_lossy().into_owned()); - let debug_path = library_info.debug_path.unwrap_or_else(|| path.clone()); - let debug_id = library_info.debug_id.unwrap_or_default(); - let code_id = library_info.code_id.map(|id| id.to_string()); - let arch = library_info.arch; - let symbols = symbol_map - .iter_symbols() - .map(|(address, name)| Symbol { - address: address + 0x07000000, - size: None, - name: demangle_any(&name), - }) - .collect(); - Ok(Self { - library_info: LibraryInfo { - name, - debug_name, - path, - debug_path, - debug_id, - code_id, - arch, - symbol_table: Some(Arc::new(SymbolTable::new(symbols))), - }, - }) - } - - pub fn name(&self) -> &str { - &self.library_info.name - } - - pub fn library_info(&self) -> &LibraryInfo { - &self.library_info - } -} From 841ded3bee62c337cc4782f85cb80b4fe721c12c Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sun, 31 Aug 2025 23:01:50 -0400 Subject: [PATCH 13/15] Include inline functions when profiling --- Cargo.lock | 16 +++- Cargo.toml | 1 + src/emulator.rs | 25 +++--- src/emulator/game_info.rs | 128 +++++++++++++++++++++++++++++-- src/emulator/inline_stack_map.rs | 87 +++++++++++++++++++++ src/emulator/shrooms_vb_core.rs | 61 +++++++++++---- src/profiler.rs | 19 ++++- src/profiler/recording.rs | 23 ++++-- src/profiler/state.rs | 34 +++++--- 9 files changed, 342 insertions(+), 52 deletions(-) create mode 100644 src/emulator/inline_stack_map.rs diff --git a/Cargo.lock b/Cargo.lock index fb7bbe3..f5fd8c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -35,7 +35,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "fallible-iterator", - "gimli", + "gimli 0.31.1", ] [[package]] @@ -1620,6 +1620,17 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "gimli" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6298e594375a7fead9efd5568f0a46e6a154fb6a9bdcbe3c06946ffd81a5f6" +dependencies = [ + "fallible-iterator", + "indexmap", + "stable_deref_trait", +] + [[package]] name = "gl_generator" version = "0.14.0" @@ -2173,6 +2184,7 @@ dependencies = [ "fixed", "fxprof-processed-profile", "gilrs", + "gimli 0.32.2", "hex", "image", "itertools 0.14.0", @@ -3868,7 +3880,7 @@ dependencies = [ "debugid", "elsa", "flate2", - "gimli", + "gimli 0.31.1", "linux-perf-data", "lzma-rs", "macho-unwind-info", diff --git a/Cargo.toml b/Cargo.toml index 449e0ad..b9bdac6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ egui-wgpu = { version = "0.32", features = ["winit"] } fxprof-processed-profile = "0.8" fixed = { version = "1.28", features = ["num-traits"] } gilrs = { version = "0.11", features = ["serde-serialize"] } +gimli = "0.32" hex = "0.4" image = { version = "0.25", default-features = false, features = ["png"] } itertools = "0.14" diff --git a/src/emulator.rs b/src/emulator.rs index 2c31bde..5c2c91c 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -18,17 +18,20 @@ use tracing::{error, warn}; use crate::{ audio::Audio, - emulator::cart::Cart, graphics::TextureSink, memory::{MemoryRange, MemoryRegion}, }; +use cart::Cart; pub use game_info::GameInfo; +pub use inline_stack_map::InlineStack; +use inline_stack_map::InlineStackMap; use shrooms_vb_core::{EXPECTED_FRAME_SIZE, Sim, StopReason}; pub use shrooms_vb_core::{SimEvent, VBKey, VBRegister, VBWatchpointType}; mod address_set; mod cart; mod game_info; +mod inline_stack_map; mod shrooms_vb_core; #[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)] @@ -231,11 +234,11 @@ impl Emulator { }) .is_ok() { - sim.monitor_events(true); + sim.monitor_events(true, cart.info.inline_stack_map().clone()); profiling = true; } if !profiling { - sim.monitor_events(false); + sim.monitor_events(false, InlineStackMap::empty()); } if self.sim_state[index].load(Ordering::Acquire) == SimState::Ready { @@ -476,15 +479,18 @@ impl Emulator { if !running { continue; } - if let Some(p) = profiler - && p.send(ProfileEvent::Update { + if let Some(p) = profiler { + let (event, inline_stack) = sim.take_profiler_updates(); + if p.send(ProfileEvent::Update { cycles, - event: sim.take_sim_event(), + event, + inline_stack, }) .is_err() - { - sim.monitor_events(false); - *profiler = None; + { + sim.monitor_events(false, InlineStackMap::empty()); + *profiler = None; + } } } @@ -842,6 +848,7 @@ pub enum ProfileEvent { Update { cycles: u32, event: Option, + inline_stack: Option, }, } diff --git a/src/emulator/game_info.rs b/src/emulator/game_info.rs index 0ff679f..9f72e7c 100644 --- a/src/emulator/game_info.rs +++ b/src/emulator/game_info.rs @@ -1,13 +1,16 @@ -use std::{path::Path, sync::Arc}; +use std::{borrow::Cow, path::Path, sync::Arc}; -use anyhow::Result; +use anyhow::{Result, bail}; use fxprof_processed_profile::{LibraryInfo, Symbol, SymbolTable, debugid::DebugId}; -use object::{Object, ObjectSymbol}; +use object::{Object, ObjectSection, ObjectSymbol}; use wholesym::samply_symbols::{DebugIdExt, demangle_any}; +use crate::emulator::inline_stack_map::{InlineStackMap, InlineStackMapBuilder}; + #[derive(Debug)] pub struct GameInfo { library_info: LibraryInfo, + inline_stack_map: InlineStackMap, } impl GameInfo { @@ -29,6 +32,9 @@ impl GameInfo { }); } + let inline_stack_map = + build_inline_stack_map(file).unwrap_or_else(|_| InlineStackMap::empty()); + let library_info = LibraryInfo { name: name.clone(), debug_name: name, @@ -40,7 +46,10 @@ impl GameInfo { symbol_table: Some(Arc::new(SymbolTable::new(symbols))), }; - Ok(Self { library_info }) + Ok(Self { + library_info, + inline_stack_map, + }) } pub fn empty(file_path: &Path) -> Self { @@ -55,7 +64,11 @@ impl GameInfo { arch: None, symbol_table: None, }; - Self { library_info } + let inline_stack_map = InlineStackMap::empty(); + Self { + library_info, + inline_stack_map, + } } pub fn name(&self) -> &str { @@ -65,6 +78,111 @@ impl GameInfo { pub fn library_info(&self) -> &LibraryInfo { &self.library_info } + + pub fn inline_stack_map(&self) -> &InlineStackMap { + &self.inline_stack_map + } +} + +fn build_inline_stack_map(file: object::File) -> Result { + let endian = if file.is_little_endian() { + gimli::RunTimeEndian::Little + } else { + gimli::RunTimeEndian::Big + }; + fn load_section<'a>(file: &'a object::File, id: gimli::SectionId) -> Result> { + let input = match file.section_by_name(id.name()) { + Some(section) => section.uncompressed_data()?, + None => Cow::Owned(vec![]), + }; + Ok(input) + } + let dorf = gimli::DwarfSections::load(|id| load_section(&file, id))?; + let dorf = dorf.borrow(|sec| gimli::EndianSlice::new(sec, endian)); + let mut units = dorf.units(); + let mut frames = InlineStackMap::builder(); + while let Some(header) = units.next()? { + let unit = dorf.unit(header)?; + let mut entree = unit.entries_tree(None)?; + let root = entree.root()?; + let mut ctx = ParseContext { + dorf: &dorf, + unit: &unit, + frames: &mut frames, + }; + parse_inline(&mut ctx, root)?; + } + Ok(frames.build()) +} + +type Reader<'a> = gimli::EndianSlice<'a, gimli::RunTimeEndian>; + +struct ParseContext<'a> { + dorf: &'a gimli::Dwarf>, + unit: &'a gimli::Unit>, + frames: &'a mut InlineStackMapBuilder, +} +impl ParseContext<'_> { + fn name_attr(&self, attr: gimli::AttributeValue) -> Result> { + match attr { + gimli::AttributeValue::DebugInfoRef(offset) => { + let mut units = self.dorf.units(); + while let Some(header) = units.next()? { + if let Some(offset) = offset.to_unit_offset(&header) { + let unit = self.dorf.unit(header)?; + return self.name_entry(&unit, offset); + } + } + Ok(None) + } + gimli::AttributeValue::UnitRef(offset) => self.name_entry(self.unit, offset), + other => { + bail!("unrecognized attr {other:?}"); + } + } + } + + fn name_entry( + &self, + unit: &gimli::Unit, + offset: gimli::UnitOffset, + ) -> Result> { + let abbreviations = self.dorf.abbreviations(&unit.header)?; + let mut entries = unit.header.entries_raw(&abbreviations, Some(offset))?; + let Some(abbrev) = entries.read_abbreviation()? else { + return Ok(None); + }; + let mut name = None; + for spec in abbrev.attributes() { + let attr = entries.read_attribute(*spec)?; + if attr.name() == gimli::DW_AT_linkage_name + || (attr.name() == gimli::DW_AT_name && name.is_none()) + { + name = Some(self.dorf.attr_string(unit, attr.value())?) + } + } + Ok(name.map(|n| demangle_any(&String::from_utf8_lossy(&n)))) + } +} + +fn parse_inline(ctx: &mut ParseContext, node: gimli::EntriesTreeNode) -> Result<()> { + if node.entry().tag() == gimli::DW_TAG_inlined_subroutine + && let Some(attr) = node.entry().attr_value(gimli::DW_AT_abstract_origin)? + && let Some(name) = ctx.name_attr(attr)? + { + let name = Arc::new(name); + let mut ranges = ctx.dorf.die_ranges(ctx.unit, node.entry())?; + while let Some(range) = ranges.next()? { + let start = range.begin as u32; + let end = range.end as u32; + ctx.frames.add(start, end, name.clone()); + } + } + let mut children = node.children(); + while let Some(child) = children.next()? { + parse_inline(ctx, child)?; + } + Ok(()) } fn name_and_path(file_path: &Path) -> (String, String) { diff --git a/src/emulator/inline_stack_map.rs b/src/emulator/inline_stack_map.rs new file mode 100644 index 0000000..ca1f50c --- /dev/null +++ b/src/emulator/inline_stack_map.rs @@ -0,0 +1,87 @@ +use std::{ + collections::{BTreeMap, HashMap}, + sync::Arc, +}; + +pub type InlineStack = Arc>>; + +#[derive(Debug, Clone)] +pub struct InlineStackMap { + entries: Vec<(u32, InlineStack)>, + empty: InlineStack, +} + +impl InlineStackMap { + pub fn empty() -> Self { + Self { + entries: vec![], + empty: Arc::new(vec![]), + } + } + pub fn builder() -> InlineStackMapBuilder { + InlineStackMapBuilder { + events: BTreeMap::new(), + } + } + pub fn get(&self, address: u32) -> &InlineStack { + match self.entries.binary_search_by_key(&address, |(a, _)| *a) { + Ok(index) => self.entries.get(index), + Err(after) => after.checked_sub(1).and_then(|i| self.entries.get(i)), + } + .map(|(_, s)| s) + .unwrap_or(&self.empty) + } +} + +#[derive(Default)] +struct Event { + end: usize, + start: Vec>, +} + +pub struct InlineStackMapBuilder { + events: BTreeMap, +} + +impl InlineStackMapBuilder { + pub fn add(&mut self, start: u32, end: u32, name: Arc) { + self.events.entry(start).or_default().start.push(name); + self.events.entry(end).or_default().end += 1; + } + + pub fn build(self) -> InlineStackMap { + let empty = Arc::new(vec![]); + let mut entries = vec![]; + let mut stack_indexes = vec![]; + let mut stack = vec![]; + let mut string_dedup = HashMap::new(); + let mut stack_dedup = BTreeMap::new(); + stack_dedup.insert(vec![], empty.clone()); + + for (address, event) in self.events { + for _ in 0..event.end { + stack.pop(); + stack_indexes.pop(); + } + for call in event.start { + if let Some(index) = string_dedup.get(&call) { + stack.push(call); + stack_indexes.push(*index); + } else { + let index = string_dedup.len(); + string_dedup.insert(call.clone(), index); + stack.push(call); + stack_indexes.push(index); + } + } + if let Some(stack) = stack_dedup.get(&stack_indexes) { + entries.push((address, stack.clone())); + } else { + let stack = Arc::new(stack.clone()); + stack_dedup.insert(stack_indexes.clone(), stack.clone()); + entries.push((address, stack)); + } + } + InlineStackMap { entries, empty } + } +} diff --git a/src/emulator/shrooms_vb_core.rs b/src/emulator/shrooms_vb_core.rs index 56684b6..b00e9b6 100644 --- a/src/emulator/shrooms_vb_core.rs +++ b/src/emulator/shrooms_vb_core.rs @@ -1,10 +1,12 @@ -use std::{borrow::Cow, ffi::c_void, ptr, slice}; +use std::{borrow::Cow, ffi::c_void, ptr, slice, sync::Arc}; use anyhow::{Result, anyhow}; use bitflags::bitflags; use num_derive::{FromPrimitive, ToPrimitive}; use serde::{Deserialize, Serialize}; +use crate::emulator::inline_stack_map::{InlineStack, InlineStackMap}; + use super::address_set::AddressSet; #[repr(C)] @@ -206,11 +208,9 @@ extern "C" fn on_execute(sim: *mut VB, address: u32, code: *const u16, length: c 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)) }; - } + data.monitor.detect_event(sim, address, code); + // Something interesting will happen after this instruction is run. + // We'll react in the on_fetch callback it does. } let mut stopped = data.stop_reason.is_some() || data.monitor.event.is_some(); @@ -231,7 +231,7 @@ extern "C" fn on_execute(sim: *mut VB, address: u32, code: *const u16, length: c extern "C" fn on_fetch( sim: *mut VB, _fetch: c_int, - _address: u32, + address: u32, _value: *mut i32, _cycles: *mut u32, ) -> c_int { @@ -239,9 +239,13 @@ extern "C" fn on_fetch( // 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.new_inline_stack = data.monitor.detect_new_inline_stack(address); unsafe { vb_set_exception_callback(sim, Some(on_exception)) }; - unsafe { vb_set_fetch_callback(sim, None) }; - 1 + if data.monitor.event.is_some() || data.monitor.new_inline_stack.is_some() { + 1 + } else { + 0 + } } #[unsafe(no_mangle)] @@ -249,17 +253,21 @@ 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(); let cause = unsafe { *cause }; let pc = if cause == 0xff70 { 0xffffff60 } else { (cause & 0xfff0) as u32 | 0xffff0000 }; + data.monitor.event = data.monitor.queued_event.take(); + data.monitor.new_inline_stack = data.monitor.detect_new_inline_stack(pc); data.monitor.queued_event = Some(SimEvent::Interrupt(cause, pc)); 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 } + if data.monitor.event.is_some() || data.monitor.new_inline_stack.is_some() { + 1 + } else { + 0 + } } #[unsafe(no_mangle)] @@ -339,18 +347,35 @@ struct EventMonitor { event: Option, queued_event: Option, just_halted: bool, + inline_stack_map: InlineStackMap, + new_inline_stack: Option, + last_inline_stack: InlineStack, } impl EventMonitor { fn new() -> Self { + let inline_stack_map = InlineStackMap::empty(); + let last_inline_stack = inline_stack_map.get(0).clone(); Self { enabled: false, event: None, queued_event: None, just_halted: false, + inline_stack_map, + new_inline_stack: None, + last_inline_stack, } } + fn detect_new_inline_stack(&mut self, address: u32) -> Option { + let stack = self.inline_stack_map.get(address); + if Arc::ptr_eq(stack, &self.last_inline_stack) { + return None; + } + self.last_inline_stack = stack.clone(); + Some(stack.clone()) + } + 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() @@ -492,14 +517,18 @@ impl Sim { unsafe { vb_reset(self.sim) }; } - pub fn monitor_events(&mut self, enabled: bool) { + pub fn monitor_events(&mut self, enabled: bool, inline_stack_map: InlineStackMap) { let state = self.get_state(); state.monitor.enabled = enabled; state.monitor.event = None; state.monitor.queued_event = None; + state.monitor.new_inline_stack = None; + state.monitor.last_inline_stack = inline_stack_map.get(0).clone(); + state.monitor.inline_stack_map = inline_stack_map; if enabled { unsafe { vb_set_execute_callback(self.sim, Some(on_execute)) }; unsafe { vb_set_exception_callback(self.sim, Some(on_exception)) }; + unsafe { vb_set_fetch_callback(self.sim, Some(on_fetch)) }; } else { if !state.needs_execute_callback() { unsafe { vb_set_execute_callback(self.sim, None) }; @@ -816,9 +845,11 @@ impl Sim { reason } - pub fn take_sim_event(&mut self) -> Option { + pub fn take_profiler_updates(&mut self) -> (Option, Option) { let data = self.get_state(); - data.monitor.event.take() + let event = data.monitor.event.take(); + let inline_stack = data.monitor.new_inline_stack.take(); + (event, inline_stack) } fn get_state(&mut self) -> &mut VBState { diff --git a/src/profiler.rs b/src/profiler.rs index fe16404..8148aad 100644 --- a/src/profiler.rs +++ b/src/profiler.rs @@ -6,7 +6,9 @@ use std::{ use anyhow::Result; use tokio::{select, sync::mpsc}; -use crate::emulator::{EmulatorClient, EmulatorCommand, GameInfo, ProfileEvent, SimEvent, SimId}; +use crate::emulator::{ + EmulatorClient, EmulatorCommand, GameInfo, InlineStack, ProfileEvent, SimEvent, SimId, +}; use recording::Recording; use state::ProgramState; @@ -131,11 +133,18 @@ async fn run_profile( async fn handle_event(event: ProfileEvent, session: &mut ProfilerSession) -> Result<()> { match event { ProfileEvent::Start { info } => session.start_profiling(info).await, - ProfileEvent::Update { cycles, event } => { + ProfileEvent::Update { + cycles, + event, + inline_stack, + } => { session.track_elapsed_cycles(cycles); if let Some(event) = event { session.track_event(event)?; } + if let Some(stack) = inline_stack { + session.track_inline_stack(stack); + } } } Ok(()) @@ -233,6 +242,12 @@ impl ProfilerSession { } } + fn track_inline_stack(&mut self, inline_stack: InlineStack) { + if let Some(program) = &mut self.program { + program.track_inline_stack(inline_stack); + } + } + fn start_recording(&mut self) { if let Some(program) = &self.program { self.recording = Some(Recording::new(program)); diff --git a/src/profiler/recording.rs b/src/profiler/recording.rs index 0723be9..69ee13e 100644 --- a/src/profiler/recording.rs +++ b/src/profiler/recording.rs @@ -89,14 +89,21 @@ impl Recording { thread: ThreadHandle, frames: &[StackFrame], ) -> Option { - self.profile.intern_stack_frames( - thread, - frames.iter().map(|f| FrameInfo { - frame: Frame::InstructionPointer(f.address as u64), - category_pair: CategoryHandle::OTHER.into(), - flags: FrameFlags::empty(), - }), - ) + let frames = frames + .iter() + .map(|f| { + let frame = match f { + StackFrame::Address(address) => Frame::InstructionPointer(*address as u64), + StackFrame::Label(label) => Frame::Label(self.profile.intern_string(label)), + }; + FrameInfo { + frame, + category_pair: CategoryHandle::OTHER.into(), + flags: FrameFlags::empty(), + } + }) + .collect::>(); + self.profile.intern_stack_frames(thread, frames.into_iter()) } } diff --git a/src/profiler/state.rs b/src/profiler/state.rs index ed7e9e8..f305bbc 100644 --- a/src/profiler/state.rs +++ b/src/profiler/state.rs @@ -3,7 +3,7 @@ use std::{collections::HashMap, sync::Arc}; use anyhow::{Result, bail}; use fxprof_processed_profile::LibraryInfo; -use crate::emulator::GameInfo; +use crate::emulator::{GameInfo, InlineStack}; pub struct ProgramState { info: Arc, @@ -11,20 +11,16 @@ pub struct ProgramState { context_stack: Vec, } -pub struct StackFrame { - pub address: u32, +pub enum StackFrame { + Address(u32), + Label(Arc), } pub const RESET_CODE: u16 = 0xfff0; impl ProgramState { pub async fn new(info: Arc) -> Self { let mut call_stacks = HashMap::new(); - call_stacks.insert( - RESET_CODE, - vec![StackFrame { - address: 0xfffffff0, - }], - ); + call_stacks.insert(RESET_CODE, vec![StackFrame::Address(0xfffffff0)]); Self { info, call_stacks, @@ -53,7 +49,7 @@ impl ProgramState { let Some(stack) = self.call_stacks.get_mut(code) else { bail!("missing stack {code:04x}"); }; - stack.push(StackFrame { address }); + stack.push(StackFrame::Address(address)); Ok(()) } @@ -89,7 +85,7 @@ impl ProgramState { self.context_stack.push(code); if self .call_stacks - .insert(code, vec![StackFrame { address }]) + .insert(code, vec![StackFrame::Address(address)]) .is_some() { bail!("{code:04x} fired twice"); @@ -109,4 +105,20 @@ impl ProgramState { } Ok(()) } + + pub fn track_inline_stack(&mut self, inline_stack: InlineStack) { + let Some(code) = self.context_stack.last() else { + return; + }; + let Some(call_stack) = self.call_stacks.get_mut(code) else { + return; + }; + while call_stack + .pop_if(|f| matches!(f, StackFrame::Label(_))) + .is_some() + {} + for label in inline_stack.iter() { + call_stack.push(StackFrame::Label(label.clone())); + } + } } From 8f9922473ee5ef3d1a9413b164347c65c6e10805 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Mon, 1 Sep 2025 00:07:55 -0400 Subject: [PATCH 14/15] Support arbitrary markers --- src/emulator/shrooms_vb_core.rs | 49 +++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/src/emulator/shrooms_vb_core.rs b/src/emulator/shrooms_vb_core.rs index b00e9b6..ffe98e7 100644 --- a/src/emulator/shrooms_vb_core.rs +++ b/src/emulator/shrooms_vb_core.rs @@ -300,7 +300,7 @@ extern "C" fn on_read( extern "C" fn on_write( sim: *mut VB, address: u32, - _type: VBDataType, + typ_: VBDataType, value: *mut i32, _cycles: *mut u32, _cancel: *mut c_int, @@ -317,6 +317,29 @@ extern "C" fn on_write( } } + // If we have profiling enabled, track custom markers + if data.monitor.enabled { + let normalized_hw_address = address & 0x0700003f; + if normalized_hw_address == 0x02000038 && matches!(typ_, VBDataType::S32) { + assert!(data.monitor.queued_event.is_none()); + // The game has written the address of a null-terminated string + // (whose length is at most 64 bytes). Read that string. + let str_address = unsafe { *value } as u32; + let mut bytes = [0u8; 64]; + let mut len = 0; + for (dst, src_address) in bytes.iter_mut().zip(str_address..str_address + 64) { + let char = unsafe { vb_read(sim, src_address, VBDataType::U8) } as u8; + if char == 0 { + break; + } + *dst = char; + len += 1; + } + let name = String::from_utf8_lossy(&bytes[..len]).into_owned(); + data.monitor.queued_event = Some(SimEvent::Marker(Cow::Owned(name))); + } + } + if let Some(start) = data.write_watchpoints.start_of_range_containing(address) { let watch = if data.read_watchpoints.contains(address) { VBWatchpointType::Access @@ -465,6 +488,10 @@ impl VBState { || !self.read_watchpoints.is_empty() || !self.write_watchpoints.is_empty() } + + fn needs_write_callback(&self) -> bool { + self.stdout.is_some() || self.monitor.enabled || !self.write_watchpoints.is_empty() + } } pub enum StopReason { @@ -529,12 +556,18 @@ impl Sim { unsafe { vb_set_execute_callback(self.sim, Some(on_execute)) }; unsafe { vb_set_exception_callback(self.sim, Some(on_exception)) }; unsafe { vb_set_fetch_callback(self.sim, Some(on_fetch)) }; + unsafe { vb_set_write_callback(self.sim, Some(on_write)) }; } else { - if !state.needs_execute_callback() { + let needs_execute = state.needs_execute_callback(); + let needs_write = state.needs_write_callback(); + if !needs_execute { unsafe { vb_set_execute_callback(self.sim, None) }; } unsafe { vb_set_exception_callback(self.sim, None) }; unsafe { vb_set_fetch_callback(self.sim, None) }; + if !needs_write { + unsafe { vb_set_write_callback(self.sim, None) }; + } } } @@ -776,9 +809,10 @@ impl Sim { fn remove_write_watchpoint(&mut self, address: u32, length: usize) { let state = self.get_state(); state.write_watchpoints.remove(address, length); + let needs_write = state.needs_write_callback(); let needs_execute = state.needs_execute_callback(); if state.write_watchpoints.is_empty() { - if state.stdout.is_none() { + if !needs_write { unsafe { vb_set_write_callback(self.sim, None) }; } if !needs_execute { @@ -802,12 +836,15 @@ impl Sim { data.breakpoints.clear(); data.read_watchpoints.clear(); data.write_watchpoints.clear(); - let needs_write = data.stdout.is_some(); + let needs_write = data.needs_write_callback(); + let needs_execute = data.needs_execute_callback(); unsafe { vb_set_read_callback(self.sim, None) }; if !needs_write { unsafe { vb_set_write_callback(self.sim, None) }; } - unsafe { vb_set_execute_callback(self.sim, None) }; + if !needs_execute { + unsafe { vb_set_execute_callback(self.sim, None) }; + } } pub fn watch_stdout(&mut self, watch: bool) { @@ -819,7 +856,7 @@ impl Sim { } } else { data.stdout.take(); - if data.write_watchpoints.is_empty() { + if !data.needs_write_callback() { unsafe { vb_set_write_callback(self.sim, None) }; } } From 6e89a0c9888257f951b80abaddc142abf8f2d136 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Mon, 1 Sep 2025 17:38:00 -0400 Subject: [PATCH 15/15] Improve profiler interface --- src/window/profile.rs | 74 ++++++++++++++++++++++++++++++++----------- 1 file changed, 55 insertions(+), 19 deletions(-) diff --git a/src/window/profile.rs b/src/window/profile.rs index 0a2d1a1..0499bfa 100644 --- a/src/window/profile.rs +++ b/src/window/profile.rs @@ -1,17 +1,18 @@ use std::{fs, time::Duration}; use anyhow::Result; -use egui::{Button, CentralPanel, Checkbox, ViewportBuilder, ViewportId}; +use egui::{Button, CentralPanel, Checkbox, Label, ViewportBuilder, ViewportId}; use egui_notify::{Anchor, Toast, Toasts}; use crate::{ - emulator::{EmulatorClient, SimId}, + emulator::{EmulatorClient, EmulatorCommand, EmulatorState, SimId}, profiler::{Profiler, ProfilerStatus}, window::AppWindow, }; pub struct ProfileWindow { sim_id: SimId, + client: EmulatorClient, profiler: Profiler, toasts: Toasts, } @@ -20,6 +21,7 @@ impl ProfileWindow { pub fn new(sim_id: SimId, client: EmulatorClient) -> Self { Self { sim_id, + client: client.clone(), profiler: Profiler::new(sim_id, client), toasts: Toasts::new() .with_anchor(Anchor::BottomLeft) @@ -33,6 +35,10 @@ impl ProfileWindow { } fn finish_recording(&mut self) { + let pause = matches!(self.client.emulator_state(), EmulatorState::Running); + if pause { + self.client.send_command(EmulatorCommand::Pause); + } match self.try_finish_recording() { Ok(Some(path)) => { let mut toast = Toast::info(format!("Saved to {path}")); @@ -46,6 +52,9 @@ impl ProfileWindow { self.toasts.add(toast); } } + if pause { + self.client.send_command(EmulatorCommand::Resume); + } } fn try_finish_recording(&mut self) -> Result> { @@ -84,8 +93,32 @@ impl AppWindow for ProfileWindow { let status = self.profiler.status(); let recording = matches!(status, ProfilerStatus::Recording); CentralPanel::default().show(ctx, |ui| { + ui.horizontal_wrapped(|ui| { + ui.spacing_mut().item_spacing.x = 0.0; + ui.add( + Label::new( + "Use this tool to record performance profiles of your game, for use in ", + ) + .wrap_mode(egui::TextWrapMode::Wrap), + ); + ui.hyperlink("https://profiler.firefox.com"); + ui.label("."); + }); + ui.horizontal_wrapped(|ui| { + ui.spacing_mut().item_spacing.x = 0.0; + ui.add( + Label::new("For more instructions, see ").wrap_mode(egui::TextWrapMode::Wrap), + ); + ui.hyperlink_to( + "the Lemur wiki", + "https://git.virtual-boy.com/PVB/lemur/wiki/Profiling-with-Lemur", + ); + ui.label("."); + }); + ui.separator(); + let mut enabled = status.enabled(); - let enabled_checkbox = Checkbox::new(&mut enabled, "Profiling enabled?"); + let enabled_checkbox = Checkbox::new(&mut enabled, "Enable profiling"); if ui.add_enabled(!recording, enabled_checkbox).changed() { if enabled { self.profiler.enable(); @@ -93,23 +126,26 @@ impl AppWindow for ProfileWindow { self.profiler.disable(); } } - - ui.horizontal(|ui| { - if !recording { - let record_button = Button::new("Record"); - let can_record = matches!(status, ProfilerStatus::Enabled); - if ui.add_enabled(can_record, record_button).clicked() { - self.profiler.start_recording(); + if !enabled { + ui.label("Enabling profiling will restart your current game."); + } else { + ui.horizontal(|ui| { + if !recording { + let record_button = Button::new("Record"); + let can_record = matches!(status, ProfilerStatus::Enabled); + if ui.add_enabled(can_record, record_button).clicked() { + self.profiler.start_recording(); + } + } else { + if ui.button("Finish recording").clicked() { + self.finish_recording(); + } + if ui.button("Cancel recording").clicked() { + self.profiler.cancel_recording(); + } } - } else { - if ui.button("Finish recording").clicked() { - self.finish_recording(); - } - if ui.button("Cancel recording").clicked() { - self.profiler.cancel_recording(); - } - } - }); + }); + } match &status { ProfilerStatus::Recording => {