Skip to main content

retch_cli/
gpu.rs

1use std::collections::HashSet;
2use std::fs;
3
4/// Information about a detected GPU.
5#[derive(Debug, Clone, Default)]
6pub struct GpuInfo {
7    /// The marketing or model name of the GPU.
8    pub name: String,
9    /// Total VRAM in bytes, if detected.
10    pub vram_bytes: Option<u64>,
11}
12
13impl GpuInfo {
14    /// Formats the GPU name and VRAM into a human-readable string.
15    pub fn format(&self) -> String {
16        if let Some(vram) = self.vram_bytes {
17            let vram_gb = vram as f64 / 1024.0 / 1024.0 / 1024.0;
18            if vram_gb >= 1.0 {
19                format!("{} ({:.0} GB)", self.name, vram_gb)
20            } else {
21                let vram_mb = vram / 1024 / 1024;
22                format!("{} ({} MB)", self.name, vram_mb)
23            }
24        } else {
25            self.name.clone()
26        }
27    }
28}
29
30/// Refines AMD GPU names by mapping codenames to marketing names.
31pub fn improve_amd_gpu_name(name: &str) -> String {
32    let codenames = [
33        ("Phoenix1", "Radeon 780M"),
34        ("Phoenix2", "Radeon 740M / 760M"),
35        ("Renoir", "Radeon Graphics (Renoir)"),
36        ("Lucienne", "Radeon Graphics (Lucienne)"),
37        ("Cezanne", "Radeon Graphics (Cezanne)"),
38        ("Barcelo", "Radeon Graphics (Barcelo)"),
39        ("Rembrandt", "Radeon 680M"),
40        ("Raphael", "Radeon Graphics (Raphael)"),
41        ("Mendocino", "Radeon 610M"),
42        ("Strix", "Radeon 880M / 890M"),
43    ];
44
45    for (codename, marketing) in codenames {
46        if name.contains(codename) {
47            return marketing.to_string();
48        }
49    }
50
51    name.to_string()
52}
53
54/// Helper to lookup PCI device name in standard system pci.ids files.
55pub fn lookup_pci_device(vendor_id: &str, device_id: &str) -> Option<String> {
56    let vendor_id = vendor_id.trim_start_matches("0x").to_lowercase();
57    let device_id = device_id.trim_start_matches("0x").to_lowercase();
58
59    let paths = ["/usr/share/hwdata/pci.ids", "/usr/share/misc/pci.ids"];
60
61    for path in &paths {
62        if let Ok(content) = fs::read_to_string(path) {
63            let mut in_vendor = false;
64            for line in content.lines() {
65                if line.starts_with('#') || line.is_empty() {
66                    continue;
67                }
68
69                if !line.starts_with('\t') {
70                    // Vendor line: "vendor_id  Vendor Name"
71                    in_vendor = line.starts_with(&vendor_id);
72                } else if in_vendor && line.starts_with('\t') && !line.starts_with("\t\t") {
73                    // Device line: "\tdevice_id  Device Name"
74                    let trimmed = line.trim_start();
75                    if trimmed.starts_with(&device_id) {
76                        let name = trimmed[device_id.len()..].trim();
77                        return Some(name.to_string());
78                    }
79                }
80            }
81        }
82    }
83    None
84}
85
86/// Detects GPUs using sysfs and PCI database lookups.
87pub fn detect_gpus() -> Vec<GpuInfo> {
88    let mut gpus = Vec::new();
89    let mut seen_devices = HashSet::new();
90
91    // Scan /sys/class/drm for all card* and renderD* entries
92    if let Ok(entries) = std::fs::read_dir("/sys/class/drm") {
93        for entry in entries.flatten() {
94            let name = entry.file_name().into_string().unwrap_or_default();
95            if !name.starts_with("card") && !name.starts_with("renderD") {
96                continue;
97            }
98
99            let device_path = entry.path().join("device");
100            if let Ok(real_path) = std::fs::canonicalize(&device_path) {
101                if !seen_devices.insert(real_path) {
102                    continue;
103                }
104
105                // Try to identify vendor and model
106                let vendor_id = fs::read_to_string(device_path.join("vendor"))
107                    .unwrap_or_default()
108                    .trim()
109                    .to_string();
110                let device_id = fs::read_to_string(device_path.join("device"))
111                    .unwrap_or_default()
112                    .trim()
113                    .to_string();
114
115                if vendor_id.is_empty() || device_id.is_empty() {
116                    continue;
117                }
118
119                let mut gpu_name = lookup_pci_device(&vendor_id, &device_id).unwrap_or_else(|| {
120                    if vendor_id.contains("10de") {
121                        "NVIDIA GPU".to_string()
122                    } else if vendor_id.contains("1002") {
123                        "AMD GPU".to_string()
124                    } else if vendor_id.contains("8086") {
125                        "Intel GPU".to_string()
126                    } else {
127                        "Unknown GPU".to_string()
128                    }
129                });
130
131                // Refine AMD GPU names
132                if vendor_id.contains("1002") {
133                    gpu_name = improve_amd_gpu_name(&gpu_name);
134                }
135
136                // NVIDIA special case: try /proc for even better name
137                if vendor_id.contains("10de") {
138                    if let Ok(pci_slot_path) = fs::read_link(&device_path) {
139                        if let Some(slot_name) = pci_slot_path.file_name() {
140                            let proc_info_path = format!(
141                                "/proc/driver/nvidia/gpus/{}/information",
142                                slot_name.to_string_lossy()
143                            );
144                            if let Ok(info) = fs::read_to_string(proc_info_path) {
145                                for line in info.lines() {
146                                    if line.starts_with("Model:") {
147                                        gpu_name = line.replace("Model:", "").trim().to_string();
148                                        break;
149                                    }
150                                }
151                            }
152                        }
153                    }
154                }
155
156                let mut vram_bytes = None;
157                // Try to get VRAM from common sysfs locations (mainly AMD)
158                let vram_path = device_path.join("mem_info_vram_total");
159                if let Ok(vram_str) = fs::read_to_string(vram_path) {
160                    if let Ok(v) = vram_str.trim().parse::<u64>() {
161                        vram_bytes = Some(v);
162                    }
163                }
164
165                gpus.push(GpuInfo {
166                    name: gpu_name,
167                    vram_bytes,
168                });
169            }
170        }
171    }
172
173    if gpus.is_empty() {
174        // Fallback for non-standard setups or if /sys/class/drm is empty
175        if let Ok(model) = fs::read_to_string("/sys/class/drm/card0/device/model") {
176            let model = model.trim();
177            if !model.is_empty() {
178                gpus.push(GpuInfo {
179                    name: model.to_string(),
180                    vram_bytes: None,
181                });
182            }
183        }
184    }
185
186    gpus
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    #[test]
194    fn test_gpu_info_format() {
195        let info = GpuInfo {
196            name: "NVIDIA GeForce RTX 4090".to_string(),
197            vram_bytes: Some(24 * 1024 * 1024 * 1024),
198        };
199        assert_eq!(info.format(), "NVIDIA GeForce RTX 4090 (24 GB)");
200
201        let info = GpuInfo {
202            name: "Intel Arc A770".to_string(),
203            vram_bytes: Some(16 * 1024 * 1024 * 1024),
204        };
205        assert_eq!(info.format(), "Intel Arc A770 (16 GB)");
206
207        let info = GpuInfo {
208            name: "Radeon 780M".to_string(),
209            vram_bytes: Some(512 * 1024 * 1024),
210        };
211        assert_eq!(info.format(), "Radeon 780M (512 MB)");
212
213        let info = GpuInfo {
214            name: "Generic GPU".to_string(),
215            vram_bytes: None,
216        };
217        assert_eq!(info.format(), "Generic GPU");
218    }
219
220    #[test]
221    fn test_improve_amd_gpu_name() {
222        assert_eq!(
223            improve_amd_gpu_name("AMD Radeon Phoenix1 Graphics"),
224            "Radeon 780M"
225        );
226        assert_eq!(improve_amd_gpu_name("AMD Rembrandt"), "Radeon 680M");
227        assert_eq!(improve_amd_gpu_name("Unknown GPU"), "Unknown GPU");
228    }
229}