1use std::collections::HashMap;
17use std::fs;
18use std::path::Path;
19
20use super::{SysRoots, read_trimmed, read_u64, sorted_entries};
21use crate::snapshot::GpuStats;
22
23const VENDOR_AMD: u16 = 0x1002;
24const VENDOR_INTEL: u16 = 0x8086;
25
26const PCI_IDS: &[&str] = &[
28 "/usr/share/hwdata/pci.ids",
29 "/usr/share/misc/pci.ids",
30 "/usr/share/pci.ids",
31];
32
33#[derive(Default)]
35pub struct DrmGpuReader {
36 names: HashMap<(u16, u16), String>,
38}
39
40impl DrmGpuReader {
41 pub fn new() -> Self {
42 Self::default()
43 }
44
45 pub fn read(&mut self, roots: &SysRoots, first_index: u32) -> Vec<GpuStats> {
47 let class_dir = roots.sys.join("class/drm");
48 let mut gpus = Vec::new();
49 for entry in sorted_entries(&class_dir) {
50 let is_card = entry
52 .strip_prefix("card")
53 .is_some_and(|n| !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit()));
54 if !is_card {
55 continue;
56 }
57 let card = class_dir.join(&entry);
58 let device = card.join("device");
59 let Some(vendor) = read_hex(&device.join("vendor")) else {
60 continue;
61 };
62 let driver = fs::read_link(device.join("driver"))
63 .ok()
64 .and_then(|p| p.file_name().map(|n| n.to_string_lossy().into_owned()))
65 .unwrap_or_default();
66 let index = first_index + gpus.len() as u32;
67 let gpu = match (vendor, driver.as_str()) {
68 (VENDOR_AMD, "amdgpu") => Some(self.read_amd(&device, index)),
69 (VENDOR_INTEL, "i915" | "xe") => Some(self.read_intel(&card, &device, index)),
70 _ => None,
71 };
72 gpus.extend(gpu);
73 }
74 gpus
75 }
76
77 fn read_amd(&mut self, device: &Path, index: u32) -> GpuStats {
78 let hwmon = Hwmon::find(device);
79 let percent = |name: &str| read_u64(&device.join(name)).map(|v| v as f32);
80 let integrated = hwmon.is_apu;
81 GpuStats {
82 index,
83 vendor: "AMD".to_owned(),
84 name: self.name(device, VENDOR_AMD, "AMD"),
85 integrated,
86 utilization_percent: percent("gpu_busy_percent"),
87 memory_utilization_percent: percent("mem_busy_percent"),
88 memory_used_bytes: read_u64(&device.join("mem_info_vram_used")),
89 memory_total_bytes: read_u64(&device.join("mem_info_vram_total")),
90 temperature_c: hwmon.temperature_c,
91 power_w: hwmon.power_w.filter(|_| !integrated),
94 power_limit_w: hwmon.power_cap_w.filter(|_| !integrated),
95 fan_percent: hwmon.fan_percent,
96 clock_mhz: hwmon.clock_mhz.or_else(|| current_sclk(device)),
97 processes: Vec::new(),
98 }
99 }
100
101 fn read_intel(&mut self, card: &Path, device: &Path, index: u32) -> GpuStats {
102 let hwmon = Hwmon::find(device);
103 let clock_mhz = read_u64(&card.join("gt_act_freq_mhz"))
105 .or_else(|| read_u64(&device.join("tile0/gt0/freq0/act_freq")))
106 .map(|mhz| mhz as u32);
107 let integrated = fs::canonicalize(device)
109 .ok()
110 .and_then(|p| {
111 p.file_name()
112 .map(|n| n.to_string_lossy().ends_with(":00:02.0"))
113 })
114 .unwrap_or(false);
115 GpuStats {
116 index,
117 vendor: "Intel".to_owned(),
118 name: self.name(device, VENDOR_INTEL, "Intel"),
119 integrated,
120 temperature_c: hwmon.temperature_c,
121 power_w: hwmon.power_w,
122 power_limit_w: hwmon.power_cap_w,
123 fan_percent: hwmon.fan_percent,
124 clock_mhz,
125 ..Default::default()
126 }
127 }
128
129 fn name(&mut self, device: &Path, vendor: u16, vendor_name: &str) -> String {
132 if let Some(name) = read_trimmed(&device.join("product_name")).filter(|n| !n.is_empty()) {
133 return with_vendor(&name, vendor_name);
134 }
135 let id = read_hex(&device.join("device")).unwrap_or(0);
136 self.names
137 .entry((vendor, id))
138 .or_insert_with(|| {
139 let name = PCI_IDS
140 .iter()
141 .find_map(|path| fs::read_to_string(path).ok())
142 .and_then(|text| pci_device_name(&text, vendor, id))
143 .unwrap_or_else(|| format!("GPU {id:04x}"));
144 with_vendor(&name, vendor_name)
145 })
146 .clone()
147 }
148}
149
150fn with_vendor(name: &str, vendor: &str) -> String {
151 if name.starts_with(vendor) {
152 name.to_owned()
153 } else {
154 format!("{vendor} {name}")
155 }
156}
157
158fn read_hex(path: &Path) -> Option<u16> {
160 let text = read_trimmed(path)?;
161 u16::from_str_radix(text.trim_start_matches("0x"), 16).ok()
162}
163
164fn current_sclk(device: &Path) -> Option<u32> {
166 let text = fs::read_to_string(device.join("pp_dpm_sclk")).ok()?;
167 let line = text.lines().find(|l| l.trim_end().ends_with('*'))?;
168 let value = line.split_whitespace().nth(1)?;
169 value
170 .trim_end_matches("Mhz")
171 .trim_end_matches("MHz")
172 .parse()
173 .ok()
174}
175
176pub(crate) fn pci_device_name(text: &str, vendor: u16, device: u16) -> Option<String> {
179 let vendor_prefix = format!("{vendor:04x} ");
180 let device_prefix = format!("\t{device:04x} ");
181 let mut in_vendor = false;
182 for line in text.lines() {
183 if line.starts_with('#') || line.is_empty() {
184 continue;
185 }
186 if !line.starts_with('\t') {
187 if in_vendor {
188 return None;
189 }
190 in_vendor = line.starts_with(&vendor_prefix);
191 } else if in_vendor && let Some(name) = line.strip_prefix(&device_prefix) {
192 return Some(name.trim().to_owned());
193 }
194 }
195 None
196}
197
198#[derive(Default)]
200struct Hwmon {
201 temperature_c: Option<f32>,
202 power_w: Option<f32>,
203 power_cap_w: Option<f32>,
204 fan_percent: Option<f32>,
205 clock_mhz: Option<u32>,
206 is_apu: bool,
208}
209
210impl Hwmon {
211 fn find(device: &Path) -> Self {
212 let dir = device.join("hwmon");
213 let Some(entry) = sorted_entries(&dir).into_iter().next() else {
214 return Self::default();
215 };
216 let dir = dir.join(entry);
217 let read = |name: &str| read_u64(&dir.join(name));
218 Self {
219 temperature_c: read("temp1_input").map(|m| m as f32 / 1000.0),
220 power_w: read("power1_average")
221 .or_else(|| read("power1_input"))
222 .map(|uw| uw as f32 / 1e6),
223 power_cap_w: read("power1_cap").map(|uw| uw as f32 / 1e6),
224 fan_percent: read("pwm1").map(|pwm| pwm as f32 * 100.0 / 255.0),
226 clock_mhz: read("freq1_input").map(|hz| (hz / 1_000_000) as u32),
227 is_apu: read_trimmed(&dir.join("in1_label")).is_some_and(|l| l == "vddnb"),
228 }
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235 use crate::linux::testutil::{symlink, write};
236
237 #[test]
238 fn finds_device_names_in_pci_ids() {
239 let ids = "# comment\n\
240 1002 Advanced Micro Devices, Inc. [AMD/ATI]\n\
241 \t15bf Phoenix1\n\
242 \t\t17aa 50d8 ThinkPad\n\
243 \t744c Navi 31 [Radeon RX 7900 XT/7900 XTX]\n\
244 1003 ULSI Systems\n\
245 \t0201 US201\n";
246 assert_eq!(
247 pci_device_name(ids, 0x1002, 0x15bf).as_deref(),
248 Some("Phoenix1")
249 );
250 assert_eq!(
251 pci_device_name(ids, 0x1002, 0x744c).as_deref(),
252 Some("Navi 31 [Radeon RX 7900 XT/7900 XTX]")
253 );
254 assert_eq!(pci_device_name(ids, 0x1002, 0x0201), None);
256 }
257
258 fn roots(root: &Path) -> SysRoots {
259 SysRoots {
260 sys: root.join("sys"),
261 proc: root.join("proc"),
262 }
263 }
264
265 fn card(
267 root: &Path,
268 card: &str,
269 pci: &str,
270 vendor: &str,
271 device: &str,
272 driver: &str,
273 ) -> String {
274 let dev = format!("sys/devices/pci0000:00/{pci}");
275 write(root, &format!("{dev}/vendor"), &format!("{vendor}\n"));
276 write(root, &format!("{dev}/device"), &format!("{device}\n"));
277 write(root, &format!("sys/bus/pci/drivers/{driver}/.keep"), "");
278 symlink(
279 root,
280 &format!("{dev}/driver"),
281 &root.join(format!("sys/bus/pci/drivers/{driver}")),
282 );
283 symlink(
284 root,
285 &format!("sys/class/drm/{card}/device"),
286 &root.join(&dev),
287 );
288 dev
289 }
290
291 #[test]
292 fn reads_amd_apu_and_discrete_skipping_nvidia() {
293 let tmp = tempfile::tempdir().unwrap();
294 let root = tmp.path();
295 card(root, "card0", "0000:01:00.0", "0x10de", "0x2684", "nvidia");
297 write(root, "sys/class/drm/card1-DP-1/status", "connected\n");
298 let apu = card(root, "card1", "0000:c3:00.0", "0x1002", "0x15bf", "amdgpu");
300 write(root, &format!("{apu}/product_name"), "Radeon 780M\n");
301 write(root, &format!("{apu}/gpu_busy_percent"), "12\n");
302 write(root, &format!("{apu}/mem_info_vram_used"), "1073741824\n");
303 write(root, &format!("{apu}/mem_info_vram_total"), "2147483648\n");
304 write(root, &format!("{apu}/hwmon/hwmon6/temp1_input"), "45000\n");
305 write(
306 root,
307 &format!("{apu}/hwmon/hwmon6/power1_input"),
308 "6000000\n",
309 );
310 write(
311 root,
312 &format!("{apu}/hwmon/hwmon6/freq1_input"),
313 "800000000\n",
314 );
315 write(root, &format!("{apu}/hwmon/hwmon6/in1_label"), "vddnb\n");
316 let dgpu = card(root, "card2", "0000:03:00.0", "0x1002", "0x744c", "amdgpu");
318 write(root, &format!("{dgpu}/gpu_busy_percent"), "99\n");
319 write(
320 root,
321 &format!("{dgpu}/pp_dpm_sclk"),
322 "0: 500Mhz\n1: 2500Mhz *\n",
323 );
324 write(
325 root,
326 &format!("{dgpu}/hwmon/hwmon9/power1_average"),
327 "250000000\n",
328 );
329 write(
330 root,
331 &format!("{dgpu}/hwmon/hwmon9/power1_cap"),
332 "300000000\n",
333 );
334 write(root, &format!("{dgpu}/hwmon/hwmon9/pwm1"), "51\n");
335
336 let gpus = DrmGpuReader::new().read(&roots(root), 1);
337 assert_eq!(gpus.len(), 2, "NVIDIA and connectors are skipped");
338
339 let apu = &gpus[0];
340 assert_eq!(apu.index, 1, "numbered after NVIDIA's");
341 assert_eq!(apu.name, "AMD Radeon 780M");
342 assert!(apu.integrated);
343 assert_eq!(apu.utilization_percent, Some(12.0));
344 assert_eq!(apu.memory_total_bytes, Some(2 << 30));
345 assert_eq!(apu.temperature_c, Some(45.0));
346 assert_eq!(apu.power_w, None, "an APU's reading is the whole SoC");
347 assert_eq!(apu.clock_mhz, Some(800));
348
349 let dgpu = &gpus[1];
350 assert!(!dgpu.integrated);
351 assert!(dgpu.name.starts_with("AMD "), "{}", dgpu.name);
352 assert_eq!(dgpu.power_w, Some(250.0));
353 assert_eq!(dgpu.power_limit_w, Some(300.0));
354 assert_eq!(dgpu.fan_percent, Some(20.0));
355 assert_eq!(
356 dgpu.clock_mhz,
357 Some(2500),
358 "from pp_dpm_sclk without hwmon freq"
359 );
360 }
361
362 #[test]
363 fn reads_intel_clock_without_utilization() {
364 let tmp = tempfile::tempdir().unwrap();
365 let root = tmp.path();
366 card(root, "card0", "0000:00:02.0", "0x8086", "0x46a6", "i915");
367 write(root, "sys/class/drm/card0/gt_act_freq_mhz", "1300\n");
368 let gpus = DrmGpuReader::new().read(&roots(root), 0);
369 assert_eq!(gpus.len(), 1);
370 let igpu = &gpus[0];
371 assert_eq!(igpu.vendor, "Intel");
372 assert!(igpu.name.starts_with("Intel "), "{}", igpu.name);
373 assert!(igpu.integrated, "at 00:02.0");
374 assert_eq!(igpu.clock_mhz, Some(1300));
375 assert_eq!(igpu.utilization_percent, None);
376 }
377}