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