diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cd8a2ae6cf..242676b35c4 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 [#10011](https://github.com/gfx-rs/wgpu/pull/10011). + ### 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..e33b7b68e58 100644 --- a/wgpu-hal/src/lib.rs +++ b/wgpu-hal/src/lib.rs @@ -668,6 +668,24 @@ pub trait Instance: Sized + WasmNotSendSync { &self, surface_hint: Option<&::Surface>, ) -> Vec>; + + /// Expose the single adapter that best matches `power_preference` and + /// `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, + _force_fallback_adapter: bool, + _surface_hint: Option<&::Surface>, + ) -> Option> { + None + } } pub trait Surface: WasmNotSendSync {