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