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