use ash::vk;
use std::ffi::CStr;
#[derive(Debug, thiserror::Error)]
pub enum VulkanError {
#[error(
"no Vulkan loader found (tried {tried}); set FERROX_VULKAN_LOADER to a libvulkan path"
)]
NoLoader { tried: String },
#[error("vkCreateInstance failed: {0:?}")]
InstanceCreation(vk::Result),
#[error("the Vulkan loader reported no physical devices")]
NoPhysicalDevice,
#[error("no physical device exposes a queue family with COMPUTE")]
NoComputeQueue,
#[error("vkCreateDevice failed: {0:?}")]
DeviceCreation(vk::Result),
#[error("no memory type is both HOST_VISIBLE and HOST_COHERENT")]
NoHostVisibleMemory,
#[error("{what} failed: {code:?}")]
Vk {
what: &'static str,
code: vk::Result,
},
}
impl VulkanError {
pub(crate) fn vk(what: &'static str) -> impl Fn(vk::Result) -> VulkanError {
move |code| VulkanError::Vk { what, code }
}
}
const LOADER_CANDIDATES: &[&str] = &[
"/opt/homebrew/lib/libvulkan.dylib",
"/usr/local/lib/libvulkan.dylib",
"libvulkan.so.1",
];
pub fn load_entry() -> Result<ash::Entry, VulkanError> {
let mut tried = Vec::new();
if let Ok(path) = std::env::var("FERROX_VULKAN_LOADER") {
match unsafe { ash::Entry::load_from(&path) } {
Ok(e) => return Ok(e),
Err(_) => tried.push(path),
}
}
if let Ok(e) = unsafe { ash::Entry::load() } {
return Ok(e);
}
tried.push("<platform default>".to_string());
for candidate in LOADER_CANDIDATES {
match unsafe { ash::Entry::load_from(candidate) } {
Ok(e) => return Ok(e),
Err(_) => tried.push((*candidate).to_string()),
}
}
Err(VulkanError::NoLoader {
tried: tried.join(", "),
})
}
pub struct Context {
pub entry: ash::Entry,
pub instance: ash::Instance,
pub physical: vk::PhysicalDevice,
pub device: ash::Device,
pub queue: vk::Queue,
pub queue_family: u32,
pub device_name: String,
pub api_version: u32,
}
impl Context {
pub fn new() -> Result<Self, VulkanError> {
let entry = load_entry()?;
let instance = create_instance(&entry)?;
let devices = unsafe { instance.enumerate_physical_devices() }
.map_err(VulkanError::vk("vkEnumeratePhysicalDevices"))?;
if devices.is_empty() {
unsafe { instance.destroy_instance(None) };
return Err(VulkanError::NoPhysicalDevice);
}
let chosen = devices.iter().find_map(|&pd| {
let families = unsafe { instance.get_physical_device_queue_family_properties(pd) };
families
.iter()
.position(|f| f.queue_flags.contains(vk::QueueFlags::COMPUTE))
.map(|i| (pd, i as u32))
});
let Some((physical, queue_family)) = chosen else {
unsafe { instance.destroy_instance(None) };
return Err(VulkanError::NoComputeQueue);
};
let props = unsafe { instance.get_physical_device_properties(physical) };
let device_name = cstr_name(&props.device_name);
let api_version = props.api_version;
let device = match create_device(&instance, physical, queue_family) {
Ok(d) => d,
Err(e) => {
unsafe { instance.destroy_instance(None) };
return Err(e);
}
};
let queue = unsafe { device.get_device_queue(queue_family, 0) };
Ok(Self {
entry,
instance,
physical,
device,
queue,
queue_family,
device_name,
api_version,
})
}
}
impl Drop for Context {
fn drop(&mut self) {
unsafe {
let _ = self.device.device_wait_idle();
self.device.destroy_device(None);
self.instance.destroy_instance(None);
}
}
}
fn create_instance(entry: &ash::Entry) -> Result<ash::Instance, VulkanError> {
let available = unsafe { entry.enumerate_instance_extension_properties(None) }
.map_err(VulkanError::vk("vkEnumerateInstanceExtensionProperties"))?;
let has = |name: &CStr| {
available
.iter()
.any(|e| cstr_name(&e.extension_name) == name.to_string_lossy())
};
let mut extensions = Vec::new();
let mut flags = vk::InstanceCreateFlags::empty();
if has(ash::khr::portability_enumeration::NAME) {
extensions.push(ash::khr::portability_enumeration::NAME.as_ptr());
flags |= vk::InstanceCreateFlags::ENUMERATE_PORTABILITY_KHR;
if has(ash::khr::get_physical_device_properties2::NAME) {
extensions.push(ash::khr::get_physical_device_properties2::NAME.as_ptr());
}
}
let app_name = c"ferrox-vulkan-beachhead";
let app_info = vk::ApplicationInfo::default()
.application_name(app_name)
.api_version(vk::make_api_version(0, 1, 0, 0));
let create_info = vk::InstanceCreateInfo::default()
.application_info(&app_info)
.flags(flags)
.enabled_extension_names(&extensions);
unsafe { entry.create_instance(&create_info, None) }.map_err(VulkanError::InstanceCreation)
}
fn create_device(
instance: &ash::Instance,
physical: vk::PhysicalDevice,
queue_family: u32,
) -> Result<ash::Device, VulkanError> {
let available = unsafe { instance.enumerate_device_extension_properties(physical) }
.map_err(VulkanError::vk("vkEnumerateDeviceExtensionProperties"))?;
let mut extensions = Vec::new();
if available.iter().any(|e| {
cstr_name(&e.extension_name) == ash::khr::portability_subset::NAME.to_string_lossy()
}) {
extensions.push(ash::khr::portability_subset::NAME.as_ptr());
}
let priorities = [1.0f32];
let queue_info = [vk::DeviceQueueCreateInfo::default()
.queue_family_index(queue_family)
.queue_priorities(&priorities)];
let create_info = vk::DeviceCreateInfo::default()
.queue_create_infos(&queue_info)
.enabled_extension_names(&extensions);
unsafe { instance.create_device(physical, &create_info, None) }
.map_err(VulkanError::DeviceCreation)
}
fn cstr_name(raw: &[i8]) -> String {
let bytes: Vec<u8> = raw
.iter()
.take_while(|c| **c != 0)
.map(|c| *c as u8)
.collect();
String::from_utf8_lossy(&bytes).into_owned()
}
pub fn probe() -> Result<String, VulkanError> {
let ctx = Context::new()?;
Ok(format!(
"{} (Vulkan {}.{}.{})",
ctx.device_name,
vk::api_version_major(ctx.api_version),
vk::api_version_minor(ctx.api_version),
vk::api_version_patch(ctx.api_version),
))
}