Skip to main content

gpui_wgpu/
wgpu_context.rs

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