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