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 #[cfg(target_family = "wasm")]
62 log::error!("wgpu validation: {error}");
63 let mut last_error = self.last_error.lock();
64 last_error.0 = last_error.0.wrapping_add(1);
65 last_error.1 = Some(error);
66 }
67
68 pub fn observe_error(&self, generation: &mut u64) -> Option<String> {
73 let last_error = self.last_error.lock();
74 if *generation == last_error.0 {
75 return None;
76 }
77 *generation = last_error.0;
78 last_error.1.clone()
79 }
80
81 pub fn current_generation(&self) -> u64 {
84 self.last_error.lock().0
85 }
86}
87
88#[derive(Clone, Copy, Debug, Eq, PartialEq)]
89pub enum WgpuBackend {
90 BrowserWebGpu,
91 Gl,
92 Native(wgpu::Backend),
93}
94
95#[cfg(target_family = "wasm")]
96#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
97pub enum WebBackendPreference {
98 #[default]
99 Auto,
100 WebGpu,
101 WebGl,
102}
103
104#[cfg(target_family = "wasm")]
105pub struct PreparedWebGraphics {
106 pub context: WgpuContext,
107 pub surface: wgpu::Surface<'static>,
108}
109
110#[cfg(target_family = "wasm")]
115#[derive(Debug)]
116struct WebDisplaySource;
117
118#[cfg(target_family = "wasm")]
119impl raw_window_handle::HasDisplayHandle for WebDisplaySource {
120 fn display_handle(
121 &self,
122 ) -> Result<raw_window_handle::DisplayHandle<'_>, raw_window_handle::HandleError> {
123 Ok(raw_window_handle::DisplayHandle::web())
124 }
125}
126
127#[derive(Clone, Copy)]
128pub struct CompositorGpuHint {
129 pub vendor_id: u32,
130 pub device_id: u32,
131}
132
133impl WgpuContext {
134 #[cfg(not(target_family = "wasm"))]
135 pub fn new(
136 instance: wgpu::Instance,
137 surface: &wgpu::Surface<'_>,
138 compositor_gpu: Option<CompositorGpuHint>,
139 ) -> anyhow::Result<Self> {
140 Self::new_with_options(instance, surface, compositor_gpu, false)
141 }
142
143 #[cfg(not(target_family = "wasm"))]
144 pub fn new_rejecting_software(
145 instance: wgpu::Instance,
146 surface: &wgpu::Surface<'_>,
147 compositor_gpu: Option<CompositorGpuHint>,
148 ) -> anyhow::Result<Self> {
149 Self::new_with_options(instance, surface, compositor_gpu, true)
150 }
151
152 #[cfg(not(target_family = "wasm"))]
153 fn new_with_options(
154 instance: wgpu::Instance,
155 surface: &wgpu::Surface<'_>,
156 compositor_gpu: Option<CompositorGpuHint>,
157 reject_software: bool,
158 ) -> anyhow::Result<Self> {
159 let device_id_filter = Self::device_id_filter();
160
161 let (adapter, device, queue, dual_source_blending, color_texture_format) =
164 gpui::block_on(Self::select_adapter_and_device(
165 &instance,
166 device_id_filter,
167 surface,
168 compositor_gpu.as_ref(),
169 reject_software,
170 ))?;
171
172 Ok(Self::from_native_adapter_and_device(
173 instance,
174 adapter,
175 device,
176 queue,
177 dual_source_blending,
178 color_texture_format,
179 ))
180 }
181
182 #[cfg(all(
183 not(target_family = "wasm"),
184 any(test, feature = "bench-support", feature = "test-support")
185 ))]
186 pub(crate) fn new_headless() -> anyhow::Result<(Self, wgpu::TextureFormat)> {
187 static HEADLESS: Mutex<Option<(WgpuContext, wgpu::TextureFormat)>> = Mutex::new(None);
193 let mut headless = HEADLESS.lock();
194 if let Some((context, format)) = headless.as_ref()
195 && !context.device_lost()
196 {
197 return Ok((context.clone(), *format));
198 }
199 let created = Self::create_headless()?;
200 *headless = Some(created.clone());
201 Ok(created)
202 }
203
204 #[cfg(all(
205 not(target_family = "wasm"),
206 any(test, feature = "bench-support", feature = "test-support")
207 ))]
208 fn create_headless() -> anyhow::Result<(Self, wgpu::TextureFormat)> {
209 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
211 backends: wgpu::Backends::from_env().unwrap_or_else(|| {
212 wgpu::Backends::VULKAN
213 | wgpu::Backends::GL
214 | if cfg!(target_os = "macos") {
215 wgpu::Backends::METAL
216 } else if cfg!(target_os = "windows") {
217 wgpu::Backends::DX12
218 } else {
219 wgpu::Backends::empty()
220 }
221 }),
222 flags: wgpu::InstanceFlags::default().with_env(),
223 backend_options: wgpu::BackendOptions::default(),
224 memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
225 display: None,
226 });
227 let device_id_filter = Self::device_id_filter();
228 let (adapter, device, queue, dual_source_blending, color_texture_format, target_format) =
229 gpui::block_on(async {
230 let mut adapters = instance.enumerate_adapters(wgpu::Backends::all()).await;
231 Self::sort_adapters(&mut adapters, device_id_filter, None);
232
233 for adapter in adapters {
234 let adapter_info = adapter.get_info();
235 let Some(target_format) = Self::headless_target_format(&adapter) else {
236 log::warn!(
237 "Adapter {:?} has no supported headless render target format",
238 adapter_info.name
239 );
240 continue;
241 };
242
243 match Self::create_device(&adapter).await {
244 Ok((device, queue, dual_source_blending, color_texture_format)) => {
245 #[cfg(feature = "bench-support")]
246 if adapter_info.device_type == wgpu::DeviceType::Cpu {
247 log::error!(
248 "Headless renderer selected software adapter {:?}; \
249 benchmark results measure CPU software rendering, not hardware GPU rendering",
250 adapter_info.name
251 );
252 }
253 return Ok((
254 adapter,
255 device,
256 queue,
257 dual_source_blending,
258 color_texture_format,
259 target_format,
260 ));
261 }
262 Err(error) => {
263 log::warn!(
264 "Failed to create a headless device for adapter {:?}: {error:#}",
265 adapter_info.name
266 );
267 }
268 }
269 }
270
271 anyhow::bail!("No usable headless GPU adapter found")
272 })?;
273
274 Ok((
275 Self::from_native_adapter_and_device(
276 instance,
277 adapter,
278 device,
279 queue,
280 dual_source_blending,
281 color_texture_format,
282 ),
283 target_format,
284 ))
285 }
286
287 #[cfg(all(
288 not(target_family = "wasm"),
289 any(test, feature = "bench-support", feature = "test-support")
290 ))]
291 fn headless_target_format(adapter: &wgpu::Adapter) -> Option<wgpu::TextureFormat> {
295 let required_usages =
296 wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC;
297 [
298 wgpu::TextureFormat::Rgba8Unorm,
299 wgpu::TextureFormat::Bgra8Unorm,
300 ]
301 .into_iter()
302 .find(|format| {
303 adapter
304 .get_texture_format_features(*format)
305 .allowed_usages
306 .contains(required_usages)
307 })
308 }
309
310 #[cfg(not(target_family = "wasm"))]
311 fn from_native_adapter_and_device(
312 instance: wgpu::Instance,
313 adapter: wgpu::Adapter,
314 device: wgpu::Device,
315 queue: wgpu::Queue,
316 dual_source_blending: bool,
317 color_texture_format: TextureFormat,
318 ) -> Self {
319 let errors = DeviceErrorState::install(&device);
320
321 log::info!(
322 "Selected GPU adapter: {:?} ({:?})",
323 adapter.get_info().name,
324 adapter.get_info().backend
325 );
326 let backend = WgpuBackend::Native(adapter.get_info().backend);
327
328 Self {
329 instance,
330 adapter,
331 device: Arc::new(device),
332 queue: Arc::new(queue),
333 backend,
334 dual_source_blending,
335 color_texture_format,
336 errors,
337 }
338 }
339
340 #[cfg(not(target_family = "wasm"))]
341 fn device_id_filter() -> Option<u32> {
342 match std::env::var("ZED_DEVICE_ID") {
343 Ok(value) => parse_pci_id(&value)
344 .context("Failed to parse device ID from `ZED_DEVICE_ID` environment variable")
345 .log_err(),
346 Err(std::env::VarError::NotPresent) => None,
347 error => {
348 error
349 .context("Failed to read value of `ZED_DEVICE_ID` environment variable")
350 .log_err();
351 None
352 }
353 }
354 }
355
356 #[cfg(target_family = "wasm")]
357 pub async fn new_web(
358 canvas: &web_sys::HtmlCanvasElement,
359 preference: WebBackendPreference,
360 ) -> anyhow::Result<PreparedWebGraphics> {
361 Self::new_web_with_backend(canvas, preference).await
362 }
363
364 #[cfg(target_family = "wasm")]
365 #[allow(clippy::arc_with_non_send_sync)]
366 async fn new_web_with_backend(
367 canvas: &web_sys::HtmlCanvasElement,
368 preference: WebBackendPreference,
369 ) -> anyhow::Result<PreparedWebGraphics> {
370 let backends = match preference {
371 WebBackendPreference::Auto => wgpu::Backends::BROWSER_WEBGPU | wgpu::Backends::GL,
372 WebBackendPreference::WebGpu => wgpu::Backends::BROWSER_WEBGPU,
373 WebBackendPreference::WebGl => wgpu::Backends::GL,
374 };
375 let descriptor = wgpu::InstanceDescriptor {
376 backends,
377 flags: wgpu::InstanceFlags::default(),
378 backend_options: wgpu::BackendOptions::default(),
379 memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
380 display: Some(Box::new(WebDisplaySource)),
381 };
382 let instance = if preference == WebBackendPreference::Auto {
383 wgpu::util::new_instance_with_webgpu_detection(descriptor).await
384 } else {
385 wgpu::Instance::new(descriptor)
386 };
387 let surface = instance
388 .create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone()))
389 .map_err(|error| {
390 anyhow::anyhow!("Failed to create browser graphics surface: {error}")
391 })?;
392
393 let adapter = instance
394 .request_adapter(&wgpu::RequestAdapterOptions {
395 power_preference: wgpu::PowerPreference::HighPerformance,
396 compatible_surface: Some(&surface),
397 force_fallback_adapter: false,
398 apply_limit_buckets: false,
399 })
400 .await
401 .map_err(|error| {
402 anyhow::anyhow!(
403 "Failed to request a {preference:?} adapter compatible with the canvas: {error}"
404 )
405 })?;
406 let adapter_info = adapter.get_info();
407 let backend = match adapter_info.backend {
408 wgpu::Backend::BrowserWebGpu => WgpuBackend::BrowserWebGpu,
409 wgpu::Backend::Gl => WgpuBackend::Gl,
410 backend => {
411 anyhow::bail!(
412 "Browser graphics initialization selected unexpected backend {backend:?}"
413 )
414 }
415 };
416
417 let (device, queue, dual_source_blending, color_texture_format) =
418 Self::create_device(&adapter).await?;
419 let errors = DeviceErrorState::install(&device);
420 log::info!(
421 "Browser graphics initialized: requested={preference:?}, selected={backend:?}, \
422 adapter={:?}, limits={:?}, dual_source_blending={dual_source_blending}",
423 adapter_info.name,
424 device.limits(),
425 );
426
427 let context = Self {
428 instance,
429 adapter,
430 device: Arc::new(device),
431 queue: Arc::new(queue),
432 backend,
433 dual_source_blending,
434 color_texture_format,
435 errors,
436 };
437 Ok(PreparedWebGraphics { context, surface })
438 }
439
440 async fn create_device(
441 adapter: &wgpu::Adapter,
442 ) -> anyhow::Result<(wgpu::Device, wgpu::Queue, bool, TextureFormat)> {
443 let dual_source_blending = adapter
444 .features()
445 .contains(wgpu::Features::DUAL_SOURCE_BLENDING);
446
447 let mut required_features = wgpu::Features::empty();
448 if dual_source_blending {
449 required_features |= wgpu::Features::DUAL_SOURCE_BLENDING;
450 } else {
451 log::warn!(
452 "Dual-source blending not available on this GPU. \
453 Subpixel text antialiasing will be disabled."
454 );
455 }
456
457 #[cfg(all(target_os = "linux", feature = "dma-buf"))]
458 if adapter.features().contains(wgpu::Features::VULKAN_EXTERNAL_MEMORY_DMA_BUF) {
459 required_features |= wgpu::Features::VULKAN_EXTERNAL_MEMORY_DMA_BUF;
460 }
461 let color_atlas_texture_format = Self::select_color_texture_format(adapter)?;
462 #[cfg(target_family = "wasm")]
463 let required_limits = if adapter.get_info().backend == wgpu::Backend::Gl {
464 wgpu::Limits::downlevel_webgl2_defaults()
465 .using_resolution(adapter.limits())
466 .using_alignment(adapter.limits())
467 } else {
468 wgpu::Limits::downlevel_defaults()
469 .using_resolution(adapter.limits())
470 .using_alignment(adapter.limits())
471 };
472 #[cfg(not(target_family = "wasm"))]
473 let required_limits = wgpu::Limits::downlevel_defaults()
474 .using_resolution(adapter.limits())
475 .using_alignment(adapter.limits());
476
477 let (device, queue) = adapter
478 .request_device(&wgpu::DeviceDescriptor {
479 label: Some("gpui_device"),
480 required_features,
481 required_limits,
482 memory_hints: wgpu::MemoryHints::MemoryUsage,
483 trace: wgpu::Trace::Off,
484 experimental_features: wgpu::ExperimentalFeatures::disabled(),
485 })
486 .await
487 .map_err(|e| anyhow::anyhow!("Failed to create wgpu device: {e}"))?;
488
489 Ok((
490 device,
491 queue,
492 dual_source_blending,
493 color_atlas_texture_format,
494 ))
495 }
496
497 #[cfg(not(target_family = "wasm"))]
498 pub fn instance(display: Option<Box<dyn wgpu::wgt::WgpuHasDisplayHandle>>) -> wgpu::Instance {
499 wgpu::Instance::new(wgpu::InstanceDescriptor {
500 backends: wgpu::Backends::VULKAN | wgpu::Backends::GL,
501 flags: wgpu::InstanceFlags::default(),
502 backend_options: wgpu::BackendOptions::default(),
503 memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
504 display,
505 })
506 }
507
508 pub fn check_compatible_with_surface(&self, surface: &wgpu::Surface<'_>) -> anyhow::Result<()> {
509 let caps = surface.get_capabilities(&self.adapter);
510 if caps.formats.is_empty() {
511 let info = self.adapter.get_info();
512 anyhow::bail!(
513 "Adapter {:?} (backend={:?}, device={:#06x}) is not compatible with the \
514 display surface for this window.",
515 info.name,
516 info.backend,
517 info.device,
518 );
519 }
520 Ok(())
521 }
522
523 #[cfg(not(target_family = "wasm"))]
529 async fn select_adapter_and_device(
530 instance: &wgpu::Instance,
531 device_id_filter: Option<u32>,
532 surface: &wgpu::Surface<'_>,
533 compositor_gpu: Option<&CompositorGpuHint>,
534 reject_software: bool,
535 ) -> anyhow::Result<(
536 wgpu::Adapter,
537 wgpu::Device,
538 wgpu::Queue,
539 bool,
540 TextureFormat,
541 )> {
542 let mut adapters: Vec<_> = instance.enumerate_adapters(wgpu::Backends::all()).await;
543
544 if adapters.is_empty() {
545 anyhow::bail!("No GPU adapters found");
546 }
547
548 if let Some(device_id) = device_id_filter {
549 log::info!("ZED_DEVICE_ID filter: {:#06x}", device_id);
550 }
551
552 Self::sort_adapters(&mut adapters, device_id_filter, compositor_gpu);
553
554 log::info!("Found {} GPU adapter(s):", adapters.len());
556 for adapter in &adapters {
557 let info = adapter.get_info();
558 log::info!(
559 " - {} (vendor={:#06x}, device={:#06x}, backend={:?}, type={:?})",
560 info.name,
561 info.vendor,
562 info.device,
563 info.backend,
564 info.device_type,
565 );
566 }
567
568 for adapter in adapters {
570 let info = adapter.get_info();
571
572 if reject_software && info.device_type == wgpu::DeviceType::Cpu {
573 log::info!(
574 "Skipping software renderer: {} ({:?})",
575 info.name,
576 info.backend
577 );
578 continue;
579 }
580
581 log::info!("Testing adapter: {} ({:?})...", info.name, info.backend);
582
583 match Self::try_adapter_with_surface(&adapter, surface).await {
584 Ok((device, queue, dual_source_blending, color_atlas_texture_format)) => {
585 log::info!(
586 "Selected GPU (passed configuration test): {} ({:?})",
587 info.name,
588 info.backend
589 );
590 return Ok((
591 adapter,
592 device,
593 queue,
594 dual_source_blending,
595 color_atlas_texture_format,
596 ));
597 }
598 Err(e) => {
599 log::info!(
600 " Adapter {} ({:?}) failed: {}, trying next...",
601 info.name,
602 info.backend,
603 e
604 );
605 }
606 }
607 }
608
609 anyhow::bail!("No GPU adapter found that can configure the display surface")
610 }
611
612 #[cfg(not(target_family = "wasm"))]
620 fn sort_adapters(
621 adapters: &mut [wgpu::Adapter],
622 device_id_filter: Option<u32>,
623 compositor_gpu: Option<&CompositorGpuHint>,
624 ) {
625 adapters.sort_by_key(|adapter| {
626 let info = adapter.get_info();
627 let device_known = info.device != 0;
630 let user_override: u8 = match device_id_filter {
631 Some(id) if device_known && info.device == id => 0,
632 _ => 1,
633 };
634 let compositor_match: u8 = match compositor_gpu {
635 Some(hint)
636 if device_known
637 && info.vendor == hint.vendor_id
638 && info.device == hint.device_id =>
639 {
640 0
641 }
642 _ => 1,
643 };
644 let type_priority: u8 = match info.device_type {
645 wgpu::DeviceType::DiscreteGpu => 0,
646 wgpu::DeviceType::IntegratedGpu => 1,
647 wgpu::DeviceType::Other => 2,
648 wgpu::DeviceType::VirtualGpu => 3,
649 wgpu::DeviceType::Cpu => 4,
650 };
651 let backend_priority: u8 = match info.backend {
652 wgpu::Backend::Vulkan | wgpu::Backend::Metal | wgpu::Backend::Dx12 => 0,
653 _ => 1,
654 };
655
656 (
657 user_override,
658 compositor_match,
659 type_priority,
660 backend_priority,
661 )
662 });
663 }
664
665 #[cfg(not(target_family = "wasm"))]
668 async fn try_adapter_with_surface(
669 adapter: &wgpu::Adapter,
670 surface: &wgpu::Surface<'_>,
671 ) -> anyhow::Result<(wgpu::Device, wgpu::Queue, bool, TextureFormat)> {
672 let caps = surface.get_capabilities(adapter);
673 if caps.formats.is_empty() {
674 anyhow::bail!("no compatible surface formats");
675 }
676 if caps.alpha_modes.is_empty() {
677 anyhow::bail!("no compatible alpha modes");
678 }
679
680 let (device, queue, dual_source_blending, color_atlas_texture_format) =
681 Self::create_device(adapter).await?;
682 let error_scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
683
684 let test_config = wgpu::SurfaceConfiguration {
685 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
686 format: caps.formats[0],
687 color_space: wgpu::SurfaceColorSpace::Auto,
688 width: 64,
689 height: 64,
690 present_mode: wgpu::PresentMode::Fifo,
691 desired_maximum_frame_latency: 2,
692 alpha_mode: caps.alpha_modes[0],
693 view_formats: vec![],
694 };
695
696 surface.configure(&device, &test_config);
697
698 let error = error_scope.pop().await;
699 if let Some(e) = error {
700 anyhow::bail!("surface configuration failed: {e}");
701 }
702
703 Ok((
704 device,
705 queue,
706 dual_source_blending,
707 color_atlas_texture_format,
708 ))
709 }
710
711 fn select_color_texture_format(adapter: &wgpu::Adapter) -> anyhow::Result<wgpu::TextureFormat> {
712 let required_usages = wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST;
713 let bgra_features = adapter.get_texture_format_features(wgpu::TextureFormat::Bgra8Unorm);
714 let rgba_features = adapter.get_texture_format_features(wgpu::TextureFormat::Rgba8Unorm);
715 #[cfg(target_family = "wasm")]
716 if adapter.get_info().backend == wgpu::Backend::Gl
717 && rgba_features.allowed_usages.contains(required_usages)
718 {
719 return Ok(wgpu::TextureFormat::Rgba8Unorm);
720 }
721 if bgra_features.allowed_usages.contains(required_usages) {
722 return Ok(wgpu::TextureFormat::Bgra8Unorm);
723 }
724 if rgba_features.allowed_usages.contains(required_usages) {
725 let info = adapter.get_info();
726 log::warn!(
727 "Adapter {} ({:?}) does not support Bgra8Unorm atlas textures with usages {:?}; \
728 falling back to Rgba8Unorm atlas textures.",
729 info.name,
730 info.backend,
731 required_usages,
732 );
733 return Ok(wgpu::TextureFormat::Rgba8Unorm);
734 }
735
736 let info = adapter.get_info();
737 Err(anyhow::anyhow!(
738 "Adapter {} ({:?}, device={:#06x}) does not support a usable color atlas texture \
739 format with usages {:?}. Bgra8Unorm allowed usages: {:?}; \
740 Rgba8Unorm allowed usages: {:?}.",
741 info.name,
742 info.backend,
743 info.device,
744 required_usages,
745 bgra_features.allowed_usages,
746 rgba_features.allowed_usages,
747 ))
748 }
749 pub fn backend(&self) -> WgpuBackend {
750 self.backend
751 }
752
753 pub fn uses_webgl_instance_data(&self) -> bool {
754 matches!(self.backend, WgpuBackend::Gl) && cfg!(target_family = "wasm")
755 }
756
757 pub fn supports_dual_source_blending(&self) -> bool {
758 self.dual_source_blending
759 }
760
761 pub fn color_texture_format(&self) -> wgpu::TextureFormat {
762 self.color_texture_format
763 }
764
765 pub fn device_lost(&self) -> bool {
768 self.errors.device_lost()
769 }
770
771 pub fn errors(&self) -> &Arc<DeviceErrorState> {
773 &self.errors
774 }
775}
776
777#[cfg(not(target_family = "wasm"))]
778fn parse_pci_id(id: &str) -> anyhow::Result<u32> {
779 let mut id = id.trim();
780
781 if id.starts_with("0x") || id.starts_with("0X") {
782 id = &id[2..];
783 }
784 let is_hex_string = id.chars().all(|c| c.is_ascii_hexdigit());
785 let is_4_chars = id.len() == 4;
786 anyhow::ensure!(
787 is_4_chars && is_hex_string,
788 "Expected a 4 digit PCI ID in hexadecimal format"
789 );
790
791 u32::from_str_radix(id, 16).context("parsing PCI ID as hex")
792}
793
794#[cfg(test)]
795mod tests {
796 use super::{DeviceErrorState, parse_pci_id};
797
798 #[test]
799 fn device_errors_are_observed_independently() {
800 let errors = DeviceErrorState::default();
801 let mut first = 0;
802 let mut second = 0;
803 assert_eq!(errors.observe_error(&mut first), None);
804 errors.record("first".into());
805 assert_eq!(errors.observe_error(&mut first).as_deref(), Some("first"));
806 assert_eq!(errors.observe_error(&mut first), None);
807 assert_eq!(errors.observe_error(&mut second).as_deref(), Some("first"));
808 assert_eq!(errors.observe_error(&mut second), None);
809
810 errors.record("second".into());
811 assert_eq!(errors.observe_error(&mut second).as_deref(), Some("second"));
812 assert_eq!(errors.observe_error(&mut first).as_deref(), Some("second"));
813 assert_eq!(errors.observe_error(&mut first), None);
814 assert_eq!(errors.observe_error(&mut second), None);
815 }
816
817 #[test]
818 fn device_errors_coalesce_and_new_observers_see_latest() {
819 let errors = DeviceErrorState::default();
820 errors.record("older".into());
821 errors.record("latest".into());
822 let mut generation = 0;
823 assert_eq!(
824 errors.observe_error(&mut generation).as_deref(),
825 Some("latest")
826 );
827 assert_eq!(errors.observe_error(&mut generation), None);
828
829 let replacement = DeviceErrorState::default();
830 replacement.record("replacement".into());
831 generation = 0;
832 assert_eq!(
833 replacement.observe_error(&mut generation).as_deref(),
834 Some("replacement")
835 );
836 assert_eq!(replacement.observe_error(&mut generation), None);
837 }
838
839 #[test]
840 fn test_parse_device_id() {
841 assert!(parse_pci_id("0xABCD").is_ok());
842 assert!(parse_pci_id("ABCD").is_ok());
843 assert!(parse_pci_id("abcd").is_ok());
844 assert!(parse_pci_id("1234").is_ok());
845 assert!(parse_pci_id("123").is_err());
846 assert_eq!(
847 parse_pci_id(&format!("{:x}", 0x1234)).unwrap(),
848 parse_pci_id(&format!("{:X}", 0x1234)).unwrap(),
849 );
850
851 assert_eq!(
852 parse_pci_id(&format!("{:#x}", 0x1234)).unwrap(),
853 parse_pci_id(&format!("{:#X}", 0x1234)).unwrap(),
854 );
855 }
856}