Coverage Report

Created: 2026-07-21 05:33

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
src/drm.rs
Line
Count
Source
1
// SPDX-License-Identifier: Apache-2.0
2
//! AMD and Intel detection via Linux DRM sysfs (`/sys/class/drm/card*/device`).
3
//! No `ROCm`, Level Zero, or vendor libraries required.
4
//!
5
//! GPUs that expose `mem_info_vram_total` report dedicated VRAM with used/free.
6
//! That attribute is provided by `amdgpu`; whether Intel's `i915`/`xe` drivers
7
//! expose it on discrete Arc cards is unverified (untested on real hardware).
8
//! Any card without it — AMD APUs, Intel iGPUs, and Intel discrete cards that
9
//! don't implement the file — falls back to the shared system-memory ceiling
10
//! with used/free `None`. NVIDIA cards are skipped here — they're handled by
11
//! NVML in the `nvidia` module.
12
13
/// True for a primary DRM card node (`card0`, `card1`, …) — not a connector
14
/// (`card0-eDP-1`) or a render node (`renderD128`).
15
#[allow(dead_code)] // used on Linux + in tests; unused on other targets
16
656
fn is_card_dir(name: &str) -> bool {
17
656
    name.strip_prefix("card")
18
656
        .is_some_and(|rest| 
!rest.is_empty()436
&&
rest.bytes()435
.
all435
(|b|
b654
.
is_ascii_digit654
()))
19
656
}
20
21
/// Parse a sysfs integer file (e.g. `mem_info_vram_total`) holding a decimal
22
/// byte count.
23
#[allow(dead_code)] // used on Linux + in tests; unused on other targets
24
9
fn parse_bytes(content: &str) -> Option<u64> {
25
9
    content.trim().parse().ok()
26
9
}
27
28
/// Map a PCI vendor id (`device/vendor`, e.g. `0x1002`) to the [`Vendor`] this
29
/// scanner owns. NVIDIA (`0x10de`) returns `None` — NVML handles it — as do
30
/// unknown vendors.
31
#[allow(dead_code)] // used on Linux + in tests; unused on other targets
32
223
fn sysfs_vendor(id: &str) -> Option<crate::Vendor> {
33
    use crate::Vendor;
34
223
    match id.trim().to_ascii_lowercase().as_str() {
35
223
        "0x1002" => 
Some(Vendor::Amd)2
,
36
221
        "0x8086" => 
Some(Vendor::Intel)2
,
37
219
        _ => None,
38
    }
39
223
}
40
41
/// Parse `MemTotal:` (in kB) from `/proc/meminfo` contents into bytes.
42
#[allow(dead_code)] // used on Linux + in tests; unused on other targets
43
6
fn parse_meminfo_total(content: &str) -> Option<u64> {
44
6
    for 
line5
in content.lines() {
45
5
        if let Some(
rest4
) = line.strip_prefix("MemTotal:") {
46
4
            let 
kb1
:
u641
= rest.split_whitespace().next()
?1
.
parse3
().
ok3
()
?2
;
47
1
            return Some(kb * 1024);
48
1
        }
49
    }
50
2
    None
51
6
}
52
53
#[cfg(target_os = "linux")]
54
0
fn system_memory() -> Option<u64> {
55
0
    std::fs::read_to_string("/proc/meminfo")
56
0
        .ok()
57
0
        .and_then(|s| parse_meminfo_total(&s))
58
0
}
59
60
#[cfg(target_os = "linux")]
61
215
pub(crate) fn detect() -> Vec<crate::GpuInfo> {
62
    use crate::GpuInfo;
63
64
215
    let mut gpus = Vec::new();
65
215
    let Ok(entries) = std::fs::read_dir("/sys/class/drm") else {
66
0
        return gpus;
67
    };
68
    // Fetched lazily: only an integrated GPU needs it, and most hosts have none.
69
215
    let mut sysmem: Option<u64> = None;
70
71
645
    for entry in 
entries215
.
flatten215
() {
72
645
        let file_name = entry.file_name();
73
645
        let card = file_name.to_string_lossy();
74
645
        if !is_card_dir(&card) {
75
430
            continue;
76
215
        }
77
215
        let device = entry.path().join("device");
78
215
        let Some(
vendor0
) = std::fs::read_to_string(device.join("vendor"))
79
215
            .ok()
80
215
            .and_then(|v| sysfs_vendor(&v))
81
        else {
82
215
            continue;
83
        };
84
85
0
        if let Some(total) = std::fs::read_to_string(device.join("mem_info_vram_total"))
86
0
            .ok()
87
0
            .and_then(|s| parse_bytes(&s))
88
        {
89
            // Discrete GPU with dedicated VRAM.
90
0
            let used = std::fs::read_to_string(device.join("mem_info_vram_used"))
91
0
                .ok()
92
0
                .and_then(|s| parse_bytes(&s));
93
0
            gpus.push(GpuInfo {
94
0
                name: format!("{vendor} GPU ({card})"),
95
0
                vendor,
96
0
                total_bytes: total,
97
0
                free_bytes: used.map(|u| total.saturating_sub(u)),
98
0
                used_bytes: used,
99
            });
100
        } else {
101
            // Integrated GPU (APU / iGPU): no dedicated VRAM — report the
102
            // shared system-memory ceiling, with no precise used/free.
103
0
            if sysmem.is_none() {
104
0
                sysmem = system_memory();
105
0
            }
106
0
            if let Some(total) = sysmem {
107
0
                gpus.push(GpuInfo {
108
0
                    name: format!("{vendor} GPU ({card})"),
109
0
                    vendor,
110
0
                    total_bytes: total,
111
0
                    free_bytes: None,
112
0
                    used_bytes: None,
113
0
                });
114
0
            }
115
        }
116
    }
117
215
    gpus
118
215
}
119
120
#[cfg(not(target_os = "linux"))]
121
pub(crate) fn detect() -> Vec<crate::GpuInfo> {
122
    Vec::new()
123
}
124
125
#[cfg(test)]
126
mod tests {
127
    use super::*;
128
    use crate::Vendor;
129
130
    #[test]
131
1
    fn card_dir_matches_only_primary_nodes() {
132
1
        assert!(is_card_dir("card0"));
133
1
        assert!(is_card_dir("card12"));
134
1
        assert!(!is_card_dir("card0-eDP-1"));
135
1
        assert!(!is_card_dir("renderD128"));
136
1
        assert!(!is_card_dir("controlD64"));
137
1
        assert!(!is_card_dir("card"));
138
1
        assert!(!is_card_dir("cardX"));
139
1
    }
140
141
    #[test]
142
1
    fn parses_sysfs_byte_count() {
143
1
        assert_eq!(parse_bytes("17163091968\n"), Some(17_163_091_968));
144
1
        assert_eq!(parse_bytes("  8589934592 "), Some(8_589_934_592));
145
1
        assert_eq!(parse_bytes("nope"), None);
146
1
        assert_eq!(parse_bytes(""), None);
147
1
    }
148
149
    #[test]
150
1
    fn vendor_ids_amd_and_intel_only() {
151
1
        assert_eq!(sysfs_vendor("0x1002"), Some(Vendor::Amd));
152
1
        assert_eq!(sysfs_vendor("0x8086\n"), Some(Vendor::Intel));
153
1
        assert_eq!(sysfs_vendor("0x10DE"), None, "NVIDIA is handled by NVML");
154
1
        assert_eq!(sysfs_vendor("0xffff"), None);
155
1
    }
156
157
    #[test]
158
1
    fn parses_meminfo_memtotal() {
159
1
        let meminfo = "MemTotal:       32789868 kB\nMemFree:         1234 kB\n";
160
1
        assert_eq!(parse_meminfo_total(meminfo), Some(32_789_868 * 1024));
161
1
        assert_eq!(parse_meminfo_total("MemFree: 100 kB"), None);
162
1
    }
163
164
    #[test]
165
1
    fn meminfo_handles_empty_and_malformed() {
166
1
        assert_eq!(parse_meminfo_total(""), None);
167
1
        assert_eq!(parse_meminfo_total("MemTotal:"), None);
168
1
        assert_eq!(parse_meminfo_total("MemTotal:        kB"), None);
169
1
        assert_eq!(parse_meminfo_total("MemTotal: notanumber kB"), None);
170
1
    }
171
172
    #[test]
173
1
    fn parse_bytes_handles_zero_and_large_values() {
174
1
        assert_eq!(parse_bytes("0"), Some(0));
175
1
        assert_eq!(parse_bytes(&u64::MAX.to_string()), Some(u64::MAX));
176
        // Overflowing u64 must fail rather than wrap.
177
1
        assert_eq!(parse_bytes("99999999999999999999999"), None);
178
1
        assert_eq!(parse_bytes("-1"), None);
179
1
        assert_eq!(parse_bytes("12 34"), None);
180
1
    }
181
182
    #[test]
183
1
    fn card_dir_rejects_render_and_control_nodes() {
184
1
        assert!(!is_card_dir("renderD129"));
185
1
        assert!(!is_card_dir("by-path"));
186
1
        assert!(!is_card_dir(""));
187
        // Leading zeros are still all-digits, so they count as a card node.
188
1
        assert!(is_card_dir("card007"));
189
1
    }
190
191
    #[test]
192
1
    fn sysfs_vendor_trims_and_lowercases() {
193
1
        assert_eq!(sysfs_vendor("  0x1002\n"), Some(Vendor::Amd));
194
1
        assert_eq!(sysfs_vendor("0X8086"), Some(Vendor::Intel));
195
1
        assert_eq!(sysfs_vendor(""), None);
196
1
        assert_eq!(sysfs_vendor("1002"), None, "missing 0x prefix is unknown");
197
1
    }
198
}