gpuviewer_core/backend.rs
1//! The vendor backend abstraction — nvtop's `struct gpu_vendor` vtable, translated to Rust.
2//!
3//! Contract:
4//! - `init()` failing is normal (driver/library absent) — the registry drops the backend
5//! silently and the rest of the tool keeps working.
6//! - Per-metric absence is `None` in the sample, never an `Err`. `Err` from a refresh means
7//! "this refresh produced nothing usable" (device fell off the bus, etc.) and is survivable.
8
9use crate::model::{DeviceId, DynamicSample, ProcessSample, StaticInfo};
10
11#[derive(Debug)]
12pub enum BackendError {
13 /// Backend or device unavailable (missing library/driver/permission). Normal outcome.
14 Unavailable(String),
15 /// Unknown device id passed in.
16 DeviceNotFound(DeviceId),
17}
18
19impl std::fmt::Display for BackendError {
20 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21 match self {
22 BackendError::Unavailable(why) => write!(f, "unavailable: {why}"),
23 BackendError::DeviceNotFound(id) => write!(f, "device not found: {id}"),
24 }
25 }
26}
27
28impl std::error::Error for BackendError {}
29
30pub trait GpuBackend: Send {
31 fn name(&self) -> &'static str;
32
33 /// Devices this backend can see. Called once after init.
34 fn devices(&mut self) -> Vec<DeviceId>;
35
36 /// Queried once per device.
37 fn static_info(&mut self, dev: &DeviceId) -> Result<StaticInfo, BackendError>;
38
39 /// Called every tick.
40 fn refresh_dynamic(&mut self, dev: &DeviceId) -> Result<DynamicSample, BackendError>;
41
42 /// Called every tick, separately from dynamic info (some sources differ).
43 fn refresh_processes(&mut self, dev: &DeviceId) -> Result<Vec<ProcessSample>, BackendError>;
44}
45
46/// Explicit registry — no constructor/inventory magic. Each backend's failed init is
47/// logged and skipped.
48///
49/// Registry order is load-bearing (design cross-platform.md §3.7/§9):
50/// **nvidia → amd → intel → wddm → apple**. The collector dedupes devices across
51/// backends by normalized PCI address, first backend wins — so NVML (the richer source)
52/// must register before wddm to claim NVIDIA boards, and AMD/Intel adapters — plus
53/// NVIDIA boards on driverless/broken-NVML machines — fall through to wddm. No new
54/// dedupe mechanism here: synthetic non-PCI ids (`wddm:…`, `apple:…`, `mock:…`) never
55/// dedupe by design — a double listing is visible and honest, a wrong merge is not.
56///
57/// `force_mock` returns ONLY the mock backend — its purpose is deterministic CI/demo
58/// output, so real devices must not leak in. Otherwise the mock is the fallback when no
59/// real backend initialized (so the TUI always has something to show, clearly labeled).
60pub fn all_backends(force_mock: bool) -> Vec<Box<dyn GpuBackend>> {
61 if force_mock {
62 return vec![Box::new(crate::mock::MockBackend::new())];
63 }
64
65 let mut backends: Vec<Box<dyn GpuBackend>> = Vec::new();
66
67 #[cfg(all(feature = "nvidia", any(target_os = "linux", target_os = "windows")))]
68 match crate::nvidia::NvidiaBackend::init() {
69 Ok(b) => backends.push(Box::new(b)),
70 Err(e) => eprintln!("gpuviewer: nvidia backend skipped: {e}"),
71 }
72
73 #[cfg(target_os = "linux")]
74 match crate::amd::AmdBackend::init() {
75 Ok(b) => backends.push(Box::new(b)),
76 Err(e) => eprintln!("gpuviewer: amd backend skipped: {e}"),
77 }
78
79 #[cfg(target_os = "linux")]
80 match crate::intel::IntelBackend::init() {
81 Ok(b) => backends.push(Box::new(b)),
82 Err(e) => eprintln!("gpuviewer: intel backend skipped: {e}"),
83 }
84
85 // wddm registers LAST among Windows backends (§3.7): the collector's first-wins PCI
86 // dedupe lets NVML claim NVIDIA boards first; AMD/Intel adapters — and NVIDIA boards
87 // on driverless/broken-NVML machines — fall through to wddm. Synthetic `wddm:` ids
88 // never dedupe by design.
89 #[cfg(all(feature = "wddm", target_os = "windows"))]
90 match crate::wddm::WddmBackend::init() {
91 Ok(b) => backends.push(Box::new(b)),
92 Err(e) => eprintln!("gpuviewer: wddm backend skipped: {e}"),
93 }
94
95 // Registry order per design §9: nvidia → amd → intel → (wddm) → apple.
96 #[cfg(all(feature = "apple", target_os = "macos"))]
97 match crate::apple::AppleBackend::init() {
98 Ok(b) => backends.push(Box::new(b)),
99 Err(e) => eprintln!("gpuviewer: apple backend skipped: {e}"),
100 }
101
102 if backends.is_empty() {
103 backends.push(Box::new(crate::mock::MockBackend::new()));
104 }
105 backends
106}