Skip to main content

gpui_wgpu/
wgpu_context.rs

1#[cfg(not(target_family = "wasm"))]
2use anyhow::Context as _;
3#[cfg(not(target_family = "wasm"))]
4use gpui_util::ResultExt;
5use std::sync::Arc;
6use std::sync::atomic::{AtomicBool, Ordering};
7use wgpu::TextureFormat;
8
9pub struct WgpuContext {
10    pub instance: wgpu::Instance,
11    pub adapter: wgpu::Adapter,
12    pub device: Arc<wgpu::Device>,
13    pub queue: Arc<wgpu::Queue>,
14    backend: WgpuBackend,
15    dual_source_blending: bool,
16    color_texture_format: wgpu::TextureFormat,
17    device_lost: Arc<AtomicBool>,
18}
19
20#[derive(Clone, Copy, Debug, Eq, PartialEq)]
21pub enum WgpuBackend {
22    BrowserWebGpu,
23    Gl,
24    Native(wgpu::Backend),
25}
26
27#[cfg(target_family = "wasm")]
28#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
29pub enum WebBackendPreference {
30    #[default]
31    Auto,
32    WebGpu,
33    WebGl,
34}
35
36#[cfg(target_family = "wasm")]
37pub struct PreparedWebGraphics {
38    pub context: WgpuContext,
39    pub surface: wgpu::Surface<'static>,
40}
41
42/// wgpu-core refuses to create a surface when neither the instance nor the surface
43/// target carries a display handle, and `SurfaceTarget::Canvas` always passes `None`.
44/// The WebGL2 backend never reads the handle (WebGPU bypasses wgpu-core entirely), so
45/// a unit web display handle on the instance satisfies the check.
46#[cfg(target_family = "wasm")]
47#[derive(Debug)]
48struct WebDisplaySource;
49
50#[cfg(target_family = "wasm")]
51impl raw_window_handle::HasDisplayHandle for WebDisplaySource {
52    fn display_handle(
53        &self,
54    ) -> Result<raw_window_handle::DisplayHandle<'_>, raw_window_handle::HandleError> {
55        Ok(raw_window_handle::DisplayHandle::web())
56    }
57}
58
59#[derive(Clone, Copy)]
60pub struct CompositorGpuHint {
61    pub vendor_id: u32,
62    pub device_id: u32,
63}
64
65impl WgpuContext {
66    #[cfg(not(target_family = "wasm"))]
67    pub fn new(
68        instance: wgpu::Instance,
69        surface: &wgpu::Surface<'_>,
70        compositor_gpu: Option<CompositorGpuHint>,
71    ) -> anyhow::Result<Self> {
72        Self::new_with_options(instance, surface, compositor_gpu, false)
73    }
74
75    #[cfg(not(target_family = "wasm"))]
76    pub fn new_rejecting_software(
77        instance: wgpu::Instance,
78        surface: &wgpu::Surface<'_>,
79        compositor_gpu: Option<CompositorGpuHint>,
80    ) -> anyhow::Result<Self> {
81        Self::new_with_options(instance, surface, compositor_gpu, true)
82    }
83
84    #[cfg(not(target_family = "wasm"))]
85    fn new_with_options(
86        instance: wgpu::Instance,
87        surface: &wgpu::Surface<'_>,
88        compositor_gpu: Option<CompositorGpuHint>,
89        reject_software: bool,
90    ) -> anyhow::Result<Self> {
91        let device_id_filter = match std::env::var("ZED_DEVICE_ID") {
92            Ok(val) => parse_pci_id(&val)
93                .context("Failed to parse device ID from `ZED_DEVICE_ID` environment variable")
94                .log_err(),
95            Err(std::env::VarError::NotPresent) => None,
96            err => {
97                err.context("Failed to read value of `ZED_DEVICE_ID` environment variable")
98                    .log_err();
99                None
100            }
101        };
102
103        // Select an adapter by actually testing surface configuration with the real device.
104        // This is the only reliable way to determine compatibility on hybrid GPU systems.
105        let (adapter, device, queue, dual_source_blending, color_texture_format) =
106            gpui::block_on(Self::select_adapter_and_device(
107                &instance,
108                device_id_filter,
109                surface,
110                compositor_gpu.as_ref(),
111                reject_software,
112            ))?;
113
114        let device_lost = Arc::new(AtomicBool::new(false));
115        device.set_device_lost_callback({
116            let device_lost = Arc::clone(&device_lost);
117            move |reason, message| {
118                log::error!("wgpu device lost: reason={reason:?}, message={message}");
119                if reason != wgpu::DeviceLostReason::Destroyed {
120                    device_lost.store(true, Ordering::Relaxed);
121                }
122            }
123        });
124
125        log::info!(
126            "Selected GPU adapter: {:?} ({:?})",
127            adapter.get_info().name,
128            adapter.get_info().backend
129        );
130
131        let backend = WgpuBackend::Native(adapter.get_info().backend);
132        Ok(Self {
133            instance,
134            adapter,
135            device: Arc::new(device),
136            queue: Arc::new(queue),
137            backend,
138            dual_source_blending,
139            color_texture_format,
140            device_lost,
141        })
142    }
143
144    #[cfg(target_family = "wasm")]
145    pub async fn new_web(
146        canvas: &web_sys::HtmlCanvasElement,
147        preference: WebBackendPreference,
148    ) -> anyhow::Result<PreparedWebGraphics> {
149        Self::new_web_with_backend(canvas, preference).await
150    }
151
152    #[cfg(target_family = "wasm")]
153    #[allow(clippy::arc_with_non_send_sync)]
154    async fn new_web_with_backend(
155        canvas: &web_sys::HtmlCanvasElement,
156        preference: WebBackendPreference,
157    ) -> anyhow::Result<PreparedWebGraphics> {
158        let backends = match preference {
159            WebBackendPreference::Auto => wgpu::Backends::BROWSER_WEBGPU | wgpu::Backends::GL,
160            WebBackendPreference::WebGpu => wgpu::Backends::BROWSER_WEBGPU,
161            WebBackendPreference::WebGl => wgpu::Backends::GL,
162        };
163        let descriptor = wgpu::InstanceDescriptor {
164            backends,
165            flags: wgpu::InstanceFlags::default(),
166            backend_options: wgpu::BackendOptions::default(),
167            memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
168            display: Some(Box::new(WebDisplaySource)),
169        };
170        let instance = if preference == WebBackendPreference::Auto {
171            wgpu::util::new_instance_with_webgpu_detection(descriptor).await
172        } else {
173            wgpu::Instance::new(descriptor)
174        };
175        let surface = instance
176            .create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone()))
177            .map_err(|error| {
178                anyhow::anyhow!("Failed to create browser graphics surface: {error}")
179            })?;
180
181        let adapter = instance
182            .request_adapter(&wgpu::RequestAdapterOptions {
183                power_preference: wgpu::PowerPreference::HighPerformance,
184                compatible_surface: Some(&surface),
185                force_fallback_adapter: false,
186            })
187            .await
188            .map_err(|error| {
189                anyhow::anyhow!(
190                    "Failed to request a {preference:?} adapter compatible with the canvas: {error}"
191                )
192            })?;
193        let adapter_info = adapter.get_info();
194        let backend = match adapter_info.backend {
195            wgpu::Backend::BrowserWebGpu => WgpuBackend::BrowserWebGpu,
196            wgpu::Backend::Gl => WgpuBackend::Gl,
197            backend => {
198                anyhow::bail!(
199                    "Browser graphics initialization selected unexpected backend {backend:?}"
200                )
201            }
202        };
203
204        let device_lost = Arc::new(AtomicBool::new(false));
205        let (device, queue, dual_source_blending, color_texture_format) =
206            Self::create_device(&adapter).await?;
207        device.set_device_lost_callback({
208            let device_lost = Arc::clone(&device_lost);
209            move |reason, message| {
210                log::error!("wgpu device lost: reason={reason:?}, message={message}");
211                if reason != wgpu::DeviceLostReason::Destroyed {
212                    device_lost.store(true, Ordering::Relaxed);
213                }
214            }
215        });
216        log::info!(
217            "Browser graphics initialized: requested={preference:?}, selected={backend:?}, \
218             adapter={:?}, limits={:?}, dual_source_blending={dual_source_blending}",
219            adapter_info.name,
220            device.limits(),
221        );
222
223        let context = Self {
224            instance,
225            adapter,
226            device: Arc::new(device),
227            queue: Arc::new(queue),
228            backend,
229            dual_source_blending,
230            color_texture_format,
231            device_lost,
232        };
233        Ok(PreparedWebGraphics { context, surface })
234    }
235
236    async fn create_device(
237        adapter: &wgpu::Adapter,
238    ) -> anyhow::Result<(wgpu::Device, wgpu::Queue, bool, TextureFormat)> {
239        let dual_source_blending = adapter
240            .features()
241            .contains(wgpu::Features::DUAL_SOURCE_BLENDING);
242
243        let mut required_features = wgpu::Features::empty();
244        if dual_source_blending {
245            required_features |= wgpu::Features::DUAL_SOURCE_BLENDING;
246        } else {
247            log::warn!(
248                "Dual-source blending not available on this GPU. \
249                Subpixel text antialiasing will be disabled."
250            );
251        }
252
253        let color_atlas_texture_format = Self::select_color_texture_format(adapter)?;
254        #[cfg(target_family = "wasm")]
255        let required_limits = if adapter.get_info().backend == wgpu::Backend::Gl {
256            wgpu::Limits::downlevel_webgl2_defaults()
257                .using_resolution(adapter.limits())
258                .using_alignment(adapter.limits())
259        } else {
260            wgpu::Limits::downlevel_defaults()
261                .using_resolution(adapter.limits())
262                .using_alignment(adapter.limits())
263        };
264        #[cfg(not(target_family = "wasm"))]
265        let required_limits = wgpu::Limits::downlevel_defaults()
266            .using_resolution(adapter.limits())
267            .using_alignment(adapter.limits());
268
269        let (device, queue) = adapter
270            .request_device(&wgpu::DeviceDescriptor {
271                label: Some("gpui_device"),
272                required_features,
273                required_limits,
274                memory_hints: wgpu::MemoryHints::MemoryUsage,
275                trace: wgpu::Trace::Off,
276                experimental_features: wgpu::ExperimentalFeatures::disabled(),
277            })
278            .await
279            .map_err(|e| anyhow::anyhow!("Failed to create wgpu device: {e}"))?;
280
281        Ok((
282            device,
283            queue,
284            dual_source_blending,
285            color_atlas_texture_format,
286        ))
287    }
288
289    #[cfg(not(target_family = "wasm"))]
290    pub fn instance(display: Box<dyn wgpu::wgt::WgpuHasDisplayHandle>) -> wgpu::Instance {
291        wgpu::Instance::new(wgpu::InstanceDescriptor {
292            backends: wgpu::Backends::VULKAN | wgpu::Backends::GL,
293            flags: wgpu::InstanceFlags::default(),
294            backend_options: wgpu::BackendOptions::default(),
295            memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
296            display: Some(display),
297        })
298    }
299
300    pub fn check_compatible_with_surface(&self, surface: &wgpu::Surface<'_>) -> anyhow::Result<()> {
301        let caps = surface.get_capabilities(&self.adapter);
302        if caps.formats.is_empty() {
303            let info = self.adapter.get_info();
304            anyhow::bail!(
305                "Adapter {:?} (backend={:?}, device={:#06x}) is not compatible with the \
306                 display surface for this window.",
307                info.name,
308                info.backend,
309                info.device,
310            );
311        }
312        Ok(())
313    }
314
315    /// Select an adapter and create a device, testing that the surface can actually be configured.
316    /// This is the only reliable way to determine compatibility on hybrid GPU systems, where
317    /// adapters may report surface compatibility via get_capabilities() but fail when actually
318    /// configuring (e.g., NVIDIA reporting Vulkan Wayland support but failing because the
319    /// Wayland compositor runs on the Intel GPU).
320    #[cfg(not(target_family = "wasm"))]
321    async fn select_adapter_and_device(
322        instance: &wgpu::Instance,
323        device_id_filter: Option<u32>,
324        surface: &wgpu::Surface<'_>,
325        compositor_gpu: Option<&CompositorGpuHint>,
326        reject_software: bool,
327    ) -> anyhow::Result<(
328        wgpu::Adapter,
329        wgpu::Device,
330        wgpu::Queue,
331        bool,
332        TextureFormat,
333    )> {
334        let mut adapters: Vec<_> = instance.enumerate_adapters(wgpu::Backends::all()).await;
335
336        if adapters.is_empty() {
337            anyhow::bail!("No GPU adapters found");
338        }
339
340        if let Some(device_id) = device_id_filter {
341            log::info!("ZED_DEVICE_ID filter: {:#06x}", device_id);
342        }
343
344        // Sort adapters into a single priority order. Tiers (from highest to lowest):
345        //
346        // 1. ZED_DEVICE_ID match — explicit user override
347        // 2. Compositor GPU match — the GPU the display server is rendering on
348        // 3. Device type (Discrete > Integrated > Other > Virtual > Cpu).
349        //    "Other" ranks above "Virtual" because OpenGL seems to count as "Other".
350        // 4. Backend — prefer Vulkan/Metal/Dx12 over GL/etc.
351        adapters.sort_by_key(|adapter| {
352            let info = adapter.get_info();
353
354            // Backends like OpenGL report device=0 for all adapters, so
355            // device-based matching is only meaningful when non-zero.
356            let device_known = info.device != 0;
357
358            let user_override: u8 = match device_id_filter {
359                Some(id) if device_known && info.device == id => 0,
360                _ => 1,
361            };
362
363            let compositor_match: u8 = match compositor_gpu {
364                Some(hint)
365                    if device_known
366                        && info.vendor == hint.vendor_id
367                        && info.device == hint.device_id =>
368                {
369                    0
370                }
371                _ => 1,
372            };
373
374            let type_priority: u8 = if info.device_type == wgpu::DeviceType::Cpu {
375                4
376            } else {
377                match info.device_type {
378                    wgpu::DeviceType::DiscreteGpu => 0,
379                    wgpu::DeviceType::IntegratedGpu => 1,
380                    wgpu::DeviceType::Other => 2,
381                    wgpu::DeviceType::VirtualGpu => 3,
382                    wgpu::DeviceType::Cpu => 4,
383                }
384            };
385
386            let backend_priority: u8 = match info.backend {
387                wgpu::Backend::Vulkan | wgpu::Backend::Metal | wgpu::Backend::Dx12 => 0,
388                _ => 1,
389            };
390
391            (
392                user_override,
393                compositor_match,
394                type_priority,
395                backend_priority,
396            )
397        });
398
399        // Log all available adapters (in sorted order)
400        log::info!("Found {} GPU adapter(s):", adapters.len());
401        for adapter in &adapters {
402            let info = adapter.get_info();
403            log::info!(
404                "  - {} (vendor={:#06x}, device={:#06x}, backend={:?}, type={:?})",
405                info.name,
406                info.vendor,
407                info.device,
408                info.backend,
409                info.device_type,
410            );
411        }
412
413        // Test each adapter by creating a device and configuring the surface
414        for adapter in adapters {
415            let info = adapter.get_info();
416
417            if reject_software && info.device_type == wgpu::DeviceType::Cpu {
418                log::info!(
419                    "Skipping software renderer: {} ({:?})",
420                    info.name,
421                    info.backend
422                );
423                continue;
424            }
425
426            log::info!("Testing adapter: {} ({:?})...", info.name, info.backend);
427
428            match Self::try_adapter_with_surface(&adapter, surface).await {
429                Ok((device, queue, dual_source_blending, color_atlas_texture_format)) => {
430                    log::info!(
431                        "Selected GPU (passed configuration test): {} ({:?})",
432                        info.name,
433                        info.backend
434                    );
435                    return Ok((
436                        adapter,
437                        device,
438                        queue,
439                        dual_source_blending,
440                        color_atlas_texture_format,
441                    ));
442                }
443                Err(e) => {
444                    log::info!(
445                        "  Adapter {} ({:?}) failed: {}, trying next...",
446                        info.name,
447                        info.backend,
448                        e
449                    );
450                }
451            }
452        }
453
454        anyhow::bail!("No GPU adapter found that can configure the display surface")
455    }
456
457    /// Try to use an adapter with a surface by creating a device and testing configuration.
458    /// Returns the device and queue if successful, allowing them to be reused.
459    #[cfg(not(target_family = "wasm"))]
460    async fn try_adapter_with_surface(
461        adapter: &wgpu::Adapter,
462        surface: &wgpu::Surface<'_>,
463    ) -> anyhow::Result<(wgpu::Device, wgpu::Queue, bool, TextureFormat)> {
464        let caps = surface.get_capabilities(adapter);
465        if caps.formats.is_empty() {
466            anyhow::bail!("no compatible surface formats");
467        }
468        if caps.alpha_modes.is_empty() {
469            anyhow::bail!("no compatible alpha modes");
470        }
471
472        let (device, queue, dual_source_blending, color_atlas_texture_format) =
473            Self::create_device(adapter).await?;
474        let error_scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
475
476        let test_config = wgpu::SurfaceConfiguration {
477            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
478            format: caps.formats[0],
479            width: 64,
480            height: 64,
481            present_mode: wgpu::PresentMode::Fifo,
482            desired_maximum_frame_latency: 2,
483            alpha_mode: caps.alpha_modes[0],
484            view_formats: vec![],
485        };
486
487        surface.configure(&device, &test_config);
488
489        let error = error_scope.pop().await;
490        if let Some(e) = error {
491            anyhow::bail!("surface configuration failed: {e}");
492        }
493
494        Ok((
495            device,
496            queue,
497            dual_source_blending,
498            color_atlas_texture_format,
499        ))
500    }
501
502    fn select_color_texture_format(adapter: &wgpu::Adapter) -> anyhow::Result<wgpu::TextureFormat> {
503        let required_usages = wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST;
504        let bgra_features = adapter.get_texture_format_features(wgpu::TextureFormat::Bgra8Unorm);
505        let rgba_features = adapter.get_texture_format_features(wgpu::TextureFormat::Rgba8Unorm);
506        #[cfg(target_family = "wasm")]
507        if adapter.get_info().backend == wgpu::Backend::Gl
508            && rgba_features.allowed_usages.contains(required_usages)
509        {
510            return Ok(wgpu::TextureFormat::Rgba8Unorm);
511        }
512        if bgra_features.allowed_usages.contains(required_usages) {
513            return Ok(wgpu::TextureFormat::Bgra8Unorm);
514        }
515        if rgba_features.allowed_usages.contains(required_usages) {
516            let info = adapter.get_info();
517            log::warn!(
518                "Adapter {} ({:?}) does not support Bgra8Unorm atlas textures with usages {:?}; \
519                 falling back to Rgba8Unorm atlas textures.",
520                info.name,
521                info.backend,
522                required_usages,
523            );
524            return Ok(wgpu::TextureFormat::Rgba8Unorm);
525        }
526
527        let info = adapter.get_info();
528        Err(anyhow::anyhow!(
529            "Adapter {} ({:?}, device={:#06x}) does not support a usable color atlas texture \
530             format with usages {:?}. Bgra8Unorm allowed usages: {:?}; \
531             Rgba8Unorm allowed usages: {:?}.",
532            info.name,
533            info.backend,
534            info.device,
535            required_usages,
536            bgra_features.allowed_usages,
537            rgba_features.allowed_usages,
538        ))
539    }
540    pub fn backend(&self) -> WgpuBackend {
541        self.backend
542    }
543
544    pub fn uses_webgl_instance_data(&self) -> bool {
545        matches!(self.backend, WgpuBackend::Gl) && cfg!(target_family = "wasm")
546    }
547
548    pub fn supports_dual_source_blending(&self) -> bool {
549        self.dual_source_blending
550    }
551
552    pub fn color_texture_format(&self) -> wgpu::TextureFormat {
553        self.color_texture_format
554    }
555
556    /// Returns true if the GPU device was lost (e.g., due to driver crash, suspend/resume).
557    /// When this returns true, the context should be recreated.
558    pub fn device_lost(&self) -> bool {
559        self.device_lost.load(Ordering::Relaxed)
560    }
561
562    /// Returns a clone of the device_lost flag for sharing with renderers.
563    pub(crate) fn device_lost_flag(&self) -> Arc<AtomicBool> {
564        Arc::clone(&self.device_lost)
565    }
566}
567
568#[cfg(not(target_family = "wasm"))]
569fn parse_pci_id(id: &str) -> anyhow::Result<u32> {
570    let mut id = id.trim();
571
572    if id.starts_with("0x") || id.starts_with("0X") {
573        id = &id[2..];
574    }
575    let is_hex_string = id.chars().all(|c| c.is_ascii_hexdigit());
576    let is_4_chars = id.len() == 4;
577    anyhow::ensure!(
578        is_4_chars && is_hex_string,
579        "Expected a 4 digit PCI ID in hexadecimal format"
580    );
581
582    u32::from_str_radix(id, 16).context("parsing PCI ID as hex")
583}
584
585#[cfg(test)]
586mod tests {
587    use super::parse_pci_id;
588
589    #[test]
590    fn test_parse_device_id() {
591        assert!(parse_pci_id("0xABCD").is_ok());
592        assert!(parse_pci_id("ABCD").is_ok());
593        assert!(parse_pci_id("abcd").is_ok());
594        assert!(parse_pci_id("1234").is_ok());
595        assert!(parse_pci_id("123").is_err());
596        assert_eq!(
597            parse_pci_id(&format!("{:x}", 0x1234)).unwrap(),
598            parse_pci_id(&format!("{:X}", 0x1234)).unwrap(),
599        );
600
601        assert_eq!(
602            parse_pci_id(&format!("{:#x}", 0x1234)).unwrap(),
603            parse_pci_id(&format!("{:#X}", 0x1234)).unwrap(),
604        );
605    }
606}