Skip to main content

gpui_wgpu/
wgpu_context.rs

1#[cfg(not(target_family = "wasm"))]
2use gpui_platform::{
3    GpuAdapterInfo, GpuBackend, GpuDeviceType, GpuId, GpuPreference, PermanentGpuId, SessionGpuId,
4};
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
129/// Describe one adapter in the terms `gpui_platform` publishes.
130///
131/// `wgpu`'s vocabulary stops at this crate's boundary: `gpui_platform` is compiled for backends
132/// that have no `wgpu`, and an application choosing a GPU should not have to depend on the one the
133/// Linux renderer happens to use.
134#[cfg(not(target_family = "wasm"))]
135pub fn adapter_info(info: &wgpu::AdapterInfo) -> GpuAdapterInfo {
136    GpuAdapterInfo::new(
137        GpuId {
138            permanent: PermanentGpuId::Pci {
139                vendor_id: Some(info.vendor),
140                device_id: info.device,
141                subsys_id: None,
142                revision: None,
143            },
144            session: Some(session_for(info)),
145        },
146        info.name.clone(),
147        match info.device_type {
148            wgpu::DeviceType::DiscreteGpu => GpuDeviceType::Discrete,
149            wgpu::DeviceType::IntegratedGpu => GpuDeviceType::Integrated,
150            wgpu::DeviceType::VirtualGpu => GpuDeviceType::Virtual,
151            wgpu::DeviceType::Cpu => GpuDeviceType::Cpu,
152            wgpu::DeviceType::Other => GpuDeviceType::Other,
153        },
154        match info.backend {
155            wgpu::Backend::Vulkan => GpuBackend::Vulkan,
156            wgpu::Backend::Metal => GpuBackend::Metal,
157            wgpu::Backend::Dx12 => GpuBackend::Dx12,
158            wgpu::Backend::Gl => GpuBackend::Gl,
159            wgpu::Backend::BrowserWebGpu => GpuBackend::BrowserWebGpu,
160            wgpu::Backend::Noop => GpuBackend::Other,
161        },
162        info.device_type == wgpu::DeviceType::Cpu,
163    )
164}
165
166/// The per-session handle for `info`.
167///
168/// Linux names the DRM render node, which is the identity `gpui_interop` matches on. `wgpu` does
169/// not expose the node, but its Vulkan backend reports the PCI bus id, and udev's `by-path` directory
170/// links that to the render node. Where the link is absent (a GL adapter, a system without udev), the
171/// PCI pair is the best handle available; it is a real session handle, just not unique between two
172/// identical cards.
173#[cfg(target_os = "linux")]
174fn session_for(info: &wgpu::AdapterInfo) -> SessionGpuId {
175    if !info.device_pci_bus_id.is_empty()
176        && let Ok(target) = std::fs::read_link(format!(
177            "/dev/dri/by-path/pci-{}-render",
178            info.device_pci_bus_id
179        ))
180        && let Some(minor) = target
181            .file_name()
182            .and_then(|name| name.to_str())
183            .and_then(|name| name.strip_prefix("renderD"))
184            .and_then(|minor| minor.parse().ok())
185    {
186        return SessionGpuId::Drm {
187            render_minor: minor,
188        };
189    }
190    SessionGpuId::Pci {
191        vendor_id: info.vendor,
192        device_id: info.device,
193    }
194}
195
196#[cfg(not(target_os = "linux"))]
197fn session_for(info: &wgpu::AdapterInfo) -> SessionGpuId {
198    SessionGpuId::Pci {
199        vendor_id: info.vendor,
200        device_id: info.device,
201    }
202}
203
204/// Every adapter this process could render on.
205///
206/// It creates an instance and enumerates, but no device: that is the cost enumeration exists to
207/// avoid, and the reason the answer carries identity and class only. A caller that already has a
208/// context should use [`enumerate_adapters_from`] with its instance, so the list is the one that
209/// context could actually have chosen from.
210#[cfg(not(target_family = "wasm"))]
211pub fn enumerate_adapters() -> Vec<GpuAdapterInfo> {
212    enumerate_adapters_from(&WgpuContext::instance(None))
213}
214
215/// Every adapter `instance` reports, in the backend's order.
216#[cfg(not(target_family = "wasm"))]
217pub fn enumerate_adapters_from(instance: &wgpu::Instance) -> Vec<GpuAdapterInfo> {
218    pollster::block_on(instance.enumerate_adapters(wgpu::Backends::all()))
219        .iter()
220        .map(|adapter| adapter_info(&adapter.get_info()))
221        .collect()
222}
223
224impl WgpuContext {
225    /// What this device can import as a dma-buf surface.
226    ///
227    /// The renderer's own report, asked on the device it already made: no device is created. `None`
228    /// where the importer has no arm for the backend (anything but Vulkan).
229    #[cfg(target_os = "linux")]
230    pub fn dmabuf_capabilities(&self) -> Option<gpui_engine::DmaBufCapabilities> {
231        crate::dmabuf::dmabuf_capabilities(&self.device)
232    }
233
234    /// Describe the adapter this context renders on, in the terms `gpui_platform` publishes.
235    #[cfg(not(target_family = "wasm"))]
236    pub fn adapter_info(&self) -> GpuAdapterInfo {
237        adapter_info(&self.adapter.get_info())
238    }
239
240    #[cfg(not(target_family = "wasm"))]
241    pub fn new(
242        instance: wgpu::Instance,
243        surface: &wgpu::Surface<'_>,
244        compositor_gpu: Option<CompositorGpuHint>,
245        gpu_preference: GpuPreference,
246    ) -> anyhow::Result<Self> {
247        Self::new_with_options(instance, surface, compositor_gpu, gpu_preference, false)
248    }
249
250    #[cfg(not(target_family = "wasm"))]
251    pub fn new_rejecting_software(
252        instance: wgpu::Instance,
253        surface: &wgpu::Surface<'_>,
254        compositor_gpu: Option<CompositorGpuHint>,
255        gpu_preference: GpuPreference,
256    ) -> anyhow::Result<Self> {
257        Self::new_with_options(instance, surface, compositor_gpu, gpu_preference, true)
258    }
259
260    #[cfg(not(target_family = "wasm"))]
261    fn new_with_options(
262        instance: wgpu::Instance,
263        surface: &wgpu::Surface<'_>,
264        compositor_gpu: Option<CompositorGpuHint>,
265        gpu_preference: GpuPreference,
266        reject_software: bool,
267    ) -> anyhow::Result<Self> {
268        // Select an adapter by actually testing surface configuration with the real device.
269        // This is the only reliable way to determine compatibility on hybrid GPU systems.
270        let (adapter, device, queue, dual_source_blending, color_texture_format) =
271            pollster::block_on(Self::select_adapter_and_device(
272                &instance,
273                &gpu_preference,
274                surface,
275                compositor_gpu.as_ref(),
276                reject_software,
277            ))?;
278
279        Ok(Self::from_native_adapter_and_device(
280            instance,
281            adapter,
282            device,
283            queue,
284            dual_source_blending,
285            color_texture_format,
286        ))
287    }
288
289    /// The headless context, selecting the adapter by `gpu_preference`.
290    ///
291    /// The context is cached process-wide, so only the first caller's preference takes effect: a
292    /// later call gets the device that already exists. Headless has no platform to state a
293    /// preference through `set_gpu_preference`, so a caller that must pin a GPU — the dma-buf tests
294    /// pinning one card of a hybrid laptop — passes it here.
295    #[cfg(all(
296        not(target_family = "wasm"),
297        any(
298            test,
299            feature = "bench-support",
300            feature = "test-support",
301            feature = "headless"
302        )
303    ))]
304    pub(crate) fn new_headless_with(
305        gpu_preference: GpuPreference,
306    ) -> anyhow::Result<(Self, wgpu::TextureFormat)> {
307        // Creating a device takes tens of milliseconds, and benchmarks create a headless
308        // renderer for every window they open, so the process shares one device until it
309        // is lost. A static rather than a thread-local: the cached device is never dropped,
310        // because wgpu's queue reads its own thread-locals on drop and would panic if
311        // dropped while a thread's thread-locals are being destroyed.
312        static HEADLESS: Mutex<Option<(WgpuContext, wgpu::TextureFormat)>> = Mutex::new(None);
313        let mut headless = HEADLESS.lock();
314        if let Some((context, format)) = headless.as_ref()
315            && !context.device_lost()
316        {
317            return Ok((context.clone(), *format));
318        }
319        let created = Self::create_headless(gpu_preference)?;
320        *headless = Some(created.clone());
321        Ok(created)
322    }
323
324    #[cfg(all(
325        not(target_family = "wasm"),
326        any(
327            test,
328            feature = "bench-support",
329            feature = "test-support",
330            feature = "headless"
331        )
332    ))]
333    fn create_headless(
334        gpu_preference: GpuPreference,
335    ) -> anyhow::Result<(Self, wgpu::TextureFormat)> {
336        let instance = Self::instance(None);
337        let (adapter, device, queue, dual_source_blending, color_texture_format, target_format) =
338            pollster::block_on(async {
339                let mut adapters = instance.enumerate_adapters(wgpu::Backends::all()).await;
340                Self::order_adapters(&mut adapters, &gpu_preference, None)?;
341
342                for adapter in adapters {
343                    let adapter_info = adapter.get_info();
344                    let Some(target_format) = Self::headless_target_format(&adapter) else {
345                        log::warn!(
346                            "Adapter {:?} has no supported headless render target format",
347                            adapter_info.name
348                        );
349                        continue;
350                    };
351
352                    match Self::create_device(&adapter).await {
353                        Ok((device, queue, dual_source_blending, color_texture_format)) => {
354                            #[cfg(feature = "bench-support")]
355                            if adapter_info.device_type == wgpu::DeviceType::Cpu {
356                                log::error!(
357                                    "Headless renderer selected software adapter {:?}; \
358                                     benchmark results measure CPU software rendering, not hardware GPU rendering",
359                                    adapter_info.name
360                                );
361                            }
362                            return Ok((
363                                adapter,
364                                device,
365                                queue,
366                                dual_source_blending,
367                                color_texture_format,
368                                target_format,
369                            ));
370                        }
371                        Err(error) => {
372                            log::warn!(
373                                "Failed to create a headless device for adapter {:?}: {error:#}",
374                                adapter_info.name
375                            );
376                        }
377                    }
378                }
379
380                anyhow::bail!("No usable headless GPU adapter found")
381            })?;
382
383        Ok((
384            Self::from_native_adapter_and_device(
385                instance,
386                adapter,
387                device,
388                queue,
389                dual_source_blending,
390                color_texture_format,
391            ),
392            target_format,
393        ))
394    }
395
396    #[cfg(all(
397        not(target_family = "wasm"),
398        any(
399            test,
400            feature = "bench-support",
401            feature = "test-support",
402            feature = "headless"
403        )
404    ))]
405    /// Both candidates are 8-bit RGBA-ordered or BGRA-ordered formats: headless readback
406    /// copies rows as 4 bytes per pixel and only swizzles, so no other formats may be added
407    /// here without updating it.
408    fn headless_target_format(adapter: &wgpu::Adapter) -> Option<wgpu::TextureFormat> {
409        let required_usages =
410            wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC;
411        [
412            wgpu::TextureFormat::Rgba8Unorm,
413            wgpu::TextureFormat::Bgra8Unorm,
414        ]
415        .into_iter()
416        .find(|format| {
417            adapter
418                .get_texture_format_features(*format)
419                .allowed_usages
420                .contains(required_usages)
421        })
422    }
423
424    #[cfg(not(target_family = "wasm"))]
425    fn from_native_adapter_and_device(
426        instance: wgpu::Instance,
427        adapter: wgpu::Adapter,
428        device: wgpu::Device,
429        queue: wgpu::Queue,
430        dual_source_blending: bool,
431        color_texture_format: TextureFormat,
432    ) -> Self {
433        let errors = DeviceErrorState::install(&device);
434
435        log::info!(
436            "Selected GPU adapter: {:?} ({:?})",
437            adapter.get_info().name,
438            adapter.get_info().backend
439        );
440        let backend = WgpuBackend::Native(adapter.get_info().backend);
441
442        Self {
443            instance,
444            adapter,
445            device: Arc::new(device),
446            queue: Arc::new(queue),
447            backend,
448            dual_source_blending,
449            color_texture_format,
450            errors,
451        }
452    }
453
454    #[cfg(target_family = "wasm")]
455    pub async fn new_web(
456        canvas: &web_sys::HtmlCanvasElement,
457        preference: WebBackendPreference,
458    ) -> anyhow::Result<PreparedWebGraphics> {
459        Self::new_web_with_backend(canvas, preference).await
460    }
461
462    #[cfg(target_family = "wasm")]
463    #[allow(clippy::arc_with_non_send_sync)]
464    async fn new_web_with_backend(
465        canvas: &web_sys::HtmlCanvasElement,
466        preference: WebBackendPreference,
467    ) -> anyhow::Result<PreparedWebGraphics> {
468        let backends = match preference {
469            WebBackendPreference::Auto => wgpu::Backends::BROWSER_WEBGPU | wgpu::Backends::GL,
470            WebBackendPreference::WebGpu => wgpu::Backends::BROWSER_WEBGPU,
471            WebBackendPreference::WebGl => wgpu::Backends::GL,
472        };
473        let descriptor = wgpu::InstanceDescriptor {
474            backends,
475            flags: wgpu::InstanceFlags::default(),
476            backend_options: wgpu::BackendOptions::default(),
477            memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
478            display: Some(Box::new(WebDisplaySource)),
479        };
480        let instance = if preference == WebBackendPreference::Auto {
481            wgpu::util::new_instance_with_webgpu_detection(descriptor).await
482        } else {
483            wgpu::Instance::new(descriptor)
484        };
485        let surface = instance
486            .create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone()))
487            .map_err(|error| {
488                anyhow::anyhow!("Failed to create browser graphics surface: {error}")
489            })?;
490
491        let adapter = instance
492            .request_adapter(&wgpu::RequestAdapterOptions {
493                power_preference: wgpu::PowerPreference::HighPerformance,
494                compatible_surface: Some(&surface),
495                force_fallback_adapter: false,
496                apply_limit_buckets: false,
497            })
498            .await
499            .map_err(|error| {
500                anyhow::anyhow!(
501                    "Failed to request a {preference:?} adapter compatible with the canvas: {error}"
502                )
503            })?;
504        let adapter_info = adapter.get_info();
505        let backend = match adapter_info.backend {
506            wgpu::Backend::BrowserWebGpu => WgpuBackend::BrowserWebGpu,
507            wgpu::Backend::Gl => WgpuBackend::Gl,
508            backend => {
509                anyhow::bail!(
510                    "Browser graphics initialization selected unexpected backend {backend:?}"
511                )
512            }
513        };
514
515        let (device, queue, dual_source_blending, color_texture_format) =
516            Self::create_device(&adapter).await?;
517        let errors = DeviceErrorState::install(&device);
518        log::info!(
519            "Browser graphics initialized: requested={preference:?}, selected={backend:?}, \
520             adapter={:?}, limits={:?}, dual_source_blending={dual_source_blending}",
521            adapter_info.name,
522            device.limits(),
523        );
524
525        let context = Self {
526            instance,
527            adapter,
528            device: Arc::new(device),
529            queue: Arc::new(queue),
530            backend,
531            dual_source_blending,
532            color_texture_format,
533            errors,
534        };
535        Ok(PreparedWebGraphics { context, surface })
536    }
537
538    async fn create_device(
539        adapter: &wgpu::Adapter,
540    ) -> anyhow::Result<(wgpu::Device, wgpu::Queue, bool, TextureFormat)> {
541        let dual_source_blending = adapter
542            .features()
543            .contains(wgpu::Features::DUAL_SOURCE_BLENDING);
544
545        let mut required_features = wgpu::Features::empty();
546        if dual_source_blending {
547            required_features |= wgpu::Features::DUAL_SOURCE_BLENDING;
548        } else {
549            log::warn!(
550                "Dual-source blending not available on this GPU. \
551                Subpixel text antialiasing will be disabled."
552            );
553        }
554
555        let color_atlas_texture_format = Self::select_color_texture_format(adapter)?;
556
557        // A `Nv12` surface is one multi-planar image, a native format the device must be allowed to
558        // create; without the feature the import is refused and the surface dropped, rather than
559        // falling back to a pair of single-plane images the modifier does not describe.
560        if adapter
561            .features()
562            .contains(wgpu::Features::TEXTURE_FORMAT_NV12)
563        {
564            required_features |= wgpu::Features::TEXTURE_FORMAT_NV12;
565        }
566
567        // Storage textures follow the adapter's format capabilities, not the WebGPU-guaranteed
568        // subset. Bevy's post-processing pipelines write R16Float and Rgba16Float, which the
569        // guaranteed set disallows; the same feature is enabled by Bevy's own device. It is a
570        // no-op for the renderer itself, and skipped where the adapter lacks it.
571        if adapter
572            .features()
573            .contains(wgpu::Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES)
574        {
575            required_features |= wgpu::Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES;
576        }
577        #[cfg(target_family = "wasm")]
578        let required_limits = if adapter.get_info().backend == wgpu::Backend::Gl {
579            wgpu::Limits::downlevel_webgl2_defaults()
580                .using_resolution(adapter.limits())
581                .using_alignment(adapter.limits())
582        } else {
583            wgpu::Limits::downlevel_defaults()
584                .using_resolution(adapter.limits())
585                .using_alignment(adapter.limits())
586        };
587        #[cfg(not(target_family = "wasm"))]
588        let required_limits = {
589            let supported = adapter.limits();
590            let mut limits = wgpu::Limits::downlevel_defaults()
591                .using_resolution(supported.clone())
592                .using_alignment(supported.clone());
593            // `downlevel_defaults` is enough for the renderer, but it is not enough to *host* one.
594            // Bevy's clustered-forward PBR pass alone asks for 5–8 storage buffers per stage, a
595            // 64 KiB uniform binding and 16 inter-stage variables; the downlevel set stops at 4,
596            // 16 KiB and 15. Raise only the fields that differ, and only to what the adapter
597            // reports, so the request stays sound. GL keeps the downlevel set: its storage-buffer
598            // support is exactly what the downlevel set exists to describe.
599            if adapter.get_info().backend != wgpu::Backend::Gl {
600                limits.max_storage_buffers_per_shader_stage =
601                    supported.max_storage_buffers_per_shader_stage;
602                limits.max_storage_textures_per_shader_stage =
603                    supported.max_storage_textures_per_shader_stage;
604                limits.max_uniform_buffer_binding_size = supported.max_uniform_buffer_binding_size;
605                limits.max_inter_stage_shader_variables =
606                    supported.max_inter_stage_shader_variables;
607                limits.max_color_attachments = supported.max_color_attachments;
608            }
609            limits
610        };
611
612        let descriptor = wgpu::DeviceDescriptor {
613            label: Some("gpui_device"),
614            required_features,
615            required_limits,
616            memory_hints: wgpu::MemoryHints::MemoryUsage,
617            trace: wgpu::Trace::Off,
618            experimental_features: wgpu::ExperimentalFeatures::disabled(),
619        };
620
621        // A producer's surface can carry a DRM format modifier, and wgpu's own device never enables
622        // `VK_EXT_image_drm_format_modifier`, so the import is refused. Where the platform allows it
623        // the device is created through the HAL instead, with that one extension added, and wgpu
624        // adopts it unchanged; see `device_with_surface_import`. The returned device is wgpu's own
625        // either way, so nothing downstream changes.
626        #[cfg(target_os = "linux")]
627        let with_surface_import = Self::device_with_surface_import(adapter, &descriptor);
628        #[cfg(not(target_os = "linux"))]
629        let with_surface_import: Option<(wgpu::Device, wgpu::Queue)> = None;
630
631        let (device, queue) = match with_surface_import {
632            Some(created) => created,
633            None => adapter
634                .request_device(&descriptor)
635                .await
636                .map_err(|e| anyhow::anyhow!("Failed to create wgpu device: {e}"))?,
637        };
638
639        Ok((
640            device,
641            queue,
642            dual_source_blending,
643            color_atlas_texture_format,
644        ))
645    }
646
647    /// Create the window's device through the HAL, enabling `VK_EXT_image_drm_format_modifier`.
648    ///
649    /// wgpu's `Adapter::request_device` never enables it, and its absence is what makes the importer
650    /// in `dmabuf.rs` refuse a tiled surface: a `DRM_FORMAT_MODIFIER_EXT` image cannot be created on a
651    /// device that does not have the extension. The same call also enables
652    /// `VK_KHR_external_semaphore_fd` where the driver supports it, because the producer half of the
653    /// surface path exports its acquire fence from a semaphore signalled by the queue. wgpu-hal exposes
654    /// the two halves needed to create a device with both — `Adapter::open_with_callback` mutates the
655    /// extension list before `vkCreateDevice`, and `wgpu::Adapter::create_device_from_hal` adopts the
656    /// result back into a `wgpu::Device` — so the extensions are genuinely enabled, not assumed.
657    ///
658    /// `None` when the adapter is not Vulkan, when the driver does not support the extension, or when
659    /// either call refuses; the caller then creates the device the ordinary way and the import keeps
660    /// to linear buffers, as it did before.
661    #[cfg(target_os = "linux")]
662    fn device_with_surface_import(
663        adapter: &wgpu::Adapter,
664        descriptor: &wgpu::DeviceDescriptor<'_>,
665    ) -> Option<(wgpu::Device, wgpu::Queue)> {
666        use wgpu::hal::vulkan::Api;
667
668        // SAFETY: the HAL device is created from this adapter with the adapter's own features and
669        // limits (exactly what `descriptor` carries), and the callback adds only an extension the
670        // adapter reports it supports and never removes one. `create_device_from_hal` requires the
671        // HAL device to come from this adapter, which it does.
672        unsafe {
673            let hal = adapter.as_hal::<Api>()?;
674            if !hal
675                .physical_device_capabilities()
676                .supports_extension(ash::vk::EXT_IMAGE_DRM_FORMAT_MODIFIER_NAME)
677            {
678                return None;
679            }
680            // The export half of a shared surface needs a fence the producer can hand over as a
681            // `sync_file`. wgpu-hal's queue can signal a semaphore on the submission that rendered
682            // the frame, and `VK_KHR_external_semaphore_fd` is what turns that signal into one. It
683            // is added only where the driver supports it; a producer that asks for it checks the
684            // device's enabled list and refuses rather than exporting without a fence.
685            let with_semaphore_fd = hal
686                .physical_device_capabilities()
687                .supports_extension(ash::vk::KHR_EXTERNAL_SEMAPHORE_FD_NAME);
688            let open = hal
689                .open_with_callback(
690                    descriptor.required_features,
691                    &descriptor.required_limits,
692                    &descriptor.memory_hints,
693                    Some(Box::new(move |args| {
694                        args.extensions
695                            .push(ash::vk::EXT_IMAGE_DRM_FORMAT_MODIFIER_NAME);
696                        if with_semaphore_fd {
697                            args.extensions
698                                .push(ash::vk::KHR_EXTERNAL_SEMAPHORE_FD_NAME);
699                        }
700                    })),
701                )
702                .ok()?;
703            let (device, queue) = adapter
704                .create_device_from_hal::<Api>(open, descriptor)
705                .ok()?;
706            // The extension is the whole point of this path; if the device did not take it, fall back
707            // so the caller builds the ordinary device rather than leave the importer assuming a
708            // capability the device does not have.
709            let enabled = device.as_hal::<Api>().is_some_and(|device| {
710                device
711                    .enabled_device_extensions()
712                    .contains(&ash::vk::EXT_IMAGE_DRM_FORMAT_MODIFIER_NAME)
713            });
714            enabled.then_some((device, queue))
715        }
716    }
717
718    #[cfg(not(target_family = "wasm"))]
719    pub fn instance(display: Option<Box<dyn wgpu::wgt::WgpuHasDisplayHandle>>) -> wgpu::Instance {
720        wgpu::Instance::new(wgpu::InstanceDescriptor {
721            backends: Self::backends(),
722            flags: wgpu::InstanceFlags::default(),
723            backend_options: wgpu::BackendOptions::default(),
724            memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
725            display,
726        })
727    }
728
729    /// The backends the instance is built with.
730    ///
731    /// Vulkan and GL are what this asked for while Linux was the only host. Neither can present
732    /// on the other desktop platforms: on Windows they find no adapter at all, and on macOS they
733    /// cannot even build a surface from the view's raw handle. So each adds the API its window
734    /// system actually has. Without them `WgpuRenderer::new` fails at instance creation, before
735    /// a frame is reached, which is not something a window can recover from.
736    #[cfg(not(target_family = "wasm"))]
737    fn backends() -> wgpu::Backends {
738        let mut backends = wgpu::Backends::VULKAN | wgpu::Backends::GL;
739        if cfg!(target_os = "windows") {
740            backends |= wgpu::Backends::DX12;
741        }
742        if cfg!(target_os = "macos") {
743            backends |= wgpu::Backends::METAL;
744        }
745        backends
746    }
747
748    pub fn check_compatible_with_surface(&self, surface: &wgpu::Surface<'_>) -> anyhow::Result<()> {
749        let caps = surface.get_capabilities(&self.adapter);
750        if caps.formats.is_empty() {
751            let info = self.adapter.get_info();
752            anyhow::bail!(
753                "Adapter {:?} (backend={:?}, device={:#06x}) is not compatible with the \
754                 display surface for this window.",
755                info.name,
756                info.backend,
757                info.device,
758            );
759        }
760        Ok(())
761    }
762
763    /// Select an adapter and create a device, testing that the surface can actually be configured.
764    /// This is the only reliable way to determine compatibility on hybrid GPU systems, where
765    /// adapters may report surface compatibility via get_capabilities() but fail when actually
766    /// configuring (e.g., NVIDIA reporting Vulkan Wayland support but failing because the
767    /// Wayland compositor runs on the Intel GPU).
768    #[cfg(not(target_family = "wasm"))]
769    async fn select_adapter_and_device(
770        instance: &wgpu::Instance,
771        gpu_preference: &GpuPreference,
772        surface: &wgpu::Surface<'_>,
773        compositor_gpu: Option<&CompositorGpuHint>,
774        reject_software: bool,
775    ) -> anyhow::Result<(
776        wgpu::Adapter,
777        wgpu::Device,
778        wgpu::Queue,
779        bool,
780        TextureFormat,
781    )> {
782        let mut adapters: Vec<_> = instance.enumerate_adapters(wgpu::Backends::all()).await;
783
784        if adapters.is_empty() {
785            anyhow::bail!("No GPU adapters found");
786        }
787
788        // `Custom` names one adapter rather than ordering them, so it is resolved before the
789        // ranking: the chosen adapter goes first and the rest stay as fallbacks in `Auto`'s order.
790        // Resolving it here means the surface test below still gets a chance to reject the choice and
791        // try the next, while the application can read back what it actually got.
792        Self::order_adapters(&mut adapters, gpu_preference, compositor_gpu)?;
793
794        if let GpuPreference::Device(gpu_id) = gpu_preference {
795            log::info!("GPU preference: {:?}", gpu_id.permanent);
796        }
797
798        // Log all available adapters (in sorted order)
799        log::info!("Found {} GPU adapter(s):", adapters.len());
800        for adapter in &adapters {
801            let info = adapter.get_info();
802            log::info!(
803                "  - {} (vendor={:#06x}, device={:#06x}, backend={:?}, type={:?})",
804                info.name,
805                info.vendor,
806                info.device,
807                info.backend,
808                info.device_type,
809            );
810        }
811
812        // Test each adapter by creating a device and configuring the surface
813        for adapter in adapters {
814            let info = adapter.get_info();
815
816            if reject_software && info.device_type == wgpu::DeviceType::Cpu {
817                log::info!(
818                    "Skipping software renderer: {} ({:?})",
819                    info.name,
820                    info.backend
821                );
822                continue;
823            }
824
825            log::info!("Testing adapter: {} ({:?})...", info.name, info.backend);
826
827            match Self::try_adapter_with_surface(&adapter, surface).await {
828                Ok((device, queue, dual_source_blending, color_atlas_texture_format)) => {
829                    log::info!(
830                        "Selected GPU (passed configuration test): {} ({:?})",
831                        info.name,
832                        info.backend
833                    );
834                    return Ok((
835                        adapter,
836                        device,
837                        queue,
838                        dual_source_blending,
839                        color_atlas_texture_format,
840                    ));
841                }
842                Err(e) => {
843                    log::info!(
844                        "  Adapter {} ({:?}) failed: {}, trying next...",
845                        info.name,
846                        info.backend,
847                        e
848                    );
849                }
850            }
851        }
852
853        anyhow::bail!("No GPU adapter found that can configure the display surface")
854    }
855
856    /// Order `adapters` for `preference`, resolving a custom choice first.
857    ///
858    /// The ranking itself lives in `gpui_platform`, so every backend agrees on it. A custom
859    /// preference names one adapter: it is moved to the front and the rest keep `Auto`'s order as
860    /// fallbacks. Everything else goes through the shared rank.
861    #[cfg(not(target_family = "wasm"))]
862    fn order_adapters(
863        adapters: &mut Vec<wgpu::Adapter>,
864        preference: &GpuPreference,
865        compositor_gpu: Option<&CompositorGpuHint>,
866    ) -> anyhow::Result<()> {
867        let compositor = compositor_gpu.map(|hint| PermanentGpuId::Pci {
868            vendor_id: Some(hint.vendor_id),
869            device_id: hint.device_id,
870            subsys_id: None,
871            revision: None,
872        });
873        match preference {
874            GpuPreference::Custom(selector) => {
875                let info: Vec<GpuAdapterInfo> = adapters
876                    .iter()
877                    .map(|adapter| adapter_info(&adapter.get_info()))
878                    .collect();
879                let chosen = gpui_platform::custom_choice(&info, selector.as_ref())?;
880                let adapter = adapters.remove(chosen);
881                // The tail is a fallback, not a ranked preference, so it keeps the historical order:
882                // if the display cannot be configured on the chosen adapter, the next best is tried.
883                adapters.sort_by_cached_key(|adapter| {
884                    gpui_platform::rank(
885                        &adapter_info(&adapter.get_info()),
886                        &GpuPreference::Auto,
887                        compositor.as_ref(),
888                    )
889                });
890                adapters.insert(0, adapter);
891            }
892            _ => adapters.sort_by_cached_key(|adapter| {
893                gpui_platform::rank(
894                    &adapter_info(&adapter.get_info()),
895                    preference,
896                    compositor.as_ref(),
897                )
898            }),
899        }
900        Ok(())
901    }
902
903    /// Try to use an adapter with a surface by creating a device and testing configuration.
904    /// Returns the device and queue if successful, allowing them to be reused.
905    #[cfg(not(target_family = "wasm"))]
906    async fn try_adapter_with_surface(
907        adapter: &wgpu::Adapter,
908        surface: &wgpu::Surface<'_>,
909    ) -> anyhow::Result<(wgpu::Device, wgpu::Queue, bool, TextureFormat)> {
910        let caps = surface.get_capabilities(adapter);
911        if caps.formats.is_empty() {
912            anyhow::bail!("no compatible surface formats");
913        }
914        if caps.alpha_modes.is_empty() {
915            anyhow::bail!("no compatible alpha modes");
916        }
917
918        let (device, queue, dual_source_blending, color_atlas_texture_format) =
919            Self::create_device(adapter).await?;
920        let error_scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
921
922        let test_config = wgpu::SurfaceConfiguration {
923            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
924            format: caps.formats[0],
925            color_space: wgpu::SurfaceColorSpace::Auto,
926            width: 64,
927            height: 64,
928            present_mode: wgpu::PresentMode::Fifo,
929            desired_maximum_frame_latency: 2,
930            alpha_mode: caps.alpha_modes[0],
931            view_formats: vec![],
932        };
933
934        surface.configure(&device, &test_config);
935
936        let error = error_scope.pop().await;
937        if let Some(e) = error {
938            anyhow::bail!("surface configuration failed: {e}");
939        }
940
941        Ok((
942            device,
943            queue,
944            dual_source_blending,
945            color_atlas_texture_format,
946        ))
947    }
948
949    fn select_color_texture_format(adapter: &wgpu::Adapter) -> anyhow::Result<wgpu::TextureFormat> {
950        let required_usages = wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST;
951        let bgra_features = adapter.get_texture_format_features(wgpu::TextureFormat::Bgra8Unorm);
952        let rgba_features = adapter.get_texture_format_features(wgpu::TextureFormat::Rgba8Unorm);
953        #[cfg(target_family = "wasm")]
954        if adapter.get_info().backend == wgpu::Backend::Gl
955            && rgba_features.allowed_usages.contains(required_usages)
956        {
957            return Ok(wgpu::TextureFormat::Rgba8Unorm);
958        }
959        if bgra_features.allowed_usages.contains(required_usages) {
960            return Ok(wgpu::TextureFormat::Bgra8Unorm);
961        }
962        if rgba_features.allowed_usages.contains(required_usages) {
963            let info = adapter.get_info();
964            log::warn!(
965                "Adapter {} ({:?}) does not support Bgra8Unorm atlas textures with usages {:?}; \
966                 falling back to Rgba8Unorm atlas textures.",
967                info.name,
968                info.backend,
969                required_usages,
970            );
971            return Ok(wgpu::TextureFormat::Rgba8Unorm);
972        }
973
974        let info = adapter.get_info();
975        Err(anyhow::anyhow!(
976            "Adapter {} ({:?}, device={:#06x}) does not support a usable color atlas texture \
977             format with usages {:?}. Bgra8Unorm allowed usages: {:?}; \
978             Rgba8Unorm allowed usages: {:?}.",
979            info.name,
980            info.backend,
981            info.device,
982            required_usages,
983            bgra_features.allowed_usages,
984            rgba_features.allowed_usages,
985        ))
986    }
987    pub fn backend(&self) -> WgpuBackend {
988        self.backend
989    }
990
991    pub fn uses_webgl_instance_data(&self) -> bool {
992        matches!(self.backend, WgpuBackend::Gl) && cfg!(target_family = "wasm")
993    }
994
995    pub fn supports_dual_source_blending(&self) -> bool {
996        self.dual_source_blending
997    }
998
999    pub fn color_texture_format(&self) -> wgpu::TextureFormat {
1000        self.color_texture_format
1001    }
1002
1003    /// Returns true if the GPU device was lost (e.g., due to driver crash, suspend/resume).
1004    /// When this returns true, the context should be recreated.
1005    pub fn device_lost(&self) -> bool {
1006        self.errors.device_lost()
1007    }
1008
1009    /// The device-wide error state, shared with renderers on this device.
1010    pub fn errors(&self) -> &Arc<DeviceErrorState> {
1011        &self.errors
1012    }
1013}
1014
1015#[cfg(test)]
1016mod tests {
1017    use super::DeviceErrorState;
1018
1019    /// The escape hatch that the whole tiled-surface path rests on: a device created through the HAL
1020    /// with `VK_EXT_image_drm_format_modifier` added must come back with the extension enabled.
1021    #[cfg(target_os = "linux")]
1022    #[test]
1023    fn the_device_enables_the_drm_format_modifier_extension() {
1024        use wgpu::hal::vulkan::Api;
1025
1026        let instance = super::WgpuContext::instance(None);
1027        let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
1028            power_preference: wgpu::PowerPreference::LowPower,
1029            compatible_surface: None,
1030            force_fallback_adapter: false,
1031            apply_limit_buckets: false,
1032        }));
1033        let Ok(adapter) = adapter else {
1034            eprintln!("skipping: no adapter");
1035            return;
1036        };
1037        let supported = unsafe {
1038            adapter.as_hal::<Api>().is_some_and(|hal| {
1039                hal.physical_device_capabilities()
1040                    .supports_extension(ash::vk::EXT_IMAGE_DRM_FORMAT_MODIFIER_NAME)
1041            })
1042        };
1043        let semaphore_supported = unsafe {
1044            adapter.as_hal::<Api>().is_some_and(|hal| {
1045                hal.physical_device_capabilities()
1046                    .supports_extension(ash::vk::KHR_EXTERNAL_SEMAPHORE_FD_NAME)
1047            })
1048        };
1049        if !supported {
1050            eprintln!(
1051                "skipping: {:?} does not support the extension",
1052                adapter.get_info().name
1053            );
1054            return;
1055        }
1056
1057        let descriptor = wgpu::DeviceDescriptor {
1058            label: Some("drm modifier extension test"),
1059            required_features: wgpu::Features::empty(),
1060            required_limits: wgpu::Limits::downlevel_defaults()
1061                .using_resolution(adapter.limits())
1062                .using_alignment(adapter.limits()),
1063            memory_hints: wgpu::MemoryHints::MemoryUsage,
1064            trace: wgpu::Trace::Off,
1065            experimental_features: wgpu::ExperimentalFeatures::disabled(),
1066        };
1067        let (device, _queue) =
1068            super::WgpuContext::device_with_surface_import(&adapter, &descriptor)
1069                .expect("the escape hatch creates the device");
1070        let enabled = unsafe { device.as_hal::<Api>() }.is_some_and(|device| {
1071            let extensions = device.enabled_device_extensions();
1072            extensions.contains(&ash::vk::EXT_IMAGE_DRM_FORMAT_MODIFIER_NAME)
1073                && (!semaphore_supported
1074                    || extensions.contains(&ash::vk::KHR_EXTERNAL_SEMAPHORE_FD_NAME))
1075        });
1076        assert!(
1077            enabled,
1078            "the created device must have the surface extensions enabled"
1079        );
1080    }
1081
1082    #[test]
1083    fn device_errors_are_observed_independently() {
1084        let errors = DeviceErrorState::default();
1085        let mut first = 0;
1086        let mut second = 0;
1087        assert_eq!(errors.observe_error(&mut first), None);
1088        errors.record("first".into());
1089        assert_eq!(errors.observe_error(&mut first).as_deref(), Some("first"));
1090        assert_eq!(errors.observe_error(&mut first), None);
1091        assert_eq!(errors.observe_error(&mut second).as_deref(), Some("first"));
1092        assert_eq!(errors.observe_error(&mut second), None);
1093
1094        errors.record("second".into());
1095        assert_eq!(errors.observe_error(&mut second).as_deref(), Some("second"));
1096        assert_eq!(errors.observe_error(&mut first).as_deref(), Some("second"));
1097        assert_eq!(errors.observe_error(&mut first), None);
1098        assert_eq!(errors.observe_error(&mut second), None);
1099    }
1100
1101    #[test]
1102    fn device_errors_coalesce_and_new_observers_see_latest() {
1103        let errors = DeviceErrorState::default();
1104        errors.record("older".into());
1105        errors.record("latest".into());
1106        let mut generation = 0;
1107        assert_eq!(
1108            errors.observe_error(&mut generation).as_deref(),
1109            Some("latest")
1110        );
1111        assert_eq!(errors.observe_error(&mut generation), None);
1112
1113        let replacement = DeviceErrorState::default();
1114        replacement.record("replacement".into());
1115        generation = 0;
1116        assert_eq!(
1117            replacement.observe_error(&mut generation).as_deref(),
1118            Some("replacement")
1119        );
1120        assert_eq!(replacement.observe_error(&mut generation), None);
1121    }
1122}