From 3daa35c4e224affeae7013b273eb8677b4cda87e Mon Sep 17 00:00:00 2001 From: Adrian Eddy Date: Wed, 5 Aug 2026 01:01:43 +0200 Subject: [PATCH 1/3] Select adapters lazily in request_adapter instead of exposing every adapter wgpu-hal gains a defaulted Instance::request_adapter hook that backends can implement to answer an adapter request without exposing every adapter in the system. The DX12 backend implements it by ranking raw DXGI adapters (EnumAdapterByGpuPreference for the preference modes) and exposing candidates one at a time, so only the selected adapter pays D3D12CreateDevice. wgpu-core validates the lazily selected adapter with the same filter pipeline as a full enumeration and falls back to the existing path whenever the backend declines or the result is rejected. --- CHANGELOG.md | 6 +++ wgpu-core/src/instance.rs | 65 ++++++++++++++++++++++ wgpu-hal/src/auxil/dxgi/factory.rs | 56 +++++++++++++++---- wgpu-hal/src/dx12/instance.rs | 86 +++++++++++++++++++++++++----- wgpu-hal/src/dynamic/instance.rs | 18 +++++++ wgpu-hal/src/lib.rs | 26 +++++++++ 6 files changed, 234 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cd8a2ae6cf..8a05d167290 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,6 +76,12 @@ Bottom level categories: - `naga::valid::ValidationError` is now always returned boxed, to avoid `clippy::large_result_err` warning. By @beicause in [#9612](https://github.com/gfx-rs/wgpu/pull/9612) +### Performance + +#### General + +- `Instance::request_adapter` no longer exposes every adapter in the system to select one. Backends can answer a request from cheap native descriptors (`wgpu_hal::Instance::request_adapter`), and the DX12 backend does: adapters are ranked via `IDXGIFactory6::EnumAdapterByGpuPreference` and only the selected adapter gets an `ID3D12Device` created. On machines with more than one GPU (hybrid-graphics laptops, desktops with an iGPU) this removes seconds of per-adapter driver initialization from startup. On DX12 systems with several adapters of the same device type, `LowPower`/`HighPerformance` ties now resolve in DXGI's GPU-preference order instead of enumeration order. By @AdrianEddy in [#0000](https://github.com/gfx-rs/wgpu/pull/0000). + ### Bug Fixes #### General diff --git a/wgpu-core/src/instance.rs b/wgpu-core/src/instance.rs index a078307c859..fc376c8f267 100644 --- a/wgpu-core/src/instance.rs +++ b/wgpu-core/src/instance.rs @@ -487,6 +487,46 @@ impl Instance { ) } + /// Apply the same validation and transforms to a lazily selected adapter + /// (`hal::Instance::request_adapter`) that [`Self::request_adapter`] + /// applies to a full enumeration. `None` means the adapter is unsuitable + /// and the caller must fall back to full enumeration. + /// + /// Keep the filter sequence in sync with the per-backend loop in + /// [`Self::request_adapter`]. + fn validate_requested_adapter( + &self, + mut raw: hal::DynExposedAdapter, + desc: &wgt::RequestAdapterOptions<&Surface>, + ) -> Option { + if desc.force_fallback_adapter && raw.info.device_type != wgt::DeviceType::Cpu { + return None; + } + + if let Some(surface) = desc.compatible_surface { + if let Err(err) = surface.get_capabilities_with_raw(&raw) { + log::debug!( + "Adapter {:?} not compatible with surface: {}", + raw.info, + err + ); + return None; + } + } + + self.adjust_limits_for_indirect_validation(&mut raw.capabilities.limits); + filter_features_and_limits(self.flags, &mut raw.features, &mut raw.capabilities.limits); + if !self.adapter_allowed(&raw) { + return None; + } + + if desc.apply_limit_buckets { + limits::apply_limit_buckets(raw) + } else { + Some(raw) + } + } + pub fn enumerate_adapters( self: &Arc, backends: Backends, @@ -562,6 +602,31 @@ impl Instance { .compatible_surface .and_then(|surface| surface.raw(backend)); + // Give the backend a chance to select an adapter without exposing + // every adapter in the system (see `hal::Instance::request_adapter`). + // The result goes through the same validation as a full + // enumeration (`validate_requested_adapter` — keep it in sync with + // the filter sequence below); if it does not survive, fall through + // to the enumeration path so selection and error reporting are + // unchanged. + let lazy_adapter = unsafe { + instance.request_adapter( + desc.power_preference, + desc.force_fallback_adapter, + compatible_hal_surface, + ) + }; + if let Some(exposed) = lazy_adapter { + if let Some(exposed) = self.validate_requested_adapter(exposed, desc) { + log::debug!( + "Backend `{backend:?}` selected adapter without full enumeration: {:?}", + exposed.info + ); + adapters.push(exposed); + continue; + } + } + let mut backend_adapters = unsafe { instance.enumerate_adapters(compatible_hal_surface) }; if backend_adapters.is_empty() { diff --git a/wgpu-hal/src/auxil/dxgi/factory.rs b/wgpu-hal/src/auxil/dxgi/factory.rs index 44a64a139ed..0d862be335f 100644 --- a/wgpu-hal/src/auxil/dxgi/factory.rs +++ b/wgpu-hal/src/auxil/dxgi/factory.rs @@ -79,6 +79,19 @@ impl Deref for DxgiAdapter { } } +fn wrap_adapter(adapter1: Dxgi::IDXGIAdapter1) -> Option { + if !should_keep_adapter(&adapter1) { + return None; + } + + if let Ok(adapter4) = adapter1.cast::() { + Some(DxgiAdapter::Adapter4(adapter4)) + } else { + let adapter3 = adapter1.cast::().unwrap(); + Some(DxgiAdapter::Adapter3(adapter3)) + } +} + pub fn enumerate_adapters(factory: DxgiFactory) -> Vec { let mut adapters = Vec::with_capacity(8); @@ -93,21 +106,44 @@ pub fn enumerate_adapters(factory: DxgiFactory) -> Vec { } }; - if !should_keep_adapter(&adapter1) { - continue; - } - - if let Ok(adapter4) = adapter1.cast::() { - adapters.push(DxgiAdapter::Adapter4(adapter4)); - } else { - let adapter3 = adapter1.cast::().unwrap(); - adapters.push(DxgiAdapter::Adapter3(adapter3)); - } + adapters.extend(wrap_adapter(adapter1)); } adapters } +/// [`enumerate_adapters`], ordered by `IDXGIFactory6::EnumAdapterByGpuPreference` +/// instead of `EnumAdapters1`'s primary-display-first order. Returns `None` +/// when DXGI 1.6 is unavailable (Windows 10 before 1803); the caller decides +/// how to rank adapters without OS preference ordering. +pub fn enumerate_adapters_by_gpu_preference( + factory: &DxgiFactory, + preference: Dxgi::DXGI_GPU_PREFERENCE, +) -> Option> { + let DxgiFactory::Factory6(factory6) = factory else { + return None; + }; + + let mut adapters = Vec::with_capacity(8); + + for cur_index in 0.. { + profiling::scope!("IDXGIFactory6::EnumAdapterByGpuPreference"); + let adapter1: Dxgi::IDXGIAdapter1 = + match unsafe { factory6.EnumAdapterByGpuPreference(cur_index, preference) } { + Ok(a) => a, + Err(e) if e.code() == Dxgi::DXGI_ERROR_NOT_FOUND => break, + Err(e) => { + log::error!("Failed enumerating adapters by GPU preference: {e}"); + break; + } + }; + + adapters.extend(wrap_adapter(adapter1)); + } + + Some(adapters) +} + #[derive(Clone, Debug)] pub enum DxgiFactory { /// Provided by DXGI 1.4 diff --git a/wgpu-hal/src/dx12/instance.rs b/wgpu-hal/src/dx12/instance.rs index fdb63bcb6ea..0ea0cf4904f 100644 --- a/wgpu-hal/src/dx12/instance.rs +++ b/wgpu-hal/src/dx12/instance.rs @@ -171,19 +171,79 @@ impl crate::Instance for super::Instance { adapters .into_iter() - .filter_map(|raw| { - super::Adapter::expose( - raw, - &self.library, - &self.device_factory, - &self.dcomp_lib, - self.flags, - self.memory_budget_thresholds, - self.compiler_container.clone(), - self.options.clone(), - self.telemetry, - ) - }) + .filter_map(|raw| self.expose_adapter(raw)) .collect() } + + unsafe fn request_adapter( + &self, + power_preference: wgt::PowerPreference, + force_fallback_adapter: bool, + _surface_hint: Option<&super::Surface>, + ) -> Option> { + // Ranking from DXGI descriptors alone, so only the selected adapter + // pays `D3D12CreateDevice`. `EnumAdapters1` order (the adapter the + // primary display is connected to first) mirrors what an unsorted + // full enumeration would select for `PowerPreference::None`. The + // preference modes need `EnumAdapterByGpuPreference` (DXGI 1.6); + // without it, decline so the caller's full-enumeration ranking runs. + let preference = match power_preference { + wgt::PowerPreference::None => None, + wgt::PowerPreference::LowPower => Some(Dxgi::DXGI_GPU_PREFERENCE_MINIMUM_POWER), + wgt::PowerPreference::HighPerformance => { + Some(Dxgi::DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE) + } + }; + let raw_adapters = match preference { + Some(preference) => auxil::dxgi::factory::enumerate_adapters_by_gpu_preference( + &self.factory, + preference, + )?, + None => auxil::dxgi::factory::enumerate_adapters(self.factory.clone()), + }; + + let mut raw_adapters: Vec<_> = raw_adapters + .into_iter() + .map(|raw| { + let is_software = unsafe { raw.GetDesc1() }.is_ok_and(|desc| { + Dxgi::DXGI_ADAPTER_FLAG(desc.Flags as i32) + .contains(Dxgi::DXGI_ADAPTER_FLAG_SOFTWARE) + }); + (raw, is_software) + }) + .collect(); + if force_fallback_adapter { + raw_adapters.retain(|&(_, is_software)| is_software); + } else if preference.is_some() { + // `EnumAdapterByGpuPreference` does not pin the software + // rasterizer last, but the caller's ranking of a full enumeration + // does (`wgt::DeviceType::Cpu` sorts last). `PowerPreference::None` + // keeps raw `EnumAdapters1` order, which an unsorted full + // enumeration also uses. + raw_adapters.sort_by_key(|&(_, is_software)| is_software); + } + + raw_adapters + .into_iter() + .find_map(|(raw, _)| self.expose_adapter(raw)) + } +} + +impl super::Instance { + fn expose_adapter( + &self, + raw: auxil::dxgi::factory::DxgiAdapter, + ) -> Option> { + super::Adapter::expose( + raw, + &self.library, + &self.device_factory, + &self.dcomp_lib, + self.flags, + self.memory_budget_thresholds, + self.compiler_container.clone(), + self.options.clone(), + self.telemetry, + ) + } } diff --git a/wgpu-hal/src/dynamic/instance.rs b/wgpu-hal/src/dynamic/instance.rs index cab14cdf152..7b04694fa76 100644 --- a/wgpu-hal/src/dynamic/instance.rs +++ b/wgpu-hal/src/dynamic/instance.rs @@ -41,6 +41,13 @@ pub trait DynInstance: DynResource { &self, surface_hint: Option<&dyn DynSurface>, ) -> Vec; + + unsafe fn request_adapter( + &self, + power_preference: wgt::PowerPreference, + force_fallback_adapter: bool, + surface_hint: Option<&dyn DynSurface>, + ) -> Option; } impl DynInstance for I { @@ -68,4 +75,15 @@ impl DynInstance for I { }) .collect() } + + unsafe fn request_adapter( + &self, + power_preference: wgt::PowerPreference, + force_fallback_adapter: bool, + surface_hint: Option<&dyn DynSurface>, + ) -> Option { + let surface_hint = surface_hint.map(|s| s.expect_downcast_ref()); + unsafe { I::request_adapter(self, power_preference, force_fallback_adapter, surface_hint) } + .map(Into::into) + } } diff --git a/wgpu-hal/src/lib.rs b/wgpu-hal/src/lib.rs index 34f0d68d387..a4c3247eefc 100644 --- a/wgpu-hal/src/lib.rs +++ b/wgpu-hal/src/lib.rs @@ -668,6 +668,32 @@ pub trait Instance: Sized + WasmNotSendSync { &self, surface_hint: Option<&::Surface>, ) -> Vec>; + + /// Expose the single adapter that best matches `power_preference` and + /// `force_fallback_adapter`, creating backend resources for as few + /// adapters as possible. + /// + /// The default returns `None`, which callers treat as "no lazy selection + /// available" and answer with a full [`Self::enumerate_adapters`] pass. + /// Backends override this when they can rank adapters from cheap native + /// descriptors before paying full exposure — on DX12, exposing an adapter + /// means creating an `ID3D12Device`, and driver initialization can take + /// seconds per adapter on machines with more than one GPU. + /// + /// Overrides must return an adapter that [`Self::enumerate_adapters`] + /// would also expose, selected consistently with how callers rank a full + /// enumeration for these options (`force_fallback_adapter` restricts the + /// choice to [`wgt::DeviceType::Cpu`] adapters). Callers re-validate the + /// returned adapter and fall back to full enumeration when it is + /// unsuitable, so an override may ignore `surface_hint`. + unsafe fn request_adapter( + &self, + _power_preference: wgt::PowerPreference, + _force_fallback_adapter: bool, + _surface_hint: Option<&::Surface>, + ) -> Option> { + None + } } pub trait Surface: WasmNotSendSync { From 8b8269b210b40cda8f0f02a7ba8e6d2c00334dba Mon Sep 17 00:00:00 2001 From: Adrian Eddy Date: Wed, 5 Aug 2026 01:03:14 +0200 Subject: [PATCH 2/3] Fill in changelog PR number --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a05d167290..242676b35c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -80,7 +80,7 @@ Bottom level categories: #### General -- `Instance::request_adapter` no longer exposes every adapter in the system to select one. Backends can answer a request from cheap native descriptors (`wgpu_hal::Instance::request_adapter`), and the DX12 backend does: adapters are ranked via `IDXGIFactory6::EnumAdapterByGpuPreference` and only the selected adapter gets an `ID3D12Device` created. On machines with more than one GPU (hybrid-graphics laptops, desktops with an iGPU) this removes seconds of per-adapter driver initialization from startup. On DX12 systems with several adapters of the same device type, `LowPower`/`HighPerformance` ties now resolve in DXGI's GPU-preference order instead of enumeration order. By @AdrianEddy in [#0000](https://github.com/gfx-rs/wgpu/pull/0000). +- `Instance::request_adapter` no longer exposes every adapter in the system to select one. Backends can answer a request from cheap native descriptors (`wgpu_hal::Instance::request_adapter`), and the DX12 backend does: adapters are ranked via `IDXGIFactory6::EnumAdapterByGpuPreference` and only the selected adapter gets an `ID3D12Device` created. On machines with more than one GPU (hybrid-graphics laptops, desktops with an iGPU) this removes seconds of per-adapter driver initialization from startup. On DX12 systems with several adapters of the same device type, `LowPower`/`HighPerformance` ties now resolve in DXGI's GPU-preference order instead of enumeration order. By @AdrianEddy in [#10011](https://github.com/gfx-rs/wgpu/pull/10011). ### Bug Fixes From d653d46a322cd6f181be0ff4c9bcf562eaa65b28 Mon Sep 17 00:00:00 2001 From: Adrian Eddy Date: Wed, 5 Aug 2026 01:07:25 +0200 Subject: [PATCH 3/3] Shorten request_adapter doc comment --- wgpu-hal/src/lib.rs | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/wgpu-hal/src/lib.rs b/wgpu-hal/src/lib.rs index a4c3247eefc..e33b7b68e58 100644 --- a/wgpu-hal/src/lib.rs +++ b/wgpu-hal/src/lib.rs @@ -670,22 +670,14 @@ pub trait Instance: Sized + WasmNotSendSync { ) -> Vec>; /// Expose the single adapter that best matches `power_preference` and - /// `force_fallback_adapter`, creating backend resources for as few - /// adapters as possible. - /// - /// The default returns `None`, which callers treat as "no lazy selection - /// available" and answer with a full [`Self::enumerate_adapters`] pass. - /// Backends override this when they can rank adapters from cheap native - /// descriptors before paying full exposure — on DX12, exposing an adapter - /// means creating an `ID3D12Device`, and driver initialization can take - /// seconds per adapter on machines with more than one GPU. - /// - /// Overrides must return an adapter that [`Self::enumerate_adapters`] - /// would also expose, selected consistently with how callers rank a full - /// enumeration for these options (`force_fallback_adapter` restricts the - /// choice to [`wgt::DeviceType::Cpu`] adapters). Callers re-validate the - /// returned adapter and fall back to full enumeration when it is - /// unsuitable, so an override may ignore `surface_hint`. + /// `force_fallback_adapter`, exposing as few adapters as possible. + /// + /// The default `None` means no lazy selection is available and the caller + /// falls back to [`Self::enumerate_adapters`]. Overrides must select + /// consistently with how callers rank a full enumeration + /// (`force_fallback_adapter` restricts the choice to + /// [`wgt::DeviceType::Cpu`]). The caller re-validates the result, so + /// `surface_hint` may be ignored. unsafe fn request_adapter( &self, _power_preference: wgt::PowerPreference,