use std::fs;
use std::path::{Path, PathBuf};
use std::time::Instant;
use anyhow::{Context, Result};
use crate::backend::{GpuBackend, require_devices};
use crate::model::{GpuInfo, GpuSample};
pub struct AmdGpuBackend {
adapters: Vec<AmdGpuAdapter>,
devices: Vec<GpuInfo>,
}
#[derive(Debug, Clone)]
struct AmdGpuAdapter {
info: GpuInfo,
device_path: PathBuf,
hwmon_path: Option<PathBuf>,
}
impl AmdGpuBackend {
pub fn new() -> Result<Self> {
let drm_path = Path::new("/sys/class/drm");
let entries = fs::read_dir(drm_path).with_context(|| "failed to read /sys/class/drm")?;
let mut adapters = Vec::new();
for entry in entries.flatten() {
let path = entry.path();
let Some(card_index) = card_index(&path) else {
continue;
};
let device_path = path.join("device");
if !is_amd_device(&device_path) {
continue;
}
let name = amd_device_name(card_index, &device_path);
let uuid = read_trimmed(device_path.join("unique_id")).ok();
let info = GpuInfo {
id: adapters.len(),
backend_index: card_index,
name,
uuid,
};
adapters.push(AmdGpuAdapter {
info,
hwmon_path: find_hwmon_path(&device_path),
device_path,
});
}
adapters.sort_by_key(|adapter| adapter.info.backend_index);
for (id, adapter) in adapters.iter_mut().enumerate() {
adapter.info.id = id;
}
let devices = adapters
.iter()
.map(|adapter| adapter.info.clone())
.collect::<Vec<_>>();
require_devices(&devices, "AMDGPU sysfs")?;
Ok(Self { adapters, devices })
}
}
impl GpuBackend for AmdGpuBackend {
fn label(&self) -> &str {
"AMDGPU sysfs"
}
fn devices(&self) -> &[GpuInfo] {
&self.devices
}
fn sample(&mut self) -> Result<Vec<GpuSample>> {
let at = Instant::now();
Ok(self
.adapters
.iter()
.map(|adapter| {
let vram_used_bytes = read_u64(adapter.device_path.join("mem_info_vram_used"));
let vram_total_bytes = read_u64(adapter.device_path.join("mem_info_vram_total"));
let mem_util_percent = read_f64(adapter.device_path.join("mem_busy_percent"));
GpuSample {
gpu_id: adapter.info.id,
at,
gpu_util_percent: read_f64(adapter.device_path.join("gpu_busy_percent")),
mem_util_percent,
vram_used_bytes,
vram_total_bytes,
power_watts: adapter.hwmon_path.as_deref().and_then(hwmon_power_watts),
power_limit_watts: adapter
.hwmon_path
.as_deref()
.and_then(hwmon_power_cap_watts),
temperature_celsius: adapter
.hwmon_path
.as_deref()
.and_then(hwmon_temperature_celsius),
fan_percent: adapter.hwmon_path.as_deref().and_then(hwmon_fan_percent),
graphics_clock_mhz: current_dpm_clock_mhz(
adapter.device_path.join("pp_dpm_sclk"),
),
memory_clock_mhz: current_dpm_clock_mhz(
adapter.device_path.join("pp_dpm_mclk"),
),
compute_processes: None,
processes: Vec::new(),
}
})
.collect())
}
}
fn card_index(path: &Path) -> Option<u32> {
let name = path.file_name()?.to_str()?;
let suffix = name.strip_prefix("card")?;
(!suffix.is_empty() && suffix.chars().all(|ch| ch.is_ascii_digit()))
.then(|| suffix.parse().ok())?
}
fn is_amd_device(device_path: &Path) -> bool {
matches!(
read_trimmed(device_path.join("vendor")).as_deref(),
Ok("0x1002")
)
}
fn amd_device_name(card_index: u32, device_path: &Path) -> String {
read_trimmed(device_path.join("product_name"))
.or_else(|_| read_name_from_uevent(device_path.join("uevent")))
.unwrap_or_else(|_| format!("AMD GPU card{card_index}"))
}
fn read_name_from_uevent(path: PathBuf) -> Result<String> {
let uevent = fs::read_to_string(path)?;
if let Some(driver) = uevent.lines().find_map(|line| line.strip_prefix("DRIVER="))
&& driver == "amdgpu"
{
return Ok("AMD GPU".to_owned());
}
if let Some(pci_id) = uevent.lines().find_map(|line| line.strip_prefix("PCI_ID=")) {
return Ok(format!("AMD GPU {pci_id}"));
}
anyhow::bail!("uevent did not contain an AMDGPU name");
}
fn find_hwmon_path(device_path: &Path) -> Option<PathBuf> {
let hwmon_root = device_path.join("hwmon");
let mut candidates = fs::read_dir(hwmon_root)
.ok()?
.flatten()
.map(|entry| entry.path())
.filter(|path| path.is_dir())
.collect::<Vec<_>>();
candidates.sort();
candidates.into_iter().next()
}
fn hwmon_power_watts(hwmon_path: &Path) -> Option<f64> {
read_u64(hwmon_path.join("power1_average"))
.or_else(|| read_u64(hwmon_path.join("power1_input")))
.map(microwatts_to_watts)
}
fn hwmon_power_cap_watts(hwmon_path: &Path) -> Option<f64> {
read_u64(hwmon_path.join("power1_cap")).map(microwatts_to_watts)
}
fn hwmon_temperature_celsius(hwmon_path: &Path) -> Option<f64> {
first_numbered_hwmon_file(hwmon_path, "temp", "_input")
.and_then(read_u64)
.map(|millidegrees| millidegrees as f64 / 1000.0)
}
fn hwmon_fan_percent(hwmon_path: &Path) -> Option<f64> {
read_u64(hwmon_path.join("pwm1")).map(|pwm| ((pwm as f64 / 255.0) * 100.0).clamp(0.0, 100.0))
}
fn first_numbered_hwmon_file(hwmon_path: &Path, prefix: &str, suffix: &str) -> Option<PathBuf> {
let mut candidates = fs::read_dir(hwmon_path)
.ok()?
.flatten()
.map(|entry| entry.path())
.filter(|path| {
let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
return false;
};
name.starts_with(prefix) && name.ends_with(suffix)
})
.collect::<Vec<_>>();
candidates.sort();
candidates.into_iter().next()
}
fn current_dpm_clock_mhz(path: PathBuf) -> Option<f64> {
parse_current_dpm_clock_mhz(&read_trimmed(path).ok()?)
}
fn parse_current_dpm_clock_mhz(contents: &str) -> Option<f64> {
contents
.lines()
.find(|line| line.contains('*'))
.and_then(clock_mhz_from_dpm_line)
}
fn clock_mhz_from_dpm_line(line: &str) -> Option<f64> {
let after_colon = line.split_once(':')?.1;
let number = after_colon
.chars()
.skip_while(|ch| ch.is_whitespace())
.take_while(|ch| ch.is_ascii_digit() || *ch == '.')
.collect::<String>();
number.parse().ok()
}
fn read_trimmed(path: impl AsRef<Path>) -> Result<String> {
Ok(fs::read_to_string(path)?.trim().to_owned())
}
fn read_u64(path: impl AsRef<Path>) -> Option<u64> {
read_trimmed(path).ok()?.parse().ok()
}
fn read_f64(path: impl AsRef<Path>) -> Option<f64> {
read_trimmed(path).ok()?.parse().ok()
}
fn microwatts_to_watts(microwatts: u64) -> f64 {
microwatts as f64 / 1_000_000.0
}
#[cfg(test)]
mod tests {
use super::{card_index, parse_current_dpm_clock_mhz};
use std::path::Path;
#[test]
fn card_index_only_accepts_primary_drm_cards() {
assert_eq!(card_index(Path::new("/sys/class/drm/card0")), Some(0));
assert_eq!(card_index(Path::new("/sys/class/drm/card12")), Some(12));
assert_eq!(card_index(Path::new("/sys/class/drm/card0-DP-1")), None);
assert_eq!(card_index(Path::new("/sys/class/drm/renderD128")), None);
}
#[test]
fn parses_current_dpm_clock() {
let contents = "0: 500Mhz\n1: 1200Mhz *\n";
assert_eq!(parse_current_dpm_clock_mhz(contents), Some(1200.0));
}
}