Skip to content

Select adapters lazily in request_adapter instead of exposing every adapter in the system - #10011

Open
AdrianEddy wants to merge 3 commits into
gfx-rs:trunkfrom
AdrianEddy:lazy-adapter-selection
Open

Select adapters lazily in request_adapter instead of exposing every adapter in the system#10011
AdrianEddy wants to merge 3 commits into
gfx-rs:trunkfrom
AdrianEddy:lazy-adapter-selection

Conversation

@AdrianEddy

@AdrianEddy AdrianEddy commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Connections
None.

Description

Instance::request_adapter currently answers a request for one adapter by fully exposing every adapter on every enabled backend, then filtering and sorting the results. On DX12, exposing an adapter means calling D3D12CreateDevice plus the capability queries — and driver initialization is not cheap. Machines with more than one adapter are the common case now, not the exception: virtually every hybrid-graphics laptop and every desktop with an iGPU-carrying CPU enumerates at least two hardware adapters plus WARP.

Measured on a desktop with a discrete NVIDIA GPU and an AMD iGPU (Windows 11), request_adapter paid for three device creations to pick one adapter:

call before after
request_adapter(HighPerformance) (warm) ~4.5 s 356 ms
request_adapter(None) (cold) ~4.5 s 1.7 s (only the selected adapter's driver init)
request_adapter(force_fallback_adapter) ~4.5 s 8 ms (no hardware driver touched at all)
enumerate_adapters (3 adapters) ~4.5 s ~4.5 s (unchanged by design)

The iGPU's driver initialization alone was ~2.7 s on every process start, paid just to lose the ranking to the discrete GPU. Firefox already instruments this exact cost in the wild (the d3d12_expose_adapter telemetry hook).

The change:

  • wgpu-hal: a new defaulted Instance::request_adapter(power_preference, force_fallback_adapter, surface_hint) trait method. The default returns None, meaning "no lazy selection available" — out-of-tree backends compile and behave exactly as before. Mirrored on DynInstance.
  • wgpu-core: Instance::request_adapter first asks each backend for a lazy selection. A returned adapter goes through the same validation pipeline as a full enumeration (fallback/device-type check, surface compatibility, indirect-validation limit adjustment, feature/limit filtering, adapter_allowed, limit bucketing); if it fails any step — or the backend returned None — the existing enumerate-everything path runs for that backend unchanged, so selection results and RequestAdapterError reporting are preserved by construction.
  • DX12 backend: implements the hook. Adapters are ranked from DXGI descriptors alone — IDXGIFactory6::EnumAdapterByGpuPreference for LowPower/HighPerformance (the OS is the authority on hybrid-graphics power splits; before DXGI 1.6 the backend declines lazy selection so the full-enumeration ranking runs unchanged), plain EnumAdapters1 order for None (matching what the unsorted full enumeration selects today, with no reordering). Under the preference modes, software rasterizers are ranked last (matching DeviceType::Cpu sorting last in the existing ranking); when force_fallback_adapter is set they are the only candidates. Adapters are then exposed one at a time and the first success is returned, so exactly one ID3D12Device is created on the common path.

No public API changes in wgpu, wgpu-core, or wgpu-types; enumerate_adapters is untouched for callers who genuinely want the full list. Other backends can adopt the hook later (Vulkan's per-device property queries are comparatively cheap, but many-GPU compute rigs would still benefit).

One deliberate behavioral note: on DX12 systems with several adapters of the same device type (e.g. two discrete GPUs), LowPower/HighPerformance selection now follows DXGI's GPU-preference ordering rather than the stable device-type bucket sort over EnumAdapters1 order, so ties within a bucket can resolve to a different adapter than before. On the common iGPU + dGPU configurations the two orderings agree. Where they disagree, DXGI's ordering reflects the OS's power/performance policy for the machine rather than DXGI enumeration order.

A possible follow-up (not in this PR): once an earlier backend has produced a validated adapter whose device-type rank is the best achievable for the request, wgpu-core could skip querying later backends entirely — on default multi-backend instances that would avoid even the one device creation this PR still pays when an earlier backend wins.

Testing

  • Verified on a 3-adapter Windows 11 machine (discrete NVIDIA + AMD iGPU + WARP) that all four request shapes (None, LowPower, HighPerformance, force_fallback_adapter) select the identical adapter the full-enumeration ranking selects, with the timings above. (This configuration cannot exhibit the same-device-type tie-break divergence described above; that case is documented rather than tested.)
  • On systems without DXGI 1.6 (Windows 10 before 1803), the backend declines lazy selection for the preference modes, so today's full-enumeration ranking runs unchanged.
  • Existing test suite exercises request_adapter in every harness bootstrap; CI runs it across backends including DX12/WARP.

Squash or Rebase?

Squash.

Checklist

  • I self-reviewed and fully understand this PR.
  • WebGPU implementations built with wgpu may be affected behaviorally.
  • Validation and feature gates are in place to confine behavioral changes.
  • Tests demonstrate the validation and altered logic works.
  • CHANGELOG.md entries for the user-facing effects of this change are present.
  • The PR is minimal, and doesn't make sense to land as multiple PRs.
  • Commits are logically scoped and individually reviewable.
  • The PR description has enough context to understand the motivation and solution implemented.

This PR was generated with Claude

@andyleiserson andyleiserson added the area: core Issues related to wgpu-core label Aug 5, 2026
@andyleiserson

Copy link
Copy Markdown
Contributor

It seems to me there are two things going on here:

  1. Being able to obtain an adapter without doing the full initialization for every adapter
  2. Picking adapters based on the DXGI power preference API

It may be necessary to do (2) in order to accomplish (1) while also maintaining the power preference functionality. But I'd like to think some about how we can cleanly separate the pieces, and have a vision to how we might do #9932 (both the original request and also the Firefox use case, which is picking the adapter that's driving the active display. It's currently done by LUID, but it doesn't have to be done that way. However, the notion of "active display" in Firefox is totally disconnected from anything that is using wgpu, so something like surface_hint probably won't work.)

One question that comes out of all that, is whether picking based on the DXGI power preference API is truly required, or if your use case would be addressed by an LUID-style filter (which could be driven by DXGI enumeration in the app). I am thinking about whether implementing/exposing backend-dependent enumeration behaviors risks them getting out of sync with the standard enumeration behavior in wgpu-core.

A different way of structuring this would be for HALs to expose some kind of "pre-adapter descriptor" that can be used by preference logic in wgpu-core, or passed to an application-provided selection hook. The "pre-adapter descriptor" could have fields "low power preference rank" and "high performance perference rank", obtained in the dx12 case from the DXGI enumeration API.

@AdrianEddy

Copy link
Copy Markdown
Contributor Author

My use case would work with an LUID-style filter. However, I think making the users resort to handwritten DXGI calls before using wgpu is not great, so we could maybe expose some independent wgpu-helper to address this, essentially separating these two pieces - Main initialization based on LUIDs alone, and a separate power preference->LUID helper. Let me know if that works and I'll update the PR

@andyleiserson

Copy link
Copy Markdown
Contributor

I do think that more sophisticated functionality than an LUID filter could be useful, I don't mean to discourage pursuing that if you want to, but the LUID filter seemed like it might be a good compromise with broad appeal. The DXGI helper would make it a lot easier on users, I'd be inclined to include it, but loose APIs like that can slip through cracks in maintenance or testing; there might be differing opinions.

I have (created last week, for unrelated reasons) an LLM-generated prototype of an LUID filter, based on @jimblandy's proposal for passthrough extensions in #9980. That branch is here: trunk...andyleiserson:wgpu:hal-extensions. I've put the extension API topic on the agenda for the maintainer's meeting next week, the extension API again seems like added complexity that may extend the timeline for getting something included. The obvious shape that fits better with existing APIs is to add something to Dx12BackendOptions, although that's passed to instance creation, not requestAdapter.

…dapter

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.
@AdrianEddy
AdrianEddy force-pushed the lazy-adapter-selection branch from 3e64492 to d653d46 Compare August 13, 2026 22:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: core Issues related to wgpu-core

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants