Skip to main content

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}