Add an option to limit the repaint rate in the web runner (#7482)

Co-authored-by: Lucas Meurer <hi@lucasmerlin.me>
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This commit is contained in:
Sven Niederberger 2025-09-09 13:49:35 +02:00 committed by GitHub
parent 9db03983dd
commit ec5bc35c38
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 48 additions and 14 deletions

View File

@ -509,6 +509,10 @@ pub struct WebOptions {
///
/// Defaults to true.
pub should_prevent_default: Box<dyn Fn(&egui::Event) -> 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<u32>,
}
#[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,
}
}
}

View File

@ -96,7 +96,8 @@ impl AppRunner {
wgpu_render_state: None,
};
let needs_repaint: std::sync::Arc<NeedRepaint> = Default::default();
let needs_repaint: std::sync::Arc<NeedRepaint> =
std::sync::Arc::new(NeedRepaint::new(web_options.max_fps));
{
let needs_repaint = needs_repaint.clone();
egui_ctx.set_request_repaint_callback(move |info| {

View File

@ -50,11 +50,20 @@ impl WebInput {
// ----------------------------------------------------------------------------
/// Stores when to do the next repaint.
pub(crate) struct NeedRepaint(Mutex<f64>);
pub(crate) struct NeedRepaint {
/// Time in seconds when the next repaint should happen.
next_repaint: Mutex<f64>,
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<u32>) -> 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
}
}
}

View File

@ -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 {