diff --git a/stats-daemon/src/gpu.rs b/stats-daemon/src/gpu.rs index 5d114b9..0b52466 100644 --- a/stats-daemon/src/gpu.rs +++ b/stats-daemon/src/gpu.rs @@ -1,5 +1,6 @@ use std::fs; use std::io::Write; +use std::time::Instant; pub struct GpuInfo { pub usage: u32, @@ -15,16 +16,41 @@ pub enum GpuBackend { hwmon_path: Option, }, Nvidia, + Intel { + card_path: String, + device_path: String, + hwmon_path: Option, + prev_rc6: Option<(u64, Instant)>, + }, None, } +fn read_sysfs(path: &str) -> Option { + fs::read_to_string(path).ok().map(|s| s.trim().to_string()) +} + +fn read_sysfs_u64(path: &str) -> Option { + read_sysfs(path)?.parse().ok() +} + +fn find_hwmon(driver_name: &str) -> Option { + for i in 0..32 { + if let Some(name) = read_sysfs(&format!("/sys/class/hwmon/hwmon{i}/name")) { + if name == driver_name { + return Some(format!("/sys/class/hwmon/hwmon{i}")); + } + } + } + None +} + pub fn detect_gpu() -> GpuBackend { // AMD: look for gpu_busy_percent exposed by the amdgpu driver for i in 0..8 { let p = format!("/sys/class/drm/card{i}/device/gpu_busy_percent"); if fs::read_to_string(&p).is_ok() { let card = format!("/sys/class/drm/card{i}/device"); - let hwmon = find_amd_hwmon(); + let hwmon = find_hwmon("amdgpu"); return GpuBackend::Amd { card_path: card, hwmon_path: hwmon, @@ -40,38 +66,42 @@ pub fn detect_gpu() -> GpuBackend { if nvidia_ok { return GpuBackend::Nvidia; } + // Intel: look for i915 or xe driver + for i in 0..8 { + let driver_link = format!("/sys/class/drm/card{i}/device/driver"); + if let Some(drv) = fs::read_link(&driver_link) + .ok() + .and_then(|p| p.file_name().map(|n| n.to_string_lossy().into_owned())) + { + if drv == "i915" || drv == "xe" { + let card_path = format!("/sys/class/drm/card{i}"); + let device_path = format!("/sys/class/drm/card{i}/device"); + let hwmon = find_hwmon(&drv); + return GpuBackend::Intel { + card_path, + device_path, + hwmon_path: hwmon, + prev_rc6: None, + }; + } + } + } GpuBackend::None } -fn find_amd_hwmon() -> Option { - for i in 0..32 { - let name = format!("/sys/class/hwmon/hwmon{i}/name"); - if fs::read_to_string(&name).ok()?.trim() == "amdgpu" { - return Some(format!("/sys/class/hwmon/hwmon{i}")); - } - } - None -} - fn read_amd(card: &str, hwmon: Option<&String>) -> Option { - let usage: u32 = fs::read_to_string(format!("{card}/gpu_busy_percent")) - .ok()? - .trim() + let usage: u32 = read_sysfs(&format!("{card}/gpu_busy_percent"))? .parse() .ok()?; - let vram_used: u64 = fs::read_to_string(format!("{card}/mem_info_vram_used")) - .ok()? - .trim() + let vram_used: u64 = read_sysfs(&format!("{card}/mem_info_vram_used"))? .parse() .ok()?; - let vram_total: u64 = fs::read_to_string(format!("{card}/mem_info_vram_total")) - .ok()? - .trim() + let vram_total: u64 = read_sysfs(&format!("{card}/mem_info_vram_total"))? .parse() .ok()?; let temp_c = hwmon - .and_then(|h| fs::read_to_string(format!("{h}/temp1_input")).ok()) - .and_then(|s| s.trim().parse::().ok()) + .and_then(|h| read_sysfs(&format!("{h}/temp1_input"))) + .and_then(|s| s.parse::().ok()) .map_or(0, |mc| mc / 1000); Some(GpuInfo { usage, @@ -107,13 +137,77 @@ fn read_nvidia() -> Option { }) } -pub fn emit_gpu(out: &mut impl Write, backend: &GpuBackend) { +fn read_intel( + card: &str, + device: &str, + hwmon: Option<&String>, + prev_rc6: &mut Option<(u64, Instant)>, +) -> GpuInfo { + let usage = read_intel_usage(card, prev_rc6).unwrap_or(0); + + // VRAM - only present on discrete GPUs (Arc) + let vram_total = read_sysfs_u64(&format!("{device}/mem_info_vram_total")).unwrap_or(0); + let vram_used = if vram_total > 0 { + read_sysfs_u64(&format!("{device}/mem_info_vram_used")).unwrap_or(0) + } else { + 0 + }; + + let temp_c = hwmon + .and_then(|h| read_sysfs(&format!("{h}/temp1_input"))) + .and_then(|s| s.parse::().ok()) + .map_or(0, |mc| mc / 1000); + + GpuInfo { + usage, + vram_used_gb: vram_used as f64 / 1_073_741_824.0, + vram_total_gb: vram_total as f64 / 1_073_741_824.0, + temp_c, + vendor: "intel", + } +} + +fn read_intel_usage(card: &str, prev_rc6: &mut Option<(u64, Instant)>) -> Option { + // RC6 is the GPU idle state - compute usage from residency delta + let rc6_ms = read_sysfs_u64(&format!("{card}/gt/gt0/rc6_residency_ms")) + .or_else(|| read_sysfs_u64(&format!("{card}/power/rc6_residency_ms")))?; + + let now = Instant::now(); + let usage = if let Some((prev_ms, prev_time)) = prev_rc6.take() { + let dt_ms = now.duration_since(prev_time).as_millis() as u64; + if dt_ms > 0 { + let idle_ms = rc6_ms.saturating_sub(prev_ms); + let idle_pct = (idle_ms * 100 / dt_ms).min(100); + (100 - idle_pct) as u32 + } else { + 0 + } + } else { + 0 // first reading, no delta yet + }; + + *prev_rc6 = Some((rc6_ms, now)); + Some(usage) +} + +pub fn emit_gpu(out: &mut impl Write, backend: &mut GpuBackend) { let info = match backend { GpuBackend::Amd { card_path, hwmon_path, } => read_amd(card_path, hwmon_path.as_ref()), GpuBackend::Nvidia => read_nvidia(), + GpuBackend::Intel { + card_path, + device_path, + hwmon_path, + prev_rc6, + } => Some(read_intel( + card_path, + device_path, + hwmon_path.as_ref(), + prev_rc6, + )), GpuBackend::None => return, }; if let Some(g) = info { diff --git a/stats-daemon/src/main.rs b/stats-daemon/src/main.rs index 66fa98d..d54847d 100644 --- a/stats-daemon/src/main.rs +++ b/stats-daemon/src/main.rs @@ -27,7 +27,7 @@ fn main() { let mut out = io::BufWriter::new(stdout.lock()); let mut prev: Vec = vec![]; let mut freqs: Vec = vec![]; - let gpu = gpu::detect_gpu(); + let mut gpu = gpu::detect_gpu(); let mut tick = 0u64; loop { @@ -43,11 +43,14 @@ fn main() { if tick.is_multiple_of(4) { temp::emit_temp(&mut out); - // AMD sysfs is instant; NVIDIA calls nvidia-smi so runs less often - match &gpu { - gpu::GpuBackend::Amd { .. } => gpu::emit_gpu(&mut out, &gpu), - gpu::GpuBackend::Nvidia if tick.is_multiple_of(8) => gpu::emit_gpu(&mut out, &gpu), - _ => {} + // AMD/Intel read sysfs (instant); NVIDIA shells out so runs less often + let emit = match &gpu { + gpu::GpuBackend::Amd { .. } | gpu::GpuBackend::Intel { .. } => true, + gpu::GpuBackend::Nvidia => tick.is_multiple_of(8), + gpu::GpuBackend::None => false, + }; + if emit { + gpu::emit_gpu(&mut out, &mut gpu); } }