Skip to main content

gpui_wgpu/
wgpu_context.rs

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