1#[cfg(not(target_family = "wasm"))]
2use anyhow::Context as _;
3#[cfg(not(target_family = "wasm"))]
4use gpui_util::ResultExt;
5use parking_lot::Mutex;
6use std::sync::Arc;
7use std::sync::atomic::{AtomicBool, Ordering};
8use wgpu::TextureFormat;
9
10#[derive(Clone)]
11pub struct WgpuContext {
12 pub instance: wgpu::Instance,
13 pub adapter: wgpu::Adapter,
14 pub device: Arc<wgpu::Device>,
15 pub queue: Arc<wgpu::Queue>,
16 backend: WgpuBackend,
17 dual_source_blending: bool,
18 color_texture_format: wgpu::TextureFormat,
19 errors: Arc<DeviceErrorState>,
20}
21
22#[derive(Default)]
29pub struct DeviceErrorState {
30 lost: AtomicBool,
31 last_error: Mutex<(u64, Option<String>)>,
32}
33
34impl DeviceErrorState {
35 fn install(device: &wgpu::Device) -> Arc<Self> {
36 let errors = Arc::new(Self::default());
37 device.set_device_lost_callback({
38 let errors = Arc::clone(&errors);
39 move |reason, message| {
40 log::error!("wgpu device lost: reason={reason:?}, message={message}");
41 if reason != wgpu::DeviceLostReason::Destroyed {
42 errors.lost.store(true, Ordering::Relaxed);
43 }
44 }
45 });
46 device.on_uncaptured_error(Arc::new({
47 let errors = Arc::clone(&errors);
48 move |error| errors.record(error.to_string())
49 }));
50 errors
51 }
52
53 pub fn device_lost(&self) -> bool {
55 self.lost.load(Ordering::Relaxed)
56 }
57
58 fn record(&self, error: String) {
59 let mut last_error = self.last_error.lock();
60 last_error.0 = last_error.0.wrapping_add(1);
61 last_error.1 = Some(error);
62 }
63
64 pub fn observe_error(&self, generation: &mut u64) -> Option<String> {
69 let last_error = self.last_error.lock();
70 if *generation == last_error.0 {
71 return None;
72 }
73 *generation = last_error.0;
74 last_error.1.clone()
75 }
76
77 pub fn current_generation(&self) -> u64 {
80 self.last_error.lock().0
81 }
82}
83
84#[derive(Clone, Copy, Debug, Eq, PartialEq)]
85pub enum WgpuBackend {
86 BrowserWebGpu,
87 Gl,
88 Native(wgpu::Backend),
89}
90
91#[cfg(target_family = "wasm")]
92#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
93pub enum WebBackendPreference {
94 #[default]
95 Auto,
96 WebGpu,
97 WebGl,
98}
99
100#[cfg(target_family = "wasm")]
101pub struct PreparedWebGraphics {
102 pub context: WgpuContext,
103 pub surface: wgpu::Surface<'static>,
104}
105
106#[cfg(target_family = "wasm")]
111#[derive(Debug)]
112struct WebDisplaySource;
113
114#[cfg(target_family = "wasm")]
115impl raw_window_handle::HasDisplayHandle for WebDisplaySource {
116 fn display_handle(
117 &self,
118 ) -> Result<raw_window_handle::DisplayHandle<'_>, raw_window_handle::HandleError> {
119 Ok(raw_window_handle::DisplayHandle::web())
120 }
121}
122
123#[derive(Clone, Copy)]
124pub struct CompositorGpuHint {
125 pub vendor_id: u32,
126 pub device_id: u32,
127}
128
129impl WgpuContext {
130 #[cfg(not(target_family = "wasm"))]
131 pub fn new(
132 instance: wgpu::Instance,
133 surface: &wgpu::Surface<'_>,
134 compositor_gpu: Option<CompositorGpuHint>,
135 ) -> anyhow::Result<Self> {
136 Self::new_with_options(instance, surface, compositor_gpu, false)
137 }
138
139 #[cfg(not(target_family = "wasm"))]
140 pub fn new_rejecting_software(
141 instance: wgpu::Instance,
142 surface: &wgpu::Surface<'_>,
143 compositor_gpu: Option<CompositorGpuHint>,
144 ) -> anyhow::Result<Self> {
145 Self::new_with_options(instance, surface, compositor_gpu, true)
146 }
147
148 #[cfg(not(target_family = "wasm"))]
149 fn new_with_options(
150 instance: wgpu::Instance,
151 surface: &wgpu::Surface<'_>,
152 compositor_gpu: Option<CompositorGpuHint>,
153 reject_software: bool,
154 ) -> anyhow::Result<Self> {
155 let device_id_filter = Self::device_id_filter();
156
157 let (adapter, device, queue, dual_source_blending, color_texture_format) =
160 pollster::block_on(Self::select_adapter_and_device(
161 &instance,
162 device_id_filter,
163 surface,
164 compositor_gpu.as_ref(),
165 reject_software,
166 ))?;
167
168 Ok(Self::from_native_adapter_and_device(
169 instance,
170 adapter,
171 device,
172 queue,
173 dual_source_blending,
174 color_texture_format,
175 ))
176 }
177
178 #[cfg(all(
179 not(target_family = "wasm"),
180 any(test, feature = "bench-support", feature = "test-support", feature = "headless")
181 ))]
182 pub(crate) fn new_headless() -> anyhow::Result<(Self, wgpu::TextureFormat)> {
183 static HEADLESS: Mutex<Option<(WgpuContext, wgpu::TextureFormat)>> = Mutex::new(None);
189 let mut headless = HEADLESS.lock();
190 if let Some((context, format)) = headless.as_ref()
191 && !context.device_lost()
192 {
193 return Ok((context.clone(), *format));
194 }
195 let created = Self::create_headless()?;
196 *headless = Some(created.clone());
197 Ok(created)
198 }
199
200 #[cfg(all(
201 not(target_family = "wasm"),
202 any(test, feature = "bench-support", feature = "test-support", feature = "headless")
203 ))]
204 fn create_headless() -> anyhow::Result<(Self, wgpu::TextureFormat)> {
205 let instance = Self::instance(None);
206 let device_id_filter = Self::device_id_filter();
207 let (adapter, device, queue, dual_source_blending, color_texture_format, target_format) =
208 pollster::block_on(async {
209 let mut adapters = instance.enumerate_adapters(wgpu::Backends::all()).await;
210 Self::sort_adapters(&mut adapters, device_id_filter, None);
211
212 for adapter in adapters {
213 let adapter_info = adapter.get_info();
214 let Some(target_format) = Self::headless_target_format(&adapter) else {
215 log::warn!(
216 "Adapter {:?} has no supported headless render target format",
217 adapter_info.name
218 );
219 continue;
220 };
221
222 match Self::create_device(&adapter).await {
223 Ok((device, queue, dual_source_blending, color_texture_format)) => {
224 #[cfg(feature = "bench-support")]
225 if adapter_info.device_type == wgpu::DeviceType::Cpu {
226 log::error!(
227 "Headless renderer selected software adapter {:?}; \
228 benchmark results measure CPU software rendering, not hardware GPU rendering",
229 adapter_info.name
230 );
231 }
232 return Ok((
233 adapter,
234 device,
235 queue,
236 dual_source_blending,
237 color_texture_format,
238 target_format,
239 ));
240 }
241 Err(error) => {
242 log::warn!(
243 "Failed to create a headless device for adapter {:?}: {error:#}",
244 adapter_info.name
245 );
246 }
247 }
248 }
249
250 anyhow::bail!("No usable headless GPU adapter found")
251 })?;
252
253 Ok((
254 Self::from_native_adapter_and_device(
255 instance,
256 adapter,
257 device,
258 queue,
259 dual_source_blending,
260 color_texture_format,
261 ),
262 target_format,
263 ))
264 }
265
266 #[cfg(all(
267 not(target_family = "wasm"),
268 any(test, feature = "bench-support", feature = "test-support", feature = "headless")
269 ))]
270 fn headless_target_format(adapter: &wgpu::Adapter) -> Option<wgpu::TextureFormat> {
274 let required_usages =
275 wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC;
276 [
277 wgpu::TextureFormat::Rgba8Unorm,
278 wgpu::TextureFormat::Bgra8Unorm,
279 ]
280 .into_iter()
281 .find(|format| {
282 adapter
283 .get_texture_format_features(*format)
284 .allowed_usages
285 .contains(required_usages)
286 })
287 }
288
289 #[cfg(not(target_family = "wasm"))]
290 fn from_native_adapter_and_device(
291 instance: wgpu::Instance,
292 adapter: wgpu::Adapter,
293 device: wgpu::Device,
294 queue: wgpu::Queue,
295 dual_source_blending: bool,
296 color_texture_format: TextureFormat,
297 ) -> Self {
298 let errors = DeviceErrorState::install(&device);
299
300 log::info!(
301 "Selected GPU adapter: {:?} ({:?})",
302 adapter.get_info().name,
303 adapter.get_info().backend
304 );
305 let backend = WgpuBackend::Native(adapter.get_info().backend);
306
307 Self {
308 instance,
309 adapter,
310 device: Arc::new(device),
311 queue: Arc::new(queue),
312 backend,
313 dual_source_blending,
314 color_texture_format,
315 errors,
316 }
317 }
318
319 #[cfg(not(target_family = "wasm"))]
320 fn device_id_filter() -> Option<u32> {
321 match std::env::var("ZED_DEVICE_ID") {
322 Ok(value) => parse_pci_id(&value)
323 .context("Failed to parse device ID from `ZED_DEVICE_ID` environment variable")
324 .log_err(),
325 Err(std::env::VarError::NotPresent) => None,
326 error => {
327 error
328 .context("Failed to read value of `ZED_DEVICE_ID` environment variable")
329 .log_err();
330 None
331 }
332 }
333 }
334
335 #[cfg(target_family = "wasm")]
336 pub async fn new_web(
337 canvas: &web_sys::HtmlCanvasElement,
338 preference: WebBackendPreference,
339 ) -> anyhow::Result<PreparedWebGraphics> {
340 Self::new_web_with_backend(canvas, preference).await
341 }
342
343 #[cfg(target_family = "wasm")]
344 #[allow(clippy::arc_with_non_send_sync)]
345 async fn new_web_with_backend(
346 canvas: &web_sys::HtmlCanvasElement,
347 preference: WebBackendPreference,
348 ) -> anyhow::Result<PreparedWebGraphics> {
349 let backends = match preference {
350 WebBackendPreference::Auto => wgpu::Backends::BROWSER_WEBGPU | wgpu::Backends::GL,
351 WebBackendPreference::WebGpu => wgpu::Backends::BROWSER_WEBGPU,
352 WebBackendPreference::WebGl => wgpu::Backends::GL,
353 };
354 let descriptor = wgpu::InstanceDescriptor {
355 backends,
356 flags: wgpu::InstanceFlags::default(),
357 backend_options: wgpu::BackendOptions::default(),
358 memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
359 display: Some(Box::new(WebDisplaySource)),
360 };
361 let instance = if preference == WebBackendPreference::Auto {
362 wgpu::util::new_instance_with_webgpu_detection(descriptor).await
363 } else {
364 wgpu::Instance::new(descriptor)
365 };
366 let surface = instance
367 .create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone()))
368 .map_err(|error| {
369 anyhow::anyhow!("Failed to create browser graphics surface: {error}")
370 })?;
371
372 let adapter = instance
373 .request_adapter(&wgpu::RequestAdapterOptions {
374 power_preference: wgpu::PowerPreference::HighPerformance,
375 compatible_surface: Some(&surface),
376 force_fallback_adapter: false,
377 })
378 .await
379 .map_err(|error| {
380 anyhow::anyhow!(
381 "Failed to request a {preference:?} adapter compatible with the canvas: {error}"
382 )
383 })?;
384 let adapter_info = adapter.get_info();
385 let backend = match adapter_info.backend {
386 wgpu::Backend::BrowserWebGpu => WgpuBackend::BrowserWebGpu,
387 wgpu::Backend::Gl => WgpuBackend::Gl,
388 backend => {
389 anyhow::bail!(
390 "Browser graphics initialization selected unexpected backend {backend:?}"
391 )
392 }
393 };
394
395 let (device, queue, dual_source_blending, color_texture_format) =
396 Self::create_device(&adapter).await?;
397 let errors = DeviceErrorState::install(&device);
398 log::info!(
399 "Browser graphics initialized: requested={preference:?}, selected={backend:?}, \
400 adapter={:?}, limits={:?}, dual_source_blending={dual_source_blending}",
401 adapter_info.name,
402 device.limits(),
403 );
404
405 let context = Self {
406 instance,
407 adapter,
408 device: Arc::new(device),
409 queue: Arc::new(queue),
410 backend,
411 dual_source_blending,
412 color_texture_format,
413 errors,
414 };
415 Ok(PreparedWebGraphics { context, surface })
416 }
417
418 async fn create_device(
419 adapter: &wgpu::Adapter,
420 ) -> anyhow::Result<(wgpu::Device, wgpu::Queue, bool, TextureFormat)> {
421 let dual_source_blending = adapter
422 .features()
423 .contains(wgpu::Features::DUAL_SOURCE_BLENDING);
424
425 let mut required_features = wgpu::Features::empty();
426 if dual_source_blending {
427 required_features |= wgpu::Features::DUAL_SOURCE_BLENDING;
428 } else {
429 log::warn!(
430 "Dual-source blending not available on this GPU. \
431 Subpixel text antialiasing will be disabled."
432 );
433 }
434
435 let color_atlas_texture_format = Self::select_color_texture_format(adapter)?;
436
437 if adapter
441 .features()
442 .contains(wgpu::Features::TEXTURE_FORMAT_NV12)
443 {
444 required_features |= wgpu::Features::TEXTURE_FORMAT_NV12;
445 }
446 #[cfg(target_family = "wasm")]
447 let required_limits = if adapter.get_info().backend == wgpu::Backend::Gl {
448 wgpu::Limits::downlevel_webgl2_defaults()
449 .using_resolution(adapter.limits())
450 .using_alignment(adapter.limits())
451 } else {
452 wgpu::Limits::downlevel_defaults()
453 .using_resolution(adapter.limits())
454 .using_alignment(adapter.limits())
455 };
456 #[cfg(not(target_family = "wasm"))]
457 let required_limits = wgpu::Limits::downlevel_defaults()
458 .using_resolution(adapter.limits())
459 .using_alignment(adapter.limits());
460
461 let descriptor = wgpu::DeviceDescriptor {
462 label: Some("gpui_device"),
463 required_features,
464 required_limits,
465 memory_hints: wgpu::MemoryHints::MemoryUsage,
466 trace: wgpu::Trace::Off,
467 experimental_features: wgpu::ExperimentalFeatures::disabled(),
468 };
469
470 #[cfg(target_os = "linux")]
476 let with_surface_import = Self::device_with_surface_import(adapter, &descriptor);
477 #[cfg(not(target_os = "linux"))]
478 let with_surface_import: Option<(wgpu::Device, wgpu::Queue)> = None;
479
480 let (device, queue) = match with_surface_import {
481 Some(created) => created,
482 None => adapter
483 .request_device(&descriptor)
484 .await
485 .map_err(|e| anyhow::anyhow!("Failed to create wgpu device: {e}"))?,
486 };
487
488 Ok((
489 device,
490 queue,
491 dual_source_blending,
492 color_atlas_texture_format,
493 ))
494 }
495
496 #[cfg(target_os = "linux")]
509 fn device_with_surface_import(
510 adapter: &wgpu::Adapter,
511 descriptor: &wgpu::DeviceDescriptor<'_>,
512 ) -> Option<(wgpu::Device, wgpu::Queue)> {
513 use wgpu::hal::vulkan::Api;
514
515 unsafe {
520 let hal = adapter.as_hal::<Api>()?;
521 if !hal
522 .physical_device_capabilities()
523 .supports_extension(ash::vk::EXT_IMAGE_DRM_FORMAT_MODIFIER_NAME)
524 {
525 return None;
526 }
527 let open = hal
528 .open_with_callback(
529 descriptor.required_features,
530 &descriptor.required_limits,
531 &descriptor.memory_hints,
532 Some(Box::new(|args| {
533 args.extensions
534 .push(ash::vk::EXT_IMAGE_DRM_FORMAT_MODIFIER_NAME);
535 })),
536 )
537 .ok()?;
538 let (device, queue) = adapter
539 .create_device_from_hal::<Api>(open, descriptor)
540 .ok()?;
541 let enabled = device.as_hal::<Api>().is_some_and(|device| {
545 device
546 .enabled_device_extensions()
547 .contains(&ash::vk::EXT_IMAGE_DRM_FORMAT_MODIFIER_NAME)
548 });
549 enabled.then_some((device, queue))
550 }
551 }
552
553 #[cfg(not(target_family = "wasm"))]
554 pub fn instance(display: Option<Box<dyn wgpu::wgt::WgpuHasDisplayHandle>>) -> wgpu::Instance {
555 wgpu::Instance::new(wgpu::InstanceDescriptor {
556 backends: Self::backends(),
557 flags: wgpu::InstanceFlags::default(),
558 backend_options: wgpu::BackendOptions::default(),
559 memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
560 display,
561 })
562 }
563
564 #[cfg(not(target_family = "wasm"))]
572 fn backends() -> wgpu::Backends {
573 let mut backends = wgpu::Backends::VULKAN | wgpu::Backends::GL;
574 if cfg!(target_os = "windows") {
575 backends |= wgpu::Backends::DX12;
576 }
577 if cfg!(target_os = "macos") {
578 backends |= wgpu::Backends::METAL;
579 }
580 backends
581 }
582
583 pub fn check_compatible_with_surface(&self, surface: &wgpu::Surface<'_>) -> anyhow::Result<()> {
584 let caps = surface.get_capabilities(&self.adapter);
585 if caps.formats.is_empty() {
586 let info = self.adapter.get_info();
587 anyhow::bail!(
588 "Adapter {:?} (backend={:?}, device={:#06x}) is not compatible with the \
589 display surface for this window.",
590 info.name,
591 info.backend,
592 info.device,
593 );
594 }
595 Ok(())
596 }
597
598 #[cfg(not(target_family = "wasm"))]
604 async fn select_adapter_and_device(
605 instance: &wgpu::Instance,
606 device_id_filter: Option<u32>,
607 surface: &wgpu::Surface<'_>,
608 compositor_gpu: Option<&CompositorGpuHint>,
609 reject_software: bool,
610 ) -> anyhow::Result<(
611 wgpu::Adapter,
612 wgpu::Device,
613 wgpu::Queue,
614 bool,
615 TextureFormat,
616 )> {
617 let mut adapters: Vec<_> = instance.enumerate_adapters(wgpu::Backends::all()).await;
618
619 if adapters.is_empty() {
620 anyhow::bail!("No GPU adapters found");
621 }
622
623 if let Some(device_id) = device_id_filter {
624 log::info!("ZED_DEVICE_ID filter: {:#06x}", device_id);
625 }
626
627 Self::sort_adapters(&mut adapters, device_id_filter, compositor_gpu);
628
629 log::info!("Found {} GPU adapter(s):", adapters.len());
631 for adapter in &adapters {
632 let info = adapter.get_info();
633 log::info!(
634 " - {} (vendor={:#06x}, device={:#06x}, backend={:?}, type={:?})",
635 info.name,
636 info.vendor,
637 info.device,
638 info.backend,
639 info.device_type,
640 );
641 }
642
643 for adapter in adapters {
645 let info = adapter.get_info();
646
647 if reject_software && info.device_type == wgpu::DeviceType::Cpu {
648 log::info!(
649 "Skipping software renderer: {} ({:?})",
650 info.name,
651 info.backend
652 );
653 continue;
654 }
655
656 log::info!("Testing adapter: {} ({:?})...", info.name, info.backend);
657
658 match Self::try_adapter_with_surface(&adapter, surface).await {
659 Ok((device, queue, dual_source_blending, color_atlas_texture_format)) => {
660 log::info!(
661 "Selected GPU (passed configuration test): {} ({:?})",
662 info.name,
663 info.backend
664 );
665 return Ok((
666 adapter,
667 device,
668 queue,
669 dual_source_blending,
670 color_atlas_texture_format,
671 ));
672 }
673 Err(e) => {
674 log::info!(
675 " Adapter {} ({:?}) failed: {}, trying next...",
676 info.name,
677 info.backend,
678 e
679 );
680 }
681 }
682 }
683
684 anyhow::bail!("No GPU adapter found that can configure the display surface")
685 }
686
687 #[cfg(not(target_family = "wasm"))]
695 fn sort_adapters(
696 adapters: &mut [wgpu::Adapter],
697 device_id_filter: Option<u32>,
698 compositor_gpu: Option<&CompositorGpuHint>,
699 ) {
700 adapters.sort_by_key(|adapter| {
701 let info = adapter.get_info();
702 let device_known = info.device != 0;
705 let user_override: u8 = match device_id_filter {
706 Some(id) if device_known && info.device == id => 0,
707 _ => 1,
708 };
709 let compositor_match: u8 = match compositor_gpu {
710 Some(hint)
711 if device_known
712 && info.vendor == hint.vendor_id
713 && info.device == hint.device_id =>
714 {
715 0
716 }
717 _ => 1,
718 };
719 let type_priority: u8 = match info.device_type {
720 wgpu::DeviceType::DiscreteGpu => 0,
721 wgpu::DeviceType::IntegratedGpu => 1,
722 wgpu::DeviceType::Other => 2,
723 wgpu::DeviceType::VirtualGpu => 3,
724 wgpu::DeviceType::Cpu => 4,
725 };
726 let backend_priority: u8 = match info.backend {
727 wgpu::Backend::Vulkan | wgpu::Backend::Metal | wgpu::Backend::Dx12 => 0,
728 _ => 1,
729 };
730
731 (
732 user_override,
733 compositor_match,
734 type_priority,
735 backend_priority,
736 )
737 });
738 }
739
740 #[cfg(not(target_family = "wasm"))]
743 async fn try_adapter_with_surface(
744 adapter: &wgpu::Adapter,
745 surface: &wgpu::Surface<'_>,
746 ) -> anyhow::Result<(wgpu::Device, wgpu::Queue, bool, TextureFormat)> {
747 let caps = surface.get_capabilities(adapter);
748 if caps.formats.is_empty() {
749 anyhow::bail!("no compatible surface formats");
750 }
751 if caps.alpha_modes.is_empty() {
752 anyhow::bail!("no compatible alpha modes");
753 }
754
755 let (device, queue, dual_source_blending, color_atlas_texture_format) =
756 Self::create_device(adapter).await?;
757 let error_scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
758
759 let test_config = wgpu::SurfaceConfiguration {
760 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
761 format: caps.formats[0],
762 width: 64,
763 height: 64,
764 present_mode: wgpu::PresentMode::Fifo,
765 desired_maximum_frame_latency: 2,
766 alpha_mode: caps.alpha_modes[0],
767 view_formats: vec![],
768 };
769
770 surface.configure(&device, &test_config);
771
772 let error = error_scope.pop().await;
773 if let Some(e) = error {
774 anyhow::bail!("surface configuration failed: {e}");
775 }
776
777 Ok((
778 device,
779 queue,
780 dual_source_blending,
781 color_atlas_texture_format,
782 ))
783 }
784
785 fn select_color_texture_format(adapter: &wgpu::Adapter) -> anyhow::Result<wgpu::TextureFormat> {
786 let required_usages = wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST;
787 let bgra_features = adapter.get_texture_format_features(wgpu::TextureFormat::Bgra8Unorm);
788 let rgba_features = adapter.get_texture_format_features(wgpu::TextureFormat::Rgba8Unorm);
789 #[cfg(target_family = "wasm")]
790 if adapter.get_info().backend == wgpu::Backend::Gl
791 && rgba_features.allowed_usages.contains(required_usages)
792 {
793 return Ok(wgpu::TextureFormat::Rgba8Unorm);
794 }
795 if bgra_features.allowed_usages.contains(required_usages) {
796 return Ok(wgpu::TextureFormat::Bgra8Unorm);
797 }
798 if rgba_features.allowed_usages.contains(required_usages) {
799 let info = adapter.get_info();
800 log::warn!(
801 "Adapter {} ({:?}) does not support Bgra8Unorm atlas textures with usages {:?}; \
802 falling back to Rgba8Unorm atlas textures.",
803 info.name,
804 info.backend,
805 required_usages,
806 );
807 return Ok(wgpu::TextureFormat::Rgba8Unorm);
808 }
809
810 let info = adapter.get_info();
811 Err(anyhow::anyhow!(
812 "Adapter {} ({:?}, device={:#06x}) does not support a usable color atlas texture \
813 format with usages {:?}. Bgra8Unorm allowed usages: {:?}; \
814 Rgba8Unorm allowed usages: {:?}.",
815 info.name,
816 info.backend,
817 info.device,
818 required_usages,
819 bgra_features.allowed_usages,
820 rgba_features.allowed_usages,
821 ))
822 }
823 pub fn backend(&self) -> WgpuBackend {
824 self.backend
825 }
826
827 pub fn uses_webgl_instance_data(&self) -> bool {
828 matches!(self.backend, WgpuBackend::Gl) && cfg!(target_family = "wasm")
829 }
830
831 pub fn supports_dual_source_blending(&self) -> bool {
832 self.dual_source_blending
833 }
834
835 pub fn color_texture_format(&self) -> wgpu::TextureFormat {
836 self.color_texture_format
837 }
838
839 pub fn device_lost(&self) -> bool {
842 self.errors.device_lost()
843 }
844
845 pub fn errors(&self) -> &Arc<DeviceErrorState> {
847 &self.errors
848 }
849}
850
851#[cfg(not(target_family = "wasm"))]
852fn parse_pci_id(id: &str) -> anyhow::Result<u32> {
853 let mut id = id.trim();
854
855 if id.starts_with("0x") || id.starts_with("0X") {
856 id = &id[2..];
857 }
858 let is_hex_string = id.chars().all(|c| c.is_ascii_hexdigit());
859 let is_4_chars = id.len() == 4;
860 anyhow::ensure!(
861 is_4_chars && is_hex_string,
862 "Expected a 4 digit PCI ID in hexadecimal format"
863 );
864
865 u32::from_str_radix(id, 16).context("parsing PCI ID as hex")
866}
867
868#[cfg(test)]
869mod tests {
870 use super::{DeviceErrorState, parse_pci_id};
871
872 #[cfg(target_os = "linux")]
875 #[test]
876 fn the_device_enables_the_drm_format_modifier_extension() {
877 use wgpu::hal::vulkan::Api;
878
879 let instance = super::WgpuContext::instance(None);
880 let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
881 power_preference: wgpu::PowerPreference::LowPower,
882 compatible_surface: None,
883 force_fallback_adapter: false,
884 }));
885 let Ok(adapter) = adapter else {
886 eprintln!("skipping: no adapter");
887 return;
888 };
889 let supported = unsafe {
890 adapter.as_hal::<Api>().is_some_and(|hal| {
891 hal.physical_device_capabilities()
892 .supports_extension(ash::vk::EXT_IMAGE_DRM_FORMAT_MODIFIER_NAME)
893 })
894 };
895 if !supported {
896 eprintln!(
897 "skipping: {:?} does not support the extension",
898 adapter.get_info().name
899 );
900 return;
901 }
902
903 let descriptor = wgpu::DeviceDescriptor {
904 label: Some("drm modifier extension test"),
905 required_features: wgpu::Features::empty(),
906 required_limits: wgpu::Limits::downlevel_defaults()
907 .using_resolution(adapter.limits())
908 .using_alignment(adapter.limits()),
909 memory_hints: wgpu::MemoryHints::MemoryUsage,
910 trace: wgpu::Trace::Off,
911 experimental_features: wgpu::ExperimentalFeatures::disabled(),
912 };
913 let (device, _queue) =
914 super::WgpuContext::device_with_surface_import(&adapter, &descriptor)
915 .expect("the escape hatch creates the device");
916 let enabled = unsafe { device.as_hal::<Api>() }.is_some_and(|device| {
917 device
918 .enabled_device_extensions()
919 .contains(&ash::vk::EXT_IMAGE_DRM_FORMAT_MODIFIER_NAME)
920 });
921 assert!(enabled, "the created device must have the extension enabled");
922 }
923
924 #[test]
925 fn device_errors_are_observed_independently() {
926 let errors = DeviceErrorState::default();
927 let mut first = 0;
928 let mut second = 0;
929 assert_eq!(errors.observe_error(&mut first), None);
930 errors.record("first".into());
931 assert_eq!(errors.observe_error(&mut first).as_deref(), Some("first"));
932 assert_eq!(errors.observe_error(&mut first), None);
933 assert_eq!(errors.observe_error(&mut second).as_deref(), Some("first"));
934 assert_eq!(errors.observe_error(&mut second), None);
935
936 errors.record("second".into());
937 assert_eq!(errors.observe_error(&mut second).as_deref(), Some("second"));
938 assert_eq!(errors.observe_error(&mut first).as_deref(), Some("second"));
939 assert_eq!(errors.observe_error(&mut first), None);
940 assert_eq!(errors.observe_error(&mut second), None);
941 }
942
943 #[test]
944 fn device_errors_coalesce_and_new_observers_see_latest() {
945 let errors = DeviceErrorState::default();
946 errors.record("older".into());
947 errors.record("latest".into());
948 let mut generation = 0;
949 assert_eq!(
950 errors.observe_error(&mut generation).as_deref(),
951 Some("latest")
952 );
953 assert_eq!(errors.observe_error(&mut generation), None);
954
955 let replacement = DeviceErrorState::default();
956 replacement.record("replacement".into());
957 generation = 0;
958 assert_eq!(
959 replacement.observe_error(&mut generation).as_deref(),
960 Some("replacement")
961 );
962 assert_eq!(replacement.observe_error(&mut generation), None);
963 }
964
965 #[test]
966 fn test_parse_device_id() {
967 assert!(parse_pci_id("0xABCD").is_ok());
968 assert!(parse_pci_id("ABCD").is_ok());
969 assert!(parse_pci_id("abcd").is_ok());
970 assert!(parse_pci_id("1234").is_ok());
971 assert!(parse_pci_id("123").is_err());
972 assert_eq!(
973 parse_pci_id(&format!("{:x}", 0x1234)).unwrap(),
974 parse_pci_id(&format!("{:X}", 0x1234)).unwrap(),
975 );
976
977 assert_eq!(
978 parse_pci_id(&format!("{:#x}", 0x1234)).unwrap(),
979 parse_pci_id(&format!("{:#X}", 0x1234)).unwrap(),
980 );
981 }
982}