diff --git a/apps/effectcraft/src/launch_guard.rs b/apps/effectcraft/src/launch_guard.rs index 14c4c440..fac91c05 100644 --- a/apps/effectcraft/src/launch_guard.rs +++ b/apps/effectcraft/src/launch_guard.rs @@ -1,39 +1,52 @@ //! Settings ▸ Startup & Repair ▸ Window Graphics, and the launch guard behind it. //! //! Some graphics drivers crash EffectCraft as its window's device is created (an access -//! violation in an older Intel UHD driver with DirectX 12 / Vulkan, #243). A crash in a driver -//! can't be caught, so each launch leaves a marker in the settings folder until its window has -//! drawn a frame. A launch that finds the previous one's marker switches Window Graphics to -//! OpenGL, which those drivers run, and says so once the window is up. `WGPU_BACKEND` (wgpu's -//! own override) still wins over both. The marker is locked while its launch runs, so a second -//! copy opened meanwhile isn't taken for a crash; and a window OpenGL can't open either puts -//! Window Graphics back to Automatic ([`opengl_failed`]) rather than failing every launch. +//! violation in an older Intel UHD driver with DirectX 12 / Vulkan, #243), or a moment after +//! its first frame, as the window is shown and its surface re-created (AMD's Vulkan driver for +//! a Radeon RX 6800M, Adrenalin 26.9.2: an access violation in amdvlk64.dll on every launch). +//! A crash in a driver can't be caught, so each launch leaves a marker in the settings folder +//! until its window has kept drawing for [`SETTLE`] (or the app quits normally before then). A +//! launch that finds the previous one's marker switches Window Graphics to OpenGL, which those +//! drivers run, and says so once the window is up. `WGPU_BACKEND` (wgpu's own override) still +//! wins over both. The marker is locked while its launch runs, so a second copy opened meanwhile +//! isn't taken for a crash; and a window OpenGL can't open either puts Window Graphics back to +//! Automatic ([`opengl_failed`]) rather than failing every launch. use std::fs::File; use std::path::{Path, PathBuf}; +use std::time::{Duration, Instant}; use effectcraft_engine::prefs::{PREFS_FILE, Prefs}; use eframe::wgpu::Backends; -/// Left in the settings folder from a launch until its window draws. +/// Left in the settings folder from a launch until its window has settled. const MARKER: &str = "launch-pending"; +/// How long the window keeps drawing after its first frame before the launch counts as made. +/// A driver that fails as the window is shown and its surface re-created does so within the +/// first second; a user quitting sooner than this is told the next launch uses OpenGL and can +/// switch back in Settings. +pub const SETTLE: Duration = Duration::from_secs(3); + /// This launch's window graphics. #[derive(Debug, Default)] pub struct Launch { /// The graphics APIs the window may use (`None`: eframe's default). pub backends: Option, - /// Switched to OpenGL because the previous launch never drew: the message to show. + /// Switched to OpenGL because the previous launch stopped as its window opened: the message + /// to show. pub notice: Option, marker: Option, - /// Holds the marker's lock until the window draws (or the process ends). + /// Holds the marker's lock until the launch settles (or the process ends). lock: Option, + /// When the window drew its first frame, until the launch settles. + first_frame: Option, } impl Launch { /// Read Window Graphics in the settings folder `dir`, switch it to OpenGL when the previous - /// launch never drew (where OpenGL exists: not macOS), and leave this launch's marker. - /// `env_override`: `WGPU_BACKEND` is set. + /// launch stopped as its window opened (where OpenGL exists: not macOS), and leave this + /// launch's marker. `env_override`: `WGPU_BACKEND` is set. pub fn begin(dir: Option<&Path>, env_override: bool) -> Launch { let Some(dir) = dir.filter(|_| !env_override) else { return Launch::default() }; let prefs_path = dir.join(PREFS_FILE); @@ -45,11 +58,11 @@ impl Launch { if marker.exists() && !held && prefs.startup.window_graphics == "auto" && cfg!(not(target_os = "macos")) { prefs.startup.window_graphics = "gl".into(); match std::fs::create_dir_all(dir).and_then(|()| std::fs::write(&prefs_path, prefs.to_json())) { - Ok(()) => log::warn!("the last launch never drew its window: switching Window Graphics to OpenGL"), - Err(e) => log::warn!("the last launch never drew its window; saving Window Graphics failed: {e}"), + Ok(()) => log::warn!("the last launch stopped as its window opened: switching Window Graphics to OpenGL"), + Err(e) => log::warn!("the last launch stopped as its window opened; saving Window Graphics failed: {e}"), } notice = Some( - "EffectCraft's last launch stopped before its window opened, so it now draws with OpenGL \ + "EffectCraft's last launch stopped as its window opened, so it now draws with OpenGL \ (Settings ▸ Startup & Repair ▸ Window Graphics). Updating the graphics driver may let it use \ Automatic again." .to_string(), @@ -64,11 +77,33 @@ impl Launch { (None, None) } }; - Launch { backends, notice, marker, lock } + Launch { backends, notice, marker, lock, first_frame: None } } - /// The window drew its first frame: the launch made it. - pub fn drawn(&mut self) { + /// The window drew a frame. Until the launch has settled, returns how long that still takes + /// (ask for a repaint then, so that an idle window settles too). + pub fn frame_drawn(&mut self) -> Option { + self.frame_drawn_at(Instant::now()) + } + + fn frame_drawn_at(&mut self, now: Instant) -> Option { + self.marker.as_ref()?; + let first = *self.first_frame.get_or_insert(now); + let left = SETTLE.saturating_sub(now.saturating_duration_since(first)); + if left.is_zero() { + self.settled(); + return None; + } + Some(left) + } + + /// The app quits normally: however soon after opening, that's no crash. + pub fn exited(&mut self) { + self.settled(); + } + + /// The launch made it: release and remove its marker. + fn settled(&mut self) { self.lock = None; if let Some(m) = self.marker.take() && let Err(e) = std::fs::remove_file(&m) @@ -103,12 +138,18 @@ mod tests { fn a_launch_that_never_drew_switches_the_next_to_opengl() { let dir = std::env::temp_dir().join(format!("ec-launch-guard-{}", std::process::id())); let _ = std::fs::remove_dir_all(&dir); - // A first launch that draws: nothing changes. + // A first launch that draws: nothing changes. Its first frames don't settle it (a driver + // may still crash as the window is shown); frames for `SETTLE` do. let mut l = Launch::begin(Some(&dir), false); assert_eq!((l.backends, l.notice.is_some()), (None, false)); assert!(dir.join(MARKER).exists()); - l.drawn(); + let t0 = Instant::now(); + assert_eq!(l.frame_drawn_at(t0), Some(SETTLE)); + assert!(l.frame_drawn_at(t0 + SETTLE / 2).is_some_and(|left| left <= SETTLE / 2)); + assert!(dir.join(MARKER).exists()); + assert_eq!(l.frame_drawn_at(t0 + SETTLE), None); assert!(!dir.join(MARKER).exists()); + assert_eq!(l.frame_drawn_at(t0 + SETTLE), None, "settled stays settled"); // A launch that crashes before drawing (it never calls `drawn`)… drop(Launch::begin(Some(&dir), false)); // …makes the next one use OpenGL and say so, once. @@ -119,7 +160,7 @@ mod tests { assert_eq!(l.backends, Some(Backends::GL)); assert!(l.notice.is_some()); assert_eq!(window_graphics(&dir), "gl", "saved in Settings"); - l.drawn(); + l.exited(); let l = Launch::begin(Some(&dir), false); assert_eq!((l.backends, l.notice.is_some()), (Some(Backends::GL), false), "it stays, without the notice"); } @@ -130,6 +171,34 @@ mod tests { let _ = std::fs::remove_dir_all(&dir); } + /// A launch that stops moments after its first frame (AMD's Vulkan driver crashing as the + /// window is shown and its surface re-created) switches the next one to OpenGL too; a + /// normal quit in that time doesn't. + #[test] + fn a_launch_that_stops_after_its_first_frame_switches_the_next_to_opengl() { + let dir = std::env::temp_dir().join(format!("ec-launch-guard-3-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + let mut l = Launch::begin(Some(&dir), false); + assert!(l.frame_drawn().is_some()); + drop(l); + let l = Launch::begin(Some(&dir), false); + if cfg!(target_os = "macos") { + assert_eq!(l.backends, None, "no OpenGL on macOS"); + } else { + assert_eq!(l.backends, Some(Backends::GL)); + assert!(l.notice.is_some()); + } + drop(l); + let _ = std::fs::remove_dir_all(&dir); + // Quitting right after the window opened leaves no marker. + let mut l = Launch::begin(Some(&dir), false); + assert!(l.frame_drawn().is_some()); + l.exited(); + assert!(!dir.join(MARKER).exists()); + assert_eq!(Launch::begin(Some(&dir), false).backends, None); + let _ = std::fs::remove_dir_all(&dir); + } + /// A second copy opened while the first is still opening isn't a crash; OpenGL that can't /// open a window either goes back to Automatic instead of failing every launch. #[test] diff --git a/apps/effectcraft/src/main.rs b/apps/effectcraft/src/main.rs index a7b24f3e..70457a47 100644 --- a/apps/effectcraft/src/main.rs +++ b/apps/effectcraft/src/main.rs @@ -97,8 +97,8 @@ fn main() -> eframe::Result { // Wayland shows the window's icon from its desktop entry: an AppImage brings its own. #[cfg(target_os = "linux")] appimage::integrate_from_env(ICON_PNG); - // Settings ▸ Startup & Repair ▸ Window Graphics, switched to OpenGL when the last launch's - // window never drew (a crashing graphics driver, #243). + // Settings ▸ Startup & Repair ▸ Window Graphics, switched to OpenGL when the last launch + // stopped as its window opened (a crashing graphics driver, #243). let mut launch = launch_guard::Launch::begin(effectcraft_host::config_dir().as_deref(), std::env::var_os("WGPU_BACKEND").is_some()); let on_gl = launch.backends == Some(eframe::wgpu::Backends::GL); let notice = launch.notice.take(); @@ -327,12 +327,17 @@ impl eframe::App for Desktop { self.app.ui(ui, frame); if !self.drawn { self.drawn = true; - self.launch.drawn(); log::info!("first frame drawn"); } + // The launch guard's marker goes once the window has kept drawing for a few seconds: a + // driver may still crash as the window is shown and its surface re-created. + if let Some(left) = self.launch.frame_drawn() { + ui.ctx().request_repaint_after(left); + } } fn on_exit(&mut self) { + self.launch.exited(); self.app.on_exit(); } diff --git a/crates/engine/src/prefs.rs b/crates/engine/src/prefs.rs index 0bfa04a5..83f0498b 100644 --- a/crates/engine/src/prefs.rs +++ b/crates/engine/src/prefs.rs @@ -121,8 +121,8 @@ page!(Startup { /// After a crash, offer to open the most recent auto-save. offer_crash_recovery: bool = true, /// What the desktop window draws with, from the next launch: `auto` (the platform's best - /// graphics API) or `gl` (OpenGL, for drivers that crash with the others). A launch whose - /// window never drew switches it to `gl` (#243). + /// graphics API) or `gl` (OpenGL, for drivers that crash with the others). A launch that + /// stops as its window opens switches it to `gl` (#243). window_graphics: String = "auto".into(), }); @@ -1081,7 +1081,7 @@ pub fn pages() -> Vec { s("startup.offerCrashRecovery", "Offer to Open the Latest Auto-Save After a Crash", B, true), s("startup.windowGraphics", "Window Graphics", Kind::Choice(WINDOW_GRAPHICS), true), Note( - "Window Graphics applies from the next launch. When the graphics driver stops EffectCraft before its window draws, the next launch switches to OpenGL.", + "Window Graphics applies from the next launch. When the graphics driver stops EffectCraft as its window opens, the next launch switches to OpenGL.", ), Section("Repair"), Button { label: "Reset Settings", command: "prefs.reset", params: "{}" }, diff --git a/docs/preferences.md b/docs/preferences.md index 298ceb9d..157eca99 100644 --- a/docs/preferences.md +++ b/docs/preferences.md @@ -38,7 +38,7 @@ Commands (CLI, MCP, control channel): - `general.defaultSpatialLinear`: Default Spatial Interpolation to Linear - `startup.showHomeOnLaunch`: Show Home Screen When Launching - `startup.offerCrashRecovery`: Offer to Open the Latest Auto-Save After a Crash -- `startup.windowGraphics`: Window Graphics (`auto` / `gl`), what the desktop window draws with from the next launch. `auto` lets the platform pick (DirectX 12, Vulkan or Metal); `gl` uses OpenGL, with CPU compositing, for graphics drivers that crash with the others. A launch whose window never drew leaves a `launch-pending` marker in the settings folder, and the next launch switches to `gl` and says so (not on macOS, which has no OpenGL backend). `WGPU_BACKEND` overrides both. +- `startup.windowGraphics`: Window Graphics (`auto` / `gl`), what the desktop window draws with from the next launch. `auto` lets the platform pick (DirectX 12, Vulkan or Metal); `gl` uses OpenGL, with CPU compositing, for graphics drivers that crash with the others. A launch leaves a `launch-pending` marker in the settings folder until its window has been drawing for a few seconds (or the app quits normally before that); a launch that stops earlier (a graphics driver crashing as the window opens or as its surface is re-created) leaves the marker behind, and the next launch switches to `gl` and says so (not on macOS, which has no OpenGL backend). `WGPU_BACKEND` overrides both. - `project.useTemplate`: New Project Loads Template - `project.templatePath`: Template Project - `autoSave.enabled`: Automatically Save Projects