diff --git a/apps/effectcraft/src/launch_guard.rs b/apps/effectcraft/src/launch_guard.rs index f80c066f..5636f988 100644 --- a/apps/effectcraft/src/launch_guard.rs +++ b/apps/effectcraft/src/launch_guard.rs @@ -5,8 +5,11 @@ //! 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. +//! 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 effectcraft_engine::prefs::{PREFS_FILE, Prefs}; @@ -23,6 +26,8 @@ pub struct Launch { /// Switched to OpenGL because the previous launch never drew: the message to show. pub notice: Option, marker: Option, + /// Holds the marker's lock until the window draws (or the process ends). + lock: Option, } impl Launch { @@ -34,8 +39,10 @@ impl Launch { let prefs_path = dir.join(PREFS_FILE); let mut prefs = std::fs::read_to_string(&prefs_path).map(|t| Prefs::from_json(&t)).unwrap_or_default(); let marker = dir.join(MARKER); + // Locked by another launch that is still opening its window: not a crash. + let held = File::open(&marker).is_ok_and(|f| f.try_lock().is_err()); let mut notice = None; - if marker.exists() && prefs.startup.window_graphics == "auto" && cfg!(not(target_os = "macos")) { + 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"), @@ -49,18 +56,20 @@ impl Launch { ); } let backends = (prefs.startup.window_graphics == "gl").then_some(Backends::GL); - let marker = match std::fs::create_dir_all(dir).and_then(|()| std::fs::write(&marker, "")) { - Ok(()) => Some(marker), + let (marker, lock) = match std::fs::create_dir_all(dir).and_then(|()| File::create(&marker)) { + // Where locks aren't available the guard works as before. + Ok(f) => (Some(marker), f.try_lock().is_ok().then_some(f)), Err(e) => { log::warn!("launch marker: {e}"); - None + (None, None) } }; - Launch { backends, notice, marker } + Launch { backends, notice, marker, lock } } /// The window drew its first frame: the launch made it. pub fn drawn(&mut self) { + self.lock = None; if let Some(m) = self.marker.take() && let Err(e) = std::fs::remove_file(&m) { @@ -69,6 +78,18 @@ impl Launch { } } +/// The window couldn't open with OpenGL (an error, not a crash): back to Automatic, and no marker. +pub fn opengl_failed(dir: Option<&Path>) { + let Some(dir) = dir else { return }; + let prefs_path = dir.join(PREFS_FILE); + let mut prefs = std::fs::read_to_string(&prefs_path).map(|t| Prefs::from_json(&t)).unwrap_or_default(); + prefs.startup.window_graphics = "auto".into(); + if let Err(e) = std::fs::write(&prefs_path, prefs.to_json()) { + log::warn!("saving Window Graphics failed: {e}"); + } + let _ = std::fs::remove_file(dir.join(MARKER)); +} + #[cfg(test)] mod tests { use super::*; @@ -89,7 +110,7 @@ mod tests { l.drawn(); assert!(!dir.join(MARKER).exists()); // A launch that crashes before drawing (it never calls `drawn`)… - let _crashed = Launch::begin(Some(&dir), false); + drop(Launch::begin(Some(&dir), false)); // …makes the next one use OpenGL and say so, once. let mut l = Launch::begin(Some(&dir), false); if cfg!(target_os = "macos") { @@ -108,4 +129,23 @@ mod tests { assert!(!dir.join(MARKER).exists()); 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] + fn a_second_copy_or_failed_opengl_doesnt_strand_window_graphics() { + let dir = std::env::temp_dir().join(format!("ec-launch-guard-2-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + let _first = Launch::begin(Some(&dir), false); + assert_eq!(Launch::begin(Some(&dir), false).backends, None); + assert_eq!(window_graphics(&dir), "auto"); + drop(_first); + if cfg!(not(target_os = "macos")) { + assert_eq!(Launch::begin(Some(&dir), false).backends, Some(Backends::GL), "a real crash still switches"); + opengl_failed(Some(&dir)); + assert_eq!(window_graphics(&dir), "auto"); + assert_eq!(Launch::begin(Some(&dir), false).backends, None); + } + let _ = std::fs::remove_dir_all(&dir); + } } diff --git a/apps/effectcraft/src/main.rs b/apps/effectcraft/src/main.rs index 0a034f19..1d62289c 100644 --- a/apps/effectcraft/src/main.rs +++ b/apps/effectcraft/src/main.rs @@ -211,7 +211,12 @@ fn main() -> eframe::Result { ); match &result { Ok(()) => log::info!("the window closed"), - Err(e) => startup_failed(e, !on_gl && cfg!(not(target_os = "macos"))), + Err(e) => { + if on_gl { + launch_guard::opengl_failed(effectcraft_host::config_dir().as_deref()); + } + startup_failed(e, !on_gl && cfg!(not(target_os = "macos"))); + } } result }