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(any(target_os = "linux", target_os = "macos"))]
37use std::ffi::c_int;
38#[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))]
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(any(target_os = "linux", target_os = "macos"))]
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/// macOS has **no Vulkan at all** out of the box — there is no system loader and no
261/// driver. Vulkan exists only through MoltenVK, a Vulkan-to-Metal translation layer, and
262/// only once a user installs it (the LunarG SDK, or Homebrew).
263///
264/// This names the **Khronos loader**, `libvulkan.1.dylib`, which the SDK installs into
265/// `/usr/local/lib` — a directory `dlopen` searches by default. Naming the loader rather
266/// than `libMoltenVK.dylib` is deliberate: the loader is the entry point a portable Vulkan
267/// application actually uses, so its presence is what "this machine has Vulkan" means. A
268/// bare MoltenVK with no loader is reported as absent, which under-reports rather than
269/// claiming an API that ordinary Vulkan software could not reach — the v0.11.6 rule.
270///
271/// On a stock Mac this simply fails to open and the field is absent, which is correct:
272/// fastfetch prints no Vulkan line here either.
273#[cfg(target_os = "macos")]
274const VULKAN_LIB: &CStr = c"libvulkan.1.dylib";
275
276/// The OpenCL ICD loader's filename on this platform.
277#[cfg(target_os = "linux")]
278const OPENCL_LIB: &CStr = c"libOpenCL.so.1";
279/// `OpenCL.dll` is the Khronos ICD loader on Windows; vendor drivers register themselves
280/// with it rather than being opened directly.
281#[cfg(target_os = "windows")]
282const OPENCL_LIB: &CStr = c"OpenCL.dll";
283/// macOS ships OpenCL as a **framework**, and the full path is required.
284///
285/// **A bare `dlopen("OpenCL")` fails**, as does `libOpenCL.dylib` — verified on macOS 26.
286/// System frameworks live in the dyld shared cache rather than on disk, so the file does
287/// not exist to `stat` but the framework path still resolves through `dlopen`. Apple has
288/// deprecated OpenCL in favour of Metal, but it is still present and still functional.
289#[cfg(target_os = "macos")]
290const OPENCL_LIB: &CStr = c"/System/Library/Frameworks/OpenCL.framework/OpenCL";
291
292#[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))]
293mod vulkan {
294    use super::dl;
295    use super::*;
296
297    const VK_STRUCTURE_TYPE_APPLICATION_INFO: u32 = 0;
298    const VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO: u32 = 1;
299    const VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2: u32 = 1000059001;
300    const VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES: u32 = 1000196000;
301
302    /// `VkPhysicalDeviceProperties2` places `properties` after `sType` + padding + `pNext`.
303    const PROPS2_BODY: usize = 16;
304    /// Offsets within `VkPhysicalDeviceProperties`.
305    const OFF_API_VERSION: usize = 0;
306    const OFF_DEVICE_TYPE: usize = 16;
307    const OFF_DEVICE_NAME: usize = 20;
308    /// Comfortably larger than `sizeof(VkPhysicalDeviceProperties)` (~824 bytes). The
309    /// struct embeds `VkPhysicalDeviceLimits` (100+ fields) that this probe never reads,
310    /// so it is handled as a sized byte buffer with documented offsets — the same approach
311    /// `memory.rs` uses for SMBIOS type-17 and `win_iftable.rs` for `MIB_IF_ROW2`.
312    const PROPS_BUF: usize = 1024;
313
314    /// Offsets within `VkPhysicalDeviceDriverProperties`.
315    const OFF_DRIVER_NAME: usize = 20;
316    const OFF_DRIVER_INFO: usize = 276;
317    const DRIVER_BUF: usize = 560;
318    const VK_MAX_NAME: usize = 256;
319
320    #[repr(C)]
321    struct AppInfo {
322        s_type: u32,
323        p_next: *const c_void,
324        app_name: *const c_char,
325        app_version: u32,
326        engine_name: *const c_char,
327        engine_version: u32,
328        api_version: u32,
329    }
330
331    #[repr(C)]
332    struct InstanceCreateInfo {
333        s_type: u32,
334        p_next: *const c_void,
335        flags: u32,
336        app_info: *const AppInfo,
337        layer_count: u32,
338        layer_names: *const *const c_char,
339        ext_count: u32,
340        ext_names: *const *const c_char,
341    }
342
343    type VkCreateInstance =
344        unsafe extern "C" fn(*const InstanceCreateInfo, *const c_void, *mut *mut c_void) -> i32;
345    type VkDestroyInstance = unsafe extern "C" fn(*mut c_void, *const c_void);
346    type VkEnumeratePhysicalDevices =
347        unsafe extern "C" fn(*mut c_void, *mut u32, *mut *mut c_void) -> i32;
348    type VkGetPhysicalDeviceProperties2 = unsafe extern "C" fn(*mut c_void, *mut c_void);
349    type VkGetInstanceProcAddr = unsafe extern "C" fn(*mut c_void, *const c_char) -> *mut c_void;
350
351    /// Query the best physical device's API version and driver identity.
352    ///
353    /// Returns `None` when Vulkan is absent, no instance can be created, or no device is
354    /// present — all normal on a headless or GPU-less machine.
355    pub fn detect() -> Option<String> {
356        let lib = dl::open(VULKAN_LIB)?;
357        let result = detect_with(lib);
358        dl::close(lib);
359        result
360    }
361
362    fn detect_with(lib: *mut c_void) -> Option<String> {
363        let create = dl::sym(lib, c"vkCreateInstance")?;
364        let gipa = dl::sym(lib, c"vkGetInstanceProcAddr")?;
365
366        // SAFETY: every pointer below is either freshly resolved from the Vulkan loader or
367        // a local we own. Buffers passed to the driver are sized at or above the structs
368        // the API writes, and every returned code is checked before the result is read.
369        unsafe {
370            let create: VkCreateInstance = std::mem::transmute(create);
371            let gipa: VkGetInstanceProcAddr = std::mem::transmute(gipa);
372
373            let app = AppInfo {
374                s_type: VK_STRUCTURE_TYPE_APPLICATION_INFO,
375                p_next: std::ptr::null(),
376                app_name: c"retch".as_ptr(),
377                app_version: 0,
378                engine_name: std::ptr::null(),
379                engine_version: 0,
380                // Must be >= 1.2. With a 1.0 or 1.1 instance the driver SILENTLY IGNORES
381                // the `VkPhysicalDeviceDriverProperties` chain below and the driver name
382                // and info come back as empty strings with no error anywhere — verified
383                // against a 1.0 instance, which returned the right version and blank
384                // driver fields.
385                api_version: (1 << 22) | (2 << 12),
386            };
387            let ci = InstanceCreateInfo {
388                s_type: VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
389                p_next: std::ptr::null(),
390                flags: 0,
391                app_info: &app,
392                layer_count: 0,
393                layer_names: std::ptr::null(),
394                ext_count: 0,
395                ext_names: std::ptr::null(),
396            };
397
398            let mut instance: *mut c_void = std::ptr::null_mut();
399            if create(&ci, std::ptr::null(), &mut instance) != 0 || instance.is_null() {
400                return None;
401            }
402
403            let out = read_best_device(instance, gipa);
404
405            if let Some(p) = dl::sym(lib, c"vkDestroyInstance") {
406                let destroy: VkDestroyInstance = std::mem::transmute(p);
407                destroy(instance, std::ptr::null());
408            }
409            out
410        }
411    }
412
413    /// SAFETY: caller guarantees `instance` is a live `VkInstance` and `gipa` is the
414    /// loader's `vkGetInstanceProcAddr`.
415    unsafe fn read_best_device(
416        instance: *mut c_void,
417        gipa: VkGetInstanceProcAddr,
418    ) -> Option<String> {
419        let enum_ptr = gipa(instance, c"vkEnumeratePhysicalDevices".as_ptr());
420        let props_ptr = gipa(instance, c"vkGetPhysicalDeviceProperties2".as_ptr());
421        if enum_ptr.is_null() || props_ptr.is_null() {
422            return None;
423        }
424        let enumerate: VkEnumeratePhysicalDevices = std::mem::transmute(enum_ptr);
425        let get_props2: VkGetPhysicalDeviceProperties2 = std::mem::transmute(props_ptr);
426
427        let mut count: u32 = 0;
428        if enumerate(instance, &mut count, std::ptr::null_mut()) != 0 || count == 0 {
429            return None;
430        }
431        let mut devices = vec![std::ptr::null_mut::<c_void>(); count as usize];
432        if enumerate(instance, &mut count, devices.as_mut_ptr()) != 0 {
433            return None;
434        }
435
436        let mut best: Option<(u8, String)> = None;
437        for device in devices.iter().take(count as usize) {
438            let mut driver = vec![0u8; DRIVER_BUF];
439            driver[0..4].copy_from_slice(
440                &VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES.to_ne_bytes(),
441            );
442            let mut props = vec![0u8; PROPS2_BODY + PROPS_BUF];
443            props[0..4]
444                .copy_from_slice(&VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2.to_ne_bytes());
445            let chain = driver.as_mut_ptr() as usize;
446            props[8..16].copy_from_slice(&chain.to_ne_bytes());
447
448            get_props2(*device, props.as_mut_ptr() as *mut c_void);
449
450            let at = |off: usize| -> u32 {
451                let s = PROPS2_BODY + off;
452                u32::from_ne_bytes(props[s..s + 4].try_into().unwrap_or([0; 4]))
453            };
454            let api = at(OFF_API_VERSION);
455            let dtype = at(OFF_DEVICE_TYPE);
456            let name_start = PROPS2_BODY + OFF_DEVICE_NAME;
457            let _device_name = cstr_field(&props[name_start..name_start + VK_MAX_NAME]);
458
459            let driver_name = cstr_field(&driver[OFF_DRIVER_NAME..OFF_DRIVER_NAME + VK_MAX_NAME]);
460            let driver_info = cstr_field(&driver[OFF_DRIVER_INFO..OFF_DRIVER_INFO + VK_MAX_NAME]);
461
462            let rank = device_type_rank(dtype);
463            let rendered = format_vulkan(&format_vulkan_version(api), &driver_name, &driver_info);
464            if best.as_ref().is_none_or(|(r, _)| rank < *r) {
465                best = Some((rank, rendered));
466            }
467        }
468        best.map(|(_, s)| s)
469    }
470}
471
472#[cfg(target_os = "linux")]
473mod opengl {
474    use super::dl;
475    use super::*;
476
477    const EGL_OPENGL_API: u32 = 0x30A2;
478    const EGL_NONE: i32 = 0x3038;
479    const EGL_SURFACE_TYPE: i32 = 0x3033;
480    const EGL_PBUFFER_BIT: i32 = 0x0001;
481    const EGL_RENDERABLE_TYPE: i32 = 0x3040;
482    const EGL_OPENGL_BIT: i32 = 0x0008;
483    const GL_VERSION: u32 = 0x1F02;
484
485    type EglGetDisplay = unsafe extern "C" fn(*mut c_void) -> *mut c_void;
486    type EglInitialize = unsafe extern "C" fn(*mut c_void, *mut i32, *mut i32) -> u32;
487    type EglBindApi = unsafe extern "C" fn(u32) -> u32;
488    type EglChooseConfig =
489        unsafe extern "C" fn(*mut c_void, *const i32, *mut *mut c_void, i32, *mut i32) -> u32;
490    type EglCreateContext =
491        unsafe extern "C" fn(*mut c_void, *mut c_void, *mut c_void, *const i32) -> *mut c_void;
492    type EglMakeCurrent =
493        unsafe extern "C" fn(*mut c_void, *mut c_void, *mut c_void, *mut c_void) -> u32;
494    type EglGetProcAddress = unsafe extern "C" fn(*const c_char) -> *mut c_void;
495    type EglTerminate = unsafe extern "C" fn(*mut c_void) -> u32;
496    type GlGetString = unsafe extern "C" fn(u32) -> *const c_char;
497
498    /// Read `GL_VERSION` from a headless EGL context.
499    ///
500    /// Deliberately uses EGL with `EGL_DEFAULT_DISPLAY` and a surfaceless
501    /// `eglMakeCurrent`, so this works with no X or Wayland connection and without
502    /// touching the environment. GLX would require a display server.
503    ///
504    /// **The context choice decides the number printed.** Passing no attribute list asks
505    /// for the driver's default, which is the highest *compatibility* profile — matching
506    /// what fastfetch reports. Requesting a core profile instead reports a different
507    /// string for the same machine (`glxinfo -B` says `4.6 (Core Profile)` here where this
508    /// returns `4.6 (Compatibility Profile)`), so the choice is deliberate, not incidental.
509    pub fn detect() -> Option<String> {
510        let lib = dl::open(c"libEGL.so.1")?;
511        let out = detect_with(lib);
512        dl::close(lib);
513        out
514    }
515
516    fn detect_with(lib: *mut c_void) -> Option<String> {
517        let get_display = dl::sym(lib, c"eglGetDisplay")?;
518        let initialize = dl::sym(lib, c"eglInitialize")?;
519        let bind_api = dl::sym(lib, c"eglBindAPI")?;
520        let choose = dl::sym(lib, c"eglChooseConfig")?;
521        let create_context = dl::sym(lib, c"eglCreateContext")?;
522        let make_current = dl::sym(lib, c"eglMakeCurrent")?;
523        let get_proc = dl::sym(lib, c"eglGetProcAddress")?;
524
525        // SAFETY: all pointers are freshly resolved from libEGL or locals we own. Every
526        // EGL call's status is checked before its output is used, and the display is
527        // terminated on the success path.
528        unsafe {
529            let get_display: EglGetDisplay = std::mem::transmute(get_display);
530            let initialize: EglInitialize = std::mem::transmute(initialize);
531            let bind_api: EglBindApi = std::mem::transmute(bind_api);
532            let choose: EglChooseConfig = std::mem::transmute(choose);
533            let create_context: EglCreateContext = std::mem::transmute(create_context);
534            let make_current: EglMakeCurrent = std::mem::transmute(make_current);
535            let get_proc: EglGetProcAddress = std::mem::transmute(get_proc);
536
537            // EGL_DEFAULT_DISPLAY is a null handle.
538            let display = get_display(std::ptr::null_mut());
539            if display.is_null() {
540                return None;
541            }
542            let (mut major, mut minor) = (0i32, 0i32);
543            if initialize(display, &mut major, &mut minor) == 0 {
544                return None;
545            }
546            // Desktop GL specifically; an ES-only stack answers 0 here and is reported as
547            // "no OpenGL" rather than being silently downgraded to an ES version string.
548            if bind_api(EGL_OPENGL_API) == 0 {
549                terminate(lib, display);
550                return None;
551            }
552
553            let attrs = [
554                EGL_SURFACE_TYPE,
555                EGL_PBUFFER_BIT,
556                EGL_RENDERABLE_TYPE,
557                EGL_OPENGL_BIT,
558                EGL_NONE,
559            ];
560            let mut config: *mut c_void = std::ptr::null_mut();
561            let mut configs = 0i32;
562            if choose(display, attrs.as_ptr(), &mut config, 1, &mut configs) == 0 || configs == 0 {
563                terminate(lib, display);
564                return None;
565            }
566            let context = create_context(display, config, std::ptr::null_mut(), std::ptr::null());
567            if context.is_null() {
568                terminate(lib, display);
569                return None;
570            }
571            if make_current(display, std::ptr::null_mut(), std::ptr::null_mut(), context) == 0 {
572                terminate(lib, display);
573                return None;
574            }
575            let gl_get_string = get_proc(c"glGetString".as_ptr());
576            let version = if gl_get_string.is_null() {
577                None
578            } else {
579                let gl_get_string: GlGetString = std::mem::transmute(gl_get_string);
580                let p = gl_get_string(GL_VERSION);
581                if p.is_null() {
582                    None
583                } else {
584                    Some(CStr::from_ptr(p).to_string_lossy().into_owned())
585                }
586            };
587            terminate(lib, display);
588            version.filter(|v| !v.trim().is_empty())
589        }
590    }
591
592    /// Best-effort `eglTerminate`; failure to release is not worth reporting to the user.
593    ///
594    /// SAFETY: `display` is a live EGL display obtained from `eglGetDisplay`.
595    unsafe fn terminate(lib: *mut c_void, display: *mut c_void) {
596        if let Some(p) = dl::sym(lib, c"eglTerminate") {
597            let terminate: EglTerminate = std::mem::transmute(p);
598            terminate(display);
599        }
600    }
601}
602
603/// Windows OpenGL, via WGL against a hidden window.
604///
605/// **Why this is a separate module rather than a wider `cfg` on the EGL one.** Vulkan and
606/// OpenCL are the same code on both platforms because those APIs are identical and only the
607/// loader's filename differs. OpenGL is not: the Linux path gets a context from EGL with no
608/// window and no display server, and **stock Windows ships no `libEGL.dll`** — verified on a
609/// Windows 11 box carrying `vulkan-1.dll`, `opengl32.dll` and `OpenCL.dll` in `System32`
610/// with no EGL at all. Windows has no headless equivalent in the base OS: WGL requires a
611/// device context, a device context requires a window, and a window requires a window class.
612/// So this is a genuinely different mechanism reaching the same `glGetString(GL_VERSION)`.
613///
614/// **The window is never shown.** It is created without `WS_VISIBLE` and `ShowWindow` is
615/// never called, so nothing appears on screen — a fetch tool that flashed a window on every
616/// run would be broken. This is asserted rather than assumed: see the visibility check
617/// recorded in NOTES for v0.13.0, which enumerates top-level windows during a run.
618///
619/// `user32` and `gdi32` are linked rather than loaded at runtime, unlike the graphics
620/// loaders: they are core OS libraries always present on any Windows that can run the
621/// binary at all, and `display.rs` already links `user32` on the same grounds. `opengl32`
622/// *is* loaded at runtime, because a machine with no OpenGL ICD is a real case and must
623/// yield an absent field rather than a failure.
624#[cfg(target_os = "windows")]
625mod opengl {
626    use super::dl;
627    use super::*;
628
629    const GL_VERSION: u32 = 0x1F02;
630
631    // PIXELFORMATDESCRIPTOR.dwFlags
632    const PFD_DOUBLEBUFFER: u32 = 0x0000_0001;
633    const PFD_DRAW_TO_WINDOW: u32 = 0x0000_0004;
634    const PFD_SUPPORT_OPENGL: u32 = 0x0000_0020;
635    /// `PFD_TYPE_RGBA`.
636    const PFD_TYPE_RGBA: u8 = 0;
637    /// `PFD_MAIN_PLANE`.
638    const PFD_MAIN_PLANE: u8 = 0;
639
640    /// `WS_OVERLAPPED` is literally zero — the absence of `WS_VISIBLE` is what keeps the
641    /// window off screen, so it is spelled out rather than left implicit.
642    const WS_OVERLAPPED: u32 = 0x0000_0000;
643
644    /// `PIXELFORMATDESCRIPTOR`, 40 bytes. Only a handful of fields are set; the rest must
645    /// be zero, which is what `ChoosePixelFormat` expects for "don't care".
646    #[repr(C)]
647    #[derive(Default)]
648    struct PixelFormatDescriptor {
649        n_size: u16,
650        n_version: u16,
651        dw_flags: u32,
652        i_pixel_type: u8,
653        c_color_bits: u8,
654        c_red_bits: u8,
655        c_red_shift: u8,
656        c_green_bits: u8,
657        c_green_shift: u8,
658        c_blue_bits: u8,
659        c_blue_shift: u8,
660        c_alpha_bits: u8,
661        c_alpha_shift: u8,
662        c_accum_bits: u8,
663        c_accum_red_bits: u8,
664        c_accum_green_bits: u8,
665        c_accum_blue_bits: u8,
666        c_accum_alpha_bits: u8,
667        c_depth_bits: u8,
668        c_stencil_bits: u8,
669        c_aux_buffers: u8,
670        i_layer_type: u8,
671        b_reserved: u8,
672        dw_layer_mask: u32,
673        dw_visible_mask: u32,
674        dw_damage_mask: u32,
675    }
676
677    /// `WNDCLASSW`, 72 bytes on x64. `lpfnWndProc` points at `DefWindowProcW`: the window
678    /// never receives messages we care about, but a class still needs a procedure.
679    #[repr(C)]
680    struct WndClassW {
681        style: u32,
682        lpfn_wnd_proc: *const c_void,
683        cb_cls_extra: i32,
684        cb_wnd_extra: i32,
685        h_instance: *mut c_void,
686        h_icon: *mut c_void,
687        h_cursor: *mut c_void,
688        hbr_background: *mut c_void,
689        lpsz_menu_name: *const u16,
690        lpsz_class_name: *const u16,
691    }
692
693    #[link(name = "user32")]
694    extern "system" {
695        fn RegisterClassW(lp_wnd_class: *const WndClassW) -> u16;
696        fn UnregisterClassW(lp_class_name: *const u16, h_instance: *mut c_void) -> i32;
697        fn CreateWindowExW(
698            dw_ex_style: u32,
699            lp_class_name: *const u16,
700            lp_window_name: *const u16,
701            dw_style: u32,
702            x: i32,
703            y: i32,
704            n_width: i32,
705            n_height: i32,
706            h_wnd_parent: *mut c_void,
707            h_menu: *mut c_void,
708            h_instance: *mut c_void,
709            lp_param: *mut c_void,
710        ) -> *mut c_void;
711        fn DestroyWindow(h_wnd: *mut c_void) -> i32;
712        fn GetDC(h_wnd: *mut c_void) -> *mut c_void;
713        fn ReleaseDC(h_wnd: *mut c_void, h_dc: *mut c_void) -> i32;
714        fn DefWindowProcW(h_wnd: *mut c_void, msg: u32, w_param: usize, l_param: isize) -> isize;
715    }
716
717    #[link(name = "gdi32")]
718    extern "system" {
719        fn ChoosePixelFormat(h_dc: *mut c_void, ppfd: *const PixelFormatDescriptor) -> i32;
720        fn SetPixelFormat(
721            h_dc: *mut c_void,
722            format: i32,
723            ppfd: *const PixelFormatDescriptor,
724        ) -> i32;
725    }
726
727    type WglCreateContext = unsafe extern "system" fn(*mut c_void) -> *mut c_void;
728    type WglMakeCurrent = unsafe extern "system" fn(*mut c_void, *mut c_void) -> i32;
729    type WglDeleteContext = unsafe extern "system" fn(*mut c_void) -> i32;
730    type GlGetString = unsafe extern "system" fn(u32) -> *const c_char;
731
732    /// A hidden window plus its class, unregistered and destroyed on drop.
733    ///
734    /// Kept as a guard type so every early return unwinds the OS objects in the right
735    /// order. Doing it by hand at each `?` is how a window or class leaks — and a leaked
736    /// class makes a *second* run in the same process fail to register.
737    struct HiddenWindow {
738        class_name: Vec<u16>,
739        hwnd: *mut c_void,
740        hdc: *mut c_void,
741    }
742
743    impl HiddenWindow {
744        fn new() -> Option<Self> {
745            // A distinctive class name: it is unregistered on drop, so a collision would
746            // only matter if two probes ran concurrently in one process, which they do not.
747            let class_name: Vec<u16> = "retch_gl_probe\0".encode_utf16().collect();
748
749            let wc = WndClassW {
750                style: 0,
751                lpfn_wnd_proc: DefWindowProcW as *const c_void,
752                cb_cls_extra: 0,
753                cb_wnd_extra: 0,
754                h_instance: std::ptr::null_mut(),
755                h_icon: std::ptr::null_mut(),
756                h_cursor: std::ptr::null_mut(),
757                hbr_background: std::ptr::null_mut(),
758                lpsz_menu_name: std::ptr::null(),
759                lpsz_class_name: class_name.as_ptr(),
760            };
761
762            // SAFETY: `wc` is a fully initialised WNDCLASSW whose string pointer outlives
763            // the call, and every handle below is checked before use.
764            unsafe {
765                if RegisterClassW(&wc) == 0 {
766                    return None;
767                }
768                // No WS_VISIBLE and no ShowWindow: the window exists only to own a device
769                // context, and must never appear on screen. 1x1 at the origin.
770                let hwnd = CreateWindowExW(
771                    0,
772                    class_name.as_ptr(),
773                    std::ptr::null(),
774                    WS_OVERLAPPED,
775                    0,
776                    0,
777                    1,
778                    1,
779                    std::ptr::null_mut(),
780                    std::ptr::null_mut(),
781                    std::ptr::null_mut(),
782                    std::ptr::null_mut(),
783                );
784                if hwnd.is_null() {
785                    UnregisterClassW(class_name.as_ptr(), std::ptr::null_mut());
786                    return None;
787                }
788                let hdc = GetDC(hwnd);
789                if hdc.is_null() {
790                    DestroyWindow(hwnd);
791                    UnregisterClassW(class_name.as_ptr(), std::ptr::null_mut());
792                    return None;
793                }
794                Some(Self {
795                    class_name,
796                    hwnd,
797                    hdc,
798                })
799            }
800        }
801    }
802
803    impl Drop for HiddenWindow {
804        fn drop(&mut self) {
805            // SAFETY: all three handles came from `new` and are released exactly once, in
806            // the reverse of the order they were acquired.
807            unsafe {
808                ReleaseDC(self.hwnd, self.hdc);
809                DestroyWindow(self.hwnd);
810                UnregisterClassW(self.class_name.as_ptr(), std::ptr::null_mut());
811            }
812        }
813    }
814
815    /// Read `GL_VERSION` from a WGL context on a hidden window.
816    ///
817    /// **The pixel format is what makes the context creatable**, and it must be set before
818    /// `wglCreateContext`: a device context with no pixel format cannot back a GL context,
819    /// and the failure is a null handle rather than an error code that says so.
820    ///
821    /// Like the Linux path, this asks for the driver's **default** context rather than a
822    /// core profile. `wglCreateContext` yields the highest compatibility profile the driver
823    /// offers, which is what fastfetch reports — measured here as
824    /// `4.6.0 Compatibility Profile Context 25.20.32.06.251214`. Requesting a core profile
825    /// would need `wglCreateContextAttribsARB` and would print a different string for the
826    /// same machine, so this is deliberate rather than the path of least resistance.
827    pub fn detect() -> Option<String> {
828        let lib = dl::open(c"opengl32.dll")?;
829        let out = detect_with(lib);
830        dl::close(lib);
831        out
832    }
833
834    fn detect_with(lib: *mut c_void) -> Option<String> {
835        let create_ctx = dl::sym(lib, c"wglCreateContext")?;
836        let make_current = dl::sym(lib, c"wglMakeCurrent")?;
837        let delete_ctx = dl::sym(lib, c"wglDeleteContext")?;
838        let get_string = dl::sym(lib, c"glGetString")?;
839
840        let window = HiddenWindow::new()?;
841
842        // SAFETY: every function pointer is freshly resolved from opengl32; `window.hdc` is
843        // a live device context owned by the guard above; the context is made non-current
844        // and deleted before returning on every path.
845        unsafe {
846            let create_ctx: WglCreateContext = std::mem::transmute(create_ctx);
847            let make_current: WglMakeCurrent = std::mem::transmute(make_current);
848            let delete_ctx: WglDeleteContext = std::mem::transmute(delete_ctx);
849            let get_string: GlGetString = std::mem::transmute(get_string);
850
851            let pfd = PixelFormatDescriptor {
852                n_size: std::mem::size_of::<PixelFormatDescriptor>() as u16,
853                n_version: 1,
854                dw_flags: PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER,
855                i_pixel_type: PFD_TYPE_RGBA,
856                c_color_bits: 32,
857                c_depth_bits: 24,
858                c_stencil_bits: 8,
859                i_layer_type: PFD_MAIN_PLANE,
860                ..Default::default()
861            };
862            let format = ChoosePixelFormat(window.hdc, &pfd);
863            if format == 0 || SetPixelFormat(window.hdc, format, &pfd) == 0 {
864                return None;
865            }
866
867            let ctx = create_ctx(window.hdc);
868            if ctx.is_null() {
869                return None;
870            }
871            let version = if make_current(window.hdc, ctx) != 0 {
872                let p = get_string(GL_VERSION);
873                let s = (!p.is_null()).then(|| CStr::from_ptr(p).to_string_lossy().into_owned());
874                // Unbind before deleting: deleting the context that is current to this
875                // thread is documented as failing, which would leak it.
876                make_current(std::ptr::null_mut(), std::ptr::null_mut());
877                s
878            } else {
879                None
880            };
881            delete_ctx(ctx);
882
883            version
884                .map(|v| v.trim().to_string())
885                .filter(|v| !v.is_empty())
886        }
887    }
888
889    #[cfg(test)]
890    mod layout {
891        use std::mem::{offset_of, size_of};
892
893        // Both structs are passed to the OS by pointer and read by fixed offset, and
894        // `PIXELFORMATDESCRIPTOR.nSize` is set from `size_of` — so a layout change would
895        // silently hand `ChoosePixelFormat` a wrong size rather than fail to compile.
896        #[test]
897        fn ffi_struct_layout() {
898            assert_eq!(size_of::<super::PixelFormatDescriptor>(), 40);
899            assert_eq!(offset_of!(super::PixelFormatDescriptor, dw_flags), 4);
900            assert_eq!(offset_of!(super::PixelFormatDescriptor, i_pixel_type), 8);
901            assert_eq!(offset_of!(super::PixelFormatDescriptor, c_color_bits), 9);
902            assert_eq!(offset_of!(super::PixelFormatDescriptor, c_depth_bits), 23);
903            assert_eq!(offset_of!(super::PixelFormatDescriptor, i_layer_type), 26);
904
905            assert_eq!(size_of::<super::WndClassW>(), 72);
906            assert_eq!(offset_of!(super::WndClassW, lpfn_wnd_proc), 8);
907            assert_eq!(offset_of!(super::WndClassW, h_instance), 24);
908            assert_eq!(offset_of!(super::WndClassW, lpsz_class_name), 64);
909        }
910    }
911}
912
913/// macOS: `GL_VERSION` from a headless CGL context.
914///
915/// **A third mechanism, which is why this is a third module rather than a wider `cfg`.**
916/// Linux takes a context from EGL, Windows needs WGL against a hidden window, and macOS
917/// has neither: it has CGL, which creates a context with **no window and no surface at
918/// all**. That makes the macOS path the simplest of the three — there is no window class
919/// to register and nothing that could flash on screen, so the visibility check the Windows
920/// arm needs (v0.13.0) has no analogue here.
921///
922/// **THE DECISION THAT SETS THE NUMBER: the pixel format's profile attribute.** Measured
923/// on this machine, all four variants in one run:
924///
925/// | requested profile | `GL_VERSION` |
926/// |---|---|
927/// | no profile attribute | `2.1 Metal - 90.5` |
928/// | `kCGLOGLPVersion_Legacy` | `2.1 Metal - 90.5` |
929/// | `kCGLOGLPVersion_3_2_Core` | `4.1 Metal - 90.5` |
930/// | `kCGLOGLPVersion_GL4_Core` | `4.1 Metal - 90.5` |
931///
932/// So the default — no attribute — reports **2.1**, less than half the version the machine
933/// actually supports, and fastfetch reports `4.1 Metal - 90.5`. Apple caps OpenGL at 4.1
934/// and only exposes it through a core profile; the legacy profile is frozen at 2.1. This
935/// requests `GL4_Core` deliberately, and the table is recorded here because the same
936/// choice on Linux (v0.11.6) and Windows (v0.13.0) changed only the profile *label*, while
937/// here it changes the version itself.
938#[cfg(target_os = "macos")]
939mod opengl {
940    use super::dl;
941    use super::*;
942
943    /// Framework path — a bare `dlopen("OpenGL")` does not resolve. See [`OPENCL_LIB`] for
944    /// why the full path is required for system frameworks.
945    const OPENGL_FRAMEWORK: &CStr = c"/System/Library/Frameworks/OpenGL.framework/OpenGL";
946
947    /// `kCGLPFAAccelerated` — require a hardware renderer rather than the software one.
948    pub(super) const KCGLPFA_ACCELERATED: u32 = 73;
949    /// `kCGLPFAOpenGLProfile` — the attribute whose value decides the reported version.
950    pub(super) const KCGLPFA_OPENGL_PROFILE: u32 = 99;
951    /// `kCGLOGLPVersion_GL4_Core` — the highest profile Apple offers (OpenGL 4.1).
952    pub(super) const KCGL_OGLP_VERSION_GL4_CORE: u32 = 0x4100;
953    /// `GL_VERSION`.
954    const GL_VERSION: u32 = 0x1F02;
955
956    type CGLChoosePixelFormat = unsafe extern "C" fn(*const u32, *mut *mut c_void, *mut i32) -> i32;
957    type CGLCreateContext = unsafe extern "C" fn(*mut c_void, *mut c_void, *mut *mut c_void) -> i32;
958    type CGLSetCurrentContext = unsafe extern "C" fn(*mut c_void) -> i32;
959    type CGLDestroyContext = unsafe extern "C" fn(*mut c_void) -> i32;
960    type CGLDestroyPixelFormat = unsafe extern "C" fn(*mut c_void) -> i32;
961    type GlGetString = unsafe extern "C" fn(u32) -> *const c_char;
962
963    /// Unwinds the CGL objects in reverse order of acquisition, on every exit path.
964    ///
965    /// Releasing by hand at each `?` is how a context or a pixel format leaks, and a leaked
966    /// *current* context would keep the GPU objects alive for the rest of the process — the
967    /// same reasoning as the Windows arm's window/class guard, minus the window.
968    struct CglGuard {
969        set_current: CGLSetCurrentContext,
970        destroy_context: CGLDestroyContext,
971        destroy_pixel_format: CGLDestroyPixelFormat,
972        context: *mut c_void,
973        pixel_format: *mut c_void,
974    }
975
976    impl Drop for CglGuard {
977        fn drop(&mut self) {
978            // SAFETY: each pointer was produced by the matching CGL create call and is
979            // destroyed exactly once. Clearing the current context before destroying it is
980            // required — destroying a context that is current to the calling thread is
981            // documented to fail, which would leak it.
982            unsafe {
983                if !self.context.is_null() {
984                    (self.set_current)(std::ptr::null_mut());
985                    (self.destroy_context)(self.context);
986                }
987                if !self.pixel_format.is_null() {
988                    (self.destroy_pixel_format)(self.pixel_format);
989                }
990            }
991        }
992    }
993
994    /// Read `GL_VERSION`, or `None` when OpenGL is unavailable.
995    pub fn detect() -> Option<String> {
996        let lib = dl::open(OPENGL_FRAMEWORK)?;
997        let result = probe(lib);
998        dl::close(lib);
999        result
1000    }
1001
1002    fn probe(lib: *mut c_void) -> Option<String> {
1003        // SAFETY: every symbol is resolved from the OpenGL framework and transmuted to the
1004        // signature Apple documents for it; a missing symbol yields None and aborts here.
1005        unsafe {
1006            let choose: CGLChoosePixelFormat =
1007                std::mem::transmute(dl::sym(lib, c"CGLChoosePixelFormat")?);
1008            let create: CGLCreateContext = std::mem::transmute(dl::sym(lib, c"CGLCreateContext")?);
1009            let set_current: CGLSetCurrentContext =
1010                std::mem::transmute(dl::sym(lib, c"CGLSetCurrentContext")?);
1011            let destroy_context: CGLDestroyContext =
1012                std::mem::transmute(dl::sym(lib, c"CGLDestroyContext")?);
1013            let destroy_pixel_format: CGLDestroyPixelFormat =
1014                std::mem::transmute(dl::sym(lib, c"CGLDestroyPixelFormat")?);
1015            let gl_get_string: GlGetString = std::mem::transmute(dl::sym(lib, c"glGetString")?);
1016
1017            // NUL-terminated attribute list, as CGL expects.
1018            let attrs: [u32; 4] = [
1019                KCGLPFA_ACCELERATED,
1020                KCGLPFA_OPENGL_PROFILE,
1021                KCGL_OGLP_VERSION_GL4_CORE,
1022                0,
1023            ];
1024            let mut pixel_format: *mut c_void = std::ptr::null_mut();
1025            let mut count: i32 = 0;
1026            if choose(attrs.as_ptr(), &mut pixel_format, &mut count) != 0
1027                || pixel_format.is_null()
1028                || count == 0
1029            {
1030                return None;
1031            }
1032
1033            let mut context: *mut c_void = std::ptr::null_mut();
1034            let err = create(pixel_format, std::ptr::null_mut(), &mut context);
1035
1036            // Guard is armed with whatever succeeded, so an early return still unwinds.
1037            let guard = CglGuard {
1038                set_current,
1039                destroy_context,
1040                destroy_pixel_format,
1041                context: if err == 0 {
1042                    context
1043                } else {
1044                    std::ptr::null_mut()
1045                },
1046                pixel_format,
1047            };
1048
1049            if err != 0 || context.is_null() {
1050                return None;
1051            }
1052            if set_current(context) != 0 {
1053                return None;
1054            }
1055            let raw = gl_get_string(GL_VERSION);
1056            let version = (!raw.is_null())
1057                .then(|| CStr::from_ptr(raw).to_string_lossy().trim().to_string())
1058                .filter(|s| !s.is_empty());
1059            drop(guard);
1060            version
1061        }
1062    }
1063}
1064
1065#[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))]
1066mod opencl {
1067    use super::dl;
1068    use super::*;
1069
1070    const CL_PLATFORM_VERSION: u32 = 0x0901;
1071    const CL_PLATFORM_NAME: u32 = 0x0902;
1072    const CL_DEVICE_TYPE_ALL: u64 = 0xFFFF_FFFF;
1073    const CL_DEVICE_NAME: u32 = 0x102B;
1074
1075    type ClGetPlatformIDs = unsafe extern "C" fn(u32, *mut *mut c_void, *mut u32) -> i32;
1076    type ClGetPlatformInfo =
1077        unsafe extern "C" fn(*mut c_void, u32, usize, *mut c_void, *mut usize) -> i32;
1078    type ClGetDeviceIDs =
1079        unsafe extern "C" fn(*mut c_void, u64, u32, *mut *mut c_void, *mut u32) -> i32;
1080    type ClGetDeviceInfo =
1081        unsafe extern "C" fn(*mut c_void, u32, usize, *mut c_void, *mut usize) -> i32;
1082
1083    #[cfg(target_os = "linux")]
1084    extern "C" {
1085        fn dup(oldfd: c_int) -> c_int;
1086        fn dup2(oldfd: c_int, newfd: c_int) -> c_int;
1087        fn close(fd: c_int) -> c_int;
1088        fn open(path: *const c_char, flags: c_int) -> c_int;
1089    }
1090    #[cfg(target_os = "linux")]
1091    const STDERR_FILENO: c_int = 2;
1092    #[cfg(target_os = "linux")]
1093    const O_WRONLY: c_int = 1;
1094
1095    /// Silences `stderr` for its lifetime, restoring the original on drop.
1096    ///
1097    /// **Why this exists:** initialising an OpenCL driver can make it print to `stderr`
1098    /// over which retch has no control. Mesa's rusticl emits a 247-byte "Patched Mesa
1099    /// libclc not detected" warning on every enumeration once `RUSTICL_ENABLE` is set, and
1100    /// a fetch tool that sprays a driver's diagnostics into the terminal is broken. This
1101    /// was caught by `test_cli_full_mode`, which asserts retch writes nothing to `stderr`;
1102    /// fastfetch has the same leak and simply lets it through.
1103    ///
1104    /// **The caveat, stated rather than hidden:** file descriptors are process-wide, so
1105    /// this suppresses `stderr` for *every* thread while it is alive, and could in
1106    /// principle swallow a concurrent probe's error message. It is therefore scoped as
1107    /// tightly as possible — only around the OpenCL calls, ~20 ms — rather than around the
1108    /// collection scope. Moving the probe out of the concurrent scope would make the
1109    /// suppression provably safe, but costs ~100 ms serially and pushes `--full` past
1110    /// `fastfetch -c all` (1.02 s here), which NOTES.md §3 treats as blocking.
1111    #[cfg(target_os = "linux")]
1112    struct SuppressStderr {
1113        saved: c_int,
1114    }
1115
1116    #[cfg(target_os = "linux")]
1117    impl SuppressStderr {
1118        fn new() -> Option<Self> {
1119            // SAFETY: plain fd manipulation. Every call's result is checked, and the
1120            // original descriptor is retained for restoration in `drop`.
1121            unsafe {
1122                let saved = dup(STDERR_FILENO);
1123                if saved < 0 {
1124                    return None;
1125                }
1126                let devnull = open(c"/dev/null".as_ptr(), O_WRONLY);
1127                if devnull < 0 {
1128                    close(saved);
1129                    return None;
1130                }
1131                dup2(devnull, STDERR_FILENO);
1132                close(devnull);
1133                Some(Self { saved })
1134            }
1135        }
1136    }
1137
1138    #[cfg(target_os = "linux")]
1139    impl Drop for SuppressStderr {
1140        fn drop(&mut self) {
1141            // SAFETY: `self.saved` is a live descriptor duplicated from stderr in `new`.
1142            unsafe {
1143                dup2(self.saved, STDERR_FILENO);
1144                close(self.saved);
1145            }
1146        }
1147    }
1148
1149    /// Report the OpenCL platform version, its provider, and whether a device exists.
1150    ///
1151    /// The device count is the point: see the module docs for why a platform advertising a
1152    /// version while exposing no device is reported as such rather than as a bare version.
1153    /// No-op stand-in on Windows and macOS.
1154    ///
1155    /// The Linux suppression exists for one specific driver: Mesa's rusticl prints a
1156    /// "Patched Mesa libclc not detected" warning to stderr on every enumeration. That
1157    /// driver does not exist on Windows, where the ICD loader dispatches to vendor DLLs
1158    /// instead, nor on macOS, where Apple's own framework is the only implementation.
1159    /// **Rather than assume those stacks are equally quiet, both were checked** — a probe
1160    /// running the full platform *and* device enumeration wrote **0 bytes** to stderr on
1161    /// each, and `test_cli_full_mode` asserts retch writes nothing to stderr and runs on
1162    /// both CI legs, so a future leak fails loudly instead of silently spraying a driver's
1163    /// diagnostics into the terminal. Adding suppression pre-emptively would mean
1164    /// reimplementing the `dup2` dance to solve a problem no observation has shown to
1165    /// exist, while silencing every other thread's diagnostics for the duration.
1166    #[cfg(any(target_os = "windows", target_os = "macos"))]
1167    struct SuppressStderr;
1168
1169    #[cfg(any(target_os = "windows", target_os = "macos"))]
1170    impl SuppressStderr {
1171        fn new() -> Option<Self> {
1172            None
1173        }
1174    }
1175
1176    pub fn detect() -> Option<String> {
1177        // Held across the whole probe: the driver can write to stderr at dlopen, at
1178        // platform enumeration, or at device enumeration, and rusticl does so at the last.
1179        let _quiet = SuppressStderr::new();
1180        let lib = dl::open(OPENCL_LIB)?;
1181        let out = detect_with(lib);
1182        dl::close(lib);
1183        out
1184    }
1185
1186    fn detect_with(lib: *mut c_void) -> Option<String> {
1187        let get_platform_ids = dl::sym(lib, c"clGetPlatformIDs")?;
1188        let get_platform_info = dl::sym(lib, c"clGetPlatformInfo")?;
1189
1190        // SAFETY: pointers are resolved from the ICD loader; every call's return code is
1191        // checked, and every buffer is sized by a preceding size query.
1192        unsafe {
1193            let get_platform_ids: ClGetPlatformIDs = std::mem::transmute(get_platform_ids);
1194            let get_platform_info: ClGetPlatformInfo = std::mem::transmute(get_platform_info);
1195
1196            let mut count: u32 = 0;
1197            if get_platform_ids(0, std::ptr::null_mut(), &mut count) != 0 || count == 0 {
1198                return None;
1199            }
1200            let mut platforms = vec![std::ptr::null_mut::<c_void>(); count as usize];
1201            if get_platform_ids(count, platforms.as_mut_ptr(), std::ptr::null_mut()) != 0 {
1202                return None;
1203            }
1204            let platform = *platforms.first()?;
1205
1206            let version = query(get_platform_info, platform, CL_PLATFORM_VERSION)?;
1207            let name = query(get_platform_info, platform, CL_PLATFORM_NAME).unwrap_or_default();
1208
1209            let device = dl::sym(lib, c"clGetDeviceIDs")
1210                .zip(dl::sym(lib, c"clGetDeviceInfo"))
1211                .and_then(|(ids, info)| first_device_name(platform, ids, info))
1212                .map(|n| shorten_device_name(&n));
1213
1214            Some(format_opencl(&version, &name, device.as_deref()))
1215        }
1216    }
1217
1218    /// Two-call size-then-read query against a platform.
1219    ///
1220    /// SAFETY: `f` is `clGetPlatformInfo` and `obj` a valid platform id.
1221    unsafe fn query(f: ClGetPlatformInfo, obj: *mut c_void, param: u32) -> Option<String> {
1222        let mut size: usize = 0;
1223        if f(obj, param, 0, std::ptr::null_mut(), &mut size) != 0 || size == 0 {
1224            return None;
1225        }
1226        let mut buf = vec![0u8; size];
1227        if f(
1228            obj,
1229            param,
1230            size,
1231            buf.as_mut_ptr() as *mut c_void,
1232            std::ptr::null_mut(),
1233        ) != 0
1234        {
1235            return None;
1236        }
1237        let s = cstr_field(&buf);
1238        (!s.trim().is_empty()).then(|| s.trim().to_string())
1239    }
1240
1241    /// Name of the first device on a platform, or `None` when it exposes none.
1242    ///
1243    /// SAFETY: `ids`/`info` are the corresponding OpenCL entry points and `platform` is a
1244    /// valid platform id.
1245    unsafe fn first_device_name(
1246        platform: *mut c_void,
1247        ids: *mut c_void,
1248        info: *mut c_void,
1249    ) -> Option<String> {
1250        let get_device_ids: ClGetDeviceIDs = std::mem::transmute(ids);
1251        let get_device_info: ClGetDeviceInfo = std::mem::transmute(info);
1252
1253        let mut count: u32 = 0;
1254        // A platform with no usable device answers CL_DEVICE_NOT_FOUND (-1) here. That is
1255        // the rusticl-without-RUSTICL_ENABLE state, and it is a real answer, not an error.
1256        if get_device_ids(
1257            platform,
1258            CL_DEVICE_TYPE_ALL,
1259            0,
1260            std::ptr::null_mut(),
1261            &mut count,
1262        ) != 0
1263            || count == 0
1264        {
1265            return None;
1266        }
1267        let mut devices = vec![std::ptr::null_mut::<c_void>(); count as usize];
1268        if get_device_ids(
1269            platform,
1270            CL_DEVICE_TYPE_ALL,
1271            count,
1272            devices.as_mut_ptr(),
1273            std::ptr::null_mut(),
1274        ) != 0
1275        {
1276            return None;
1277        }
1278        let device = *devices.first()?;
1279        let mut size: usize = 0;
1280        if get_device_info(device, CL_DEVICE_NAME, 0, std::ptr::null_mut(), &mut size) != 0
1281            || size == 0
1282        {
1283            return None;
1284        }
1285        let mut buf = vec![0u8; size];
1286        if get_device_info(
1287            device,
1288            CL_DEVICE_NAME,
1289            size,
1290            buf.as_mut_ptr() as *mut c_void,
1291            std::ptr::null_mut(),
1292        ) != 0
1293        {
1294            return None;
1295        }
1296        let s = cstr_field(&buf);
1297        (!s.trim().is_empty()).then(|| s.trim().to_string())
1298    }
1299}
1300
1301/// Detect Vulkan, OpenGL and OpenCL versions.
1302#[cfg(target_os = "linux")]
1303pub fn detect_gpu_apis() -> GpuApis {
1304    GpuApis {
1305        vulkan: vulkan::detect(),
1306        opengl: opengl::detect(),
1307        opencl: opencl::detect(),
1308    }
1309}
1310
1311/// Windows: Vulkan and OpenCL, but not OpenGL.
1312///
1313/// The Vulkan and OpenCL probes are the *same code* as Linux — those APIs are identical
1314/// across platforms and only the loader filename differs, which is why the split lives in
1315/// [`dl`] and the two `*_LIB` constants rather than in duplicated probes.
1316///
1317/// **OpenGL is absent here deliberately, not by oversight.** The Linux path gets a headless
1318/// context through EGL (`EGL_DEFAULT_DISPLAY` plus a surfaceless `eglMakeCurrent`), and
1319/// **stock Windows ships no `libEGL.dll`** — checked on a Windows 11 box that has
1320/// `vulkan-1.dll`, `opengl32.dll` and `OpenCL.dll` in `System32` but no EGL at all. A
1321/// Windows OpenGL version therefore needs WGL against a hidden window, which is a different
1322/// mechanism rather than a different library name, so it is tracked as separate work
1323/// (NOTES.md §6a) instead of being half-done here.
1324#[cfg(target_os = "windows")]
1325pub fn detect_gpu_apis() -> GpuApis {
1326    GpuApis {
1327        vulkan: vulkan::detect(),
1328        opengl: opengl::detect(),
1329        opencl: opencl::detect(),
1330    }
1331}
1332
1333/// macOS: all three, with Vulkan normally absent.
1334///
1335/// The Vulkan and OpenCL probes are the *same code* as Linux and Windows — those APIs are
1336/// identical across platforms and only the loader's filename differs, which is why the
1337/// split lives in [`dl`] and the two `*_LIB` constants. **OpenGL is a genuinely different
1338/// mechanism** and has its own module: CGL, which yields a context with no window and no
1339/// surface, where Linux uses EGL and Windows needs WGL against a hidden window.
1340///
1341/// **Vulkan will report nothing on a stock Mac, and that is correct.** macOS has no system
1342/// Vulkan; it exists only via MoltenVK once a user installs it. fastfetch prints no Vulkan
1343/// line here either. The probe is still wired up so that a machine *with* the SDK reports
1344/// accurately, and a failed `dlopen` of a missing library is the cheapest possible answer.
1345///
1346/// **OpenGL and OpenCL are both deprecated by Apple in favour of Metal** but are still
1347/// shipped and still functional. Reporting the version they actually return is the honest
1348/// answer; retch does not editorialise about deprecation in the field value.
1349#[cfg(target_os = "macos")]
1350pub fn detect_gpu_apis() -> GpuApis {
1351    GpuApis {
1352        vulkan: vulkan::detect(),
1353        opengl: opengl::detect(),
1354        opencl: opencl::detect(),
1355    }
1356}
1357
1358/// Other platforms: reports nothing rather than guessing.
1359#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
1360pub fn detect_gpu_apis() -> GpuApis {
1361    GpuApis::default()
1362}
1363
1364#[cfg(test)]
1365mod tests {
1366    use super::*;
1367
1368    /// The loader filenames are the one part of the Windows arm with no runtime guard: a
1369    /// typo does not fail, it makes the probe report "not installed", which is
1370    /// indistinguishable from a machine that genuinely has no Vulkan. Pin them.
1371    ///
1372    /// `vulkan-1.dll` and `OpenCL.dll` are the Khronos loaders' fixed names on Windows —
1373    /// not vendor DLLs, which register themselves behind these. Confirmed present in
1374    /// `System32` on the machine this was developed against.
1375    #[cfg(target_os = "windows")]
1376    #[test]
1377    fn test_windows_loader_names_are_the_khronos_loaders() {
1378        assert_eq!(VULKAN_LIB.to_str().unwrap(), "vulkan-1.dll");
1379        assert_eq!(OPENCL_LIB.to_str().unwrap(), "OpenCL.dll");
1380    }
1381
1382    /// The Linux sonames, pinned for the same reason and to keep the two arms visibly
1383    /// paired — a change to one should prompt a look at the other.
1384    #[cfg(target_os = "linux")]
1385    #[test]
1386    fn test_linux_loader_sonames() {
1387        assert_eq!(VULKAN_LIB.to_str().unwrap(), "libvulkan.so.1");
1388        assert_eq!(OPENCL_LIB.to_str().unwrap(), "libOpenCL.so.1");
1389    }
1390
1391    /// The AMD platform string this machine reports, run through the same formatter the
1392    /// Linux Mesa strings go through.
1393    ///
1394    /// Windows drivers phrase `CL_PLATFORM_VERSION` differently from Mesa — AMD's carries
1395    /// a build number in parentheses — so this pins that the `OpenCL ` prefix strip still
1396    /// does the right thing on a non-Mesa string, and that the parenthesised build number
1397    /// is **not** mistaken for the device descriptor `shorten_device_name` strips.
1398    #[test]
1399    fn test_format_opencl_handles_a_windows_vendor_platform_string() {
1400        assert_eq!(
1401            format_opencl(
1402                "OpenCL 2.1 AMD-APP (3661.0)",
1403                "AMD Accelerated Parallel Processing",
1404                Some("gfx1151"),
1405            ),
1406            "2.1 AMD-APP (3661.0) - AMD Accelerated Parallel Processing (gfx1151)"
1407        );
1408    }
1409
1410    /// A device name with no parenthesised driver descriptor must survive intact.
1411    ///
1412    /// The Linux fixtures all have one (Mesa appends `(radeonsi, phoenix, ACO, …)`), so
1413    /// nothing pinned the other branch until Windows produced a bare `gfx1151`.
1414    #[test]
1415    fn test_shorten_device_name_leaves_a_bare_name_alone() {
1416        assert_eq!(shorten_device_name("gfx1151"), "gfx1151");
1417        assert_eq!(shorten_device_name("  gfx1151  "), "gfx1151");
1418    }
1419
1420    #[test]
1421    fn test_format_vulkan_version_decodes_packed_fields() {
1422        // 0x00404155 is what this machine's loader reports: 1.4.341.
1423        assert_eq!(format_vulkan_version(0x0040_4155), "1.4.341");
1424        // major/minor/patch boundaries
1425        assert_eq!(format_vulkan_version(1 << 22), "1.0.0");
1426        assert_eq!(format_vulkan_version((1 << 22) | (2 << 12)), "1.2.0");
1427        assert_eq!(
1428            format_vulkan_version((1 << 22) | (3 << 12) | 290),
1429            "1.3.290"
1430        );
1431    }
1432
1433    #[test]
1434    fn test_format_vulkan_version_ignores_variant_bits() {
1435        // The top 3 bits are the variant; a non-Khronos variant must not leak into the
1436        // printed version or users see a leading number that means nothing to them.
1437        let with_variant = (1u32 << 29) | (1 << 22) | (4 << 12) | 354;
1438        assert_eq!(format_vulkan_version(with_variant), "1.4.354");
1439    }
1440
1441    #[test]
1442    fn test_device_type_rank_prefers_real_gpu_over_software() {
1443        // The case that matters: a real GPU (integrated=1) must outrank llvmpipe (CPU=4),
1444        // which is enumerated alongside it on any Mesa system.
1445        assert!(device_type_rank(1) < device_type_rank(4));
1446        assert!(device_type_rank(2) < device_type_rank(1)); // discrete beats integrated
1447        assert!(device_type_rank(3) < device_type_rank(4)); // virtual beats CPU
1448        assert!(device_type_rank(0) < device_type_rank(4)); // even "other" beats CPU
1449    }
1450
1451    #[test]
1452    fn test_format_vulkan_handles_unfilled_driver_chain() {
1453        // An instance below Vulkan 1.2 leaves these empty with no error, so the version
1454        // alone must still render.
1455        assert_eq!(format_vulkan("1.4.354", "", ""), "1.4.354");
1456        assert_eq!(format_vulkan("1.4.354", "radv", ""), "1.4.354 - radv");
1457        assert_eq!(
1458            format_vulkan("1.4.354", "radv", "Mesa 26.1.8"),
1459            "1.4.354 - radv [Mesa 26.1.8]"
1460        );
1461    }
1462
1463    #[test]
1464    fn test_format_opencl_distinguishes_inert_platform_from_working_one() {
1465        // The whole point of the field: rusticl without RUSTICL_ENABLE advertises 3.0 and
1466        // exposes nothing. fastfetch prints "3.0" for both of these.
1467        assert_eq!(
1468            format_opencl("OpenCL 3.0", "rusticl", None),
1469            "3.0 - rusticl (no device enabled)"
1470        );
1471        assert_eq!(
1472            format_opencl("OpenCL 3.0", "rusticl", Some("AMD Radeon 780M Graphics")),
1473            "3.0 - rusticl (AMD Radeon 780M Graphics)"
1474        );
1475        // A device string that is only whitespace is not a device.
1476        assert_eq!(
1477            format_opencl("OpenCL 3.0", "rusticl", Some("   ")),
1478            "3.0 - rusticl (no device enabled)"
1479        );
1480    }
1481
1482    #[test]
1483    fn test_format_opencl_without_platform_name() {
1484        assert_eq!(
1485            format_opencl("OpenCL 1.2", "", None),
1486            "1.2 (no device enabled)"
1487        );
1488        assert_eq!(format_opencl("OpenCL 1.2", "", Some("GPU")), "1.2 (GPU)");
1489        // A platform that does not carry the spec-mandated prefix is left alone rather
1490        // than having its first word eaten.
1491        assert_eq!(format_opencl("3.0", "x", Some("GPU")), "3.0 - x (GPU)");
1492    }
1493
1494    #[test]
1495    fn test_shorten_device_name_drops_the_driver_descriptor() {
1496        // The real string this machine returns, otherwise 80+ characters of driver detail.
1497        assert_eq!(
1498            shorten_device_name(
1499                "AMD Radeon 780M Graphics (radeonsi, phoenix, ACO, DRM 3.64, 7.1.13-200.fc44.x86_64)"
1500            ),
1501            "AMD Radeon 780M Graphics"
1502        );
1503        // A name with no descriptor is returned intact rather than truncated.
1504        assert_eq!(
1505            shorten_device_name("NVIDIA GeForce RTX 4090"),
1506            "NVIDIA GeForce RTX 4090"
1507        );
1508        // Only " (" splits, so a parenthesis inside a model name survives.
1509        assert_eq!(
1510            shorten_device_name("Intel(R) Arc(TM) A770"),
1511            "Intel(R) Arc(TM) A770"
1512        );
1513    }
1514
1515    #[test]
1516    fn test_cstr_field_stops_at_nul() {
1517        let mut buf = [0u8; 16];
1518        buf[..4].copy_from_slice(b"radv");
1519        assert_eq!(cstr_field(&buf), "radv");
1520        // An unwritten field is empty, not garbage — this is how an ignored pNext presents.
1521        assert_eq!(cstr_field(&[0u8; 16]), "");
1522        // No NUL at all: use the whole buffer rather than reading past it.
1523        assert_eq!(cstr_field(b"abcd"), "abcd");
1524    }
1525
1526    /// macOS loader names, pinned for the same reason as the Windows ones: a typo here
1527    /// does not fail, it makes the probe report "not installed" — indistinguishable from a
1528    /// machine that genuinely lacks the API.
1529    ///
1530    /// **The framework paths must be absolute.** A bare `dlopen("OpenCL")` does *not*
1531    /// resolve on macOS — verified on macOS 26 — because system frameworks live in the
1532    /// dyld shared cache rather than on disk. Shortening either of these to a bare name
1533    /// would silently disable the field on every Mac.
1534    #[cfg(target_os = "macos")]
1535    #[test]
1536    fn test_macos_loader_names() {
1537        assert_eq!(VULKAN_LIB.to_str().unwrap(), "libvulkan.1.dylib");
1538        assert_eq!(
1539            OPENCL_LIB.to_str().unwrap(),
1540            "/System/Library/Frameworks/OpenCL.framework/OpenCL"
1541        );
1542        // The framework path is absolute precisely because the short name does not work.
1543        assert!(OPENCL_LIB.to_str().unwrap().starts_with('/'));
1544    }
1545
1546    /// The CGL profile attribute is the single value that decides the OpenGL version
1547    /// reported on macOS, so it is pinned.
1548    ///
1549    /// Measured on an M3 Pro, all four variants in one run: no attribute and
1550    /// `kCGLOGLPVersion_Legacy` both yield **`2.1 Metal - 90.5`**, while
1551    /// `kCGLOGLPVersion_3_2_Core` and `kCGLOGLPVersion_GL4_Core` yield
1552    /// **`4.1 Metal - 90.5`** — which is what fastfetch reports. Dropping this attribute
1553    /// would silently halve the reported version on every Mac while still producing a
1554    /// perfectly plausible-looking string, which is exactly the failure mode this repo
1555    /// keeps recording.
1556    #[cfg(target_os = "macos")]
1557    #[test]
1558    fn test_macos_cgl_requests_a_core_profile() {
1559        // 0x4100 is kCGLOGLPVersion_GL4_Core; 0x1000 is kCGLOGLPVersion_Legacy, which
1560        // caps at OpenGL 2.1 and must not be what we ask for.
1561        assert_eq!(opengl::KCGL_OGLP_VERSION_GL4_CORE, 0x4100);
1562        assert_ne!(opengl::KCGL_OGLP_VERSION_GL4_CORE, 0x1000);
1563        assert_eq!(opengl::KCGLPFA_OPENGL_PROFILE, 99);
1564        assert_eq!(opengl::KCGLPFA_ACCELERATED, 73);
1565    }
1566}