#[cfg(any(target_os = "linux", target_os = "macos"))]
use std::ffi::c_int;
#[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))]
use std::ffi::{c_char, c_void, CStr};
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct GpuApis {
pub vulkan: Option<String>,
pub opengl: Option<String>,
pub opencl: Option<String>,
}
pub fn format_vulkan_version(packed: u32) -> String {
let major = (packed >> 22) & 0x7F;
let minor = (packed >> 12) & 0x3FF;
let patch = packed & 0xFFF;
format!("{major}.{minor}.{patch}")
}
pub fn device_type_rank(device_type: u32) -> u8 {
match device_type {
2 => 0, 1 => 1, 3 => 2, 4 => 4, _ => 3, }
}
pub fn format_vulkan(version: &str, driver_name: &str, driver_info: &str) -> String {
match (driver_name.trim(), driver_info.trim()) {
("", _) => version.to_string(),
(name, "") => format!("{version} - {name}"),
(name, info) => format!("{version} - {name} [{info}]"),
}
}
pub fn format_opencl(version: &str, platform: &str, device: Option<&str>) -> String {
let version = version
.trim()
.strip_prefix("OpenCL ")
.unwrap_or(version.trim())
.trim();
let platform = platform.trim();
match device {
Some(d) if !d.trim().is_empty() => {
if platform.is_empty() {
format!("{version} ({})", d.trim())
} else {
format!("{version} - {platform} ({})", d.trim())
}
}
_ => {
if platform.is_empty() {
format!("{version} (no device enabled)")
} else {
format!("{version} - {platform} (no device enabled)")
}
}
}
}
pub fn shorten_device_name(name: &str) -> String {
match name.find(" (") {
Some(i) => name[..i].trim().to_string(),
None => name.trim().to_string(),
}
}
pub fn cstr_field(buf: &[u8]) -> String {
let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
String::from_utf8_lossy(&buf[..end]).into_owned()
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
mod dl {
use super::*;
extern "C" {
pub fn dlopen(filename: *const c_char, flags: c_int) -> *mut c_void;
pub fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void;
pub fn dlclose(handle: *mut c_void) -> c_int;
}
pub const RTLD_NOW: c_int = 2;
pub const RTLD_LOCAL: c_int = 0;
pub fn open(soname: &CStr) -> Option<*mut c_void> {
let h = unsafe { dlopen(soname.as_ptr(), RTLD_NOW | RTLD_LOCAL) };
(!h.is_null()).then_some(h)
}
pub fn sym(handle: *mut c_void, name: &CStr) -> Option<*mut c_void> {
let p = unsafe { dlsym(handle, name.as_ptr()) };
(!p.is_null()).then_some(p)
}
pub fn close(handle: *mut c_void) {
unsafe {
dlclose(handle);
}
}
}
#[cfg(target_os = "windows")]
mod dl {
use super::*;
#[link(name = "kernel32")]
extern "system" {
fn LoadLibraryA(lp_lib_file_name: *const c_char) -> *mut c_void;
fn GetProcAddress(h_module: *mut c_void, lp_proc_name: *const c_char) -> *mut c_void;
fn FreeLibrary(h_module: *mut c_void) -> i32;
}
pub fn open(name: &CStr) -> Option<*mut c_void> {
let h = unsafe { LoadLibraryA(name.as_ptr()) };
(!h.is_null()).then_some(h)
}
pub fn sym(handle: *mut c_void, name: &CStr) -> Option<*mut c_void> {
let p = unsafe { GetProcAddress(handle, name.as_ptr()) };
(!p.is_null()).then_some(p)
}
pub fn close(handle: *mut c_void) {
unsafe {
FreeLibrary(handle);
}
}
}
#[cfg(target_os = "linux")]
const VULKAN_LIB: &CStr = c"libvulkan.so.1";
#[cfg(target_os = "windows")]
const VULKAN_LIB: &CStr = c"vulkan-1.dll";
#[cfg(target_os = "macos")]
const VULKAN_LIB: &CStr = c"libvulkan.1.dylib";
#[cfg(target_os = "linux")]
const OPENCL_LIB: &CStr = c"libOpenCL.so.1";
#[cfg(target_os = "windows")]
const OPENCL_LIB: &CStr = c"OpenCL.dll";
#[cfg(target_os = "macos")]
const OPENCL_LIB: &CStr = c"/System/Library/Frameworks/OpenCL.framework/OpenCL";
#[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))]
mod vulkan {
use super::dl;
use super::*;
const VK_STRUCTURE_TYPE_APPLICATION_INFO: u32 = 0;
const VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO: u32 = 1;
const VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2: u32 = 1000059001;
const VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES: u32 = 1000196000;
const PROPS2_BODY: usize = 16;
const OFF_API_VERSION: usize = 0;
const OFF_DEVICE_TYPE: usize = 16;
const OFF_DEVICE_NAME: usize = 20;
const PROPS_BUF: usize = 1024;
const OFF_DRIVER_NAME: usize = 20;
const OFF_DRIVER_INFO: usize = 276;
const DRIVER_BUF: usize = 560;
const VK_MAX_NAME: usize = 256;
#[repr(C)]
struct AppInfo {
s_type: u32,
p_next: *const c_void,
app_name: *const c_char,
app_version: u32,
engine_name: *const c_char,
engine_version: u32,
api_version: u32,
}
#[repr(C)]
struct InstanceCreateInfo {
s_type: u32,
p_next: *const c_void,
flags: u32,
app_info: *const AppInfo,
layer_count: u32,
layer_names: *const *const c_char,
ext_count: u32,
ext_names: *const *const c_char,
}
type VkCreateInstance =
unsafe extern "C" fn(*const InstanceCreateInfo, *const c_void, *mut *mut c_void) -> i32;
type VkDestroyInstance = unsafe extern "C" fn(*mut c_void, *const c_void);
type VkEnumeratePhysicalDevices =
unsafe extern "C" fn(*mut c_void, *mut u32, *mut *mut c_void) -> i32;
type VkGetPhysicalDeviceProperties2 = unsafe extern "C" fn(*mut c_void, *mut c_void);
type VkGetInstanceProcAddr = unsafe extern "C" fn(*mut c_void, *const c_char) -> *mut c_void;
pub fn detect() -> Option<String> {
let lib = dl::open(VULKAN_LIB)?;
let result = detect_with(lib);
dl::close(lib);
result
}
fn detect_with(lib: *mut c_void) -> Option<String> {
let create = dl::sym(lib, c"vkCreateInstance")?;
let gipa = dl::sym(lib, c"vkGetInstanceProcAddr")?;
unsafe {
let create: VkCreateInstance = std::mem::transmute(create);
let gipa: VkGetInstanceProcAddr = std::mem::transmute(gipa);
let app = AppInfo {
s_type: VK_STRUCTURE_TYPE_APPLICATION_INFO,
p_next: std::ptr::null(),
app_name: c"retch".as_ptr(),
app_version: 0,
engine_name: std::ptr::null(),
engine_version: 0,
api_version: (1 << 22) | (2 << 12),
};
let ci = InstanceCreateInfo {
s_type: VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
p_next: std::ptr::null(),
flags: 0,
app_info: &app,
layer_count: 0,
layer_names: std::ptr::null(),
ext_count: 0,
ext_names: std::ptr::null(),
};
let mut instance: *mut c_void = std::ptr::null_mut();
if create(&ci, std::ptr::null(), &mut instance) != 0 || instance.is_null() {
return None;
}
let out = read_best_device(instance, gipa);
if let Some(p) = dl::sym(lib, c"vkDestroyInstance") {
let destroy: VkDestroyInstance = std::mem::transmute(p);
destroy(instance, std::ptr::null());
}
out
}
}
unsafe fn read_best_device(
instance: *mut c_void,
gipa: VkGetInstanceProcAddr,
) -> Option<String> {
let enum_ptr = gipa(instance, c"vkEnumeratePhysicalDevices".as_ptr());
let props_ptr = gipa(instance, c"vkGetPhysicalDeviceProperties2".as_ptr());
if enum_ptr.is_null() || props_ptr.is_null() {
return None;
}
let enumerate: VkEnumeratePhysicalDevices = std::mem::transmute(enum_ptr);
let get_props2: VkGetPhysicalDeviceProperties2 = std::mem::transmute(props_ptr);
let mut count: u32 = 0;
if enumerate(instance, &mut count, std::ptr::null_mut()) != 0 || count == 0 {
return None;
}
let mut devices = vec![std::ptr::null_mut::<c_void>(); count as usize];
if enumerate(instance, &mut count, devices.as_mut_ptr()) != 0 {
return None;
}
let mut best: Option<(u8, String)> = None;
for device in devices.iter().take(count as usize) {
let mut driver = vec![0u8; DRIVER_BUF];
driver[0..4].copy_from_slice(
&VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES.to_ne_bytes(),
);
let mut props = vec![0u8; PROPS2_BODY + PROPS_BUF];
props[0..4]
.copy_from_slice(&VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2.to_ne_bytes());
let chain = driver.as_mut_ptr() as usize;
props[8..16].copy_from_slice(&chain.to_ne_bytes());
get_props2(*device, props.as_mut_ptr() as *mut c_void);
let at = |off: usize| -> u32 {
let s = PROPS2_BODY + off;
u32::from_ne_bytes(props[s..s + 4].try_into().unwrap_or([0; 4]))
};
let api = at(OFF_API_VERSION);
let dtype = at(OFF_DEVICE_TYPE);
let name_start = PROPS2_BODY + OFF_DEVICE_NAME;
let _device_name = cstr_field(&props[name_start..name_start + VK_MAX_NAME]);
let driver_name = cstr_field(&driver[OFF_DRIVER_NAME..OFF_DRIVER_NAME + VK_MAX_NAME]);
let driver_info = cstr_field(&driver[OFF_DRIVER_INFO..OFF_DRIVER_INFO + VK_MAX_NAME]);
let rank = device_type_rank(dtype);
let rendered = format_vulkan(&format_vulkan_version(api), &driver_name, &driver_info);
if best.as_ref().is_none_or(|(r, _)| rank < *r) {
best = Some((rank, rendered));
}
}
best.map(|(_, s)| s)
}
}
#[cfg(target_os = "linux")]
mod opengl {
use super::dl;
use super::*;
const EGL_OPENGL_API: u32 = 0x30A2;
const EGL_NONE: i32 = 0x3038;
const EGL_SURFACE_TYPE: i32 = 0x3033;
const EGL_PBUFFER_BIT: i32 = 0x0001;
const EGL_RENDERABLE_TYPE: i32 = 0x3040;
const EGL_OPENGL_BIT: i32 = 0x0008;
const GL_VERSION: u32 = 0x1F02;
type EglGetDisplay = unsafe extern "C" fn(*mut c_void) -> *mut c_void;
type EglInitialize = unsafe extern "C" fn(*mut c_void, *mut i32, *mut i32) -> u32;
type EglBindApi = unsafe extern "C" fn(u32) -> u32;
type EglChooseConfig =
unsafe extern "C" fn(*mut c_void, *const i32, *mut *mut c_void, i32, *mut i32) -> u32;
type EglCreateContext =
unsafe extern "C" fn(*mut c_void, *mut c_void, *mut c_void, *const i32) -> *mut c_void;
type EglMakeCurrent =
unsafe extern "C" fn(*mut c_void, *mut c_void, *mut c_void, *mut c_void) -> u32;
type EglGetProcAddress = unsafe extern "C" fn(*const c_char) -> *mut c_void;
type EglTerminate = unsafe extern "C" fn(*mut c_void) -> u32;
type GlGetString = unsafe extern "C" fn(u32) -> *const c_char;
pub fn detect() -> Option<String> {
let lib = dl::open(c"libEGL.so.1")?;
let out = detect_with(lib);
dl::close(lib);
out
}
fn detect_with(lib: *mut c_void) -> Option<String> {
let get_display = dl::sym(lib, c"eglGetDisplay")?;
let initialize = dl::sym(lib, c"eglInitialize")?;
let bind_api = dl::sym(lib, c"eglBindAPI")?;
let choose = dl::sym(lib, c"eglChooseConfig")?;
let create_context = dl::sym(lib, c"eglCreateContext")?;
let make_current = dl::sym(lib, c"eglMakeCurrent")?;
let get_proc = dl::sym(lib, c"eglGetProcAddress")?;
unsafe {
let get_display: EglGetDisplay = std::mem::transmute(get_display);
let initialize: EglInitialize = std::mem::transmute(initialize);
let bind_api: EglBindApi = std::mem::transmute(bind_api);
let choose: EglChooseConfig = std::mem::transmute(choose);
let create_context: EglCreateContext = std::mem::transmute(create_context);
let make_current: EglMakeCurrent = std::mem::transmute(make_current);
let get_proc: EglGetProcAddress = std::mem::transmute(get_proc);
let display = get_display(std::ptr::null_mut());
if display.is_null() {
return None;
}
let (mut major, mut minor) = (0i32, 0i32);
if initialize(display, &mut major, &mut minor) == 0 {
return None;
}
if bind_api(EGL_OPENGL_API) == 0 {
terminate(lib, display);
return None;
}
let attrs = [
EGL_SURFACE_TYPE,
EGL_PBUFFER_BIT,
EGL_RENDERABLE_TYPE,
EGL_OPENGL_BIT,
EGL_NONE,
];
let mut config: *mut c_void = std::ptr::null_mut();
let mut configs = 0i32;
if choose(display, attrs.as_ptr(), &mut config, 1, &mut configs) == 0 || configs == 0 {
terminate(lib, display);
return None;
}
let context = create_context(display, config, std::ptr::null_mut(), std::ptr::null());
if context.is_null() {
terminate(lib, display);
return None;
}
if make_current(display, std::ptr::null_mut(), std::ptr::null_mut(), context) == 0 {
terminate(lib, display);
return None;
}
let gl_get_string = get_proc(c"glGetString".as_ptr());
let version = if gl_get_string.is_null() {
None
} else {
let gl_get_string: GlGetString = std::mem::transmute(gl_get_string);
let p = gl_get_string(GL_VERSION);
if p.is_null() {
None
} else {
Some(CStr::from_ptr(p).to_string_lossy().into_owned())
}
};
terminate(lib, display);
version.filter(|v| !v.trim().is_empty())
}
}
unsafe fn terminate(lib: *mut c_void, display: *mut c_void) {
if let Some(p) = dl::sym(lib, c"eglTerminate") {
let terminate: EglTerminate = std::mem::transmute(p);
terminate(display);
}
}
}
#[cfg(target_os = "windows")]
mod opengl {
use super::dl;
use super::*;
const GL_VERSION: u32 = 0x1F02;
const PFD_DOUBLEBUFFER: u32 = 0x0000_0001;
const PFD_DRAW_TO_WINDOW: u32 = 0x0000_0004;
const PFD_SUPPORT_OPENGL: u32 = 0x0000_0020;
const PFD_TYPE_RGBA: u8 = 0;
const PFD_MAIN_PLANE: u8 = 0;
const WS_OVERLAPPED: u32 = 0x0000_0000;
#[repr(C)]
#[derive(Default)]
struct PixelFormatDescriptor {
n_size: u16,
n_version: u16,
dw_flags: u32,
i_pixel_type: u8,
c_color_bits: u8,
c_red_bits: u8,
c_red_shift: u8,
c_green_bits: u8,
c_green_shift: u8,
c_blue_bits: u8,
c_blue_shift: u8,
c_alpha_bits: u8,
c_alpha_shift: u8,
c_accum_bits: u8,
c_accum_red_bits: u8,
c_accum_green_bits: u8,
c_accum_blue_bits: u8,
c_accum_alpha_bits: u8,
c_depth_bits: u8,
c_stencil_bits: u8,
c_aux_buffers: u8,
i_layer_type: u8,
b_reserved: u8,
dw_layer_mask: u32,
dw_visible_mask: u32,
dw_damage_mask: u32,
}
#[repr(C)]
struct WndClassW {
style: u32,
lpfn_wnd_proc: *const c_void,
cb_cls_extra: i32,
cb_wnd_extra: i32,
h_instance: *mut c_void,
h_icon: *mut c_void,
h_cursor: *mut c_void,
hbr_background: *mut c_void,
lpsz_menu_name: *const u16,
lpsz_class_name: *const u16,
}
#[link(name = "user32")]
extern "system" {
fn RegisterClassW(lp_wnd_class: *const WndClassW) -> u16;
fn UnregisterClassW(lp_class_name: *const u16, h_instance: *mut c_void) -> i32;
fn CreateWindowExW(
dw_ex_style: u32,
lp_class_name: *const u16,
lp_window_name: *const u16,
dw_style: u32,
x: i32,
y: i32,
n_width: i32,
n_height: i32,
h_wnd_parent: *mut c_void,
h_menu: *mut c_void,
h_instance: *mut c_void,
lp_param: *mut c_void,
) -> *mut c_void;
fn DestroyWindow(h_wnd: *mut c_void) -> i32;
fn GetDC(h_wnd: *mut c_void) -> *mut c_void;
fn ReleaseDC(h_wnd: *mut c_void, h_dc: *mut c_void) -> i32;
fn DefWindowProcW(h_wnd: *mut c_void, msg: u32, w_param: usize, l_param: isize) -> isize;
}
#[link(name = "gdi32")]
extern "system" {
fn ChoosePixelFormat(h_dc: *mut c_void, ppfd: *const PixelFormatDescriptor) -> i32;
fn SetPixelFormat(
h_dc: *mut c_void,
format: i32,
ppfd: *const PixelFormatDescriptor,
) -> i32;
}
type WglCreateContext = unsafe extern "system" fn(*mut c_void) -> *mut c_void;
type WglMakeCurrent = unsafe extern "system" fn(*mut c_void, *mut c_void) -> i32;
type WglDeleteContext = unsafe extern "system" fn(*mut c_void) -> i32;
type GlGetString = unsafe extern "system" fn(u32) -> *const c_char;
struct HiddenWindow {
class_name: Vec<u16>,
hwnd: *mut c_void,
hdc: *mut c_void,
}
impl HiddenWindow {
fn new() -> Option<Self> {
let class_name: Vec<u16> = "retch_gl_probe\0".encode_utf16().collect();
let wc = WndClassW {
style: 0,
lpfn_wnd_proc: DefWindowProcW as *const c_void,
cb_cls_extra: 0,
cb_wnd_extra: 0,
h_instance: std::ptr::null_mut(),
h_icon: std::ptr::null_mut(),
h_cursor: std::ptr::null_mut(),
hbr_background: std::ptr::null_mut(),
lpsz_menu_name: std::ptr::null(),
lpsz_class_name: class_name.as_ptr(),
};
unsafe {
if RegisterClassW(&wc) == 0 {
return None;
}
let hwnd = CreateWindowExW(
0,
class_name.as_ptr(),
std::ptr::null(),
WS_OVERLAPPED,
0,
0,
1,
1,
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
);
if hwnd.is_null() {
UnregisterClassW(class_name.as_ptr(), std::ptr::null_mut());
return None;
}
let hdc = GetDC(hwnd);
if hdc.is_null() {
DestroyWindow(hwnd);
UnregisterClassW(class_name.as_ptr(), std::ptr::null_mut());
return None;
}
Some(Self {
class_name,
hwnd,
hdc,
})
}
}
}
impl Drop for HiddenWindow {
fn drop(&mut self) {
unsafe {
ReleaseDC(self.hwnd, self.hdc);
DestroyWindow(self.hwnd);
UnregisterClassW(self.class_name.as_ptr(), std::ptr::null_mut());
}
}
}
pub fn detect() -> Option<String> {
let lib = dl::open(c"opengl32.dll")?;
let out = detect_with(lib);
dl::close(lib);
out
}
fn detect_with(lib: *mut c_void) -> Option<String> {
let create_ctx = dl::sym(lib, c"wglCreateContext")?;
let make_current = dl::sym(lib, c"wglMakeCurrent")?;
let delete_ctx = dl::sym(lib, c"wglDeleteContext")?;
let get_string = dl::sym(lib, c"glGetString")?;
let window = HiddenWindow::new()?;
unsafe {
let create_ctx: WglCreateContext = std::mem::transmute(create_ctx);
let make_current: WglMakeCurrent = std::mem::transmute(make_current);
let delete_ctx: WglDeleteContext = std::mem::transmute(delete_ctx);
let get_string: GlGetString = std::mem::transmute(get_string);
let pfd = PixelFormatDescriptor {
n_size: std::mem::size_of::<PixelFormatDescriptor>() as u16,
n_version: 1,
dw_flags: PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER,
i_pixel_type: PFD_TYPE_RGBA,
c_color_bits: 32,
c_depth_bits: 24,
c_stencil_bits: 8,
i_layer_type: PFD_MAIN_PLANE,
..Default::default()
};
let format = ChoosePixelFormat(window.hdc, &pfd);
if format == 0 || SetPixelFormat(window.hdc, format, &pfd) == 0 {
return None;
}
let ctx = create_ctx(window.hdc);
if ctx.is_null() {
return None;
}
let version = if make_current(window.hdc, ctx) != 0 {
let p = get_string(GL_VERSION);
let s = (!p.is_null()).then(|| CStr::from_ptr(p).to_string_lossy().into_owned());
make_current(std::ptr::null_mut(), std::ptr::null_mut());
s
} else {
None
};
delete_ctx(ctx);
version
.map(|v| v.trim().to_string())
.filter(|v| !v.is_empty())
}
}
#[cfg(test)]
mod layout {
use std::mem::{offset_of, size_of};
#[test]
fn ffi_struct_layout() {
assert_eq!(size_of::<super::PixelFormatDescriptor>(), 40);
assert_eq!(offset_of!(super::PixelFormatDescriptor, dw_flags), 4);
assert_eq!(offset_of!(super::PixelFormatDescriptor, i_pixel_type), 8);
assert_eq!(offset_of!(super::PixelFormatDescriptor, c_color_bits), 9);
assert_eq!(offset_of!(super::PixelFormatDescriptor, c_depth_bits), 23);
assert_eq!(offset_of!(super::PixelFormatDescriptor, i_layer_type), 26);
assert_eq!(size_of::<super::WndClassW>(), 72);
assert_eq!(offset_of!(super::WndClassW, lpfn_wnd_proc), 8);
assert_eq!(offset_of!(super::WndClassW, h_instance), 24);
assert_eq!(offset_of!(super::WndClassW, lpsz_class_name), 64);
}
}
}
#[cfg(target_os = "macos")]
mod opengl {
use super::dl;
use super::*;
const OPENGL_FRAMEWORK: &CStr = c"/System/Library/Frameworks/OpenGL.framework/OpenGL";
pub(super) const KCGLPFA_ACCELERATED: u32 = 73;
pub(super) const KCGLPFA_OPENGL_PROFILE: u32 = 99;
pub(super) const KCGL_OGLP_VERSION_GL4_CORE: u32 = 0x4100;
const GL_VERSION: u32 = 0x1F02;
type CGLChoosePixelFormat = unsafe extern "C" fn(*const u32, *mut *mut c_void, *mut i32) -> i32;
type CGLCreateContext = unsafe extern "C" fn(*mut c_void, *mut c_void, *mut *mut c_void) -> i32;
type CGLSetCurrentContext = unsafe extern "C" fn(*mut c_void) -> i32;
type CGLDestroyContext = unsafe extern "C" fn(*mut c_void) -> i32;
type CGLDestroyPixelFormat = unsafe extern "C" fn(*mut c_void) -> i32;
type GlGetString = unsafe extern "C" fn(u32) -> *const c_char;
struct CglGuard {
set_current: CGLSetCurrentContext,
destroy_context: CGLDestroyContext,
destroy_pixel_format: CGLDestroyPixelFormat,
context: *mut c_void,
pixel_format: *mut c_void,
}
impl Drop for CglGuard {
fn drop(&mut self) {
unsafe {
if !self.context.is_null() {
(self.set_current)(std::ptr::null_mut());
(self.destroy_context)(self.context);
}
if !self.pixel_format.is_null() {
(self.destroy_pixel_format)(self.pixel_format);
}
}
}
}
pub fn detect() -> Option<String> {
let lib = dl::open(OPENGL_FRAMEWORK)?;
let result = probe(lib);
dl::close(lib);
result
}
fn probe(lib: *mut c_void) -> Option<String> {
unsafe {
let choose: CGLChoosePixelFormat =
std::mem::transmute(dl::sym(lib, c"CGLChoosePixelFormat")?);
let create: CGLCreateContext = std::mem::transmute(dl::sym(lib, c"CGLCreateContext")?);
let set_current: CGLSetCurrentContext =
std::mem::transmute(dl::sym(lib, c"CGLSetCurrentContext")?);
let destroy_context: CGLDestroyContext =
std::mem::transmute(dl::sym(lib, c"CGLDestroyContext")?);
let destroy_pixel_format: CGLDestroyPixelFormat =
std::mem::transmute(dl::sym(lib, c"CGLDestroyPixelFormat")?);
let gl_get_string: GlGetString = std::mem::transmute(dl::sym(lib, c"glGetString")?);
let attrs: [u32; 4] = [
KCGLPFA_ACCELERATED,
KCGLPFA_OPENGL_PROFILE,
KCGL_OGLP_VERSION_GL4_CORE,
0,
];
let mut pixel_format: *mut c_void = std::ptr::null_mut();
let mut count: i32 = 0;
if choose(attrs.as_ptr(), &mut pixel_format, &mut count) != 0
|| pixel_format.is_null()
|| count == 0
{
return None;
}
let mut context: *mut c_void = std::ptr::null_mut();
let err = create(pixel_format, std::ptr::null_mut(), &mut context);
let guard = CglGuard {
set_current,
destroy_context,
destroy_pixel_format,
context: if err == 0 {
context
} else {
std::ptr::null_mut()
},
pixel_format,
};
if err != 0 || context.is_null() {
return None;
}
if set_current(context) != 0 {
return None;
}
let raw = gl_get_string(GL_VERSION);
let version = (!raw.is_null())
.then(|| CStr::from_ptr(raw).to_string_lossy().trim().to_string())
.filter(|s| !s.is_empty());
drop(guard);
version
}
}
}
#[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))]
mod opencl {
use super::dl;
use super::*;
const CL_PLATFORM_VERSION: u32 = 0x0901;
const CL_PLATFORM_NAME: u32 = 0x0902;
const CL_DEVICE_TYPE_ALL: u64 = 0xFFFF_FFFF;
const CL_DEVICE_NAME: u32 = 0x102B;
type ClGetPlatformIDs = unsafe extern "C" fn(u32, *mut *mut c_void, *mut u32) -> i32;
type ClGetPlatformInfo =
unsafe extern "C" fn(*mut c_void, u32, usize, *mut c_void, *mut usize) -> i32;
type ClGetDeviceIDs =
unsafe extern "C" fn(*mut c_void, u64, u32, *mut *mut c_void, *mut u32) -> i32;
type ClGetDeviceInfo =
unsafe extern "C" fn(*mut c_void, u32, usize, *mut c_void, *mut usize) -> i32;
#[cfg(target_os = "linux")]
extern "C" {
fn dup(oldfd: c_int) -> c_int;
fn dup2(oldfd: c_int, newfd: c_int) -> c_int;
fn close(fd: c_int) -> c_int;
fn open(path: *const c_char, flags: c_int) -> c_int;
}
#[cfg(target_os = "linux")]
const STDERR_FILENO: c_int = 2;
#[cfg(target_os = "linux")]
const O_WRONLY: c_int = 1;
#[cfg(target_os = "linux")]
struct SuppressStderr {
saved: c_int,
}
#[cfg(target_os = "linux")]
impl SuppressStderr {
fn new() -> Option<Self> {
unsafe {
let saved = dup(STDERR_FILENO);
if saved < 0 {
return None;
}
let devnull = open(c"/dev/null".as_ptr(), O_WRONLY);
if devnull < 0 {
close(saved);
return None;
}
dup2(devnull, STDERR_FILENO);
close(devnull);
Some(Self { saved })
}
}
}
#[cfg(target_os = "linux")]
impl Drop for SuppressStderr {
fn drop(&mut self) {
unsafe {
dup2(self.saved, STDERR_FILENO);
close(self.saved);
}
}
}
#[cfg(any(target_os = "windows", target_os = "macos"))]
struct SuppressStderr;
#[cfg(any(target_os = "windows", target_os = "macos"))]
impl SuppressStderr {
fn new() -> Option<Self> {
None
}
}
pub fn detect() -> Option<String> {
let _quiet = SuppressStderr::new();
let lib = dl::open(OPENCL_LIB)?;
let out = detect_with(lib);
dl::close(lib);
out
}
fn detect_with(lib: *mut c_void) -> Option<String> {
let get_platform_ids = dl::sym(lib, c"clGetPlatformIDs")?;
let get_platform_info = dl::sym(lib, c"clGetPlatformInfo")?;
unsafe {
let get_platform_ids: ClGetPlatformIDs = std::mem::transmute(get_platform_ids);
let get_platform_info: ClGetPlatformInfo = std::mem::transmute(get_platform_info);
let mut count: u32 = 0;
if get_platform_ids(0, std::ptr::null_mut(), &mut count) != 0 || count == 0 {
return None;
}
let mut platforms = vec![std::ptr::null_mut::<c_void>(); count as usize];
if get_platform_ids(count, platforms.as_mut_ptr(), std::ptr::null_mut()) != 0 {
return None;
}
let platform = *platforms.first()?;
let version = query(get_platform_info, platform, CL_PLATFORM_VERSION)?;
let name = query(get_platform_info, platform, CL_PLATFORM_NAME).unwrap_or_default();
let device = dl::sym(lib, c"clGetDeviceIDs")
.zip(dl::sym(lib, c"clGetDeviceInfo"))
.and_then(|(ids, info)| first_device_name(platform, ids, info))
.map(|n| shorten_device_name(&n));
Some(format_opencl(&version, &name, device.as_deref()))
}
}
unsafe fn query(f: ClGetPlatformInfo, obj: *mut c_void, param: u32) -> Option<String> {
let mut size: usize = 0;
if f(obj, param, 0, std::ptr::null_mut(), &mut size) != 0 || size == 0 {
return None;
}
let mut buf = vec![0u8; size];
if f(
obj,
param,
size,
buf.as_mut_ptr() as *mut c_void,
std::ptr::null_mut(),
) != 0
{
return None;
}
let s = cstr_field(&buf);
(!s.trim().is_empty()).then(|| s.trim().to_string())
}
unsafe fn first_device_name(
platform: *mut c_void,
ids: *mut c_void,
info: *mut c_void,
) -> Option<String> {
let get_device_ids: ClGetDeviceIDs = std::mem::transmute(ids);
let get_device_info: ClGetDeviceInfo = std::mem::transmute(info);
let mut count: u32 = 0;
if get_device_ids(
platform,
CL_DEVICE_TYPE_ALL,
0,
std::ptr::null_mut(),
&mut count,
) != 0
|| count == 0
{
return None;
}
let mut devices = vec![std::ptr::null_mut::<c_void>(); count as usize];
if get_device_ids(
platform,
CL_DEVICE_TYPE_ALL,
count,
devices.as_mut_ptr(),
std::ptr::null_mut(),
) != 0
{
return None;
}
let device = *devices.first()?;
let mut size: usize = 0;
if get_device_info(device, CL_DEVICE_NAME, 0, std::ptr::null_mut(), &mut size) != 0
|| size == 0
{
return None;
}
let mut buf = vec![0u8; size];
if get_device_info(
device,
CL_DEVICE_NAME,
size,
buf.as_mut_ptr() as *mut c_void,
std::ptr::null_mut(),
) != 0
{
return None;
}
let s = cstr_field(&buf);
(!s.trim().is_empty()).then(|| s.trim().to_string())
}
}
#[cfg(target_os = "linux")]
pub fn detect_gpu_apis() -> GpuApis {
GpuApis {
vulkan: vulkan::detect(),
opengl: opengl::detect(),
opencl: opencl::detect(),
}
}
#[cfg(target_os = "windows")]
pub fn detect_gpu_apis() -> GpuApis {
GpuApis {
vulkan: vulkan::detect(),
opengl: opengl::detect(),
opencl: opencl::detect(),
}
}
#[cfg(target_os = "macos")]
pub fn detect_gpu_apis() -> GpuApis {
GpuApis {
vulkan: vulkan::detect(),
opengl: opengl::detect(),
opencl: opencl::detect(),
}
}
#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
pub fn detect_gpu_apis() -> GpuApis {
GpuApis::default()
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(target_os = "windows")]
#[test]
fn test_windows_loader_names_are_the_khronos_loaders() {
assert_eq!(VULKAN_LIB.to_str().unwrap(), "vulkan-1.dll");
assert_eq!(OPENCL_LIB.to_str().unwrap(), "OpenCL.dll");
}
#[cfg(target_os = "linux")]
#[test]
fn test_linux_loader_sonames() {
assert_eq!(VULKAN_LIB.to_str().unwrap(), "libvulkan.so.1");
assert_eq!(OPENCL_LIB.to_str().unwrap(), "libOpenCL.so.1");
}
#[test]
fn test_format_opencl_handles_a_windows_vendor_platform_string() {
assert_eq!(
format_opencl(
"OpenCL 2.1 AMD-APP (3661.0)",
"AMD Accelerated Parallel Processing",
Some("gfx1151"),
),
"2.1 AMD-APP (3661.0) - AMD Accelerated Parallel Processing (gfx1151)"
);
}
#[test]
fn test_shorten_device_name_leaves_a_bare_name_alone() {
assert_eq!(shorten_device_name("gfx1151"), "gfx1151");
assert_eq!(shorten_device_name(" gfx1151 "), "gfx1151");
}
#[test]
fn test_format_vulkan_version_decodes_packed_fields() {
assert_eq!(format_vulkan_version(0x0040_4155), "1.4.341");
assert_eq!(format_vulkan_version(1 << 22), "1.0.0");
assert_eq!(format_vulkan_version((1 << 22) | (2 << 12)), "1.2.0");
assert_eq!(
format_vulkan_version((1 << 22) | (3 << 12) | 290),
"1.3.290"
);
}
#[test]
fn test_format_vulkan_version_ignores_variant_bits() {
let with_variant = (1u32 << 29) | (1 << 22) | (4 << 12) | 354;
assert_eq!(format_vulkan_version(with_variant), "1.4.354");
}
#[test]
fn test_device_type_rank_prefers_real_gpu_over_software() {
assert!(device_type_rank(1) < device_type_rank(4));
assert!(device_type_rank(2) < device_type_rank(1)); assert!(device_type_rank(3) < device_type_rank(4)); assert!(device_type_rank(0) < device_type_rank(4)); }
#[test]
fn test_format_vulkan_handles_unfilled_driver_chain() {
assert_eq!(format_vulkan("1.4.354", "", ""), "1.4.354");
assert_eq!(format_vulkan("1.4.354", "radv", ""), "1.4.354 - radv");
assert_eq!(
format_vulkan("1.4.354", "radv", "Mesa 26.1.8"),
"1.4.354 - radv [Mesa 26.1.8]"
);
}
#[test]
fn test_format_opencl_distinguishes_inert_platform_from_working_one() {
assert_eq!(
format_opencl("OpenCL 3.0", "rusticl", None),
"3.0 - rusticl (no device enabled)"
);
assert_eq!(
format_opencl("OpenCL 3.0", "rusticl", Some("AMD Radeon 780M Graphics")),
"3.0 - rusticl (AMD Radeon 780M Graphics)"
);
assert_eq!(
format_opencl("OpenCL 3.0", "rusticl", Some(" ")),
"3.0 - rusticl (no device enabled)"
);
}
#[test]
fn test_format_opencl_without_platform_name() {
assert_eq!(
format_opencl("OpenCL 1.2", "", None),
"1.2 (no device enabled)"
);
assert_eq!(format_opencl("OpenCL 1.2", "", Some("GPU")), "1.2 (GPU)");
assert_eq!(format_opencl("3.0", "x", Some("GPU")), "3.0 - x (GPU)");
}
#[test]
fn test_shorten_device_name_drops_the_driver_descriptor() {
assert_eq!(
shorten_device_name(
"AMD Radeon 780M Graphics (radeonsi, phoenix, ACO, DRM 3.64, 7.1.13-200.fc44.x86_64)"
),
"AMD Radeon 780M Graphics"
);
assert_eq!(
shorten_device_name("NVIDIA GeForce RTX 4090"),
"NVIDIA GeForce RTX 4090"
);
assert_eq!(
shorten_device_name("Intel(R) Arc(TM) A770"),
"Intel(R) Arc(TM) A770"
);
}
#[test]
fn test_cstr_field_stops_at_nul() {
let mut buf = [0u8; 16];
buf[..4].copy_from_slice(b"radv");
assert_eq!(cstr_field(&buf), "radv");
assert_eq!(cstr_field(&[0u8; 16]), "");
assert_eq!(cstr_field(b"abcd"), "abcd");
}
#[cfg(target_os = "macos")]
#[test]
fn test_macos_loader_names() {
assert_eq!(VULKAN_LIB.to_str().unwrap(), "libvulkan.1.dylib");
assert_eq!(
OPENCL_LIB.to_str().unwrap(),
"/System/Library/Frameworks/OpenCL.framework/OpenCL"
);
assert!(OPENCL_LIB.to_str().unwrap().starts_with('/'));
}
#[cfg(target_os = "macos")]
#[test]
fn test_macos_cgl_requests_a_core_profile() {
assert_eq!(opengl::KCGL_OGLP_VERSION_GL4_CORE, 0x4100);
assert_ne!(opengl::KCGL_OGLP_VERSION_GL4_CORE, 0x1000);
assert_eq!(opengl::KCGLPFA_OPENGL_PROFILE, 99);
assert_eq!(opengl::KCGLPFA_ACCELERATED, 73);
}
}