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:
bflatasticandClaude Opus 5.5 committed 2026-10-08 13:57:21 +07:00
1 parent fdb655e78e
commit d554724aa4
5 files changed
+180 -13

No files matched your search

+111
View File
@@ -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);
}
}
+24 -7
View File
@@ -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");
}
}
+31 -6
View File
@@ -25,8 +25,20 @@ pub const PREFS_FILE: &str = "prefs.json";
/// Settings ▸ General ▸ Language: (label, `general.language` value).
pub const LANGUAGES: &[(&str, &str)] = &[("Match System", "system"), ("English", "en"), ("日本語", "ja")];
fn is_language(v: &str) -> bool {
LANGUAGES.iter().any(|(_, l)| *l == v)
/// Settings ▸ Startup & Repair ▸ Window Graphics: (label, `startup.windowGraphics` value).
pub const WINDOW_GRAPHICS: &[(&str, &str)] = &[("Automatic", "auto"), ("OpenGL (compatibility)", "gl")];
/// The settings whose value must be one of their choices.
fn choices(key: &str) -> Option<&'static [(&'static str, &'static str)]> {
match key {
"general.language" => Some(LANGUAGES),
"startup.windowGraphics" => Some(WINDOW_GRAPHICS),
_ => None,
}
}
fn is_choice(choices: &[(&str, &str)], v: &str) -> bool {
choices.iter().any(|(_, c)| *c == v)
}
/// Unknown keys inside a page, kept so a newer version's settings survive a round trip.
@@ -79,6 +91,10 @@ page!(Startup {
show_home_on_open_project: bool = false,
/// 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).
window_graphics: String = "auto".into(),
});
page!(ProjectPrefs {
@@ -424,8 +440,11 @@ impl Prefs {
/// Clamp values into their valid ranges and fill missing labels.
pub fn normalize(&mut self) {
self.version = PREFS_VERSION;
if !is_choice(WINDOW_GRAPHICS, &self.startup.window_graphics) {
self.startup.window_graphics = Startup::default().window_graphics;
}
let g = &mut self.general;
if !is_language(&g.language) {
if !is_choice(LANGUAGES, &g.language) {
g.language = General::default().language;
}
g.undo_levels = g.undo_levels.clamp(1, 99);
@@ -466,9 +485,11 @@ impl Prefs {
/// Set the value at a dotted key. The key must exist and the value must have its type.
pub fn set(&mut self, key: &str, value: Value) -> Result<(), String> {
if key == "general.language" && !value.as_str().is_some_and(is_language) {
let all: Vec<&str> = LANGUAGES.iter().map(|(_, l)| *l).collect();
return Err(format!("`general.language` expects one of {}", all.join(", ")));
if let Some(c) = choices(key)
&& !value.as_str().is_some_and(|v| is_choice(c, v))
{
let all: Vec<&str> = c.iter().map(|(_, v)| *v).collect();
return Err(format!("`{key}` expects one of {}", all.join(", ")));
}
let mut v = serde_json::to_value(&*self).map_err(|e| e.to_string())?;
let ptr = format!("/{}", key.replace('.', "/"));
@@ -918,6 +939,10 @@ pub fn pages() -> Vec<Page> {
s("startup.showHomeOnLaunch", "Show Home Screen When Launching", B, true),
s("startup.showHomeOnOpenProject", "Show Home Screen When Opening a Project", B, true),
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.",
),
Section("Repair"),
Button { label: "Reset Settings", command: "prefs.reset", params: "{}" },
Button { label: "Reset Keyboard Shortcuts", command: "shortcuts.reset", params: "{}" },
+13
View File
@@ -487,6 +487,19 @@ fn memory_tick_never_waits_for_the_system() {
}
}
/// Settings ▸ Startup & Repair ▸ Window Graphics takes `auto` or `gl` (#243).
#[test]
fn window_graphics_is_validated() {
let mut s = Session::default();
assert_eq!(s.prefs.startup.window_graphics, "auto");
s.execute("prefs.set", json!({"key": "startup.windowGraphics", "value": "gl"})).unwrap();
for bad in [json!("vulkan"), json!(true)] {
assert!(s.execute("prefs.set", json!({"key": "startup.windowGraphics", "value": bad})).is_err());
}
assert_eq!(s.prefs.startup.window_graphics, "gl");
assert_eq!(Prefs::from_json(r#"{"startup":{"windowGraphics":"dx9"}}"#).startup.window_graphics, "auto");
}
#[test]
fn interface_language_is_validated_persisted_and_backward_compatible() {
let store = Arc::new(MemoryConfig::default());
+1
View File
@@ -38,6 +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.
- `project.useTemplate`: New Project Loads Template
- `project.templatePath`: Template Project
- `autoSave.enabled`: Automatically Save Projects