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 gpui::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")
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")
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 gpui::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")
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 #[cfg(target_family = "wasm")]
437 let required_limits = if adapter.get_info().backend == wgpu::Backend::Gl {
438 wgpu::Limits::downlevel_webgl2_defaults()
439 .using_resolution(adapter.limits())
440 .using_alignment(adapter.limits())
441 } else {
442 wgpu::Limits::downlevel_defaults()
443 .using_resolution(adapter.limits())
444 .using_alignment(adapter.limits())
445 };
446 #[cfg(not(target_family = "wasm"))]
447 let required_limits = wgpu::Limits::downlevel_defaults()
448 .using_resolution(adapter.limits())
449 .using_alignment(adapter.limits());
450
451 let (device, queue) = adapter
452 .request_device(&wgpu::DeviceDescriptor {
453 label: Some("gpui_device"),
454 required_features,
455 required_limits,
456 memory_hints: wgpu::MemoryHints::MemoryUsage,
457 trace: wgpu::Trace::Off,
458 experimental_features: wgpu::ExperimentalFeatures::disabled(),
459 })
460 .await
461 .map_err(|e| anyhow::anyhow!("Failed to create wgpu device: {e}"))?;
462
463 Ok((
464 device,
465 queue,
466 dual_source_blending,
467 color_atlas_texture_format,
468 ))
469 }
470
471 #[cfg(not(target_family = "wasm"))]
472 pub fn instance(display: Option<Box<dyn wgpu::wgt::WgpuHasDisplayHandle>>) -> wgpu::Instance {
473 wgpu::Instance::new(wgpu::InstanceDescriptor {
474 backends: wgpu::Backends::VULKAN | wgpu::Backends::GL,
475 flags: wgpu::InstanceFlags::default(),
476 backend_options: wgpu::BackendOptions::default(),
477 memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
478 display,
479 })
480 }
481
482 pub fn check_compatible_with_surface(&self, surface: &wgpu::Surface<'_>) -> anyhow::Result<()> {
483 let caps = surface.get_capabilities(&self.adapter);
484 if caps.formats.is_empty() {
485 let info = self.adapter.get_info();
486 anyhow::bail!(
487 "Adapter {:?} (backend={:?}, device={:#06x}) is not compatible with the \
488 display surface for this window.",
489 info.name,
490 info.backend,
491 info.device,
492 );
493 }
494 Ok(())
495 }
496
497 #[cfg(not(target_family = "wasm"))]
503 async fn select_adapter_and_device(
504 instance: &wgpu::Instance,
505 device_id_filter: Option<u32>,
506 surface: &wgpu::Surface<'_>,
507 compositor_gpu: Option<&CompositorGpuHint>,
508 reject_software: bool,
509 ) -> anyhow::Result<(
510 wgpu::Adapter,
511 wgpu::Device,
512 wgpu::Queue,
513 bool,
514 TextureFormat,
515 )> {
516 let mut adapters: Vec<_> = instance.enumerate_adapters(wgpu::Backends::all()).await;
517
518 if adapters.is_empty() {
519 anyhow::bail!("No GPU adapters found");
520 }
521
522 if let Some(device_id) = device_id_filter {
523 log::info!("ZED_DEVICE_ID filter: {:#06x}", device_id);
524 }
525
526 Self::sort_adapters(&mut adapters, device_id_filter, compositor_gpu);
527
528 log::info!("Found {} GPU adapter(s):", adapters.len());
530 for adapter in &adapters {
531 let info = adapter.get_info();
532 log::info!(
533 " - {} (vendor={:#06x}, device={:#06x}, backend={:?}, type={:?})",
534 info.name,
535 info.vendor,
536 info.device,
537 info.backend,
538 info.device_type,
539 );
540 }
541
542 for adapter in adapters {
544 let info = adapter.get_info();
545
546 if reject_software && info.device_type == wgpu::DeviceType::Cpu {
547 log::info!(
548 "Skipping software renderer: {} ({:?})",
549 info.name,
550 info.backend
551 );
552 continue;
553 }
554
555 log::info!("Testing adapter: {} ({:?})...", info.name, info.backend);
556
557 match Self::try_adapter_with_surface(&adapter, surface).await {
558 Ok((device, queue, dual_source_blending, color_atlas_texture_format)) => {
559 log::info!(
560 "Selected GPU (passed configuration test): {} ({:?})",
561 info.name,
562 info.backend
563 );
564 return Ok((
565 adapter,
566 device,
567 queue,
568 dual_source_blending,
569 color_atlas_texture_format,
570 ));
571 }
572 Err(e) => {
573 log::info!(
574 " Adapter {} ({:?}) failed: {}, trying next...",
575 info.name,
576 info.backend,
577 e
578 );
579 }
580 }
581 }
582
583 anyhow::bail!("No GPU adapter found that can configure the display surface")
584 }
585
586 #[cfg(not(target_family = "wasm"))]
594 fn sort_adapters(
595 adapters: &mut [wgpu::Adapter],
596 device_id_filter: Option<u32>,
597 compositor_gpu: Option<&CompositorGpuHint>,
598 ) {
599 adapters.sort_by_key(|adapter| {
600 let info = adapter.get_info();
601 let device_known = info.device != 0;
604 let user_override: u8 = match device_id_filter {
605 Some(id) if device_known && info.device == id => 0,
606 _ => 1,
607 };
608 let compositor_match: u8 = match compositor_gpu {
609 Some(hint)
610 if device_known
611 && info.vendor == hint.vendor_id
612 && info.device == hint.device_id =>
613 {
614 0
615 }
616 _ => 1,
617 };
618 let type_priority: u8 = match info.device_type {
619 wgpu::DeviceType::DiscreteGpu => 0,
620 wgpu::DeviceType::IntegratedGpu => 1,
621 wgpu::DeviceType::Other => 2,
622 wgpu::DeviceType::VirtualGpu => 3,
623 wgpu::DeviceType::Cpu => 4,
624 };
625 let backend_priority: u8 = match info.backend {
626 wgpu::Backend::Vulkan | wgpu::Backend::Metal | wgpu::Backend::Dx12 => 0,
627 _ => 1,
628 };
629
630 (
631 user_override,
632 compositor_match,
633 type_priority,
634 backend_priority,
635 )
636 });
637 }
638
639 #[cfg(not(target_family = "wasm"))]
642 async fn try_adapter_with_surface(
643 adapter: &wgpu::Adapter,
644 surface: &wgpu::Surface<'_>,
645 ) -> anyhow::Result<(wgpu::Device, wgpu::Queue, bool, TextureFormat)> {
646 let caps = surface.get_capabilities(adapter);
647 if caps.formats.is_empty() {
648 anyhow::bail!("no compatible surface formats");
649 }
650 if caps.alpha_modes.is_empty() {
651 anyhow::bail!("no compatible alpha modes");
652 }
653
654 let (device, queue, dual_source_blending, color_atlas_texture_format) =
655 Self::create_device(adapter).await?;
656 let error_scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
657
658 let test_config = wgpu::SurfaceConfiguration {
659 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
660 format: caps.formats[0],
661 width: 64,
662 height: 64,
663 present_mode: wgpu::PresentMode::Fifo,
664 desired_maximum_frame_latency: 2,
665 alpha_mode: caps.alpha_modes[0],
666 view_formats: vec![],
667 };
668
669 surface.configure(&device, &test_config);
670
671 let error = error_scope.pop().await;
672 if let Some(e) = error {
673 anyhow::bail!("surface configuration failed: {e}");
674 }
675
676 Ok((
677 device,
678 queue,
679 dual_source_blending,
680 color_atlas_texture_format,
681 ))
682 }
683
684 fn select_color_texture_format(adapter: &wgpu::Adapter) -> anyhow::Result<wgpu::TextureFormat> {
685 let required_usages = wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST;
686 let bgra_features = adapter.get_texture_format_features(wgpu::TextureFormat::Bgra8Unorm);
687 let rgba_features = adapter.get_texture_format_features(wgpu::TextureFormat::Rgba8Unorm);
688 #[cfg(target_family = "wasm")]
689 if adapter.get_info().backend == wgpu::Backend::Gl
690 && rgba_features.allowed_usages.contains(required_usages)
691 {
692 return Ok(wgpu::TextureFormat::Rgba8Unorm);
693 }
694 if bgra_features.allowed_usages.contains(required_usages) {
695 return Ok(wgpu::TextureFormat::Bgra8Unorm);
696 }
697 if rgba_features.allowed_usages.contains(required_usages) {
698 let info = adapter.get_info();
699 log::warn!(
700 "Adapter {} ({:?}) does not support Bgra8Unorm atlas textures with usages {:?}; \
701 falling back to Rgba8Unorm atlas textures.",
702 info.name,
703 info.backend,
704 required_usages,
705 );
706 return Ok(wgpu::TextureFormat::Rgba8Unorm);
707 }
708
709 let info = adapter.get_info();
710 Err(anyhow::anyhow!(
711 "Adapter {} ({:?}, device={:#06x}) does not support a usable color atlas texture \
712 format with usages {:?}. Bgra8Unorm allowed usages: {:?}; \
713 Rgba8Unorm allowed usages: {:?}.",
714 info.name,
715 info.backend,
716 info.device,
717 required_usages,
718 bgra_features.allowed_usages,
719 rgba_features.allowed_usages,
720 ))
721 }
722 pub fn backend(&self) -> WgpuBackend {
723 self.backend
724 }
725
726 pub fn uses_webgl_instance_data(&self) -> bool {
727 matches!(self.backend, WgpuBackend::Gl) && cfg!(target_family = "wasm")
728 }
729
730 pub fn supports_dual_source_blending(&self) -> bool {
731 self.dual_source_blending
732 }
733
734 pub fn color_texture_format(&self) -> wgpu::TextureFormat {
735 self.color_texture_format
736 }
737
738 pub fn device_lost(&self) -> bool {
741 self.errors.device_lost()
742 }
743
744 pub fn errors(&self) -> &Arc<DeviceErrorState> {
746 &self.errors
747 }
748}
749
750#[cfg(not(target_family = "wasm"))]
751fn parse_pci_id(id: &str) -> anyhow::Result<u32> {
752 let mut id = id.trim();
753
754 if id.starts_with("0x") || id.starts_with("0X") {
755 id = &id[2..];
756 }
757 let is_hex_string = id.chars().all(|c| c.is_ascii_hexdigit());
758 let is_4_chars = id.len() == 4;
759 anyhow::ensure!(
760 is_4_chars && is_hex_string,
761 "Expected a 4 digit PCI ID in hexadecimal format"
762 );
763
764 u32::from_str_radix(id, 16).context("parsing PCI ID as hex")
765}
766
767#[cfg(test)]
768mod tests {
769 use super::{DeviceErrorState, parse_pci_id};
770
771 #[test]
772 fn device_errors_are_observed_independently() {
773 let errors = DeviceErrorState::default();
774 let mut first = 0;
775 let mut second = 0;
776 assert_eq!(errors.observe_error(&mut first), None);
777 errors.record("first".into());
778 assert_eq!(errors.observe_error(&mut first).as_deref(), Some("first"));
779 assert_eq!(errors.observe_error(&mut first), None);
780 assert_eq!(errors.observe_error(&mut second).as_deref(), Some("first"));
781 assert_eq!(errors.observe_error(&mut second), None);
782
783 errors.record("second".into());
784 assert_eq!(errors.observe_error(&mut second).as_deref(), Some("second"));
785 assert_eq!(errors.observe_error(&mut first).as_deref(), Some("second"));
786 assert_eq!(errors.observe_error(&mut first), None);
787 assert_eq!(errors.observe_error(&mut second), None);
788 }
789
790 #[test]
791 fn device_errors_coalesce_and_new_observers_see_latest() {
792 let errors = DeviceErrorState::default();
793 errors.record("older".into());
794 errors.record("latest".into());
795 let mut generation = 0;
796 assert_eq!(
797 errors.observe_error(&mut generation).as_deref(),
798 Some("latest")
799 );
800 assert_eq!(errors.observe_error(&mut generation), None);
801
802 let replacement = DeviceErrorState::default();
803 replacement.record("replacement".into());
804 generation = 0;
805 assert_eq!(
806 replacement.observe_error(&mut generation).as_deref(),
807 Some("replacement")
808 );
809 assert_eq!(replacement.observe_error(&mut generation), None);
810 }
811
812 #[test]
813 fn test_parse_device_id() {
814 assert!(parse_pci_id("0xABCD").is_ok());
815 assert!(parse_pci_id("ABCD").is_ok());
816 assert!(parse_pci_id("abcd").is_ok());
817 assert!(parse_pci_id("1234").is_ok());
818 assert!(parse_pci_id("123").is_err());
819 assert_eq!(
820 parse_pci_id(&format!("{:x}", 0x1234)).unwrap(),
821 parse_pci_id(&format!("{:X}", 0x1234)).unwrap(),
822 );
823
824 assert_eq!(
825 parse_pci_id(&format!("{:#x}", 0x1234)).unwrap(),
826 parse_pci_id(&format!("{:#X}", 0x1234)).unwrap(),
827 );
828 }
829}