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