wgpu_context.rs

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