Merge pull request #589 from JeremyYao/fix/amd-vulkan-launch-guard

The two test-only conflicts keep main's versions, which already carry the
same fixes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
This commit is contained in:
Claude committed 2026-10-10 16:29:06 +00:00
commit 4de3354806
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
@@ -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();
}
+3 -3
View File
@@ -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<Page> {
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: "{}" },
+1 -1
View File
@@ -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