From 2f9e72b4199377733e4defecaae14b5b4c5c2d4d Mon Sep 17 00:00:00 2001 From: VoiceLessQ Date: Thu, 29 Oct 2026 12:35:28 -0200 Subject: [PATCH] Fix #4150: Windows doesn't start OpenGL unless a start needs it wgpu's OpenGL backend on Windows keeps a hidden window on a helper thread that pumps no messages (gfx-rs/wgpu#10504). Windows hands an input-language change to every UI thread of the process and waits for each, so switching the keyboard to another layout hung the whole app, even while it drew with Direct3D 12. OpenGL was started on every Windows start, as the fallback. A Windows start now lists Direct3D 12 alone. OpenGL is still listed where it is the fallback: after a start crashed on an adapter or while listing them (#2243), after a restart that left an adapter out, and when Direct3D 12 lists no adapter at all (Windows 7). WGPU_BACKEND still picks the backends as before. --- apps/vectorcraft/Cargo.toml | 3 ++ apps/vectorcraft/src/gpu.rs | 46 ++++++++++++++++++++++++++----- apps/vectorcraft/src/gpu/tests.rs | 20 +++++++++++--- apps/vectorcraft/src/main.rs | 5 ++-- docs/development.md | 2 +- 5 files changed, 62 insertions(+), 14 deletions(-) diff --git a/apps/vectorcraft/Cargo.toml b/apps/vectorcraft/Cargo.toml index 1137c0d6b..3173d3bc9 100644 --- a/apps/vectorcraft/Cargo.toml +++ b/apps/vectorcraft/Cargo.toml @@ -44,6 +44,9 @@ clipboard-win = { workspace = true } # GPU that shows it: drawing on a GPU without a monitor reset the display driver. Safe wrappers over # Win32 (MIT); the `user` feature is User32.dll only. Only the wgpu renderer chooses an adapter. winsafe = { version = "0.0.29", default-features = false, features = ["user"], optional = true } +# Waits for Direct3D 12's adapter list before the window starts (src/gpu.rs, #4150). MIT OR +# Apache-2.0, already in the build through eframe. +pollster = "1" # DirectWrite's system font collection and GDI's font list (src/system_fonts.rs): fonts that font # services such as Adobe Fonts load in place (#579, #1258). MIT OR Apache-2.0, already in the build diff --git a/apps/vectorcraft/src/gpu.rs b/apps/vectorcraft/src/gpu.rs index f683f03b4..dc6ac1929 100644 --- a/apps/vectorcraft/src/gpu.rs +++ b/apps/vectorcraft/src/gpu.rs @@ -126,13 +126,45 @@ pub fn preferred_compositor(pref: Option<&str>, env: Option) -> if automatic(pref, env) { crate::compositor_gpu::compositor_gpu() } else { None } } -/// The graphics backends the window starts with: on Windows Direct3D 12, falling back to OpenGL, -/// unless `WGPU_BACKEND` names others (`env_set`; `default` then holds them). Creating a Vulkan -/// instance loads every installed Vulkan driver into the process, and a faulty one (Intel's -/// `igvk64.dll`, #806) crashed the app before its window appeared. DX12 is Windows' own backend, -/// as in PdfCraft. Elsewhere, `default`. -pub fn backends(default: Backends, env_set: bool) -> Backends { - if cfg!(windows) && !env_set { Backends::DX12 | Backends::GL } else { default } +/// The graphics backends the window starts with: on Windows Direct3D 12, with OpenGL beside it +/// only when `opengl` ([`windows_opengl`]), unless `WGPU_BACKEND` names others (`env_set`; +/// `default` then holds them). Creating a Vulkan instance loads every installed Vulkan driver into +/// the process, and a faulty one (Intel's `igvk64.dll`, #806) crashed the app before its window +/// appeared. DX12 is Windows' own backend, as in PdfCraft. Elsewhere, `default`. +pub fn backends(default: Backends, env_set: bool, opengl: bool) -> Backends { + match (cfg!(windows) && !env_set, opengl) { + (true, true) => Backends::DX12 | Backends::GL, + (true, false) => Backends::DX12, + (false, _) => default, + } +} + +/// Whether a Windows start lists OpenGL's adapters beside Direct3D 12's: only when it leaves an +/// adapter out (an earlier start crashed on it, or a restart failed on it) or DX12 lists none +/// (Windows 7). wgpu's OpenGL backend keeps a hidden window on a thread that pumps no messages, +/// and Windows waits on it when the input language changes, so the whole app hung on a switch to +/// another keyboard layout (#4150, gfx-rs/wgpu#10504), even drawing with DX12. +pub fn windows_opengl(left_out: &LeftOut) -> bool { + left_out.opengl_only || !left_out.adapters.is_empty() || !skipped().is_empty() || !dx12_lists_adapters() +} + +/// Whether Direct3D 12 lists any adapter, WARP's software one included. A driver that crashes here +/// crashes a start while it lists the adapters, which the next start answers with OpenGL alone. +#[cfg(windows)] +fn dx12_lists_adapters() -> bool { + let mut descriptor = wgpu::InstanceDescriptor::new_without_display_handle(); + descriptor.backends = Backends::DX12; + // FXC is part of Windows, as for the window's own instance (#1194). + descriptor.backend_options.dx12.shader_compiler = wgpu::Dx12Compiler::Fxc; + let instance = wgpu::Instance::new(descriptor); + let found = pollster::block_on(instance.enumerate_adapters(Backends::DX12)).len(); + log::info!("Direct3D 12 lists {found} graphics adapters"); + found > 0 +} + +#[cfg(not(windows))] +fn dx12_lists_adapters() -> bool { + false } /// The largest texture side to ask the device for, which bounds the window's surface: the diff --git a/apps/vectorcraft/src/gpu/tests.rs b/apps/vectorcraft/src/gpu/tests.rs index d13a1c417..e4824ecd9 100644 --- a/apps/vectorcraft/src/gpu/tests.rs +++ b/apps/vectorcraft/src/gpu/tests.rs @@ -451,13 +451,25 @@ fn a_windows_gpu_renders_through_dx12_before_vulkan() { #[test] fn windows_never_loads_vulkan_drivers_unless_asked() { let all = Backends::all(); - let start = backends(all, false); + let start = backends(all, false, false); + let fallback = backends(all, false, true); if cfg!(windows) { - assert_eq!(start, Backends::DX12 | Backends::GL, "#806"); + assert_eq!(start, Backends::DX12, "#806, and no OpenGL unless needed (#4150)"); + assert_eq!(fallback, Backends::DX12 | Backends::GL); } else { - assert_eq!(start, all); + assert_eq!((start, fallback), (all, all)); } - assert_eq!(backends(Backends::VULKAN, true), Backends::VULKAN, "WGPU_BACKEND=vulkan still picks it"); + assert_eq!(backends(Backends::VULKAN, true, false), Backends::VULKAN, "WGPU_BACKEND=vulkan still picks it"); + assert_eq!(backends(Backends::GL, true, false), Backends::GL, "WGPU_BACKEND=gl still picks it"); +} + +#[test] +fn windows_lists_opengl_once_a_start_leaves_an_adapter_out() { + let crashed = LeftOut { adapters: vec!["Dx12:8086:3ea0".into()], ..LeftOut::default() }; + assert!(windows_opengl(&crashed), "#2243: the next start can fall back to OpenGL"); + let listing = LeftOut { opengl_only: true, ..LeftOut::default() }; + assert!(windows_opengl(&listing)); + assert_eq!(listing.backends(backends(Backends::all(), false, true)), Backends::GL); } #[test] diff --git a/apps/vectorcraft/src/main.rs b/apps/vectorcraft/src/main.rs index 2dc62fb1d..f02cbda18 100644 --- a/apps/vectorcraft/src/main.rs +++ b/apps/vectorcraft/src/main.rs @@ -313,8 +313,9 @@ fn main() -> std::process::ExitCode { // Metal on an Iris Pro, #651). `WGPU_VALIDATION_INDIRECT_CALL=1` turns it back on. create.instance_descriptor.flags = (eframe::wgpu::InstanceFlags::from_build_config() - eframe::wgpu::InstanceFlags::VALIDATION_INDIRECT_CALL).with_env(); - create.instance_descriptor.backends = - left_out.backends(gpu::backends(create.instance_descriptor.backends, std::env::var_os("WGPU_BACKEND").is_some())); + let env_set = std::env::var_os("WGPU_BACKEND").is_some(); + let opengl = cfg!(windows) && !env_set && gpu::windows_opengl(&left_out); + create.instance_descriptor.backends = left_out.backends(gpu::backends(create.instance_descriptor.backends, env_set, opengl)); // DX12 shaders compile with FXC, part of Windows: wgpu's default loads whichever // dxcompiler.dll the search path finds, and one without its dxil.dll fails every shader, // losing the device before the first frame (#1194). `WGPU_DX12_COMPILER` picks another. diff --git a/docs/development.md b/docs/development.md index 9427fcbef..90ae340cc 100644 --- a/docs/development.md +++ b/docs/development.md @@ -55,7 +55,7 @@ How the web shell (`apps/vectorcraft-web/src/web.rs`) differs from desktop: The canvas is rasterized on the CPU (`vectorcraft-render`, vello_cpu); the GPU (wgpu, through eframe) only composites the canvas texture and draws the UI, so any GPU that can show the window will do. The desktop app picks the window's adapter itself (`apps/vectorcraft/src/gpu.rs`, eframe's `native_adapter_selector`) and logs every adapter it found and the one it uses. - **Order:** adapters that report they can't present to the window are never tried; then the one `WGPU_ADAPTER_NAME` names; hardware before software (llvmpipe, WARP); the native backends (Vulkan, Metal, DX12) before OpenGL; with Automatic, the GPU the Wayland compositor draws with, then the GPU that drives a display, and among those the one that drives the primary display; then the power preference; then a GPU's DX12 adapter before its Vulkan one (Intel's Vulkan driver made the whole window flicker black, #545), keeping the system's order among equals. -- **Windows backends:** Direct3D 12, falling back to OpenGL. Vulkan isn't started at all unless `WGPU_BACKEND` asks for it (`WGPU_BACKEND=vulkan`): creating a Vulkan instance loads every installed Vulkan driver into the process, and a faulty one (Intel's `igvk64.dll`, #806) crashed the app before its window appeared. After a start that crashed while it listed the Direct3D 12 adapters, later starts list OpenGL's alone (see the crash bullet below). +- **Windows backends:** Direct3D 12. OpenGL is listed beside it only when a start leaves an adapter out (a crash or a failed restart) or Direct3D 12 lists no adapter (Windows 7): wgpu's OpenGL backend keeps a hidden window whose thread pumps no messages, and the app hung on a switch to another input language (#4150, gfx-rs/wgpu#10504). Vulkan isn't started at all unless `WGPU_BACKEND` asks for it (`WGPU_BACKEND=vulkan`): creating a Vulkan instance loads every installed Vulkan driver into the process, and a faulty one (Intel's `igvk64.dll`, #806) crashed the app before its window appeared. After a start that crashed while it listed the Direct3D 12 adapters, later starts list OpenGL's alone (see the crash bullet below). - **The GPU that drives the display.** A GPU without a monitor is the wrong one even when it says it can show the window: on a desktop with a Ryzen's integrated GPU (no monitor) and an NVIDIA card driving both monitors, power saving picked the integrated GPU, Windows had to present every frame across adapters, and the integrated GPU's driver reset under that: the graphics device was lost, or the desktop's compositor went down and every monitor stayed black for minutes (pdfcraft#378). So Automatic asks the system which GPU drives which display and puts that GPU first; the power preference only orders the rest, and decides alone where the system can't say. - **Windows:** `EnumDisplayDevices` (the display devices attached to the desktop, and the primary one), through the `winsafe` crate; the PCI ids in each device id are matched against wgpu's adapters. The registry doesn't tell a user which adapter drives a display, and WMI reports a resolution for a GPU without a monitor, so neither is used. - **Linux:** the connected connectors in `/sys/class/drm` and their cards' PCI ids; a built-in panel (`eDP`, `LVDS`, `DSI`) counts as the primary display.