use std::collections::HashMap;
use std::fs;
use std::path::Path;
use super::{SysRoots, read_trimmed, read_u64, sorted_entries};
use crate::snapshot::GpuStats;
const VENDOR_AMD: u16 = 0x1002;
const VENDOR_INTEL: u16 = 0x8086;
const PCI_IDS: &[&str] = &[
"/usr/share/hwdata/pci.ids",
"/usr/share/misc/pci.ids",
"/usr/share/pci.ids",
];
#[derive(Default)]
pub struct DrmGpuReader {
names: HashMap<(u16, u16), String>,
}
impl DrmGpuReader {
pub fn new() -> Self {
Self::default()
}
pub fn read(&mut self, roots: &SysRoots, first_index: u32) -> Vec<GpuStats> {
let class_dir = roots.sys.join("class/drm");
let mut gpus = Vec::new();
for entry in sorted_entries(&class_dir) {
let is_card = entry
.strip_prefix("card")
.is_some_and(|n| !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit()));
if !is_card {
continue;
}
let card = class_dir.join(&entry);
let device = card.join("device");
let Some(vendor) = read_hex(&device.join("vendor")) else {
continue;
};
let driver = fs::read_link(device.join("driver"))
.ok()
.and_then(|p| p.file_name().map(|n| n.to_string_lossy().into_owned()))
.unwrap_or_default();
let index = first_index + gpus.len() as u32;
let gpu = match (vendor, driver.as_str()) {
(VENDOR_AMD, "amdgpu") => Some(self.read_amd(&device, index)),
(VENDOR_INTEL, "i915" | "xe") => Some(self.read_intel(&card, &device, index)),
_ => None,
};
gpus.extend(gpu);
}
gpus
}
fn read_amd(&mut self, device: &Path, index: u32) -> GpuStats {
let hwmon = Hwmon::find(device);
let percent = |name: &str| read_u64(&device.join(name)).map(|v| v as f32);
let integrated = hwmon.is_apu;
GpuStats {
index,
vendor: "AMD".to_owned(),
name: self.name(device, VENDOR_AMD, "AMD"),
integrated,
utilization_percent: percent("gpu_busy_percent"),
memory_utilization_percent: percent("mem_busy_percent"),
memory_used_bytes: read_u64(&device.join("mem_info_vram_used")),
memory_total_bytes: read_u64(&device.join("mem_info_vram_total")),
temperature_c: hwmon.temperature_c,
power_w: hwmon.power_w.filter(|_| !integrated),
power_limit_w: hwmon.power_cap_w.filter(|_| !integrated),
fan_percent: hwmon.fan_percent,
clock_mhz: hwmon.clock_mhz.or_else(|| current_sclk(device)),
processes: Vec::new(),
}
}
fn read_intel(&mut self, card: &Path, device: &Path, index: u32) -> GpuStats {
let hwmon = Hwmon::find(device);
let clock_mhz = read_u64(&card.join("gt_act_freq_mhz"))
.or_else(|| read_u64(&device.join("tile0/gt0/freq0/act_freq")))
.map(|mhz| mhz as u32);
let integrated = fs::canonicalize(device)
.ok()
.and_then(|p| {
p.file_name()
.map(|n| n.to_string_lossy().ends_with(":00:02.0"))
})
.unwrap_or(false);
GpuStats {
index,
vendor: "Intel".to_owned(),
name: self.name(device, VENDOR_INTEL, "Intel"),
integrated,
temperature_c: hwmon.temperature_c,
power_w: hwmon.power_w,
power_limit_w: hwmon.power_cap_w,
fan_percent: hwmon.fan_percent,
clock_mhz,
..Default::default()
}
}
fn name(&mut self, device: &Path, vendor: u16, vendor_name: &str) -> String {
if let Some(name) = read_trimmed(&device.join("product_name")).filter(|n| !n.is_empty()) {
return with_vendor(&name, vendor_name);
}
let id = read_hex(&device.join("device")).unwrap_or(0);
self.names
.entry((vendor, id))
.or_insert_with(|| {
let name = PCI_IDS
.iter()
.find_map(|path| fs::read_to_string(path).ok())
.and_then(|text| pci_device_name(&text, vendor, id))
.unwrap_or_else(|| format!("GPU {id:04x}"));
with_vendor(&name, vendor_name)
})
.clone()
}
}
fn with_vendor(name: &str, vendor: &str) -> String {
if name.starts_with(vendor) {
name.to_owned()
} else {
format!("{vendor} {name}")
}
}
fn read_hex(path: &Path) -> Option<u16> {
let text = read_trimmed(path)?;
u16::from_str_radix(text.trim_start_matches("0x"), 16).ok()
}
fn current_sclk(device: &Path) -> Option<u32> {
let text = fs::read_to_string(device.join("pp_dpm_sclk")).ok()?;
let line = text.lines().find(|l| l.trim_end().ends_with('*'))?;
let value = line.split_whitespace().nth(1)?;
value
.trim_end_matches("Mhz")
.trim_end_matches("MHz")
.parse()
.ok()
}
pub(crate) fn pci_device_name(text: &str, vendor: u16, device: u16) -> Option<String> {
let vendor_prefix = format!("{vendor:04x} ");
let device_prefix = format!("\t{device:04x} ");
let mut in_vendor = false;
for line in text.lines() {
if line.starts_with('#') || line.is_empty() {
continue;
}
if !line.starts_with('\t') {
if in_vendor {
return None;
}
in_vendor = line.starts_with(&vendor_prefix);
} else if in_vendor && let Some(name) = line.strip_prefix(&device_prefix) {
return Some(name.trim().to_owned());
}
}
None
}
#[derive(Default)]
struct Hwmon {
temperature_c: Option<f32>,
power_w: Option<f32>,
power_cap_w: Option<f32>,
fan_percent: Option<f32>,
clock_mhz: Option<u32>,
is_apu: bool,
}
impl Hwmon {
fn find(device: &Path) -> Self {
let dir = device.join("hwmon");
let Some(entry) = sorted_entries(&dir).into_iter().next() else {
return Self::default();
};
let dir = dir.join(entry);
let read = |name: &str| read_u64(&dir.join(name));
Self {
temperature_c: read("temp1_input").map(|m| m as f32 / 1000.0),
power_w: read("power1_average")
.or_else(|| read("power1_input"))
.map(|uw| uw as f32 / 1e6),
power_cap_w: read("power1_cap").map(|uw| uw as f32 / 1e6),
fan_percent: read("pwm1").map(|pwm| pwm as f32 * 100.0 / 255.0),
clock_mhz: read("freq1_input").map(|hz| (hz / 1_000_000) as u32),
is_apu: read_trimmed(&dir.join("in1_label")).is_some_and(|l| l == "vddnb"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::linux::testutil::{symlink, write};
#[test]
fn finds_device_names_in_pci_ids() {
let ids = "# comment\n\
1002 Advanced Micro Devices, Inc. [AMD/ATI]\n\
\t15bf Phoenix1\n\
\t\t17aa 50d8 ThinkPad\n\
\t744c Navi 31 [Radeon RX 7900 XT/7900 XTX]\n\
1003 ULSI Systems\n\
\t0201 US201\n";
assert_eq!(
pci_device_name(ids, 0x1002, 0x15bf).as_deref(),
Some("Phoenix1")
);
assert_eq!(
pci_device_name(ids, 0x1002, 0x744c).as_deref(),
Some("Navi 31 [Radeon RX 7900 XT/7900 XTX]")
);
assert_eq!(pci_device_name(ids, 0x1002, 0x0201), None);
}
fn roots(root: &Path) -> SysRoots {
SysRoots {
sys: root.join("sys"),
proc: root.join("proc"),
}
}
fn card(
root: &Path,
card: &str,
pci: &str,
vendor: &str,
device: &str,
driver: &str,
) -> String {
let dev = format!("sys/devices/pci0000:00/{pci}");
write(root, &format!("{dev}/vendor"), &format!("{vendor}\n"));
write(root, &format!("{dev}/device"), &format!("{device}\n"));
write(root, &format!("sys/bus/pci/drivers/{driver}/.keep"), "");
symlink(
root,
&format!("{dev}/driver"),
&root.join(format!("sys/bus/pci/drivers/{driver}")),
);
symlink(
root,
&format!("sys/class/drm/{card}/device"),
&root.join(&dev),
);
dev
}
#[test]
fn reads_amd_apu_and_discrete_skipping_nvidia() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
card(root, "card0", "0000:01:00.0", "0x10de", "0x2684", "nvidia");
write(root, "sys/class/drm/card1-DP-1/status", "connected\n");
let apu = card(root, "card1", "0000:c3:00.0", "0x1002", "0x15bf", "amdgpu");
write(root, &format!("{apu}/product_name"), "Radeon 780M\n");
write(root, &format!("{apu}/gpu_busy_percent"), "12\n");
write(root, &format!("{apu}/mem_info_vram_used"), "1073741824\n");
write(root, &format!("{apu}/mem_info_vram_total"), "2147483648\n");
write(root, &format!("{apu}/hwmon/hwmon6/temp1_input"), "45000\n");
write(
root,
&format!("{apu}/hwmon/hwmon6/power1_input"),
"6000000\n",
);
write(
root,
&format!("{apu}/hwmon/hwmon6/freq1_input"),
"800000000\n",
);
write(root, &format!("{apu}/hwmon/hwmon6/in1_label"), "vddnb\n");
let dgpu = card(root, "card2", "0000:03:00.0", "0x1002", "0x744c", "amdgpu");
write(root, &format!("{dgpu}/gpu_busy_percent"), "99\n");
write(
root,
&format!("{dgpu}/pp_dpm_sclk"),
"0: 500Mhz\n1: 2500Mhz *\n",
);
write(
root,
&format!("{dgpu}/hwmon/hwmon9/power1_average"),
"250000000\n",
);
write(
root,
&format!("{dgpu}/hwmon/hwmon9/power1_cap"),
"300000000\n",
);
write(root, &format!("{dgpu}/hwmon/hwmon9/pwm1"), "51\n");
let gpus = DrmGpuReader::new().read(&roots(root), 1);
assert_eq!(gpus.len(), 2, "NVIDIA and connectors are skipped");
let apu = &gpus[0];
assert_eq!(apu.index, 1, "numbered after NVIDIA's");
assert_eq!(apu.name, "AMD Radeon 780M");
assert!(apu.integrated);
assert_eq!(apu.utilization_percent, Some(12.0));
assert_eq!(apu.memory_total_bytes, Some(2 << 30));
assert_eq!(apu.temperature_c, Some(45.0));
assert_eq!(apu.power_w, None, "an APU's reading is the whole SoC");
assert_eq!(apu.clock_mhz, Some(800));
let dgpu = &gpus[1];
assert!(!dgpu.integrated);
assert!(dgpu.name.starts_with("AMD "), "{}", dgpu.name);
assert_eq!(dgpu.power_w, Some(250.0));
assert_eq!(dgpu.power_limit_w, Some(300.0));
assert_eq!(dgpu.fan_percent, Some(20.0));
assert_eq!(
dgpu.clock_mhz,
Some(2500),
"from pp_dpm_sclk without hwmon freq"
);
}
#[test]
fn reads_intel_clock_without_utilization() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
card(root, "card0", "0000:00:02.0", "0x8086", "0x46a6", "i915");
write(root, "sys/class/drm/card0/gt_act_freq_mhz", "1300\n");
let gpus = DrmGpuReader::new().read(&roots(root), 0);
assert_eq!(gpus.len(), 1);
let igpu = &gpus[0];
assert_eq!(igpu.vendor, "Intel");
assert!(igpu.name.starts_with("Intel "), "{}", igpu.name);
assert!(igpu.integrated, "at 00:02.0");
assert_eq!(igpu.clock_mhz, Some(1300));
assert_eq!(igpu.utilization_percent, None);
}
}