From ac3982c68ac05ebf88bb1a68a93a6299479a5c17 Mon Sep 17 00:00:00 2001 From: VoiceLessQ Date: Thu, 29 Oct 2026 13:46:02 -0200 Subject: [PATCH] Windows: keep OpenGL for a GPU without Direct3D 12 and for a named adapter (#4150) Follow-up to #4600, from its review. A start left OpenGL out whenever Direct3D 12 listed an adapter, and WARP, its software adapter, is always listed. So a GPU without a Direct3D 12 driver, which drew through OpenGL before, now drew on the CPU. An adapter that WGPU_ADAPTER_NAME named was left out too when it was an OpenGL one. OpenGL is now listed when Direct3D 12 lists no hardware adapter and when WGPU_ADAPTER_NAME is set. The Direct3D 12 check takes the shader compiler the window does (WGPU_DX12_COMPILER, else FXC). --- apps/vectorcraft/src/gpu.rs | 43 +++++++++++++++++++++---------- apps/vectorcraft/src/gpu/tests.rs | 25 ++++++++++++++++++ docs/development.md | 2 +- 3 files changed, 55 insertions(+), 15 deletions(-) diff --git a/apps/vectorcraft/src/gpu.rs b/apps/vectorcraft/src/gpu.rs index dc6ac1929..431d40346 100644 --- a/apps/vectorcraft/src/gpu.rs +++ b/apps/vectorcraft/src/gpu.rs @@ -140,33 +140,48 @@ pub fn backends(default: Backends, env_set: bool, opengl: bool) -> Backends { } /// 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. +/// adapter out (an earlier start crashed on it, or a restart failed on it), when +/// `WGPU_ADAPTER_NAME` names one (it may be an OpenGL adapter), or when DX12 lists no hardware +/// adapter (Windows 7, or a GPU without a DX12 driver, which then draws through OpenGL as before +/// rather than on the CPU with WARP). 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() + opengl_needed(left_out, !skipped().is_empty(), std::env::var_os(NAME_ENV).is_some(), dx12_lists_hardware) } -/// 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. +/// [`windows_opengl`], with a restart leaving adapters out (`skipping`) and an adapter named +/// (`named`): `dx12_hardware` is asked only when nothing else decides. +fn opengl_needed(left_out: &LeftOut, skipping: bool, named: bool, dx12_hardware: impl FnOnce() -> bool) -> bool { + left_out.opengl_only || !left_out.adapters.is_empty() || skipping || named || !dx12_hardware() +} + +/// Whether Direct3D 12 lists a hardware adapter; WARP, its software one, doesn't count. 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 { +fn dx12_lists_hardware() -> 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; + // The shader compiler the window's own instance takes (#1194). + descriptor.backend_options.dx12.shader_compiler = wgpu::Dx12Compiler::from_env().unwrap_or(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 + let kinds: Vec = pollster::block_on(instance.enumerate_adapters(Backends::DX12)).iter().map(|a| a.get_info().device_type).collect(); + log::info!("Direct3D 12 lists these graphics adapters: {kinds:?}"); + any_hardware(&kinds) } #[cfg(not(windows))] -fn dx12_lists_adapters() -> bool { +fn dx12_lists_hardware() -> bool { false } +/// Is one of the adapters of these kinds hardware, not software such as WARP? +#[cfg(any(windows, test))] +fn any_hardware(kinds: &[DeviceType]) -> bool { + kinds.iter().any(|k| *k != DeviceType::Cpu) +} + /// The largest texture side to ask the device for, which bounds the window's surface: the /// adapter's own limit when it is above eframe's `default` (8192 px), so a window larger than /// that keeps its surface (#1459). Never below `default`, which is what was asked for before. diff --git a/apps/vectorcraft/src/gpu/tests.rs b/apps/vectorcraft/src/gpu/tests.rs index e4824ecd9..3d13b5157 100644 --- a/apps/vectorcraft/src/gpu/tests.rs +++ b/apps/vectorcraft/src/gpu/tests.rs @@ -472,6 +472,31 @@ fn windows_lists_opengl_once_a_start_leaves_an_adapter_out() { assert_eq!(listing.backends(backends(Backends::all(), false, true)), Backends::GL); } +/// OpenGL stays where it drew before #4150's fix: a GPU without a Direct3D 12 driver (DX12 lists +/// WARP alone) and an adapter `WGPU_ADAPTER_NAME` names; a start with a DX12 GPU lists DX12 alone. +#[test] +fn windows_lists_opengl_without_a_dx12_gpu_or_with_an_adapter_named() { + let start = LeftOut::default(); + let asked = std::cell::Cell::new(false); + let hardware = |found: bool| { + asked.set(true); + found + }; + assert!(!opengl_needed(&start, false, false, || hardware(true)), "a DX12 GPU: DX12 alone (#4150)"); + assert!(asked.replace(false)); + assert!(opengl_needed(&start, false, false, || hardware(false)), "WARP alone: the GPU draws through OpenGL, not the CPU"); + assert!(opengl_needed(&start, false, true, || hardware(true)), "a named adapter may be an OpenGL one"); + assert!(opengl_needed(&start, true, false, || hardware(true)), "a restart left an adapter out"); + let listing = LeftOut { opengl_only: true, ..LeftOut::default() }; + asked.set(false); + assert!(opengl_needed(&listing, false, false, || hardware(true))); + assert!(!asked.get(), "DX12 isn't listed once OpenGL must be"); + assert!(any_hardware(&[DeviceType::Cpu, DeviceType::IntegratedGpu])); + assert!(any_hardware(&[DeviceType::VirtualGpu])); + assert!(!any_hardware(&[DeviceType::Cpu]), "WARP"); + assert!(!any_hardware(&[]), "Windows 7"); +} + #[test] fn power_preference_follows_the_environment_then_the_preference() { assert_eq!(power_preference(None, None), AUTOMATIC); diff --git a/docs/development.md b/docs/development.md index 90ae340cc..8bab05786 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. 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). +- **Windows backends:** Direct3D 12. OpenGL is listed beside it only when a start leaves an adapter out (a crash or a failed restart), when `WGPU_ADAPTER_NAME` names an adapter, or when Direct3D 12 lists no hardware adapter (Windows 7, or a GPU without a Direct3D 12 driver, which then draws through OpenGL rather than on the CPU with WARP): 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.