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