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