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 backend: WgpuBackend,
15 dual_source_blending: bool,
16 color_texture_format: wgpu::TextureFormat,
17 device_lost: Arc<AtomicBool>,
18}
19
20#[derive(Clone, Copy, Debug, Eq, PartialEq)]
21pub enum WgpuBackend {
22 BrowserWebGpu,
23 Gl,
24 Native(wgpu::Backend),
25}
26
27#[cfg(target_family = "wasm")]
28#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
29pub enum WebBackendPreference {
30 #[default]
31 Auto,
32 WebGpu,
33 WebGl,
34}
35
36#[cfg(target_family = "wasm")]
37pub struct PreparedWebGraphics {
38 pub context: WgpuContext,
39 pub surface: wgpu::Surface<'static>,
40}
41
42#[cfg(target_family = "wasm")]
47#[derive(Debug)]
48struct WebDisplaySource;
49
50#[cfg(target_family = "wasm")]
51impl raw_window_handle::HasDisplayHandle for WebDisplaySource {
52 fn display_handle(
53 &self,
54 ) -> Result<raw_window_handle::DisplayHandle<'_>, raw_window_handle::HandleError> {
55 Ok(raw_window_handle::DisplayHandle::web())
56 }
57}
58
59#[derive(Clone, Copy)]
60pub struct CompositorGpuHint {
61 pub vendor_id: u32,
62 pub device_id: u32,
63}
64
65impl WgpuContext {
66 #[cfg(not(target_family = "wasm"))]
67 pub fn new(
68 instance: wgpu::Instance,
69 surface: &wgpu::Surface<'_>,
70 compositor_gpu: Option<CompositorGpuHint>,
71 ) -> anyhow::Result<Self> {
72 Self::new_with_options(instance, surface, compositor_gpu, false)
73 }
74
75 #[cfg(not(target_family = "wasm"))]
76 pub fn new_rejecting_software(
77 instance: wgpu::Instance,
78 surface: &wgpu::Surface<'_>,
79 compositor_gpu: Option<CompositorGpuHint>,
80 ) -> anyhow::Result<Self> {
81 Self::new_with_options(instance, surface, compositor_gpu, true)
82 }
83
84 #[cfg(not(target_family = "wasm"))]
85 fn new_with_options(
86 instance: wgpu::Instance,
87 surface: &wgpu::Surface<'_>,
88 compositor_gpu: Option<CompositorGpuHint>,
89 reject_software: bool,
90 ) -> anyhow::Result<Self> {
91 let device_id_filter = match std::env::var("ZED_DEVICE_ID") {
92 Ok(val) => parse_pci_id(&val)
93 .context("Failed to parse device ID from `ZED_DEVICE_ID` environment variable")
94 .log_err(),
95 Err(std::env::VarError::NotPresent) => None,
96 err => {
97 err.context("Failed to read value of `ZED_DEVICE_ID` environment variable")
98 .log_err();
99 None
100 }
101 };
102
103 let (adapter, device, queue, dual_source_blending, color_texture_format) =
106 gpui::block_on(Self::select_adapter_and_device(
107 &instance,
108 device_id_filter,
109 surface,
110 compositor_gpu.as_ref(),
111 reject_software,
112 ))?;
113
114 let device_lost = Arc::new(AtomicBool::new(false));
115 device.set_device_lost_callback({
116 let device_lost = Arc::clone(&device_lost);
117 move |reason, message| {
118 log::error!("wgpu device lost: reason={reason:?}, message={message}");
119 if reason != wgpu::DeviceLostReason::Destroyed {
120 device_lost.store(true, Ordering::Relaxed);
121 }
122 }
123 });
124
125 log::info!(
126 "Selected GPU adapter: {:?} ({:?})",
127 adapter.get_info().name,
128 adapter.get_info().backend
129 );
130
131 let backend = WgpuBackend::Native(adapter.get_info().backend);
132 Ok(Self {
133 instance,
134 adapter,
135 device: Arc::new(device),
136 queue: Arc::new(queue),
137 backend,
138 dual_source_blending,
139 color_texture_format,
140 device_lost,
141 })
142 }
143
144 #[cfg(target_family = "wasm")]
145 pub async fn new_web(
146 canvas: &web_sys::HtmlCanvasElement,
147 preference: WebBackendPreference,
148 ) -> anyhow::Result<PreparedWebGraphics> {
149 Self::new_web_with_backend(canvas, preference).await
150 }
151
152 #[cfg(target_family = "wasm")]
153 #[allow(clippy::arc_with_non_send_sync)]
154 async fn new_web_with_backend(
155 canvas: &web_sys::HtmlCanvasElement,
156 preference: WebBackendPreference,
157 ) -> anyhow::Result<PreparedWebGraphics> {
158 let backends = match preference {
159 WebBackendPreference::Auto => wgpu::Backends::BROWSER_WEBGPU | wgpu::Backends::GL,
160 WebBackendPreference::WebGpu => wgpu::Backends::BROWSER_WEBGPU,
161 WebBackendPreference::WebGl => wgpu::Backends::GL,
162 };
163 let descriptor = wgpu::InstanceDescriptor {
164 backends,
165 flags: wgpu::InstanceFlags::default(),
166 backend_options: wgpu::BackendOptions::default(),
167 memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
168 display: Some(Box::new(WebDisplaySource)),
169 };
170 let instance = if preference == WebBackendPreference::Auto {
171 wgpu::util::new_instance_with_webgpu_detection(descriptor).await
172 } else {
173 wgpu::Instance::new(descriptor)
174 };
175 let surface = instance
176 .create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone()))
177 .map_err(|error| {
178 anyhow::anyhow!("Failed to create browser graphics surface: {error}")
179 })?;
180
181 let adapter = instance
182 .request_adapter(&wgpu::RequestAdapterOptions {
183 power_preference: wgpu::PowerPreference::HighPerformance,
184 compatible_surface: Some(&surface),
185 force_fallback_adapter: false,
186 })
187 .await
188 .map_err(|error| {
189 anyhow::anyhow!(
190 "Failed to request a {preference:?} adapter compatible with the canvas: {error}"
191 )
192 })?;
193 let adapter_info = adapter.get_info();
194 let backend = match adapter_info.backend {
195 wgpu::Backend::BrowserWebGpu => WgpuBackend::BrowserWebGpu,
196 wgpu::Backend::Gl => WgpuBackend::Gl,
197 backend => {
198 anyhow::bail!(
199 "Browser graphics initialization selected unexpected backend {backend:?}"
200 )
201 }
202 };
203
204 let device_lost = Arc::new(AtomicBool::new(false));
205 let (device, queue, dual_source_blending, color_texture_format) =
206 Self::create_device(&adapter).await?;
207 device.set_device_lost_callback({
208 let device_lost = Arc::clone(&device_lost);
209 move |reason, message| {
210 log::error!("wgpu device lost: reason={reason:?}, message={message}");
211 if reason != wgpu::DeviceLostReason::Destroyed {
212 device_lost.store(true, Ordering::Relaxed);
213 }
214 }
215 });
216 log::info!(
217 "Browser graphics initialized: requested={preference:?}, selected={backend:?}, \
218 adapter={:?}, limits={:?}, dual_source_blending={dual_source_blending}",
219 adapter_info.name,
220 device.limits(),
221 );
222
223 let context = Self {
224 instance,
225 adapter,
226 device: Arc::new(device),
227 queue: Arc::new(queue),
228 backend,
229 dual_source_blending,
230 color_texture_format,
231 device_lost,
232 };
233 Ok(PreparedWebGraphics { context, surface })
234 }
235
236 async fn create_device(
237 adapter: &wgpu::Adapter,
238 ) -> anyhow::Result<(wgpu::Device, wgpu::Queue, bool, TextureFormat)> {
239 let dual_source_blending = adapter
240 .features()
241 .contains(wgpu::Features::DUAL_SOURCE_BLENDING);
242
243 let mut required_features = wgpu::Features::empty();
244 if dual_source_blending {
245 required_features |= wgpu::Features::DUAL_SOURCE_BLENDING;
246 } else {
247 log::warn!(
248 "Dual-source blending not available on this GPU. \
249 Subpixel text antialiasing will be disabled."
250 );
251 }
252
253 let color_atlas_texture_format = Self::select_color_texture_format(adapter)?;
254 #[cfg(target_family = "wasm")]
255 let required_limits = if adapter.get_info().backend == wgpu::Backend::Gl {
256 wgpu::Limits::downlevel_webgl2_defaults()
257 .using_resolution(adapter.limits())
258 .using_alignment(adapter.limits())
259 } else {
260 wgpu::Limits::downlevel_defaults()
261 .using_resolution(adapter.limits())
262 .using_alignment(adapter.limits())
263 };
264 #[cfg(not(target_family = "wasm"))]
265 let required_limits = wgpu::Limits::downlevel_defaults()
266 .using_resolution(adapter.limits())
267 .using_alignment(adapter.limits());
268
269 let (device, queue) = adapter
270 .request_device(&wgpu::DeviceDescriptor {
271 label: Some("gpui_device"),
272 required_features,
273 required_limits,
274 memory_hints: wgpu::MemoryHints::MemoryUsage,
275 trace: wgpu::Trace::Off,
276 experimental_features: wgpu::ExperimentalFeatures::disabled(),
277 })
278 .await
279 .map_err(|e| anyhow::anyhow!("Failed to create wgpu device: {e}"))?;
280
281 Ok((
282 device,
283 queue,
284 dual_source_blending,
285 color_atlas_texture_format,
286 ))
287 }
288
289 #[cfg(not(target_family = "wasm"))]
290 pub fn instance(display: Box<dyn wgpu::wgt::WgpuHasDisplayHandle>) -> wgpu::Instance {
291 wgpu::Instance::new(wgpu::InstanceDescriptor {
292 backends: wgpu::Backends::VULKAN | wgpu::Backends::GL,
293 flags: wgpu::InstanceFlags::default(),
294 backend_options: wgpu::BackendOptions::default(),
295 memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
296 display: Some(display),
297 })
298 }
299
300 pub fn check_compatible_with_surface(&self, surface: &wgpu::Surface<'_>) -> anyhow::Result<()> {
301 let caps = surface.get_capabilities(&self.adapter);
302 if caps.formats.is_empty() {
303 let info = self.adapter.get_info();
304 anyhow::bail!(
305 "Adapter {:?} (backend={:?}, device={:#06x}) is not compatible with the \
306 display surface for this window.",
307 info.name,
308 info.backend,
309 info.device,
310 );
311 }
312 Ok(())
313 }
314
315 #[cfg(not(target_family = "wasm"))]
321 async fn select_adapter_and_device(
322 instance: &wgpu::Instance,
323 device_id_filter: Option<u32>,
324 surface: &wgpu::Surface<'_>,
325 compositor_gpu: Option<&CompositorGpuHint>,
326 reject_software: bool,
327 ) -> anyhow::Result<(
328 wgpu::Adapter,
329 wgpu::Device,
330 wgpu::Queue,
331 bool,
332 TextureFormat,
333 )> {
334 let mut adapters: Vec<_> = instance.enumerate_adapters(wgpu::Backends::all()).await;
335
336 if adapters.is_empty() {
337 anyhow::bail!("No GPU adapters found");
338 }
339
340 if let Some(device_id) = device_id_filter {
341 log::info!("ZED_DEVICE_ID filter: {:#06x}", device_id);
342 }
343
344 adapters.sort_by_key(|adapter| {
352 let info = adapter.get_info();
353
354 let device_known = info.device != 0;
357
358 let user_override: u8 = match device_id_filter {
359 Some(id) if device_known && info.device == id => 0,
360 _ => 1,
361 };
362
363 let compositor_match: u8 = match compositor_gpu {
364 Some(hint)
365 if device_known
366 && info.vendor == hint.vendor_id
367 && info.device == hint.device_id =>
368 {
369 0
370 }
371 _ => 1,
372 };
373
374 let type_priority: u8 = if info.device_type == wgpu::DeviceType::Cpu {
375 4
376 } else {
377 match info.device_type {
378 wgpu::DeviceType::DiscreteGpu => 0,
379 wgpu::DeviceType::IntegratedGpu => 1,
380 wgpu::DeviceType::Other => 2,
381 wgpu::DeviceType::VirtualGpu => 3,
382 wgpu::DeviceType::Cpu => 4,
383 }
384 };
385
386 let backend_priority: u8 = match info.backend {
387 wgpu::Backend::Vulkan | wgpu::Backend::Metal | wgpu::Backend::Dx12 => 0,
388 _ => 1,
389 };
390
391 (
392 user_override,
393 compositor_match,
394 type_priority,
395 backend_priority,
396 )
397 });
398
399 log::info!("Found {} GPU adapter(s):", adapters.len());
401 for adapter in &adapters {
402 let info = adapter.get_info();
403 log::info!(
404 " - {} (vendor={:#06x}, device={:#06x}, backend={:?}, type={:?})",
405 info.name,
406 info.vendor,
407 info.device,
408 info.backend,
409 info.device_type,
410 );
411 }
412
413 for adapter in adapters {
415 let info = adapter.get_info();
416
417 if reject_software && info.device_type == wgpu::DeviceType::Cpu {
418 log::info!(
419 "Skipping software renderer: {} ({:?})",
420 info.name,
421 info.backend
422 );
423 continue;
424 }
425
426 log::info!("Testing adapter: {} ({:?})...", info.name, info.backend);
427
428 match Self::try_adapter_with_surface(&adapter, surface).await {
429 Ok((device, queue, dual_source_blending, color_atlas_texture_format)) => {
430 log::info!(
431 "Selected GPU (passed configuration test): {} ({:?})",
432 info.name,
433 info.backend
434 );
435 return Ok((
436 adapter,
437 device,
438 queue,
439 dual_source_blending,
440 color_atlas_texture_format,
441 ));
442 }
443 Err(e) => {
444 log::info!(
445 " Adapter {} ({:?}) failed: {}, trying next...",
446 info.name,
447 info.backend,
448 e
449 );
450 }
451 }
452 }
453
454 anyhow::bail!("No GPU adapter found that can configure the display surface")
455 }
456
457 #[cfg(not(target_family = "wasm"))]
460 async fn try_adapter_with_surface(
461 adapter: &wgpu::Adapter,
462 surface: &wgpu::Surface<'_>,
463 ) -> anyhow::Result<(wgpu::Device, wgpu::Queue, bool, TextureFormat)> {
464 let caps = surface.get_capabilities(adapter);
465 if caps.formats.is_empty() {
466 anyhow::bail!("no compatible surface formats");
467 }
468 if caps.alpha_modes.is_empty() {
469 anyhow::bail!("no compatible alpha modes");
470 }
471
472 let (device, queue, dual_source_blending, color_atlas_texture_format) =
473 Self::create_device(adapter).await?;
474 let error_scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
475
476 let test_config = wgpu::SurfaceConfiguration {
477 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
478 format: caps.formats[0],
479 width: 64,
480 height: 64,
481 present_mode: wgpu::PresentMode::Fifo,
482 desired_maximum_frame_latency: 2,
483 alpha_mode: caps.alpha_modes[0],
484 view_formats: vec![],
485 };
486
487 surface.configure(&device, &test_config);
488
489 let error = error_scope.pop().await;
490 if let Some(e) = error {
491 anyhow::bail!("surface configuration failed: {e}");
492 }
493
494 Ok((
495 device,
496 queue,
497 dual_source_blending,
498 color_atlas_texture_format,
499 ))
500 }
501
502 fn select_color_texture_format(adapter: &wgpu::Adapter) -> anyhow::Result<wgpu::TextureFormat> {
503 let required_usages = wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST;
504 let bgra_features = adapter.get_texture_format_features(wgpu::TextureFormat::Bgra8Unorm);
505 let rgba_features = adapter.get_texture_format_features(wgpu::TextureFormat::Rgba8Unorm);
506 #[cfg(target_family = "wasm")]
507 if adapter.get_info().backend == wgpu::Backend::Gl
508 && rgba_features.allowed_usages.contains(required_usages)
509 {
510 return Ok(wgpu::TextureFormat::Rgba8Unorm);
511 }
512 if bgra_features.allowed_usages.contains(required_usages) {
513 return Ok(wgpu::TextureFormat::Bgra8Unorm);
514 }
515 if rgba_features.allowed_usages.contains(required_usages) {
516 let info = adapter.get_info();
517 log::warn!(
518 "Adapter {} ({:?}) does not support Bgra8Unorm atlas textures with usages {:?}; \
519 falling back to Rgba8Unorm atlas textures.",
520 info.name,
521 info.backend,
522 required_usages,
523 );
524 return Ok(wgpu::TextureFormat::Rgba8Unorm);
525 }
526
527 let info = adapter.get_info();
528 Err(anyhow::anyhow!(
529 "Adapter {} ({:?}, device={:#06x}) does not support a usable color atlas texture \
530 format with usages {:?}. Bgra8Unorm allowed usages: {:?}; \
531 Rgba8Unorm allowed usages: {:?}.",
532 info.name,
533 info.backend,
534 info.device,
535 required_usages,
536 bgra_features.allowed_usages,
537 rgba_features.allowed_usages,
538 ))
539 }
540 pub fn backend(&self) -> WgpuBackend {
541 self.backend
542 }
543
544 pub fn uses_webgl_instance_data(&self) -> bool {
545 matches!(self.backend, WgpuBackend::Gl) && cfg!(target_family = "wasm")
546 }
547
548 pub fn supports_dual_source_blending(&self) -> bool {
549 self.dual_source_blending
550 }
551
552 pub fn color_texture_format(&self) -> wgpu::TextureFormat {
553 self.color_texture_format
554 }
555
556 pub fn device_lost(&self) -> bool {
559 self.device_lost.load(Ordering::Relaxed)
560 }
561
562 pub(crate) fn device_lost_flag(&self) -> Arc<AtomicBool> {
564 Arc::clone(&self.device_lost)
565 }
566}
567
568#[cfg(not(target_family = "wasm"))]
569fn parse_pci_id(id: &str) -> anyhow::Result<u32> {
570 let mut id = id.trim();
571
572 if id.starts_with("0x") || id.starts_with("0X") {
573 id = &id[2..];
574 }
575 let is_hex_string = id.chars().all(|c| c.is_ascii_hexdigit());
576 let is_4_chars = id.len() == 4;
577 anyhow::ensure!(
578 is_4_chars && is_hex_string,
579 "Expected a 4 digit PCI ID in hexadecimal format"
580 );
581
582 u32::from_str_radix(id, 16).context("parsing PCI ID as hex")
583}
584
585#[cfg(test)]
586mod tests {
587 use super::parse_pci_id;
588
589 #[test]
590 fn test_parse_device_id() {
591 assert!(parse_pci_id("0xABCD").is_ok());
592 assert!(parse_pci_id("ABCD").is_ok());
593 assert!(parse_pci_id("abcd").is_ok());
594 assert!(parse_pci_id("1234").is_ok());
595 assert!(parse_pci_id("123").is_err());
596 assert_eq!(
597 parse_pci_id(&format!("{:x}", 0x1234)).unwrap(),
598 parse_pci_id(&format!("{:X}", 0x1234)).unwrap(),
599 );
600
601 assert_eq!(
602 parse_pci_id(&format!("{:#x}", 0x1234)).unwrap(),
603 parse_pci_id(&format!("{:#X}", 0x1234)).unwrap(),
604 );
605 }
606}