wgpu_context.rs

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