1#[cfg(not(target_family = "wasm"))]
2use anyhow::Context as _;
3#[cfg(not(target_family = "wasm"))]
4use open_gpui_core_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 Self::new_with_options(instance, surface, compositor_gpu, false)
33 }
34
35 #[cfg(not(target_family = "wasm"))]
36 pub fn new_rejecting_software(
37 instance: wgpu::Instance,
38 surface: &wgpu::Surface<'_>,
39 compositor_gpu: Option<CompositorGpuHint>,
40 ) -> anyhow::Result<Self> {
41 Self::new_with_options(instance, surface, compositor_gpu, true)
42 }
43
44 #[cfg(not(target_family = "wasm"))]
45 fn new_with_options(
46 instance: wgpu::Instance,
47 surface: &wgpu::Surface<'_>,
48 compositor_gpu: Option<CompositorGpuHint>,
49 reject_software: bool,
50 ) -> anyhow::Result<Self> {
51 let device_id_filter = match std::env::var("ZED_DEVICE_ID") {
52 Ok(val) => parse_pci_id(&val)
53 .context("Failed to parse device ID from `ZED_DEVICE_ID` environment variable")
54 .log_err(),
55 Err(std::env::VarError::NotPresent) => None,
56 err => {
57 err.context("Failed to read value of `ZED_DEVICE_ID` environment variable")
58 .log_err();
59 None
60 }
61 };
62
63 let (adapter, device, queue, dual_source_blending, color_texture_format) =
66 open_gpui::block_on(Self::select_adapter_and_device(
67 &instance,
68 device_id_filter,
69 surface,
70 compositor_gpu.as_ref(),
71 reject_software,
72 ))?;
73
74 let device_lost = Arc::new(AtomicBool::new(false));
75 device.set_device_lost_callback({
76 let device_lost = Arc::clone(&device_lost);
77 move |reason, message| {
78 log::error!("wgpu device lost: reason={reason:?}, message={message}");
79 if reason != wgpu::DeviceLostReason::Destroyed {
80 device_lost.store(true, Ordering::Relaxed);
81 }
82 }
83 });
84
85 log::info!(
86 "Selected GPU adapter: {:?} ({:?})",
87 adapter.get_info().name,
88 adapter.get_info().backend
89 );
90
91 Ok(Self {
92 instance,
93 adapter,
94 device: Arc::new(device),
95 queue: Arc::new(queue),
96 dual_source_blending,
97 color_texture_format,
98 device_lost,
99 })
100 }
101
102 #[cfg(target_family = "wasm")]
103 pub async fn new_web() -> anyhow::Result<Self> {
104 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
105 backends: wgpu::Backends::BROWSER_WEBGPU | wgpu::Backends::GL,
106 flags: wgpu::InstanceFlags::default(),
107 backend_options: wgpu::BackendOptions::default(),
108 memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
109 display: None,
110 });
111
112 let adapter = instance
113 .request_adapter(&wgpu::RequestAdapterOptions {
114 power_preference: wgpu::PowerPreference::HighPerformance,
115 compatible_surface: None,
116 force_fallback_adapter: false,
117 })
118 .await
119 .map_err(|e| anyhow::anyhow!("Failed to request GPU adapter: {e}"))?;
120
121 log::info!(
122 "Selected GPU adapter: {:?} ({:?})",
123 adapter.get_info().name,
124 adapter.get_info().backend
125 );
126
127 let device_lost = Arc::new(AtomicBool::new(false));
128 let (device, queue, dual_source_blending, color_texture_format) =
129 Self::create_device(&adapter).await?;
130
131 Ok(Self {
132 instance,
133 adapter,
134 device: Arc::new(device),
135 queue: Arc::new(queue),
136 dual_source_blending,
137 color_texture_format,
138 device_lost,
139 })
140 }
141
142 async fn create_device(
143 adapter: &wgpu::Adapter,
144 ) -> anyhow::Result<(wgpu::Device, wgpu::Queue, bool, TextureFormat)> {
145 let dual_source_blending = adapter
146 .features()
147 .contains(wgpu::Features::DUAL_SOURCE_BLENDING);
148
149 let mut required_features = wgpu::Features::empty();
150 if dual_source_blending {
151 required_features |= wgpu::Features::DUAL_SOURCE_BLENDING;
152 } else {
153 log::warn!(
154 "Dual-source blending not available on this GPU. \
155 Subpixel text antialiasing will be disabled."
156 );
157 }
158
159 let color_atlas_texture_format = Self::select_color_texture_format(adapter)?;
160
161 let (device, queue) = adapter
162 .request_device(&wgpu::DeviceDescriptor {
163 label: Some("gpui_device"),
164 required_features,
165 required_limits: wgpu::Limits::downlevel_defaults()
166 .using_resolution(adapter.limits())
167 .using_alignment(adapter.limits()),
168 memory_hints: wgpu::MemoryHints::MemoryUsage,
169 trace: wgpu::Trace::Off,
170 experimental_features: wgpu::ExperimentalFeatures::disabled(),
171 })
172 .await
173 .map_err(|e| anyhow::anyhow!("Failed to create wgpu device: {e}"))?;
174
175 Ok((
176 device,
177 queue,
178 dual_source_blending,
179 color_atlas_texture_format,
180 ))
181 }
182
183 #[cfg(not(target_family = "wasm"))]
184 pub fn instance(display: Box<dyn wgpu::wgt::WgpuHasDisplayHandle>) -> wgpu::Instance {
185 wgpu::Instance::new(wgpu::InstanceDescriptor {
186 backends: wgpu::Backends::VULKAN | wgpu::Backends::GL,
187 flags: wgpu::InstanceFlags::default(),
188 backend_options: wgpu::BackendOptions::default(),
189 memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
190 display: Some(display),
191 })
192 }
193
194 pub fn check_compatible_with_surface(&self, surface: &wgpu::Surface<'_>) -> anyhow::Result<()> {
195 let caps = surface.get_capabilities(&self.adapter);
196 if caps.formats.is_empty() {
197 let info = self.adapter.get_info();
198 anyhow::bail!(
199 "Adapter {:?} (backend={:?}, device={:#06x}) is not compatible with the \
200 display surface for this window.",
201 info.name,
202 info.backend,
203 info.device,
204 );
205 }
206 Ok(())
207 }
208
209 #[cfg(not(target_family = "wasm"))]
215 async fn select_adapter_and_device(
216 instance: &wgpu::Instance,
217 device_id_filter: Option<u32>,
218 surface: &wgpu::Surface<'_>,
219 compositor_gpu: Option<&CompositorGpuHint>,
220 reject_software: bool,
221 ) -> anyhow::Result<(
222 wgpu::Adapter,
223 wgpu::Device,
224 wgpu::Queue,
225 bool,
226 TextureFormat,
227 )> {
228 let mut adapters: Vec<_> = instance.enumerate_adapters(wgpu::Backends::all()).await;
229
230 if adapters.is_empty() {
231 anyhow::bail!("No GPU adapters found");
232 }
233
234 if let Some(device_id) = device_id_filter {
235 log::info!("ZED_DEVICE_ID filter: {:#06x}", device_id);
236 }
237
238 adapters.sort_by_key(|adapter| {
246 let info = adapter.get_info();
247
248 let device_known = info.device != 0;
251
252 let user_override: u8 = match device_id_filter {
253 Some(id) if device_known && info.device == id => 0,
254 _ => 1,
255 };
256
257 let compositor_match: u8 = match compositor_gpu {
258 Some(hint)
259 if device_known
260 && info.vendor == hint.vendor_id
261 && info.device == hint.device_id =>
262 {
263 0
264 }
265 _ => 1,
266 };
267
268 let type_priority: u8 = if info.device_type == wgpu::DeviceType::Cpu {
269 4
270 } else {
271 match info.device_type {
272 wgpu::DeviceType::DiscreteGpu => 0,
273 wgpu::DeviceType::IntegratedGpu => 1,
274 wgpu::DeviceType::Other => 2,
275 wgpu::DeviceType::VirtualGpu => 3,
276 wgpu::DeviceType::Cpu => 4,
277 }
278 };
279
280 let backend_priority: u8 = match info.backend {
281 wgpu::Backend::Vulkan | wgpu::Backend::Metal | wgpu::Backend::Dx12 => 0,
282 _ => 1,
283 };
284
285 (
286 user_override,
287 compositor_match,
288 type_priority,
289 backend_priority,
290 )
291 });
292
293 log::info!("Found {} GPU adapter(s):", adapters.len());
295 for adapter in &adapters {
296 let info = adapter.get_info();
297 log::info!(
298 " - {} (vendor={:#06x}, device={:#06x}, backend={:?}, type={:?})",
299 info.name,
300 info.vendor,
301 info.device,
302 info.backend,
303 info.device_type,
304 );
305 }
306
307 for adapter in adapters {
309 let info = adapter.get_info();
310
311 if reject_software && info.device_type == wgpu::DeviceType::Cpu {
312 log::info!(
313 "Skipping software renderer: {} ({:?})",
314 info.name,
315 info.backend
316 );
317 continue;
318 }
319
320 log::info!("Testing adapter: {} ({:?})...", info.name, info.backend);
321
322 match Self::try_adapter_with_surface(&adapter, surface).await {
323 Ok((device, queue, dual_source_blending, color_atlas_texture_format)) => {
324 log::info!(
325 "Selected GPU (passed configuration test): {} ({:?})",
326 info.name,
327 info.backend
328 );
329 return Ok((
330 adapter,
331 device,
332 queue,
333 dual_source_blending,
334 color_atlas_texture_format,
335 ));
336 }
337 Err(e) => {
338 log::info!(
339 " Adapter {} ({:?}) failed: {}, trying next...",
340 info.name,
341 info.backend,
342 e
343 );
344 }
345 }
346 }
347
348 anyhow::bail!("No GPU adapter found that can configure the display surface")
349 }
350
351 #[cfg(not(target_family = "wasm"))]
354 async fn try_adapter_with_surface(
355 adapter: &wgpu::Adapter,
356 surface: &wgpu::Surface<'_>,
357 ) -> anyhow::Result<(wgpu::Device, wgpu::Queue, bool, TextureFormat)> {
358 let caps = surface.get_capabilities(adapter);
359 if caps.formats.is_empty() {
360 anyhow::bail!("no compatible surface formats");
361 }
362 if caps.alpha_modes.is_empty() {
363 anyhow::bail!("no compatible alpha modes");
364 }
365
366 let (device, queue, dual_source_blending, color_atlas_texture_format) =
367 Self::create_device(adapter).await?;
368 let error_scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
369
370 let test_config = wgpu::SurfaceConfiguration {
371 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
372 format: caps.formats[0],
373 width: 64,
374 height: 64,
375 present_mode: wgpu::PresentMode::Fifo,
376 desired_maximum_frame_latency: 2,
377 alpha_mode: caps.alpha_modes[0],
378 view_formats: vec![],
379 };
380
381 surface.configure(&device, &test_config);
382
383 let error = error_scope.pop().await;
384 if let Some(e) = error {
385 anyhow::bail!("surface configuration failed: {e}");
386 }
387
388 Ok((
389 device,
390 queue,
391 dual_source_blending,
392 color_atlas_texture_format,
393 ))
394 }
395
396 fn select_color_texture_format(adapter: &wgpu::Adapter) -> anyhow::Result<wgpu::TextureFormat> {
397 let required_usages = wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST;
398 let bgra_features = adapter.get_texture_format_features(wgpu::TextureFormat::Bgra8Unorm);
399 if bgra_features.allowed_usages.contains(required_usages) {
400 return Ok(wgpu::TextureFormat::Bgra8Unorm);
401 }
402
403 let rgba_features = adapter.get_texture_format_features(wgpu::TextureFormat::Rgba8Unorm);
404 if rgba_features.allowed_usages.contains(required_usages) {
405 let info = adapter.get_info();
406 log::warn!(
407 "Adapter {} ({:?}) does not support Bgra8Unorm atlas textures with usages {:?}; \
408 falling back to Rgba8Unorm atlas textures.",
409 info.name,
410 info.backend,
411 required_usages,
412 );
413 return Ok(wgpu::TextureFormat::Rgba8Unorm);
414 }
415
416 let info = adapter.get_info();
417 Err(anyhow::anyhow!(
418 "Adapter {} ({:?}, device={:#06x}) does not support a usable color atlas texture \
419 format with usages {:?}. Bgra8Unorm allowed usages: {:?}; \
420 Rgba8Unorm allowed usages: {:?}.",
421 info.name,
422 info.backend,
423 info.device,
424 required_usages,
425 bgra_features.allowed_usages,
426 rgba_features.allowed_usages,
427 ))
428 }
429 pub fn supports_dual_source_blending(&self) -> bool {
430 self.dual_source_blending
431 }
432
433 pub fn color_texture_format(&self) -> wgpu::TextureFormat {
434 self.color_texture_format
435 }
436
437 pub fn device_lost(&self) -> bool {
440 self.device_lost.load(Ordering::Relaxed)
441 }
442
443 pub(crate) fn device_lost_flag(&self) -> Arc<AtomicBool> {
445 Arc::clone(&self.device_lost)
446 }
447}
448
449#[cfg(not(target_family = "wasm"))]
450fn parse_pci_id(id: &str) -> anyhow::Result<u32> {
451 let mut id = id.trim();
452
453 if id.starts_with("0x") || id.starts_with("0X") {
454 id = &id[2..];
455 }
456 let is_hex_string = id.chars().all(|c| c.is_ascii_hexdigit());
457 let is_4_chars = id.len() == 4;
458 anyhow::ensure!(
459 is_4_chars && is_hex_string,
460 "Expected a 4 digit PCI ID in hexadecimal format"
461 );
462
463 u32::from_str_radix(id, 16).context("parsing PCI ID as hex")
464}
465
466#[cfg(test)]
467mod tests {
468 use super::parse_pci_id;
469
470 #[test]
471 fn test_parse_device_id() {
472 assert!(parse_pci_id("0xABCD").is_ok());
473 assert!(parse_pci_id("ABCD").is_ok());
474 assert!(parse_pci_id("abcd").is_ok());
475 assert!(parse_pci_id("1234").is_ok());
476 assert!(parse_pci_id("123").is_err());
477 assert_eq!(
478 parse_pci_id(&format!("{:x}", 0x1234)).unwrap(),
479 parse_pci_id(&format!("{:X}", 0x1234)).unwrap(),
480 );
481
482 assert_eq!(
483 parse_pci_id(&format!("{:#x}", 0x1234)).unwrap(),
484 parse_pci_id(&format!("{:#X}", 0x1234)).unwrap(),
485 );
486 }
487}