mod amd;
mod apple;
mod intel;
#[cfg(target_os = "linux")]
mod linux;
mod mock;
mod nvidia;
mod replay;
mod windows;
use anyhow::Result;
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct GpuSnapshot {
pub name: String,
pub device_id: Option<String>,
pub integrated: bool,
pub utilization_pct: Option<f64>,
pub mem_util_pct: Option<f64>,
pub video_util_pct: Option<f64>,
pub enc_util_pct: Option<f64>,
pub dec_util_pct: Option<f64>,
pub throttle: Option<String>,
pub vram_used_bytes: Option<u64>,
pub vram_total_bytes: Option<u64>,
pub temperature_c: Option<f64>,
pub temp_junction_c: Option<f64>,
pub temp_mem_c: Option<f64>,
pub power_w: Option<f64>,
pub power_limit_w: Option<f64>,
pub fan_pct: Option<f64>,
pub fan_rpm: Option<u64>,
pub clock_mhz: Option<u64>,
pub mem_clock_mhz: Option<u64>,
pub pcie_gen: Option<u8>,
pub pcie_width: Option<u32>,
pub pcie_max_gen: Option<u8>,
pub pcie_max_width: Option<u32>,
pub pcie_rx_kbs: Option<u64>,
pub pcie_tx_kbs: Option<u64>,
pub gtt_used_bytes: Option<u64>,
pub gtt_total_bytes: Option<u64>,
pub volt_mv: Option<u64>,
pub perf_level: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MemReadout {
pub used: Option<u64>,
pub total: Option<u64>,
pub shared: bool,
}
impl MemReadout {
pub fn pct(&self) -> Option<f64> {
let (used, total) = (self.used?, self.total?);
(total > 0).then(|| used as f64 / total as f64 * 100.0)
}
}
impl GpuSnapshot {
pub fn mem_primary(&self) -> MemReadout {
if self.has_device_pool() {
return MemReadout {
used: self.vram_used_bytes,
total: self.vram_total_bytes,
shared: self.integrated && !self.has_system_pool(),
};
}
MemReadout {
used: self.gtt_used_bytes,
total: self.gtt_total_bytes,
shared: self.has_system_pool(),
}
}
fn has_device_pool(&self) -> bool {
self.vram_used_bytes.is_some() || self.vram_total_bytes.is_some()
}
fn has_system_pool(&self) -> bool {
self.gtt_used_bytes.is_some() || self.gtt_total_bytes.is_some()
}
pub fn mem_secondary(&self) -> Option<MemReadout> {
(self.has_device_pool() && self.has_system_pool()).then_some(MemReadout {
used: self.gtt_used_bytes,
total: self.gtt_total_bytes,
shared: self.integrated,
})
}
pub fn mem_pct(&self) -> Option<f64> {
self.mem_primary().pct()
}
}
pub fn join_throttle(parts: &[&str]) -> Option<String> {
(!parts.is_empty()).then(|| parts.join("+"))
}
pub fn clamp_pct(v: f64) -> f64 {
v.clamp(0.0, 100.0)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
pub enum ProcKind {
Graphics,
#[default]
Compute,
}
impl ProcKind {
pub fn label(&self) -> &'static str {
match self {
ProcKind::Graphics => "Graphic",
ProcKind::Compute => "Compute",
}
}
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct GpuProcess {
pub pid: u32,
pub gpu_index: usize,
pub kind: ProcKind,
pub gpu_util_pct: Option<f64>,
pub gpu_mem_bytes: Option<u64>,
pub user: Option<String>,
pub command: Option<String>,
pub cpu_pct: Option<f32>,
pub host_mem_bytes: Option<u64>,
pub container: Option<String>,
}
pub trait GpuBackend {
fn name(&self) -> &'static str;
fn poll(&mut self) -> Result<Vec<GpuSnapshot>>;
fn processes(&mut self) -> Vec<GpuProcess> {
Vec::new()
}
fn driver_info(&self) -> Option<String> {
None
}
fn can_signal(&self) -> bool {
true
}
}
pub fn sweep_on_poll_thread() {
#[cfg(target_os = "linux")]
linux::ProcScanner::shared().set_synchronous(true);
}
const NO_BACKEND: &str = "no supported GPU backend found (run with --mock to demo the UI)";
const PCI_VENDOR_NVIDIA: u16 = 0x10de;
const PCI_VENDOR_AMD: u16 = 0x1002;
const PCI_VENDOR_INTEL: u16 = 0x8086;
struct Child {
backend: Box<dyn GpuBackend>,
slots: usize,
names: Vec<String>,
ids: Vec<Option<String>>,
}
struct CompositeBackend {
children: Vec<Child>,
}
impl CompositeBackend {
fn new(backends: Vec<Box<dyn GpuBackend>>) -> Self {
Self {
children: backends
.into_iter()
.map(|backend| Child {
backend,
slots: 0,
names: Vec::new(),
ids: Vec::new(),
})
.collect(),
}
}
}
impl GpuBackend for CompositeBackend {
fn name(&self) -> &'static str {
"multi"
}
fn poll(&mut self) -> Result<Vec<GpuSnapshot>> {
let mut out = Vec::new();
let mut errors = Vec::new();
for (ci, c) in self.children.iter_mut().enumerate() {
let mut snaps = match c.backend.poll() {
Ok(s) => s,
Err(e) => {
errors.push(format!("{}: {e:#}", c.backend.name()));
Vec::new()
}
};
for s in &mut snaps {
if let Some(id) = s.device_id.take() {
s.device_id = Some(format!("{}#{ci}:{id}", c.backend.name()));
}
}
c.slots = c.slots.max(snaps.len());
c.names.resize(c.slots, String::new());
c.ids.resize(c.slots, None);
for (i, snap) in snaps.iter().enumerate() {
snap.name.clone_into(&mut c.names[i]);
c.ids[i].clone_from(&snap.device_id);
}
for i in snaps.len()..c.slots {
if c.ids[i].is_some() && c.ids[..snaps.len()].contains(&c.ids[i]) {
c.ids[i] = None;
c.names[i].clear();
}
let label = match c.names.get(i) {
Some(n) if !n.is_empty() => n.clone(),
_ => format!("{} GPU {i}", c.backend.name()),
};
snaps.push(GpuSnapshot {
name: format!("{label} (unavailable)"),
device_id: c.ids.get(i).cloned().flatten(),
..Default::default()
});
}
out.append(&mut snaps);
}
if !errors.is_empty() && (out.is_empty() || errors.len() == self.children.len()) {
anyhow::bail!("{}", errors.join("; "));
}
Ok(out)
}
fn processes(&mut self) -> Vec<GpuProcess> {
let mut out = Vec::new();
let mut base = 0;
for c in &mut self.children {
for mut p in c.backend.processes() {
if p.gpu_index >= c.slots {
continue;
}
p.gpu_index += base;
out.push(p);
}
base += c.slots;
}
out
}
fn driver_info(&self) -> Option<String> {
let parts: Vec<String> = self
.children
.iter()
.map(|c| {
let name = c.backend.name();
match c.backend.driver_info() {
Some(d) if d.contains(name) => d,
Some(d) => format!("{name} {d}"),
None => name.to_string(),
}
})
.collect();
(!parts.is_empty()).then(|| parts.join(" · "))
}
fn can_signal(&self) -> bool {
self.children.iter().all(|c| c.backend.can_signal())
}
}
fn compose(mut found: Vec<Box<dyn GpuBackend>>) -> Result<Box<dyn GpuBackend>> {
if found.len() <= 1 {
return found.pop().ok_or_else(|| anyhow::anyhow!(NO_BACKEND));
}
Ok(Box::new(CompositeBackend::new(found)))
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum BackendSource {
Live,
Mock(usize),
Replay(std::path::PathBuf),
}
impl BackendSource {
pub fn from_cli(mock: Option<usize>, replay: Option<std::path::PathBuf>) -> Self {
match (mock, replay) {
(_, Some(path)) => Self::Replay(path),
(Some(n), None) => Self::Mock(n),
(None, None) => Self::Live,
}
}
pub fn detect(&self) -> Result<Box<dyn GpuBackend>> {
match self {
Self::Live => detect(None, None),
Self::Mock(n) => detect(Some(*n), None),
Self::Replay(path) => detect(None, Some(path)),
}
}
}
pub fn detect(
mock: Option<usize>,
replay: Option<&std::path::Path>,
) -> Result<Box<dyn GpuBackend>> {
if let Some(path) = replay {
return replay::load(path);
}
if let Some(n) = mock {
return Ok(Box::new(mock::MockBackend::new(n.clamp(1, 16))));
}
let found: Vec<(Box<dyn GpuBackend>, &'static [u16])> = [
(nvidia::probe(), &[PCI_VENDOR_NVIDIA][..]),
(amd::probe(), &[PCI_VENDOR_AMD][..]),
(intel::probe(), &[PCI_VENDOR_INTEL][..]),
(apple::probe(), &[][..]),
]
.into_iter()
.filter_map(|(backend, vendors)| backend.map(|b| (b, vendors)))
.collect();
compose_with_generic(found, windows::probe)
}
fn compose_with_generic(
found: Vec<(Box<dyn GpuBackend>, &'static [u16])>,
generic: impl FnOnce(&[u16]) -> Option<Box<dyn GpuBackend>>,
) -> Result<Box<dyn GpuBackend>> {
let claimed: Vec<u16> = found.iter().flat_map(|(_, v)| v.iter().copied()).collect();
let mut backends: Vec<Box<dyn GpuBackend>> = found.into_iter().map(|(b, _)| b).collect();
backends.extend(generic(&claimed));
compose(backends)
}
#[cfg(test)]
mod mem_pool_tests {
use super::*;
#[test]
fn a_discrete_card_meters_its_own_vram_and_keeps_gtt_beside_it() {
let g = GpuSnapshot {
vram_used_bytes: Some(8 << 30),
vram_total_bytes: Some(24 << 30),
gtt_used_bytes: Some(3 << 30),
gtt_total_bytes: Some(16 << 30),
..Default::default()
};
let m = g.mem_primary();
assert_eq!((m.used, m.total), (Some(8 << 30), Some(24 << 30)));
assert!(!m.shared, "a card's own VRAM is not system RAM");
let s = g.mem_secondary().expect("GTT is a pool of its own here");
assert_eq!((s.used, s.total), (Some(3 << 30), Some(16 << 30)));
assert!(!s.shared, "GTT on a dGPU is mapped host RAM, not unified");
assert_eq!(g.mem_pct(), Some(1.0 / 3.0 * 100.0));
}
#[test]
fn an_igpu_with_no_local_pool_meters_the_system_one() {
let g = GpuSnapshot {
integrated: true,
gtt_used_bytes: Some(734 << 20),
gtt_total_bytes: Some(15 << 30),
..Default::default()
};
let m = g.mem_primary();
assert_eq!((m.used, m.total), (Some(734 << 20), Some(15 << 30)));
assert!(m.shared);
assert_eq!(g.mem_secondary(), None, "one pool, so nothing beside it");
assert!(
g.mem_pct().is_some(),
"the memory graph must not stay blank"
);
}
#[test]
fn unified_memory_arriving_through_the_vram_fields_is_still_shared() {
let g = GpuSnapshot {
integrated: true,
vram_used_bytes: Some(12 << 30),
vram_total_bytes: Some(32 << 30),
..Default::default()
};
let m = g.mem_primary();
assert_eq!((m.used, m.total), (Some(12 << 30), Some(32 << 30)));
assert!(m.shared, "32 GB of dedicated VRAM is what this is not");
assert_eq!(g.mem_secondary(), None);
}
#[test]
fn an_apu_meters_its_carve_out_and_calls_the_rest_shared() {
let g = GpuSnapshot {
integrated: true,
vram_used_bytes: Some(412 << 20),
vram_total_bytes: Some(512 << 20),
gtt_used_bytes: Some(3 << 30),
gtt_total_bytes: Some(16 << 30),
..Default::default()
};
let m = g.mem_primary();
assert_eq!((m.used, m.total), (Some(412 << 20), Some(512 << 20)));
assert!(!m.shared, "the carve-out is reserved for the GPU alone");
let s = g
.mem_secondary()
.expect("the system pool is the other half");
assert!(s.shared, "nothing spilled anywhere on an APU");
}
#[test]
fn a_card_with_no_memory_figures_at_all_reports_none() {
let g = GpuSnapshot::default();
let m = g.mem_primary();
assert_eq!((m.used, m.total), (None, None));
assert_eq!(g.mem_secondary(), None);
assert_eq!(g.mem_pct(), None);
}
#[test]
fn one_known_half_still_selects_the_pool_it_came_from() {
let g = GpuSnapshot {
vram_used_bytes: Some(2 << 30),
gtt_used_bytes: Some(9 << 30),
..Default::default()
};
let m = g.mem_primary();
assert_eq!((m.used, m.total), (Some(2 << 30), None));
assert_eq!(m.pct(), None, "no total, so no fill level to draw");
assert!(g.mem_secondary().is_some());
}
#[test]
fn a_measured_empty_pool_is_not_an_absent_one() {
let g = GpuSnapshot {
vram_used_bytes: Some(0),
vram_total_bytes: Some(8 << 30),
..Default::default()
};
assert_eq!(g.mem_pct(), Some(0.0));
assert_eq!(
MemReadout {
used: Some(0),
total: Some(0),
shared: false
}
.pct(),
None
);
}
}
#[cfg(test)]
mod tests {
use super::*;
struct Stub {
name: &'static str,
ticks: Vec<Option<Vec<&'static str>>>,
tick: usize,
procs: Vec<(u32, usize)>,
driver: Option<&'static str>,
signal: bool,
ids: bool,
}
impl Stub {
fn new(name: &'static str, ticks: Vec<Option<Vec<&'static str>>>) -> Self {
Self {
name,
ticks,
tick: 0,
procs: Vec::new(),
driver: None,
signal: true,
ids: true,
}
}
fn no_ids(mut self) -> Self {
self.ids = false;
self
}
fn procs(mut self, procs: &[(u32, usize)]) -> Self {
self.procs = procs.to_vec();
self
}
fn driver(mut self, driver: &'static str) -> Self {
self.driver = Some(driver);
self
}
fn no_signal(mut self) -> Self {
self.signal = false;
self
}
}
impl GpuBackend for Stub {
fn name(&self) -> &'static str {
self.name
}
fn poll(&mut self) -> Result<Vec<GpuSnapshot>> {
let i = self.tick.min(self.ticks.len() - 1);
self.tick += 1;
match &self.ticks[i] {
Some(names) => Ok(names
.iter()
.map(|n| GpuSnapshot {
name: (*n).to_string(),
device_id: self.ids.then(|| (*n).to_string()),
..Default::default()
})
.collect()),
None => anyhow::bail!("{} is down", self.name),
}
}
fn processes(&mut self) -> Vec<GpuProcess> {
self.procs
.iter()
.map(|&(pid, gpu_index)| GpuProcess {
pid,
gpu_index,
..Default::default()
})
.collect()
}
fn driver_info(&self) -> Option<String> {
self.driver.map(str::to_string)
}
fn can_signal(&self) -> bool {
self.signal
}
}
fn names(snaps: &[GpuSnapshot]) -> Vec<&str> {
snaps.iter().map(|s| s.name.as_str()).collect()
}
fn ids(snaps: &[GpuSnapshot]) -> Vec<Option<&str>> {
snaps.iter().map(|s| s.device_id.as_deref()).collect()
}
#[test]
fn one_backend_is_not_wrapped() {
let b = compose(vec![Box::new(Stub::new("solo", vec![Some(vec!["a"])]))]).unwrap();
assert_eq!(b.name(), "solo");
assert!(compose(Vec::new()).is_err());
}
#[test]
fn children_concatenate_in_probe_order() {
let mut b = CompositeBackend::new(vec![
Box::new(Stub::new("nv", vec![Some(vec!["4090", "4080"])])),
Box::new(Stub::new("amd", vec![Some(vec!["APU"])])),
]);
assert_eq!(names(&b.poll().unwrap()), ["4090", "4080", "APU"]);
assert_eq!(b.name(), "multi");
}
#[test]
fn process_indices_rebase_onto_the_child_offset() {
let mut b = CompositeBackend::new(vec![
Box::new(Stub::new("nv", vec![Some(vec!["4090", "4080"])]).procs(&[(1, 0), (2, 1)])),
Box::new(Stub::new("amd", vec![Some(vec!["APU"])]).procs(&[(3, 0)])),
]);
b.poll().unwrap();
let procs: Vec<(u32, usize)> = b.processes().iter().map(|p| (p.pid, p.gpu_index)).collect();
assert_eq!(procs, [(1, 0), (2, 1), (3, 2)]);
}
#[test]
fn a_failing_child_holds_its_slots_open() {
let mut b = CompositeBackend::new(vec![
Box::new(Stub::new("nv", vec![Some(vec!["4090", "4080"]), None]).procs(&[(1, 0)])),
Box::new(Stub::new("amd", vec![Some(vec!["APU"])]).procs(&[(3, 0)])),
]);
b.poll().unwrap();
let snaps = b.poll().unwrap();
assert_eq!(
names(&snaps),
["4090 (unavailable)", "4080 (unavailable)", "APU"]
);
let procs: Vec<(u32, usize)> = b.processes().iter().map(|p| (p.pid, p.gpu_index)).collect();
assert_eq!(procs, [(1, 0), (3, 2)]);
}
#[test]
fn a_short_child_holds_its_slots_open() {
let mut b = CompositeBackend::new(vec![
Box::new(Stub::new(
"nv",
vec![Some(vec!["4090", "4080"]), Some(vec!["4090"])],
)),
Box::new(Stub::new("amd", vec![Some(vec!["APU"])])),
]);
b.poll().unwrap();
assert_eq!(
names(&b.poll().unwrap()),
["4090", "4080 (unavailable)", "APU"]
);
}
#[test]
fn device_ids_are_namespaced_per_child() {
let mut b = CompositeBackend::new(vec![
Box::new(Stub::new("nv", vec![Some(vec!["gpu0"])])),
Box::new(Stub::new("amd", vec![Some(vec!["gpu0"])])),
]);
assert_eq!(
ids(&b.poll().unwrap()),
[Some("nv#0:gpu0"), Some("amd#1:gpu0")]
);
let mut same = CompositeBackend::new(vec![
Box::new(Stub::new("nv", vec![Some(vec!["gpu0"])])),
Box::new(Stub::new("nv", vec![Some(vec!["gpu0"])])),
]);
assert_eq!(
ids(&same.poll().unwrap()),
[Some("nv#0:gpu0"), Some("nv#1:gpu0")]
);
}
#[test]
fn a_child_without_ids_reports_none() {
let mut b = CompositeBackend::new(vec![
Box::new(Stub::new("nv", vec![Some(vec!["4090"])])),
Box::new(Stub::new("pdh", vec![Some(vec!["iGPU"])]).no_ids()),
]);
assert_eq!(ids(&b.poll().unwrap()), [Some("nv#0:4090"), None]);
}
#[test]
fn a_placeholder_keeps_the_id_of_the_slot_it_holds() {
let mut b = CompositeBackend::new(vec![
Box::new(Stub::new(
"nv",
vec![Some(vec!["4090", "4080"]), Some(vec!["4090"])],
)),
Box::new(Stub::new("amd", vec![Some(vec!["APU"]), None])),
]);
b.poll().unwrap();
assert_eq!(
ids(&b.poll().unwrap()),
[Some("nv#0:4090"), Some("nv#0:4080"), Some("amd#1:APU")]
);
}
#[test]
fn a_process_outside_its_child_span_is_dropped() {
let mut b = CompositeBackend::new(vec![
Box::new(Stub::new("nv", vec![Some(vec!["4090"])]).procs(&[(1, 0), (2, 7)])),
Box::new(Stub::new("amd", vec![Some(vec!["APU"])]).procs(&[(3, 0)])),
]);
b.poll().unwrap();
let procs: Vec<(u32, usize)> = b.processes().iter().map(|p| (p.pid, p.gpu_index)).collect();
assert_eq!(procs, [(1, 0), (3, 1)]);
}
#[test]
fn three_children_concatenate_and_rebase_processes() {
let mut b = CompositeBackend::new(vec![
Box::new(Stub::new("nvml", vec![Some(vec!["4090", "4080"])]).procs(&[(1, 0), (2, 1)])),
Box::new(Stub::new("amdgpu", vec![Some(vec!["780M"])]).procs(&[(3, 0)])),
Box::new(Stub::new("intel", vec![Some(vec!["UHD", "Arc"])]).procs(&[(4, 0), (5, 1)])),
]);
assert_eq!(
names(&b.poll().unwrap()),
["4090", "4080", "780M", "UHD", "Arc"]
);
assert_eq!(
ids(&b.poll().unwrap()),
[
Some("nvml#0:4090"),
Some("nvml#0:4080"),
Some("amdgpu#1:780M"),
Some("intel#2:UHD"),
Some("intel#2:Arc"),
]
);
let procs: Vec<(u32, usize)> = b.processes().iter().map(|p| (p.pid, p.gpu_index)).collect();
assert_eq!(procs, [(1, 0), (2, 1), (3, 2), (4, 3), (5, 4)]);
}
#[test]
fn a_middle_child_changing_size_moves_the_children_after_it() {
let mut b = CompositeBackend::new(vec![
Box::new(Stub::new("nvml", vec![Some(vec!["4090"])]).procs(&[(1, 0)])),
Box::new(Stub::new(
"amdgpu",
vec![
Some(vec!["W7900", "W7800"]),
Some(vec!["W7900"]),
Some(vec!["W7900", "W7800", "W7700"]),
],
)),
Box::new(Stub::new("intel", vec![Some(vec!["Arc"])]).procs(&[(9, 0)])),
]);
let last = |b: &mut CompositeBackend| {
b.processes()
.iter()
.map(|p| (p.pid, p.gpu_index))
.collect::<Vec<_>>()
};
assert_eq!(names(&b.poll().unwrap()), ["4090", "W7900", "W7800", "Arc"]);
assert_eq!(last(&mut b), [(1, 0), (9, 3)]);
let snaps = b.poll().unwrap();
assert_eq!(
names(&snaps),
["4090", "W7900", "W7800 (unavailable)", "Arc"]
);
assert_eq!(ids(&snaps)[2], Some("amdgpu#1:W7800"));
assert_eq!(last(&mut b), [(1, 0), (9, 3)]);
let snaps = b.poll().unwrap();
assert_eq!(names(&snaps), ["4090", "W7900", "W7800", "W7700", "Arc"]);
assert_eq!(ids(&snaps)[4], Some("intel#2:Arc"));
assert_eq!(last(&mut b), [(1, 0), (9, 4)]);
}
#[test]
fn a_child_with_no_devices_takes_no_slots() {
let mut b = CompositeBackend::new(vec![
Box::new(Stub::new("nvml", vec![Some(vec!["4090"])]).procs(&[(1, 0)])),
Box::new(Stub::new("empty", vec![Some(Vec::new())]).procs(&[(7, 0)])),
Box::new(Stub::new("amdgpu", vec![Some(vec!["780M"])]).procs(&[(3, 0)])),
]);
assert_eq!(names(&b.poll().unwrap()), ["4090", "780M"]);
let procs: Vec<(u32, usize)> = b.processes().iter().map(|p| (p.pid, p.gpu_index)).collect();
assert_eq!(procs, [(1, 0), (3, 1)]);
}
#[test]
fn a_vacated_slot_does_not_duplicate_a_live_device_id() {
let mut b = CompositeBackend::new(vec![
Box::new(Stub::new("nv", vec![Some(vec!["A", "B"]), Some(vec!["B"])])),
Box::new(Stub::new("amd", vec![Some(vec!["APU"])])),
]);
b.poll().unwrap();
let snaps = b.poll().unwrap();
assert_eq!(ids(&snaps), [Some("nv#0:B"), None, Some("amd#1:APU")]);
assert_eq!(names(&snaps), ["B", "nv GPU 1 (unavailable)", "APU"]);
}
#[test]
fn every_child_failing_is_a_poll_error() {
let mut b = CompositeBackend::new(vec![
Box::new(Stub::new("nv", vec![None])),
Box::new(Stub::new("amd", vec![None])),
]);
let err = format!("{:#}", b.poll().unwrap_err());
assert!(
err.contains("nv is down") && err.contains("amd is down"),
"{err}"
);
}
#[test]
fn an_empty_result_surfaces_a_partial_failure() {
let mut b = CompositeBackend::new(vec![
Box::new(Stub::new("nv", vec![None])),
Box::new(Stub::new("amd", vec![Some(Vec::new())])),
]);
let err = format!("{:#}", b.poll().unwrap_err());
assert!(err.contains("nv is down"), "{err}");
}
#[test]
fn can_signal_is_the_and_of_the_children() {
let live = || Box::new(Stub::new("nv", vec![Some(vec!["4090"])]));
assert!(CompositeBackend::new(vec![live(), live()]).can_signal());
assert!(
!CompositeBackend::new(vec![
live(),
Box::new(Stub::new("mock", vec![Some(vec!["fake"])]).no_signal()),
])
.can_signal()
);
assert!(CompositeBackend::new(vec![live(), live(), live()]).can_signal());
assert!(
!CompositeBackend::new(vec![
live(),
Box::new(Stub::new("replay", vec![Some(vec!["rec"])]).no_signal()),
live(),
])
.can_signal()
);
}
fn generic(
adapters: &[(&'static str, u16)],
) -> impl FnOnce(&[u16]) -> Option<Box<dyn GpuBackend>> {
let adapters = adapters.to_vec();
move |claimed| {
let kept: Vec<&'static str> = adapters
.iter()
.filter(|(_, vendor)| !claimed.contains(vendor))
.map(|&(name, _)| name)
.collect();
(!kept.is_empty())
.then(|| Box::new(Stub::new("pdh", vec![Some(kept)])) as Box<dyn GpuBackend>)
}
}
fn vendor(
name: &'static str,
vendors: &'static [u16],
) -> (Box<dyn GpuBackend>, &'static [u16]) {
(
Box::new(Stub::new(name, vec![Some(vec!["card"])])) as Box<dyn GpuBackend>,
vendors,
)
}
#[test]
fn a_generic_backend_contributes_only_unclaimed_vendors() {
let nv = (
Box::new(Stub::new("nvml", vec![Some(vec!["4090"])])) as Box<dyn GpuBackend>,
&[PCI_VENDOR_NVIDIA][..],
);
let mut b = compose_with_generic(
vec![nv],
generic(&[("4090", PCI_VENDOR_NVIDIA), ("780M", PCI_VENDOR_AMD)]),
)
.unwrap();
assert_eq!(b.name(), "multi");
assert_eq!(names(&b.poll().unwrap()), ["4090", "780M"]);
}
#[test]
fn a_generic_backend_without_overlap_contributes_everything() {
let mut b = compose_with_generic(
vec![
vendor("nvml", &[PCI_VENDOR_NVIDIA]),
vendor("amdgpu", &[PCI_VENDOR_AMD]),
],
generic(&[("UHD", PCI_VENDOR_INTEL), ("Arc", PCI_VENDOR_INTEL)]),
)
.unwrap();
assert_eq!(names(&b.poll().unwrap()), ["card", "card", "UHD", "Arc"]);
let mut both = compose_with_generic(
vec![
vendor("nvml", &[PCI_VENDOR_NVIDIA]),
vendor("amdgpu", &[PCI_VENDOR_AMD]),
],
generic(&[("4090", PCI_VENDOR_NVIDIA), ("780M", PCI_VENDOR_AMD)]),
)
.unwrap();
assert_eq!(names(&both.poll().unwrap()), ["card", "card"]);
}
#[test]
fn a_generic_backend_alone_is_unfiltered_and_unwrapped() {
let mut b = compose_with_generic(
Vec::new(),
generic(&[("780M", PCI_VENDOR_AMD), ("UHD", PCI_VENDOR_INTEL)]),
)
.unwrap();
assert_eq!(b.name(), "pdh");
assert_eq!(names(&b.poll().unwrap()), ["780M", "UHD"]);
}
#[test]
fn a_fully_claimed_generic_backend_drops_out() {
let b = compose_with_generic(
vec![vendor("nvml", &[PCI_VENDOR_NVIDIA])],
generic(&[("4090", PCI_VENDOR_NVIDIA)]),
)
.unwrap();
assert_eq!(b.name(), "nvml");
}
#[test]
fn a_vendor_that_did_not_probe_claims_nothing() {
let mut b = compose_with_generic(
vec![vendor("nvml", &[PCI_VENDOR_NVIDIA])],
generic(&[
("4090", PCI_VENDOR_NVIDIA),
("780M", PCI_VENDOR_AMD),
("UHD", PCI_VENDOR_INTEL),
]),
)
.unwrap();
assert_eq!(b.name(), "multi");
assert_eq!(names(&b.poll().unwrap()), ["card", "780M", "UHD"]);
assert_eq!(
b.driver_info().as_deref(),
Some("nvml · pdh"),
"a child must not drop out of the header"
);
}
#[test]
fn nothing_probing_at_all_is_still_an_error() {
assert!(compose_with_generic(Vec::new(), generic(&[])).is_err());
}
#[test]
fn driver_info_joins_and_names_the_children() {
let b = CompositeBackend::new(vec![
Box::new(Stub::new("nvml", vec![Some(vec!["4090"])]).driver("driver 550.1")),
Box::new(Stub::new("amdgpu", vec![Some(vec!["APU"])]).driver("amdgpu · kernel 7.1")),
Box::new(Stub::new("intel", vec![Some(vec!["iGPU"])])),
]);
assert_eq!(
b.driver_info().as_deref(),
Some("nvml driver 550.1 · amdgpu · kernel 7.1 · intel")
);
}
}