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_char, c_int, c_void, CStr};
38
39/// Versions reported by each graphics/compute API present on the system.
40///
41/// A `None` means the loader is absent or answered nothing usable — both are normal.
42#[derive(Debug, Default, Clone, PartialEq, Eq)]
43pub struct GpuApis {
44    /// Vulkan: device `apiVersion`, driver name and driver info, e.g.
45    /// `1.4.354 - radv [Mesa 26.1.8]`.
46    pub vulkan: Option<String>,
47    /// OpenGL: the `GL_VERSION` string of a headless context, e.g.
48    /// `4.6 (Compatibility Profile) Mesa 26.1.8`.
49    pub opengl: Option<String>,
50    /// OpenCL: platform version, provider, and what device (if any) is actually exposed.
51    pub opencl: Option<String>,
52}
53
54/// Decode a packed Vulkan version into `major.minor.patch`.
55///
56/// Vulkan packs the version as `variant:3 | major:7 | minor:10 | patch:12`. The variant
57/// field is deliberately ignored: it is non-zero only for non-Khronos derivatives, and
58/// including it would print a leading number no user recognises.
59pub fn format_vulkan_version(packed: u32) -> String {
60    let major = (packed >> 22) & 0x7F;
61    let minor = (packed >> 12) & 0x3FF;
62    let patch = packed & 0xFFF;
63    format!("{major}.{minor}.{patch}")
64}
65
66/// Rank a Vulkan `VkPhysicalDeviceType` so the most capable real device wins.
67///
68/// Lower is better. The ordering is load-bearing rather than cosmetic: a machine with a
69/// real GPU almost always *also* exposes Mesa's `llvmpipe` software rasteriser as a
70/// `CPU` device, so picking the first enumerated device would report software rendering
71/// on a box with a perfectly good GPU. Observed on this hardware: the AMD 780M enumerates
72/// as `INTEGRATED_GPU` (1) alongside `llvmpipe` as `CPU` (4).
73pub fn device_type_rank(device_type: u32) -> u8 {
74    match device_type {
75        2 => 0, // DISCRETE_GPU
76        1 => 1, // INTEGRATED_GPU
77        3 => 2, // VIRTUAL_GPU
78        4 => 4, // CPU (software rasteriser — a last resort, never a preference)
79        _ => 3, // OTHER
80    }
81}
82
83/// Render the Vulkan field from its parts.
84///
85/// `driver_name`/`driver_info` are empty when the driver did not fill the
86/// `VkPhysicalDeviceDriverProperties` chain, which happens on any instance created below
87/// Vulkan 1.2 — silently, with no error. The version alone is still worth printing.
88pub fn format_vulkan(version: &str, driver_name: &str, driver_info: &str) -> String {
89    match (driver_name.trim(), driver_info.trim()) {
90        ("", _) => version.to_string(),
91        (name, "") => format!("{version} - {name}"),
92        (name, info) => format!("{version} - {name} [{info}]"),
93    }
94}
95
96/// Render the OpenCL field, distinguishing "usable" from "present but inert".
97///
98/// A platform that advertises a version while exposing no device cannot run anything, so
99/// saying so is the whole point of the field. See the module docs for why this does not
100/// simply enable rusticl for itself and report the better-looking answer.
101pub fn format_opencl(version: &str, platform: &str, device: Option<&str>) -> String {
102    // CL_PLATFORM_VERSION is specified to start with "OpenCL <major>.<minor>", so the raw
103    // string would render as "OpenCL: OpenCL 3.0" under the field's own label.
104    let version = version
105        .trim()
106        .strip_prefix("OpenCL ")
107        .unwrap_or(version.trim())
108        .trim();
109    let platform = platform.trim();
110    match device {
111        Some(d) if !d.trim().is_empty() => {
112            if platform.is_empty() {
113                format!("{version} ({})", d.trim())
114            } else {
115                format!("{version} - {platform} ({})", d.trim())
116            }
117        }
118        _ => {
119            if platform.is_empty() {
120                format!("{version} (no device enabled)")
121            } else {
122                format!("{version} - {platform} (no device enabled)")
123            }
124        }
125    }
126}
127
128/// Shorten a driver-reported device name to the part a human recognises.
129///
130/// Mesa reports OpenCL and GL device names with a full driver descriptor appended, e.g.
131/// `AMD Radeon 780M Graphics (radeonsi, phoenix, ACO, DRM 3.64, 7.1.13-200.fc44.x86_64)`.
132/// That is 80+ characters of kernel and driver detail that pushes the line into wrapping
133/// and tells the reader nothing the `GPU` field does not already say, so everything from
134/// the first parenthesised descriptor on is dropped.
135pub fn shorten_device_name(name: &str) -> String {
136    match name.find(" (") {
137        Some(i) => name[..i].trim().to_string(),
138        None => name.trim().to_string(),
139    }
140}
141
142/// Trim a NUL-terminated fixed-size C string field into a `String`.
143///
144/// Reads up to the first NUL and ignores the rest of the buffer. Returns an empty string
145/// when the field was never written, which is how an unfilled `pNext` chain presents.
146pub fn cstr_field(buf: &[u8]) -> String {
147    let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
148    String::from_utf8_lossy(&buf[..end]).into_owned()
149}
150
151// ---------------------------------------------------------------------------
152// Linux implementation
153// ---------------------------------------------------------------------------
154
155#[cfg(target_os = "linux")]
156mod dl {
157    use super::*;
158
159    extern "C" {
160        pub fn dlopen(filename: *const c_char, flags: c_int) -> *mut c_void;
161        pub fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void;
162        pub fn dlclose(handle: *mut c_void) -> c_int;
163    }
164    pub const RTLD_NOW: c_int = 2;
165    pub const RTLD_LOCAL: c_int = 0;
166
167    /// Open a shared library by soname, or `None` if it is not installed.
168    ///
169    /// `RTLD_LOCAL` keeps the symbols out of the global namespace so loading, say, a
170    /// software OpenCL ICD cannot shadow symbols another probe resolves later.
171    pub fn open(soname: &CStr) -> Option<*mut c_void> {
172        // SAFETY: `soname` is a valid NUL-terminated C string for the duration of the
173        // call. A null return is the documented "not found" answer and is handled.
174        let h = unsafe { dlopen(soname.as_ptr(), RTLD_NOW | RTLD_LOCAL) };
175        (!h.is_null()).then_some(h)
176    }
177
178    /// Resolve a symbol, or `None` if the library does not export it.
179    pub fn sym(handle: *mut c_void, name: &CStr) -> Option<*mut c_void> {
180        // SAFETY: `handle` came from `open` above and has not been closed; `name` is a
181        // valid NUL-terminated C string.
182        let p = unsafe { dlsym(handle, name.as_ptr()) };
183        (!p.is_null()).then_some(p)
184    }
185
186    /// Close a handle opened by [`open`].
187    pub fn close(handle: *mut c_void) {
188        // SAFETY: `handle` came from `open` and is not used afterwards.
189        unsafe {
190            dlclose(handle);
191        }
192    }
193}
194
195#[cfg(target_os = "linux")]
196mod vulkan {
197    use super::dl;
198    use super::*;
199
200    const VK_STRUCTURE_TYPE_APPLICATION_INFO: u32 = 0;
201    const VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO: u32 = 1;
202    const VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2: u32 = 1000059001;
203    const VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES: u32 = 1000196000;
204
205    /// `VkPhysicalDeviceProperties2` places `properties` after `sType` + padding + `pNext`.
206    const PROPS2_BODY: usize = 16;
207    /// Offsets within `VkPhysicalDeviceProperties`.
208    const OFF_API_VERSION: usize = 0;
209    const OFF_DEVICE_TYPE: usize = 16;
210    const OFF_DEVICE_NAME: usize = 20;
211    /// Comfortably larger than `sizeof(VkPhysicalDeviceProperties)` (~824 bytes). The
212    /// struct embeds `VkPhysicalDeviceLimits` (100+ fields) that this probe never reads,
213    /// so it is handled as a sized byte buffer with documented offsets — the same approach
214    /// `memory.rs` uses for SMBIOS type-17 and `win_iftable.rs` for `MIB_IF_ROW2`.
215    const PROPS_BUF: usize = 1024;
216
217    /// Offsets within `VkPhysicalDeviceDriverProperties`.
218    const OFF_DRIVER_NAME: usize = 20;
219    const OFF_DRIVER_INFO: usize = 276;
220    const DRIVER_BUF: usize = 560;
221    const VK_MAX_NAME: usize = 256;
222
223    #[repr(C)]
224    struct AppInfo {
225        s_type: u32,
226        p_next: *const c_void,
227        app_name: *const c_char,
228        app_version: u32,
229        engine_name: *const c_char,
230        engine_version: u32,
231        api_version: u32,
232    }
233
234    #[repr(C)]
235    struct InstanceCreateInfo {
236        s_type: u32,
237        p_next: *const c_void,
238        flags: u32,
239        app_info: *const AppInfo,
240        layer_count: u32,
241        layer_names: *const *const c_char,
242        ext_count: u32,
243        ext_names: *const *const c_char,
244    }
245
246    type VkCreateInstance =
247        unsafe extern "C" fn(*const InstanceCreateInfo, *const c_void, *mut *mut c_void) -> i32;
248    type VkDestroyInstance = unsafe extern "C" fn(*mut c_void, *const c_void);
249    type VkEnumeratePhysicalDevices =
250        unsafe extern "C" fn(*mut c_void, *mut u32, *mut *mut c_void) -> i32;
251    type VkGetPhysicalDeviceProperties2 = unsafe extern "C" fn(*mut c_void, *mut c_void);
252    type VkGetInstanceProcAddr = unsafe extern "C" fn(*mut c_void, *const c_char) -> *mut c_void;
253
254    /// Query the best physical device's API version and driver identity.
255    ///
256    /// Returns `None` when Vulkan is absent, no instance can be created, or no device is
257    /// present — all normal on a headless or GPU-less machine.
258    pub fn detect() -> Option<String> {
259        let lib = dl::open(c"libvulkan.so.1")?;
260        let result = detect_with(lib);
261        dl::close(lib);
262        result
263    }
264
265    fn detect_with(lib: *mut c_void) -> Option<String> {
266        let create = dl::sym(lib, c"vkCreateInstance")?;
267        let gipa = dl::sym(lib, c"vkGetInstanceProcAddr")?;
268
269        // SAFETY: every pointer below is either freshly resolved from the Vulkan loader or
270        // a local we own. Buffers passed to the driver are sized at or above the structs
271        // the API writes, and every returned code is checked before the result is read.
272        unsafe {
273            let create: VkCreateInstance = std::mem::transmute(create);
274            let gipa: VkGetInstanceProcAddr = std::mem::transmute(gipa);
275
276            let app = AppInfo {
277                s_type: VK_STRUCTURE_TYPE_APPLICATION_INFO,
278                p_next: std::ptr::null(),
279                app_name: c"retch".as_ptr(),
280                app_version: 0,
281                engine_name: std::ptr::null(),
282                engine_version: 0,
283                // Must be >= 1.2. With a 1.0 or 1.1 instance the driver SILENTLY IGNORES
284                // the `VkPhysicalDeviceDriverProperties` chain below and the driver name
285                // and info come back as empty strings with no error anywhere — verified
286                // against a 1.0 instance, which returned the right version and blank
287                // driver fields.
288                api_version: (1 << 22) | (2 << 12),
289            };
290            let ci = InstanceCreateInfo {
291                s_type: VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
292                p_next: std::ptr::null(),
293                flags: 0,
294                app_info: &app,
295                layer_count: 0,
296                layer_names: std::ptr::null(),
297                ext_count: 0,
298                ext_names: std::ptr::null(),
299            };
300
301            let mut instance: *mut c_void = std::ptr::null_mut();
302            if create(&ci, std::ptr::null(), &mut instance) != 0 || instance.is_null() {
303                return None;
304            }
305
306            let out = read_best_device(instance, gipa);
307
308            if let Some(p) = dl::sym(lib, c"vkDestroyInstance") {
309                let destroy: VkDestroyInstance = std::mem::transmute(p);
310                destroy(instance, std::ptr::null());
311            }
312            out
313        }
314    }
315
316    /// SAFETY: caller guarantees `instance` is a live `VkInstance` and `gipa` is the
317    /// loader's `vkGetInstanceProcAddr`.
318    unsafe fn read_best_device(
319        instance: *mut c_void,
320        gipa: VkGetInstanceProcAddr,
321    ) -> Option<String> {
322        let enum_ptr = gipa(instance, c"vkEnumeratePhysicalDevices".as_ptr());
323        let props_ptr = gipa(instance, c"vkGetPhysicalDeviceProperties2".as_ptr());
324        if enum_ptr.is_null() || props_ptr.is_null() {
325            return None;
326        }
327        let enumerate: VkEnumeratePhysicalDevices = std::mem::transmute(enum_ptr);
328        let get_props2: VkGetPhysicalDeviceProperties2 = std::mem::transmute(props_ptr);
329
330        let mut count: u32 = 0;
331        if enumerate(instance, &mut count, std::ptr::null_mut()) != 0 || count == 0 {
332            return None;
333        }
334        let mut devices = vec![std::ptr::null_mut::<c_void>(); count as usize];
335        if enumerate(instance, &mut count, devices.as_mut_ptr()) != 0 {
336            return None;
337        }
338
339        let mut best: Option<(u8, String)> = None;
340        for device in devices.iter().take(count as usize) {
341            let mut driver = vec![0u8; DRIVER_BUF];
342            driver[0..4].copy_from_slice(
343                &VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES.to_ne_bytes(),
344            );
345            let mut props = vec![0u8; PROPS2_BODY + PROPS_BUF];
346            props[0..4]
347                .copy_from_slice(&VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2.to_ne_bytes());
348            let chain = driver.as_mut_ptr() as usize;
349            props[8..16].copy_from_slice(&chain.to_ne_bytes());
350
351            get_props2(*device, props.as_mut_ptr() as *mut c_void);
352
353            let at = |off: usize| -> u32 {
354                let s = PROPS2_BODY + off;
355                u32::from_ne_bytes(props[s..s + 4].try_into().unwrap_or([0; 4]))
356            };
357            let api = at(OFF_API_VERSION);
358            let dtype = at(OFF_DEVICE_TYPE);
359            let name_start = PROPS2_BODY + OFF_DEVICE_NAME;
360            let _device_name = cstr_field(&props[name_start..name_start + VK_MAX_NAME]);
361
362            let driver_name = cstr_field(&driver[OFF_DRIVER_NAME..OFF_DRIVER_NAME + VK_MAX_NAME]);
363            let driver_info = cstr_field(&driver[OFF_DRIVER_INFO..OFF_DRIVER_INFO + VK_MAX_NAME]);
364
365            let rank = device_type_rank(dtype);
366            let rendered = format_vulkan(&format_vulkan_version(api), &driver_name, &driver_info);
367            if best.as_ref().is_none_or(|(r, _)| rank < *r) {
368                best = Some((rank, rendered));
369            }
370        }
371        best.map(|(_, s)| s)
372    }
373}
374
375#[cfg(target_os = "linux")]
376mod opengl {
377    use super::dl;
378    use super::*;
379
380    const EGL_OPENGL_API: u32 = 0x30A2;
381    const EGL_NONE: i32 = 0x3038;
382    const EGL_SURFACE_TYPE: i32 = 0x3033;
383    const EGL_PBUFFER_BIT: i32 = 0x0001;
384    const EGL_RENDERABLE_TYPE: i32 = 0x3040;
385    const EGL_OPENGL_BIT: i32 = 0x0008;
386    const GL_VERSION: u32 = 0x1F02;
387
388    type EglGetDisplay = unsafe extern "C" fn(*mut c_void) -> *mut c_void;
389    type EglInitialize = unsafe extern "C" fn(*mut c_void, *mut i32, *mut i32) -> u32;
390    type EglBindApi = unsafe extern "C" fn(u32) -> u32;
391    type EglChooseConfig =
392        unsafe extern "C" fn(*mut c_void, *const i32, *mut *mut c_void, i32, *mut i32) -> u32;
393    type EglCreateContext =
394        unsafe extern "C" fn(*mut c_void, *mut c_void, *mut c_void, *const i32) -> *mut c_void;
395    type EglMakeCurrent =
396        unsafe extern "C" fn(*mut c_void, *mut c_void, *mut c_void, *mut c_void) -> u32;
397    type EglGetProcAddress = unsafe extern "C" fn(*const c_char) -> *mut c_void;
398    type EglTerminate = unsafe extern "C" fn(*mut c_void) -> u32;
399    type GlGetString = unsafe extern "C" fn(u32) -> *const c_char;
400
401    /// Read `GL_VERSION` from a headless EGL context.
402    ///
403    /// Deliberately uses EGL with `EGL_DEFAULT_DISPLAY` and a surfaceless
404    /// `eglMakeCurrent`, so this works with no X or Wayland connection and without
405    /// touching the environment. GLX would require a display server.
406    ///
407    /// **The context choice decides the number printed.** Passing no attribute list asks
408    /// for the driver's default, which is the highest *compatibility* profile — matching
409    /// what fastfetch reports. Requesting a core profile instead reports a different
410    /// string for the same machine (`glxinfo -B` says `4.6 (Core Profile)` here where this
411    /// returns `4.6 (Compatibility Profile)`), so the choice is deliberate, not incidental.
412    pub fn detect() -> Option<String> {
413        let lib = dl::open(c"libEGL.so.1")?;
414        let out = detect_with(lib);
415        dl::close(lib);
416        out
417    }
418
419    fn detect_with(lib: *mut c_void) -> Option<String> {
420        let get_display = dl::sym(lib, c"eglGetDisplay")?;
421        let initialize = dl::sym(lib, c"eglInitialize")?;
422        let bind_api = dl::sym(lib, c"eglBindAPI")?;
423        let choose = dl::sym(lib, c"eglChooseConfig")?;
424        let create_context = dl::sym(lib, c"eglCreateContext")?;
425        let make_current = dl::sym(lib, c"eglMakeCurrent")?;
426        let get_proc = dl::sym(lib, c"eglGetProcAddress")?;
427
428        // SAFETY: all pointers are freshly resolved from libEGL or locals we own. Every
429        // EGL call's status is checked before its output is used, and the display is
430        // terminated on the success path.
431        unsafe {
432            let get_display: EglGetDisplay = std::mem::transmute(get_display);
433            let initialize: EglInitialize = std::mem::transmute(initialize);
434            let bind_api: EglBindApi = std::mem::transmute(bind_api);
435            let choose: EglChooseConfig = std::mem::transmute(choose);
436            let create_context: EglCreateContext = std::mem::transmute(create_context);
437            let make_current: EglMakeCurrent = std::mem::transmute(make_current);
438            let get_proc: EglGetProcAddress = std::mem::transmute(get_proc);
439
440            // EGL_DEFAULT_DISPLAY is a null handle.
441            let display = get_display(std::ptr::null_mut());
442            if display.is_null() {
443                return None;
444            }
445            let (mut major, mut minor) = (0i32, 0i32);
446            if initialize(display, &mut major, &mut minor) == 0 {
447                return None;
448            }
449            // Desktop GL specifically; an ES-only stack answers 0 here and is reported as
450            // "no OpenGL" rather than being silently downgraded to an ES version string.
451            if bind_api(EGL_OPENGL_API) == 0 {
452                terminate(lib, display);
453                return None;
454            }
455
456            let attrs = [
457                EGL_SURFACE_TYPE,
458                EGL_PBUFFER_BIT,
459                EGL_RENDERABLE_TYPE,
460                EGL_OPENGL_BIT,
461                EGL_NONE,
462            ];
463            let mut config: *mut c_void = std::ptr::null_mut();
464            let mut configs = 0i32;
465            if choose(display, attrs.as_ptr(), &mut config, 1, &mut configs) == 0 || configs == 0 {
466                terminate(lib, display);
467                return None;
468            }
469            let context = create_context(display, config, std::ptr::null_mut(), std::ptr::null());
470            if context.is_null() {
471                terminate(lib, display);
472                return None;
473            }
474            if make_current(display, std::ptr::null_mut(), std::ptr::null_mut(), context) == 0 {
475                terminate(lib, display);
476                return None;
477            }
478            let gl_get_string = get_proc(c"glGetString".as_ptr());
479            let version = if gl_get_string.is_null() {
480                None
481            } else {
482                let gl_get_string: GlGetString = std::mem::transmute(gl_get_string);
483                let p = gl_get_string(GL_VERSION);
484                if p.is_null() {
485                    None
486                } else {
487                    Some(CStr::from_ptr(p).to_string_lossy().into_owned())
488                }
489            };
490            terminate(lib, display);
491            version.filter(|v| !v.trim().is_empty())
492        }
493    }
494
495    /// Best-effort `eglTerminate`; failure to release is not worth reporting to the user.
496    ///
497    /// SAFETY: `display` is a live EGL display obtained from `eglGetDisplay`.
498    unsafe fn terminate(lib: *mut c_void, display: *mut c_void) {
499        if let Some(p) = dl::sym(lib, c"eglTerminate") {
500            let terminate: EglTerminate = std::mem::transmute(p);
501            terminate(display);
502        }
503    }
504}
505
506#[cfg(target_os = "linux")]
507mod opencl {
508    use super::dl;
509    use super::*;
510
511    const CL_PLATFORM_VERSION: u32 = 0x0901;
512    const CL_PLATFORM_NAME: u32 = 0x0902;
513    const CL_DEVICE_TYPE_ALL: u64 = 0xFFFF_FFFF;
514    const CL_DEVICE_NAME: u32 = 0x102B;
515
516    type ClGetPlatformIDs = unsafe extern "C" fn(u32, *mut *mut c_void, *mut u32) -> i32;
517    type ClGetPlatformInfo =
518        unsafe extern "C" fn(*mut c_void, u32, usize, *mut c_void, *mut usize) -> i32;
519    type ClGetDeviceIDs =
520        unsafe extern "C" fn(*mut c_void, u64, u32, *mut *mut c_void, *mut u32) -> i32;
521    type ClGetDeviceInfo =
522        unsafe extern "C" fn(*mut c_void, u32, usize, *mut c_void, *mut usize) -> i32;
523
524    extern "C" {
525        fn dup(oldfd: c_int) -> c_int;
526        fn dup2(oldfd: c_int, newfd: c_int) -> c_int;
527        fn close(fd: c_int) -> c_int;
528        fn open(path: *const c_char, flags: c_int) -> c_int;
529    }
530    const STDERR_FILENO: c_int = 2;
531    const O_WRONLY: c_int = 1;
532
533    /// Silences `stderr` for its lifetime, restoring the original on drop.
534    ///
535    /// **Why this exists:** initialising an OpenCL driver can make it print to `stderr`
536    /// over which retch has no control. Mesa's rusticl emits a 247-byte "Patched Mesa
537    /// libclc not detected" warning on every enumeration once `RUSTICL_ENABLE` is set, and
538    /// a fetch tool that sprays a driver's diagnostics into the terminal is broken. This
539    /// was caught by `test_cli_full_mode`, which asserts retch writes nothing to `stderr`;
540    /// fastfetch has the same leak and simply lets it through.
541    ///
542    /// **The caveat, stated rather than hidden:** file descriptors are process-wide, so
543    /// this suppresses `stderr` for *every* thread while it is alive, and could in
544    /// principle swallow a concurrent probe's error message. It is therefore scoped as
545    /// tightly as possible — only around the OpenCL calls, ~20 ms — rather than around the
546    /// collection scope. Moving the probe out of the concurrent scope would make the
547    /// suppression provably safe, but costs ~100 ms serially and pushes `--full` past
548    /// `fastfetch -c all` (1.02 s here), which NOTES.md §3 treats as blocking.
549    struct SuppressStderr {
550        saved: c_int,
551    }
552
553    impl SuppressStderr {
554        fn new() -> Option<Self> {
555            // SAFETY: plain fd manipulation. Every call's result is checked, and the
556            // original descriptor is retained for restoration in `drop`.
557            unsafe {
558                let saved = dup(STDERR_FILENO);
559                if saved < 0 {
560                    return None;
561                }
562                let devnull = open(c"/dev/null".as_ptr(), O_WRONLY);
563                if devnull < 0 {
564                    close(saved);
565                    return None;
566                }
567                dup2(devnull, STDERR_FILENO);
568                close(devnull);
569                Some(Self { saved })
570            }
571        }
572    }
573
574    impl Drop for SuppressStderr {
575        fn drop(&mut self) {
576            // SAFETY: `self.saved` is a live descriptor duplicated from stderr in `new`.
577            unsafe {
578                dup2(self.saved, STDERR_FILENO);
579                close(self.saved);
580            }
581        }
582    }
583
584    /// Report the OpenCL platform version, its provider, and whether a device exists.
585    ///
586    /// The device count is the point: see the module docs for why a platform advertising a
587    /// version while exposing no device is reported as such rather than as a bare version.
588    pub fn detect() -> Option<String> {
589        // Held across the whole probe: the driver can write to stderr at dlopen, at
590        // platform enumeration, or at device enumeration, and rusticl does so at the last.
591        let _quiet = SuppressStderr::new();
592        let lib = dl::open(c"libOpenCL.so.1")?;
593        let out = detect_with(lib);
594        dl::close(lib);
595        out
596    }
597
598    fn detect_with(lib: *mut c_void) -> Option<String> {
599        let get_platform_ids = dl::sym(lib, c"clGetPlatformIDs")?;
600        let get_platform_info = dl::sym(lib, c"clGetPlatformInfo")?;
601
602        // SAFETY: pointers are resolved from the ICD loader; every call's return code is
603        // checked, and every buffer is sized by a preceding size query.
604        unsafe {
605            let get_platform_ids: ClGetPlatformIDs = std::mem::transmute(get_platform_ids);
606            let get_platform_info: ClGetPlatformInfo = std::mem::transmute(get_platform_info);
607
608            let mut count: u32 = 0;
609            if get_platform_ids(0, std::ptr::null_mut(), &mut count) != 0 || count == 0 {
610                return None;
611            }
612            let mut platforms = vec![std::ptr::null_mut::<c_void>(); count as usize];
613            if get_platform_ids(count, platforms.as_mut_ptr(), std::ptr::null_mut()) != 0 {
614                return None;
615            }
616            let platform = *platforms.first()?;
617
618            let version = query(get_platform_info, platform, CL_PLATFORM_VERSION)?;
619            let name = query(get_platform_info, platform, CL_PLATFORM_NAME).unwrap_or_default();
620
621            let device = dl::sym(lib, c"clGetDeviceIDs")
622                .zip(dl::sym(lib, c"clGetDeviceInfo"))
623                .and_then(|(ids, info)| first_device_name(platform, ids, info))
624                .map(|n| shorten_device_name(&n));
625
626            Some(format_opencl(&version, &name, device.as_deref()))
627        }
628    }
629
630    /// Two-call size-then-read query against a platform.
631    ///
632    /// SAFETY: `f` is `clGetPlatformInfo` and `obj` a valid platform id.
633    unsafe fn query(f: ClGetPlatformInfo, obj: *mut c_void, param: u32) -> Option<String> {
634        let mut size: usize = 0;
635        if f(obj, param, 0, std::ptr::null_mut(), &mut size) != 0 || size == 0 {
636            return None;
637        }
638        let mut buf = vec![0u8; size];
639        if f(
640            obj,
641            param,
642            size,
643            buf.as_mut_ptr() as *mut c_void,
644            std::ptr::null_mut(),
645        ) != 0
646        {
647            return None;
648        }
649        let s = cstr_field(&buf);
650        (!s.trim().is_empty()).then(|| s.trim().to_string())
651    }
652
653    /// Name of the first device on a platform, or `None` when it exposes none.
654    ///
655    /// SAFETY: `ids`/`info` are the corresponding OpenCL entry points and `platform` is a
656    /// valid platform id.
657    unsafe fn first_device_name(
658        platform: *mut c_void,
659        ids: *mut c_void,
660        info: *mut c_void,
661    ) -> Option<String> {
662        let get_device_ids: ClGetDeviceIDs = std::mem::transmute(ids);
663        let get_device_info: ClGetDeviceInfo = std::mem::transmute(info);
664
665        let mut count: u32 = 0;
666        // A platform with no usable device answers CL_DEVICE_NOT_FOUND (-1) here. That is
667        // the rusticl-without-RUSTICL_ENABLE state, and it is a real answer, not an error.
668        if get_device_ids(
669            platform,
670            CL_DEVICE_TYPE_ALL,
671            0,
672            std::ptr::null_mut(),
673            &mut count,
674        ) != 0
675            || count == 0
676        {
677            return None;
678        }
679        let mut devices = vec![std::ptr::null_mut::<c_void>(); count as usize];
680        if get_device_ids(
681            platform,
682            CL_DEVICE_TYPE_ALL,
683            count,
684            devices.as_mut_ptr(),
685            std::ptr::null_mut(),
686        ) != 0
687        {
688            return None;
689        }
690        let device = *devices.first()?;
691        let mut size: usize = 0;
692        if get_device_info(device, CL_DEVICE_NAME, 0, std::ptr::null_mut(), &mut size) != 0
693            || size == 0
694        {
695            return None;
696        }
697        let mut buf = vec![0u8; size];
698        if get_device_info(
699            device,
700            CL_DEVICE_NAME,
701            size,
702            buf.as_mut_ptr() as *mut c_void,
703            std::ptr::null_mut(),
704        ) != 0
705        {
706            return None;
707        }
708        let s = cstr_field(&buf);
709        (!s.trim().is_empty()).then(|| s.trim().to_string())
710    }
711}
712
713/// Detect Vulkan, OpenGL and OpenCL versions.
714///
715/// Linux only for now: the loaders are opened by their Linux sonames. Every other platform
716/// returns an empty set rather than a wrong answer, matching the v0.5.0/v0.7.0 precedent
717/// for Linux-first field groups.
718#[cfg(target_os = "linux")]
719pub fn detect_gpu_apis() -> GpuApis {
720    GpuApis {
721        vulkan: vulkan::detect(),
722        opengl: opengl::detect(),
723        opencl: opencl::detect(),
724    }
725}
726
727/// Non-Linux stub: reports nothing rather than guessing.
728#[cfg(not(target_os = "linux"))]
729pub fn detect_gpu_apis() -> GpuApis {
730    GpuApis::default()
731}
732
733#[cfg(test)]
734mod tests {
735    use super::*;
736
737    #[test]
738    fn test_format_vulkan_version_decodes_packed_fields() {
739        // 0x00404155 is what this machine's loader reports: 1.4.341.
740        assert_eq!(format_vulkan_version(0x0040_4155), "1.4.341");
741        // major/minor/patch boundaries
742        assert_eq!(format_vulkan_version(1 << 22), "1.0.0");
743        assert_eq!(format_vulkan_version((1 << 22) | (2 << 12)), "1.2.0");
744        assert_eq!(
745            format_vulkan_version((1 << 22) | (3 << 12) | 290),
746            "1.3.290"
747        );
748    }
749
750    #[test]
751    fn test_format_vulkan_version_ignores_variant_bits() {
752        // The top 3 bits are the variant; a non-Khronos variant must not leak into the
753        // printed version or users see a leading number that means nothing to them.
754        let with_variant = (1u32 << 29) | (1 << 22) | (4 << 12) | 354;
755        assert_eq!(format_vulkan_version(with_variant), "1.4.354");
756    }
757
758    #[test]
759    fn test_device_type_rank_prefers_real_gpu_over_software() {
760        // The case that matters: a real GPU (integrated=1) must outrank llvmpipe (CPU=4),
761        // which is enumerated alongside it on any Mesa system.
762        assert!(device_type_rank(1) < device_type_rank(4));
763        assert!(device_type_rank(2) < device_type_rank(1)); // discrete beats integrated
764        assert!(device_type_rank(3) < device_type_rank(4)); // virtual beats CPU
765        assert!(device_type_rank(0) < device_type_rank(4)); // even "other" beats CPU
766    }
767
768    #[test]
769    fn test_format_vulkan_handles_unfilled_driver_chain() {
770        // An instance below Vulkan 1.2 leaves these empty with no error, so the version
771        // alone must still render.
772        assert_eq!(format_vulkan("1.4.354", "", ""), "1.4.354");
773        assert_eq!(format_vulkan("1.4.354", "radv", ""), "1.4.354 - radv");
774        assert_eq!(
775            format_vulkan("1.4.354", "radv", "Mesa 26.1.8"),
776            "1.4.354 - radv [Mesa 26.1.8]"
777        );
778    }
779
780    #[test]
781    fn test_format_opencl_distinguishes_inert_platform_from_working_one() {
782        // The whole point of the field: rusticl without RUSTICL_ENABLE advertises 3.0 and
783        // exposes nothing. fastfetch prints "3.0" for both of these.
784        assert_eq!(
785            format_opencl("OpenCL 3.0", "rusticl", None),
786            "3.0 - rusticl (no device enabled)"
787        );
788        assert_eq!(
789            format_opencl("OpenCL 3.0", "rusticl", Some("AMD Radeon 780M Graphics")),
790            "3.0 - rusticl (AMD Radeon 780M Graphics)"
791        );
792        // A device string that is only whitespace is not a device.
793        assert_eq!(
794            format_opencl("OpenCL 3.0", "rusticl", Some("   ")),
795            "3.0 - rusticl (no device enabled)"
796        );
797    }
798
799    #[test]
800    fn test_format_opencl_without_platform_name() {
801        assert_eq!(
802            format_opencl("OpenCL 1.2", "", None),
803            "1.2 (no device enabled)"
804        );
805        assert_eq!(format_opencl("OpenCL 1.2", "", Some("GPU")), "1.2 (GPU)");
806        // A platform that does not carry the spec-mandated prefix is left alone rather
807        // than having its first word eaten.
808        assert_eq!(format_opencl("3.0", "x", Some("GPU")), "3.0 - x (GPU)");
809    }
810
811    #[test]
812    fn test_shorten_device_name_drops_the_driver_descriptor() {
813        // The real string this machine returns, otherwise 80+ characters of driver detail.
814        assert_eq!(
815            shorten_device_name(
816                "AMD Radeon 780M Graphics (radeonsi, phoenix, ACO, DRM 3.64, 7.1.13-200.fc44.x86_64)"
817            ),
818            "AMD Radeon 780M Graphics"
819        );
820        // A name with no descriptor is returned intact rather than truncated.
821        assert_eq!(
822            shorten_device_name("NVIDIA GeForce RTX 4090"),
823            "NVIDIA GeForce RTX 4090"
824        );
825        // Only " (" splits, so a parenthesis inside a model name survives.
826        assert_eq!(
827            shorten_device_name("Intel(R) Arc(TM) A770"),
828            "Intel(R) Arc(TM) A770"
829        );
830    }
831
832    #[test]
833    fn test_cstr_field_stops_at_nul() {
834        let mut buf = [0u8; 16];
835        buf[..4].copy_from_slice(b"radv");
836        assert_eq!(cstr_field(&buf), "radv");
837        // An unwritten field is empty, not garbage — this is how an ignored pNext presents.
838        assert_eq!(cstr_field(&[0u8; 16]), "");
839        // No NUL at all: use the whole buffer rather than reading past it.
840        assert_eq!(cstr_field(b"abcd"), "abcd");
841    }
842}