Skip to main content

alpha2_gpu/
context.rs

1use std::sync::Arc;
2use std::time::Duration;
3
4use web_time::Instant;
5
6use crate::diagnostic::DiagnosticHook;
7use crate::{
8    Capabilities, CapabilityTier, GpuDiagnostic, GpuDiagnosticHandler, GpuError, IntermediateFormat,
9};
10
11/// A GPU device and queue owned by one render host.
12///
13/// Clones are cheap and share the same device and diagnostic handler
14/// ([`GpuContext::set_diagnostic_handler`]).
15///
16/// # The queue (`wgpu` interop)
17///
18/// Every `wgpu` call that can reach the native queue is a method here and takes the shared-queue
19/// lock when there is one: `submit`, [`Self::on_submitted_work_done`] and `map_async` (both may
20/// flush pending writes), and `present`. Dropping the last clone also drops the queue under the
21/// lock, because `wgpu` waits for the native queue to idle there. `queue` hands out the handle
22/// for everything else, and each crate's clippy configuration forbids queue-reaching calls on it.
23#[derive(Clone, Debug)]
24pub struct GpuContext {
25    adapter: wgpu::Adapter,
26    device: wgpu::Device,
27    queue: Arc<LockedQueue>,
28    // Shared so clones (one per readback) copy no strings.
29    capabilities: Arc<Capabilities>,
30    diagnostics: Arc<DiagnosticHook>,
31    rate: Arc<crate::rate::GpuRate>,
32}
33
34/// The queue and its shared lock.
35#[derive(Debug)]
36struct LockedQueue {
37    // Declared before `lock` so it drops while `Drop` holds the lock.
38    queue: wgpu::Queue,
39    lock: QueueLockSlot,
40}
41
42/// The shared-queue lock, released on drop if [`LockedQueue`]'s `Drop` took it.
43#[derive(Debug)]
44struct QueueLockSlot {
45    lock: Option<Arc<dyn ExternalQueueLock>>,
46    held: bool,
47}
48
49impl LockedQueue {
50    // On wasm32 the handles are not Send or Sync, so this Arc acts as an Rc; one type keeps every
51    // target the same.
52    #[cfg_attr(target_arch = "wasm32", allow(clippy::arc_with_non_send_sync))]
53    fn shared(queue: wgpu::Queue, lock: Option<Arc<dyn ExternalQueueLock>>) -> Arc<Self> {
54        Arc::new(Self {
55            queue,
56            lock: QueueLockSlot { lock, held: false },
57        })
58    }
59
60    /// Runs `call` holding the shared-queue lock, when there is one.
61    fn locked<T>(&self, call: impl FnOnce(&wgpu::Queue) -> T) -> T {
62        let guard = self.lock.lock.as_ref().map(|lock| {
63            lock.lock();
64            QueueGuard(lock.as_ref())
65        });
66        let result = call(&self.queue);
67        drop(guard);
68        result
69    }
70}
71
72impl Drop for LockedQueue {
73    fn drop(&mut self) {
74        // Dropping the last handle waits for the native queue to idle, a queue access that needs
75        // the lock.
76        if let Some(lock) = &self.lock.lock {
77            lock.lock();
78            self.lock.held = true;
79        }
80    }
81}
82
83impl Drop for QueueLockSlot {
84    fn drop(&mut self) {
85        if self.held
86            && let Some(lock) = &self.lock
87        {
88            lock.unlock();
89        }
90    }
91}
92
93/// Serializes queue access with another API that shares the same queue.
94///
95/// A native decoder sharing the renderer's queue must take the same lock. [`GpuContext`] takes
96/// it around every call that can reach the native queue. Implementations must be re-entrant,
97/// because a queue-completion callback may submit while the lock is held.
98pub trait ExternalQueueLock: std::fmt::Debug + Send + Sync + 'static {
99    /// Acquires exclusive access to the shared queue.
100    fn lock(&self);
101    /// Releases access taken by [`ExternalQueueLock::lock`].
102    fn unlock(&self);
103}
104
105/// Where one [`GpuContext::submit_timed`] call spent its time.
106#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
107#[non_exhaustive]
108#[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
109pub struct SubmitTimings {
110    /// Blocked acquiring the queue lock shared with another API.
111    pub queue_lock_wait: Duration,
112    /// Inside `wgpu`'s own submission, with the lock already held.
113    pub queue_submit: Duration,
114}
115
116struct QueueGuard<'a>(&'a dyn ExternalQueueLock);
117
118impl Drop for QueueGuard<'_> {
119    fn drop(&mut self) {
120        self.0.unlock();
121    }
122}
123
124impl GpuContext {
125    /// Adopts an already-created adapter, device, and queue.
126    ///
127    /// Used by native interop backends that create one device for both wgpu and a platform
128    /// decoder.
129    #[must_use]
130    #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
131    pub fn from_parts(adapter: wgpu::Adapter, device: wgpu::Device, queue: wgpu::Queue) -> Self {
132        Self::from_parts_with_queue_lock(adapter, device, queue, None)
133    }
134
135    /// Adopts existing handles whose queue is shared with another API.
136    ///
137    /// Every call that can reach the native queue takes `queue_lock` first; see [`GpuContext`].
138    /// Such a context has no surface, since presenting would bypass the lock.
139    #[must_use]
140    #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
141    pub fn from_parts_with_queue_lock(
142        adapter: wgpu::Adapter,
143        device: wgpu::Device,
144        queue: wgpu::Queue,
145        queue_lock: Option<Arc<dyn ExternalQueueLock>>,
146    ) -> Self {
147        let capabilities = Arc::new(Capabilities::from_device(&adapter, &device));
148        crate::validation_log::watch_device(&device, &capabilities.summary());
149        let diagnostics = Arc::new(DiagnosticHook::new());
150        install_device_callbacks(&device, &diagnostics);
151        Self {
152            adapter,
153            device,
154            queue: LockedQueue::shared(queue, queue_lock),
155            capabilities,
156            diagnostics,
157            rate: Arc::default(),
158        }
159    }
160
161    /// The logical device.
162    #[must_use]
163    #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
164    pub const fn device(&self) -> &wgpu::Device {
165        &self.device
166    }
167
168    /// The queue, for calls that never reach the native queue (writes, timestamp periods,
169    /// backend interop). Submit, present, wait and map through this context instead.
170    #[must_use]
171    #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
172    pub fn queue(&self) -> &wgpu::Queue {
173        &self.queue.queue
174    }
175
176    /// Calls `callback` once submitted work completes, holding the shared-queue lock.
177    ///
178    /// `wgpu` flushes pending writes here, which is a native submission.
179    pub fn on_submitted_work_done(&self, callback: impl FnOnce() + Send + 'static) {
180        #[expect(clippy::disallowed_methods, reason = "this is the locked wrapper")]
181        self.queue
182            .locked(|queue| queue.on_submitted_work_done(callback));
183    }
184
185    /// Maps `slice` for `mode`, holding the shared-queue lock, since `wgpu` may first submit
186    /// pending writes.
187    #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
188    pub fn map_async(
189        &self,
190        slice: &wgpu::BufferSlice<'_>,
191        mode: wgpu::MapMode,
192        callback: impl FnOnce(Result<(), wgpu::BufferAsyncError>) + wgpu::WasmNotSend + 'static,
193    ) {
194        #[expect(clippy::disallowed_methods, reason = "this is the locked wrapper")]
195        self.queue.locked(|_| slice.map_async(mode, callback));
196    }
197
198    /// Presents `texture`, holding the shared-queue lock when one is set.
199    #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
200    pub fn present(&self, texture: wgpu::SurfaceTexture) {
201        #[expect(clippy::disallowed_methods, reason = "this is the locked wrapper")]
202        self.queue.locked(|queue| queue.present(texture));
203    }
204
205    /// Submits command buffers, holding the shared-queue lock. Never submit through the queue
206    /// directly.
207    #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
208    pub fn submit<I>(&self, command_buffers: I) -> wgpu::SubmissionIndex
209    where
210        I: IntoIterator<Item = wgpu::CommandBuffer>,
211    {
212        self.submit_measured(command_buffers, false).0
213    }
214
215    /// Submits command buffers and reports where the call spent its time.
216    ///
217    /// Lock wait reflects a native decoder using the queue; submit time is `wgpu`'s own
218    /// validation cost. Plain [`GpuContext::submit`] reads no clock.
219    #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
220    pub fn submit_timed<I>(&self, command_buffers: I) -> (wgpu::SubmissionIndex, SubmitTimings)
221    where
222        I: IntoIterator<Item = wgpu::CommandBuffer>,
223    {
224        self.submit_measured(command_buffers, true)
225    }
226
227    fn submit_measured<I>(
228        &self,
229        command_buffers: I,
230        measure: bool,
231    ) -> (wgpu::SubmissionIndex, SubmitTimings)
232    where
233        I: IntoIterator<Item = wgpu::CommandBuffer>,
234    {
235        let mut timings = SubmitTimings::default();
236        let started = measure.then(Instant::now);
237        let index = self.queue.locked(|queue| {
238            let acquired = started.map(|started| {
239                let now = Instant::now();
240                timings.queue_lock_wait = now.duration_since(started);
241                now
242            });
243            #[expect(clippy::disallowed_methods, reason = "this is the locked wrapper")]
244            let index = queue.submit(command_buffers);
245            if let Some(acquired) = acquired {
246                timings.queue_submit = acquired.elapsed();
247            }
248            index
249        });
250        (index, timings)
251    }
252
253    /// Requests a surface-independent device.
254    ///
255    /// # Errors
256    /// Returns [`GpuError`] when adapter or device acquisition fails.
257    pub async fn request() -> Result<Self, GpuError> {
258        let _turn = crate::serialize_acquisition();
259        let instance = crate::new_instance(wgpu::Backends::all());
260        Self::request_compatible(&instance, None).await
261    }
262
263    /// Requests the product device with the default request ([`Self::request_product_with`]).
264    ///
265    /// # Errors
266    /// Returns [`GpuError`] when no adapter opens a device.
267    #[cfg(not(target_arch = "wasm32"))]
268    pub async fn request_product() -> Result<Self, GpuError> {
269        Self::request_product_with(&ProductRequest::default()).await
270    }
271
272    /// Requests the product device on [`product_backends`]: the adapter `ALPHA2_GPU_ADAPTER`
273    /// pins, else the first in high-performance order that opens a device.
274    ///
275    /// A DX12 group that lists no adapter is a failed instance, since WARP is always listed.
276    /// GL adapters older than GL 4.3 or GLES 3.1 are refused. `WGPU_POWER_PREF` is ignored.
277    ///
278    /// # Errors
279    /// Returns [`GpuError`] for an unmatched pin, a failed DX12 instance, no hardware adapter
280    /// under [`SoftwareAdapters::Refuse`], or the first device error.
281    #[cfg(not(target_arch = "wasm32"))]
282    pub async fn request_product_with(request: &ProductRequest) -> Result<Self, GpuError> {
283        let _turn = crate::serialize_acquisition();
284        let mut tried = ProductAttempt::default();
285        for adapter in product_candidates(request).await? {
286            match Self::open_device(adapter.adapter, request.withheld).await {
287                Ok(context) => return Ok(context),
288                Err(error) => tried.refused(adapter.name, error),
289            }
290        }
291        Err(tried.into_error(request))
292    }
293
294    /// Requests the product device that `accept` takes, in [`Self::request_product_with`] order.
295    ///
296    /// A shared-device hardware decoder may open on only some adapters (D3D12VA needs a D3D12
297    /// video device). Each hardware adapter is tried until `accept` takes one; a CPU adapter
298    /// only when pinned. If none is taken, the result holds the first device.
299    ///
300    /// # Errors
301    /// Returns [`GpuError`] when no adapter opens a device.
302    #[cfg(not(target_arch = "wasm32"))]
303    #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
304    pub async fn request_product_accepted<T, E>(
305        mut accept: impl FnMut(&Self) -> Result<T, E>,
306    ) -> Result<AcceptedDevice<T, E>, GpuError> {
307        let _turn = crate::serialize_acquisition();
308        let request = ProductRequest::default();
309        let mut tried = ProductAttempt::default();
310        let mut refusals = AdapterRefusals(Vec::new());
311        let mut first: Option<Self> = None;
312        for candidate in product_candidates(&request).await? {
313            if first.is_some() && candidate.cpu && !candidate.pinned {
314                continue;
315            }
316            let context = match Self::open_device(candidate.adapter, request.withheld).await {
317                Ok(context) => context,
318                Err(error) => {
319                    tried.refused(candidate.name, error);
320                    continue;
321                }
322            };
323            match accept(&context) {
324                Ok(value) => {
325                    return Ok(AcceptedDevice {
326                        context,
327                        accepted: Some(value),
328                        refusals,
329                    });
330                }
331                Err(error) => {
332                    refusals.0.push((candidate.name, error));
333                    first.get_or_insert(context);
334                }
335            }
336        }
337        match first {
338            Some(context) => Ok(AcceptedDevice {
339                context,
340                accepted: None,
341                refusals,
342            }),
343            None => Err(tried.into_error(&request)),
344        }
345    }
346
347    /// Every adapter on `backends` in product-request order, each with its enumeration index.
348    /// Open them with [`Self::open_adapter`].
349    #[cfg(not(target_arch = "wasm32"))]
350    #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
351    pub async fn adapters_by_preference_on(
352        backends: wgpu::Backends,
353    ) -> Vec<(usize, wgpu::Adapter)> {
354        let _turn = crate::serialize_acquisition();
355        let instance = crate::new_instance(backends);
356        let mut adapters: Vec<Option<wgpu::Adapter>> = instance
357            .enumerate_adapters(wgpu::Backends::all())
358            .await
359            .into_iter()
360            .map(Some)
361            .collect();
362        let kinds: Vec<(usize, wgpu::DeviceType)> = adapters
363            .iter()
364            .enumerate()
365            .map(|(index, adapter)| {
366                let kind = adapter.as_ref().map_or(wgpu::DeviceType::Other, |adapter| {
367                    adapter.get_info().device_type
368                });
369                (index, kind)
370            })
371            .collect();
372        crate::adapter_pin::product_order(&kinds)
373            .into_iter()
374            .filter_map(|index| adapters[index].take().map(|adapter| (index, adapter)))
375            .collect()
376    }
377
378    /// Opens a device on `adapter` with the standard features and limits.
379    ///
380    /// # Errors
381    /// Returns [`GpuError`] when the adapter cannot create a device.
382    #[cfg(not(target_arch = "wasm32"))]
383    #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
384    pub async fn open_adapter(adapter: wgpu::Adapter) -> Result<Self, GpuError> {
385        Self::open_adapter_without(adapter, wgpu::Features::empty()).await
386    }
387
388    /// Like [`Self::open_adapter`] but without the optional features in `withheld`, for tests.
389    ///
390    /// # Errors
391    /// Returns [`GpuError`] when the adapter cannot create the device.
392    #[cfg(not(target_arch = "wasm32"))]
393    #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
394    pub async fn open_adapter_without(
395        adapter: wgpu::Adapter,
396        withheld: wgpu::Features,
397    ) -> Result<Self, GpuError> {
398        let _turn = crate::serialize_acquisition();
399        Self::open_device(adapter, withheld).await
400    }
401
402    /// The adapter this device was opened on.
403    #[must_use]
404    #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
405    pub const fn adapter(&self) -> &wgpu::Adapter {
406        &self.adapter
407    }
408
409    /// Requests a device compatible with an optional presentation surface.
410    ///
411    /// WebGL2 requires a compatible surface during adapter selection, while
412    /// browser WebGPU and native headless callers may pass `None`.
413    ///
414    /// # Errors
415    /// Returns [`GpuError`] when adapter or device acquisition fails.
416    #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
417    pub async fn request_compatible(
418        instance: &wgpu::Instance,
419        compatible_surface: Option<&wgpu::Surface<'_>>,
420    ) -> Result<Self, GpuError> {
421        let preference = wgpu::PowerPreference::from_env().unwrap_or_default();
422        Self::request_compatible_with_preference(
423            instance,
424            compatible_surface,
425            preference,
426            wgpu::Features::empty(),
427        )
428        .await
429    }
430
431    /// Opens a device on the adapter `wgpu` prefers, falling back to the next adapter that can.
432    ///
433    /// A listed adapter can still fail every device request (such as a GL driver beside a
434    /// lavapipe-only Vulkan loader). On failure of all, the preferred adapter's error is returned.
435    async fn request_compatible_with_preference(
436        instance: &wgpu::Instance,
437        compatible_surface: Option<&wgpu::Surface<'_>>,
438        power_preference: wgpu::PowerPreference,
439        withheld: wgpu::Features,
440    ) -> Result<Self, GpuError> {
441        let _turn = crate::serialize_acquisition();
442        let adapter = instance
443            .request_adapter(&wgpu::RequestAdapterOptions {
444                compatible_surface,
445                power_preference,
446                ..Default::default()
447            })
448            .await
449            .map_err(GpuError::adapter)?;
450        #[cfg(not(target_arch = "wasm32"))]
451        let preferred = adapter.get_info();
452        let error = match Self::open_device(adapter, withheld).await {
453            Ok(context) => return Ok(context),
454            Err(error) => error,
455        };
456        #[cfg(not(target_arch = "wasm32"))]
457        {
458            let mut others: Vec<wgpu::Adapter> = instance
459                .enumerate_adapters(wgpu::Backends::all())
460                .await
461                .into_iter()
462                .filter(|other| other.get_info() != preferred)
463                .filter(|other| {
464                    compatible_surface.is_none_or(|surface| other.is_surface_supported(surface))
465                })
466                .collect();
467            others
468                .sort_by_key(|other| fallback_rank(other.get_info().device_type, power_preference));
469            for other in others {
470                if let Ok(context) = Self::open_device(other, withheld).await {
471                    return Ok(context);
472                }
473            }
474        }
475        Err(error)
476    }
477
478    async fn open_device(
479        adapter: wgpu::Adapter,
480        withheld: wgpu::Features,
481    ) -> Result<Self, GpuError> {
482        // 16-bit norm formats hold CoreVideo's 10-bit planes as R16/RG16 textures; NV12/P010 are
483        // only needed for bi-planar decoder resources.
484        let optional_features = wgpu::Features::TIMESTAMP_QUERY
485            | wgpu::Features::TEXTURE_FORMAT_16BIT_NORM
486            | if cfg!(target_os = "windows") {
487                wgpu::Features::TEXTURE_FORMAT_NV12 | wgpu::Features::TEXTURE_FORMAT_P010
488            } else {
489                wgpu::Features::empty()
490            };
491        let required_features = adapter.features() & optional_features & !withheld;
492        let required_limits =
493            wgpu::Limits::downlevel_webgl2_defaults().using_resolution(adapter.limits());
494        let (device, queue) = adapter
495            .request_device(&wgpu::DeviceDescriptor {
496                required_features,
497                required_limits,
498                ..wgpu::DeviceDescriptor::default()
499            })
500            .await
501            .map_err(GpuError::device)?;
502        let capabilities = Arc::new(Capabilities::from_device(&adapter, &device));
503        crate::validation_log::watch_device(&device, &capabilities.summary());
504        let diagnostics = Arc::new(DiagnosticHook::new());
505        install_device_callbacks(&device, &diagnostics);
506        Ok(Self {
507            adapter,
508            device,
509            queue: LockedQueue::shared(queue, None),
510            capabilities,
511            diagnostics,
512            rate: Arc::default(),
513        })
514    }
515
516    /// Routes uncaptured errors and device-loss notifications to `handler`.
517    ///
518    /// Without a handler, uncaptured errors are dropped in release and panic in debug or under
519    /// `ALPHA2_GPU_STRICT=1`; device loss is dropped. The handler may run on any thread and must
520    /// not block. It replaces any previous handler and is never invoked on `wasm32`.
521    pub fn set_diagnostic_handler(&self, handler: impl Fn(GpuDiagnostic) + Send + Sync + 'static) {
522        let handler: GpuDiagnosticHandler = Arc::new(handler);
523        self.diagnostics.set(Some(handler));
524    }
525
526    /// Drops the installed diagnostic handler, if any.
527    pub fn clear_diagnostic_handler(&self) {
528        self.diagnostics.set(None);
529    }
530
531    /// Returns immutable capability decisions.
532    #[must_use]
533    pub fn capabilities(&self) -> &Capabilities {
534        &self.capabilities
535    }
536
537    /// This device's throughput rate.
538    #[must_use]
539    pub fn rate(&self) -> &Arc<crate::rate::GpuRate> {
540        &self.rate
541    }
542
543    /// This device with an `Rgba8Unorm` intermediate format, so the 8-bit fallback can be tested
544    /// on any device. Shares the device, queue and handler with `self`.
545    #[must_use]
546    pub fn with_rgba8_intermediate(&self) -> Self {
547        let mut capabilities = Capabilities::clone(&self.capabilities);
548        capabilities.intermediate_format = IntermediateFormat::Rgba8Unorm;
549        Self {
550            capabilities: Arc::new(capabilities),
551            ..self.clone()
552        }
553    }
554
555    /// This device reported as `tier`, so downlevel policy can be tested on any device. Shares
556    /// the device, queue and handler with `self`.
557    #[must_use]
558    pub fn with_tier(&self, tier: CapabilityTier) -> Self {
559        let mut capabilities = Capabilities::clone(&self.capabilities);
560        capabilities.tier = tier;
561        Self {
562            capabilities: Arc::new(capabilities),
563            ..self.clone()
564        }
565    }
566
567    /// Chooses a surface configuration with a non-sRGB format where available.
568    ///
569    /// Presented frames are already sRGB-encoded, so an sRGB format would encode them twice
570    /// (wgpu defaults WebGL2 canvases to `Rgba8UnormSrgb`).
571    #[must_use]
572    #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
573    pub fn surface_config(
574        &self,
575        surface: &wgpu::Surface<'_>,
576        width: u32,
577        height: u32,
578    ) -> Option<wgpu::SurfaceConfiguration> {
579        let mut config = surface.get_default_config(&self.adapter, width, height)?;
580        if let Some(format) = surface
581            .get_capabilities(&self.adapter)
582            .formats
583            .into_iter()
584            .find(|format| !format.is_srgb())
585        {
586            config.format = format;
587        }
588        Some(config)
589    }
590}
591
592/// The result of [`GpuContext::request_product_accepted`].
593#[derive(Debug)]
594#[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
595pub struct AcceptedDevice<T, E> {
596    /// The device `accept` took, or the preferred adapter's when none was.
597    pub context: GpuContext,
598    /// What `accept` made of `context`; `None` when it took no device.
599    pub accepted: Option<T>,
600    /// Each adapter `accept` refused before, in the order they were tried.
601    pub refusals: AdapterRefusals<E>,
602}
603
604/// Refused adapters by name with reasons, in the order tried.
605#[derive(Debug)]
606#[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
607pub struct AdapterRefusals<E>(pub Vec<(String, E)>);
608
609impl<E: std::fmt::Display> std::fmt::Display for AdapterRefusals<E> {
610    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
611        for (index, (adapter, error)) in self.0.iter().enumerate() {
612            if index > 0 {
613                formatter.write_str("; ")?;
614            }
615            write!(formatter, "{adapter}: {error}")?;
616        }
617        Ok(())
618    }
619}
620
621impl<E: std::fmt::Display + std::fmt::Debug> std::error::Error for AdapterRefusals<E> {}
622
623/// Whether a product request may open a device on a software adapter
624/// (lavapipe, llvmpipe, WARP, `SwiftShader`).
625#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
626pub enum SoftwareAdapters {
627    /// A software adapter is tried after every hardware one.
628    #[default]
629    Allow,
630    /// Software adapters open only when `ALPHA2_GPU_ADAPTER` pins them.
631    Refuse,
632}
633
634/// What a product request may open.
635#[derive(Clone, Debug, Default)]
636pub struct ProductRequest {
637    /// Whether software adapters are acceptable.
638    pub software: SoftwareAdapters,
639    /// Optional features to withhold, for tests.
640    #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
641    pub withheld: wgpu::Features,
642}
643
644/// Product backend groups in order: DX12 on Windows, Metal on macOS, Vulkan then GL elsewhere,
645/// any in the browser. `WGPU_BACKEND` replaces them when set.
646#[must_use]
647pub fn product_backends() -> Vec<wgpu::Backends> {
648    if let Some(backends) = wgpu::Backends::from_env() {
649        return vec![backends];
650    }
651    if cfg!(target_arch = "wasm32") {
652        vec![wgpu::Backends::all()]
653    } else if cfg!(target_os = "windows") {
654        vec![wgpu::Backends::DX12]
655    } else if cfg!(any(target_os = "macos", target_os = "ios")) {
656        vec![wgpu::Backends::METAL]
657    } else {
658        vec![wgpu::Backends::VULKAN, wgpu::Backends::GL]
659    }
660}
661
662/// An adapter a product request will try.
663#[cfg(not(target_arch = "wasm32"))]
664struct Candidate {
665    adapter: wgpu::Adapter,
666    name: String,
667    cpu: bool,
668    pinned: bool,
669}
670
671/// What a product request tried and why each failed.
672#[cfg(not(target_arch = "wasm32"))]
673#[derive(Default)]
674struct ProductAttempt {
675    refusals: Vec<(String, String)>,
676    first: Option<GpuError>,
677}
678
679#[cfg(not(target_arch = "wasm32"))]
680impl ProductAttempt {
681    fn refused(&mut self, adapter: String, error: GpuError) {
682        self.refusals.push((adapter, error.to_string()));
683        self.first.get_or_insert(error);
684    }
685
686    fn into_error(self, request: &ProductRequest) -> GpuError {
687        match (request.software, self.first) {
688            (SoftwareAdapters::Allow, Some(first)) => first,
689            _ => GpuError::no_hardware(Vec::new(), self.refusals),
690        }
691    }
692}
693
694/// The adapters of the first product backend group with an acceptable adapter, in request order.
695#[cfg(not(target_arch = "wasm32"))]
696async fn product_candidates(request: &ProductRequest) -> Result<Vec<Candidate>, GpuError> {
697    let pin = crate::AdapterPolicy::from_env();
698    let mut listed = Vec::new();
699    let mut software = Vec::new();
700    let mut refusals = Vec::new();
701    for group in product_backends() {
702        let instance = crate::new_instance(group);
703        let adapters = instance.enumerate_adapters(group).await;
704        if group == wgpu::Backends::DX12 && adapters.is_empty() {
705            return Err(GpuError::no_dx12_adapter());
706        }
707        let kinds: Vec<(usize, wgpu::DeviceType)> = adapters
708            .iter()
709            .enumerate()
710            .map(|(index, adapter)| (index, adapter.get_info().device_type))
711            .collect();
712        let mut adapters: Vec<Option<wgpu::Adapter>> = adapters.into_iter().map(Some).collect();
713        let mut candidates = Vec::new();
714        for index in crate::adapter_pin::product_order(&kinds) {
715            let Some(adapter) = adapters[index].take() else {
716                continue;
717            };
718            let info = adapter.get_info();
719            listed.push(format!("[{index}] {} ({:?})", info.name, info.backend));
720            let pinned = pin.names(index, &info.name);
721            if pin.is_pin() && !pinned {
722                continue;
723            }
724            let cpu = crate::software_adapter(&info);
725            if cpu && request.software == SoftwareAdapters::Refuse && !pinned {
726                software.push(info.name);
727                continue;
728            }
729            if info.backend == wgpu::Backend::Gl
730                && let Err(reason) = crate::adapter_pin::gl_version_supported(&info.driver_info)
731            {
732                refusals.push((info.name.clone(), reason));
733                continue;
734            }
735            candidates.push(Candidate {
736                adapter,
737                name: info.name,
738                cpu,
739                pinned,
740            });
741        }
742        if !candidates.is_empty() {
743            return Ok(candidates);
744        }
745    }
746    if pin.is_pin() {
747        return Err(GpuError::pin(format!(
748            "{} names no adapter; the adapters are {}",
749            pin.setting(),
750            if listed.is_empty() {
751                "none".to_owned()
752            } else {
753                listed.join(", ")
754            }
755        )));
756    }
757    Err(GpuError::no_hardware(software, refusals))
758}
759
760/// Fallback rank for `preference`: requested GPU kinds first, CPU last.
761#[cfg(not(target_arch = "wasm32"))]
762fn fallback_rank(device_type: wgpu::DeviceType, preference: wgpu::PowerPreference) -> u8 {
763    use wgpu::DeviceType::{Cpu, DiscreteGpu, IntegratedGpu, Other, VirtualGpu};
764    let order: &[wgpu::DeviceType] = match preference {
765        wgpu::PowerPreference::HighPerformance => {
766            &[DiscreteGpu, IntegratedGpu, VirtualGpu, Other, Cpu]
767        }
768        wgpu::PowerPreference::LowPower => &[IntegratedGpu, DiscreteGpu, VirtualGpu, Other, Cpu],
769        wgpu::PowerPreference::None => return 0,
770    };
771    order
772        .iter()
773        .position(|kind| *kind == device_type)
774        .and_then(|rank| u8::try_from(rank).ok())
775        .unwrap_or(u8::MAX)
776}
777
778// A library must not write to stderr, so device events go through the opt-in hook.
779#[cfg(not(target_arch = "wasm32"))]
780fn install_device_callbacks(device: &wgpu::Device, diagnostics: &Arc<DiagnosticHook>) {
781    let uncaptured = Arc::clone(diagnostics);
782    device.on_uncaptured_error(Arc::new(move |error| {
783        uncaptured.emit(GpuDiagnostic::uncaptured(&error));
784    }));
785    let lost = Arc::clone(diagnostics);
786    device.set_device_lost_callback(move |reason, message| {
787        lost.emit(GpuDiagnostic::DeviceLost {
788            reason: format!("{reason:?}"),
789            message,
790        });
791    });
792}
793
794#[cfg(target_arch = "wasm32")]
795fn install_device_callbacks(_device: &wgpu::Device, _diagnostics: &Arc<DiagnosticHook>) {}
796
797#[cfg(all(test, not(target_arch = "wasm32")))]
798mod tests {
799    use super::fallback_rank;
800    use wgpu::DeviceType::{Cpu, DiscreteGpu, IntegratedGpu, Other, VirtualGpu};
801    use wgpu::PowerPreference;
802
803    #[test]
804    fn fallback_adapters_follow_the_preference_and_a_cpu_comes_last() {
805        let ordered = |preference| {
806            let mut kinds = vec![Cpu, Other, VirtualGpu, IntegratedGpu, DiscreteGpu];
807            kinds.sort_by_key(|kind| fallback_rank(*kind, preference));
808            kinds
809        };
810        assert_eq!(
811            ordered(PowerPreference::HighPerformance),
812            [DiscreteGpu, IntegratedGpu, VirtualGpu, Other, Cpu]
813        );
814        assert_eq!(
815            ordered(PowerPreference::LowPower),
816            [IntegratedGpu, DiscreteGpu, VirtualGpu, Other, Cpu]
817        );
818        assert_eq!(
819            ordered(PowerPreference::None),
820            [Cpu, Other, VirtualGpu, IntegratedGpu, DiscreteGpu]
821        );
822    }
823}