Skip to main content

rightkit_gpu/
backend.rs

1//! Which wgpu backends an app lets wgpu use, for its compute device and for its window, and the
2//! crash sentinel around device creation (issue #136). Adapted from Ember `crates/gpu`.
3//!
4//! wgpu loads the system driver of *every* backend in the instance's set while it enumerates
5//! adapters, even when it then picks another one: with Vulkan in the set, a broken Vulkan driver
6//! can crash the process (an access violation inside the driver DLL, which nothing can catch)
7//! although DX12 would have been used. So Windows defaults to DX12 alone. macOS uses Metal, Linux
8//! Vulkan (plus GL for a window when the build has it).
9//!
10//! Overrides, read from the environment each call: the app's own variable (its name is passed as
11//! `app_var`, for example `EMBER_GPU_BACKEND`) = `dx12` | `vulkan` | `metal` | `gl` (comma lists
12//! allowed), `auto` (the platform default), or `off` (no GPU compute; the window still needs one
13//! backend and keeps the platform default); else `WGPU_BACKEND` (wgpu's own variable, same names)
14//! for both the window and compute.
15//!
16//! Backends this build doesn't contain (for example `gl`) are ignored with a warning and the
17//! platform default is used.
18
19use std::path::PathBuf;
20use std::sync::Mutex;
21
22pub use wgpu::Backends;
23
24/// What the environment asks for.
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub enum BackendChoice {
27    /// The platform default.
28    Auto,
29    /// No GPU compute.
30    Off,
31    /// These backends.
32    Use(Backends),
33}
34
35/// Parse a backend list (`"dx12"`, `"vulkan,gl"`, `"auto"`, `"off"`…). `None`: nothing usable in
36/// it (empty or unknown names).
37pub fn parse_backends(s: &str) -> Option<BackendChoice> {
38    let s = s.trim().to_ascii_lowercase();
39    match s.as_str() {
40        "" => None,
41        "auto" | "default" => Some(BackendChoice::Auto),
42        "off" | "none" | "cpu" | "0" | "false" | "no" => Some(BackendChoice::Off),
43        // the window's OpenGL renderer (see `WindowRenderer`): compute keeps its default
44        "glow" => Some(BackendChoice::Auto),
45        _ => {
46            let b = Backends::from_comma_list(&s);
47            (!b.is_empty()).then_some(BackendChoice::Use(b))
48        }
49    }
50}
51
52/// The choice from the app's variable `app_var` (value `app`) or else `WGPU_BACKEND` (`wgpu`): the
53/// first one that parses wins; an unknown value is skipped (with a warning).
54pub fn choose(app_var: &str, app: Option<&str>, wgpu: Option<&str>) -> BackendChoice {
55    for (name, v) in [(app_var, app), ("WGPU_BACKEND", wgpu)] {
56        let Some(v) = v else { continue };
57        match parse_backends(v) {
58            // WGPU_BACKEND is wgpu's variable: it selects backends, it never turns the GPU off
59            Some(BackendChoice::Off) if name == "WGPU_BACKEND" => {
60                log::warn!("gpu: WGPU_BACKEND={v} ignored (use {app_var}=off)")
61            }
62            Some(c) => return c,
63            None => log::warn!("gpu: {name}={v} names no known backend; ignored"),
64        }
65    }
66    BackendChoice::Auto
67}
68
69/// The backends compute devices use when nothing is asked for on `os` (`std::env::consts::OS`).
70pub fn default_compute_backends(os: &str) -> Backends {
71    match os {
72        // never touch the Vulkan driver unless asked: see the module docs (issue #136)
73        "windows" => Backends::DX12,
74        "macos" | "ios" => Backends::METAL,
75        _ => Backends::PRIMARY,
76    }
77}
78
79/// The backends the window uses when nothing is asked for on `os`.
80pub fn default_window_backends(os: &str) -> Backends {
81    match os {
82        "windows" => Backends::DX12,
83        "macos" | "ios" => Backends::METAL,
84        // eframe's default (GL only when the build has it)
85        _ => Backends::PRIMARY | Backends::GL,
86    }
87}
88
89/// Resolve `choice` against the backends this build contains (`compiled`): `None` = GPU off.
90/// Asked-for backends that aren't compiled in fall back to `default`.
91pub fn resolve(choice: BackendChoice, default: Backends, compiled: Backends) -> Option<Backends> {
92    match choice {
93        BackendChoice::Off => None,
94        BackendChoice::Auto => Some(default),
95        BackendChoice::Use(b) if b.intersects(compiled) => Some(b & compiled),
96        BackendChoice::Use(b) => {
97            log::warn!("gpu: backend {b:?} is not in this build ({compiled:?}); using {default:?}");
98            Some(default)
99        }
100    }
101}
102
103fn env_choice(app_var: &str) -> BackendChoice {
104    let app = std::env::var(app_var).ok();
105    let wgpu = std::env::var("WGPU_BACKEND").ok();
106    choose(app_var, app.as_deref(), wgpu.as_deref())
107}
108
109/// The backends of the compute device (`None`: the app's variable is `off`).
110pub fn compute_backends(app_var: &str) -> Option<Backends> {
111    resolve(
112        env_choice(app_var),
113        default_compute_backends(std::env::consts::OS),
114        wgpu::Instance::enabled_backend_features(),
115    )
116}
117
118/// The backends for a desktop window's renderer. The window always needs one: `off` keeps the
119/// platform default.
120pub fn window_backends(app_var: &str) -> Backends {
121    let default = default_window_backends(std::env::consts::OS);
122    let choice = match env_choice(app_var) {
123        BackendChoice::Off => BackendChoice::Auto,
124        c => c,
125    };
126    resolve(choice, default, wgpu::Instance::enabled_backend_features()).unwrap_or(default)
127}
128
129/// The DX12 shader compiler: FXC (`d3dcompiler_47.dll`, part of Windows) unless
130/// `WGPU_DX12_COMPILER` (`wgpu`, wgpu's own variable: `fxc`, `dxc`, `auto`…) names another.
131///
132/// Issue #471: wgpu's default (`Auto`) loads whichever `dxcompiler.dll` the DLL search path finds
133/// first. An app ships none, so it is some other program's (an SDK's, a folder on `PATH`). A
134/// copy without its `dxil.dll` beside it warns that the DXIL is unsigned; wgpu takes the warning for
135/// a compile error, its own validation pipelines fail, the device is lost and the window never
136/// opens. FXC is always there and compiles what the apps' shaders need.
137pub fn dx12_compiler(env: Option<&str>) -> wgpu::Dx12Compiler {
138    let Some(v) = env else {
139        return wgpu::Dx12Compiler::Fxc;
140    };
141    v.parse().unwrap_or_else(|e| {
142        log::warn!("gpu: WGPU_DX12_COMPILER={v} ignored: {e}");
143        wgpu::Dx12Compiler::Fxc
144    })
145}
146
147/// wgpu's backend options for every instance an app creates — the window's and the compute
148/// devices': wgpu's environment variables, with the DX12 compiler from [`dx12_compiler`].
149pub fn backend_options() -> wgpu::BackendOptions {
150    let mut o = wgpu::BackendOptions::from_env_or_default();
151    o.dx12.shader_compiler = dx12_compiler(std::env::var("WGPU_DX12_COMPILER").ok().as_deref());
152    o
153}
154
155/// wgpu's instance flags for every instance an app creates, from `flags` (wgpu's defaults with its
156/// environment variables applied) without `VALIDATION_INDIRECT_CALL`, unless
157/// `WGPU_VALIDATION_INDIRECT_CALL` asks for it (any value but `0`). That validation builds an internal
158/// compute pipeline when each device is created; Intel Macs' Metal compiler fails on it ("Compiler
159/// encountered an internal error"), the device is lost and the window never opens (#250). It only
160/// guards indirect draws and dispatches, which the apps do not issue.
161pub fn instance_flags(flags: wgpu::InstanceFlags, env: Option<&str>) -> wgpu::InstanceFlags {
162    if env.is_some_and(|v| v != "0") {
163        flags | wgpu::InstanceFlags::VALIDATION_INDIRECT_CALL
164    } else {
165        flags - wgpu::InstanceFlags::VALIDATION_INDIRECT_CALL
166    }
167}
168
169/// [`instance_flags`] with `WGPU_VALIDATION_INDIRECT_CALL` read from the environment.
170pub fn instance_flags_from_env(flags: wgpu::InstanceFlags) -> wgpu::InstanceFlags {
171    instance_flags(
172        flags,
173        std::env::var("WGPU_VALIDATION_INDIRECT_CALL")
174            .ok()
175            .as_deref(),
176    )
177}
178
179/// Whether the app's variable `app_var` is `off`.
180pub fn env_off(app_var: &str) -> bool {
181    env_choice(app_var) == BackendChoice::Off
182}
183
184#[derive(Default)]
185struct MarkerState {
186    path: Option<PathBuf>,
187    startup_active: bool,
188}
189
190/// One tracker per process.
191#[derive(Default)]
192struct MarkerTracker {
193    state: Mutex<MarkerState>,
194    active: Mutex<u32>,
195}
196
197static MARKER: MarkerTracker = MarkerTracker {
198    state: Mutex::new(MarkerState {
199        path: None,
200        startup_active: false,
201    }),
202    active: Mutex::new(0),
203};
204
205impl MarkerTracker {
206    fn set_init_marker(&self, path: Option<PathBuf>) {
207        let mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());
208        let old = state.path.clone();
209        let clear_old = path.is_none();
210        let same_path = old == path;
211        state.path = path;
212        if !same_path && state.startup_active {
213            if let Some(old) = old.as_deref() {
214                disarm(old, &self.active);
215            }
216            state.startup_active = false;
217        }
218        drop(state);
219        if clear_old && let Some(old) = old {
220            let _ = std::fs::remove_file(old);
221        }
222    }
223
224    fn begin_startup_marker(&self, backends: Backends) -> bool {
225        let mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());
226        let Some(path) = state.path.as_deref() else {
227            return false;
228        };
229        if !state.startup_active {
230            arm(
231                path,
232                &self.active,
233                &format!("startup (backends {backends:?})"),
234            );
235            state.startup_active = true;
236        }
237        true
238    }
239
240    fn startup_succeeded(&self) {
241        let mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());
242        if state.startup_active {
243            if let Some(path) = state.path.as_deref() {
244                disarm(path, &self.active);
245            }
246            state.startup_active = false;
247        }
248    }
249
250    fn with_marker<T>(&self, what: &str, f: impl FnOnce() -> T) -> T {
251        let marker = self
252            .state
253            .lock()
254            .unwrap_or_else(|e| e.into_inner())
255            .path
256            .clone();
257        if let Some(m) = &marker {
258            arm(m, &self.active, what);
259        }
260        let _armed = Armed {
261            marker,
262            active: &self.active,
263        };
264        f()
265    }
266}
267
268/// Configure the marker used around compute device creation and native startup. `None` (the
269/// default) writes nothing.
270pub fn set_init_marker(path: Option<PathBuf>) {
271    MARKER.set_init_marker(path);
272}
273
274/// Begin guarding native window startup and its first frame with the same marker used by GPU
275/// device creation. A process terminated by a driver while this is active leaves the marker for
276/// the next launch to recover from. Returns whether a marker path is configured.
277pub fn begin_startup_marker(backends: Backends) -> bool {
278    MARKER.begin_startup_marker(backends)
279}
280
281/// Finish a successful startup guard without clearing concurrent driver-work scopes.
282pub fn startup_succeeded() {
283    MARKER.startup_succeeded();
284}
285
286/// Finish a startup guard after a recoverable window creation error.
287pub fn startup_failed() {
288    startup_succeeded();
289}
290
291/// The marker's text: what started, and when.
292fn marker_text(what: &str, secs: u64) -> String {
293    format!("GPU {what} started (unix time {secs})\n")
294}
295
296/// Enter a driver-work scope on `marker`: the first one writes the file.
297fn arm(marker: &std::path::Path, active: &Mutex<u32>, what: &str) {
298    let mut n = active.lock().unwrap_or_else(|e| e.into_inner());
299    if *n == 0 {
300        if let Some(d) = marker.parent() {
301            let _ = std::fs::create_dir_all(d);
302        }
303        let secs = std::time::SystemTime::now()
304            .duration_since(std::time::UNIX_EPOCH)
305            .map(|d| d.as_secs())
306            .unwrap_or(0);
307        let _ = rightkit_fs::atomic_write(marker, marker_text(what, secs).as_bytes());
308    }
309    *n = n.saturating_add(1);
310}
311
312/// Leave a scope: the last one removes the file.
313fn disarm(marker: &std::path::Path, active: &Mutex<u32>) {
314    let mut n = active.lock().unwrap_or_else(|e| e.into_inner());
315    *n = n.saturating_sub(1);
316    if *n == 0 {
317        let _ = std::fs::remove_file(marker);
318    }
319}
320
321/// Removes the marker on the way out, a panic included (the process survived).
322struct Armed<'a> {
323    marker: Option<PathBuf>,
324    active: &'a Mutex<u32>,
325}
326
327impl Drop for Armed<'_> {
328    fn drop(&mut self) {
329        if let Some(m) = &self.marker {
330            disarm(m, self.active);
331        }
332    }
333}
334
335/// Run driver work `f` (`what`: "device creation", "kernel compilation"…) between writing and
336/// removing the init marker (if one is set).
337pub fn with_marker<T>(what: &str, f: impl FnOnce() -> T) -> T {
338    MARKER.with_marker(what, f)
339}
340
341/// Run device creation (and the kernel build that follows it) `f` under the init marker.
342pub fn with_init_marker<T>(backends: Backends, f: impl FnOnce() -> T) -> T {
343    with_marker(
344        &format!("device and kernel creation (backends {backends:?})"),
345        f,
346    )
347}
348
349/// If the init marker `path` is present — the last process died while creating the GPU device —
350/// remove it and return what it recorded.
351pub fn take_init_marker(path: &std::path::Path) -> Option<String> {
352    let text = read_init_marker(path)?;
353    let _ = std::fs::remove_file(path);
354    Some(text)
355}
356
357/// What the init marker `path` recorded, if it is present — without removing it. For sessions
358/// that must leave the disk as they found it (a memory-only session): GPU rendering
359/// still starts off, and the marker stays for the next ordinary launch to report and clear.
360pub fn read_init_marker(path: &std::path::Path) -> Option<String> {
361    let text = std::fs::read_to_string(path).ok()?;
362    Some(text.trim().to_string())
363}