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