1#[cfg(not(any(target_os = "macos", target_os = "windows")))]
10use std::collections::HashSet;
11use std::fs;
12
13#[derive(Debug, Clone, Default)]
15pub struct GpuInfo {
16 pub name: String,
18 pub vram_bytes: Option<u64>,
20}
21
22impl GpuInfo {
23 pub fn format(&self) -> String {
25 if let Some(vram) = self.vram_bytes {
26 let vram_gb = vram as f64 / 1024.0 / 1024.0 / 1024.0;
27 if vram_gb >= 1.0 {
28 format!("{} ({:.0} GB)", self.name, vram_gb)
29 } else {
30 let vram_mb = vram / 1024 / 1024;
31 format!("{} ({} MB)", self.name, vram_mb)
32 }
33 } else {
34 self.name.clone()
35 }
36 }
37}
38
39pub fn improve_amd_gpu_name(name: &str) -> String {
41 let codenames = [
42 ("Phoenix1", "Radeon 780M"),
43 ("Phoenix2", "Radeon 740M / 760M"),
44 ("Renoir", "Radeon Graphics (Renoir)"),
45 ("Lucienne", "Radeon Graphics (Lucienne)"),
46 ("Cezanne", "Radeon Graphics (Cezanne)"),
47 ("Barcelo", "Radeon Graphics (Barcelo)"),
48 ("Rembrandt", "Radeon 680M"),
49 ("Raphael", "Radeon Graphics (Raphael)"),
50 ("Mendocino", "Radeon 610M"),
51 ("Strix", "Radeon 880M / 890M"),
52 ];
53
54 for (codename, marketing) in codenames {
55 if name.contains(codename) {
56 return marketing.to_string();
57 }
58 }
59
60 name.to_string()
61}
62
63pub fn lookup_pci_device(vendor_id: &str, device_id: &str) -> Option<String> {
65 let vendor_id = vendor_id.trim_start_matches("0x").to_lowercase();
66 let device_id = device_id.trim_start_matches("0x").to_lowercase();
67
68 let paths = ["/usr/share/hwdata/pci.ids", "/usr/share/misc/pci.ids"];
69
70 for path in &paths {
71 if let Ok(content) = fs::read_to_string(path) {
72 let mut in_vendor = false;
73 for line in content.lines() {
74 if line.starts_with('#') || line.is_empty() {
75 continue;
76 }
77
78 if !line.starts_with('\t') {
79 in_vendor = line.starts_with(&vendor_id);
81 } else if in_vendor && line.starts_with('\t') && !line.starts_with("\t\t") {
82 let trimmed = line.trim_start();
84 if let Some(stripped) = trimmed.strip_prefix(&device_id) {
85 let name = stripped.trim();
86 return Some(name.to_string());
87 }
88 }
89 }
90 }
91 }
92 None
93}
94
95pub fn parse_vram_str(s: &str) -> Option<u64> {
97 let parts: Vec<&str> = s.split_whitespace().collect();
98 if parts.len() >= 2 {
99 if let Ok(val) = parts[0].parse::<f64>() {
100 let unit = parts[1].to_uppercase();
101 if unit.starts_with("GB") {
102 return Some((val * 1024.0 * 1024.0 * 1024.0) as u64);
103 } else if unit.starts_with("MB") {
104 return Some((val * 1024.0 * 1024.0) as u64);
105 } else if unit.starts_with("KB") {
106 return Some((val * 1024.0) as u64);
107 }
108 }
109 }
110 None
111}
112
113pub fn parse_system_profiler_displays(stdout: &str) -> Vec<GpuInfo> {
115 let mut gpus = Vec::new();
116 let mut current_gpu = None;
117 for line in stdout.lines() {
118 let trimmed = line.trim();
119 if let Some(stripped) = trimmed.strip_prefix("Chipset Model:") {
120 if let Some(gpu) = current_gpu.take() {
121 gpus.push(gpu);
122 }
123 let name = stripped.trim().to_string();
124 current_gpu = Some(GpuInfo {
125 name,
126 vram_bytes: None,
127 });
128 } else if let Some(stripped) = trimmed.strip_prefix("VRAM (Total):") {
129 if let Some(ref mut gpu) = current_gpu {
130 let vram_str = stripped.trim();
131 gpu.vram_bytes = parse_vram_str(vram_str);
132 }
133 } else if let Some(stripped) = trimmed.strip_prefix("VRAM (Dynamic, Max):") {
134 if let Some(ref mut gpu) = current_gpu {
135 let vram_str = stripped.trim();
136 gpu.vram_bytes = parse_vram_str(vram_str);
137 }
138 } else if trimmed.starts_with("Displays:") {
139 if let Some(gpu) = current_gpu.take() {
140 gpus.push(gpu);
141 }
142 }
143 }
144 if let Some(gpu) = current_gpu {
145 gpus.push(gpu);
146 }
147 gpus
148}
149
150pub fn parse_wmi_videocontroller(output: &str) -> Vec<GpuInfo> {
152 let mut gpus = Vec::new();
153 let mut name = String::new();
154 let mut vram = None;
155
156 for line in output.lines() {
157 let trimmed = line.trim();
158 let parts: Vec<&str> = if trimmed.contains('=') {
159 trimmed.splitn(2, '=').collect()
160 } else if trimmed.contains(':') {
161 trimmed.splitn(2, ':').collect()
162 } else {
163 continue;
164 };
165
166 if parts.len() == 2 {
167 let key = parts[0].trim().to_lowercase();
168 let val = parts[1].trim();
169
170 if key == "name" {
171 if !name.is_empty() {
172 gpus.push(GpuInfo {
173 name: name.clone(),
174 vram_bytes: vram,
175 });
176 vram = None;
177 }
178 name = val.to_string();
179 } else if key == "adapterram" {
180 if vram.is_some() && !name.is_empty() {
181 gpus.push(GpuInfo {
182 name: name.clone(),
183 vram_bytes: vram,
184 });
185 name = String::new();
186 vram = None;
187 }
188 if let Ok(bytes) = val.parse::<u64>() {
189 vram = Some(bytes);
190 }
191 }
192 }
193 }
194
195 if !name.is_empty() {
196 gpus.push(GpuInfo {
197 name,
198 vram_bytes: vram,
199 });
200 }
201
202 gpus
203}
204
205pub fn detect_gpus() -> Vec<GpuInfo> {
207 #[cfg(target_os = "macos")]
208 {
209 match std::process::Command::new("system_profiler")
210 .arg("SPDisplaysDataType")
211 .output()
212 {
213 Ok(output) => {
214 if let Ok(stdout) = String::from_utf8(output.stdout) {
215 return parse_system_profiler_displays(&stdout);
216 } else {
217 eprintln!("warning: system_profiler output was not valid UTF-8");
218 }
219 }
220 Err(e) => {
221 eprintln!("warning: failed to run system_profiler SPDisplaysDataType: {} (GPU detection skipped)", e);
222 }
223 }
224 Vec::new()
225 }
226
227 #[cfg(target_os = "windows")]
228 {
229 let mut wmic_ok = false;
231 match std::process::Command::new("wmic")
232 .args([
233 "path",
234 "win32_VideoController",
235 "get",
236 "Name,AdapterRAM",
237 "/value",
238 ])
239 .output()
240 {
241 Ok(output) => {
242 if let Ok(stdout) = String::from_utf8(output.stdout) {
243 let gpus = parse_wmi_videocontroller(&stdout);
244 if !gpus.is_empty() {
245 return gpus;
246 }
247 wmic_ok = true;
248 }
249 }
250 Err(e) => {
251 eprintln!("warning: wmic failed: {} (trying PowerShell fallback)", e);
252 }
253 }
254
255 if !wmic_ok {
257 match std::process::Command::new("powershell")
258 .args(["-Command", "Get-CimInstance Win32_VideoController | Select-Object Name, AdapterRAM | Format-List"])
259 .output()
260 {
261 Ok(output) => {
262 if let Ok(stdout) = String::from_utf8(output.stdout) {
263 return parse_wmi_videocontroller(&stdout);
264 } else {
265 eprintln!("warning: powershell output was not valid UTF-8");
266 }
267 }
268 Err(e) => {
269 eprintln!("warning: powershell failed: {} (GPU detection skipped)", e);
270 }
271 }
272 }
273
274 Vec::new()
275 }
276
277 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
278 {
279 let mut gpus = Vec::new();
280 let mut seen_devices = HashSet::new();
281
282 if let Ok(entries) = std::fs::read_dir("/sys/class/drm") {
284 for entry in entries.flatten() {
285 let name = entry.file_name().into_string().unwrap_or_default();
286 if !name.starts_with("card") && !name.starts_with("renderD") {
287 continue;
288 }
289
290 let device_path = entry.path().join("device");
291 if let Ok(real_path) = std::fs::canonicalize(&device_path) {
292 if !seen_devices.insert(real_path) {
293 continue;
294 }
295
296 let vendor_id = fs::read_to_string(device_path.join("vendor"))
298 .unwrap_or_default()
299 .trim()
300 .to_string();
301 let device_id = fs::read_to_string(device_path.join("device"))
302 .unwrap_or_default()
303 .trim()
304 .to_string();
305
306 if vendor_id.is_empty() || device_id.is_empty() {
307 continue;
308 }
309
310 let mut gpu_name =
311 lookup_pci_device(&vendor_id, &device_id).unwrap_or_else(|| {
312 if vendor_id.contains("10de") {
313 "NVIDIA GPU".to_string()
314 } else if vendor_id.contains("1002") {
315 "AMD GPU".to_string()
316 } else if vendor_id.contains("8086") {
317 "Intel GPU".to_string()
318 } else {
319 "Unknown GPU".to_string()
320 }
321 });
322
323 if vendor_id.contains("1002") {
325 gpu_name = improve_amd_gpu_name(&gpu_name);
326 }
327
328 if vendor_id.contains("10de") {
330 if let Ok(pci_slot_path) = fs::read_link(&device_path) {
331 if let Some(slot_name) = pci_slot_path.file_name() {
332 let proc_info_path = format!(
333 "/proc/driver/nvidia/gpus/{}/information",
334 slot_name.to_string_lossy()
335 );
336 if let Ok(info) = fs::read_to_string(proc_info_path) {
337 for line in info.lines() {
338 if line.starts_with("Model:") {
339 gpu_name =
340 line.replace("Model:", "").trim().to_string();
341 break;
342 }
343 }
344 }
345 }
346 }
347 }
348
349 let mut vram_bytes = None;
350 let vram_path = device_path.join("mem_info_vram_total");
352 if let Ok(vram_str) = fs::read_to_string(vram_path) {
353 if let Ok(v) = vram_str.trim().parse::<u64>() {
354 vram_bytes = Some(v);
355 }
356 }
357
358 gpus.push(GpuInfo {
359 name: gpu_name,
360 vram_bytes,
361 });
362 }
363 }
364 }
365
366 if gpus.is_empty() {
367 if let Ok(model) = fs::read_to_string("/sys/class/drm/card0/device/model") {
369 let model = model.trim();
370 if !model.is_empty() {
371 gpus.push(GpuInfo {
372 name: model.to_string(),
373 vram_bytes: None,
374 });
375 }
376 }
377 }
378
379 gpus
380 }
381}
382
383#[cfg(test)]
384mod tests {
385 use super::*;
386
387 #[test]
388 fn test_gpu_info_format() {
389 let info = GpuInfo {
390 name: "NVIDIA GeForce RTX 4090".to_string(),
391 vram_bytes: Some(24 * 1024 * 1024 * 1024),
392 };
393 assert_eq!(info.format(), "NVIDIA GeForce RTX 4090 (24 GB)");
394
395 let info = GpuInfo {
396 name: "Intel Arc A770".to_string(),
397 vram_bytes: Some(16 * 1024 * 1024 * 1024),
398 };
399 assert_eq!(info.format(), "Intel Arc A770 (16 GB)");
400
401 let info = GpuInfo {
402 name: "Radeon 780M".to_string(),
403 vram_bytes: Some(512 * 1024 * 1024),
404 };
405 assert_eq!(info.format(), "Radeon 780M (512 MB)");
406
407 let info = GpuInfo {
408 name: "Generic GPU".to_string(),
409 vram_bytes: None,
410 };
411 assert_eq!(info.format(), "Generic GPU");
412 }
413
414 #[test]
415 fn test_improve_amd_gpu_name() {
416 assert_eq!(
417 improve_amd_gpu_name("AMD Radeon Phoenix1 Graphics"),
418 "Radeon 780M"
419 );
420 assert_eq!(improve_amd_gpu_name("AMD Rembrandt"), "Radeon 680M");
421 assert_eq!(improve_amd_gpu_name("Unknown GPU"), "Unknown GPU");
422 }
423
424 #[test]
425 fn test_parse_vram_str() {
426 assert_eq!(parse_vram_str("8 GB"), Some(8 * 1024 * 1024 * 1024));
427 assert_eq!(parse_vram_str("1536 MB"), Some(1536 * 1024 * 1024));
428 assert_eq!(parse_vram_str("512 MB"), Some(512 * 1024 * 1024));
429 assert_eq!(parse_vram_str("1024 KB"), Some(1024 * 1024));
430 assert_eq!(parse_vram_str("invalid"), None);
431 assert_eq!(parse_vram_str("8"), None);
432 }
433
434 #[test]
435 fn test_parse_system_profiler_displays() {
436 let mock_output = r#"
437Graphics/Displays:
438
439 Apple M1 Max:
440
441 Chipset Model: Apple M1 Max
442 Type: GPU
443 Bus: Built-In
444 Total Number of Cores: 32
445 Vendor: Apple (0x106b)
446 Metal Support: Metal 3
447 VRAM (Dynamic, Max): 8192 MB
448 Displays:
449 Color LCD:
450 Display Type: Built-In Retina LCD
451
452 Intel UHD Graphics 630:
453
454 Chipset Model: Intel UHD Graphics 630
455 Type: GPU
456 Bus: Built-In
457 VRAM (Total): 1536 MB
458 Vendor: Intel (0x8086)
459"#;
460 let gpus = parse_system_profiler_displays(mock_output);
461 assert_eq!(gpus.len(), 2);
462 assert_eq!(gpus[0].name, "Apple M1 Max");
463 assert_eq!(gpus[0].vram_bytes, Some(8192 * 1024 * 1024));
464 assert_eq!(gpus[1].name, "Intel UHD Graphics 630");
465 assert_eq!(gpus[1].vram_bytes, Some(1536 * 1024 * 1024));
466 }
467
468 #[test]
469 fn test_parse_wmi_videocontroller() {
470 let wmic_output = r#"
471AdapterRAM=4294967296
472Name=NVIDIA GeForce RTX 4090
473
474AdapterRAM=2147483648
475Name=Intel(R) UHD Graphics 770
476"#;
477 let gpus = parse_wmi_videocontroller(wmic_output);
478 assert_eq!(gpus.len(), 2);
479 assert_eq!(gpus[0].name, "NVIDIA GeForce RTX 4090");
480 assert_eq!(gpus[0].vram_bytes, Some(4294967296));
481 assert_eq!(gpus[1].name, "Intel(R) UHD Graphics 770");
482 assert_eq!(gpus[1].vram_bytes, Some(2147483648));
483
484 let powershell_output = r#"
485Name : NVIDIA GeForce RTX 4090
486AdapterRAM : 4294967296
487
488Name : Intel(R) UHD Graphics 770
489AdapterRAM : 2147483648
490"#;
491 let gpus = parse_wmi_videocontroller(powershell_output);
492 assert_eq!(gpus.len(), 2);
493 assert_eq!(gpus[0].name, "NVIDIA GeForce RTX 4090");
494 assert_eq!(gpus[0].vram_bytes, Some(4294967296));
495 assert_eq!(gpus[1].name, "Intel(R) UHD Graphics 770");
496 assert_eq!(gpus[1].vram_bytes, Some(2147483648));
497 }
498}