mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
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:
1 parent
813c7c4650
commit
d86c4aff52
4 files changed
+102
-28
No files matched your search
@@ -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]
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user