Skip to main content

rigtop_core/linux/
drm.rs

1//! AMD and Intel GPUs from `/sys/class/drm/card<N>/device`.
2//!
3//! NVIDIA GPUs come from NVML (`gpu.rs`) and are skipped here. Everything
4//! read is world-readable:
5//!
6//! - amdgpu: `gpu_busy_percent`, `mem_busy_percent`,
7//!   `mem_info_vram_{used,total}`, and the device's hwmon (temperature,
8//!   power, fan, clock).
9//! - i915 / xe (Intel): the clock and, on discrete cards, hwmon. Intel
10//!   exposes utilization only through perf events, which need privileges,
11//!   so it is left unknown.
12//!
13//! Names come from the driver (`product_name` on some AMD boards) or the PCI
14//! ID database (`pci.ids`, from hwdata / pciutils).
15
16use 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
26/// Where distributions put the PCI ID database.
27const PCI_IDS: &[&str] = &[
28    "/usr/share/hwdata/pci.ids",
29    "/usr/share/misc/pci.ids",
30    "/usr/share/pci.ids",
31];
32
33/// Reads AMD and Intel GPUs, remembering their names.
34#[derive(Default)]
35pub struct DrmGpuReader {
36    /// Names by (vendor, device), looked up once.
37    names: HashMap<(u16, u16), String>,
38}
39
40impl DrmGpuReader {
41    pub fn new() -> Self {
42        Self::default()
43    }
44
45    /// AMD and Intel GPUs, numbered from `first_index` (after NVIDIA's).
46    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            // card0, card1, ...; not connectors such as card1-DP-1.
51            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            // An APU's reading is the whole SoC (see PowerStats::igpu_w),
92            // not the GPU's own share.
93            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        // i915 has gt_act_freq_mhz on the card; xe has it per tile and GT.
104        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        // The integrated GPU sits at PCI 00:02.0 on Intel platforms.
108        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    /// Driver-provided name, else the PCI ID database, else the device id;
130    /// prefixed with the vendor.
131    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
158/// `0x1002` style sysfs attributes.
159fn 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
164/// The `*`-marked line of `pp_dpm_sclk` ("1: 1800Mhz *").
165fn 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
176/// Device name from `pci.ids`: a vendor line `vvvv  Name`, then device
177/// lines `\tdddd  Name` until the next vendor.
178pub(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/// The readings of a GPU's own hwmon directory.
199#[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    /// amdgpu's `vddnb` rail exists only on APUs.
207    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            // pwm1 is 0-255.
225            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        // A device id of another vendor does not match.
255        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    /// A card whose `device` points at `pci` with `driver`.
266    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        // An NVIDIA card (left to NVML) and a connector entry.
296        card(root, "card0", "0000:01:00.0", "0x10de", "0x2684", "nvidia");
297        write(root, "sys/class/drm/card1-DP-1/status", "connected\n");
298        // A laptop APU.
299        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        // A discrete card with a fan and a power cap.
317        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}