diff --git a/.cargo/config.toml b/.cargo/config.toml index f34c1e67..25d8229b 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,5 +1,2 @@ [build] -rustflags = ["-Z", "threads=8"] - -[target.'cfg(target_arch = "wasm32")'] rustflags = ["-Z", "threads=8", "--cfg=web_sys_unstable_apis"] diff --git a/tetanes/src/nes.rs b/tetanes/src/nes.rs index 5b201635..d59735ec 100644 --- a/tetanes/src/nes.rs +++ b/tetanes/src/nes.rs @@ -177,7 +177,7 @@ impl Nes { viewport_builder, painter: painter_rx.recv()?, }; - let (frame_tx, frame_rx) = blocking::with_recycle::(3, FrameRecycle); + let (frame_tx, frame_rx) = blocking::with_recycle::(10, FrameRecycle); let (mut cfg, tx) = self .init_state .take() diff --git a/tetanes/src/nes/config.rs b/tetanes/src/nes/config.rs index cc49915f..3bd38228 100644 --- a/tetanes/src/nes/config.rs +++ b/tetanes/src/nes/config.rs @@ -30,9 +30,9 @@ impl Default for AudioConfig { 512 }, latency: if cfg!(target_arch = "wasm32") { - Duration::from_millis(60) + Duration::from_millis(80) } else { - Duration::from_millis(40) + Duration::from_millis(50) }, } } diff --git a/tetanes/src/nes/emulation.rs b/tetanes/src/nes/emulation.rs index f7768b37..efe0d747 100644 --- a/tetanes/src/nes/emulation.rs +++ b/tetanes/src/nes/emulation.rs @@ -9,7 +9,6 @@ use crate::{ }, renderer::{gui::MessageType, FrameRecycle}, }, - platform::{self, Feature}, thread, }; use anyhow::anyhow; @@ -192,7 +191,7 @@ impl Multi { state.on_event(&event); } - state.clock_frame(); + state.try_clock_frame(); } } } @@ -237,9 +236,9 @@ impl Emulation { } } - pub fn clock_frame(&mut self) { + pub fn try_clock_frame(&mut self) { match &mut self.threads { - Threads::Single(Single { state }) => state.clock_frame(), + Threads::Single(Single { state }) => state.try_clock_frame(), // Multi-threaded emulation handles it's own clock timing and redraw requests Threads::Multi(Multi { handle, .. }) => handle.thread().unpark(), } @@ -260,6 +259,7 @@ pub struct State { last_frame_time: Instant, frame_time_diag: FrameTimeDiag, run_state: RunState, + threaded: bool, rewinding: bool, rewind: Rewind, record: Record, @@ -314,6 +314,8 @@ impl State { last_frame_time: Instant::now(), frame_time_diag: FrameTimeDiag::new(), run_state: RunState::Paused, + threaded: cfg.emulation.threaded + && std::thread::available_parallelism().map_or(false, |count| count.get() > 1), rewinding: false, rewind, record: Record::new(), @@ -501,6 +503,9 @@ impl State { /// Handle config event. fn on_config_event(&mut self, event: &ConfigEvent) { + #[cfg(feature = "profiling")] + puffin::profile_function!(); + match event { ConfigEvent::ApuChannelEnabled((channel, enabled)) => { self.control_deck @@ -584,6 +589,9 @@ impl State { return; } + #[cfg(feature = "profiling")] + puffin::profile_function!(); + self.frame_time_diag .push(self.last_frame_time.elapsed().as_secs_f32()); self.last_frame_time = Instant::now(); @@ -611,20 +619,10 @@ impl State { } fn send_frame(&mut self) { - // Indicate we want to redraw to ensure there's a frame slot made available if - // the pool is already full - self.tx.nes_event(RendererEvent::RequestRedraw { - viewport_id: ViewportId::ROOT, - when: Instant::now(), - }); - if self.audio.enabled() || !platform::supports(Feature::Blocking) { - match self.frame_tx.try_send_ref() { - Ok(mut frame) => self.control_deck.frame_buffer_into(&mut frame), - Err(TrySendError::Full(_)) => debug!("dropped frame"), - Err(_) => shutdown(&self.tx, "failed to get frame"), - } - } else if let Ok(mut frame) = self.frame_tx.send_ref() { - self.control_deck.frame_buffer_into(&mut frame); + match self.frame_tx.try_send_ref() { + Ok(mut frame) => self.control_deck.frame_buffer_into(&mut frame), + Err(TrySendError::Full(_)) => debug!("dropped frame"), + Err(_) => shutdown(&self.tx, "failed to get frame"), } } @@ -828,10 +826,13 @@ impl State { Ok(image.save(&filename).map(|_| filename)?) } - fn park_timeout(&self) -> Option { + fn park_duration(&self) -> Option { + #[cfg(feature = "profiling")] + puffin::profile_function!(); + let park_epsilon = Duration::from_millis(1); // Park if we're paused, occluded, or not running - if self.run_state.paused() || !self.control_deck.is_running() { + let duration = if self.run_state.paused() || !self.control_deck.is_running() { Some(self.target_frame_duration - park_epsilon) } else if self.rewinding || !self.audio.enabled() { (self.clock_time_accumulator < self.target_frame_duration.as_secs_f32()).then(|| { @@ -842,13 +843,21 @@ impl State { }) } else { (self.audio.queued_time() > self.audio.latency) - // Even though we just did a check for >=, audio is still being consumed so this + // Even though we just did a comparison, audio is still being consumed so this // could underflow .then(|| self.audio.queued_time().saturating_sub(self.audio.latency)) - } + }; + duration.map(|duration| { + // Parking thread is only required for Multi-threaded emulation to save CPU cycles. + if self.threaded { + Duration::ZERO + } else { + duration + } + }) } - fn clock_frame(&mut self) { + fn try_clock_frame(&mut self) { #[cfg(feature = "profiling")] puffin::profile_function!(); @@ -859,7 +868,15 @@ impl State { self.clock_time_accumulator = 0.020; } - if let Some(park_timeout) = self.park_timeout() { + // If any frames are still pending, request a redraw + if !self.frame_tx.is_empty() { + self.tx.nes_event(RendererEvent::RequestRedraw { + viewport_id: ViewportId::ROOT, + when: Instant::now(), + }); + } + + if let Some(park_timeout) = self.park_duration() { self.tx.nes_event(RendererEvent::RequestRedraw { viewport_id: ViewportId::ROOT, when: Instant::now() + park_timeout, @@ -871,7 +888,6 @@ impl State { if self.rewinding { match self.rewind.pop() { Some(cpu) => { - self.clock_time_accumulator -= self.target_frame_duration.as_secs_f32(); self.control_deck.load_cpu(cpu); self.send_frame(); self.update_frame_stats(); @@ -883,44 +899,18 @@ impl State { self.on_emulation_event(&event); } - // Clock frames until we catch up to the audio queue latency as long as audio is enabled and we're - // not rewinding, otherwise fall back to time-based clocking - // let mut clocked_frames = 0; // Prevent infinite loop when queued audio falls behind - let mut run_ahead = self.run_ahead; - if self.speed > 1.0 { - run_ahead = 0; - } + let run_ahead = if self.speed > 1.0 { 0 } else { self.run_ahead }; let res = self.control_deck.clock_frame_ahead( run_ahead, |_cycles, frame_buffer, audio_samples| { self.audio.process(audio_samples); - let send_frame = |frame: &mut Frame| { - frame.clear(); - frame.extend_from_slice(frame_buffer); - }; - self.clock_time_accumulator -= self.target_frame_duration.as_secs_f32(); - - // Indicate we want to redraw to ensure there's a frame slot made available if - // the pool is already full - self.tx.nes_event(RendererEvent::RequestRedraw { - viewport_id: ViewportId::ROOT, - when: Instant::now(), - }); - - if self.audio.enabled() || !platform::supports(Feature::Blocking) { - // If audio is enabled or wasm, frame rate is controlled by park_timeout - // above - match self.frame_tx.try_send_ref() { - Ok(mut frame) => send_frame(&mut frame), - Err(TrySendError::Full(_)) => debug!("dropped frame"), - Err(_) => shutdown(&self.tx, "failed to get frame"), - } - } else { - // Otherwise we'll block on vsync - match self.frame_tx.send_ref() { - Ok(mut frame) => send_frame(&mut frame), - Err(_) => shutdown(&self.tx, "failed to get frame"), + match self.frame_tx.try_send_ref() { + Ok(mut frame) => { + frame.clear(); + frame.extend_from_slice(frame_buffer); } + Err(TrySendError::Full(_)) => debug!("dropped frame"), + Err(_) => shutdown(&self.tx, "failed to get frame"), } }, ); @@ -942,5 +932,12 @@ impl State { } } } + + self.clock_time_accumulator -= self.target_frame_duration.as_secs_f32(); + // Request to draw this frame + self.tx.nes_event(RendererEvent::RequestRedraw { + viewport_id: ViewportId::ROOT, + when: Instant::now(), + }); } } diff --git a/tetanes/src/nes/event.rs b/tetanes/src/nes/event.rs index 68f5b094..e348cef2 100644 --- a/tetanes/src/nes/event.rs +++ b/tetanes/src/nes/event.rs @@ -308,6 +308,9 @@ impl Running { event: Event, event_loop: &EventLoopWindowTarget, ) { + #[cfg(feature = "profiling")] + puffin::profile_function!(); + match event { Event::Suspended => { if platform::supports(platform::Feature::Suspend) { @@ -351,9 +354,8 @@ impl Running { if !res.consumed { match event { WindowEvent::RedrawRequested => { - self.emulation.clock_frame(); + self.emulation.try_clock_frame(); - self.repaint_times.remove(&window_id); if let Err(err) = self.renderer.redraw( window_id, event_loop, @@ -362,6 +364,7 @@ impl Running { ) { self.renderer.on_error(err); } + self.repaint_times.remove(&window_id); } WindowEvent::Resized(_) => { if Some(window_id) == self.renderer.root_window_id() { @@ -376,8 +379,12 @@ impl Running { self.nes_event(EmulationEvent::RunState(self.run_state)); } } else { - if let Err(err) = self.renderer.save(&self.cfg) { - error!("failed to save rendererer state: {err:?}"); + let time_since_last_save = + Instant::now() - self.renderer.last_save_time; + if time_since_last_save > Duration::from_secs(30) { + if let Err(err) = self.renderer.save(&self.cfg) { + error!("failed to save rendererer state: {err:?}"); + } } if self .renderer @@ -479,6 +486,9 @@ impl Running { } pub fn on_ui_event(&mut self, event: UiEvent) { + #[cfg(feature = "profiling")] + puffin::profile_function!(); + match event { UiEvent::Message((ty, msg)) => self.renderer.add_message(ty, msg), UiEvent::Error(err) => self.renderer.on_error(anyhow!(err)), @@ -531,6 +541,9 @@ impl Running { /// Trigger a custom event. pub fn nes_event(&mut self, event: impl Into) { + #[cfg(feature = "profiling")] + puffin::profile_function!(); + let event = event.into(); trace!("Nes event: {event:?}"); @@ -551,6 +564,9 @@ impl Running { pub fn on_gamepad_event(&mut self, window_id: WindowId, event: gilrs::Event) { use gilrs::EventType; + #[cfg(feature = "profiling")] + puffin::profile_function!(); + // Connect first because we may not have a name set yet if event.event == EventType::Connected { self.gamepads.connect(event.id); @@ -647,6 +663,9 @@ impl Running { state: ElementState, repeat: bool, ) { + #[cfg(feature = "profiling")] + puffin::profile_function!(); + if let Some(action) = self.input_bindings.get(&input).copied() { trace!("action: {action:?}, state: {state:?}, repeat: {repeat:?}"); let released = state == ElementState::Released; diff --git a/tetanes/src/nes/renderer.rs b/tetanes/src/nes/renderer.rs index 7098a5da..f5df17b8 100644 --- a/tetanes/src/nes/renderer.rs +++ b/tetanes/src/nes/renderer.rs @@ -112,7 +112,7 @@ pub struct Renderer { render_state: Option, texture: Texture, first_frame: bool, - last_save_time: Instant, + pub(crate) last_save_time: Instant, } impl std::fmt::Debug for Renderer { @@ -401,6 +401,9 @@ impl Renderer { /// Handle event. pub fn on_event(&mut self, event: &NesEvent, cfg: &Config) { + #[cfg(feature = "profiling")] + puffin::profile_function!(); + self.gui.borrow_mut().on_event(event); if let NesEvent::Renderer(event) = event { @@ -447,6 +450,9 @@ impl Renderer { } fn initialize_all_windows(&mut self, event_loop: &EventLoopWindowTarget) { + #[cfg(feature = "profiling")] + puffin::profile_function!(); + if self.ctx.embed_viewports() { return; } @@ -480,6 +486,9 @@ impl Renderer { event: &WindowEvent, cfg: &Config, ) -> EventResponse { + #[cfg(feature = "profiling")] + puffin::profile_function!(); + let viewport_id = self.viewport_id_for_window(window_id); match event { WindowEvent::Focused(focused) => { @@ -625,6 +634,9 @@ impl Renderer { /// Handle gamepad event updates. pub fn on_gamepad_update(&self, gamepads: &Gamepads) -> EventResponse { + #[cfg(feature = "profiling")] + puffin::profile_function!(); + if self.gui.borrow().pending_keybind.is_some() && gamepads.has_events() { return EventResponse { consumed: true, @@ -847,6 +859,9 @@ impl Renderer { viewport_ui_cb: Option>, focused: Option, ) -> &'a mut Viewport { + #[cfg(feature = "profiling")] + puffin::profile_function!(); + if builder.icon.is_none() { builder.icon = viewports .get_mut(&ids.parent) @@ -1029,6 +1044,9 @@ impl Renderer { state: &Rc>, gui: &Rc>, ) -> EventResponse { + #[cfg(feature = "profiling")] + puffin::profile_function!(); + let raw_input = { let State { viewports, .. } = &mut *state.borrow_mut(); @@ -1082,8 +1100,6 @@ impl Renderer { ) -> anyhow::Result<()> { #[cfg(feature = "profiling")] puffin::profile_function!(); - #[cfg(feature = "profiling")] - puffin::GlobalProfiler::lock().new_frame(); if self.first_frame { self.initialize()?; @@ -1098,6 +1114,9 @@ impl Renderer { return Ok(()); }; + #[cfg(feature = "profiling")] + puffin::GlobalProfiler::lock().new_frame(); + self.handle_resize(viewport_id, cfg); let (viewport_ui_cb, raw_input) = { @@ -1139,10 +1158,9 @@ impl Renderer { // resize tied to a configuration change. if viewport_id == ViewportId::ROOT { if let Some(render_state) = &self.render_state { - // We only care about the latest frame let mut frame_buffer = self.frame_rx.try_recv_ref(); - while !self.frame_rx.is_empty() { - debug!("dropped frame"); + while self.frame_rx.remaining() < 2 { + debug!("skipping frame"); frame_buffer = self.frame_rx.try_recv_ref(); } match frame_buffer { @@ -1168,17 +1186,17 @@ impl Renderer { event_loop.exit(); return Ok(()); } + // Empty frames are fine as we may repaint more often than 60fps due to + // UI interactions with keyboard/mouse _ => (), } } } let always_on_top = cfg.renderer.always_on_top; - let output = self.ctx.run(raw_input, |ctx| { - if let Some(viewport_ui_cb) = viewport_ui_cb { - viewport_ui_cb(ctx); - } - self.gui.borrow_mut().ui(ctx, gamepads, cfg); + let output = self.ctx.run(raw_input, |ctx| match viewport_ui_cb { + Some(viewport_ui_cb) => viewport_ui_cb(ctx), + None => self.gui.borrow_mut().ui(ctx, gamepads, cfg), }); { @@ -1265,6 +1283,9 @@ impl Renderer { } fn handle_resize(&mut self, viewport_id: ViewportId, cfg: &Config) { + #[cfg(feature = "profiling")] + puffin::profile_function!(); + if viewport_id == ViewportId::ROOT { if self.gui.borrow().resize_window { if !self.fullscreen() { @@ -1311,6 +1332,9 @@ impl Viewport { return; } + #[cfg(feature = "profiling")] + puffin::profile_function!(); + let viewport_id = self.ids.this; let window_builder = egui_winit::create_winit_window_builder(ctx, event_loop, self.builder.clone()) diff --git a/tetanes/src/nes/renderer/gui.rs b/tetanes/src/nes/renderer/gui.rs index cab7aec7..cb4aa2ea 100644 --- a/tetanes/src/nes/renderer/gui.rs +++ b/tetanes/src/nes/renderer/gui.rs @@ -252,6 +252,9 @@ impl Gui { } pub fn on_window_event(&mut self, event: &WindowEvent) -> EventResponse { + #[cfg(feature = "profiling")] + puffin::profile_function!(); + if self.pending_keybind.is_some() && matches!( event, @@ -268,6 +271,9 @@ impl Gui { } pub fn on_event(&mut self, event: &NesEvent) { + #[cfg(feature = "profiling")] + puffin::profile_function!(); + match event { NesEvent::Ui(UiEvent::UpdateAvailable(version)) => { self.version.set_latest(version.clone()); @@ -415,6 +421,9 @@ impl Gui { } fn initialize(&mut self, ctx: &Context, cfg: &Config) { + #[cfg(feature = "profiling")] + puffin::profile_function!(); + let theme = if cfg.renderer.dark_theme { Self::dark_theme() } else { @@ -463,6 +472,9 @@ impl Gui { } fn check_for_updates(&mut self, notify_latest: bool) { + #[cfg(feature = "profiling")] + puffin::profile_function!(); + let spawn_update = std::thread::Builder::new() .name("check_updates".into()) .spawn({ @@ -501,6 +513,9 @@ impl Gui { return; } + #[cfg(feature = "profiling")] + puffin::profile_function!(); + let title = "Set Keybind"; // TODO: Make this deferred? Requires `tx` and `cfg` to be Send + Sync ctx.show_viewport_immediate( @@ -555,6 +570,9 @@ impl Gui { return; }; + #[cfg(feature = "profiling")] + puffin::profile_function!(); + if let Some(action) = conflict { ui.label(format!("Conflict with {action}.")); ui.horizontal(|ui| { @@ -673,6 +691,9 @@ impl Gui { return; } + #[cfg(feature = "profiling")] + puffin::profile_function!(); + let title = "Unassign Gamepad"; // TODO: Make this deferred? Requires `tx` and `cfg` to be Send + Sync ctx.show_viewport_immediate( @@ -759,6 +780,9 @@ impl Gui { } fn show_performance_window(&mut self, ctx: &Context, cfg: &mut Config) { + #[cfg(feature = "profiling")] + puffin::profile_function!(); + let mut perf_stats_open = self.perf_stats_open; egui::Window::new("Performance Stats") .open(&mut perf_stats_open) @@ -771,6 +795,9 @@ impl Gui { return; } + #[cfg(feature = "profiling")] + puffin::profile_function!(); + let title = "Preferences"; let mut viewport_builder = egui::ViewportBuilder::default().with_title(title); if cfg.renderer.always_on_top { @@ -810,6 +837,9 @@ impl Gui { return; } + #[cfg(feature = "profiling")] + puffin::profile_function!(); + let title = "Keybinds"; let mut viewport_builder = egui::ViewportBuilder::default().with_title(title); if cfg.renderer.always_on_top { @@ -843,6 +873,9 @@ impl Gui { } fn show_about_window(&mut self, ctx: &Context) { + #[cfg(feature = "profiling")] + puffin::profile_function!(); + let mut about_open = self.about_open; egui::Window::new("About TetaNES") .open(&mut about_open) @@ -855,6 +888,9 @@ impl Gui { return; }; + #[cfg(feature = "profiling")] + puffin::profile_function!(); + let mut about_homebrew_open = true; egui::Window::new(format!("About {}", rom.name)) .open(&mut about_homebrew_open) @@ -878,6 +914,9 @@ impl Gui { } fn show_update_window(&mut self, ctx: &Context) { + #[cfg(feature = "profiling")] + puffin::profile_function!(); + let mut update_window_open = self.update_window_open; let mut close_window = false; let enable_auto_update = false; @@ -1097,6 +1136,9 @@ impl Gui { } fn homebrew_rom_menu(&mut self, ui: &mut Ui) { + #[cfg(feature = "profiling")] + puffin::profile_function!(); + ScrollArea::vertical().show(ui, |ui| { for rom in HOMEBREW_ROMS { ui.horizontal(|ui| { @@ -1370,7 +1412,7 @@ impl Gui { #[cfg(feature = "profiling")] { let mut profile = puffin::are_scopes_on(); - ui.checkbox(&mut profile, "Enable Profiling") + ui.toggle_value(&mut profile, "Profiler") .on_hover_text("Toggle the Puffin profiling window"); puffin::set_scopes_on(profile); } @@ -1495,6 +1537,9 @@ impl Gui { } fn help_menu(&mut self, ui: &mut Ui) { + #[cfg(feature = "profiling")] + puffin::profile_function!(); + ui.allocate_space(Vec2::new(Self::MENU_WIDTH, 0.0)); if self.version.requires_updates() && ui.button("🌐 Check for Updates...").clicked() { @@ -1855,9 +1900,11 @@ impl Gui { } } } - if ui.button("Clear Recent ROMs").clicked() { - cfg.renderer.recent_roms.clear(); - } + } + if platform::supports(platform::Feature::Filesystem) + && ui.button("Clear Recent ROMs").clicked() + { + cfg.renderer.recent_roms.clear(); } }); });