mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
App: a launch whose window never drew makes the next use OpenGL (#243)
On some older Intel integrated GPUs (UHD 600, UHD with 2020 drivers) the graphics driver crashes EffectCraft as its window's DirectX 12 or Vulkan device is created (exit code 0xC0000005): the process disappears with no message, and only WGPU_BACKEND=gl helped. A driver crash can't be caught, so each launch now leaves a marker in the settings folder until its window draws. A launch that finds the previous one's marker switches the new Settings ▸ Startup & Repair ▸ Window Graphics (Automatic / OpenGL) to OpenGL and says so in the status bar. A window that fails to open says the next launch will try OpenGL. WGPU_BACKEND still wins; macOS has no OpenGL backend, so it never switches there. Tests: a_launch_that_never_drew_switches_the_next_to_opengl (app), window_graphics_is_validated (engine). Checked end to end with a leftover marker: the app opened on OpenGL with the notice and cleared the marker. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
fdb655e78e
commit
d554724aa4
5 files changed
+180
-13
No files matched your search
@@ -0,0 +1,111 @@
|
||||
//! 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.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use effectcraft_engine::prefs::{PREFS_FILE, Prefs};
|
||||
use eframe::wgpu::Backends;
|
||||
|
||||
/// Left in the settings folder from a launch until its window draws.
|
||||
const MARKER: &str = "launch-pending";
|
||||
|
||||
/// 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.
|
||||
pub notice: Option<String>,
|
||||
marker: Option<PathBuf>,
|
||||
}
|
||||
|
||||
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.
|
||||
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);
|
||||
let mut prefs = std::fs::read_to_string(&prefs_path).map(|t| Prefs::from_json(&t)).unwrap_or_default();
|
||||
let marker = dir.join(MARKER);
|
||||
let mut notice = None;
|
||||
if marker.exists() && 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}"),
|
||||
}
|
||||
notice = Some(
|
||||
"EffectCraft's last launch stopped before 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(),
|
||||
);
|
||||
}
|
||||
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),
|
||||
Err(e) => {
|
||||
log::warn!("launch marker: {e}");
|
||||
None
|
||||
}
|
||||
};
|
||||
Launch { backends, notice, marker }
|
||||
}
|
||||
|
||||
/// The window drew its first frame: the launch made it.
|
||||
pub fn drawn(&mut self) {
|
||||
if let Some(m) = self.marker.take()
|
||||
&& let Err(e) = std::fs::remove_file(&m)
|
||||
{
|
||||
log::warn!("launch marker: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn window_graphics(dir: &Path) -> String {
|
||||
Prefs::from_json(&std::fs::read_to_string(dir.join(PREFS_FILE)).unwrap_or_default()).startup.window_graphics
|
||||
}
|
||||
|
||||
/// A launch that never drew switches the next one to OpenGL, which then stays (#243).
|
||||
#[test]
|
||||
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.
|
||||
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();
|
||||
assert!(!dir.join(MARKER).exists());
|
||||
// A launch that crashes before drawing (it never calls `drawn`)…
|
||||
let _crashed = 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") {
|
||||
assert_eq!((l.backends, l.notice.is_some()), (None, false), "no OpenGL on macOS");
|
||||
} else {
|
||||
assert_eq!(l.backends, Some(Backends::GL));
|
||||
assert!(l.notice.is_some());
|
||||
assert_eq!(window_graphics(&dir), "gl", "saved in Settings");
|
||||
l.drawn();
|
||||
let l = Launch::begin(Some(&dir), false);
|
||||
assert_eq!((l.backends, l.notice.is_some()), (Some(Backends::GL), false), "it stays, without the notice");
|
||||
}
|
||||
// `WGPU_BACKEND` wins, and leaves no marker.
|
||||
let _ = std::fs::remove_file(dir.join(MARKER));
|
||||
assert_eq!(Launch::begin(Some(&dir), true).backends, None);
|
||||
assert!(!dir.join(MARKER).exists());
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@
|
||||
mod appimage;
|
||||
mod audio_out;
|
||||
mod control_server;
|
||||
mod launch_guard;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod native_menu;
|
||||
|
||||
@@ -77,6 +78,11 @@ 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).
|
||||
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();
|
||||
let options = eframe::NativeOptions {
|
||||
viewport: egui::ViewportBuilder::default()
|
||||
.with_title("EffectCraft")
|
||||
@@ -89,7 +95,7 @@ fn main() -> eframe::Result {
|
||||
// Wayland matches the window to `ai.storyteller.effectcraft.desktop` by this ID.
|
||||
.with_app_id(appimage::APP_ID),
|
||||
event_loop_builder: agent_event_loop(control_port.is_some()),
|
||||
wgpu_options: wgpu_options(),
|
||||
wgpu_options: wgpu_options(launch.backends),
|
||||
..Default::default()
|
||||
};
|
||||
// Start-up milestones: with `RUST_LOG=info` they go to stderr, so a window that never
|
||||
@@ -155,6 +161,9 @@ fn main() -> eframe::Result {
|
||||
let mut app = EffectcraftApp::new(session);
|
||||
app.set_gpu_failure_bridge(gpu_failures);
|
||||
app.ui.start_screen = show_home;
|
||||
if let Some(n) = notice {
|
||||
app.ui.status = n;
|
||||
}
|
||||
if let Some(r) = recovery {
|
||||
app.offer_recovery(r);
|
||||
}
|
||||
@@ -195,13 +204,14 @@ fn main() -> eframe::Result {
|
||||
#[cfg(target_os = "macos")]
|
||||
menu: native_menu::NativeBar::new(&cc.egui_ctx),
|
||||
app,
|
||||
launch,
|
||||
drawn: false,
|
||||
}))
|
||||
}),
|
||||
);
|
||||
match &result {
|
||||
Ok(()) => log::info!("the window closed"),
|
||||
Err(e) => startup_failed(e),
|
||||
Err(e) => startup_failed(e, !on_gl && cfg!(not(target_os = "macos"))),
|
||||
}
|
||||
result
|
||||
}
|
||||
@@ -209,11 +219,14 @@ fn main() -> eframe::Result {
|
||||
/// The window's wgpu device asks for the adapter's own limits (textures up to 16384 px)
|
||||
/// instead of eframe's fixed set: an older GPU that can't meet those (Intel HD Graphics 5500,
|
||||
/// #198) still opens the app, and the compositor then checks what it needs and falls back to
|
||||
/// the CPU when the device can't run it.
|
||||
fn wgpu_options() -> eframe::WgpuConfiguration {
|
||||
/// the CPU when the device can't run it. `backends`: Window Graphics' choice, when it makes one.
|
||||
fn wgpu_options(backends: Option<eframe::wgpu::Backends>) -> eframe::WgpuConfiguration {
|
||||
use eframe::egui_wgpu::WgpuSetup;
|
||||
let mut config = eframe::WgpuConfiguration::default();
|
||||
if let WgpuSetup::CreateNew(setup) = &mut config.wgpu_setup {
|
||||
if let Some(b) = backends {
|
||||
setup.instance_descriptor.backends = b;
|
||||
}
|
||||
setup.device_descriptor = std::sync::Arc::new(|adapter| eframe::wgpu::DeviceDescriptor {
|
||||
label: Some("EffectCraft device"),
|
||||
required_limits: device_limits(adapter.limits()),
|
||||
@@ -230,10 +243,12 @@ fn device_limits(adapter: eframe::wgpu::Limits) -> eframe::wgpu::Limits {
|
||||
}
|
||||
|
||||
/// The window couldn't open (no usable graphics adapter or device): say so instead of quitting
|
||||
/// silently, since release builds on Windows have no console.
|
||||
fn startup_failed(e: &eframe::Error) {
|
||||
/// silently, since release builds on Windows have no console. `gl_next`: the launch guard has
|
||||
/// the next launch try OpenGL.
|
||||
fn startup_failed(e: &eframe::Error, gl_next: bool) {
|
||||
let next = if gl_next { "The next launch will try OpenGL instead. " } else { "" };
|
||||
let message = format!(
|
||||
"EffectCraft couldn't start its window: {e}\n\nUpdating the graphics driver often helps. If it keeps happening, please report it at https://github.com/storytold/effectcraft/issues with this message."
|
||||
"EffectCraft couldn't start its window: {e}\n\n{next}Updating the graphics driver often helps. If it keeps happening, please report it at https://github.com/storytold/effectcraft/issues with this message."
|
||||
);
|
||||
log::error!("{message}");
|
||||
eprintln!("effectcraft: {message}");
|
||||
@@ -250,6 +265,7 @@ struct Desktop {
|
||||
app: EffectcraftApp,
|
||||
#[cfg(target_os = "macos")]
|
||||
menu: native_menu::NativeBar,
|
||||
launch: launch_guard::Launch,
|
||||
/// The first frame was drawn (logged once).
|
||||
drawn: bool,
|
||||
}
|
||||
@@ -265,6 +281,7 @@ impl eframe::App for Desktop {
|
||||
self.app.ui(ui, frame);
|
||||
if !self.drawn {
|
||||
self.drawn = true;
|
||||
self.launch.drawn();
|
||||
log::info!("first frame drawn");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user