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            })
497            .await
498            .map_err(|error| {
499                anyhow::anyhow!(
500                    "Failed to request a {preference:?} adapter compatible with the canvas: {error}"
501                )
502            })?;
503        let adapter_info = adapter.get_info();
504        let backend = match adapter_info.backend {
505            wgpu::Backend::BrowserWebGpu => WgpuBackend::BrowserWebGpu,
506            wgpu::Backend::Gl => WgpuBackend::Gl,
507            backend => {
508                anyhow::bail!(
509                    "Browser graphics initialization selected unexpected backend {backend:?}"
510                )
511            }
512        };
513
514        let (device, queue, dual_source_blending, color_texture_format) =
515            Self::create_device(&adapter).await?;
516        let errors = DeviceErrorState::install(&device);
517        log::info!(
518            "Browser graphics initialized: requested={preference:?}, selected={backend:?}, \
519             adapter={:?}, limits={:?}, dual_source_blending={dual_source_blending}",
520            adapter_info.name,
521            device.limits(),
522        );
523
524        let context = Self {
525            instance,
526            adapter,
527            device: Arc::new(device),
528            queue: Arc::new(queue),
529            backend,
530            dual_source_blending,
531            color_texture_format,
532            errors,
533        };
534        Ok(PreparedWebGraphics { context, surface })
535    }
536
537    async fn create_device(
538        adapter: &wgpu::Adapter,
539    ) -> anyhow::Result<(wgpu::Device, wgpu::Queue, bool, TextureFormat)> {
540        let dual_source_blending = adapter
541            .features()
542            .contains(wgpu::Features::DUAL_SOURCE_BLENDING);
543
544        let mut required_features = wgpu::Features::empty();
545        if dual_source_blending {
546            required_features |= wgpu::Features::DUAL_SOURCE_BLENDING;
547        } else {
548            log::warn!(
549                "Dual-source blending not available on this GPU. \
550                Subpixel text antialiasing will be disabled."
551            );
552        }
553
554        let color_atlas_texture_format = Self::select_color_texture_format(adapter)?;
555
556        // A `Nv12` surface is one multi-planar image, a native format the device must be allowed to
557        // create; without the feature the import is refused and the surface dropped, rather than
558        // falling back to a pair of single-plane images the modifier does not describe.
559        if adapter
560            .features()
561            .contains(wgpu::Features::TEXTURE_FORMAT_NV12)
562        {
563            required_features |= wgpu::Features::TEXTURE_FORMAT_NV12;
564        }
565
566        // Storage textures follow the adapter's format capabilities, not the WebGPU-guaranteed
567        // subset. Bevy's post-processing pipelines write R16Float and Rgba16Float, which the
568        // guaranteed set disallows; the same feature is enabled by Bevy's own device. It is a
569        // no-op for the renderer itself, and skipped where the adapter lacks it.
570        if adapter
571            .features()
572            .contains(wgpu::Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES)
573        {
574            required_features |= wgpu::Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES;
575        }
576        #[cfg(target_family = "wasm")]
577        let required_limits = if adapter.get_info().backend == wgpu::Backend::Gl {
578            wgpu::Limits::downlevel_webgl2_defaults()
579                .using_resolution(adapter.limits())
580                .using_alignment(adapter.limits())
581        } else {
582            wgpu::Limits::downlevel_defaults()
583                .using_resolution(adapter.limits())
584                .using_alignment(adapter.limits())
585        };
586        #[cfg(not(target_family = "wasm"))]
587        let required_limits = {
588            let supported = adapter.limits();
589            let mut limits = wgpu::Limits::downlevel_defaults()
590                .using_resolution(supported.clone())
591                .using_alignment(supported.clone());
592            // `downlevel_defaults` is enough for the renderer, but it is not enough to *host* one.
593            // Bevy's clustered-forward PBR pass alone asks for 5–8 storage buffers per stage, a
594            // 64 KiB uniform binding and 16 inter-stage variables; the downlevel set stops at 4,
595            // 16 KiB and 15. Raise only the fields that differ, and only to what the adapter
596            // reports, so the request stays sound. GL keeps the downlevel set: its storage-buffer
597            // support is exactly what the downlevel set exists to describe.
598            if adapter.get_info().backend != wgpu::Backend::Gl {
599                limits.max_storage_buffers_per_shader_stage =
600                    supported.max_storage_buffers_per_shader_stage;
601                limits.max_storage_textures_per_shader_stage =
602                    supported.max_storage_textures_per_shader_stage;
603                limits.max_uniform_buffer_binding_size = supported.max_uniform_buffer_binding_size;
604                limits.max_inter_stage_shader_variables =
605                    supported.max_inter_stage_shader_variables;
606                limits.max_color_attachments = supported.max_color_attachments;
607            }
608            limits
609        };
610
611        let descriptor = wgpu::DeviceDescriptor {
612            label: Some("gpui_device"),
613            required_features,
614            required_limits,
615            memory_hints: wgpu::MemoryHints::MemoryUsage,
616            trace: wgpu::Trace::Off,
617            experimental_features: wgpu::ExperimentalFeatures::disabled(),
618        };
619
620        // A producer's surface can carry a DRM format modifier, and wgpu's own device never enables
621        // `VK_EXT_image_drm_format_modifier`, so the import is refused. Where the platform allows it
622        // the device is created through the HAL instead, with that one extension added, and wgpu
623        // adopts it unchanged; see `device_with_surface_import`. The returned device is wgpu's own
624        // either way, so nothing downstream changes.
625        #[cfg(target_os = "linux")]
626        let with_surface_import = Self::device_with_surface_import(adapter, &descriptor);
627        #[cfg(not(target_os = "linux"))]
628        let with_surface_import: Option<(wgpu::Device, wgpu::Queue)> = None;
629
630        let (device, queue) = match with_surface_import {
631            Some(created) => created,
632            None => adapter
633                .request_device(&descriptor)
634                .await
635                .map_err(|e| anyhow::anyhow!("Failed to create wgpu device: {e}"))?,
636        };
637
638        Ok((
639            device,
640            queue,
641            dual_source_blending,
642            color_atlas_texture_format,
643        ))
644    }
645
646    /// Create the window's device through the HAL, enabling `VK_EXT_image_drm_format_modifier`.
647    ///
648    /// wgpu's `Adapter::request_device` never enables it, and its absence is what makes the importer
649    /// in `dmabuf.rs` refuse a tiled surface: a `DRM_FORMAT_MODIFIER_EXT` image cannot be created on a
650    /// device that does not have the extension. The same call also enables
651    /// `VK_KHR_external_semaphore_fd` where the driver supports it, because the producer half of the
652    /// surface path exports its acquire fence from a semaphore signalled by the queue. wgpu-hal exposes
653    /// the two halves needed to create a device with both — `Adapter::open_with_callback` mutates the
654    /// extension list before `vkCreateDevice`, and `wgpu::Adapter::create_device_from_hal` adopts the
655    /// result back into a `wgpu::Device` — so the extensions are genuinely enabled, not assumed.
656    ///
657    /// `None` when the adapter is not Vulkan, when the driver does not support the extension, or when
658    /// either call refuses; the caller then creates the device the ordinary way and the import keeps
659    /// to linear buffers, as it did before.
660    #[cfg(target_os = "linux")]
661    fn device_with_surface_import(
662        adapter: &wgpu::Adapter,
663        descriptor: &wgpu::DeviceDescriptor<'_>,
664    ) -> Option<(wgpu::Device, wgpu::Queue)> {
665        use wgpu::hal::vulkan::Api;
666
667        // SAFETY: the HAL device is created from this adapter with the adapter's own features and
668        // limits (exactly what `descriptor` carries), and the callback adds only an extension the
669        // adapter reports it supports and never removes one. `create_device_from_hal` requires the
670        // HAL device to come from this adapter, which it does.
671        unsafe {
672            let hal = adapter.as_hal::<Api>()?;
673            if !hal
674                .physical_device_capabilities()
675                .supports_extension(ash::vk::EXT_IMAGE_DRM_FORMAT_MODIFIER_NAME)
676            {
677                return None;
678            }
679            // The export half of a shared surface needs a fence the producer can hand over as a
680            // `sync_file`. wgpu-hal's queue can signal a semaphore on the submission that rendered
681            // the frame, and `VK_KHR_external_semaphore_fd` is what turns that signal into one. It
682            // is added only where the driver supports it; a producer that asks for it checks the
683            // device's enabled list and refuses rather than exporting without a fence.
684            let with_semaphore_fd = hal
685                .physical_device_capabilities()
686                .supports_extension(ash::vk::KHR_EXTERNAL_SEMAPHORE_FD_NAME);
687            let open = hal
688                .open_with_callback(
689                    descriptor.required_features,
690                    &descriptor.required_limits,
691                    &descriptor.memory_hints,
692                    Some(Box::new(move |args| {
693                        args.extensions
694                            .push(ash::vk::EXT_IMAGE_DRM_FORMAT_MODIFIER_NAME);
695                        if with_semaphore_fd {
696                            args.extensions
697                                .push(ash::vk::KHR_EXTERNAL_SEMAPHORE_FD_NAME);
698                        }
699                    })),
700                )
701                .ok()?;
702            let (device, queue) = adapter
703                .create_device_from_hal::<Api>(open, descriptor)
704                .ok()?;
705            // The extension is the whole point of this path; if the device did not take it, fall back
706            // so the caller builds the ordinary device rather than leave the importer assuming a
707            // capability the device does not have.
708            let enabled = device.as_hal::<Api>().is_some_and(|device| {
709                device
710                    .enabled_device_extensions()
711                    .contains(&ash::vk::EXT_IMAGE_DRM_FORMAT_MODIFIER_NAME)
712            });
713            enabled.then_some((device, queue))
714        }
715    }
716
717    #[cfg(not(target_family = "wasm"))]
718    pub fn instance(display: Option<Box<dyn wgpu::wgt::WgpuHasDisplayHandle>>) -> wgpu::Instance {
719        wgpu::Instance::new(wgpu::InstanceDescriptor {
720            backends: Self::backends(),
721            flags: wgpu::InstanceFlags::default(),
722            backend_options: wgpu::BackendOptions::default(),
723            memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
724            display,
725        })
726    }
727
728    /// The backends the instance is built with.
729    ///
730    /// Vulkan and GL are what this asked for while Linux was the only host. Neither can present
731    /// on the other desktop platforms: on Windows they find no adapter at all, and on macOS they
732    /// cannot even build a surface from the view's raw handle. So each adds the API its window
733    /// system actually has. Without them `WgpuRenderer::new` fails at instance creation, before
734    /// a frame is reached, which is not something a window can recover from.
735    #[cfg(not(target_family = "wasm"))]
736    fn backends() -> wgpu::Backends {
737        let mut backends = wgpu::Backends::VULKAN | wgpu::Backends::GL;
738        if cfg!(target_os = "windows") {
739            backends |= wgpu::Backends::DX12;
740        }
741        if cfg!(target_os = "macos") {
742            backends |= wgpu::Backends::METAL;
743        }
744        backends
745    }
746
747    pub fn check_compatible_with_surface(&self, surface: &wgpu::Surface<'_>) -> anyhow::Result<()> {
748        let caps = surface.get_capabilities(&self.adapter);
749        if caps.formats.is_empty() {
750            let info = self.adapter.get_info();
751            anyhow::bail!(
752                "Adapter {:?} (backend={:?}, device={:#06x}) is not compatible with the \
753                 display surface for this window.",
754                info.name,
755                info.backend,
756                info.device,
757            );
758        }
759        Ok(())
760    }
761
762    /// Select an adapter and create a device, testing that the surface can actually be configured.
763    /// This is the only reliable way to determine compatibility on hybrid GPU systems, where
764    /// adapters may report surface compatibility via get_capabilities() but fail when actually
765    /// configuring (e.g., NVIDIA reporting Vulkan Wayland support but failing because the
766    /// Wayland compositor runs on the Intel GPU).
767    #[cfg(not(target_family = "wasm"))]
768    async fn select_adapter_and_device(
769        instance: &wgpu::Instance,
770        gpu_preference: &GpuPreference,
771        surface: &wgpu::Surface<'_>,
772        compositor_gpu: Option<&CompositorGpuHint>,
773        reject_software: bool,
774    ) -> anyhow::Result<(
775        wgpu::Adapter,
776        wgpu::Device,
777        wgpu::Queue,
778        bool,
779        TextureFormat,
780    )> {
781        let mut adapters: Vec<_> = instance.enumerate_adapters(wgpu::Backends::all()).await;
782
783        if adapters.is_empty() {
784            anyhow::bail!("No GPU adapters found");
785        }
786
787        // `Custom` names one adapter rather than ordering them, so it is resolved before the
788        // ranking: the chosen adapter goes first and the rest stay as fallbacks in `Auto`'s order.
789        // Resolving it here means the surface test below still gets a chance to reject the choice and
790        // try the next, while the application can read back what it actually got.
791        Self::order_adapters(&mut adapters, gpu_preference, compositor_gpu)?;
792
793        if let GpuPreference::Device(gpu_id) = gpu_preference {
794            log::info!("GPU preference: {:?}", gpu_id.permanent);
795        }
796
797        // Log all available adapters (in sorted order)
798        log::info!("Found {} GPU adapter(s):", adapters.len());
799        for adapter in &adapters {
800            let info = adapter.get_info();
801            log::info!(
802                "  - {} (vendor={:#06x}, device={:#06x}, backend={:?}, type={:?})",
803                info.name,
804                info.vendor,
805                info.device,
806                info.backend,
807                info.device_type,
808            );
809        }
810
811        // Test each adapter by creating a device and configuring the surface
812        for adapter in adapters {
813            let info = adapter.get_info();
814
815            if reject_software && info.device_type == wgpu::DeviceType::Cpu {
816                log::info!(
817                    "Skipping software renderer: {} ({:?})",
818                    info.name,
819                    info.backend
820                );
821                continue;
822            }
823
824            log::info!("Testing adapter: {} ({:?})...", info.name, info.backend);
825
826            match Self::try_adapter_with_surface(&adapter, surface).await {
827                Ok((device, queue, dual_source_blending, color_atlas_texture_format)) => {
828                    log::info!(
829                        "Selected GPU (passed configuration test): {} ({:?})",
830                        info.name,
831                        info.backend
832                    );
833                    return Ok((
834                        adapter,
835                        device,
836                        queue,
837                        dual_source_blending,
838                        color_atlas_texture_format,
839                    ));
840                }
841                Err(e) => {
842                    log::info!(
843                        "  Adapter {} ({:?}) failed: {}, trying next...",
844                        info.name,
845                        info.backend,
846                        e
847                    );
848                }
849            }
850        }
851
852        anyhow::bail!("No GPU adapter found that can configure the display surface")
853    }
854
855    /// Order `adapters` for `preference`, resolving a custom choice first.
856    ///
857    /// The ranking itself lives in `gpui_platform`, so every backend agrees on it. A custom
858    /// preference names one adapter: it is moved to the front and the rest keep `Auto`'s order as
859    /// fallbacks. Everything else goes through the shared rank.
860    #[cfg(not(target_family = "wasm"))]
861    fn order_adapters(
862        adapters: &mut Vec<wgpu::Adapter>,
863        preference: &GpuPreference,
864        compositor_gpu: Option<&CompositorGpuHint>,
865    ) -> anyhow::Result<()> {
866        let compositor = compositor_gpu.map(|hint| PermanentGpuId::Pci {
867            vendor_id: Some(hint.vendor_id),
868            device_id: hint.device_id,
869            subsys_id: None,
870            revision: None,
871        });
872        match preference {
873            GpuPreference::Custom(selector) => {
874                let info: Vec<GpuAdapterInfo> = adapters
875                    .iter()
876                    .map(|adapter| adapter_info(&adapter.get_info()))
877                    .collect();
878                let chosen = gpui_platform::custom_choice(&info, selector.as_ref())?;
879                let adapter = adapters.remove(chosen);
880                // The tail is a fallback, not a ranked preference, so it keeps the historical order:
881                // if the display cannot be configured on the chosen adapter, the next best is tried.
882                adapters.sort_by_cached_key(|adapter| {
883                    gpui_platform::rank(
884                        &adapter_info(&adapter.get_info()),
885                        &GpuPreference::Auto,
886                        compositor.as_ref(),
887                    )
888                });
889                adapters.insert(0, adapter);
890            }
891            _ => adapters.sort_by_cached_key(|adapter| {
892                gpui_platform::rank(
893                    &adapter_info(&adapter.get_info()),
894                    preference,
895                    compositor.as_ref(),
896                )
897            }),
898        }
899        Ok(())
900    }
901
902    /// Try to use an adapter with a surface by creating a device and testing configuration.
903    /// Returns the device and queue if successful, allowing them to be reused.
904    #[cfg(not(target_family = "wasm"))]
905    async fn try_adapter_with_surface(
906        adapter: &wgpu::Adapter,
907        surface: &wgpu::Surface<'_>,
908    ) -> anyhow::Result<(wgpu::Device, wgpu::Queue, bool, TextureFormat)> {
909        let caps = surface.get_capabilities(adapter);
910        if caps.formats.is_empty() {
911            anyhow::bail!("no compatible surface formats");
912        }
913        if caps.alpha_modes.is_empty() {
914            anyhow::bail!("no compatible alpha modes");
915        }
916
917        let (device, queue, dual_source_blending, color_atlas_texture_format) =
918            Self::create_device(adapter).await?;
919        let error_scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
920
921        let test_config = wgpu::SurfaceConfiguration {
922            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
923            format: caps.formats[0],
924            width: 64,
925            height: 64,
926            present_mode: wgpu::PresentMode::Fifo,
927            desired_maximum_frame_latency: 2,
928            alpha_mode: caps.alpha_modes[0],
929            view_formats: vec![],
930        };
931
932        surface.configure(&device, &test_config);
933
934        let error = error_scope.pop().await;
935        if let Some(e) = error {
936            anyhow::bail!("surface configuration failed: {e}");
937        }
938
939        Ok((
940            device,
941            queue,
942            dual_source_blending,
943            color_atlas_texture_format,
944        ))
945    }
946
947    fn select_color_texture_format(adapter: &wgpu::Adapter) -> anyhow::Result<wgpu::TextureFormat> {
948        let required_usages = wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST;
949        let bgra_features = adapter.get_texture_format_features(wgpu::TextureFormat::Bgra8Unorm);
950        let rgba_features = adapter.get_texture_format_features(wgpu::TextureFormat::Rgba8Unorm);
951        #[cfg(target_family = "wasm")]
952        if adapter.get_info().backend == wgpu::Backend::Gl
953            && rgba_features.allowed_usages.contains(required_usages)
954        {
955            return Ok(wgpu::TextureFormat::Rgba8Unorm);
956        }
957        if bgra_features.allowed_usages.contains(required_usages) {
958            return Ok(wgpu::TextureFormat::Bgra8Unorm);
959        }
960        if rgba_features.allowed_usages.contains(required_usages) {
961            let info = adapter.get_info();
962            log::warn!(
963                "Adapter {} ({:?}) does not support Bgra8Unorm atlas textures with usages {:?}; \
964                 falling back to Rgba8Unorm atlas textures.",
965                info.name,
966                info.backend,
967                required_usages,
968            );
969            return Ok(wgpu::TextureFormat::Rgba8Unorm);
970        }
971
972        let info = adapter.get_info();
973        Err(anyhow::anyhow!(
974            "Adapter {} ({:?}, device={:#06x}) does not support a usable color atlas texture \
975             format with usages {:?}. Bgra8Unorm allowed usages: {:?}; \
976             Rgba8Unorm allowed usages: {:?}.",
977            info.name,
978            info.backend,
979            info.device,
980            required_usages,
981            bgra_features.allowed_usages,
982            rgba_features.allowed_usages,
983        ))
984    }
985    pub fn backend(&self) -> WgpuBackend {
986        self.backend
987    }
988
989    pub fn uses_webgl_instance_data(&self) -> bool {
990        matches!(self.backend, WgpuBackend::Gl) && cfg!(target_family = "wasm")
991    }
992
993    pub fn supports_dual_source_blending(&self) -> bool {
994        self.dual_source_blending
995    }
996
997    pub fn color_texture_format(&self) -> wgpu::TextureFormat {
998        self.color_texture_format
999    }
1000
1001    /// Returns true if the GPU device was lost (e.g., due to driver crash, suspend/resume).
1002    /// When this returns true, the context should be recreated.
1003    pub fn device_lost(&self) -> bool {
1004        self.errors.device_lost()
1005    }
1006
1007    /// The device-wide error state, shared with renderers on this device.
1008    pub fn errors(&self) -> &Arc<DeviceErrorState> {
1009        &self.errors
1010    }
1011}
1012
1013#[cfg(test)]
1014mod tests {
1015    use super::DeviceErrorState;
1016
1017    /// The escape hatch that the whole tiled-surface path rests on: a device created through the HAL
1018    /// with `VK_EXT_image_drm_format_modifier` added must come back with the extension enabled.
1019    #[cfg(target_os = "linux")]
1020    #[test]
1021    fn the_device_enables_the_drm_format_modifier_extension() {
1022        use wgpu::hal::vulkan::Api;
1023
1024        let instance = super::WgpuContext::instance(None);
1025        let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
1026            power_preference: wgpu::PowerPreference::LowPower,
1027            compatible_surface: None,
1028            force_fallback_adapter: false,
1029        }));
1030        let Ok(adapter) = adapter else {
1031            eprintln!("skipping: no adapter");
1032            return;
1033        };
1034        let supported = unsafe {
1035            adapter.as_hal::<Api>().is_some_and(|hal| {
1036                hal.physical_device_capabilities()
1037                    .supports_extension(ash::vk::EXT_IMAGE_DRM_FORMAT_MODIFIER_NAME)
1038            })
1039        };
1040        let semaphore_supported = unsafe {
1041            adapter.as_hal::<Api>().is_some_and(|hal| {
1042                hal.physical_device_capabilities()
1043                    .supports_extension(ash::vk::KHR_EXTERNAL_SEMAPHORE_FD_NAME)
1044            })
1045        };
1046        if !supported {
1047            eprintln!(
1048                "skipping: {:?} does not support the extension",
1049                adapter.get_info().name
1050            );
1051            return;
1052        }
1053
1054        let descriptor = wgpu::DeviceDescriptor {
1055            label: Some("drm modifier extension test"),
1056            required_features: wgpu::Features::empty(),
1057            required_limits: wgpu::Limits::downlevel_defaults()
1058                .using_resolution(adapter.limits())
1059                .using_alignment(adapter.limits()),
1060            memory_hints: wgpu::MemoryHints::MemoryUsage,
1061            trace: wgpu::Trace::Off,
1062            experimental_features: wgpu::ExperimentalFeatures::disabled(),
1063        };
1064        let (device, _queue) =
1065            super::WgpuContext::device_with_surface_import(&adapter, &descriptor)
1066                .expect("the escape hatch creates the device");
1067        let enabled = unsafe { device.as_hal::<Api>() }.is_some_and(|device| {
1068            let extensions = device.enabled_device_extensions();
1069            extensions.contains(&ash::vk::EXT_IMAGE_DRM_FORMAT_MODIFIER_NAME)
1070                && (!semaphore_supported
1071                    || extensions.contains(&ash::vk::KHR_EXTERNAL_SEMAPHORE_FD_NAME))
1072        });
1073        assert!(
1074            enabled,
1075            "the created device must have the surface extensions enabled"
1076        );
1077    }
1078
1079    #[test]
1080    fn device_errors_are_observed_independently() {
1081        let errors = DeviceErrorState::default();
1082        let mut first = 0;
1083        let mut second = 0;
1084        assert_eq!(errors.observe_error(&mut first), None);
1085        errors.record("first".into());
1086        assert_eq!(errors.observe_error(&mut first).as_deref(), Some("first"));
1087        assert_eq!(errors.observe_error(&mut first), None);
1088        assert_eq!(errors.observe_error(&mut second).as_deref(), Some("first"));
1089        assert_eq!(errors.observe_error(&mut second), None);
1090
1091        errors.record("second".into());
1092        assert_eq!(errors.observe_error(&mut second).as_deref(), Some("second"));
1093        assert_eq!(errors.observe_error(&mut first).as_deref(), Some("second"));
1094        assert_eq!(errors.observe_error(&mut first), None);
1095        assert_eq!(errors.observe_error(&mut second), None);
1096    }
1097
1098    #[test]
1099    fn device_errors_coalesce_and_new_observers_see_latest() {
1100        let errors = DeviceErrorState::default();
1101        errors.record("older".into());
1102        errors.record("latest".into());
1103        let mut generation = 0;
1104        assert_eq!(
1105            errors.observe_error(&mut generation).as_deref(),
1106            Some("latest")
1107        );
1108        assert_eq!(errors.observe_error(&mut generation), None);
1109
1110        let replacement = DeviceErrorState::default();
1111        replacement.record("replacement".into());
1112        generation = 0;
1113        assert_eq!(
1114            replacement.observe_error(&mut generation).as_deref(),
1115            Some("replacement")
1116        );
1117        assert_eq!(replacement.observe_error(&mut generation), None);
1118    }
1119}