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