diff --git a/crates/eframe/src/epi.rs b/crates/eframe/src/epi.rs index 4b5e69d1..5502b634 100644 --- a/crates/eframe/src/epi.rs +++ b/crates/eframe/src/epi.rs @@ -509,6 +509,10 @@ pub struct WebOptions { /// /// Defaults to true. pub should_prevent_default: Box bool>, + + /// Maximum rate at which to repaint. This can be used to artificially reduce the repaint rate below + /// vsync in order to save resources. + pub max_fps: Option, } #[cfg(target_arch = "wasm32")] @@ -527,6 +531,8 @@ impl Default for WebOptions { should_stop_propagation: Box::new(|_| true), should_prevent_default: Box::new(|_| true), + + max_fps: None, } } } diff --git a/crates/eframe/src/web/app_runner.rs b/crates/eframe/src/web/app_runner.rs index 9f2d2beb..9421f9b4 100644 --- a/crates/eframe/src/web/app_runner.rs +++ b/crates/eframe/src/web/app_runner.rs @@ -96,7 +96,8 @@ impl AppRunner { wgpu_render_state: None, }; - let needs_repaint: std::sync::Arc = Default::default(); + let needs_repaint: std::sync::Arc = + std::sync::Arc::new(NeedRepaint::new(web_options.max_fps)); { let needs_repaint = needs_repaint.clone(); egui_ctx.set_request_repaint_callback(move |info| { diff --git a/crates/eframe/src/web/backend.rs b/crates/eframe/src/web/backend.rs index 5877f424..e81e5487 100644 --- a/crates/eframe/src/web/backend.rs +++ b/crates/eframe/src/web/backend.rs @@ -50,11 +50,20 @@ impl WebInput { // ---------------------------------------------------------------------------- /// Stores when to do the next repaint. -pub(crate) struct NeedRepaint(Mutex); +pub(crate) struct NeedRepaint { + /// Time in seconds when the next repaint should happen. + next_repaint: Mutex, -impl Default for NeedRepaint { - fn default() -> Self { - Self(Mutex::new(f64::NEG_INFINITY)) // start with a repaint + /// Rate limit for repaint. 0 means "unlimited". The rate may still be limited by vsync. + max_fps: u32, +} + +impl NeedRepaint { + pub fn new(max_fps: Option) -> Self { + Self { + next_repaint: Mutex::new(f64::NEG_INFINITY), // start with a repaint + max_fps: max_fps.unwrap_or(0), + } } } @@ -62,25 +71,43 @@ impl NeedRepaint { /// Returns the time (in [`now_sec`] scale) when /// we should next repaint. pub fn when_to_repaint(&self) -> f64 { - *self.0.lock() + *self.next_repaint.lock() } /// Unschedule repainting. pub fn clear(&self) { - *self.0.lock() = f64::INFINITY; + *self.next_repaint.lock() = f64::INFINITY; } pub fn repaint_after(&self, num_seconds: f64) { - let mut repaint_time = self.0.lock(); - *repaint_time = repaint_time.min(super::now_sec() + num_seconds); + let mut time = super::now_sec() + num_seconds; + time = self.round_repaint_time_to_rate(time); + let mut repaint_time = self.next_repaint.lock(); + *repaint_time = repaint_time.min(time); + } + + /// Request a repaint. Depending on the presence of rate limiting, this may not be instant. + pub fn repaint(&self) { + let time = self.round_repaint_time_to_rate(super::now_sec()); + let mut repaint_time = self.next_repaint.lock(); + *repaint_time = repaint_time.min(time); + } + + pub fn repaint_asap(&self) { + *self.next_repaint.lock() = f64::NEG_INFINITY; } pub fn needs_repaint(&self) -> bool { self.when_to_repaint() <= super::now_sec() } - pub fn repaint_asap(&self) { - *self.0.lock() = f64::NEG_INFINITY; + fn round_repaint_time_to_rate(&self, time: f64) -> f64 { + if self.max_fps == 0 { + time + } else { + let interval = 1.0 / self.max_fps as f64; + (time / interval).ceil() * interval + } } } diff --git a/crates/eframe/src/web/events.rs b/crates/eframe/src/web/events.rs index 1a79937b..a5bb4c0e 100644 --- a/crates/eframe/src/web/events.rs +++ b/crates/eframe/src/web/events.rs @@ -638,7 +638,7 @@ fn install_mousemove(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), let should_stop_propagation = (runner.web_options.should_stop_propagation)(&egui_event); let should_prevent_default = (runner.web_options.should_prevent_default)(&egui_event); runner.input.raw.events.push(egui_event); - runner.needs_repaint.repaint_asap(); + runner.needs_repaint.repaint(); // Use web options to tell if the web event should be propagated to parent elements based on the egui event. if should_stop_propagation { @@ -721,7 +721,7 @@ fn install_touchmove(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), runner.input.raw.events.push(egui_event); push_touches(runner, egui::TouchPhase::Move, &event); - runner.needs_repaint.repaint_asap(); + runner.needs_repaint.repaint(); // Use web options to tell if the web event should be propagated to parent elements based on the egui event. if should_stop_propagation { @@ -834,7 +834,7 @@ fn install_wheel(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsV let should_prevent_default = (runner.web_options.should_prevent_default)(&egui_event); runner.input.raw.events.push(egui_event); - runner.needs_repaint.repaint_asap(); + runner.needs_repaint.repaint(); // Use web options to tell if the web event should be propagated to parent elements based on the egui event. if should_stop_propagation {