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();
}
});
});