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}