mirror of
https://github.com/emilk/egui.git
synced 2026-06-26 22:53:14 -04:00
Improve behavior of invisible windows (#7905)
## Summary * Closes #5229 * Closes #7776 On Windows, once a window is hidden with `ViewportCommand::Visible(false)`, two problems occur: 1. **Window can never be shown again** — Windows stops sending `RedrawRequested` events to invisible windows, and viewport commands are only processed during `run_ui_and_paint`, which is triggered by `RedrawRequested`. This creates a deadlock: ``` Visible(false) → window hidden → no RedrawRequested → run_ui_and_paint never called → Visible(true) stuck in queue → window stays hidden forever ``` 2. **High CPU usage** — The event loop spins at full speed with `ControlFlow::Poll` even for invisible windows, and repaint requests are scheduled immediately, causing a tight loop that burns CPU. ## Fix **For #5229:** In `check_redraw_requests`, after calling `window.request_redraw()`, detect invisible windows via `window.is_visible() == Some(false)` and call `run_ui_and_paint` directly for them. This ensures pending viewport commands (including `Visible(true)`) are still processed even when the OS doesn't send redraw events. **For #7776:** Three layers of throttling for invisible windows: - **Heartbeat scheduling:** After painting an invisible window, schedule the next repaint 100ms in the future (instead of immediately). This keeps viewport commands flowing while limiting to ~10 repaints/sec. - **Event throttling:** In `user_event`, throttle `RequestRepaint` events for invisible windows to at least 100ms delay, preventing egui's repaint callback from bypassing the heartbeat. - **ControlFlow fix:** Only set `ControlFlow::Poll` for visible windows. Invisible windows use `WaitUntil` instead of spinning. - **Backend sleep:** Add `is_visible() == Some(false)` alongside the existing `is_minimized()` sleep check in both wgpu and glow backends (defense in depth). The fix is platform-agnostic: `is_visible()` returns `Some(false)` only when the platform can confirm invisibility, so it won't trigger on platforms where invisible windows still receive `RedrawRequested`. ## Test plan - [x] `cargo fmt` passes - [x] `cargo clippy -p eframe --all-features` passes with no warnings - [x] Manual test on Windows: window reappears after `Visible(true)` when hidden - [x] Manual test on Windows: CPU stays near 0% while window is invisible (was ~16% before fix) --------- Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This commit is contained in:
@@ -34,7 +34,7 @@ use egui_winit::accesskit_winit;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
|
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
|
||||||
native::epi_integration::EpiIntegration,
|
native::{epi_integration::EpiIntegration, winit_integration::is_invisible_or_minimized},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
@@ -761,9 +761,11 @@ impl GlowWinitRunning<'_> {
|
|||||||
|
|
||||||
integration.maybe_autosave(app.as_mut(), Some(&window));
|
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:
|
// On Mac, a minimized Window uses up all CPU:
|
||||||
// https://github.com/emilk/egui/issues/325
|
// 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");
|
profiling::scope!("minimized_sleep");
|
||||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::time::Instant;
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use winit::{
|
use winit::{
|
||||||
application::ApplicationHandler,
|
application::ApplicationHandler,
|
||||||
@@ -11,9 +11,20 @@ use ahash::HashMap;
|
|||||||
use super::winit_integration::{UserEvent, WinitApp};
|
use super::winit_integration::{UserEvent, WinitApp};
|
||||||
use crate::{
|
use crate::{
|
||||||
Result, epi,
|
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 <https://github.com/emilk/egui/issues/7776>.
|
||||||
|
const INVISIBLE_WINDOW_REPAINT_INTERVAL: Duration = Duration::from_millis(100);
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
fn create_event_loop(native_options: &mut epi::NativeOptions) -> Result<EventLoop<UserEvent>> {
|
fn create_event_loop(native_options: &mut epi::NativeOptions) -> Result<EventLoop<UserEvent>> {
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
@@ -177,23 +188,54 @@ impl<T: WinitApp> WinitAppWrapper<T> {
|
|||||||
fn check_redraw_requests(&mut self, event_loop: &ActiveEventLoop) {
|
fn check_redraw_requests(&mut self, event_loop: &ActiveEventLoop) {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
|
|
||||||
|
let mut invisible_window_ids = Vec::new();
|
||||||
|
|
||||||
self.windows_next_repaint_times
|
self.windows_next_repaint_times
|
||||||
.retain(|window_id, repaint_time| {
|
.retain(|window_id, repaint_time| {
|
||||||
if now < *repaint_time {
|
if now < *repaint_time {
|
||||||
return true; // not yet ready
|
return true; // not yet ready
|
||||||
}
|
}
|
||||||
|
|
||||||
event_loop.set_control_flow(ControlFlow::Poll);
|
|
||||||
|
|
||||||
if let Some(window) = self.winit_app.window(*window_id) {
|
if let Some(window) = self.winit_app.window(*window_id) {
|
||||||
log::trace!("request_redraw for {window_id:?}");
|
// On Windows, invisible windows don't receive RedrawRequested
|
||||||
window.request_redraw();
|
// 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 {
|
} else {
|
||||||
log::trace!("No window found for {window_id:?}");
|
log::trace!("No window found for {window_id:?}");
|
||||||
}
|
}
|
||||||
false
|
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();
|
let next_repaint_time = self.windows_next_repaint_times.values().min().copied();
|
||||||
if let Some(next_repaint_time) = next_repaint_time {
|
if let Some(next_repaint_time) = next_repaint_time {
|
||||||
event_loop.set_control_flow(ControlFlow::WaitUntil(next_repaint_time));
|
event_loop.set_control_flow(ControlFlow::WaitUntil(next_repaint_time));
|
||||||
@@ -270,6 +312,16 @@ impl<T: WinitApp> ApplicationHandler<UserEvent> for WinitAppWrapper<T> {
|
|||||||
if let Some(window_id) =
|
if let Some(window_id) =
|
||||||
self.winit_app.window_id_from_viewport_id(viewport_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))
|
Ok(EventResult::RepaintAt(window_id, when))
|
||||||
} else {
|
} else {
|
||||||
Ok(EventResult::Wait)
|
Ok(EventResult::Wait)
|
||||||
|
|||||||
@@ -27,7 +27,10 @@ use winit_integration::UserEvent;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
|
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};
|
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()));
|
integration.maybe_autosave(app.as_mut(), window.map(|w| w.as_ref()));
|
||||||
|
|
||||||
if let Some(window) = window
|
if let Some(window) = window
|
||||||
&& window.is_minimized() == Some(true)
|
&& is_invisible_or_minimized(window)
|
||||||
{
|
{
|
||||||
// On Mac, a minimized Window uses up all CPU:
|
// On Mac, a minimized Window uses up all CPU:
|
||||||
// https://github.com/emilk/egui/issues/325
|
// 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");
|
profiling::scope!("minimized_sleep");
|
||||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,14 @@ use egui::ViewportId;
|
|||||||
#[cfg(feature = "accesskit")]
|
#[cfg(feature = "accesskit")]
|
||||||
use egui_winit::accesskit_winit;
|
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.
|
/// Create an egui context, restoring it from storage if possible.
|
||||||
pub fn create_egui_context(storage: Option<&dyn crate::Storage>) -> egui::Context {
|
pub fn create_egui_context(storage: Option<&dyn crate::Storage>) -> egui::Context {
|
||||||
profiling::function_scope!();
|
profiling::function_scope!();
|
||||||
|
|||||||
Reference in New Issue
Block a user