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}