Windows: never load Vulkan by default; OpenGL fallback when DirectX 12 can't start (#120)
Enumerating Vulkan adapters crashes inside Intel's igvk64.dll on some UHD Graphics drivers, before any Rust error handling runs. Since #22 the Windows instance is DirectX 12 only; this keeps Vulkan out of every default path and adds two fallbacks so DX12-only can't leave a PC without a window: - A DirectX 12 probe before eframe starts: no hardware adapter that meets eframe's limits -> OpenGL (plus DX12/WARP as last resort when WARP is all DX12 offers). - If run_native fails with a wgpu error before the app was created, start once more with the other of DX12/GL (eframe reuses its event loop). WGPU_BACKEND still overrides everything, and nothing else is tried then. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
9f896c1249
commit
da99015b11
|
|
@ -1427,6 +1427,7 @@ dependencies = [
|
|||
"image",
|
||||
"log",
|
||||
"muda",
|
||||
"pollster 1.0.1",
|
||||
"rfd",
|
||||
"serde_json",
|
||||
"winresource",
|
||||
|
|
|
|||
12
README.md
12
README.md
|
|
@ -230,8 +230,16 @@ $env:WGPU_BACKEND = "vulkan"
|
|||
Remove-Item Env:WGPU_BACKEND # restore the default for later launches
|
||||
```
|
||||
|
||||
An explicit `gl` override can bring the driver crash back on affected systems. The macOS, Linux
|
||||
and web backend defaults are unchanged.
|
||||
An explicit `gl` override can bring the driver crash back on affected systems, and `vulkan` can
|
||||
bring back a crash in some Intel UHD Graphics drivers (`igvk64.dll`, #120). The macOS, Linux and
|
||||
web backend defaults are unchanged.
|
||||
|
||||
**If the app is slow on an older Windows PC, or DirectX 12 can't start:** when DirectX 12 finds no
|
||||
graphics card with a driver for it, it only offers Microsoft's software renderer, which draws every
|
||||
frame on the processor. In that case GridCraft uses OpenGL instead, without setting anything; if
|
||||
the first renderer fails to start, it tries the other one once. The log file records the choice
|
||||
(lines starting with `graphics:`). `WGPU_BACKEND` still overrides it (and then nothing else is
|
||||
tried), e.g. `$env:WGPU_BACKEND = "gl"` or `"dx12"`.
|
||||
|
||||
### macOS
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,10 @@ arboard = { workspace = true }
|
|||
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies]
|
||||
arboard = { workspace = true, features = ["wayland-data-control"] }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
# Blocks on the start-up DirectX 12 adapter probe (`graphics.rs`); already in the tree via eframe.
|
||||
pollster = "1"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
muda = { version = "0.21", default-features = false }
|
||||
# Finder double-click, Open With, Dock drops and `open -a GridCraft file.xlsx` arrive as a
|
||||
|
|
|
|||
|
|
@ -1,47 +1,192 @@
|
|||
//! Windows graphics instance configuration, applied before wgpu can initialize any driver.
|
||||
//!
|
||||
//! Vulkan is never initialized unless `WGPU_BACKEND` asks for it: enumerating Vulkan adapters
|
||||
//! crashes inside Intel's `igvk64.dll` on some UHD Graphics drivers (#120), and that access
|
||||
//! violation happens in the driver, where no Rust error handling can catch it.
|
||||
|
||||
use eframe::{NativeOptions, egui_wgpu::WgpuSetup, wgpu::Backends};
|
||||
use eframe::{
|
||||
NativeOptions,
|
||||
egui_wgpu::WgpuSetup,
|
||||
wgpu::{Backends, DeviceType},
|
||||
};
|
||||
|
||||
/// Restrict the wgpu instance to DirectX 12 unless `backend_override` (normally
|
||||
/// `Backends::from_env()`, i.e. `WGPU_BACKEND`) names other backends.
|
||||
pub fn configure(options: &mut NativeOptions, backend_override: Option<Backends>) {
|
||||
if let WgpuSetup::CreateNew(create) = &mut options.wgpu_options.wgpu_setup {
|
||||
// eframe's default includes GL. Creating that backend can crash inside AMD's
|
||||
// atio6axx.dll before adapter selection or any Rust error handling runs, so the
|
||||
// window never appears. Choosing an adapter afterwards is too late: only leaving GL
|
||||
// out of the instance avoids it. This matches the verified WGPU_BACKEND=dx12
|
||||
// workaround without mutating process environment variables.
|
||||
create.instance_descriptor.backends = backend_override.unwrap_or(Backends::DX12);
|
||||
/// What the start-up probe learned about one DirectX 12 adapter.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Dx12Adapter {
|
||||
/// `Cpu` is WARP (Microsoft's software rasteriser): it works, but every frame is drawn on the
|
||||
/// processor, which makes the app lag on older machines.
|
||||
pub device_type: DeviceType,
|
||||
/// Whether the adapter offers the limits eframe will ask for when it creates the device.
|
||||
pub meets_limits: bool,
|
||||
}
|
||||
|
||||
/// The backends to initialize on Windows.
|
||||
///
|
||||
/// An explicit `backend_override` (normally `Backends::from_env()`, i.e. `WGPU_BACKEND`) always
|
||||
/// wins. Otherwise DirectX 12 only, as long as `dx12_adapters` (what a DirectX 12-only probe found)
|
||||
/// contains a usable hardware GPU. Without one, OpenGL is the way to the GPU: graphics chips whose
|
||||
/// driver has no DirectX 12 support still have an OpenGL driver, and GL can't reach the AMD driver
|
||||
/// crash described in [`configure_backends`], because an AMD GPU with a driver shows up as a
|
||||
/// DirectX 12 adapter and keeps us on DirectX 12. When DirectX 12 offers only WARP, it stays
|
||||
/// enabled next to GL as a last resort for machines without an OpenGL driver either (virtual
|
||||
/// machines): wgpu prefers any GPU over a software adapter, so it's only picked when GL has
|
||||
/// nothing better. Vulkan is never part of the default.
|
||||
pub fn choose_backends(backend_override: Option<Backends>, dx12_adapters: &[Dx12Adapter]) -> Backends {
|
||||
if let Some(backends) = backend_override {
|
||||
return backends;
|
||||
}
|
||||
let hardware = |a: &&Dx12Adapter| a.device_type != DeviceType::Cpu;
|
||||
if dx12_adapters.iter().filter(hardware).any(|a| a.meets_limits) {
|
||||
Backends::DX12
|
||||
} else if !dx12_adapters.is_empty() && !dx12_adapters.iter().any(|a| hardware(&a)) {
|
||||
Backends::DX12 | Backends::GL
|
||||
} else {
|
||||
// No DirectX 12 adapter at all, or only GPUs that can't run the app: GL alone, so a
|
||||
// DirectX 12 GPU that would fail device creation isn't picked over the OpenGL one.
|
||||
Backends::GL
|
||||
}
|
||||
}
|
||||
|
||||
/// The backends for a second start when the first one failed with a graphics error before the
|
||||
/// app was created (no adapter, no device, no surface): the other one of DirectX 12 and OpenGL.
|
||||
/// `None` when `WGPU_BACKEND` chose the backends (the user asked for exactly those) or the first
|
||||
/// start already tried both. Never Vulkan (#120).
|
||||
pub fn fallback_backends(backend_override: Option<Backends>, first: Backends) -> Option<Backends> {
|
||||
if backend_override.is_some() {
|
||||
return None;
|
||||
}
|
||||
let rest = (Backends::DX12 | Backends::GL).difference(first);
|
||||
(!rest.is_empty()).then_some(rest)
|
||||
}
|
||||
|
||||
/// Restricts the wgpu instance eframe creates to `backends`.
|
||||
pub fn configure_backends(options: &mut NativeOptions, backends: Backends) {
|
||||
if let WgpuSetup::CreateNew(create) = &mut options.wgpu_options.wgpu_setup {
|
||||
// eframe's default includes Vulkan and GL. Creating the GL backend can crash inside AMD's
|
||||
// atio6axx.dll, and enumerating Vulkan adapters inside Intel's igvk64.dll (#120), before
|
||||
// adapter selection or any Rust error handling runs, so the window never appears.
|
||||
// Choosing an adapter afterwards is too late: only leaving the backend out of the
|
||||
// instance avoids it. This matches the verified WGPU_BACKEND=dx12 workaround without
|
||||
// mutating process environment variables.
|
||||
create.instance_descriptor.backends = backends;
|
||||
}
|
||||
}
|
||||
|
||||
/// Chooses the backends ([`choose_backends`]), applies them to `options` and returns them.
|
||||
/// Without a `backend_override` this first probes DirectX 12 with a short-lived instance of its
|
||||
/// own, which is dropped before eframe creates the real one.
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn configure(options: &mut NativeOptions, backend_override: Option<Backends>) -> Backends {
|
||||
let backends = match backend_override {
|
||||
Some(backends) => {
|
||||
log::info!("graphics: WGPU_BACKEND override, backends {backends:?}");
|
||||
backends
|
||||
}
|
||||
None => {
|
||||
let adapters = probe_dx12(options);
|
||||
let backends = choose_backends(None, &adapters);
|
||||
log::info!("graphics: {} DirectX 12 adapter(s) {adapters:?}, backends {backends:?}", adapters.len());
|
||||
backends
|
||||
}
|
||||
};
|
||||
configure_backends(options, backends);
|
||||
backends
|
||||
}
|
||||
|
||||
/// Lists the DirectX 12 adapters, checking each against the limits eframe's device descriptor
|
||||
/// will require. Empty when this build has no DirectX 12 backend.
|
||||
#[cfg(target_os = "windows")]
|
||||
fn probe_dx12(options: &NativeOptions) -> Vec<Dx12Adapter> {
|
||||
use eframe::wgpu::{Instance, InstanceDescriptor};
|
||||
// `Instance::new` panics when the build has no backend for the platform.
|
||||
if !Instance::enabled_backend_features().contains(Backends::DX12) {
|
||||
return Vec::new();
|
||||
}
|
||||
let WgpuSetup::CreateNew(create) = &options.wgpu_options.wgpu_setup else {
|
||||
return Vec::new();
|
||||
};
|
||||
let instance = Instance::new(InstanceDescriptor { backends: Backends::DX12, ..InstanceDescriptor::new_without_display_handle() });
|
||||
pollster::block_on(instance.enumerate_adapters(Backends::DX12))
|
||||
.iter()
|
||||
.map(|adapter| {
|
||||
let info = adapter.get_info();
|
||||
let required = (create.device_descriptor)(adapter).required_limits;
|
||||
let meets_limits = required.check_limits(&adapter.limits());
|
||||
log::info!("graphics: DirectX 12 adapter {:?} ({:?}, driver {:?}), limits ok: {meets_limits}", info.name, info.device_type, info.driver);
|
||||
Dx12Adapter { device_type: info.device_type, meets_limits }
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn configured_backends(backend_override: Option<Backends>) -> Backends {
|
||||
fn configured_backends(backends: Backends) -> Backends {
|
||||
let mut options = NativeOptions::default();
|
||||
configure(&mut options, backend_override);
|
||||
configure_backends(&mut options, backends);
|
||||
let WgpuSetup::CreateNew(create) = options.wgpu_options.wgpu_setup else {
|
||||
panic!("default native options must create a wgpu instance");
|
||||
};
|
||||
create.instance_descriptor.backends
|
||||
}
|
||||
|
||||
const fn adapter(device_type: DeviceType, meets_limits: bool) -> Dx12Adapter {
|
||||
Dx12Adapter { device_type, meets_limits }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_default_initializes_only_dx12() {
|
||||
// eframe's own default (PRIMARY | GL) would initialize OpenGL as well.
|
||||
fn windows_default_never_initializes_vulkan() {
|
||||
// eframe's own default (PRIMARY | GL) would initialize Vulkan and OpenGL as well.
|
||||
let WgpuSetup::CreateNew(eframe_default) = NativeOptions::default().wgpu_options.wgpu_setup else {
|
||||
panic!("default native options must create a wgpu instance");
|
||||
};
|
||||
assert!(eframe_default.instance_descriptor.backends.contains(Backends::GL));
|
||||
assert_eq!(configured_backends(None), Backends::DX12);
|
||||
let gpu = [adapter(DeviceType::IntegratedGpu, true)];
|
||||
assert_eq!(configured_backends(choose_backends(None, &gpu)), Backends::DX12);
|
||||
// #120: whatever the DirectX 12 probe finds, and whatever the retry picks, no Vulkan.
|
||||
let probes: [&[Dx12Adapter]; 4] = [&[], &gpu, &[adapter(DeviceType::Cpu, true)], &[adapter(DeviceType::IntegratedGpu, false)]];
|
||||
for adapters in probes {
|
||||
let first = choose_backends(None, adapters);
|
||||
assert!(!first.contains(Backends::VULKAN), "{adapters:?}");
|
||||
if let Some(second) = fallback_backends(None, first) {
|
||||
assert!(!second.contains(Backends::VULKAN), "{adapters:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_backend_override_is_preserved() {
|
||||
let warp_only = [adapter(DeviceType::Cpu, true)];
|
||||
for backends in [Backends::VULKAN, Backends::GL, Backends::DX12, Backends::VULKAN | Backends::DX12, Backends::empty()] {
|
||||
assert_eq!(configured_backends(Some(backends)), backends);
|
||||
// Whatever the probe would have found, the override wins (the probe doesn't run then),
|
||||
// and a failed start isn't retried with something the user didn't ask for.
|
||||
assert_eq!(choose_backends(Some(backends), &[]), backends);
|
||||
assert_eq!(choose_backends(Some(backends), &warp_only), backends);
|
||||
assert_eq!(configured_backends(backends), backends);
|
||||
assert_eq!(fallback_backends(Some(backends), backends), None);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gl_when_dx12_has_no_usable_gpu() {
|
||||
// No DirectX 12 driver, so DirectX 12 offers only WARP (or nothing) and lags.
|
||||
let warp_only = [adapter(DeviceType::Cpu, true)];
|
||||
assert_eq!(choose_backends(None, &warp_only), Backends::GL | Backends::DX12);
|
||||
assert_eq!(choose_backends(None, &[]), Backends::GL);
|
||||
// A GPU that can't create eframe's device: GL alone, so it isn't picked over the GL one.
|
||||
let weak_gpu = [adapter(DeviceType::IntegratedGpu, false), adapter(DeviceType::Cpu, true)];
|
||||
assert_eq!(choose_backends(None, &weak_gpu), Backends::GL);
|
||||
// Any usable GPU keeps DirectX 12 only, next to WARP or a weaker GPU.
|
||||
let mixed = [adapter(DeviceType::Cpu, true), adapter(DeviceType::IntegratedGpu, false), adapter(DeviceType::DiscreteGpu, true)];
|
||||
assert_eq!(choose_backends(None, &mixed), Backends::DX12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_start_retries_with_the_other_backend() {
|
||||
assert_eq!(fallback_backends(None, Backends::DX12), Some(Backends::GL));
|
||||
assert_eq!(fallback_backends(None, Backends::GL), Some(Backends::DX12));
|
||||
// Both were already tried: nothing left to fall back to.
|
||||
assert_eq!(fallback_backends(None, Backends::DX12 | Backends::GL), None);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -260,37 +260,65 @@ fn main() -> eframe::Result<()> {
|
|||
viewport = viewport.with_icon(i);
|
||||
}
|
||||
let options = eframe::NativeOptions { viewport, ..Default::default() };
|
||||
// Before eframe creates the wgpu instance: default Windows to DirectX 12 only (see graphics.rs).
|
||||
// Before eframe creates the wgpu instance: Windows starts on DirectX 12, or OpenGL when
|
||||
// DirectX 12 has no GPU driver, and never touches Vulkan unless asked (see graphics.rs).
|
||||
#[cfg(target_os = "windows")]
|
||||
let options = {
|
||||
let backend_override = eframe::wgpu::Backends::from_env();
|
||||
#[cfg(target_os = "windows")]
|
||||
let (options, first_backends) = {
|
||||
let mut options = options;
|
||||
graphics::configure(&mut options, eframe::wgpu::Backends::from_env());
|
||||
options
|
||||
let backends = graphics::configure(&mut options, backend_override);
|
||||
(options, backends)
|
||||
};
|
||||
#[cfg(target_os = "windows")]
|
||||
let retry_options = options.clone();
|
||||
// Registered before the event loop starts, so it catches the Finder event that launched us as
|
||||
// well as later ones. Lives until the event loop returns; the app creator only borrows it.
|
||||
#[cfg(target_os = "macos")]
|
||||
let apple_events = apple_events::AppleEvents::install();
|
||||
#[cfg(target_os = "macos")]
|
||||
let apple_events = &apple_events;
|
||||
eframe::run_native(
|
||||
"GridCraft",
|
||||
options,
|
||||
Box::new(move |cc| {
|
||||
SheetApp::setup_context_for_language(&cc.egui_ctx, app.ui.dark, app.ui.language);
|
||||
if let Some(port) = control_port {
|
||||
match control_server::start(port, cc.egui_ctx.clone()) {
|
||||
Ok(rx) => app.control_rx = Some(rx),
|
||||
Err(e) => log::error!("control channel on port {port}: {e}"),
|
||||
// The app waits here until a start gets as far as creating it; a start that fails before
|
||||
// (no graphics adapter, device or surface) leaves it here for the next attempt.
|
||||
let pending = std::cell::Cell::new(Some(app));
|
||||
let run = |options: eframe::NativeOptions| {
|
||||
eframe::run_native(
|
||||
"GridCraft",
|
||||
options,
|
||||
Box::new(|cc| {
|
||||
let Some(mut app) = pending.take() else {
|
||||
return Err("the app was already created".into());
|
||||
};
|
||||
SheetApp::setup_context_for_language(&cc.egui_ctx, app.ui.dark, app.ui.language);
|
||||
if let Some(port) = control_port {
|
||||
match control_server::start(port, cc.egui_ctx.clone()) {
|
||||
Ok(rx) => app.control_rx = Some(rx),
|
||||
Err(e) => log::error!("control channel on port {port}: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Box::new(App(
|
||||
app,
|
||||
#[cfg(target_os = "macos")]
|
||||
None,
|
||||
#[cfg(target_os = "macos")]
|
||||
apple_events.connect(&cc.egui_ctx),
|
||||
)))
|
||||
}),
|
||||
)
|
||||
Ok(Box::new(App(
|
||||
app,
|
||||
#[cfg(target_os = "macos")]
|
||||
None,
|
||||
#[cfg(target_os = "macos")]
|
||||
apple_events.connect(&cc.egui_ctx),
|
||||
)))
|
||||
}),
|
||||
)
|
||||
};
|
||||
let result = run(options);
|
||||
// A graphics error before the app existed: start once more with the other backend (eframe
|
||||
// reuses its event loop for the second run). Errors after that, or from winit, are returned.
|
||||
#[cfg(target_os = "windows")]
|
||||
if let Err(eframe::Error::Wgpu(e)) = &result
|
||||
&& let Some(app) = pending.take()
|
||||
&& let Some(backends) = graphics::fallback_backends(backend_override, first_backends)
|
||||
{
|
||||
log::error!("graphics: the renderer didn't start on {first_backends:?} ({e}); trying {backends:?}");
|
||||
pending.set(Some(app));
|
||||
let mut options = retry_options;
|
||||
graphics::configure_backends(&mut options, backends);
|
||||
return run(options);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Hardware parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (created from a source audit) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** trivial (Windows renderer fallback, #120) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
|
||||
|
||||
The hardware Excel uses, per platform, against GridCraft. Part of
|
||||
[target-app-parity.md](target-app-parity.md). **Hardware: ~40% ready, 25–40 h** (estimated).
|
||||
|
|
@ -8,7 +8,7 @@ The hardware Excel uses, per platform, against GridCraft. Part of
|
|||
| Hardware feature | Excel | GridCraft (macOS · Windows · Linux · BSD · web) | Gap and estimate |
|
||||
|---|---|---|---|
|
||||
| Multi-core recalculation | Multi-threaded recalculation on all cores (Windows and Mac), configurable thread count | Single-threaded everywhere (no threading in `crates/calc`) | Parallel recalc of independent dependency chains on native builds; WASM stays single-threaded or uses workers later: 12–18 h |
|
||||
| GPU rendering of the grid and charts | DirectX / Metal accelerated drawing | wgpu: Metal · DirectX 12 (default since 10-08) · Vulkan/GL · Vulkan/GL · WebGPU or WebGL2 | At parity in kind. Driver problems: Intel UHD crash on Windows (#120), screen flashing on Windows 11 (#76): 3–6 h |
|
||||
| GPU rendering of the grid and charts | DirectX / Metal accelerated drawing | wgpu: Metal · DirectX 12 (default since 10-08; OpenGL when DirectX 12 has no GPU driver or fails to start; Vulkan only on request, so the Intel UHD `igvk64.dll` crash (#120) can't happen) · Vulkan/GL · Vulkan/GL · WebGPU or WebGL2 | At parity in kind. Driver problems: screen flashing on Windows 11 (#76): 2–4 h |
|
||||
| Pen and touch (Draw tab) | Pen pressure and tilt, eraser end, touch mode, Ink to Shape, ink replay | Pen, highlighter, eraser, Ink to Shape with mouse/pen input; **no pressure or tilt**, no touch mode | Pressure-sensitive strokes where winit reports force; touch-friendly ribbon spacing: 4–6 h |
|
||||
| Trackpad and mouse | Smooth scrolling, pinch zoom, horizontal scroll, Magic Mouse | Smooth scrolling and zoom via egui | Verify pinch zoom on each platform: 1–2 h |
|
||||
| Multiple windows and monitors | One window per workbook, New Window for the same workbook, per-monitor DPI | One window; multiple workbooks switch inside it (#162; PR #171) | Native window per workbook, New Window, Arrange All across monitors: 6–8 h |
|
||||
|
|
@ -20,4 +20,5 @@ The hardware Excel uses, per platform, against GridCraft. Part of
|
|||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-11 | trivial | Windows: OpenGL fallback when DirectX 12 has no GPU or fails to start; Vulkan never by default (#120) |
|
||||
| 2026-10-10 | major | Created: single-threaded calc, wgpu rendering, no pen pressure, single window |
|
||||
|
|
|
|||
Loading…
Reference in New Issue