Skip to main content

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}