diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs index 724ddc6d5..37e3faa69 100644 --- a/crates/eframe/src/native/glow_integration.rs +++ b/crates/eframe/src/native/glow_integration.rs @@ -34,7 +34,7 @@ use egui_winit::accesskit_winit; use crate::{ App, AppCreator, CreationContext, NativeOptions, Result, Storage, - native::epi_integration::EpiIntegration, + native::{epi_integration::EpiIntegration, winit_integration::is_invisible_or_minimized}, }; use super::{ @@ -761,9 +761,11 @@ impl GlowWinitRunning<'_> { integration.maybe_autosave(app.as_mut(), Some(&window)); - if window.is_minimized() == Some(true) { + if is_invisible_or_minimized(&window) { // On Mac, a minimized Window uses up all CPU: // https://github.com/emilk/egui/issues/325 + // On Windows, an invisible window also uses up all CPU: + // https://github.com/emilk/egui/issues/7776 profiling::scope!("minimized_sleep"); std::thread::sleep(std::time::Duration::from_millis(10)); } diff --git a/crates/eframe/src/native/run.rs b/crates/eframe/src/native/run.rs index 0597d318c..73b58ae61 100644 --- a/crates/eframe/src/native/run.rs +++ b/crates/eframe/src/native/run.rs @@ -1,4 +1,4 @@ -use std::time::Instant; +use std::time::{Duration, Instant}; use winit::{ application::ApplicationHandler, @@ -11,9 +11,20 @@ use ahash::HashMap; use super::winit_integration::{UserEvent, WinitApp}; use crate::{ Result, epi, - native::{event_loop_context, winit_integration::EventResult}, + native::{ + event_loop_context, + winit_integration::{EventResult, is_invisible_or_minimized}, + }, }; +/// Minimum interval between repaints for invisible windows. +/// +/// On Windows, invisible windows don't receive `RedrawRequested` events, +/// so we throttle their repaints to avoid busy-looping while still +/// processing viewport commands like `Visible(true)`. +/// See . +const INVISIBLE_WINDOW_REPAINT_INTERVAL: Duration = Duration::from_millis(100); + // ---------------------------------------------------------------------------- fn create_event_loop(native_options: &mut epi::NativeOptions) -> Result> { #[cfg(target_os = "android")] @@ -177,23 +188,54 @@ impl WinitAppWrapper { fn check_redraw_requests(&mut self, event_loop: &ActiveEventLoop) { let now = Instant::now(); + let mut invisible_window_ids = Vec::new(); + self.windows_next_repaint_times .retain(|window_id, repaint_time| { if now < *repaint_time { return true; // not yet ready } - event_loop.set_control_flow(ControlFlow::Poll); - if let Some(window) = self.winit_app.window(*window_id) { - log::trace!("request_redraw for {window_id:?}"); - window.request_redraw(); + // On Windows, invisible windows don't receive RedrawRequested + // events, so pending viewport commands (e.g. Visible(true)) would + // never be processed. We collect these windows to paint them + // directly below. + // See: https://github.com/emilk/egui/issues/5229 + if is_invisible_or_minimized(&window) { + invisible_window_ids.push(*window_id); + } else { + log::trace!("request_redraw for {window_id:?}"); + event_loop.set_control_flow(ControlFlow::Poll); + window.request_redraw(); + } } else { log::trace!("No window found for {window_id:?}"); } false }); + // Paint invisible windows directly, since they won't receive + // RedrawRequested events on Windows. This ensures that viewport + // commands like Visible(true) are still processed. + for window_id in &invisible_window_ids { + let event_result = self.winit_app.run_ui_and_paint(event_loop, *window_id); + self.handle_event_result(event_loop, event_result); + } + + // Throttle any already-scheduled repaints for invisible windows + // to avoid busy-looping. If no repaint was requested by the app, + // the window will simply sleep. + // See: https://github.com/emilk/egui/issues/7776 + if !invisible_window_ids.is_empty() { + let next_paint = Instant::now() + INVISIBLE_WINDOW_REPAINT_INTERVAL; + for window_id in &invisible_window_ids { + self.windows_next_repaint_times + .entry(*window_id) + .and_modify(|t| *t = (*t).min(next_paint)); + } + } + let next_repaint_time = self.windows_next_repaint_times.values().min().copied(); if let Some(next_repaint_time) = next_repaint_time { event_loop.set_control_flow(ControlFlow::WaitUntil(next_repaint_time)); @@ -270,6 +312,16 @@ impl ApplicationHandler for WinitAppWrapper { if let Some(window_id) = self.winit_app.window_id_from_viewport_id(viewport_id) { + // Throttle repaints for invisible windows to prevent + // high CPU usage on Windows. + // See: https://github.com/emilk/egui/issues/7776 + let when = if let Some(window) = self.winit_app.window(window_id) + && is_invisible_or_minimized(&window) + { + when.max(Instant::now() + INVISIBLE_WINDOW_REPAINT_INTERVAL) + } else { + when + }; Ok(EventResult::RepaintAt(window_id, when)) } else { Ok(EventResult::Wait) diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs index ea96a1845..6d300d513 100644 --- a/crates/eframe/src/native/wgpu_integration.rs +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -27,7 +27,10 @@ use winit_integration::UserEvent; use crate::{ App, AppCreator, CreationContext, NativeOptions, Result, Storage, - native::{epi_integration::EpiIntegration, winit_integration::EventResult}, + native::{ + epi_integration::EpiIntegration, + winit_integration::{EventResult, is_invisible_or_minimized}, + }, }; use super::{epi_integration, event_loop_context, winit_integration, winit_integration::WinitApp}; @@ -778,10 +781,12 @@ impl WgpuWinitRunning<'_> { integration.maybe_autosave(app.as_mut(), window.map(|w| w.as_ref())); if let Some(window) = window - && window.is_minimized() == Some(true) + && is_invisible_or_minimized(window) { // On Mac, a minimized Window uses up all CPU: // https://github.com/emilk/egui/issues/325 + // On Windows, an invisible window also uses up all CPU: + // https://github.com/emilk/egui/issues/7776 profiling::scope!("minimized_sleep"); std::thread::sleep(std::time::Duration::from_millis(10)); } diff --git a/crates/eframe/src/native/winit_integration.rs b/crates/eframe/src/native/winit_integration.rs index 012c22f8e..b4ec62c09 100644 --- a/crates/eframe/src/native/winit_integration.rs +++ b/crates/eframe/src/native/winit_integration.rs @@ -9,6 +9,14 @@ use egui::ViewportId; #[cfg(feature = "accesskit")] use egui_winit::accesskit_winit; +/// Returns `true` if the window is invisible or minimized. +/// +/// These windows don't receive `RedrawRequested` events on Windows, +/// so they need special handling to keep processing viewport commands. +pub fn is_invisible_or_minimized(window: &Window) -> bool { + window.is_visible() == Some(false) || window.is_minimized() == Some(true) +} + /// Create an egui context, restoring it from storage if possible. pub fn create_egui_context(storage: Option<&dyn crate::Storage>) -> egui::Context { profiling::function_scope!();