Skip to main content

gpui_wgpu/
wgpu_context.rs

1// Modified for gpui-pre (snapshot of zed@d2a4fb6): adapt device queues and sRGB detection to wgpu 30.0.1.
2#[cfg(not(target_family = "wasm"))]
3use crate::{WgpuRenderer, WgpuSurfaceConfig};
4#[cfg(not(target_family = "wasm"))]
5use anyhow::Context as _;
6#[cfg(not(target_family = "wasm"))]
7use gpui_util::ResultExt;
8use parking_lot::Mutex;
9use std::sync::Arc;
10use std::sync::atomic::{AtomicBool, Ordering};
11use wgpu::TextureFormat;
12
13#[derive(Clone)]
14pub struct WgpuContext {
15    pub instance: wgpu::Instance,
16    pub adapter: wgpu::Adapter,
17    pub device: Arc<wgpu::Device>,
18    pub queue: Arc<wgpu::Queue>,
19    backend: WgpuBackend,
20    dual_source_blending: bool,
21    color_texture_format: wgpu::TextureFormat,
22    errors: Arc<DeviceErrorState>,
23}
24
25/// Errors reported by wgpu's device-wide callbacks.
26///
27/// A device has exactly one lost callback and one uncaptured-error callback, so this
28/// state is installed once per device and shared by every renderer using it. Renderers
29/// keep their own handle so they can keep observing a loss after the context that
30/// produced it has been dropped for recovery.
31#[derive(Default)]
32pub struct DeviceErrorState {
33    lost: AtomicBool,
34    last_error: Mutex<(u64, Option<String>)>,
35}
36
37impl DeviceErrorState {
38    fn install(device: &wgpu::Device) -> Arc<Self> {
39        let errors = Arc::new(Self::default());
40        device.set_device_lost_callback({
41            let errors = Arc::clone(&errors);
42            move |reason, message| {
43                log::error!("wgpu device lost: reason={reason:?}, message={message}");
44                if reason != wgpu::DeviceLostReason::Destroyed {
45                    errors.lost.store(true, Ordering::Relaxed);
46                }
47            }
48        });
49        device.on_uncaptured_error(Arc::new({
50            let errors = Arc::clone(&errors);
51            move |error| errors.record(error.to_string())
52        }));
53        errors
54    }
55
56    /// Returns true if the GPU device was lost (e.g., due to driver crash, suspend/resume).
57    pub fn device_lost(&self) -> bool {
58        self.lost.load(Ordering::Relaxed)
59    }
60
61    fn record(&self, error: String) {
62        let mut last_error = self.last_error.lock();
63        last_error.0 = last_error.0.wrapping_add(1);
64        last_error.1 = Some(error);
65    }
66
67    /// Returns the latest error once per observer, without consuming shared state.
68    /// Start each observer at zero, including after switching devices, so errors
69    /// raised during construction are observed. Multiple errors between observations
70    /// coalesce into the latest error; this is not an error queue.
71    pub fn observe_error(&self, generation: &mut u64) -> Option<String> {
72        let last_error = self.last_error.lock();
73        if *generation == last_error.0 {
74            return None;
75        }
76        *generation = last_error.0;
77        last_error.1.clone()
78    }
79
80    /// The generation an observer should start from to see only errors raised after
81    /// this call, for a renderer joining a device other renderers already used.
82    pub fn current_generation(&self) -> u64 {
83        self.last_error.lock().0
84    }
85}
86
87#[derive(Clone, Copy, Debug, Eq, PartialEq)]
88pub enum WgpuBackend {
89    BrowserWebGpu,
90    Gl,
91    Native(wgpu::Backend),
92}
93
94#[cfg(target_family = "wasm")]
95#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
96pub enum WebBackendPreference {
97    #[default]
98    Auto,
99    WebGpu,
100    WebGl,
101}
102
103#[cfg(target_family = "wasm")]
104pub struct PreparedWebGraphics {
105    pub context: WgpuContext,
106    pub surface: wgpu::Surface<'static>,
107}
108
109/// wgpu-core refuses to create a surface when neither the instance nor the surface
110/// target carries a display handle, and `SurfaceTarget::Canvas` always passes `None`.
111/// The WebGL2 backend never reads the handle (WebGPU bypasses wgpu-core entirely), so
112/// a unit web display handle on the instance satisfies the check.
113#[cfg(target_family = "wasm")]
114#[derive(Debug)]
115struct WebDisplaySource;
116
117#[cfg(target_family = "wasm")]
118impl raw_window_handle::HasDisplayHandle for WebDisplaySource {
119    fn display_handle(
120        &self,
121    ) -> Result<raw_window_handle::DisplayHandle<'_>, raw_window_handle::HandleError> {
122        Ok(raw_window_handle::DisplayHandle::web())
123    }
124}
125
126#[derive(Clone, Copy)]
127pub struct CompositorGpuHint {
128    pub vendor_id: u32,
129    pub device_id: u32,
130}
131
132impl WgpuContext {
133    #[cfg(not(target_family = "wasm"))]
134    pub fn new(
135        instance: wgpu::Instance,
136        surface: &wgpu::Surface<'_>,
137        config: &WgpuSurfaceConfig,
138        compositor_gpu: Option<CompositorGpuHint>,
139    ) -> anyhow::Result<Self> {
140        Self::new_with_options(instance, surface, config, compositor_gpu, false)
141    }
142
143    #[cfg(not(target_family = "wasm"))]
144    pub fn new_rejecting_software(
145        instance: wgpu::Instance,
146        surface: &wgpu::Surface<'_>,
147        config: &WgpuSurfaceConfig,
148        compositor_gpu: Option<CompositorGpuHint>,
149    ) -> anyhow::Result<Self> {
150        Self::new_with_options(instance, surface, config, compositor_gpu, true)
151    }
152
153    #[cfg(not(target_family = "wasm"))]
154    fn new_with_options(
155        instance: wgpu::Instance,
156        surface: &wgpu::Surface<'_>,
157        config: &WgpuSurfaceConfig,
158        compositor_gpu: Option<CompositorGpuHint>,
159        reject_software: bool,
160    ) -> anyhow::Result<Self> {
161        let device_id_filter = Self::device_id_filter();
162
163        // Select an adapter by actually testing surface configuration with the real device.
164        // This is the only reliable way to determine compatibility on hybrid GPU systems.
165        let (adapter, device, queue, dual_source_blending, color_texture_format) =
166            gpui::block_on(Self::select_adapter_and_device(
167                &instance,
168                device_id_filter,
169                surface,
170                config,
171                compositor_gpu.as_ref(),
172                reject_software,
173            ))?;
174
175        Ok(Self::from_native_adapter_and_device(
176            instance,
177            adapter,
178            device,
179            queue,
180            dual_source_blending,
181            color_texture_format,
182        ))
183    }
184
185    #[cfg(all(
186        not(target_family = "wasm"),
187        any(test, feature = "bench-support", feature = "test-support")
188    ))]
189    pub(crate) fn new_headless() -> anyhow::Result<(Self, wgpu::TextureFormat)> {
190        // Creating a device takes tens of milliseconds, and benchmarks create a headless
191        // renderer for every window they open, so the process shares one device until it
192        // is lost. A static rather than a thread-local: the cached device is never dropped,
193        // because wgpu's queue reads its own thread-locals on drop and would panic if
194        // dropped while a thread's thread-locals are being destroyed.
195        static HEADLESS: Mutex<Option<(WgpuContext, wgpu::TextureFormat)>> = Mutex::new(None);
196        let mut headless = HEADLESS.lock();
197        if let Some((context, format)) = headless.as_ref()
198            && !context.device_lost()
199        {
200            return Ok((context.clone(), *format));
201        }
202        let created = Self::create_headless()?;
203        *headless = Some(created.clone());
204        Ok(created)
205    }
206
207    #[cfg(all(
208        not(target_family = "wasm"),
209        any(test, feature = "bench-support", feature = "test-support")
210    ))]
211    fn create_headless() -> anyhow::Result<(Self, wgpu::TextureFormat)> {
212        let instance = Self::instance(None);
213        let device_id_filter = Self::device_id_filter();
214        let (adapter, device, queue, dual_source_blending, color_texture_format, target_format) =
215            gpui::block_on(async {
216                let mut adapters = instance.enumerate_adapters(wgpu::Backends::all()).await;
217                Self::sort_adapters(&mut adapters, device_id_filter, None);
218
219                for adapter in adapters {
220                    let adapter_info = adapter.get_info();
221                    let Some(target_format) = Self::headless_target_format(&adapter) else {
222                        log::warn!(
223                            "Adapter {:?} has no supported headless render target format",
224                            adapter_info.name
225                        );
226                        continue;
227                    };
228
229                    match Self::create_device(&adapter).await {
230                        Ok((device, queue, dual_source_blending, color_texture_format)) => {
231                            #[cfg(feature = "bench-support")]
232                            if adapter_info.device_type == wgpu::DeviceType::Cpu {
233                                log::error!(
234                                    "Headless renderer selected software adapter {:?}; \
235                                     benchmark results measure CPU software rendering, not hardware GPU rendering",
236                                    adapter_info.name
237                                );
238                            }
239                            return Ok((
240                                adapter,
241                                device,
242                                queue,
243                                dual_source_blending,
244                                color_texture_format,
245                                target_format,
246                            ));
247                        }
248                        Err(error) => {
249                            log::warn!(
250                                "Failed to create a headless device for adapter {:?}: {error:#}",
251                                adapter_info.name
252                            );
253                        }
254                    }
255                }
256
257                anyhow::bail!("No usable headless GPU adapter found")
258            })?;
259
260        Ok((
261            Self::from_native_adapter_and_device(
262                instance,
263                adapter,
264                device,
265                queue,
266                dual_source_blending,
267                color_texture_format,
268            ),
269            target_format,
270        ))
271    }
272
273    #[cfg(all(
274        not(target_family = "wasm"),
275        any(test, feature = "bench-support", feature = "test-support")
276    ))]
277    /// Both candidates are 8-bit RGBA-ordered or BGRA-ordered formats: headless readback
278    /// copies rows as 4 bytes per pixel and only swizzles, so no other formats may be added
279    /// here without updating it.
280    fn headless_target_format(adapter: &wgpu::Adapter) -> Option<wgpu::TextureFormat> {
281        let required_usages =
282            wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC;
283        [
284            wgpu::TextureFormat::Rgba8Unorm,
285            wgpu::TextureFormat::Bgra8Unorm,
286        ]
287        .into_iter()
288        .find(|format| {
289            adapter
290                .get_texture_format_features(*format)
291                .allowed_usages
292                .contains(required_usages)
293        })
294    }
295
296    #[cfg(not(target_family = "wasm"))]
297    fn from_native_adapter_and_device(
298        instance: wgpu::Instance,
299        adapter: wgpu::Adapter,
300        device: wgpu::Device,
301        queue: wgpu::Queue,
302        dual_source_blending: bool,
303        color_texture_format: TextureFormat,
304    ) -> Self {
305        let errors = DeviceErrorState::install(&device);
306
307        log::info!(
308            "Selected GPU adapter: {:?} ({:?})",
309            adapter.get_info().name,
310            adapter.get_info().backend
311        );
312        let backend = WgpuBackend::Native(adapter.get_info().backend);
313
314        Self {
315            instance,
316            adapter,
317            device: Arc::new(device),
318            queue: Arc::new(queue),
319            backend,
320            dual_source_blending,
321            color_texture_format,
322            errors,
323        }
324    }
325
326    #[cfg(not(target_family = "wasm"))]
327    fn device_id_filter() -> Option<u32> {
328        match std::env::var("ZED_DEVICE_ID") {
329            Ok(value) => parse_pci_id(&value)
330                .context("Failed to parse device ID from `ZED_DEVICE_ID` environment variable")
331                .log_err(),
332            Err(std::env::VarError::NotPresent) => None,
333            error => {
334                error
335                    .context("Failed to read value of `ZED_DEVICE_ID` environment variable")
336                    .log_err();
337                None
338            }
339        }
340    }
341
342    #[cfg(target_family = "wasm")]
343    pub async fn new_web(
344        canvas: &web_sys::HtmlCanvasElement,
345        preference: WebBackendPreference,
346    ) -> anyhow::Result<PreparedWebGraphics> {
347        Self::new_web_with_backend(canvas, preference).await
348    }
349
350    #[cfg(target_family = "wasm")]
351    #[allow(clippy::arc_with_non_send_sync)]
352    async fn new_web_with_backend(
353        canvas: &web_sys::HtmlCanvasElement,
354        preference: WebBackendPreference,
355    ) -> anyhow::Result<PreparedWebGraphics> {
356        let backends = match preference {
357            WebBackendPreference::Auto => wgpu::Backends::BROWSER_WEBGPU | wgpu::Backends::GL,
358            WebBackendPreference::WebGpu => wgpu::Backends::BROWSER_WEBGPU,
359            WebBackendPreference::WebGl => wgpu::Backends::GL,
360        };
361        let descriptor = wgpu::InstanceDescriptor {
362            backends,
363            flags: wgpu::InstanceFlags::default(),
364            backend_options: wgpu::BackendOptions::default(),
365            memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
366            display: Some(Box::new(WebDisplaySource)),
367        };
368        let instance = if preference == WebBackendPreference::Auto {
369            wgpu::util::new_instance_with_webgpu_detection(descriptor).await
370        } else {
371            wgpu::Instance::new(descriptor)
372        };
373        let surface = instance
374            .create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone()))
375            .map_err(|error| {
376                anyhow::anyhow!("Failed to create browser graphics surface: {error}")
377            })?;
378
379        let adapter = instance
380            .request_adapter(&wgpu::RequestAdapterOptions {
381                power_preference: wgpu::PowerPreference::HighPerformance,
382                compatible_surface: Some(&surface),
383                force_fallback_adapter: false,
384                apply_limit_buckets: false,
385            })
386            .await
387            .map_err(|error| {
388                anyhow::anyhow!(
389                    "Failed to request a {preference:?} adapter compatible with the canvas: {error}"
390                )
391            })?;
392        let adapter_info = adapter.get_info();
393        let backend = match adapter_info.backend {
394            wgpu::Backend::BrowserWebGpu => WgpuBackend::BrowserWebGpu,
395            wgpu::Backend::Gl => WgpuBackend::Gl,
396            backend => {
397                anyhow::bail!(
398                    "Browser graphics initialization selected unexpected backend {backend:?}"
399                )
400            }
401        };
402
403        let (device, queue, dual_source_blending, color_texture_format) =
404            Self::create_device(&adapter).await?;
405        let errors = DeviceErrorState::install(&device);
406        log::info!(
407            "Browser graphics initialized: requested={preference:?}, selected={backend:?}, \
408             adapter={:?}, limits={:?}, dual_source_blending={dual_source_blending}",
409            adapter_info.name,
410            device.limits(),
411        );
412
413        let context = Self {
414            instance,
415            adapter,
416            device: Arc::new(device),
417            queue: Arc::new(queue),
418            backend,
419            dual_source_blending,
420            color_texture_format,
421            errors,
422        };
423        Ok(PreparedWebGraphics { context, surface })
424    }
425
426    async fn create_device(
427        adapter: &wgpu::Adapter,
428    ) -> anyhow::Result<(wgpu::Device, wgpu::Queue, bool, TextureFormat)> {
429        let dual_source_blending = adapter
430            .features()
431            .contains(wgpu::Features::DUAL_SOURCE_BLENDING);
432
433        let mut required_features = wgpu::Features::empty();
434        if dual_source_blending {
435            required_features |= wgpu::Features::DUAL_SOURCE_BLENDING;
436        } else {
437            log::warn!(
438                "Dual-source blending not available on this GPU. \
439                Subpixel text antialiasing will be disabled."
440            );
441        }
442
443        let color_atlas_texture_format = Self::select_color_texture_format(adapter)?;
444        #[cfg(target_family = "wasm")]
445        let required_limits = if adapter.get_info().backend == wgpu::Backend::Gl {
446            wgpu::Limits::downlevel_webgl2_defaults()
447                .using_resolution(adapter.limits())
448                .using_alignment(adapter.limits())
449        } else {
450            wgpu::Limits::downlevel_defaults()
451                .using_resolution(adapter.limits())
452                .using_alignment(adapter.limits())
453        };
454        #[cfg(not(target_family = "wasm"))]
455        let required_limits = wgpu::Limits::downlevel_defaults()
456            .using_resolution(adapter.limits())
457            .using_alignment(adapter.limits());
458
459        let (device, queue) = adapter
460            .request_device(&wgpu::DeviceDescriptor {
461                label: Some("gpui_device"),
462                required_features,
463                required_limits,
464                memory_hints: wgpu::MemoryHints::MemoryUsage,
465                trace: wgpu::Trace::Off,
466                experimental_features: wgpu::ExperimentalFeatures::disabled(),
467            })
468            .await
469            .map_err(|e| anyhow::anyhow!("Failed to create wgpu device: {e}"))?;
470
471        Ok((
472            device,
473            queue,
474            dual_source_blending,
475            color_atlas_texture_format,
476        ))
477    }
478
479    #[cfg(not(target_family = "wasm"))]
480    pub fn instance(display: Option<Box<dyn wgpu::wgt::WgpuHasDisplayHandle>>) -> wgpu::Instance {
481        wgpu::Instance::new(wgpu::InstanceDescriptor {
482            backends: wgpu::Backends::VULKAN | wgpu::Backends::GL,
483            flags: wgpu::InstanceFlags::default(),
484            backend_options: wgpu::BackendOptions::default(),
485            memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
486            display,
487        })
488    }
489
490    pub fn check_compatible_with_surface(&self, surface: &wgpu::Surface<'_>) -> anyhow::Result<()> {
491        let caps = surface.get_capabilities(&self.adapter);
492        if caps.formats.is_empty() {
493            let info = self.adapter.get_info();
494            anyhow::bail!(
495                "Adapter {:?} (backend={:?}, device={:#06x}) is not compatible with the \
496                 display surface for this window.",
497                info.name,
498                info.backend,
499                info.device,
500            );
501        }
502        Ok(())
503    }
504
505    /// Select an adapter and create a device, testing that the surface can actually be configured.
506    /// This is the only reliable way to determine compatibility on hybrid GPU systems, where
507    /// adapters may report surface compatibility via get_capabilities() but fail when actually
508    /// configuring (e.g., NVIDIA reporting Vulkan Wayland support but failing because the
509    /// Wayland compositor runs on the Intel GPU).
510    #[cfg(not(target_family = "wasm"))]
511    async fn select_adapter_and_device(
512        instance: &wgpu::Instance,
513        device_id_filter: Option<u32>,
514        surface: &wgpu::Surface<'_>,
515        config: &WgpuSurfaceConfig,
516        compositor_gpu: Option<&CompositorGpuHint>,
517        reject_software: bool,
518    ) -> anyhow::Result<(
519        wgpu::Adapter,
520        wgpu::Device,
521        wgpu::Queue,
522        bool,
523        TextureFormat,
524    )> {
525        let mut adapters: Vec<_> = instance.enumerate_adapters(wgpu::Backends::all()).await;
526
527        if adapters.is_empty() {
528            anyhow::bail!("No GPU adapters found");
529        }
530
531        if let Some(device_id) = device_id_filter {
532            log::info!("ZED_DEVICE_ID filter: {:#06x}", device_id);
533        }
534
535        Self::sort_adapters(&mut adapters, device_id_filter, compositor_gpu);
536
537        // Log all available adapters (in sorted order)
538        log::info!("Found {} GPU adapter(s):", adapters.len());
539        for adapter in &adapters {
540            let info = adapter.get_info();
541            log::info!(
542                "  - {} (vendor={:#06x}, device={:#06x}, backend={:?}, type={:?})",
543                info.name,
544                info.vendor,
545                info.device,
546                info.backend,
547                info.device_type,
548            );
549        }
550
551        // Test each adapter by creating a device and configuring the surface
552        for adapter in adapters {
553            let info = adapter.get_info();
554
555            if reject_software && info.device_type == wgpu::DeviceType::Cpu {
556                log::info!(
557                    "Skipping software renderer: {} ({:?})",
558                    info.name,
559                    info.backend
560                );
561                continue;
562            }
563
564            log::info!("Testing adapter: {} ({:?})...", info.name, info.backend);
565
566            match Self::try_adapter_with_surface(&adapter, surface, config).await {
567                Ok((device, queue, dual_source_blending, color_atlas_texture_format)) => {
568                    log::info!(
569                        "Selected GPU (passed configuration test): {} ({:?})",
570                        info.name,
571                        info.backend
572                    );
573                    return Ok((
574                        adapter,
575                        device,
576                        queue,
577                        dual_source_blending,
578                        color_atlas_texture_format,
579                    ));
580                }
581                Err(e) => {
582                    log::info!(
583                        "  Adapter {} ({:?}) failed: {}, trying next...",
584                        info.name,
585                        info.backend,
586                        e
587                    );
588                }
589            }
590        }
591
592        anyhow::bail!("No GPU adapter found that can configure the display surface")
593    }
594
595    /// Sort adapters into a single priority order. Tiers (from highest to lowest):
596    ///
597    /// 1. ZED_DEVICE_ID match — explicit user override
598    /// 2. Compositor GPU match — the GPU the display server is rendering on
599    /// 3. Device type (Discrete > Integrated > Other > Virtual > Cpu).
600    ///    "Other" ranks above "Virtual" because OpenGL seems to count as "Other".
601    /// 4. Backend — prefer Vulkan/Metal/Dx12 over GL/etc.
602    #[cfg(not(target_family = "wasm"))]
603    fn sort_adapters(
604        adapters: &mut [wgpu::Adapter],
605        device_id_filter: Option<u32>,
606        compositor_gpu: Option<&CompositorGpuHint>,
607    ) {
608        adapters.sort_by_key(|adapter| {
609            let info = adapter.get_info();
610            // Backends like OpenGL report device=0 for all adapters, so
611            // device-based matching is only meaningful when non-zero.
612            let device_known = info.device != 0;
613            let user_override: u8 = match device_id_filter {
614                Some(id) if device_known && info.device == id => 0,
615                _ => 1,
616            };
617            let compositor_match: u8 = match compositor_gpu {
618                Some(hint)
619                    if device_known
620                        && info.vendor == hint.vendor_id
621                        && info.device == hint.device_id =>
622                {
623                    0
624                }
625                _ => 1,
626            };
627            let type_priority: u8 = match info.device_type {
628                wgpu::DeviceType::DiscreteGpu => 0,
629                wgpu::DeviceType::IntegratedGpu => 1,
630                wgpu::DeviceType::Other => 2,
631                wgpu::DeviceType::VirtualGpu => 3,
632                wgpu::DeviceType::Cpu => 4,
633            };
634            let backend_priority: u8 = match info.backend {
635                wgpu::Backend::Vulkan | wgpu::Backend::Metal | wgpu::Backend::Dx12 => 0,
636                _ => 1,
637            };
638
639            (
640                user_override,
641                compositor_match,
642                type_priority,
643                backend_priority,
644            )
645        });
646    }
647
648    /// Try to use an adapter with a surface by creating a device and testing configuration.
649    /// Returns the device and queue if successful, allowing them to be reused.
650    #[cfg(not(target_family = "wasm"))]
651    async fn try_adapter_with_surface(
652        adapter: &wgpu::Adapter,
653        surface: &wgpu::Surface<'_>,
654        config: &WgpuSurfaceConfig,
655    ) -> anyhow::Result<(wgpu::Device, wgpu::Queue, bool, TextureFormat)> {
656        let caps = surface.get_capabilities(adapter);
657        if caps.formats.is_empty() {
658            anyhow::bail!("no compatible surface formats");
659        }
660        if caps.alpha_modes.is_empty() {
661            anyhow::bail!("no compatible alpha modes");
662        }
663
664        let (device, queue, dual_source_blending, color_atlas_texture_format) =
665            Self::create_device(adapter).await?;
666        // Test with the configuration the renderer will use, so that it doesn't need to
667        // configure the surface a second time.
668        let (surface_config, _, _) =
669            WgpuRenderer::surface_configuration(surface, adapter, &device, config)?;
670        let error_scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
671
672        surface.configure(&device, &surface_config);
673
674        let error = error_scope.pop().await;
675        if let Some(e) = error {
676            anyhow::bail!("surface configuration failed: {e}");
677        }
678
679        Ok((
680            device,
681            queue,
682            dual_source_blending,
683            color_atlas_texture_format,
684        ))
685    }
686
687    fn select_color_texture_format(adapter: &wgpu::Adapter) -> anyhow::Result<wgpu::TextureFormat> {
688        let required_usages = wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST;
689        let bgra_features = adapter.get_texture_format_features(wgpu::TextureFormat::Bgra8Unorm);
690        let rgba_features = adapter.get_texture_format_features(wgpu::TextureFormat::Rgba8Unorm);
691        #[cfg(target_family = "wasm")]
692        if adapter.get_info().backend == wgpu::Backend::Gl
693            && rgba_features.allowed_usages.contains(required_usages)
694        {
695            return Ok(wgpu::TextureFormat::Rgba8Unorm);
696        }
697        if bgra_features.allowed_usages.contains(required_usages) {
698            return Ok(wgpu::TextureFormat::Bgra8Unorm);
699        }
700        if rgba_features.allowed_usages.contains(required_usages) {
701            let info = adapter.get_info();
702            log::warn!(
703                "Adapter {} ({:?}) does not support Bgra8Unorm atlas textures with usages {:?}; \
704                 falling back to Rgba8Unorm atlas textures.",
705                info.name,
706                info.backend,
707                required_usages,
708            );
709            return Ok(wgpu::TextureFormat::Rgba8Unorm);
710        }
711
712        let info = adapter.get_info();
713        Err(anyhow::anyhow!(
714            "Adapter {} ({:?}, device={:#06x}) does not support a usable color atlas texture \
715             format with usages {:?}. Bgra8Unorm allowed usages: {:?}; \
716             Rgba8Unorm allowed usages: {:?}.",
717            info.name,
718            info.backend,
719            info.device,
720            required_usages,
721            bgra_features.allowed_usages,
722            rgba_features.allowed_usages,
723        ))
724    }
725    pub fn backend(&self) -> WgpuBackend {
726        self.backend
727    }
728
729    pub fn uses_webgl_instance_data(&self) -> bool {
730        matches!(self.backend, WgpuBackend::Gl) && cfg!(target_family = "wasm")
731    }
732
733    pub fn supports_dual_source_blending(&self) -> bool {
734        self.dual_source_blending
735    }
736
737    pub fn color_texture_format(&self) -> wgpu::TextureFormat {
738        self.color_texture_format
739    }
740
741    /// Returns true if the GPU device was lost (e.g., due to driver crash, suspend/resume).
742    /// When this returns true, the context should be recreated.
743    pub fn device_lost(&self) -> bool {
744        self.errors.device_lost()
745    }
746
747    /// The device-wide error state, shared with renderers on this device.
748    pub fn errors(&self) -> &Arc<DeviceErrorState> {
749        &self.errors
750    }
751}
752
753#[cfg(not(target_family = "wasm"))]
754fn parse_pci_id(id: &str) -> anyhow::Result<u32> {
755    let mut id = id.trim();
756
757    if id.starts_with("0x") || id.starts_with("0X") {
758        id = &id[2..];
759    }
760    let is_hex_string = id.chars().all(|c| c.is_ascii_hexdigit());
761    let is_4_chars = id.len() == 4;
762    anyhow::ensure!(
763        is_4_chars && is_hex_string,
764        "Expected a 4 digit PCI ID in hexadecimal format"
765    );
766
767    u32::from_str_radix(id, 16).context("parsing PCI ID as hex")
768}
769
770#[cfg(test)]
771mod tests {
772    use super::{DeviceErrorState, parse_pci_id};
773
774    #[test]
775    fn device_errors_are_observed_independently() {
776        let errors = DeviceErrorState::default();
777        let mut first = 0;
778        let mut second = 0;
779        assert_eq!(errors.observe_error(&mut first), None);
780        errors.record("first".into());
781        assert_eq!(errors.observe_error(&mut first).as_deref(), Some("first"));
782        assert_eq!(errors.observe_error(&mut first), None);
783        assert_eq!(errors.observe_error(&mut second).as_deref(), Some("first"));
784        assert_eq!(errors.observe_error(&mut second), None);
785
786        errors.record("second".into());
787        assert_eq!(errors.observe_error(&mut second).as_deref(), Some("second"));
788        assert_eq!(errors.observe_error(&mut first).as_deref(), Some("second"));
789        assert_eq!(errors.observe_error(&mut first), None);
790        assert_eq!(errors.observe_error(&mut second), None);
791    }
792
793    #[test]
794    fn device_errors_coalesce_and_new_observers_see_latest() {
795        let errors = DeviceErrorState::default();
796        errors.record("older".into());
797        errors.record("latest".into());
798        let mut generation = 0;
799        assert_eq!(
800            errors.observe_error(&mut generation).as_deref(),
801            Some("latest")
802        );
803        assert_eq!(errors.observe_error(&mut generation), None);
804
805        let replacement = DeviceErrorState::default();
806        replacement.record("replacement".into());
807        generation = 0;
808        assert_eq!(
809            replacement.observe_error(&mut generation).as_deref(),
810            Some("replacement")
811        );
812        assert_eq!(replacement.observe_error(&mut generation), None);
813    }
814
815    #[test]
816    fn test_parse_device_id() {
817        assert!(parse_pci_id("0xABCD").is_ok());
818        assert!(parse_pci_id("ABCD").is_ok());
819        assert!(parse_pci_id("abcd").is_ok());
820        assert!(parse_pci_id("1234").is_ok());
821        assert!(parse_pci_id("123").is_err());
822        assert_eq!(
823            parse_pci_id(&format!("{:x}", 0x1234)).unwrap(),
824            parse_pci_id(&format!("{:X}", 0x1234)).unwrap(),
825        );
826
827        assert_eq!(
828            parse_pci_id(&format!("{:#x}", 0x1234)).unwrap(),
829            parse_pci_id(&format!("{:#X}", 0x1234)).unwrap(),
830        );
831    }
832}