1mod drm;
26mod metal;
27mod nvidia;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31#[non_exhaustive]
32pub enum Vendor {
33 Nvidia,
34 Amd,
35 Intel,
36 Apple,
37 Unknown,
38}
39
40impl std::fmt::Display for Vendor {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 f.write_str(match self {
43 Vendor::Nvidia => "NVIDIA",
44 Vendor::Amd => "AMD",
45 Vendor::Intel => "Intel",
46 Vendor::Apple => "Apple",
47 Vendor::Unknown => "Unknown",
48 })
49 }
50}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
54#[non_exhaustive]
55pub struct GpuInfo {
56 pub name: String,
58 pub vendor: Vendor,
60 pub total_bytes: u64,
64 pub free_bytes: Option<u64>,
66 pub used_bytes: Option<u64>,
68}
69
70impl std::fmt::Display for GpuInfo {
71 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72 write!(
73 f,
74 "{} ({}): {:.1} GiB total",
75 self.name,
76 self.vendor,
77 gib(self.total_bytes)
78 )?;
79 if let Some(free) = self.free_bytes {
80 write!(f, ", {:.1} GiB free", gib(free))?;
81 }
82 Ok(())
83 }
84}
85
86#[allow(clippy::cast_precision_loss)] fn gib(bytes: u64) -> f64 {
88 bytes as f64 / (1024.0 * 1024.0 * 1024.0)
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
103pub struct ComputeCapability {
104 pub major: u32,
106 pub minor: u32,
108}
109
110impl ComputeCapability {
111 #[must_use]
113 pub const fn new(major: u32, minor: u32) -> Self {
114 Self { major, minor }
115 }
116}
117
118impl std::fmt::Display for ComputeCapability {
119 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121 write!(f, "{}.{}", self.major, self.minor)
122 }
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
136pub struct CudaVersion {
137 pub major: u32,
139 pub minor: u32,
141}
142
143impl CudaVersion {
144 #[must_use]
146 pub const fn new(major: u32, minor: u32) -> Self {
147 Self { major, minor }
148 }
149}
150
151impl std::fmt::Display for CudaVersion {
152 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154 write!(f, "{}.{}", self.major, self.minor)
155 }
156}
157
158#[derive(Debug, Clone, Copy, PartialEq, Eq)]
164#[non_exhaustive]
165pub struct CudaHost {
166 pub compute_capability: ComputeCapability,
168 pub driver_version: CudaVersion,
170}
171
172#[must_use]
178pub fn detect() -> Vec<GpuInfo> {
179 let mut gpus = Vec::new();
180 gpus.extend(nvidia::detect());
181 gpus.extend(drm::detect());
182 gpus.extend(metal::detect());
183 gpus
184}
185
186#[must_use]
208pub fn cuda_host() -> Option<CudaHost> {
209 nvidia::cuda_host()
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215
216 #[test]
217 fn detect_never_panics() {
218 for gpu in detect() {
221 assert!(!gpu.name.is_empty());
222 let _ = gpu.to_string();
223 }
224 }
225
226 #[test]
227 fn display_includes_free_when_present() {
228 let gpu = GpuInfo {
229 name: "Test GPU".to_string(),
230 vendor: Vendor::Nvidia,
231 total_bytes: 24 * 1024 * 1024 * 1024,
232 free_bytes: Some(12 * 1024 * 1024 * 1024),
233 used_bytes: Some(12 * 1024 * 1024 * 1024),
234 };
235 let shown = gpu.to_string();
236 assert!(shown.contains("NVIDIA"));
237 assert!(shown.contains("24.0 GiB total"));
238 assert!(shown.contains("12.0 GiB free"));
239 }
240
241 #[test]
242 fn display_omits_free_when_absent() {
243 let gpu = GpuInfo {
244 name: "AMD GPU (card0)".to_string(),
245 vendor: Vendor::Amd,
246 total_bytes: 8 * 1024 * 1024 * 1024,
247 free_bytes: None,
248 used_bytes: None,
249 };
250 let shown = gpu.to_string();
251 assert!(shown.contains("8.0 GiB total"));
252 assert!(!shown.contains("free"));
253 }
254
255 #[test]
256 fn vendor_display_covers_every_variant() {
257 assert_eq!(Vendor::Nvidia.to_string(), "NVIDIA");
258 assert_eq!(Vendor::Amd.to_string(), "AMD");
259 assert_eq!(Vendor::Intel.to_string(), "Intel");
260 assert_eq!(Vendor::Apple.to_string(), "Apple");
261 assert_eq!(Vendor::Unknown.to_string(), "Unknown");
262 }
263
264 #[test]
265 fn gib_converts_using_binary_units() {
266 assert!((gib(0) - 0.0).abs() < f64::EPSILON);
267 assert!((gib(1024 * 1024 * 1024) - 1.0).abs() < f64::EPSILON);
268 assert!((gib(3 * 1024 * 1024 * 1024 / 2) - 1.5).abs() < f64::EPSILON);
270 }
271
272 #[test]
273 fn display_rounds_to_one_decimal_place() {
274 let gpu = GpuInfo {
276 name: "Rounding".to_string(),
277 vendor: Vendor::Nvidia,
278 total_bytes: 25 * 1024 * 1024 * 1024 + 256 * 1024 * 1024,
279 free_bytes: None,
280 used_bytes: None,
281 };
282 assert!(gpu.to_string().contains("25.2 GiB total"));
283 }
284
285 #[test]
286 fn detect_results_have_consistent_memory_fields() {
287 for gpu in detect() {
289 assert!(!gpu.name.is_empty());
290 if let Some(free) = gpu.free_bytes {
291 assert!(free <= gpu.total_bytes, "free must not exceed total");
292 }
293 if let (Some(free), Some(used)) = (gpu.free_bytes, gpu.used_bytes) {
294 assert!(
295 free.saturating_add(used) <= gpu.total_bytes.saturating_add(used),
296 "free/used must be coherent",
297 );
298 }
299 }
300 }
301
302 #[test]
303 fn versions_display_as_major_dot_minor() {
304 assert_eq!(ComputeCapability::new(8, 6).to_string(), "8.6");
305 assert_eq!(CudaVersion::new(12, 9).to_string(), "12.9");
306 assert_eq!(ComputeCapability::new(8, 10).to_string(), "8.10");
309 }
310
311 #[test]
312 fn versions_order_by_major_then_minor() {
313 assert!(ComputeCapability::new(8, 6) > ComputeCapability::new(8, 0));
314 assert!(ComputeCapability::new(9, 0) > ComputeCapability::new(8, 9));
315 assert_eq!(ComputeCapability::new(8, 6), ComputeCapability::new(8, 6));
316 assert!(CudaVersion::new(12, 9) > CudaVersion::new(12, 0));
317 assert!(CudaVersion::new(13, 0) > CudaVersion::new(12, 9));
318 assert!(ComputeCapability::new(9, 0) > ComputeCapability::new(8, 10));
321 }
322
323 #[test]
324 fn cuda_host_is_environment_dependent_but_coherent() {
325 if let Some(cuda) = cuda_host() {
327 assert!(
328 cuda.compute_capability.major > 0,
329 "a real device has a nonzero major capability",
330 );
331 assert!(cuda.driver_version.major > 0, "a real driver has a version");
332 assert_eq!(
333 cuda_host(),
334 Some(cuda),
335 "host/driver properties must be stable across calls",
336 );
337 }
338 }
339
340 #[test]
341 fn gpu_info_equality_compares_all_fields() {
342 let base = GpuInfo {
343 name: "G".to_string(),
344 vendor: Vendor::Intel,
345 total_bytes: 16 * 1024 * 1024 * 1024,
346 free_bytes: None,
347 used_bytes: None,
348 };
349 assert_eq!(base.clone(), base);
350 let mut other = base.clone();
351 other.vendor = Vendor::Amd;
352 assert_ne!(base, other);
353 }
354}