retch_sysinfo/gpu_api.rs
1// SPDX-FileCopyrightText: 2026 Ken Tobias
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4//! Graphics and compute API versions: Vulkan, OpenGL and OpenCL.
5//!
6//! The last user-visible gap against fastfetch (NOTES.md §6). Each API is reached by
7//! `dlopen`ing its loader at runtime rather than linking it, for three reasons:
8//!
9//! 1. **Linking would make the libraries hard requirements.** A machine without Vulkan
10//! must still run retch; a `#[link]` on `libvulkan` would refuse to start.
11//! 2. **They are genuinely optional.** Absence is a normal answer ("no Vulkan here"),
12//! not an error, so the field is simply omitted.
13//! 3. It keeps the crate free of new dependencies, matching the hand-written FFI house
14//! style used for the Windows and macOS probes.
15//!
16//! # These probes never modify the process environment
17//!
18//! Mesa's **rusticl** OpenCL driver is opt-in via `RUSTICL_ENABLE`: without it the ICD
19//! still registers a platform advertising OpenCL 3.0 while exposing **zero devices**.
20//! It is tempting to set that variable in-process before loading the ICD so the field
21//! looks better. This module deliberately does not, for two reasons:
22//!
23//! - **It would be a data race.** Fields are collected inside a `std::thread::scope`, and
24//! mutating the environment while sibling threads read it is unsound. `std::env::set_var`
25//! became `unsafe` in Rust 2024 precisely for this; this crate is on edition 2021, where
26//! it still compiles silently — a trap rather than a compile error.
27//! - **It would report something false.** A device visible only because retch enabled it
28//! for itself is not a device the user's own programs can use.
29//!
30//! So the OpenCL field reports the device count it actually observes, and says when that
31//! count is zero. fastfetch prints a bare `OpenCL: 3.0` in both states — i.e. it reports a
32//! working stack when nothing can run on it. Under-reporting beats asserting something
33//! false, the same call as the `Users: 0` suppression (v0.6.1) and the v0.7.0 input
34//! classification.
35
36#[cfg(target_os = "linux")]
37use std::ffi::c_int;
38#[cfg(any(target_os = "linux", target_os = "windows"))]
39use std::ffi::{c_char, c_void, CStr};
40
41/// Versions reported by each graphics/compute API present on the system.
42///
43/// A `None` means the loader is absent or answered nothing usable — both are normal.
44#[derive(Debug, Default, Clone, PartialEq, Eq)]
45pub struct GpuApis {
46 /// Vulkan: device `apiVersion`, driver name and driver info, e.g.
47 /// `1.4.354 - radv [Mesa 26.1.8]`.
48 pub vulkan: Option<String>,
49 /// OpenGL: the `GL_VERSION` string of a headless context, e.g.
50 /// `4.6 (Compatibility Profile) Mesa 26.1.8`.
51 pub opengl: Option<String>,
52 /// OpenCL: platform version, provider, and what device (if any) is actually exposed.
53 pub opencl: Option<String>,
54}
55
56/// Decode a packed Vulkan version into `major.minor.patch`.
57///
58/// Vulkan packs the version as `variant:3 | major:7 | minor:10 | patch:12`. The variant
59/// field is deliberately ignored: it is non-zero only for non-Khronos derivatives, and
60/// including it would print a leading number no user recognises.
61pub fn format_vulkan_version(packed: u32) -> String {
62 let major = (packed >> 22) & 0x7F;
63 let minor = (packed >> 12) & 0x3FF;
64 let patch = packed & 0xFFF;
65 format!("{major}.{minor}.{patch}")
66}
67
68/// Rank a Vulkan `VkPhysicalDeviceType` so the most capable real device wins.
69///
70/// Lower is better. The ordering is load-bearing rather than cosmetic: a machine with a
71/// real GPU almost always *also* exposes Mesa's `llvmpipe` software rasteriser as a
72/// `CPU` device, so picking the first enumerated device would report software rendering
73/// on a box with a perfectly good GPU. Observed on this hardware: the AMD 780M enumerates
74/// as `INTEGRATED_GPU` (1) alongside `llvmpipe` as `CPU` (4).
75pub fn device_type_rank(device_type: u32) -> u8 {
76 match device_type {
77 2 => 0, // DISCRETE_GPU
78 1 => 1, // INTEGRATED_GPU
79 3 => 2, // VIRTUAL_GPU
80 4 => 4, // CPU (software rasteriser — a last resort, never a preference)
81 _ => 3, // OTHER
82 }
83}
84
85/// Render the Vulkan field from its parts.
86///
87/// `driver_name`/`driver_info` are empty when the driver did not fill the
88/// `VkPhysicalDeviceDriverProperties` chain, which happens on any instance created below
89/// Vulkan 1.2 — silently, with no error. The version alone is still worth printing.
90pub fn format_vulkan(version: &str, driver_name: &str, driver_info: &str) -> String {
91 match (driver_name.trim(), driver_info.trim()) {
92 ("", _) => version.to_string(),
93 (name, "") => format!("{version} - {name}"),
94 (name, info) => format!("{version} - {name} [{info}]"),
95 }
96}
97
98/// Render the OpenCL field, distinguishing "usable" from "present but inert".
99///
100/// A platform that advertises a version while exposing no device cannot run anything, so
101/// saying so is the whole point of the field. See the module docs for why this does not
102/// simply enable rusticl for itself and report the better-looking answer.
103pub fn format_opencl(version: &str, platform: &str, device: Option<&str>) -> String {
104 // CL_PLATFORM_VERSION is specified to start with "OpenCL <major>.<minor>", so the raw
105 // string would render as "OpenCL: OpenCL 3.0" under the field's own label.
106 let version = version
107 .trim()
108 .strip_prefix("OpenCL ")
109 .unwrap_or(version.trim())
110 .trim();
111 let platform = platform.trim();
112 match device {
113 Some(d) if !d.trim().is_empty() => {
114 if platform.is_empty() {
115 format!("{version} ({})", d.trim())
116 } else {
117 format!("{version} - {platform} ({})", d.trim())
118 }
119 }
120 _ => {
121 if platform.is_empty() {
122 format!("{version} (no device enabled)")
123 } else {
124 format!("{version} - {platform} (no device enabled)")
125 }
126 }
127 }
128}
129
130/// Shorten a driver-reported device name to the part a human recognises.
131///
132/// Mesa reports OpenCL and GL device names with a full driver descriptor appended, e.g.
133/// `AMD Radeon 780M Graphics (radeonsi, phoenix, ACO, DRM 3.64, 7.1.13-200.fc44.x86_64)`.
134/// That is 80+ characters of kernel and driver detail that pushes the line into wrapping
135/// and tells the reader nothing the `GPU` field does not already say, so everything from
136/// the first parenthesised descriptor on is dropped.
137pub fn shorten_device_name(name: &str) -> String {
138 match name.find(" (") {
139 Some(i) => name[..i].trim().to_string(),
140 None => name.trim().to_string(),
141 }
142}
143
144/// Trim a NUL-terminated fixed-size C string field into a `String`.
145///
146/// Reads up to the first NUL and ignores the rest of the buffer. Returns an empty string
147/// when the field was never written, which is how an unfilled `pNext` chain presents.
148pub fn cstr_field(buf: &[u8]) -> String {
149 let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
150 String::from_utf8_lossy(&buf[..end]).into_owned()
151}
152
153// ---------------------------------------------------------------------------
154// Runtime loader — one interface, two backends
155// ---------------------------------------------------------------------------
156
157/// Runtime library loading, presenting the same `open`/`sym`/`close` interface on every
158/// platform so the probes above it need no `cfg` of their own.
159///
160/// The probes are the same code on Linux and Windows — the Vulkan and OpenCL APIs are
161/// identical, and only the loader's *name* differs — so the platform split lives here
162/// rather than being duplicated per API. A second copy of the
163/// `VkPhysicalDeviceProperties2` offset arithmetic is exactly the drift that the shared
164/// `win_setupapi` and `win_iftable` modules exist to prevent.
165#[cfg(target_os = "linux")]
166mod dl {
167 use super::*;
168
169 extern "C" {
170 pub fn dlopen(filename: *const c_char, flags: c_int) -> *mut c_void;
171 pub fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void;
172 pub fn dlclose(handle: *mut c_void) -> c_int;
173 }
174 pub const RTLD_NOW: c_int = 2;
175 pub const RTLD_LOCAL: c_int = 0;
176
177 /// Open a shared library by soname, or `None` if it is not installed.
178 ///
179 /// `RTLD_LOCAL` keeps the symbols out of the global namespace so loading, say, a
180 /// software OpenCL ICD cannot shadow symbols another probe resolves later.
181 pub fn open(soname: &CStr) -> Option<*mut c_void> {
182 // SAFETY: `soname` is a valid NUL-terminated C string for the duration of the
183 // call. A null return is the documented "not found" answer and is handled.
184 let h = unsafe { dlopen(soname.as_ptr(), RTLD_NOW | RTLD_LOCAL) };
185 (!h.is_null()).then_some(h)
186 }
187
188 /// Resolve a symbol, or `None` if the library does not export it.
189 pub fn sym(handle: *mut c_void, name: &CStr) -> Option<*mut c_void> {
190 // SAFETY: `handle` came from `open` above and has not been closed; `name` is a
191 // valid NUL-terminated C string.
192 let p = unsafe { dlsym(handle, name.as_ptr()) };
193 (!p.is_null()).then_some(p)
194 }
195
196 /// Close a handle opened by [`open`].
197 pub fn close(handle: *mut c_void) {
198 // SAFETY: `handle` came from `open` and is not used afterwards.
199 unsafe {
200 dlclose(handle);
201 }
202 }
203}
204
205/// Windows backend for the loader interface above.
206///
207/// `LoadLibraryA` rather than `LoadLibraryW`: the names are ASCII DLL filenames resolved
208/// through the standard search order, so widening them would buy nothing and would mean
209/// converting a `CStr` the callers already hold. `media.rs`'s `combase.dll` bootstrap is
210/// the precedent for loading a system DLL at runtime rather than linking it.
211///
212/// There is no `RTLD_LOCAL` equivalent to worry about — Windows does not have the global
213/// symbol namespace that flag exists to avoid polluting.
214#[cfg(target_os = "windows")]
215mod dl {
216 use super::*;
217
218 #[link(name = "kernel32")]
219 extern "system" {
220 fn LoadLibraryA(lp_lib_file_name: *const c_char) -> *mut c_void;
221 fn GetProcAddress(h_module: *mut c_void, lp_proc_name: *const c_char) -> *mut c_void;
222 fn FreeLibrary(h_module: *mut c_void) -> i32;
223 }
224
225 /// Open a DLL by name, or `None` if it is not installed.
226 ///
227 /// A missing loader is the normal answer on a machine without that API — a headless
228 /// server, or one with no GPU driver — not an error.
229 pub fn open(name: &CStr) -> Option<*mut c_void> {
230 // SAFETY: `name` is a valid NUL-terminated C string for the duration of the call.
231 // A null return is the documented "not found" answer and is handled.
232 let h = unsafe { LoadLibraryA(name.as_ptr()) };
233 (!h.is_null()).then_some(h)
234 }
235
236 /// Resolve an exported symbol, or `None` if the DLL does not export it.
237 pub fn sym(handle: *mut c_void, name: &CStr) -> Option<*mut c_void> {
238 // SAFETY: `handle` came from `open` above and has not been freed; `name` is a
239 // valid NUL-terminated C string.
240 let p = unsafe { GetProcAddress(handle, name.as_ptr()) };
241 (!p.is_null()).then_some(p)
242 }
243
244 /// Release a handle opened by [`open`].
245 pub fn close(handle: *mut c_void) {
246 // SAFETY: `handle` came from `open` and is not used afterwards.
247 unsafe {
248 FreeLibrary(handle);
249 }
250 }
251}
252
253/// The Vulkan loader's filename on this platform.
254#[cfg(target_os = "linux")]
255const VULKAN_LIB: &CStr = c"libvulkan.so.1";
256/// `vulkan-1.dll` is the Khronos loader's fixed name on Windows, installed by every
257/// conformant driver into `System32`.
258#[cfg(target_os = "windows")]
259const VULKAN_LIB: &CStr = c"vulkan-1.dll";
260
261/// The OpenCL ICD loader's filename on this platform.
262#[cfg(target_os = "linux")]
263const OPENCL_LIB: &CStr = c"libOpenCL.so.1";
264/// `OpenCL.dll` is the Khronos ICD loader on Windows; vendor drivers register themselves
265/// with it rather than being opened directly.
266#[cfg(target_os = "windows")]
267const OPENCL_LIB: &CStr = c"OpenCL.dll";
268
269#[cfg(any(target_os = "linux", target_os = "windows"))]
270mod vulkan {
271 use super::dl;
272 use super::*;
273
274 const VK_STRUCTURE_TYPE_APPLICATION_INFO: u32 = 0;
275 const VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO: u32 = 1;
276 const VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2: u32 = 1000059001;
277 const VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES: u32 = 1000196000;
278
279 /// `VkPhysicalDeviceProperties2` places `properties` after `sType` + padding + `pNext`.
280 const PROPS2_BODY: usize = 16;
281 /// Offsets within `VkPhysicalDeviceProperties`.
282 const OFF_API_VERSION: usize = 0;
283 const OFF_DEVICE_TYPE: usize = 16;
284 const OFF_DEVICE_NAME: usize = 20;
285 /// Comfortably larger than `sizeof(VkPhysicalDeviceProperties)` (~824 bytes). The
286 /// struct embeds `VkPhysicalDeviceLimits` (100+ fields) that this probe never reads,
287 /// so it is handled as a sized byte buffer with documented offsets — the same approach
288 /// `memory.rs` uses for SMBIOS type-17 and `win_iftable.rs` for `MIB_IF_ROW2`.
289 const PROPS_BUF: usize = 1024;
290
291 /// Offsets within `VkPhysicalDeviceDriverProperties`.
292 const OFF_DRIVER_NAME: usize = 20;
293 const OFF_DRIVER_INFO: usize = 276;
294 const DRIVER_BUF: usize = 560;
295 const VK_MAX_NAME: usize = 256;
296
297 #[repr(C)]
298 struct AppInfo {
299 s_type: u32,
300 p_next: *const c_void,
301 app_name: *const c_char,
302 app_version: u32,
303 engine_name: *const c_char,
304 engine_version: u32,
305 api_version: u32,
306 }
307
308 #[repr(C)]
309 struct InstanceCreateInfo {
310 s_type: u32,
311 p_next: *const c_void,
312 flags: u32,
313 app_info: *const AppInfo,
314 layer_count: u32,
315 layer_names: *const *const c_char,
316 ext_count: u32,
317 ext_names: *const *const c_char,
318 }
319
320 type VkCreateInstance =
321 unsafe extern "C" fn(*const InstanceCreateInfo, *const c_void, *mut *mut c_void) -> i32;
322 type VkDestroyInstance = unsafe extern "C" fn(*mut c_void, *const c_void);
323 type VkEnumeratePhysicalDevices =
324 unsafe extern "C" fn(*mut c_void, *mut u32, *mut *mut c_void) -> i32;
325 type VkGetPhysicalDeviceProperties2 = unsafe extern "C" fn(*mut c_void, *mut c_void);
326 type VkGetInstanceProcAddr = unsafe extern "C" fn(*mut c_void, *const c_char) -> *mut c_void;
327
328 /// Query the best physical device's API version and driver identity.
329 ///
330 /// Returns `None` when Vulkan is absent, no instance can be created, or no device is
331 /// present — all normal on a headless or GPU-less machine.
332 pub fn detect() -> Option<String> {
333 let lib = dl::open(VULKAN_LIB)?;
334 let result = detect_with(lib);
335 dl::close(lib);
336 result
337 }
338
339 fn detect_with(lib: *mut c_void) -> Option<String> {
340 let create = dl::sym(lib, c"vkCreateInstance")?;
341 let gipa = dl::sym(lib, c"vkGetInstanceProcAddr")?;
342
343 // SAFETY: every pointer below is either freshly resolved from the Vulkan loader or
344 // a local we own. Buffers passed to the driver are sized at or above the structs
345 // the API writes, and every returned code is checked before the result is read.
346 unsafe {
347 let create: VkCreateInstance = std::mem::transmute(create);
348 let gipa: VkGetInstanceProcAddr = std::mem::transmute(gipa);
349
350 let app = AppInfo {
351 s_type: VK_STRUCTURE_TYPE_APPLICATION_INFO,
352 p_next: std::ptr::null(),
353 app_name: c"retch".as_ptr(),
354 app_version: 0,
355 engine_name: std::ptr::null(),
356 engine_version: 0,
357 // Must be >= 1.2. With a 1.0 or 1.1 instance the driver SILENTLY IGNORES
358 // the `VkPhysicalDeviceDriverProperties` chain below and the driver name
359 // and info come back as empty strings with no error anywhere — verified
360 // against a 1.0 instance, which returned the right version and blank
361 // driver fields.
362 api_version: (1 << 22) | (2 << 12),
363 };
364 let ci = InstanceCreateInfo {
365 s_type: VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
366 p_next: std::ptr::null(),
367 flags: 0,
368 app_info: &app,
369 layer_count: 0,
370 layer_names: std::ptr::null(),
371 ext_count: 0,
372 ext_names: std::ptr::null(),
373 };
374
375 let mut instance: *mut c_void = std::ptr::null_mut();
376 if create(&ci, std::ptr::null(), &mut instance) != 0 || instance.is_null() {
377 return None;
378 }
379
380 let out = read_best_device(instance, gipa);
381
382 if let Some(p) = dl::sym(lib, c"vkDestroyInstance") {
383 let destroy: VkDestroyInstance = std::mem::transmute(p);
384 destroy(instance, std::ptr::null());
385 }
386 out
387 }
388 }
389
390 /// SAFETY: caller guarantees `instance` is a live `VkInstance` and `gipa` is the
391 /// loader's `vkGetInstanceProcAddr`.
392 unsafe fn read_best_device(
393 instance: *mut c_void,
394 gipa: VkGetInstanceProcAddr,
395 ) -> Option<String> {
396 let enum_ptr = gipa(instance, c"vkEnumeratePhysicalDevices".as_ptr());
397 let props_ptr = gipa(instance, c"vkGetPhysicalDeviceProperties2".as_ptr());
398 if enum_ptr.is_null() || props_ptr.is_null() {
399 return None;
400 }
401 let enumerate: VkEnumeratePhysicalDevices = std::mem::transmute(enum_ptr);
402 let get_props2: VkGetPhysicalDeviceProperties2 = std::mem::transmute(props_ptr);
403
404 let mut count: u32 = 0;
405 if enumerate(instance, &mut count, std::ptr::null_mut()) != 0 || count == 0 {
406 return None;
407 }
408 let mut devices = vec![std::ptr::null_mut::<c_void>(); count as usize];
409 if enumerate(instance, &mut count, devices.as_mut_ptr()) != 0 {
410 return None;
411 }
412
413 let mut best: Option<(u8, String)> = None;
414 for device in devices.iter().take(count as usize) {
415 let mut driver = vec![0u8; DRIVER_BUF];
416 driver[0..4].copy_from_slice(
417 &VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES.to_ne_bytes(),
418 );
419 let mut props = vec![0u8; PROPS2_BODY + PROPS_BUF];
420 props[0..4]
421 .copy_from_slice(&VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2.to_ne_bytes());
422 let chain = driver.as_mut_ptr() as usize;
423 props[8..16].copy_from_slice(&chain.to_ne_bytes());
424
425 get_props2(*device, props.as_mut_ptr() as *mut c_void);
426
427 let at = |off: usize| -> u32 {
428 let s = PROPS2_BODY + off;
429 u32::from_ne_bytes(props[s..s + 4].try_into().unwrap_or([0; 4]))
430 };
431 let api = at(OFF_API_VERSION);
432 let dtype = at(OFF_DEVICE_TYPE);
433 let name_start = PROPS2_BODY + OFF_DEVICE_NAME;
434 let _device_name = cstr_field(&props[name_start..name_start + VK_MAX_NAME]);
435
436 let driver_name = cstr_field(&driver[OFF_DRIVER_NAME..OFF_DRIVER_NAME + VK_MAX_NAME]);
437 let driver_info = cstr_field(&driver[OFF_DRIVER_INFO..OFF_DRIVER_INFO + VK_MAX_NAME]);
438
439 let rank = device_type_rank(dtype);
440 let rendered = format_vulkan(&format_vulkan_version(api), &driver_name, &driver_info);
441 if best.as_ref().is_none_or(|(r, _)| rank < *r) {
442 best = Some((rank, rendered));
443 }
444 }
445 best.map(|(_, s)| s)
446 }
447}
448
449#[cfg(target_os = "linux")]
450mod opengl {
451 use super::dl;
452 use super::*;
453
454 const EGL_OPENGL_API: u32 = 0x30A2;
455 const EGL_NONE: i32 = 0x3038;
456 const EGL_SURFACE_TYPE: i32 = 0x3033;
457 const EGL_PBUFFER_BIT: i32 = 0x0001;
458 const EGL_RENDERABLE_TYPE: i32 = 0x3040;
459 const EGL_OPENGL_BIT: i32 = 0x0008;
460 const GL_VERSION: u32 = 0x1F02;
461
462 type EglGetDisplay = unsafe extern "C" fn(*mut c_void) -> *mut c_void;
463 type EglInitialize = unsafe extern "C" fn(*mut c_void, *mut i32, *mut i32) -> u32;
464 type EglBindApi = unsafe extern "C" fn(u32) -> u32;
465 type EglChooseConfig =
466 unsafe extern "C" fn(*mut c_void, *const i32, *mut *mut c_void, i32, *mut i32) -> u32;
467 type EglCreateContext =
468 unsafe extern "C" fn(*mut c_void, *mut c_void, *mut c_void, *const i32) -> *mut c_void;
469 type EglMakeCurrent =
470 unsafe extern "C" fn(*mut c_void, *mut c_void, *mut c_void, *mut c_void) -> u32;
471 type EglGetProcAddress = unsafe extern "C" fn(*const c_char) -> *mut c_void;
472 type EglTerminate = unsafe extern "C" fn(*mut c_void) -> u32;
473 type GlGetString = unsafe extern "C" fn(u32) -> *const c_char;
474
475 /// Read `GL_VERSION` from a headless EGL context.
476 ///
477 /// Deliberately uses EGL with `EGL_DEFAULT_DISPLAY` and a surfaceless
478 /// `eglMakeCurrent`, so this works with no X or Wayland connection and without
479 /// touching the environment. GLX would require a display server.
480 ///
481 /// **The context choice decides the number printed.** Passing no attribute list asks
482 /// for the driver's default, which is the highest *compatibility* profile — matching
483 /// what fastfetch reports. Requesting a core profile instead reports a different
484 /// string for the same machine (`glxinfo -B` says `4.6 (Core Profile)` here where this
485 /// returns `4.6 (Compatibility Profile)`), so the choice is deliberate, not incidental.
486 pub fn detect() -> Option<String> {
487 let lib = dl::open(c"libEGL.so.1")?;
488 let out = detect_with(lib);
489 dl::close(lib);
490 out
491 }
492
493 fn detect_with(lib: *mut c_void) -> Option<String> {
494 let get_display = dl::sym(lib, c"eglGetDisplay")?;
495 let initialize = dl::sym(lib, c"eglInitialize")?;
496 let bind_api = dl::sym(lib, c"eglBindAPI")?;
497 let choose = dl::sym(lib, c"eglChooseConfig")?;
498 let create_context = dl::sym(lib, c"eglCreateContext")?;
499 let make_current = dl::sym(lib, c"eglMakeCurrent")?;
500 let get_proc = dl::sym(lib, c"eglGetProcAddress")?;
501
502 // SAFETY: all pointers are freshly resolved from libEGL or locals we own. Every
503 // EGL call's status is checked before its output is used, and the display is
504 // terminated on the success path.
505 unsafe {
506 let get_display: EglGetDisplay = std::mem::transmute(get_display);
507 let initialize: EglInitialize = std::mem::transmute(initialize);
508 let bind_api: EglBindApi = std::mem::transmute(bind_api);
509 let choose: EglChooseConfig = std::mem::transmute(choose);
510 let create_context: EglCreateContext = std::mem::transmute(create_context);
511 let make_current: EglMakeCurrent = std::mem::transmute(make_current);
512 let get_proc: EglGetProcAddress = std::mem::transmute(get_proc);
513
514 // EGL_DEFAULT_DISPLAY is a null handle.
515 let display = get_display(std::ptr::null_mut());
516 if display.is_null() {
517 return None;
518 }
519 let (mut major, mut minor) = (0i32, 0i32);
520 if initialize(display, &mut major, &mut minor) == 0 {
521 return None;
522 }
523 // Desktop GL specifically; an ES-only stack answers 0 here and is reported as
524 // "no OpenGL" rather than being silently downgraded to an ES version string.
525 if bind_api(EGL_OPENGL_API) == 0 {
526 terminate(lib, display);
527 return None;
528 }
529
530 let attrs = [
531 EGL_SURFACE_TYPE,
532 EGL_PBUFFER_BIT,
533 EGL_RENDERABLE_TYPE,
534 EGL_OPENGL_BIT,
535 EGL_NONE,
536 ];
537 let mut config: *mut c_void = std::ptr::null_mut();
538 let mut configs = 0i32;
539 if choose(display, attrs.as_ptr(), &mut config, 1, &mut configs) == 0 || configs == 0 {
540 terminate(lib, display);
541 return None;
542 }
543 let context = create_context(display, config, std::ptr::null_mut(), std::ptr::null());
544 if context.is_null() {
545 terminate(lib, display);
546 return None;
547 }
548 if make_current(display, std::ptr::null_mut(), std::ptr::null_mut(), context) == 0 {
549 terminate(lib, display);
550 return None;
551 }
552 let gl_get_string = get_proc(c"glGetString".as_ptr());
553 let version = if gl_get_string.is_null() {
554 None
555 } else {
556 let gl_get_string: GlGetString = std::mem::transmute(gl_get_string);
557 let p = gl_get_string(GL_VERSION);
558 if p.is_null() {
559 None
560 } else {
561 Some(CStr::from_ptr(p).to_string_lossy().into_owned())
562 }
563 };
564 terminate(lib, display);
565 version.filter(|v| !v.trim().is_empty())
566 }
567 }
568
569 /// Best-effort `eglTerminate`; failure to release is not worth reporting to the user.
570 ///
571 /// SAFETY: `display` is a live EGL display obtained from `eglGetDisplay`.
572 unsafe fn terminate(lib: *mut c_void, display: *mut c_void) {
573 if let Some(p) = dl::sym(lib, c"eglTerminate") {
574 let terminate: EglTerminate = std::mem::transmute(p);
575 terminate(display);
576 }
577 }
578}
579
580/// Windows OpenGL, via WGL against a hidden window.
581///
582/// **Why this is a separate module rather than a wider `cfg` on the EGL one.** Vulkan and
583/// OpenCL are the same code on both platforms because those APIs are identical and only the
584/// loader's filename differs. OpenGL is not: the Linux path gets a context from EGL with no
585/// window and no display server, and **stock Windows ships no `libEGL.dll`** — verified on a
586/// Windows 11 box carrying `vulkan-1.dll`, `opengl32.dll` and `OpenCL.dll` in `System32`
587/// with no EGL at all. Windows has no headless equivalent in the base OS: WGL requires a
588/// device context, a device context requires a window, and a window requires a window class.
589/// So this is a genuinely different mechanism reaching the same `glGetString(GL_VERSION)`.
590///
591/// **The window is never shown.** It is created without `WS_VISIBLE` and `ShowWindow` is
592/// never called, so nothing appears on screen — a fetch tool that flashed a window on every
593/// run would be broken. This is asserted rather than assumed: see the visibility check
594/// recorded in NOTES for v0.13.0, which enumerates top-level windows during a run.
595///
596/// `user32` and `gdi32` are linked rather than loaded at runtime, unlike the graphics
597/// loaders: they are core OS libraries always present on any Windows that can run the
598/// binary at all, and `display.rs` already links `user32` on the same grounds. `opengl32`
599/// *is* loaded at runtime, because a machine with no OpenGL ICD is a real case and must
600/// yield an absent field rather than a failure.
601#[cfg(target_os = "windows")]
602mod opengl {
603 use super::dl;
604 use super::*;
605
606 const GL_VERSION: u32 = 0x1F02;
607
608 // PIXELFORMATDESCRIPTOR.dwFlags
609 const PFD_DOUBLEBUFFER: u32 = 0x0000_0001;
610 const PFD_DRAW_TO_WINDOW: u32 = 0x0000_0004;
611 const PFD_SUPPORT_OPENGL: u32 = 0x0000_0020;
612 /// `PFD_TYPE_RGBA`.
613 const PFD_TYPE_RGBA: u8 = 0;
614 /// `PFD_MAIN_PLANE`.
615 const PFD_MAIN_PLANE: u8 = 0;
616
617 /// `WS_OVERLAPPED` is literally zero — the absence of `WS_VISIBLE` is what keeps the
618 /// window off screen, so it is spelled out rather than left implicit.
619 const WS_OVERLAPPED: u32 = 0x0000_0000;
620
621 /// `PIXELFORMATDESCRIPTOR`, 40 bytes. Only a handful of fields are set; the rest must
622 /// be zero, which is what `ChoosePixelFormat` expects for "don't care".
623 #[repr(C)]
624 #[derive(Default)]
625 struct PixelFormatDescriptor {
626 n_size: u16,
627 n_version: u16,
628 dw_flags: u32,
629 i_pixel_type: u8,
630 c_color_bits: u8,
631 c_red_bits: u8,
632 c_red_shift: u8,
633 c_green_bits: u8,
634 c_green_shift: u8,
635 c_blue_bits: u8,
636 c_blue_shift: u8,
637 c_alpha_bits: u8,
638 c_alpha_shift: u8,
639 c_accum_bits: u8,
640 c_accum_red_bits: u8,
641 c_accum_green_bits: u8,
642 c_accum_blue_bits: u8,
643 c_accum_alpha_bits: u8,
644 c_depth_bits: u8,
645 c_stencil_bits: u8,
646 c_aux_buffers: u8,
647 i_layer_type: u8,
648 b_reserved: u8,
649 dw_layer_mask: u32,
650 dw_visible_mask: u32,
651 dw_damage_mask: u32,
652 }
653
654 /// `WNDCLASSW`, 72 bytes on x64. `lpfnWndProc` points at `DefWindowProcW`: the window
655 /// never receives messages we care about, but a class still needs a procedure.
656 #[repr(C)]
657 struct WndClassW {
658 style: u32,
659 lpfn_wnd_proc: *const c_void,
660 cb_cls_extra: i32,
661 cb_wnd_extra: i32,
662 h_instance: *mut c_void,
663 h_icon: *mut c_void,
664 h_cursor: *mut c_void,
665 hbr_background: *mut c_void,
666 lpsz_menu_name: *const u16,
667 lpsz_class_name: *const u16,
668 }
669
670 #[link(name = "user32")]
671 extern "system" {
672 fn RegisterClassW(lp_wnd_class: *const WndClassW) -> u16;
673 fn UnregisterClassW(lp_class_name: *const u16, h_instance: *mut c_void) -> i32;
674 fn CreateWindowExW(
675 dw_ex_style: u32,
676 lp_class_name: *const u16,
677 lp_window_name: *const u16,
678 dw_style: u32,
679 x: i32,
680 y: i32,
681 n_width: i32,
682 n_height: i32,
683 h_wnd_parent: *mut c_void,
684 h_menu: *mut c_void,
685 h_instance: *mut c_void,
686 lp_param: *mut c_void,
687 ) -> *mut c_void;
688 fn DestroyWindow(h_wnd: *mut c_void) -> i32;
689 fn GetDC(h_wnd: *mut c_void) -> *mut c_void;
690 fn ReleaseDC(h_wnd: *mut c_void, h_dc: *mut c_void) -> i32;
691 fn DefWindowProcW(h_wnd: *mut c_void, msg: u32, w_param: usize, l_param: isize) -> isize;
692 }
693
694 #[link(name = "gdi32")]
695 extern "system" {
696 fn ChoosePixelFormat(h_dc: *mut c_void, ppfd: *const PixelFormatDescriptor) -> i32;
697 fn SetPixelFormat(
698 h_dc: *mut c_void,
699 format: i32,
700 ppfd: *const PixelFormatDescriptor,
701 ) -> i32;
702 }
703
704 type WglCreateContext = unsafe extern "system" fn(*mut c_void) -> *mut c_void;
705 type WglMakeCurrent = unsafe extern "system" fn(*mut c_void, *mut c_void) -> i32;
706 type WglDeleteContext = unsafe extern "system" fn(*mut c_void) -> i32;
707 type GlGetString = unsafe extern "system" fn(u32) -> *const c_char;
708
709 /// A hidden window plus its class, unregistered and destroyed on drop.
710 ///
711 /// Kept as a guard type so every early return unwinds the OS objects in the right
712 /// order. Doing it by hand at each `?` is how a window or class leaks — and a leaked
713 /// class makes a *second* run in the same process fail to register.
714 struct HiddenWindow {
715 class_name: Vec<u16>,
716 hwnd: *mut c_void,
717 hdc: *mut c_void,
718 }
719
720 impl HiddenWindow {
721 fn new() -> Option<Self> {
722 // A distinctive class name: it is unregistered on drop, so a collision would
723 // only matter if two probes ran concurrently in one process, which they do not.
724 let class_name: Vec<u16> = "retch_gl_probe\0".encode_utf16().collect();
725
726 let wc = WndClassW {
727 style: 0,
728 lpfn_wnd_proc: DefWindowProcW as *const c_void,
729 cb_cls_extra: 0,
730 cb_wnd_extra: 0,
731 h_instance: std::ptr::null_mut(),
732 h_icon: std::ptr::null_mut(),
733 h_cursor: std::ptr::null_mut(),
734 hbr_background: std::ptr::null_mut(),
735 lpsz_menu_name: std::ptr::null(),
736 lpsz_class_name: class_name.as_ptr(),
737 };
738
739 // SAFETY: `wc` is a fully initialised WNDCLASSW whose string pointer outlives
740 // the call, and every handle below is checked before use.
741 unsafe {
742 if RegisterClassW(&wc) == 0 {
743 return None;
744 }
745 // No WS_VISIBLE and no ShowWindow: the window exists only to own a device
746 // context, and must never appear on screen. 1x1 at the origin.
747 let hwnd = CreateWindowExW(
748 0,
749 class_name.as_ptr(),
750 std::ptr::null(),
751 WS_OVERLAPPED,
752 0,
753 0,
754 1,
755 1,
756 std::ptr::null_mut(),
757 std::ptr::null_mut(),
758 std::ptr::null_mut(),
759 std::ptr::null_mut(),
760 );
761 if hwnd.is_null() {
762 UnregisterClassW(class_name.as_ptr(), std::ptr::null_mut());
763 return None;
764 }
765 let hdc = GetDC(hwnd);
766 if hdc.is_null() {
767 DestroyWindow(hwnd);
768 UnregisterClassW(class_name.as_ptr(), std::ptr::null_mut());
769 return None;
770 }
771 Some(Self {
772 class_name,
773 hwnd,
774 hdc,
775 })
776 }
777 }
778 }
779
780 impl Drop for HiddenWindow {
781 fn drop(&mut self) {
782 // SAFETY: all three handles came from `new` and are released exactly once, in
783 // the reverse of the order they were acquired.
784 unsafe {
785 ReleaseDC(self.hwnd, self.hdc);
786 DestroyWindow(self.hwnd);
787 UnregisterClassW(self.class_name.as_ptr(), std::ptr::null_mut());
788 }
789 }
790 }
791
792 /// Read `GL_VERSION` from a WGL context on a hidden window.
793 ///
794 /// **The pixel format is what makes the context creatable**, and it must be set before
795 /// `wglCreateContext`: a device context with no pixel format cannot back a GL context,
796 /// and the failure is a null handle rather than an error code that says so.
797 ///
798 /// Like the Linux path, this asks for the driver's **default** context rather than a
799 /// core profile. `wglCreateContext` yields the highest compatibility profile the driver
800 /// offers, which is what fastfetch reports — measured here as
801 /// `4.6.0 Compatibility Profile Context 25.20.32.06.251214`. Requesting a core profile
802 /// would need `wglCreateContextAttribsARB` and would print a different string for the
803 /// same machine, so this is deliberate rather than the path of least resistance.
804 pub fn detect() -> Option<String> {
805 let lib = dl::open(c"opengl32.dll")?;
806 let out = detect_with(lib);
807 dl::close(lib);
808 out
809 }
810
811 fn detect_with(lib: *mut c_void) -> Option<String> {
812 let create_ctx = dl::sym(lib, c"wglCreateContext")?;
813 let make_current = dl::sym(lib, c"wglMakeCurrent")?;
814 let delete_ctx = dl::sym(lib, c"wglDeleteContext")?;
815 let get_string = dl::sym(lib, c"glGetString")?;
816
817 let window = HiddenWindow::new()?;
818
819 // SAFETY: every function pointer is freshly resolved from opengl32; `window.hdc` is
820 // a live device context owned by the guard above; the context is made non-current
821 // and deleted before returning on every path.
822 unsafe {
823 let create_ctx: WglCreateContext = std::mem::transmute(create_ctx);
824 let make_current: WglMakeCurrent = std::mem::transmute(make_current);
825 let delete_ctx: WglDeleteContext = std::mem::transmute(delete_ctx);
826 let get_string: GlGetString = std::mem::transmute(get_string);
827
828 let pfd = PixelFormatDescriptor {
829 n_size: std::mem::size_of::<PixelFormatDescriptor>() as u16,
830 n_version: 1,
831 dw_flags: PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER,
832 i_pixel_type: PFD_TYPE_RGBA,
833 c_color_bits: 32,
834 c_depth_bits: 24,
835 c_stencil_bits: 8,
836 i_layer_type: PFD_MAIN_PLANE,
837 ..Default::default()
838 };
839 let format = ChoosePixelFormat(window.hdc, &pfd);
840 if format == 0 || SetPixelFormat(window.hdc, format, &pfd) == 0 {
841 return None;
842 }
843
844 let ctx = create_ctx(window.hdc);
845 if ctx.is_null() {
846 return None;
847 }
848 let version = if make_current(window.hdc, ctx) != 0 {
849 let p = get_string(GL_VERSION);
850 let s = (!p.is_null()).then(|| CStr::from_ptr(p).to_string_lossy().into_owned());
851 // Unbind before deleting: deleting the context that is current to this
852 // thread is documented as failing, which would leak it.
853 make_current(std::ptr::null_mut(), std::ptr::null_mut());
854 s
855 } else {
856 None
857 };
858 delete_ctx(ctx);
859
860 version
861 .map(|v| v.trim().to_string())
862 .filter(|v| !v.is_empty())
863 }
864 }
865
866 #[cfg(test)]
867 mod layout {
868 use std::mem::{offset_of, size_of};
869
870 // Both structs are passed to the OS by pointer and read by fixed offset, and
871 // `PIXELFORMATDESCRIPTOR.nSize` is set from `size_of` — so a layout change would
872 // silently hand `ChoosePixelFormat` a wrong size rather than fail to compile.
873 #[test]
874 fn ffi_struct_layout() {
875 assert_eq!(size_of::<super::PixelFormatDescriptor>(), 40);
876 assert_eq!(offset_of!(super::PixelFormatDescriptor, dw_flags), 4);
877 assert_eq!(offset_of!(super::PixelFormatDescriptor, i_pixel_type), 8);
878 assert_eq!(offset_of!(super::PixelFormatDescriptor, c_color_bits), 9);
879 assert_eq!(offset_of!(super::PixelFormatDescriptor, c_depth_bits), 23);
880 assert_eq!(offset_of!(super::PixelFormatDescriptor, i_layer_type), 26);
881
882 assert_eq!(size_of::<super::WndClassW>(), 72);
883 assert_eq!(offset_of!(super::WndClassW, lpfn_wnd_proc), 8);
884 assert_eq!(offset_of!(super::WndClassW, h_instance), 24);
885 assert_eq!(offset_of!(super::WndClassW, lpsz_class_name), 64);
886 }
887 }
888}
889
890#[cfg(any(target_os = "linux", target_os = "windows"))]
891mod opencl {
892 use super::dl;
893 use super::*;
894
895 const CL_PLATFORM_VERSION: u32 = 0x0901;
896 const CL_PLATFORM_NAME: u32 = 0x0902;
897 const CL_DEVICE_TYPE_ALL: u64 = 0xFFFF_FFFF;
898 const CL_DEVICE_NAME: u32 = 0x102B;
899
900 type ClGetPlatformIDs = unsafe extern "C" fn(u32, *mut *mut c_void, *mut u32) -> i32;
901 type ClGetPlatformInfo =
902 unsafe extern "C" fn(*mut c_void, u32, usize, *mut c_void, *mut usize) -> i32;
903 type ClGetDeviceIDs =
904 unsafe extern "C" fn(*mut c_void, u64, u32, *mut *mut c_void, *mut u32) -> i32;
905 type ClGetDeviceInfo =
906 unsafe extern "C" fn(*mut c_void, u32, usize, *mut c_void, *mut usize) -> i32;
907
908 #[cfg(target_os = "linux")]
909 extern "C" {
910 fn dup(oldfd: c_int) -> c_int;
911 fn dup2(oldfd: c_int, newfd: c_int) -> c_int;
912 fn close(fd: c_int) -> c_int;
913 fn open(path: *const c_char, flags: c_int) -> c_int;
914 }
915 #[cfg(target_os = "linux")]
916 const STDERR_FILENO: c_int = 2;
917 #[cfg(target_os = "linux")]
918 const O_WRONLY: c_int = 1;
919
920 /// Silences `stderr` for its lifetime, restoring the original on drop.
921 ///
922 /// **Why this exists:** initialising an OpenCL driver can make it print to `stderr`
923 /// over which retch has no control. Mesa's rusticl emits a 247-byte "Patched Mesa
924 /// libclc not detected" warning on every enumeration once `RUSTICL_ENABLE` is set, and
925 /// a fetch tool that sprays a driver's diagnostics into the terminal is broken. This
926 /// was caught by `test_cli_full_mode`, which asserts retch writes nothing to `stderr`;
927 /// fastfetch has the same leak and simply lets it through.
928 ///
929 /// **The caveat, stated rather than hidden:** file descriptors are process-wide, so
930 /// this suppresses `stderr` for *every* thread while it is alive, and could in
931 /// principle swallow a concurrent probe's error message. It is therefore scoped as
932 /// tightly as possible — only around the OpenCL calls, ~20 ms — rather than around the
933 /// collection scope. Moving the probe out of the concurrent scope would make the
934 /// suppression provably safe, but costs ~100 ms serially and pushes `--full` past
935 /// `fastfetch -c all` (1.02 s here), which NOTES.md §3 treats as blocking.
936 #[cfg(target_os = "linux")]
937 struct SuppressStderr {
938 saved: c_int,
939 }
940
941 #[cfg(target_os = "linux")]
942 impl SuppressStderr {
943 fn new() -> Option<Self> {
944 // SAFETY: plain fd manipulation. Every call's result is checked, and the
945 // original descriptor is retained for restoration in `drop`.
946 unsafe {
947 let saved = dup(STDERR_FILENO);
948 if saved < 0 {
949 return None;
950 }
951 let devnull = open(c"/dev/null".as_ptr(), O_WRONLY);
952 if devnull < 0 {
953 close(saved);
954 return None;
955 }
956 dup2(devnull, STDERR_FILENO);
957 close(devnull);
958 Some(Self { saved })
959 }
960 }
961 }
962
963 #[cfg(target_os = "linux")]
964 impl Drop for SuppressStderr {
965 fn drop(&mut self) {
966 // SAFETY: `self.saved` is a live descriptor duplicated from stderr in `new`.
967 unsafe {
968 dup2(self.saved, STDERR_FILENO);
969 close(self.saved);
970 }
971 }
972 }
973
974 /// Report the OpenCL platform version, its provider, and whether a device exists.
975 ///
976 /// The device count is the point: see the module docs for why a platform advertising a
977 /// version while exposing no device is reported as such rather than as a bare version.
978 /// No-op stand-in on Windows.
979 ///
980 /// The Linux suppression exists for one specific driver: Mesa's rusticl prints a
981 /// "Patched Mesa libclc not detected" warning to stderr on every enumeration. That
982 /// driver does not exist on Windows, where the ICD loader dispatches to vendor DLLs
983 /// instead. **Rather than assume the Windows ICDs are equally quiet, this is checked**
984 /// — `test_cli_full_mode` asserts retch writes nothing to stderr, and it runs on the
985 /// Windows CI leg. Adding suppression here pre-emptively would mean reimplementing the
986 /// `dup2` dance on the CRT to solve a problem no observation has shown to exist, while
987 /// silencing every other thread's diagnostics for the duration.
988 #[cfg(target_os = "windows")]
989 struct SuppressStderr;
990
991 #[cfg(target_os = "windows")]
992 impl SuppressStderr {
993 fn new() -> Option<Self> {
994 None
995 }
996 }
997
998 pub fn detect() -> Option<String> {
999 // Held across the whole probe: the driver can write to stderr at dlopen, at
1000 // platform enumeration, or at device enumeration, and rusticl does so at the last.
1001 let _quiet = SuppressStderr::new();
1002 let lib = dl::open(OPENCL_LIB)?;
1003 let out = detect_with(lib);
1004 dl::close(lib);
1005 out
1006 }
1007
1008 fn detect_with(lib: *mut c_void) -> Option<String> {
1009 let get_platform_ids = dl::sym(lib, c"clGetPlatformIDs")?;
1010 let get_platform_info = dl::sym(lib, c"clGetPlatformInfo")?;
1011
1012 // SAFETY: pointers are resolved from the ICD loader; every call's return code is
1013 // checked, and every buffer is sized by a preceding size query.
1014 unsafe {
1015 let get_platform_ids: ClGetPlatformIDs = std::mem::transmute(get_platform_ids);
1016 let get_platform_info: ClGetPlatformInfo = std::mem::transmute(get_platform_info);
1017
1018 let mut count: u32 = 0;
1019 if get_platform_ids(0, std::ptr::null_mut(), &mut count) != 0 || count == 0 {
1020 return None;
1021 }
1022 let mut platforms = vec![std::ptr::null_mut::<c_void>(); count as usize];
1023 if get_platform_ids(count, platforms.as_mut_ptr(), std::ptr::null_mut()) != 0 {
1024 return None;
1025 }
1026 let platform = *platforms.first()?;
1027
1028 let version = query(get_platform_info, platform, CL_PLATFORM_VERSION)?;
1029 let name = query(get_platform_info, platform, CL_PLATFORM_NAME).unwrap_or_default();
1030
1031 let device = dl::sym(lib, c"clGetDeviceIDs")
1032 .zip(dl::sym(lib, c"clGetDeviceInfo"))
1033 .and_then(|(ids, info)| first_device_name(platform, ids, info))
1034 .map(|n| shorten_device_name(&n));
1035
1036 Some(format_opencl(&version, &name, device.as_deref()))
1037 }
1038 }
1039
1040 /// Two-call size-then-read query against a platform.
1041 ///
1042 /// SAFETY: `f` is `clGetPlatformInfo` and `obj` a valid platform id.
1043 unsafe fn query(f: ClGetPlatformInfo, obj: *mut c_void, param: u32) -> Option<String> {
1044 let mut size: usize = 0;
1045 if f(obj, param, 0, std::ptr::null_mut(), &mut size) != 0 || size == 0 {
1046 return None;
1047 }
1048 let mut buf = vec![0u8; size];
1049 if f(
1050 obj,
1051 param,
1052 size,
1053 buf.as_mut_ptr() as *mut c_void,
1054 std::ptr::null_mut(),
1055 ) != 0
1056 {
1057 return None;
1058 }
1059 let s = cstr_field(&buf);
1060 (!s.trim().is_empty()).then(|| s.trim().to_string())
1061 }
1062
1063 /// Name of the first device on a platform, or `None` when it exposes none.
1064 ///
1065 /// SAFETY: `ids`/`info` are the corresponding OpenCL entry points and `platform` is a
1066 /// valid platform id.
1067 unsafe fn first_device_name(
1068 platform: *mut c_void,
1069 ids: *mut c_void,
1070 info: *mut c_void,
1071 ) -> Option<String> {
1072 let get_device_ids: ClGetDeviceIDs = std::mem::transmute(ids);
1073 let get_device_info: ClGetDeviceInfo = std::mem::transmute(info);
1074
1075 let mut count: u32 = 0;
1076 // A platform with no usable device answers CL_DEVICE_NOT_FOUND (-1) here. That is
1077 // the rusticl-without-RUSTICL_ENABLE state, and it is a real answer, not an error.
1078 if get_device_ids(
1079 platform,
1080 CL_DEVICE_TYPE_ALL,
1081 0,
1082 std::ptr::null_mut(),
1083 &mut count,
1084 ) != 0
1085 || count == 0
1086 {
1087 return None;
1088 }
1089 let mut devices = vec![std::ptr::null_mut::<c_void>(); count as usize];
1090 if get_device_ids(
1091 platform,
1092 CL_DEVICE_TYPE_ALL,
1093 count,
1094 devices.as_mut_ptr(),
1095 std::ptr::null_mut(),
1096 ) != 0
1097 {
1098 return None;
1099 }
1100 let device = *devices.first()?;
1101 let mut size: usize = 0;
1102 if get_device_info(device, CL_DEVICE_NAME, 0, std::ptr::null_mut(), &mut size) != 0
1103 || size == 0
1104 {
1105 return None;
1106 }
1107 let mut buf = vec![0u8; size];
1108 if get_device_info(
1109 device,
1110 CL_DEVICE_NAME,
1111 size,
1112 buf.as_mut_ptr() as *mut c_void,
1113 std::ptr::null_mut(),
1114 ) != 0
1115 {
1116 return None;
1117 }
1118 let s = cstr_field(&buf);
1119 (!s.trim().is_empty()).then(|| s.trim().to_string())
1120 }
1121}
1122
1123/// Detect Vulkan, OpenGL and OpenCL versions.
1124#[cfg(target_os = "linux")]
1125pub fn detect_gpu_apis() -> GpuApis {
1126 GpuApis {
1127 vulkan: vulkan::detect(),
1128 opengl: opengl::detect(),
1129 opencl: opencl::detect(),
1130 }
1131}
1132
1133/// Windows: Vulkan and OpenCL, but not OpenGL.
1134///
1135/// The Vulkan and OpenCL probes are the *same code* as Linux — those APIs are identical
1136/// across platforms and only the loader filename differs, which is why the split lives in
1137/// [`dl`] and the two `*_LIB` constants rather than in duplicated probes.
1138///
1139/// **OpenGL is absent here deliberately, not by oversight.** The Linux path gets a headless
1140/// context through EGL (`EGL_DEFAULT_DISPLAY` plus a surfaceless `eglMakeCurrent`), and
1141/// **stock Windows ships no `libEGL.dll`** — checked on a Windows 11 box that has
1142/// `vulkan-1.dll`, `opengl32.dll` and `OpenCL.dll` in `System32` but no EGL at all. A
1143/// Windows OpenGL version therefore needs WGL against a hidden window, which is a different
1144/// mechanism rather than a different library name, so it is tracked as separate work
1145/// (NOTES.md §6a) instead of being half-done here.
1146#[cfg(target_os = "windows")]
1147pub fn detect_gpu_apis() -> GpuApis {
1148 GpuApis {
1149 vulkan: vulkan::detect(),
1150 opengl: opengl::detect(),
1151 opencl: opencl::detect(),
1152 }
1153}
1154
1155/// Other platforms: reports nothing rather than guessing.
1156#[cfg(not(any(target_os = "linux", target_os = "windows")))]
1157pub fn detect_gpu_apis() -> GpuApis {
1158 GpuApis::default()
1159}
1160
1161#[cfg(test)]
1162mod tests {
1163 use super::*;
1164
1165 /// The loader filenames are the one part of the Windows arm with no runtime guard: a
1166 /// typo does not fail, it makes the probe report "not installed", which is
1167 /// indistinguishable from a machine that genuinely has no Vulkan. Pin them.
1168 ///
1169 /// `vulkan-1.dll` and `OpenCL.dll` are the Khronos loaders' fixed names on Windows —
1170 /// not vendor DLLs, which register themselves behind these. Confirmed present in
1171 /// `System32` on the machine this was developed against.
1172 #[cfg(target_os = "windows")]
1173 #[test]
1174 fn test_windows_loader_names_are_the_khronos_loaders() {
1175 assert_eq!(VULKAN_LIB.to_str().unwrap(), "vulkan-1.dll");
1176 assert_eq!(OPENCL_LIB.to_str().unwrap(), "OpenCL.dll");
1177 }
1178
1179 /// The Linux sonames, pinned for the same reason and to keep the two arms visibly
1180 /// paired — a change to one should prompt a look at the other.
1181 #[cfg(target_os = "linux")]
1182 #[test]
1183 fn test_linux_loader_sonames() {
1184 assert_eq!(VULKAN_LIB.to_str().unwrap(), "libvulkan.so.1");
1185 assert_eq!(OPENCL_LIB.to_str().unwrap(), "libOpenCL.so.1");
1186 }
1187
1188 /// The AMD platform string this machine reports, run through the same formatter the
1189 /// Linux Mesa strings go through.
1190 ///
1191 /// Windows drivers phrase `CL_PLATFORM_VERSION` differently from Mesa — AMD's carries
1192 /// a build number in parentheses — so this pins that the `OpenCL ` prefix strip still
1193 /// does the right thing on a non-Mesa string, and that the parenthesised build number
1194 /// is **not** mistaken for the device descriptor `shorten_device_name` strips.
1195 #[test]
1196 fn test_format_opencl_handles_a_windows_vendor_platform_string() {
1197 assert_eq!(
1198 format_opencl(
1199 "OpenCL 2.1 AMD-APP (3661.0)",
1200 "AMD Accelerated Parallel Processing",
1201 Some("gfx1151"),
1202 ),
1203 "2.1 AMD-APP (3661.0) - AMD Accelerated Parallel Processing (gfx1151)"
1204 );
1205 }
1206
1207 /// A device name with no parenthesised driver descriptor must survive intact.
1208 ///
1209 /// The Linux fixtures all have one (Mesa appends `(radeonsi, phoenix, ACO, …)`), so
1210 /// nothing pinned the other branch until Windows produced a bare `gfx1151`.
1211 #[test]
1212 fn test_shorten_device_name_leaves_a_bare_name_alone() {
1213 assert_eq!(shorten_device_name("gfx1151"), "gfx1151");
1214 assert_eq!(shorten_device_name(" gfx1151 "), "gfx1151");
1215 }
1216
1217 #[test]
1218 fn test_format_vulkan_version_decodes_packed_fields() {
1219 // 0x00404155 is what this machine's loader reports: 1.4.341.
1220 assert_eq!(format_vulkan_version(0x0040_4155), "1.4.341");
1221 // major/minor/patch boundaries
1222 assert_eq!(format_vulkan_version(1 << 22), "1.0.0");
1223 assert_eq!(format_vulkan_version((1 << 22) | (2 << 12)), "1.2.0");
1224 assert_eq!(
1225 format_vulkan_version((1 << 22) | (3 << 12) | 290),
1226 "1.3.290"
1227 );
1228 }
1229
1230 #[test]
1231 fn test_format_vulkan_version_ignores_variant_bits() {
1232 // The top 3 bits are the variant; a non-Khronos variant must not leak into the
1233 // printed version or users see a leading number that means nothing to them.
1234 let with_variant = (1u32 << 29) | (1 << 22) | (4 << 12) | 354;
1235 assert_eq!(format_vulkan_version(with_variant), "1.4.354");
1236 }
1237
1238 #[test]
1239 fn test_device_type_rank_prefers_real_gpu_over_software() {
1240 // The case that matters: a real GPU (integrated=1) must outrank llvmpipe (CPU=4),
1241 // which is enumerated alongside it on any Mesa system.
1242 assert!(device_type_rank(1) < device_type_rank(4));
1243 assert!(device_type_rank(2) < device_type_rank(1)); // discrete beats integrated
1244 assert!(device_type_rank(3) < device_type_rank(4)); // virtual beats CPU
1245 assert!(device_type_rank(0) < device_type_rank(4)); // even "other" beats CPU
1246 }
1247
1248 #[test]
1249 fn test_format_vulkan_handles_unfilled_driver_chain() {
1250 // An instance below Vulkan 1.2 leaves these empty with no error, so the version
1251 // alone must still render.
1252 assert_eq!(format_vulkan("1.4.354", "", ""), "1.4.354");
1253 assert_eq!(format_vulkan("1.4.354", "radv", ""), "1.4.354 - radv");
1254 assert_eq!(
1255 format_vulkan("1.4.354", "radv", "Mesa 26.1.8"),
1256 "1.4.354 - radv [Mesa 26.1.8]"
1257 );
1258 }
1259
1260 #[test]
1261 fn test_format_opencl_distinguishes_inert_platform_from_working_one() {
1262 // The whole point of the field: rusticl without RUSTICL_ENABLE advertises 3.0 and
1263 // exposes nothing. fastfetch prints "3.0" for both of these.
1264 assert_eq!(
1265 format_opencl("OpenCL 3.0", "rusticl", None),
1266 "3.0 - rusticl (no device enabled)"
1267 );
1268 assert_eq!(
1269 format_opencl("OpenCL 3.0", "rusticl", Some("AMD Radeon 780M Graphics")),
1270 "3.0 - rusticl (AMD Radeon 780M Graphics)"
1271 );
1272 // A device string that is only whitespace is not a device.
1273 assert_eq!(
1274 format_opencl("OpenCL 3.0", "rusticl", Some(" ")),
1275 "3.0 - rusticl (no device enabled)"
1276 );
1277 }
1278
1279 #[test]
1280 fn test_format_opencl_without_platform_name() {
1281 assert_eq!(
1282 format_opencl("OpenCL 1.2", "", None),
1283 "1.2 (no device enabled)"
1284 );
1285 assert_eq!(format_opencl("OpenCL 1.2", "", Some("GPU")), "1.2 (GPU)");
1286 // A platform that does not carry the spec-mandated prefix is left alone rather
1287 // than having its first word eaten.
1288 assert_eq!(format_opencl("3.0", "x", Some("GPU")), "3.0 - x (GPU)");
1289 }
1290
1291 #[test]
1292 fn test_shorten_device_name_drops_the_driver_descriptor() {
1293 // The real string this machine returns, otherwise 80+ characters of driver detail.
1294 assert_eq!(
1295 shorten_device_name(
1296 "AMD Radeon 780M Graphics (radeonsi, phoenix, ACO, DRM 3.64, 7.1.13-200.fc44.x86_64)"
1297 ),
1298 "AMD Radeon 780M Graphics"
1299 );
1300 // A name with no descriptor is returned intact rather than truncated.
1301 assert_eq!(
1302 shorten_device_name("NVIDIA GeForce RTX 4090"),
1303 "NVIDIA GeForce RTX 4090"
1304 );
1305 // Only " (" splits, so a parenthesis inside a model name survives.
1306 assert_eq!(
1307 shorten_device_name("Intel(R) Arc(TM) A770"),
1308 "Intel(R) Arc(TM) A770"
1309 );
1310 }
1311
1312 #[test]
1313 fn test_cstr_field_stops_at_nul() {
1314 let mut buf = [0u8; 16];
1315 buf[..4].copy_from_slice(b"radv");
1316 assert_eq!(cstr_field(&buf), "radv");
1317 // An unwritten field is empty, not garbage — this is how an ignored pNext presents.
1318 assert_eq!(cstr_field(&[0u8; 16]), "");
1319 // No NUL at all: use the whole buffer rather than reading past it.
1320 assert_eq!(cstr_field(b"abcd"), "abcd");
1321 }
1322}