Skip to main content

open_gpui_wgpu/
wgpu_context.rs

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