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