Startup: a launch that stops as its window is shown switches the next to OpenGL

On an ASUS G513QY (Radeon RX 6800M, Adrenalin 26.9.2, Windows 11) every
launch died with STATUS_ACCESS_VIOLATION inside amdvlk64.dll, on a
driver worker thread, a moment after the first frame: eframe shows the
window it created hidden, the resulting resize re-creates the swapchain,
and the driver crashes there (same offset on two driver versions, with
the OBS/Steam/Epic Vulkan layers disabled, and with FIFO, immediate and
latency-1 presentation). DirectX 12 and OpenGL open the window fine.

A crash in a driver can't be caught, and the launch guard (#243) removed
its marker at the first frame, so this looked like a successful launch
every time and Window Graphics never switched. The marker now stays
until the window has kept drawing for SETTLE (3 s) or the app quits
normally before that; frame_drawn returns the time left and the app
asks for a repaint then, so an idle window settles too. The notice,
the Settings note and docs/preferences.md describe the new rule.

Verified on that machine with a scratch settings folder: launch 1
(Vulkan) crashed and left the marker, launch 2 switched to OpenGL with
the notice and closed with exit code 0, launch 3 stayed on OpenGL
without the notice.

Tests: effectcraft launch_guard
a_launch_that_stops_after_its_first_frame_switches_the_next_to_opengl
(a first frame then drop: the next launch gets GL and the notice; a
normal quit in that time leaves no marker), and the existing test now
asserts that the first frames don't settle a launch (marker present at
SETTLE/2, gone at SETTLE).

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
Jeremy YaoandClaude Fable 5.1 committed 2026-10-10 09:36:59 -04:00
1 parent 813c7c4650
commit d86c4aff52
4 files changed
+102 -28

No files matched your search

+90 -21
View File
@@ -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<Backends>,
/// 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<String>,
marker: Option<PathBuf>,
/// 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<File>,
/// When the window drew its first frame, until the launch settles.
first_frame: Option<Instant>,
}
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<Duration> {
self.frame_drawn_at(Instant::now())
}
fn frame_drawn_at(&mut self, now: Instant) -> Option<Duration> {
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]
+8 -3
View File
@@ -78,8 +78,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();
@@ -290,12 +290,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();
}