use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use crate::backend::{BackendError, GpuBackend};
use crate::model::{
now_ms, DeviceId, DynamicSample, ProcessKind, ProcessSample, StaticInfo, ThrottleReasons,
Vendor,
};
fn read_parse<T: std::str::FromStr>(path: &Path) -> Option<T> {
fs::read_to_string(path).ok()?.trim().parse().ok()
}
fn read_trim(path: &Path) -> Option<String> {
fs::read_to_string(path).ok().map(|s| s.trim().to_string())
}
fn read_hex_id(path: &Path) -> Option<String> {
let s = read_trim(path)?;
let s = s.strip_prefix("0x").unwrap_or(&s).to_ascii_lowercase();
(!s.is_empty()).then_some(s)
}
fn dpm_line_mhz(line: &str) -> Option<u32> {
let after = line.split(':').nth(1)?.trim_start();
let digits: String = after.chars().take_while(|c| c.is_ascii_digit()).collect();
digits.parse().ok()
}
fn dpm_current_mhz(table: &str) -> Option<u32> {
table
.lines()
.find(|l| l.contains('*'))
.and_then(dpm_line_mhz)
}
fn dpm_max_mhz(table: &str) -> Option<u32> {
table
.lines()
.rev()
.find(|l| !l.trim().is_empty())
.and_then(dpm_line_mhz)
}
fn edge_temp_c(hwmon: &Path) -> Option<f32> {
let millic = edge_temp_millic(hwmon)?;
Some(millic as f32 / 1000.0)
}
fn edge_temp_millic(hwmon: &Path) -> Option<i64> {
if let Ok(entries) = fs::read_dir(hwmon) {
for entry in entries.flatten() {
let name = entry.file_name();
let Some(stem) = name.to_str().and_then(|n| n.strip_suffix("_label")) else {
continue;
};
if !stem.starts_with("temp") {
continue;
}
if read_trim(&entry.path()).as_deref() == Some("edge") {
if let Some(millic) = read_parse(&hwmon.join(format!("{stem}_input"))) {
return Some(millic);
}
}
}
}
read_parse(&hwmon.join("temp1_input"))
}
fn power_mw(hwmon: &Path) -> Option<u32> {
let uw: u64 = read_parse(&hwmon.join("power1_average"))
.or_else(|| read_parse(&hwmon.join("power1_input")))?;
Some((uw / 1000) as u32)
}
fn power_cap_mw(hwmon: &Path) -> Option<u32> {
let uw: u64 = read_parse(&hwmon.join("power1_cap"))?;
Some((uw / 1000) as u32)
}
fn fan_pct(hwmon: &Path) -> Option<f32> {
let rpm: f32 = read_parse(&hwmon.join("fan1_input"))?;
let max: f32 = read_parse(&hwmon.join("fan1_max"))?;
fan_pct_of_max(rpm, max)
}
fn fan_pct_of_max(rpm: f32, max: f32) -> Option<f32> {
if !rpm.is_finite() || !max.is_finite() || rpm < 0.0 || max <= 0.0 {
return None;
}
Some((rpm / max * 100.0).clamp(0.0, 100.0))
}
const HEADER_LEN: usize = 4;
fn read_u32_le(buf: &[u8], off: usize) -> Option<u32> {
let bytes = buf.get(off..off.checked_add(4)?)?;
Some(u32::from_le_bytes(bytes.try_into().ok()?))
}
fn read_u64_le(buf: &[u8], off: usize) -> Option<u64> {
let bytes = buf.get(off..off.checked_add(8)?)?;
Some(u64::from_le_bytes(bytes.try_into().ok()?))
}
const SMU_THROTTLER_PPT0_BIT: u32 = 0;
const SMU_THROTTLER_PPT1_BIT: u32 = 1;
const SMU_THROTTLER_PPT2_BIT: u32 = 2;
const SMU_THROTTLER_PPT3_BIT: u32 = 3;
const SMU_THROTTLER_SPL_BIT: u32 = 4;
const SMU_THROTTLER_FPPT_BIT: u32 = 5;
const SMU_THROTTLER_SPPT_BIT: u32 = 6;
const SMU_THROTTLER_SPPT_APU_BIT: u32 = 7;
const SMU_THROTTLER_TDC_GFX_BIT: u32 = 16;
const SMU_THROTTLER_TDC_SOC_BIT: u32 = 17;
const SMU_THROTTLER_TDC_MEM_BIT: u32 = 18;
const SMU_THROTTLER_TDC_VDD_BIT: u32 = 19;
const SMU_THROTTLER_TDC_CVIP_BIT: u32 = 20;
const SMU_THROTTLER_EDC_CPU_BIT: u32 = 21;
const SMU_THROTTLER_EDC_GFX_BIT: u32 = 22;
const SMU_THROTTLER_APCC_BIT: u32 = 23;
const SMU_THROTTLER_TEMP_GPU_BIT: u32 = 32;
const SMU_THROTTLER_TEMP_CORE_BIT: u32 = 33;
const SMU_THROTTLER_TEMP_MEM_BIT: u32 = 34;
const SMU_THROTTLER_TEMP_EDGE_BIT: u32 = 35;
const SMU_THROTTLER_TEMP_HOTSPOT_BIT: u32 = 36;
const SMU_THROTTLER_TEMP_SOC_BIT: u32 = 37;
const SMU_THROTTLER_TEMP_VR_GFX_BIT: u32 = 38;
const SMU_THROTTLER_TEMP_VR_SOC_BIT: u32 = 39;
const SMU_THROTTLER_TEMP_VR_MEM0_BIT: u32 = 40;
const SMU_THROTTLER_TEMP_VR_MEM1_BIT: u32 = 41;
const SMU_THROTTLER_TEMP_LIQUID0_BIT: u32 = 42;
const SMU_THROTTLER_TEMP_LIQUID1_BIT: u32 = 43;
const SMU_THROTTLER_VRHOT0_BIT: u32 = 44;
const SMU_THROTTLER_VRHOT1_BIT: u32 = 45;
const SMU_THROTTLER_PROCHOT_CPU_BIT: u32 = 46;
const SMU_THROTTLER_PROCHOT_GFX_BIT: u32 = 47;
const SMU_THROTTLER_PPM_BIT: u32 = 56;
const SMU_THROTTLER_FIT_BIT: u32 = 57;
const INDEP_THERMAL_MASK: u64 = (1 << SMU_THROTTLER_TEMP_GPU_BIT)
| (1 << SMU_THROTTLER_TEMP_CORE_BIT)
| (1 << SMU_THROTTLER_TEMP_MEM_BIT)
| (1 << SMU_THROTTLER_TEMP_EDGE_BIT)
| (1 << SMU_THROTTLER_TEMP_HOTSPOT_BIT)
| (1 << SMU_THROTTLER_TEMP_SOC_BIT)
| (1 << SMU_THROTTLER_TEMP_VR_GFX_BIT)
| (1 << SMU_THROTTLER_TEMP_VR_SOC_BIT)
| (1 << SMU_THROTTLER_TEMP_VR_MEM0_BIT)
| (1 << SMU_THROTTLER_TEMP_VR_MEM1_BIT)
| (1 << SMU_THROTTLER_TEMP_LIQUID0_BIT)
| (1 << SMU_THROTTLER_TEMP_LIQUID1_BIT)
| (1 << SMU_THROTTLER_VRHOT0_BIT)
| (1 << SMU_THROTTLER_VRHOT1_BIT);
const INDEP_POWER_MASK: u64 = (1 << SMU_THROTTLER_PPT0_BIT)
| (1 << SMU_THROTTLER_PPT1_BIT)
| (1 << SMU_THROTTLER_PPT2_BIT)
| (1 << SMU_THROTTLER_PPT3_BIT)
| (1 << SMU_THROTTLER_SPL_BIT)
| (1 << SMU_THROTTLER_FPPT_BIT)
| (1 << SMU_THROTTLER_SPPT_BIT)
| (1 << SMU_THROTTLER_SPPT_APU_BIT)
| (1 << SMU_THROTTLER_TDC_GFX_BIT)
| (1 << SMU_THROTTLER_TDC_SOC_BIT)
| (1 << SMU_THROTTLER_TDC_MEM_BIT)
| (1 << SMU_THROTTLER_TDC_VDD_BIT)
| (1 << SMU_THROTTLER_TDC_CVIP_BIT)
| (1 << SMU_THROTTLER_EDC_CPU_BIT)
| (1 << SMU_THROTTLER_EDC_GFX_BIT);
const INDEP_HW_SLOWDOWN_MASK: u64 =
(1 << SMU_THROTTLER_PROCHOT_CPU_BIT) | (1 << SMU_THROTTLER_PROCHOT_GFX_BIT);
const INDEP_OTHER_MASK: u64 =
(1 << SMU_THROTTLER_APCC_BIT) | (1 << SMU_THROTTLER_PPM_BIT) | (1 << SMU_THROTTLER_FIT_BIT);
fn map_indep_throttle(bits: u64) -> ThrottleReasons {
let known = INDEP_THERMAL_MASK | INDEP_POWER_MASK | INDEP_HW_SLOWDOWN_MASK | INDEP_OTHER_MASK;
ThrottleReasons {
thermal: bits & INDEP_THERMAL_MASK != 0,
power_cap: bits & INDEP_POWER_MASK != 0,
hw_slowdown: bits & INDEP_HW_SLOWDOWN_MASK != 0,
sync_boost: false,
other: (bits & INDEP_OTHER_MASK != 0) || (bits & !known != 0),
}
}
struct ThrottleLayout {
indep: Option<usize>,
legacy: Option<usize>,
size: usize,
}
fn throttle_layout(format: u8, content: u8) -> Option<ThrottleLayout> {
match (format, content) {
(1, 1) => Some(ThrottleLayout {
indep: None,
legacy: Some(68),
size: 96,
}),
(1, 2) => Some(ThrottleLayout {
indep: None,
legacy: Some(68),
size: 104,
}),
(1, 3) => Some(ThrottleLayout {
indep: Some(112),
legacy: Some(68),
size: 120,
}),
(2, 0) => Some(ThrottleLayout {
indep: None,
legacy: Some(112),
size: 120,
}),
(2, 1) => Some(ThrottleLayout {
indep: None,
legacy: Some(108),
size: 120,
}),
(2, 2) => Some(ThrottleLayout {
indep: Some(120),
legacy: Some(108),
size: 128,
}),
(2, 3) => Some(ThrottleLayout {
indep: Some(120),
legacy: Some(108),
size: 152,
}),
(2, 4) => Some(ThrottleLayout {
indep: Some(120),
legacy: Some(108),
size: 168,
}),
_ => None,
}
}
pub fn decode_gpu_metrics_throttle(buf: &[u8]) -> Option<ThrottleReasons> {
if buf.len() < HEADER_LEN {
return None;
}
let structure_size = u16::from_le_bytes([buf[0], buf[1]]) as usize;
let format = buf[2];
let content = buf[3];
if structure_size < HEADER_LEN || buf.len() < structure_size {
return None;
}
if let Some(layout) = throttle_layout(format, content) {
if structure_size != layout.size {
return None;
}
if let Some(off) = layout.indep {
let bits = read_u64_le(buf, off)?;
if bits != u64::MAX {
return Some(map_indep_throttle(bits));
}
}
if let Some(off) = layout.legacy {
let v = read_u32_le(buf, off)?;
if v != u32::MAX {
return Some(ThrottleReasons {
other: v != 0,
..Default::default()
});
}
}
return None;
}
if (format, content) == (3, 0) {
return None;
}
None
}
#[derive(Default)]
struct FdinfoDrm {
pdev: Option<String>,
vram_kib: Option<u64>,
gfx_ns: Option<u64>,
compute_ns: Option<u64>,
}
impl FdinfoDrm {
fn merge_max(&mut self, other: FdinfoDrm) {
fn mx(a: &mut Option<u64>, b: Option<u64>) {
*a = match (*a, b) {
(Some(x), Some(y)) => Some(x.max(y)),
(x, y) => x.or(y),
};
}
mx(&mut self.vram_kib, other.vram_kib);
mx(&mut self.gfx_ns, other.gfx_ns);
mx(&mut self.compute_ns, other.compute_ns);
}
}
fn parse_fdinfo(contents: &str) -> FdinfoDrm {
let mut out = FdinfoDrm::default();
for line in contents.lines() {
let Some((key, val)) = line.split_once(':') else {
continue;
};
let val = val.trim();
match key.trim() {
"drm-pdev" => out.pdev = Some(val.to_ascii_lowercase()),
"drm-memory-vram" => out.vram_kib = parse_suffixed(val, "KiB"),
"drm-engine-gfx" => out.gfx_ns = parse_suffixed(val, "ns"),
"drm-engine-compute" => out.compute_ns = parse_suffixed(val, "ns"),
_ => {}
}
}
out
}
fn parse_suffixed(val: &str, unit: &str) -> Option<u64> {
val.strip_suffix(unit)?.trim().parse().ok()
}
fn kib_to_bytes(kib: u64) -> Option<u64> {
kib.checked_mul(1024)
}
fn engine_util_pct(prev_ns: u64, prev_ts_ms: u64, cur_ns: u64, cur_ts_ms: u64) -> Option<f32> {
let wall_ms = cur_ts_ms.checked_sub(prev_ts_ms)?;
if wall_ms == 0 {
return None;
}
let busy_ns = cur_ns.checked_sub(prev_ns)?;
let pct = busy_ns as f64 / (wall_ms as f64 * 1_000_000.0) * 100.0;
Some(pct.min(100.0) as f32)
}
fn status_grants_full_proc_scan(status: &str) -> bool {
const CAP_SYS_PTRACE: u32 = 19;
for line in status.lines() {
if let Some(uids) = line.strip_prefix("Uid:") {
if uids.split_whitespace().nth(1) == Some("0") {
return true;
}
}
if let Some(mask) = line.strip_prefix("CapEff:") {
if let Ok(bits) = u64::from_str_radix(mask.trim(), 16) {
if bits & (1 << CAP_SYS_PTRACE) != 0 {
return true;
}
}
}
}
false
}
fn fdinfo_process_hint(root: &Path) -> Option<String> {
let full = fs::read_to_string(root.join("proc/self/status"))
.is_ok_and(|s| status_grants_full_proc_scan(&s));
(!full)
.then(|| "showing your processes only — others need root or CAP_SYS_PTRACE (fdinfo)".into())
}
fn amdgpu_ids_name(ids: &str, device: &str, revision: &str) -> Option<String> {
for line in ids.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let mut fields = line.splitn(3, ',');
let (Some(dev), Some(rev), Some(name)) = (fields.next(), fields.next(), fields.next())
else {
continue;
};
if dev.trim().eq_ignore_ascii_case(device) && rev.trim().eq_ignore_ascii_case(revision) {
let name = name.trim();
if !name.is_empty() {
return Some(name.to_string());
}
}
}
None
}
fn gpu_name(root: &Path, dev_path: &Path) -> String {
let device = read_hex_id(&dev_path.join("device"));
if let (Some(dev_id), Some(rev_id)) = (&device, read_hex_id(&dev_path.join("revision"))) {
if let Ok(ids) = fs::read_to_string(root.join("usr/share/libdrm/amdgpu.ids")) {
if let Some(name) = amdgpu_ids_name(&ids, dev_id, &rev_id) {
return name;
}
}
}
match device {
Some(id) => format!("AMD GPU [1002:{id}]"),
None => "AMD GPU".into(),
}
}
fn comm_name(root: &Path, pid: u32) -> String {
if let Ok(comm) = fs::read_to_string(root.join(format!("proc/{pid}/comm"))) {
let comm = comm.trim();
if !comm.is_empty() {
return comm.to_string();
}
}
format!("pid {pid}")
}
fn pci_slot_name(dev_path: &Path) -> Option<String> {
fs::read_to_string(dev_path.join("uevent"))
.ok()?
.lines()
.find_map(|l| l.strip_prefix("PCI_SLOT_NAME="))
.map(|s| s.trim().to_ascii_lowercase())
.filter(|s| !s.is_empty())
}
fn first_hwmon(dev_path: &Path) -> Option<PathBuf> {
let entries = fs::read_dir(dev_path.join("hwmon")).ok()?;
let mut dirs: Vec<PathBuf> = entries.flatten().map(|e| e.path()).collect();
dirs.sort(); dirs.into_iter().next()
}
struct AmdDevice {
id: DeviceId,
dev_path: PathBuf,
hwmon: Option<PathBuf>,
}
fn discover(root: &Path) -> Vec<AmdDevice> {
let Ok(entries) = fs::read_dir(root.join("sys/class/drm")) else {
return Vec::new();
};
let mut cards: Vec<(u32, PathBuf)> = entries
.flatten()
.filter_map(|e| {
let name = e.file_name().into_string().ok()?;
let idx: u32 = name.strip_prefix("card")?.parse().ok()?;
Some((idx, e.path().join("device")))
})
.collect();
cards.sort_by_key(|(idx, _)| *idx);
let mut devs = Vec::new();
for (_, dev_path) in cards {
if read_trim(&dev_path.join("vendor")).as_deref() != Some("0x1002") {
continue;
}
let Some(pci) = pci_slot_name(&dev_path) else {
continue;
};
let hwmon = first_hwmon(&dev_path);
devs.push(AmdDevice {
id: DeviceId(pci),
dev_path,
hwmon,
});
}
devs
}
pub struct AmdBackend {
root: PathBuf,
devs: Vec<AmdDevice>,
last_gfx: HashMap<(DeviceId, u32), (u64, u64)>,
process_hint: Option<String>,
#[cfg(target_os = "linux")]
cpu: crate::proc_meta::CpuTracker,
}
impl AmdBackend {
pub fn init() -> Result<Self, BackendError> {
Self::with_root("/")
}
pub fn with_root(root: impl Into<PathBuf>) -> Result<Self, BackendError> {
let root = root.into();
let devs = discover(&root);
if devs.is_empty() {
return Err(BackendError::Unavailable(
"no amdgpu devices under sys/class/drm".into(),
));
}
let process_hint = fdinfo_process_hint(&root);
Ok(Self {
root,
devs,
last_gfx: HashMap::new(),
process_hint,
#[cfg(target_os = "linux")]
cpu: crate::proc_meta::CpuTracker::new(),
})
}
fn device(&self, dev: &DeviceId) -> Result<&AmdDevice, BackendError> {
self.devs
.iter()
.find(|d| &d.id == dev)
.ok_or_else(|| BackendError::DeviceNotFound(dev.clone()))
}
}
impl GpuBackend for AmdBackend {
fn name(&self) -> &'static str {
"amd"
}
fn devices(&mut self) -> Vec<DeviceId> {
self.devs.iter().map(|d| d.id.clone()).collect()
}
fn static_info(&mut self, dev: &DeviceId) -> Result<StaticInfo, BackendError> {
let d = self.device(dev)?;
let p = &d.dev_path;
Ok(StaticInfo {
id: dev.clone(),
vendor: Vendor::Amd,
name: gpu_name(&self.root, p),
backend: "amd".into(),
mem_total_bytes: read_parse(&p.join("mem_info_vram_total")),
power_limit_mw: d.hwmon.as_deref().and_then(power_cap_mw),
max_sm_clock_mhz: fs::read_to_string(p.join("pp_dpm_sclk"))
.ok()
.as_deref()
.and_then(dpm_max_mhz),
temp_slowdown_c: None,
driver_version: None,
process_hint: self.process_hint.clone(),
source_caveat: None,
})
}
fn refresh_dynamic(&mut self, dev: &DeviceId) -> Result<DynamicSample, BackendError> {
let d = self.device(dev)?;
let p = &d.dev_path;
let hwmon = d.hwmon.as_deref();
Ok(DynamicSample {
ts_ms: now_ms(),
util_pct: read_parse(&p.join("gpu_busy_percent")),
util_engine: None, mem_used_bytes: read_parse(&p.join("mem_info_vram_used")),
power_mw: hwmon.and_then(power_mw),
temp_c: hwmon.and_then(edge_temp_c),
fan_pct: hwmon.and_then(fan_pct),
sm_clock_mhz: fs::read_to_string(p.join("pp_dpm_sclk"))
.ok()
.as_deref()
.and_then(dpm_current_mhz),
mem_clock_mhz: fs::read_to_string(p.join("pp_dpm_mclk"))
.ok()
.as_deref()
.and_then(dpm_current_mhz),
encoder_pct: None,
decoder_pct: None,
throttle: fs::read(p.join("gpu_metrics"))
.ok()
.and_then(|buf| decode_gpu_metrics_throttle(&buf)),
})
}
fn refresh_processes(&mut self, dev: &DeviceId) -> Result<Vec<ProcessSample>, BackendError> {
let pci = self.device(dev)?.id.0.clone();
let ts = now_ms();
let mut by_pid: HashMap<u32, FdinfoDrm> = HashMap::new();
if let Ok(entries) = fs::read_dir(self.root.join("proc")) {
for entry in entries.flatten() {
let Some(pid) = entry
.file_name()
.to_str()
.and_then(|s| s.parse::<u32>().ok())
else {
continue;
};
let Ok(fds) = fs::read_dir(entry.path().join("fdinfo")) else {
continue;
};
for fd in fds.flatten() {
let Ok(contents) = fs::read_to_string(fd.path()) else {
continue;
};
let info = parse_fdinfo(&contents);
if info.pdev.as_deref() != Some(pci.as_str()) {
continue;
}
by_pid.entry(pid).or_default().merge_max(info);
}
}
}
let mut out: Vec<ProcessSample> = Vec::with_capacity(by_pid.len());
for (&pid, agg) in &by_pid {
let util_pct = agg.gfx_ns.and_then(|cur| {
let prev = self.last_gfx.insert((dev.clone(), pid), (cur, ts));
prev.and_then(|(p_ns, p_ts)| engine_util_pct(p_ns, p_ts, cur, ts))
});
out.push(ProcessSample {
pid,
name: comm_name(&self.root, pid),
kind: if agg.compute_ns.unwrap_or(0) > 0 {
ProcessKind::Compute
} else {
ProcessKind::Graphics
},
mem_bytes: agg.vram_kib.and_then(kib_to_bytes),
util_pct,
cpu_pct: None,
container: None,
});
}
self.last_gfx
.retain(|(d, pid), _| d != dev || by_pid.contains_key(pid));
#[cfg(target_os = "linux")]
{
for p in &mut out {
p.cpu_pct = self.cpu.sample(p.pid);
p.container = crate::proc_meta::container_of(p.pid);
}
let live: Vec<u32> = out.iter().map(|p| p.pid).collect();
self.cpu.prune(&live);
}
out.sort_by_key(|p| p.pid); Ok(out)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dpm_table_parses_current_and_max_levels() {
let table = "0: 500Mhz\n1: 1138Mhz *\n2: 2890Mhz\n";
assert_eq!(dpm_current_mhz(table), Some(1138));
assert_eq!(dpm_max_mhz(table), Some(2890));
assert_eq!(dpm_current_mhz("garbage"), None);
assert_eq!(dpm_max_mhz(""), None);
}
#[test]
fn fdinfo_unit_suffixes_are_mandatory() {
let blob = "drm-pdev:\t0000:03:00.0\ndrm-engine-gfx:\t123 ns\ndrm-memory-vram:\t456 KiB\n";
let f = parse_fdinfo(blob);
assert_eq!(f.pdev.as_deref(), Some("0000:03:00.0"));
assert_eq!(f.gfx_ns, Some(123));
assert_eq!(f.vram_kib, Some(456));
assert_eq!(f.compute_ns, None, "absent key stays None");
assert_eq!(parse_suffixed("456", "KiB"), None);
assert_eq!(parse_suffixed("456 MiB", "KiB"), None);
}
#[test]
fn engine_util_needs_baseline_and_handles_resets() {
assert_eq!(engine_util_pct(0, 0, 500_000_000, 1_000), Some(50.0));
assert_eq!(engine_util_pct(900, 0, 100, 1_000), None);
assert_eq!(engine_util_pct(0, 1_000, 100, 1_000), None);
assert_eq!(engine_util_pct(0, 0, 10_000_000_000, 1_000), Some(100.0));
}
#[test]
fn hostile_fdinfo_vram_cannot_panic_or_wrap() {
assert_eq!(kib_to_bytes(456), Some(466_944));
assert_eq!(kib_to_bytes(u64::MAX), None);
}
#[test]
fn fan_pct_rejects_non_physical_readings() {
assert_eq!(fan_pct_of_max(1650.0, 3300.0), Some(50.0));
assert_eq!(fan_pct_of_max(4000.0, 3300.0), Some(100.0));
assert_eq!(fan_pct_of_max(f32::NAN, f32::NAN), None);
assert_eq!(fan_pct_of_max(f32::INFINITY, 3300.0), None);
assert_eq!(fan_pct_of_max(1650.0, f32::NAN), None);
assert_eq!(fan_pct_of_max(-500.0, 3300.0), None);
assert_eq!(fan_pct_of_max(1650.0, 0.0), None);
}
#[test]
fn proc_status_privilege_detection() {
assert!(status_grants_full_proc_scan(
"Uid:\t1000\t0\t1000\t1000\nCapEff:\t0000000000000000"
));
assert!(status_grants_full_proc_scan(
"Uid:\t1000\t1000\t1000\t1000\nCapEff:\t0000000000080000"
));
assert!(!status_grants_full_proc_scan(
"Uid:\t1000\t1000\t1000\t1000\nCapEff:\t0000000000000000"
));
assert!(!status_grants_full_proc_scan(""));
}
fn build_v1_3(indep: u64) -> Vec<u8> {
let mut b = vec![0u8; 120];
b[0..2].copy_from_slice(&120u16.to_le_bytes()); b[2] = 1; b[3] = 3; b[68..72].copy_from_slice(&0x0000_0080u32.to_le_bytes()); b[104..112].copy_from_slice(&(1u64 << 32).to_le_bytes()); b[112..120].copy_from_slice(&indep.to_le_bytes()); b
}
fn build_v2_3(indep: u64) -> Vec<u8> {
let mut b = vec![0u8; 152];
b[0..2].copy_from_slice(&152u16.to_le_bytes());
b[2] = 2;
b[3] = 3;
b[108..112].copy_from_slice(&0x0000_0080u32.to_le_bytes()); b[112..120].copy_from_slice(&(1u64 << 32).to_le_bytes()); b[120..128].copy_from_slice(&indep.to_le_bytes());
b
}
fn build_v2_1(throttle_status: u32) -> Vec<u8> {
let mut b = vec![0u8; 120];
b[0..2].copy_from_slice(&120u16.to_le_bytes());
b[2] = 2;
b[3] = 1;
b[112..114].copy_from_slice(&0xBEEFu16.to_le_bytes()); b[108..112].copy_from_slice(&throttle_status.to_le_bytes());
b
}
fn build_v2_2(indep: u64, legacy: u32) -> Vec<u8> {
let mut b = vec![0u8; 128];
b[0..2].copy_from_slice(&128u16.to_le_bytes());
b[2] = 2;
b[3] = 2;
b[108..112].copy_from_slice(&legacy.to_le_bytes());
b[112..114].copy_from_slice(&0xBEEFu16.to_le_bytes()); b[120..128].copy_from_slice(&indep.to_le_bytes());
b
}
fn build_v2_4(indep: u64, legacy: u32) -> Vec<u8> {
let mut b = vec![0u8; 168];
b[0..2].copy_from_slice(&168u16.to_le_bytes());
b[2] = 2;
b[3] = 4;
b[108..112].copy_from_slice(&legacy.to_le_bytes()); b[112..120].copy_from_slice(&(1u64 << 32).to_le_bytes()); b[120..128].copy_from_slice(&indep.to_le_bytes()); b[152..164].copy_from_slice(&[0xAB; 12]); b[164..168].copy_from_slice(&[0xFF; 4]); b
}
fn build_v3_0(prochot: u32, spl: u32, thm_gfx: u32) -> Vec<u8> {
let mut b = vec![0u8; 264];
b[0..2].copy_from_slice(&264u16.to_le_bytes());
b[2] = 3;
b[3] = 0;
b[224..226].copy_from_slice(&2900u16.to_le_bytes()); b[226..228].copy_from_slice(&[0xFF, 0xFF]); b[228..232].copy_from_slice(&prochot.to_le_bytes()); b[232..236].copy_from_slice(&spl.to_le_bytes()); b[248..252].copy_from_slice(&thm_gfx.to_le_bytes()); b
}
#[test]
fn indep_thermal_bit_decodes_to_thermal_only() {
let t = decode_gpu_metrics_throttle(&build_v1_3(1 << SMU_THROTTLER_TEMP_HOTSPOT_BIT))
.expect("well-formed v1_3 must decode");
assert!(t.thermal);
assert!(!t.power_cap && !t.hw_slowdown && !t.sync_boost && !t.other);
}
#[test]
fn indep_ppt_bit_decodes_to_power_cap_only() {
let t = decode_gpu_metrics_throttle(&build_v1_3(1 << SMU_THROTTLER_PPT0_BIT))
.expect("well-formed v1_3 must decode");
assert!(t.power_cap);
assert!(!t.thermal && !t.hw_slowdown && !t.sync_boost && !t.other);
}
#[test]
fn indep_prochot_bit_decodes_to_hw_slowdown() {
let t = decode_gpu_metrics_throttle(&build_v1_3(1 << SMU_THROTTLER_PROCHOT_GFX_BIT))
.expect("well-formed v1_3 must decode");
assert!(t.hw_slowdown);
assert!(!t.thermal && !t.power_cap && !t.sync_boost && !t.other);
}
#[test]
fn indep_combined_bits_decode_to_multiple_reasons() {
let bits = (1 << SMU_THROTTLER_TEMP_MEM_BIT)
| (1 << SMU_THROTTLER_TDC_GFX_BIT)
| (1 << SMU_THROTTLER_PROCHOT_CPU_BIT)
| (1u64 << 60);
let t =
decode_gpu_metrics_throttle(&build_v2_3(bits)).expect("well-formed v2_3 must decode");
assert!(t.thermal && t.power_cap && t.hw_slowdown);
assert!(t.other);
assert!(!t.sync_boost);
}
#[test]
fn legacy_only_nonzero_is_coarse_other_never_a_guessed_cause() {
let t = decode_gpu_metrics_throttle(&build_v2_1(0x0000_00FF))
.expect("well-formed v2_1 must decode");
assert!(t.other);
assert!(!t.thermal && !t.power_cap && !t.hw_slowdown && !t.sync_boost);
assert_eq!(
decode_gpu_metrics_throttle(&build_v2_1(0)),
Some(ThrottleReasons::default())
);
}
#[test]
fn v2_4_decodes_at_kernel_sizeof_168() {
let t = decode_gpu_metrics_throttle(&build_v2_4(1 << SMU_THROTTLER_SPPT_APU_BIT, 0x40))
.expect("a real 168-byte v2_4 blob must decode");
assert!(t.power_cap);
assert!(!t.thermal && !t.hw_slowdown && !t.sync_boost && !t.other);
}
#[test]
fn v2_4_blob_claiming_164_is_rejected() {
let mut b = build_v2_4(1 << SMU_THROTTLER_SPPT_APU_BIT, 0);
b.truncate(164);
b[0..2].copy_from_slice(&164u16.to_le_bytes());
assert_eq!(
decode_gpu_metrics_throttle(&b),
None,
"a structure_size of 164 does not match v2_4's real sizeof"
);
}
#[test]
fn indep_all_ff_sentinel_falls_through_to_legacy() {
assert_eq!(
decode_gpu_metrics_throttle(&build_v2_2(u64::MAX, 0)),
Some(ThrottleReasons::default())
);
let t = decode_gpu_metrics_throttle(&build_v2_2(u64::MAX, 0x2))
.expect("legacy word must still decode behind an indep sentinel");
assert!(t.other);
assert!(!t.thermal && !t.power_cap && !t.hw_slowdown && !t.sync_boost);
let t = decode_gpu_metrics_throttle(&build_v1_3(u64::MAX))
.expect("v1_3 legacy word must decode behind an indep sentinel");
assert!(t.other);
assert!(!t.thermal && !t.power_cap && !t.hw_slowdown && !t.sync_boost);
}
#[test]
fn all_words_sentinel_decodes_to_none() {
assert_eq!(
decode_gpu_metrics_throttle(&build_v2_2(u64::MAX, u32::MAX)),
None
);
}
#[test]
fn legacy_only_sentinel_decodes_to_none() {
assert_eq!(decode_gpu_metrics_throttle(&build_v2_1(u32::MAX)), None);
}
#[test]
fn v3_residency_accumulators_never_assert_per_sample_throttle() {
assert_eq!(decode_gpu_metrics_throttle(&build_v3_0(37, 9, 12)), None);
assert_eq!(decode_gpu_metrics_throttle(&build_v3_0(0, 0, 0)), None);
assert_eq!(
decode_gpu_metrics_throttle(&build_v3_0(u32::MAX, u32::MAX, u32::MAX)),
None
);
}
#[test]
fn unknown_revision_decodes_to_none() {
let mut b = build_v1_3(1 << SMU_THROTTLER_PPT0_BIT);
b[2] = 9;
b[3] = 9;
assert_eq!(
decode_gpu_metrics_throttle(&b),
None,
"unknown revision must be unobservable, not quiet"
);
}
#[test]
fn truncated_blob_decodes_to_none_without_panic() {
let full = build_v1_3(1 << SMU_THROTTLER_PPT0_BIT);
for len in 0..full.len() {
assert_eq!(
decode_gpu_metrics_throttle(&full[..len]),
None,
"a {len}-byte prefix of a v1_3 blob must decode to None"
);
}
assert_eq!(decode_gpu_metrics_throttle(&[]), None);
}
#[test]
fn lying_structure_size_decodes_to_none() {
let mut b = build_v1_3(1 << SMU_THROTTLER_PPT0_BIT);
b[0..2].copy_from_slice(&100u16.to_le_bytes()); assert_eq!(
decode_gpu_metrics_throttle(&b),
None,
"a structure_size that disagrees with the version size is not trusted"
);
let mut b = build_v1_3(1 << SMU_THROTTLER_PPT0_BIT);
b[0..2].copy_from_slice(&200u16.to_le_bytes());
assert_eq!(decode_gpu_metrics_throttle(&b), None);
}
#[test]
fn amdgpu_ids_lookup_is_keyed_by_device_and_revision() {
let ids = "# header\n1.0.0\n744C,\tC8,\tAMD Radeon RX 7900 XTX\n744C,\tCC,\tAMD Radeon RX 7900 XT\n";
assert_eq!(
amdgpu_ids_name(ids, "744c", "c8").as_deref(),
Some("AMD Radeon RX 7900 XTX")
);
assert_eq!(
amdgpu_ids_name(ids, "744c", "cc").as_deref(),
Some("AMD Radeon RX 7900 XT")
);
assert_eq!(amdgpu_ids_name(ids, "744c", "ff"), None);
}
}