use super::{GpuProcess, ProcKind, clamp_pct};
use std::collections::{BTreeSet, HashMap, HashSet};
use std::fs;
use std::os::unix::fs::{FileTypeExt, MetadataExt};
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use std::time::Instant;
pub const PCI_IDS_PATHS: &[&str] = &["/usr/share/hwdata/pci.ids", "/usr/share/misc/pci.ids"];
const DRM_MAJOR: u64 = 226;
#[derive(Debug, Default)]
pub struct FdClient {
pub driver: String,
pub id: u64,
pub pdev: Option<String>,
pub engine_ns: HashMap<String, u64>,
pub cycles: HashMap<String, (u64, u64)>,
pub memory: HashMap<String, u64>,
}
impl FdClient {
pub fn total_engine_ns(&self) -> u64 {
self.engine_ns.values().sum()
}
pub fn engine_ns_where(&self, pred: impl Fn(&str) -> bool) -> u64 {
self.engine_ns
.iter()
.filter(|(k, _)| pred(k))
.map(|(_, v)| *v)
.sum()
}
pub fn xe_ratio(&self, prev: &FdClient, pred: impl Fn(&str) -> bool) -> f64 {
let mut best = 0.0f64;
for (name, (cyc, total)) in &self.cycles {
if !pred(name) {
continue;
}
let (pcyc, ptotal) = prev.cycles.get(name).copied().unwrap_or((0, 0));
let dt = total.saturating_sub(ptotal);
if dt == 0 {
continue;
}
best = best.max(cyc.saturating_sub(pcyc) as f64 / dt as f64);
}
best
}
}
pub fn ns_delta_util(
prev: Option<&(u64, u64, Instant)>,
engine_ns: u64,
video_ns: u64,
now: Instant,
) -> (f64, f64) {
let Some((prev_ns, prev_video, prev_at)) = prev else {
return (0.0, 0.0);
};
let wall = now.duration_since(*prev_at).as_nanos() as f64;
if wall <= 0.0 {
return (0.0, 0.0);
}
(
(engine_ns.saturating_sub(*prev_ns) as f64 / wall * 100.0).clamp(0.0, 100.0),
(video_ns.saturating_sub(*prev_video) as f64 / wall * 100.0).clamp(0.0, 100.0),
)
}
pub fn build_proc(pid: u32, gpu_index: usize, util: f64, mem: u64, graphics: bool) -> GpuProcess {
GpuProcess {
pid,
gpu_index,
kind: if graphics {
ProcKind::Graphics
} else {
ProcKind::Compute
},
gpu_util_pct: Some(clamp_pct(util)),
gpu_mem_bytes: mem,
..Default::default()
}
}
pub struct SweepDevice {
pub pdev: Option<String>,
pub driver: String,
}
#[derive(Debug, Default, Clone, Copy)]
pub struct ClientSample {
pub util_pct: f64,
pub video_pct: f64,
pub mem_bytes: u64,
pub graphics: bool,
}
#[derive(Debug, Default)]
pub struct Sweep {
pub util: HashMap<usize, f64>,
pub video_util: HashMap<usize, f64>,
pub procs: Vec<GpuProcess>,
pub seen: HashSet<(u32, u64)>,
}
pub fn sweep_clients<F>(devices: &[SweepDevice], mut per_client: F) -> Sweep
where
F: FnMut(u32, usize, &FdClient) -> ClientSample,
{
let mut sweep = Sweep::default();
let mut agg: HashMap<(u32, usize), (f64, u64, bool)> = HashMap::new();
for pid in proc_pids() {
for client in drm_clients(pid) {
let Some(gpu) = client_device(devices, &client) else {
continue;
};
if !sweep.seen.insert((pid, client.id)) {
continue;
}
let s = per_client(pid, gpu, &client);
*sweep.util.entry(gpu).or_default() += s.util_pct;
*sweep.video_util.entry(gpu).or_default() += s.video_pct;
let e = agg.entry((pid, gpu)).or_insert((0.0, 0, false));
e.0 += s.util_pct;
e.1 += s.mem_bytes;
e.2 |= s.graphics;
}
}
sweep.procs = agg
.into_iter()
.map(|((pid, gpu_index), (util, mem, graphics))| {
build_proc(pid, gpu_index, util, mem, graphics)
})
.collect();
sweep
}
pub fn client_device(devices: &[SweepDevice], client: &FdClient) -> Option<usize> {
let pdev = client.pdev.as_deref()?;
devices
.iter()
.position(|d| d.pdev.as_deref() == Some(pdev) && d.driver == client.driver)
}
pub fn proc_pids() -> Vec<u32> {
fs::read_dir("/proc")
.map(|rd| {
rd.flatten()
.filter_map(|e| e.file_name().to_string_lossy().parse().ok())
.collect()
})
.unwrap_or_default()
}
pub fn drm_clients(pid: u32) -> Vec<FdClient> {
let fd_dir = format!("/proc/{pid}/fd");
let Ok(entries) = fs::read_dir(&fd_dir) else {
return Vec::new(); };
entries
.flatten()
.filter_map(|e| {
let meta = fs::metadata(e.path()).ok()?;
if !meta.file_type().is_char_device() || linux_major(meta.rdev()) != DRM_MAJOR {
return None;
}
let fd = e.file_name();
let info =
fs::read_to_string(format!("/proc/{pid}/fdinfo/{}", fd.to_string_lossy())).ok()?;
parse_fdinfo(&info)
})
.collect()
}
fn linux_major(rdev: u64) -> u64 {
((rdev >> 8) & 0xfff) | ((rdev >> 32) & !0xfff)
}
pub fn parse_fdinfo(info: &str) -> Option<FdClient> {
let mut c = FdClient::default();
let mut have_id = false;
let mut resident: HashMap<String, u64> = HashMap::new();
for line in info.lines() {
let Some((key, value)) = line.split_once(':') else {
continue;
};
let value = value.trim();
if key == "drm-driver" {
c.driver = value.to_string();
} else if key == "drm-client-id" {
c.id = value.parse().ok()?;
have_id = true;
} else if key == "drm-pdev" {
c.pdev = Some(value.to_string());
} else if let Some(name) = key.strip_prefix("drm-engine-") {
if !name.starts_with("capacity") {
c.engine_ns.insert(name.to_string(), parse_ns(value));
}
} else if let Some(name) = key.strip_prefix("drm-total-cycles-") {
c.cycles.entry(name.to_string()).or_default().1 = parse_ns(value);
} else if let Some(name) = key.strip_prefix("drm-cycles-") {
c.cycles.entry(name.to_string()).or_default().0 = parse_ns(value);
} else if let Some(region) = key.strip_prefix("drm-memory-") {
c.memory.insert(region.to_string(), parse_size(value));
} else if let Some(region) = key.strip_prefix("drm-resident-") {
resident.insert(region.to_string(), parse_size(value));
}
}
for (region, bytes) in resident {
c.memory.entry(region).or_insert(bytes);
}
(have_id && !c.driver.is_empty()).then_some(c)
}
fn parse_ns(v: &str) -> u64 {
v.split_whitespace()
.next()
.and_then(|n| n.parse().ok())
.unwrap_or(0)
}
fn parse_size(v: &str) -> u64 {
let mut it = v.split_whitespace();
let n: u64 = it.next().and_then(|n| n.parse().ok()).unwrap_or(0);
match it.next() {
Some("KiB") => n.saturating_mul(1 << 10),
Some("MiB") => n.saturating_mul(1 << 20),
Some("GiB") => n.saturating_mul(1 << 30),
_ => n,
}
}
pub fn read_u64(path: &Path) -> Option<u64> {
read_trim(path)?.parse().ok()
}
pub fn hwmon_u64(hwmon: Option<&Path>, file: &str) -> Option<u64> {
read_u64(&hwmon?.join(file))
}
pub fn fan_pct(hwmon: Option<&Path>) -> Option<f64> {
let pwm = hwmon_u64(hwmon, "pwm1")?;
let max = hwmon_u64(hwmon, "pwm1_max")
.filter(|v| *v > 0)
.unwrap_or(255);
Some(pwm as f64 / max as f64 * 100.0)
}
pub fn read_trim(path: &Path) -> Option<String> {
fs::read_to_string(path).ok().map(|s| s.trim().to_string())
}
pub fn first_dir(path: &Path) -> Option<PathBuf> {
fs::read_dir(path)
.ok()?
.flatten()
.map(|e| e.path())
.find(|p| p.is_dir())
}
pub fn card_index(file_name: &str) -> Option<u32> {
file_name.strip_prefix("card")?.parse().ok()
}
pub fn driver_of(dev: &Path) -> Option<String> {
Some(
fs::read_link(dev.join("driver"))
.ok()?
.file_name()?
.to_string_lossy()
.into_owned(),
)
}
pub fn cards_with_driver(
drm: &str,
vendor: &str,
accept: impl Fn(&str) -> bool,
) -> Vec<(u32, PathBuf, String)> {
let Ok(entries) = fs::read_dir(drm) else {
return Vec::new();
};
let mut cards: Vec<(u32, PathBuf, String)> = entries
.flatten()
.filter_map(|e| {
let idx = card_index(&e.file_name().to_string_lossy())?;
let dev = e.path().join("device");
if read_trim(&dev.join("vendor")).as_deref() != Some(vendor) {
return None;
}
let driver = driver_of(&dev).filter(|d| accept(d))?;
Some((idx, dev, driver))
})
.collect();
cards.sort_by_key(|(idx, _, _)| *idx);
cards
}
pub fn gts_to_gen(speed: &str) -> Option<u8> {
let gts: f64 = speed.split_whitespace().next()?.parse().ok()?;
Some(match gts {
s if s >= 128.0 => 7,
s if s >= 64.0 => 6,
s if s >= 32.0 => 5,
s if s >= 16.0 => 4,
s if s >= 8.0 => 3,
s if s >= 5.0 => 2,
_ => 1,
})
}
pub fn pcie_link(dev: &Path) -> (Option<u8>, Option<u32>, Option<u8>, Option<u32>) {
let speed = |f: &str| read_trim(&dev.join(f)).as_deref().and_then(gts_to_gen);
let width = |f: &str| read_trim(&dev.join(f)).and_then(|w| w.parse().ok());
(
speed("current_link_speed"),
width("current_link_width"),
speed("max_link_speed"),
width("max_link_width"),
)
}
pub fn driver_line(prefix: &str) -> Option<String> {
sysinfo::System::kernel_version().map(|k| format!("{prefix} · kernel {k}"))
}
pub fn driver_line_for<'a>(drivers: impl IntoIterator<Item = &'a str>) -> Option<String> {
let unique: BTreeSet<&str> = drivers.into_iter().collect();
driver_line(&unique.into_iter().collect::<Vec<_>>().join("+"))
}
pub fn sys_mem_total_bytes() -> Option<u64> {
parse_meminfo_total(&fs::read_to_string("/proc/meminfo").ok()?)
}
fn parse_meminfo_total(meminfo: &str) -> Option<u64> {
let line = meminfo.lines().find(|l| l.starts_with("MemTotal:"))?;
let kb: u64 = line.split_whitespace().nth(1)?.parse().ok()?;
kb.checked_mul(1024)
}
pub fn pdev_of(dev: &Path) -> Option<String> {
fs::canonicalize(dev)
.ok()
.and_then(|p| p.file_name().map(|n| n.to_string_lossy().into_owned()))
}
pub fn pci_device_id(pdev: Option<&str>) -> Option<String> {
pdev.map(|p| format!("pci:{p}"))
}
pub fn pci_device_name(ids: &str, vendor: &str, device: &str) -> Option<String> {
let mut in_vendor = false;
for line in ids.lines() {
if line.starts_with('#') || line.is_empty() {
continue;
}
if !line.starts_with('\t') {
in_vendor = line
.split_whitespace()
.next()
.is_some_and(|v| v.eq_ignore_ascii_case(vendor));
continue;
}
if !in_vendor || line.starts_with("\t\t") {
continue; }
let rest = line.trim_start();
if let Some((id, name)) = rest.split_once(char::is_whitespace)
&& id.eq_ignore_ascii_case(device)
{
return Some(name.trim().to_string());
}
}
None
}
static PCI_IDS: OnceLock<Option<String>> = OnceLock::new();
fn pci_ids() -> Option<&'static str> {
PCI_IDS
.get_or_init(|| {
PCI_IDS_PATHS
.iter()
.find_map(|p| fs::read_to_string(p).ok())
})
.as_deref()
}
pub fn card_name(dev: &Path, idx: u32, vendor_hex: &str, fallback_brand: &str) -> String {
let device_id = read_trim(&dev.join("device")).unwrap_or_default();
pci_ids()
.and_then(|ids| pci_device_name(ids, vendor_hex, device_id.trim_start_matches("0x")))
.unwrap_or_else(|| format!("{fallback_brand} GPU {device_id} (card{idx})"))
}
#[cfg(test)]
pub mod testing {
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU32, Ordering};
pub struct Sandbox(PathBuf);
impl Sandbox {
pub fn new(tag: &str) -> Self {
static N: AtomicU32 = AtomicU32::new(0);
let dir = std::env::temp_dir().join(format!(
"gpur-unit-{tag}-{}-{}",
std::process::id(),
N.fetch_add(1, Ordering::Relaxed)
));
fs::create_dir_all(&dir).unwrap();
Sandbox(dir)
}
}
impl std::ops::Deref for Sandbox {
type Target = Path;
fn deref(&self) -> &Path {
&self.0
}
}
impl Drop for Sandbox {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
pub fn card(root: &Path, idx: u32, bdf: &str, vendor: &str, device: &str, driver: &str) {
let pci = root.join("pci").join(bdf);
fs::create_dir_all(&pci).unwrap();
fs::write(pci.join("vendor"), format!("{vendor}\n")).unwrap();
fs::write(pci.join("device"), format!("{device}\n")).unwrap();
if !driver.is_empty() {
let target = root.join("pci/drivers").join(driver);
fs::create_dir_all(&target).unwrap();
std::os::unix::fs::symlink(&target, pci.join("driver")).unwrap();
}
let drm = root.join("drm");
for entry in [
format!("card{idx}"),
format!("renderD{}", 128 + idx),
format!("card{idx}-DP-1"),
] {
let d = drm.join(entry);
fs::create_dir_all(&d).unwrap();
std::os::unix::fs::symlink(&pci, d.join("device")).unwrap();
}
}
pub fn platform_card(root: &Path, idx: u32) {
let plat = root.join("platform/simple-framebuffer.0");
fs::create_dir_all(&plat).unwrap();
let d = root.join("drm").join(format!("card{idx}"));
fs::create_dir_all(&d).unwrap();
std::os::unix::fs::symlink(&plat, d.join("device")).unwrap();
}
pub fn tri_vendor(name: &str) -> Sandbox {
let root = Sandbox::new(name);
card(&root, 0, "0000:00:02.0", "0x8086", "0x7d55", "i915");
card(&root, 1, "0000:03:00.0", "0x1002", "0x744c", "amdgpu");
card(&root, 2, "0000:01:00.0", "0x10de", "0x2684", "nvidia");
card(&root, 3, "0000:04:00.0", "0x10de", "0x1c03", "nouveau");
card(&root, 4, "0000:05:00.0", "0x1002", "0x6779", "radeon");
card(&root, 5, "0000:06:00.0", "0x8086", "0xe20b", "xe");
card(&root, 6, "0000:07:00.0", "0x1002", "0x1636", "");
card(&root, 7, "0000:08:00.0", "0x8086", "0x9a49", "");
card(&root, 8, "0000:09:00.0", "0x10de", "0x2504", "");
platform_card(&root, 9);
root
}
pub fn drm(root: &Path) -> String {
root.join("drm").to_string_lossy().into_owned()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pci_device_ids_are_namespaced_and_optional() {
assert_eq!(
pci_device_id(Some("0000:75:00.0")).as_deref(),
Some("pci:0000:75:00.0")
);
assert_eq!(pci_device_id(None), None);
}
const AMD_FDINFO: &str = "\
drm-driver:\tamdgpu
drm-client-id:\t7568
drm-pdev:\t0000:75:00.0
drm-engine-gfx:\t123456789 ns
drm-engine-dec:\t1000 ns
drm-memory-vram:\t12 KiB
drm-memory-gtt: \t2048 KiB
";
const I915_FDINFO: &str = "\
drm-driver:\ti915
drm-client-id:\t42
drm-pdev:\t0000:00:02.0
drm-engine-render:\t9876543 ns
drm-engine-video:\t100 ns
drm-engine-capacity-video:\t2
drm-total-local0:\t512 MiB
drm-resident-local0:\t256 MiB
drm-resident-stolen-local0:\t131072 KiB
drm-resident-system0:\t1234567
";
const XE_FDINFO: &str = "\
drm-driver:\txe
drm-client-id:\t7
drm-pdev:\t0000:03:00.0
drm-cycles-rcs:\t500
drm-total-cycles-rcs:\t1000
drm-cycles-vcs:\t10
drm-total-cycles-vcs:\t1000
drm-resident-vram0:\t4096 KiB
";
#[test]
fn parses_amdgpu_client() {
let c = parse_fdinfo(AMD_FDINFO).unwrap();
assert_eq!(c.driver, "amdgpu");
assert_eq!(c.id, 7568);
assert_eq!(c.pdev.as_deref(), Some("0000:75:00.0"));
assert_eq!(c.engine_ns["gfx"], 123_456_789);
assert_eq!(c.total_engine_ns(), 123_456_789 + 1000);
assert_eq!(c.memory["vram"], 12 << 10);
assert_eq!(c.memory["gtt"], 2048 << 10);
}
#[test]
fn parses_i915_client_skipping_capacity() {
let c = parse_fdinfo(I915_FDINFO).unwrap();
assert_eq!(c.driver, "i915");
assert_eq!(c.engine_ns["render"], 9_876_543);
assert!(!c.engine_ns.contains_key("capacity-video"));
assert_eq!(c.memory["local0"], 256 << 20);
assert_eq!(c.memory["stolen-local0"], 131_072 << 10);
assert_eq!(c.memory["system0"], 1_234_567);
assert!(!c.memory.contains_key("total-local0"));
}
#[test]
fn parse_size_honours_unit_suffix() {
assert_eq!(parse_size("1234567"), 1_234_567); assert_eq!(parse_size("12 KiB"), 12 << 10);
assert_eq!(parse_size("512 MiB"), 512 << 20);
assert_eq!(parse_size("2 GiB"), 2 << 30);
assert_eq!(parse_size("0"), 0);
assert_eq!(parse_size("0 KiB"), 0);
assert_eq!(parse_size(""), 0);
assert_eq!(parse_size("42 TiB"), 42);
assert_eq!(parse_size(&format!("{} GiB", u64::MAX)), u64::MAX);
}
#[test]
fn xe_cycles_utilization() {
let prev = parse_fdinfo(XE_FDINFO).unwrap();
let mut cur = parse_fdinfo(XE_FDINFO).unwrap();
cur.cycles.insert("rcs".into(), (800, 2000));
cur.cycles.insert("vcs".into(), (110, 2000));
assert!((cur.xe_ratio(&prev, |_| true) - 0.3).abs() < 1e-9);
assert!((cur.xe_ratio(&prev, |n| n.starts_with("vcs")) - 0.1).abs() < 1e-9);
assert_eq!(cur.memory["vram0"], 4096 * 1024);
}
#[test]
fn non_drm_fdinfo_is_none() {
assert!(parse_fdinfo("pos:\t0\nflags:\t0100002\n").is_none());
}
#[test]
fn card_index_filters_connectors_and_render_nodes() {
assert_eq!(card_index("card0"), Some(0));
assert_eq!(card_index("card12"), Some(12));
assert_eq!(card_index("card1-DP-1"), None);
assert_eq!(card_index("renderD128"), None);
assert_eq!(card_index("version"), None);
}
const IDS: &str = "\
# comment
1002 Advanced Micro Devices, Inc. [AMD/ATI]
\t13c0 Phoenix2
\t744c Navi 31 [Radeon RX 7900 XT/7900 XTX/7900M]
\t\t1002 0e3b Some subsystem
8086 Intel Corporation
\t56a0 DG2 [Arc A770]
";
fn scratch(name: &str) -> testing::Sandbox {
testing::Sandbox::new(name)
}
#[test]
fn pcie_gen_from_gts_string() {
assert_eq!(gts_to_gen("2.5 GT/s PCIe"), Some(1));
assert_eq!(gts_to_gen("8.0 GT/s PCIe"), Some(3));
assert_eq!(gts_to_gen("16.0 GT/s PCIe"), Some(4));
assert_eq!(gts_to_gen("32.0 GT/s PCIe"), Some(5));
assert_eq!(gts_to_gen("64.0 GT/s PCIe"), Some(6));
assert_eq!(gts_to_gen("Unknown"), None);
assert_eq!(gts_to_gen("garbage"), None);
}
#[test]
fn pcie_link_reads_current_and_max() {
let dev = scratch("pcie");
fs::write(dev.join("current_link_speed"), "8.0 GT/s PCIe\n").unwrap();
fs::write(dev.join("current_link_width"), "8\n").unwrap();
fs::write(dev.join("max_link_speed"), "16.0 GT/s PCIe\n").unwrap();
fs::write(dev.join("max_link_width"), "16\n").unwrap();
assert_eq!(pcie_link(&dev), (Some(3), Some(8), Some(4), Some(16)));
}
#[test]
fn pcie_link_absent_files_are_none() {
let dev = scratch("pcie-missing");
assert_eq!(pcie_link(&dev), (None, None, None, None));
fs::write(dev.join("current_link_width"), "4\n").unwrap();
fs::write(dev.join("max_link_speed"), "Unknown\n").unwrap();
assert_eq!(pcie_link(&dev), (None, Some(4), None, None));
}
#[test]
fn fan_duty_is_a_percentage_of_this_cards_own_pwm_scale() {
let h = scratch("fan");
fs::write(h.join("pwm1"), "128\n").unwrap();
assert!((fan_pct(Some(&h)).unwrap() - 128.0 / 255.0 * 100.0).abs() < 1e-9);
fs::write(h.join("pwm1_max"), "100\n").unwrap();
fs::write(h.join("pwm1"), "50\n").unwrap();
assert!((fan_pct(Some(&h)).unwrap() - 50.0).abs() < 1e-9);
fs::write(h.join("pwm1_max"), "0\n").unwrap();
assert!((fan_pct(Some(&h)).unwrap() - 50.0 / 255.0 * 100.0).abs() < 1e-9);
fs::remove_file(h.join("pwm1")).unwrap();
assert_eq!(fan_pct(Some(&h)), None);
assert_eq!(fan_pct(None), None);
}
#[test]
fn the_driver_line_names_every_driver_in_use_once() {
let line = |drivers: &[&str]| driver_line_for(drivers.to_vec());
let Some(kernel) = sysinfo::System::kernel_version() else {
return; };
assert_eq!(
line(&["amdgpu", "radeon"]),
Some(format!("amdgpu+radeon · kernel {kernel}"))
);
assert_eq!(line(&["radeon", "amdgpu"]), line(&["amdgpu", "radeon"]));
assert_eq!(
line(&["i915", "i915", "xe"]),
Some(format!("i915+xe · kernel {kernel}"))
);
assert_eq!(line(&["nouveau"]), driver_line("nouveau"));
}
#[test]
fn meminfo_total_parses_kb() {
assert_eq!(
parse_meminfo_total("MemTotal: 32770396 kB\nMemFree: 100 kB\n"),
Some(32_770_396 * 1024)
);
assert_eq!(parse_meminfo_total("MemFree: 100 kB\n"), None);
assert_eq!(parse_meminfo_total("MemTotal:\n"), None);
}
#[test]
fn card_scan_filters_by_name_and_by_driver() {
let root = testing::tri_vendor("scan-filters");
let drm = testing::drm(&root);
let amd = cards_with_driver(&drm, "0x1002", |d| d == "amdgpu");
assert_eq!(
amd.iter()
.map(|(i, _, d)| (*i, d.as_str()))
.collect::<Vec<_>>(),
[(1, "amdgpu")],
"renderD129/device and card1-DP-1/device read vendor 0x1002 too"
);
let both = cards_with_driver(&drm, "0x1002", |d| d == "amdgpu" || d == "radeon");
assert_eq!(
both.iter()
.map(|(i, _, d)| (*i, d.as_str()))
.collect::<Vec<_>>(),
[(1, "amdgpu"), (4, "radeon")]
);
assert!(!both.iter().any(|(i, _, _)| *i == 6));
assert!(
cards_with_driver(&drm, "0x1002", |_| true)
.iter()
.all(|(i, _, _)| *i != 9)
);
assert_eq!(
cards_with_driver(&drm, "0x8086", |d| d == "i915" || d == "xe")
.iter()
.map(|(i, _, d)| (*i, d.as_str()))
.collect::<Vec<_>>(),
[(0, "i915"), (5, "xe")]
);
assert_eq!(
cards_with_driver(&drm, "0x10de", |d| d == "nouveau")
.iter()
.map(|(i, _, d)| (*i, d.as_str()))
.collect::<Vec<_>>(),
[(3, "nouveau")]
);
assert!(
cards_with_driver(&drm, "0x1002", |_| true)
.windows(2)
.all(|w| w[0].0 < w[1].0)
);
}
#[test]
fn the_linux_backends_partition_one_drm_directory() {
let root = testing::tri_vendor("partition");
let drm = testing::drm(&root);
let amd = crate::backend::amd::claimed_ids(&drm);
let intel = crate::backend::intel::claimed_ids(&drm);
let nvidia = crate::backend::nvidia::claimed_ids(&drm);
assert_eq!(amd, ["pci:0000:03:00.0", "pci:0000:05:00.0"]);
assert_eq!(intel, ["pci:0000:00:02.0", "pci:0000:06:00.0"]);
assert_eq!(nvidia, ["pci:0000:04:00.0"]);
let all: Vec<&String> = amd.iter().chain(&intel).chain(&nvidia).collect();
let unique: HashSet<&&String> = all.iter().collect();
assert_eq!(all.len(), 5);
assert_eq!(unique.len(), all.len(), "a device claimed by two backends");
assert!(!all.iter().any(|id| id.ends_with("0000:01:00.0")));
}
#[test]
fn card_scan_resolves_the_pci_address() {
let root = testing::tri_vendor("scan-pdev");
let cards = cards_with_driver(&testing::drm(&root), "0x1002", |d| d == "amdgpu");
let (_, dev, _) = &cards[0];
assert_eq!(pdev_of(dev).as_deref(), Some("0000:03:00.0"));
assert_eq!(
pci_device_id(pdev_of(dev).as_deref()).as_deref(),
Some("pci:0000:03:00.0")
);
}
#[test]
fn clients_attribute_by_address_and_driver() {
let dev = |pdev: &str, driver: &str| SweepDevice {
pdev: Some(pdev.to_string()),
driver: driver.to_string(),
};
let amd = [dev("0000:03:00.0", "amdgpu")];
let intel = [dev("0000:00:02.0", "i915"), dev("0000:06:00.0", "xe")];
let i915_client = parse_fdinfo(I915_FDINFO).unwrap();
assert_eq!(client_device(&intel, &i915_client), Some(0));
assert_eq!(
client_device(&amd, &i915_client),
None,
"an Intel client must not land on the AMD backend's only card"
);
let amd_client = parse_fdinfo(AMD_FDINFO).unwrap();
let impostor = [dev("0000:75:00.0", "i915")];
assert_eq!(client_device(&impostor, &amd_client), None);
assert_eq!(
client_device(&[dev("0000:75:00.0", "amdgpu")], &amd_client),
Some(0)
);
let xe_client = parse_fdinfo(XE_FDINFO).unwrap();
assert_eq!(
client_device(
&[dev("0000:00:02.0", "xe"), dev("0000:03:00.0", "xe")],
&xe_client
),
Some(1)
);
let mut anon = parse_fdinfo(AMD_FDINFO).unwrap();
anon.pdev = None;
assert_eq!(client_device(&[dev("0000:75:00.0", "amdgpu")], &anon), None);
}
#[test]
fn pci_lookup_finds_device_in_vendor_section() {
assert_eq!(
pci_device_name(IDS, "1002", "744c").as_deref(),
Some("Navi 31 [Radeon RX 7900 XT/7900 XTX/7900M]")
);
assert_eq!(
pci_device_name(IDS, "8086", "56a0").as_deref(),
Some("DG2 [Arc A770]")
);
assert_eq!(pci_device_name(IDS, "1002", "0e3b"), None);
}
}