1use std::collections::HashSet;
2use std::fs;
3
4#[derive(Debug, Clone, Default)]
6pub struct GpuInfo {
7 pub name: String,
9 pub vram_bytes: Option<u64>,
11}
12
13impl GpuInfo {
14 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
30pub 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
54pub 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 in_vendor = line.starts_with(&vendor_id);
72 } else if in_vendor && line.starts_with('\t') && !line.starts_with("\t\t") {
73 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
86pub fn detect_gpus() -> Vec<GpuInfo> {
88 let mut gpus = Vec::new();
89 let mut seen_devices = HashSet::new();
90
91 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 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 if vendor_id.contains("1002") {
133 gpu_name = improve_amd_gpu_name(&gpu_name);
134 }
135
136 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 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 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}