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 let mut gpus = Vec::new();
232 if let Ok(output) = std::process::Command::new("wmic")
233 .args([
234 "path",
235 "win32_VideoController",
236 "get",
237 "Name,AdapterRAM",
238 "/value",
239 ])
240 .output()
241 {
242 if let Ok(stdout) = String::from_utf8(output.stdout) {
243 gpus = parse_wmi_videocontroller(&stdout);
244 if !gpus.is_empty() {
245 return gpus;
246 }
247 wmic_ok = true;
248 }
249 }
250
251 if !wmic_ok {
253 if let Ok(output) = std::process::Command::new("powershell")
254 .args(["-Command", "Get-CimInstance Win32_VideoController | Select-Object Name, AdapterRAM | Format-List"])
255 .output()
256 {
257 if let Ok(stdout) = String::from_utf8(output.stdout) {
258 gpus = parse_wmi_videocontroller(&stdout);
259 }
260 }
261 }
262
263 if gpus.is_empty() {
264 eprintln!("warning: GPU detection failed on Windows (both wmic and powershell query returned no results)");
265 }
266
267 gpus
268 }
269
270 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
271 {
272 let mut gpus = Vec::new();
273 let mut seen_devices = HashSet::new();
274
275 if let Ok(entries) = std::fs::read_dir("/sys/class/drm") {
277 for entry in entries.flatten() {
278 let name = entry.file_name().into_string().unwrap_or_default();
279 if !name.starts_with("card") && !name.starts_with("renderD") {
280 continue;
281 }
282
283 let device_path = entry.path().join("device");
284 if let Ok(real_path) = std::fs::canonicalize(&device_path) {
285 if !seen_devices.insert(real_path) {
286 continue;
287 }
288
289 let vendor_id = fs::read_to_string(device_path.join("vendor"))
291 .unwrap_or_default()
292 .trim()
293 .to_string();
294 let device_id = fs::read_to_string(device_path.join("device"))
295 .unwrap_or_default()
296 .trim()
297 .to_string();
298
299 if vendor_id.is_empty() || device_id.is_empty() {
300 continue;
301 }
302
303 let mut gpu_name =
304 lookup_pci_device(&vendor_id, &device_id).unwrap_or_else(|| {
305 if vendor_id.contains("10de") {
306 "NVIDIA GPU".to_string()
307 } else if vendor_id.contains("1002") {
308 "AMD GPU".to_string()
309 } else if vendor_id.contains("8086") {
310 "Intel GPU".to_string()
311 } else {
312 "Unknown GPU".to_string()
313 }
314 });
315
316 if vendor_id.contains("1002") {
318 gpu_name = improve_amd_gpu_name(&gpu_name);
319 }
320
321 if vendor_id.contains("10de") {
323 if let Ok(pci_slot_path) = fs::read_link(&device_path) {
324 if let Some(slot_name) = pci_slot_path.file_name() {
325 let proc_info_path = format!(
326 "/proc/driver/nvidia/gpus/{}/information",
327 slot_name.to_string_lossy()
328 );
329 if let Ok(info) = fs::read_to_string(proc_info_path) {
330 for line in info.lines() {
331 if line.starts_with("Model:") {
332 gpu_name =
333 line.replace("Model:", "").trim().to_string();
334 break;
335 }
336 }
337 }
338 }
339 }
340 }
341
342 let mut vram_bytes = None;
343 let vram_path = device_path.join("mem_info_vram_total");
345 if let Ok(vram_str) = fs::read_to_string(vram_path) {
346 if let Ok(v) = vram_str.trim().parse::<u64>() {
347 vram_bytes = Some(v);
348 }
349 }
350
351 gpus.push(GpuInfo {
352 name: gpu_name,
353 vram_bytes,
354 });
355 }
356 }
357 }
358
359 if gpus.is_empty() {
360 if let Ok(model) = fs::read_to_string("/sys/class/drm/card0/device/model") {
362 let model = model.trim();
363 if !model.is_empty() {
364 gpus.push(GpuInfo {
365 name: model.to_string(),
366 vram_bytes: None,
367 });
368 }
369 }
370 }
371
372 gpus
373 }
374}
375
376#[cfg(test)]
377mod tests {
378 use super::*;
379
380 #[test]
381 fn test_gpu_info_format() {
382 let info = GpuInfo {
383 name: "NVIDIA GeForce RTX 4090".to_string(),
384 vram_bytes: Some(24 * 1024 * 1024 * 1024),
385 };
386 assert_eq!(info.format(), "NVIDIA GeForce RTX 4090 (24 GB)");
387
388 let info = GpuInfo {
389 name: "Intel Arc A770".to_string(),
390 vram_bytes: Some(16 * 1024 * 1024 * 1024),
391 };
392 assert_eq!(info.format(), "Intel Arc A770 (16 GB)");
393
394 let info = GpuInfo {
395 name: "Radeon 780M".to_string(),
396 vram_bytes: Some(512 * 1024 * 1024),
397 };
398 assert_eq!(info.format(), "Radeon 780M (512 MB)");
399
400 let info = GpuInfo {
401 name: "Generic GPU".to_string(),
402 vram_bytes: None,
403 };
404 assert_eq!(info.format(), "Generic GPU");
405 }
406
407 #[test]
408 fn test_improve_amd_gpu_name() {
409 assert_eq!(
410 improve_amd_gpu_name("AMD Radeon Phoenix1 Graphics"),
411 "Radeon 780M"
412 );
413 assert_eq!(improve_amd_gpu_name("AMD Rembrandt"), "Radeon 680M");
414 assert_eq!(improve_amd_gpu_name("Unknown GPU"), "Unknown GPU");
415 }
416
417 #[test]
418 fn test_parse_vram_str() {
419 assert_eq!(parse_vram_str("8 GB"), Some(8 * 1024 * 1024 * 1024));
420 assert_eq!(parse_vram_str("1536 MB"), Some(1536 * 1024 * 1024));
421 assert_eq!(parse_vram_str("512 MB"), Some(512 * 1024 * 1024));
422 assert_eq!(parse_vram_str("1024 KB"), Some(1024 * 1024));
423 assert_eq!(parse_vram_str("invalid"), None);
424 assert_eq!(parse_vram_str("8"), None);
425 }
426
427 #[test]
428 fn test_parse_system_profiler_displays() {
429 let mock_output = r#"
430Graphics/Displays:
431
432 Apple M1 Max:
433
434 Chipset Model: Apple M1 Max
435 Type: GPU
436 Bus: Built-In
437 Total Number of Cores: 32
438 Vendor: Apple (0x106b)
439 Metal Support: Metal 3
440 VRAM (Dynamic, Max): 8192 MB
441 Displays:
442 Color LCD:
443 Display Type: Built-In Retina LCD
444
445 Intel UHD Graphics 630:
446
447 Chipset Model: Intel UHD Graphics 630
448 Type: GPU
449 Bus: Built-In
450 VRAM (Total): 1536 MB
451 Vendor: Intel (0x8086)
452"#;
453 let gpus = parse_system_profiler_displays(mock_output);
454 assert_eq!(gpus.len(), 2);
455 assert_eq!(gpus[0].name, "Apple M1 Max");
456 assert_eq!(gpus[0].vram_bytes, Some(8192 * 1024 * 1024));
457 assert_eq!(gpus[1].name, "Intel UHD Graphics 630");
458 assert_eq!(gpus[1].vram_bytes, Some(1536 * 1024 * 1024));
459 }
460
461 #[test]
462 fn test_parse_wmi_videocontroller() {
463 let wmic_output = r#"
464AdapterRAM=4294967296
465Name=NVIDIA GeForce RTX 4090
466
467AdapterRAM=2147483648
468Name=Intel(R) UHD Graphics 770
469"#;
470 let gpus = parse_wmi_videocontroller(wmic_output);
471 assert_eq!(gpus.len(), 2);
472 assert_eq!(gpus[0].name, "NVIDIA GeForce RTX 4090");
473 assert_eq!(gpus[0].vram_bytes, Some(4294967296));
474 assert_eq!(gpus[1].name, "Intel(R) UHD Graphics 770");
475 assert_eq!(gpus[1].vram_bytes, Some(2147483648));
476
477 let powershell_output = r#"
478Name : NVIDIA GeForce RTX 4090
479AdapterRAM : 4294967296
480
481Name : Intel(R) UHD Graphics 770
482AdapterRAM : 2147483648
483"#;
484 let gpus = parse_wmi_videocontroller(powershell_output);
485 assert_eq!(gpus.len(), 2);
486 assert_eq!(gpus[0].name, "NVIDIA GeForce RTX 4090");
487 assert_eq!(gpus[0].vram_bytes, Some(4294967296));
488 assert_eq!(gpus[1].name, "Intel(R) UHD Graphics 770");
489 assert_eq!(gpus[1].vram_bytes, Some(2147483648));
490 }
491}