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            pollster::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", feature = "headless")
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", feature = "headless")
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            pollster::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", feature = "headless")
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
437        // A `Nv12` surface is one multi-planar image, a native format the device must be allowed to
438        // create; without the feature the import is refused and the surface dropped, rather than
439        // falling back to a pair of single-plane images the modifier does not describe.
440        if adapter
441            .features()
442            .contains(wgpu::Features::TEXTURE_FORMAT_NV12)
443        {
444            required_features |= wgpu::Features::TEXTURE_FORMAT_NV12;
445        }
446        #[cfg(target_family = "wasm")]
447        let required_limits = if adapter.get_info().backend == wgpu::Backend::Gl {
448            wgpu::Limits::downlevel_webgl2_defaults()
449                .using_resolution(adapter.limits())
450                .using_alignment(adapter.limits())
451        } else {
452            wgpu::Limits::downlevel_defaults()
453                .using_resolution(adapter.limits())
454                .using_alignment(adapter.limits())
455        };
456        #[cfg(not(target_family = "wasm"))]
457        let required_limits = wgpu::Limits::downlevel_defaults()
458            .using_resolution(adapter.limits())
459            .using_alignment(adapter.limits());
460
461        let descriptor = wgpu::DeviceDescriptor {
462            label: Some("gpui_device"),
463            required_features,
464            required_limits,
465            memory_hints: wgpu::MemoryHints::MemoryUsage,
466            trace: wgpu::Trace::Off,
467            experimental_features: wgpu::ExperimentalFeatures::disabled(),
468        };
469
470        // A producer's surface can carry a DRM format modifier, and wgpu's own device never enables
471        // `VK_EXT_image_drm_format_modifier`, so the import is refused. Where the platform allows it
472        // the device is created through the HAL instead, with that one extension added, and wgpu
473        // adopts it unchanged; see `device_with_surface_import`. The returned device is wgpu's own
474        // either way, so nothing downstream changes.
475        #[cfg(target_os = "linux")]
476        let with_surface_import = Self::device_with_surface_import(adapter, &descriptor);
477        #[cfg(not(target_os = "linux"))]
478        let with_surface_import: Option<(wgpu::Device, wgpu::Queue)> = None;
479
480        let (device, queue) = match with_surface_import {
481            Some(created) => created,
482            None => adapter
483                .request_device(&descriptor)
484                .await
485                .map_err(|e| anyhow::anyhow!("Failed to create wgpu device: {e}"))?,
486        };
487
488        Ok((
489            device,
490            queue,
491            dual_source_blending,
492            color_atlas_texture_format,
493        ))
494    }
495
496    /// Create the window's device through the HAL, enabling `VK_EXT_image_drm_format_modifier`.
497    ///
498    /// wgpu's `Adapter::request_device` never enables it, and its absence is what makes the importer
499    /// in `dmabuf.rs` refuse a tiled surface: a `DRM_FORMAT_MODIFIER_EXT` image cannot be created on a
500    /// device that does not have the extension. wgpu-hal exposes the two halves needed to create a
501    /// device that does — `Adapter::open_with_callback` mutates the extension list before
502    /// `vkCreateDevice`, and `wgpu::Adapter::create_device_from_hal` adopts the result back into a
503    /// `wgpu::Device` — so the extension is genuinely enabled, not assumed.
504    ///
505    /// `None` when the adapter is not Vulkan, when the driver does not support the extension, or when
506    /// either call refuses; the caller then creates the device the ordinary way and the import keeps
507    /// to linear buffers, as it did before.
508    #[cfg(target_os = "linux")]
509    fn device_with_surface_import(
510        adapter: &wgpu::Adapter,
511        descriptor: &wgpu::DeviceDescriptor<'_>,
512    ) -> Option<(wgpu::Device, wgpu::Queue)> {
513        use wgpu::hal::vulkan::Api;
514
515        // SAFETY: the HAL device is created from this adapter with the adapter's own features and
516        // limits (exactly what `descriptor` carries), and the callback adds only an extension the
517        // adapter reports it supports and never removes one. `create_device_from_hal` requires the
518        // HAL device to come from this adapter, which it does.
519        unsafe {
520            let hal = adapter.as_hal::<Api>()?;
521            if !hal
522                .physical_device_capabilities()
523                .supports_extension(ash::vk::EXT_IMAGE_DRM_FORMAT_MODIFIER_NAME)
524            {
525                return None;
526            }
527            let open = hal
528                .open_with_callback(
529                    descriptor.required_features,
530                    &descriptor.required_limits,
531                    &descriptor.memory_hints,
532                    Some(Box::new(|args| {
533                        args.extensions
534                            .push(ash::vk::EXT_IMAGE_DRM_FORMAT_MODIFIER_NAME);
535                    })),
536                )
537                .ok()?;
538            let (device, queue) = adapter
539                .create_device_from_hal::<Api>(open, descriptor)
540                .ok()?;
541            // The extension is the whole point of this path; if the device did not take it, fall back
542            // so the caller builds the ordinary device rather than leave the importer assuming a
543            // capability the device does not have.
544            let enabled = device.as_hal::<Api>().is_some_and(|device| {
545                device
546                    .enabled_device_extensions()
547                    .contains(&ash::vk::EXT_IMAGE_DRM_FORMAT_MODIFIER_NAME)
548            });
549            enabled.then_some((device, queue))
550        }
551    }
552
553    #[cfg(not(target_family = "wasm"))]
554    pub fn instance(display: Option<Box<dyn wgpu::wgt::WgpuHasDisplayHandle>>) -> wgpu::Instance {
555        wgpu::Instance::new(wgpu::InstanceDescriptor {
556            backends: Self::backends(),
557            flags: wgpu::InstanceFlags::default(),
558            backend_options: wgpu::BackendOptions::default(),
559            memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
560            display,
561        })
562    }
563
564    /// The backends the instance is built with.
565    ///
566    /// Vulkan and GL are what this asked for while Linux was the only host. Neither can present
567    /// on the other desktop platforms: on Windows they find no adapter at all, and on macOS they
568    /// cannot even build a surface from the view's raw handle. So each adds the API its window
569    /// system actually has. Without them `WgpuRenderer::new` fails at instance creation, before
570    /// a frame is reached, which is not something a window can recover from.
571    #[cfg(not(target_family = "wasm"))]
572    fn backends() -> wgpu::Backends {
573        let mut backends = wgpu::Backends::VULKAN | wgpu::Backends::GL;
574        if cfg!(target_os = "windows") {
575            backends |= wgpu::Backends::DX12;
576        }
577        if cfg!(target_os = "macos") {
578            backends |= wgpu::Backends::METAL;
579        }
580        backends
581    }
582
583    pub fn check_compatible_with_surface(&self, surface: &wgpu::Surface<'_>) -> anyhow::Result<()> {
584        let caps = surface.get_capabilities(&self.adapter);
585        if caps.formats.is_empty() {
586            let info = self.adapter.get_info();
587            anyhow::bail!(
588                "Adapter {:?} (backend={:?}, device={:#06x}) is not compatible with the \
589                 display surface for this window.",
590                info.name,
591                info.backend,
592                info.device,
593            );
594        }
595        Ok(())
596    }
597
598    /// Select an adapter and create a device, testing that the surface can actually be configured.
599    /// This is the only reliable way to determine compatibility on hybrid GPU systems, where
600    /// adapters may report surface compatibility via get_capabilities() but fail when actually
601    /// configuring (e.g., NVIDIA reporting Vulkan Wayland support but failing because the
602    /// Wayland compositor runs on the Intel GPU).
603    #[cfg(not(target_family = "wasm"))]
604    async fn select_adapter_and_device(
605        instance: &wgpu::Instance,
606        device_id_filter: Option<u32>,
607        surface: &wgpu::Surface<'_>,
608        compositor_gpu: Option<&CompositorGpuHint>,
609        reject_software: bool,
610    ) -> anyhow::Result<(
611        wgpu::Adapter,
612        wgpu::Device,
613        wgpu::Queue,
614        bool,
615        TextureFormat,
616    )> {
617        let mut adapters: Vec<_> = instance.enumerate_adapters(wgpu::Backends::all()).await;
618
619        if adapters.is_empty() {
620            anyhow::bail!("No GPU adapters found");
621        }
622
623        if let Some(device_id) = device_id_filter {
624            log::info!("ZED_DEVICE_ID filter: {:#06x}", device_id);
625        }
626
627        Self::sort_adapters(&mut adapters, device_id_filter, compositor_gpu);
628
629        // Log all available adapters (in sorted order)
630        log::info!("Found {} GPU adapter(s):", adapters.len());
631        for adapter in &adapters {
632            let info = adapter.get_info();
633            log::info!(
634                "  - {} (vendor={:#06x}, device={:#06x}, backend={:?}, type={:?})",
635                info.name,
636                info.vendor,
637                info.device,
638                info.backend,
639                info.device_type,
640            );
641        }
642
643        // Test each adapter by creating a device and configuring the surface
644        for adapter in adapters {
645            let info = adapter.get_info();
646
647            if reject_software && info.device_type == wgpu::DeviceType::Cpu {
648                log::info!(
649                    "Skipping software renderer: {} ({:?})",
650                    info.name,
651                    info.backend
652                );
653                continue;
654            }
655
656            log::info!("Testing adapter: {} ({:?})...", info.name, info.backend);
657
658            match Self::try_adapter_with_surface(&adapter, surface).await {
659                Ok((device, queue, dual_source_blending, color_atlas_texture_format)) => {
660                    log::info!(
661                        "Selected GPU (passed configuration test): {} ({:?})",
662                        info.name,
663                        info.backend
664                    );
665                    return Ok((
666                        adapter,
667                        device,
668                        queue,
669                        dual_source_blending,
670                        color_atlas_texture_format,
671                    ));
672                }
673                Err(e) => {
674                    log::info!(
675                        "  Adapter {} ({:?}) failed: {}, trying next...",
676                        info.name,
677                        info.backend,
678                        e
679                    );
680                }
681            }
682        }
683
684        anyhow::bail!("No GPU adapter found that can configure the display surface")
685    }
686
687    /// Sort adapters into a single priority order. Tiers (from highest to lowest):
688    ///
689    /// 1. ZED_DEVICE_ID match — explicit user override
690    /// 2. Compositor GPU match — the GPU the display server is rendering on
691    /// 3. Device type (Discrete > Integrated > Other > Virtual > Cpu).
692    ///    "Other" ranks above "Virtual" because OpenGL seems to count as "Other".
693    /// 4. Backend — prefer Vulkan/Metal/Dx12 over GL/etc.
694    #[cfg(not(target_family = "wasm"))]
695    fn sort_adapters(
696        adapters: &mut [wgpu::Adapter],
697        device_id_filter: Option<u32>,
698        compositor_gpu: Option<&CompositorGpuHint>,
699    ) {
700        adapters.sort_by_key(|adapter| {
701            let info = adapter.get_info();
702            // Backends like OpenGL report device=0 for all adapters, so
703            // device-based matching is only meaningful when non-zero.
704            let device_known = info.device != 0;
705            let user_override: u8 = match device_id_filter {
706                Some(id) if device_known && info.device == id => 0,
707                _ => 1,
708            };
709            let compositor_match: u8 = match compositor_gpu {
710                Some(hint)
711                    if device_known
712                        && info.vendor == hint.vendor_id
713                        && info.device == hint.device_id =>
714                {
715                    0
716                }
717                _ => 1,
718            };
719            let type_priority: u8 = match info.device_type {
720                wgpu::DeviceType::DiscreteGpu => 0,
721                wgpu::DeviceType::IntegratedGpu => 1,
722                wgpu::DeviceType::Other => 2,
723                wgpu::DeviceType::VirtualGpu => 3,
724                wgpu::DeviceType::Cpu => 4,
725            };
726            let backend_priority: u8 = match info.backend {
727                wgpu::Backend::Vulkan | wgpu::Backend::Metal | wgpu::Backend::Dx12 => 0,
728                _ => 1,
729            };
730
731            (
732                user_override,
733                compositor_match,
734                type_priority,
735                backend_priority,
736            )
737        });
738    }
739
740    /// Try to use an adapter with a surface by creating a device and testing configuration.
741    /// Returns the device and queue if successful, allowing them to be reused.
742    #[cfg(not(target_family = "wasm"))]
743    async fn try_adapter_with_surface(
744        adapter: &wgpu::Adapter,
745        surface: &wgpu::Surface<'_>,
746    ) -> anyhow::Result<(wgpu::Device, wgpu::Queue, bool, TextureFormat)> {
747        let caps = surface.get_capabilities(adapter);
748        if caps.formats.is_empty() {
749            anyhow::bail!("no compatible surface formats");
750        }
751        if caps.alpha_modes.is_empty() {
752            anyhow::bail!("no compatible alpha modes");
753        }
754
755        let (device, queue, dual_source_blending, color_atlas_texture_format) =
756            Self::create_device(adapter).await?;
757        let error_scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
758
759        let test_config = wgpu::SurfaceConfiguration {
760            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
761            format: caps.formats[0],
762            width: 64,
763            height: 64,
764            present_mode: wgpu::PresentMode::Fifo,
765            desired_maximum_frame_latency: 2,
766            alpha_mode: caps.alpha_modes[0],
767            view_formats: vec![],
768        };
769
770        surface.configure(&device, &test_config);
771
772        let error = error_scope.pop().await;
773        if let Some(e) = error {
774            anyhow::bail!("surface configuration failed: {e}");
775        }
776
777        Ok((
778            device,
779            queue,
780            dual_source_blending,
781            color_atlas_texture_format,
782        ))
783    }
784
785    fn select_color_texture_format(adapter: &wgpu::Adapter) -> anyhow::Result<wgpu::TextureFormat> {
786        let required_usages = wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST;
787        let bgra_features = adapter.get_texture_format_features(wgpu::TextureFormat::Bgra8Unorm);
788        let rgba_features = adapter.get_texture_format_features(wgpu::TextureFormat::Rgba8Unorm);
789        #[cfg(target_family = "wasm")]
790        if adapter.get_info().backend == wgpu::Backend::Gl
791            && rgba_features.allowed_usages.contains(required_usages)
792        {
793            return Ok(wgpu::TextureFormat::Rgba8Unorm);
794        }
795        if bgra_features.allowed_usages.contains(required_usages) {
796            return Ok(wgpu::TextureFormat::Bgra8Unorm);
797        }
798        if rgba_features.allowed_usages.contains(required_usages) {
799            let info = adapter.get_info();
800            log::warn!(
801                "Adapter {} ({:?}) does not support Bgra8Unorm atlas textures with usages {:?}; \
802                 falling back to Rgba8Unorm atlas textures.",
803                info.name,
804                info.backend,
805                required_usages,
806            );
807            return Ok(wgpu::TextureFormat::Rgba8Unorm);
808        }
809
810        let info = adapter.get_info();
811        Err(anyhow::anyhow!(
812            "Adapter {} ({:?}, device={:#06x}) does not support a usable color atlas texture \
813             format with usages {:?}. Bgra8Unorm allowed usages: {:?}; \
814             Rgba8Unorm allowed usages: {:?}.",
815            info.name,
816            info.backend,
817            info.device,
818            required_usages,
819            bgra_features.allowed_usages,
820            rgba_features.allowed_usages,
821        ))
822    }
823    pub fn backend(&self) -> WgpuBackend {
824        self.backend
825    }
826
827    pub fn uses_webgl_instance_data(&self) -> bool {
828        matches!(self.backend, WgpuBackend::Gl) && cfg!(target_family = "wasm")
829    }
830
831    pub fn supports_dual_source_blending(&self) -> bool {
832        self.dual_source_blending
833    }
834
835    pub fn color_texture_format(&self) -> wgpu::TextureFormat {
836        self.color_texture_format
837    }
838
839    /// Returns true if the GPU device was lost (e.g., due to driver crash, suspend/resume).
840    /// When this returns true, the context should be recreated.
841    pub fn device_lost(&self) -> bool {
842        self.errors.device_lost()
843    }
844
845    /// The device-wide error state, shared with renderers on this device.
846    pub fn errors(&self) -> &Arc<DeviceErrorState> {
847        &self.errors
848    }
849}
850
851#[cfg(not(target_family = "wasm"))]
852fn parse_pci_id(id: &str) -> anyhow::Result<u32> {
853    let mut id = id.trim();
854
855    if id.starts_with("0x") || id.starts_with("0X") {
856        id = &id[2..];
857    }
858    let is_hex_string = id.chars().all(|c| c.is_ascii_hexdigit());
859    let is_4_chars = id.len() == 4;
860    anyhow::ensure!(
861        is_4_chars && is_hex_string,
862        "Expected a 4 digit PCI ID in hexadecimal format"
863    );
864
865    u32::from_str_radix(id, 16).context("parsing PCI ID as hex")
866}
867
868#[cfg(test)]
869mod tests {
870    use super::{DeviceErrorState, parse_pci_id};
871
872    /// The escape hatch that the whole tiled-surface path rests on: a device created through the HAL
873    /// with `VK_EXT_image_drm_format_modifier` added must come back with the extension enabled.
874    #[cfg(target_os = "linux")]
875    #[test]
876    fn the_device_enables_the_drm_format_modifier_extension() {
877        use wgpu::hal::vulkan::Api;
878
879        let instance = super::WgpuContext::instance(None);
880        let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
881            power_preference: wgpu::PowerPreference::LowPower,
882            compatible_surface: None,
883            force_fallback_adapter: false,
884        }));
885        let Ok(adapter) = adapter else {
886            eprintln!("skipping: no adapter");
887            return;
888        };
889        let supported = unsafe {
890            adapter.as_hal::<Api>().is_some_and(|hal| {
891                hal.physical_device_capabilities()
892                    .supports_extension(ash::vk::EXT_IMAGE_DRM_FORMAT_MODIFIER_NAME)
893            })
894        };
895        if !supported {
896            eprintln!(
897                "skipping: {:?} does not support the extension",
898                adapter.get_info().name
899            );
900            return;
901        }
902
903        let descriptor = wgpu::DeviceDescriptor {
904            label: Some("drm modifier extension test"),
905            required_features: wgpu::Features::empty(),
906            required_limits: wgpu::Limits::downlevel_defaults()
907                .using_resolution(adapter.limits())
908                .using_alignment(adapter.limits()),
909            memory_hints: wgpu::MemoryHints::MemoryUsage,
910            trace: wgpu::Trace::Off,
911            experimental_features: wgpu::ExperimentalFeatures::disabled(),
912        };
913        let (device, _queue) =
914            super::WgpuContext::device_with_surface_import(&adapter, &descriptor)
915                .expect("the escape hatch creates the device");
916        let enabled = unsafe { device.as_hal::<Api>() }.is_some_and(|device| {
917            device
918                .enabled_device_extensions()
919                .contains(&ash::vk::EXT_IMAGE_DRM_FORMAT_MODIFIER_NAME)
920        });
921        assert!(enabled, "the created device must have the extension enabled");
922    }
923
924    #[test]
925    fn device_errors_are_observed_independently() {
926        let errors = DeviceErrorState::default();
927        let mut first = 0;
928        let mut second = 0;
929        assert_eq!(errors.observe_error(&mut first), None);
930        errors.record("first".into());
931        assert_eq!(errors.observe_error(&mut first).as_deref(), Some("first"));
932        assert_eq!(errors.observe_error(&mut first), None);
933        assert_eq!(errors.observe_error(&mut second).as_deref(), Some("first"));
934        assert_eq!(errors.observe_error(&mut second), None);
935
936        errors.record("second".into());
937        assert_eq!(errors.observe_error(&mut second).as_deref(), Some("second"));
938        assert_eq!(errors.observe_error(&mut first).as_deref(), Some("second"));
939        assert_eq!(errors.observe_error(&mut first), None);
940        assert_eq!(errors.observe_error(&mut second), None);
941    }
942
943    #[test]
944    fn device_errors_coalesce_and_new_observers_see_latest() {
945        let errors = DeviceErrorState::default();
946        errors.record("older".into());
947        errors.record("latest".into());
948        let mut generation = 0;
949        assert_eq!(
950            errors.observe_error(&mut generation).as_deref(),
951            Some("latest")
952        );
953        assert_eq!(errors.observe_error(&mut generation), None);
954
955        let replacement = DeviceErrorState::default();
956        replacement.record("replacement".into());
957        generation = 0;
958        assert_eq!(
959            replacement.observe_error(&mut generation).as_deref(),
960            Some("replacement")
961        );
962        assert_eq!(replacement.observe_error(&mut generation), None);
963    }
964
965    #[test]
966    fn test_parse_device_id() {
967        assert!(parse_pci_id("0xABCD").is_ok());
968        assert!(parse_pci_id("ABCD").is_ok());
969        assert!(parse_pci_id("abcd").is_ok());
970        assert!(parse_pci_id("1234").is_ok());
971        assert!(parse_pci_id("123").is_err());
972        assert_eq!(
973            parse_pci_id(&format!("{:x}", 0x1234)).unwrap(),
974            parse_pci_id(&format!("{:X}", 0x1234)).unwrap(),
975        );
976
977        assert_eq!(
978            parse_pci_id(&format!("{:#x}", 0x1234)).unwrap(),
979            parse_pci_id(&format!("{:#X}", 0x1234)).unwrap(),
980        );
981    }
982}