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