Programming, graphics, and more
May 4, 2019
Vulkan is a C API so we’ll need some kind of bindings to be able to use it in Rust. The API is defined in a XML file distributed by Khronos. This file describes the structs, enums, constants, and functions for each version of the API, and all published extensions. The functions can then be loaded from the Vulkan dynamic library and other functions from the API.
However using a raw C API isn’t easy in Rust because it requires using a lot of unsafe code. This is why we’ll also generate builders for all structs so we can for instance fill in pointer/size pairs using slices, but we’ll also generate methods that return Rust’s Result
s and take in Rust-friendly types like references instead of raw C types. Finally we’ll also generate loaders so we don’t have to manually load the function we need.
The XML spec for Vulkan is notoriously annoying to use outside of the C/C++ ecosystem because it’s basically C code with tags around it. For instance, good luck parsing this:
<member>const <type>void</type>* <name>pNext</name></member>
For my purpose, I wanted to transform this awkward representation into some kind of AST. For example, given the code above, this is what the parser outputs:
<member name="pNext" optional="False">
<pointer const="False">
<type const="True">void</type>
</pointer>
</member>
This is much easier to use for generating code in any other language than C.
On a higher level, the parser is also able to resolve type dependencies. The goal is to tell it what version of the API and what extensions to use. With that, the parser visits all necessary types and commands, possibly extends some enums, and this is what is then written to the output file.
With that in mind, I won’t go into too much details into how the parser works. Just know that it’s written in Python and that it was both incredibly boring and annoying to do, I never want to do it again! I’ll now show a few examples of the generated XML file.
The source code for the parser and generators is available on GitHub.
<alias name="VkInstanceCreateFlags" type="VkFlags"/>
<base name="VkDeviceSize" type="uint64_t"/>
<handle name="VkBufferView" type="uint64_t"/>
The alias
tag is a Vulkan-to-Vulkan type alias while base
is a Vulkan-to-native alias. In this instance VkInstanceCreateFlags
is an alias for VkFlags
because it is an empty enum.
<command name="vkGetPhysicalDeviceSurfacePresentModesKHR" success-codes="VK_SUCCESS,VK_INCOMPLETE" type="instance">
<return-type>
<type const="False">VkResult</type>
</return-type>
<arg name="physicalDevice" optional="False">
<type const="False">VkPhysicalDevice</type>
</arg>
<arg name="surface" optional="False">
<type const="False">VkSurfaceKHR</type>
</arg>
<arg name="pPresentModeCount" optional="True">
<pointer const="False">
<type const="False">uint32_t</type>
</pointer>
</arg>
<arg length="pPresentModeCount" name="pPresentModes" optional="True">
<pointer const="False">
<type const="False">VkPresentModeKHR</type>
</pointer>
</arg>
</command>
command
define their name of course, but also their success codes for functions that return a VkResult
(this will be used for error checking) and their type, which defines how the associated function pointer must be loaded.
The command has one return-type
and many arg
s with a type AST inside.
Each argument defines whether it is optional or not (this is useful for pointers) and pointer arguments can indicate what argument defines their length.
length
is pPresentModeCount
, it means that the pPresentModeCount
argument defines the length of the array pointed to by the pointer.pCreateInfo::somethingCount
, it means that the lenth of the array is the somethingCount
field from the struct in the pCreateInfo
argument.These info are useful to generate functions that take in slices instead of pointer/length pairs, which is much more idiomatic in Rust. Also, whether the pointer type is const or not indicates whether it is an input argument or not.
<enum name="VkPresentModeKHR" type="enum">
<entry name="VK_PRESENT_MODE_IMMEDIATE_KHR">0</entry>
<entry name="VK_PRESENT_MODE_MAILBOX_KHR">1</entry>
<entry name="VK_PRESENT_MODE_FIFO_KHR">2</entry>
<entry name="VK_PRESENT_MODE_FIFO_RELAXED_KHR">3</entry>
</enum>
Not much to say here, everything is pretty obvious. :)
<bitmask flags="VkShaderStageFlagBits" name="VkShaderStageFlags"/>
<enum name="VkShaderStageFlagBits" type="bitmask">
<entry name="VK_SHADER_STAGE_VERTEX_BIT">1</entry>
<entry name="VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT">2</entry>
<entry name="VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT">4</entry>
<entry name="VK_SHADER_STAGE_GEOMETRY_BIT">8</entry>
<entry name="VK_SHADER_STAGE_FRAGMENT_BIT">16</entry>
<entry name="VK_SHADER_STAGE_ALL_GRAPHICS">31</entry>
<entry name="VK_SHADER_STAGE_COMPUTE_BIT">32</entry>
<entry name="VK_SHADER_STAGE_ALL">2147483647</entry>
</enum>
For bit flags, we associate a top-level bitmask
tag which indicates the association between the enum that defines the flags and the type that combines those flags.
In Vulkan, the flags enum are always called XxxFlagBits
and the combined flags XxxFlags
.
<function-pointer name="PFN_vkFreeFunction">
<return-type>
<type const="False">void</type>
</return-type>
<arg name="pUserData" optional="False">
<pointer const="False">
<type const="False">void</type>
</pointer>
</arg>
<arg name="pMemory" optional="False">
<pointer const="False">
<type const="False">void</type>
</pointer>
</arg>
</function-pointer>
Function pointers follow the exact same syntax as commands.
<integer-constant name="VK_SUBPASS_EXTERNAL" size="32">4294967295</integer-constant>
<real-constant name="VK_LOD_CLAMP_NONE" size="32">1000.0</real-constant>
<string-constant name="VK_KHR_WIN32_SURFACE_EXTENSION_NAME">VK_KHR_win32_surface</string-constant>
<struct name="VkInstanceCreateInfo">
<member default_value="VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO" name="sType" optional="False">
<type const="False">VkStructureType</type>
</member>
<member name="pNext" optional="False">
<pointer const="False">
<type const="True">void</type>
</pointer>
</member>
<member name="flags" optional="True">
<type const="False">VkInstanceCreateFlags</type>
</member>
<member name="pApplicationInfo" optional="True">
<pointer const="False">
<type const="True">VkApplicationInfo</type>
</pointer>
</member>
<member name="enabledLayerCount" optional="True">
<type const="False">uint32_t</type>
</member>
<member length="enabledLayerCount" name="ppEnabledLayerNames" optional="False">
<pointer const="False">
<pointer const="True">
<type const="True">char</type>
</pointer>
</pointer>
</member>
<member name="enabledExtensionCount" optional="True">
<type const="False">uint32_t</type>
</member>
<member length="enabledExtensionCount" name="ppEnabledExtensionNames" optional="False">
<pointer const="False">
<pointer const="True">
<type const="True">char</type>
</pointer>
</pointer>
</member>
</struct>
On their top-level tag, structs can define an extends
attribute which indicates what structures can receive it in the pNext
chain.
Another interesting note here is that pointer fields can have a length
attribute, which serves the exacts same purpose as on function arguments.
Unions are the exact same thing as structures, so I won’t detail them here.
#[repr(transparent)]
#[derive(Default, Copy, Clone, PartialEq, Eq)]
pub struct VkPhysicalDevice(usize);
impl VkPhysicalDevice
{
#[inline]
pub fn null() -> Self
{
Self(0)
}
#[inline]
pub fn from_raw(r: usize) -> Self
{
Self(r)
}
#[inline]
pub fn as_raw(&self) -> usize
{
self.0
}
}
Handles are generated as a newtypes with the transparent
attribute, which guarantees that it has the exact same layout as the underlying type, allowing us to pass it to the API directly. We also generate a few helper methods.
#[repr(transparent)]
#[derive(Default, PartialOrd, Copy, Clone, Ord, PartialEq, Eq, Hash)]
pub struct VkSystemAllocationScope(u32);
impl VkSystemAllocationScope
{
pub const COMMAND: VkSystemAllocationScope = VkSystemAllocationScope(0);
pub const OBJECT: VkSystemAllocationScope = VkSystemAllocationScope(1);
pub const CACHE: VkSystemAllocationScope = VkSystemAllocationScope(2);
pub const DEVICE: VkSystemAllocationScope = VkSystemAllocationScope(3);
pub const INSTANCE: VkSystemAllocationScope = VkSystemAllocationScope(4);
}
Enums are also generated as newtypes with transparent
. Each value is an associated constant. It was not possible to use an enum
here because there can be multiple symbols with the same value, which is not allowed in Rust enums.
#[repr(transparent)]
#[derive(Default, Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
pub struct VkCompositeAlphaFlagBitsKHR(VkFlags);
impl VkCompositeAlphaFlagBitsKHR
{
pub const OPAQUE_BIT_KHR: VkCompositeAlphaFlagBitsKHR = VkCompositeAlphaFlagBitsKHR(1);
pub const PRE_MULTIPLIED_BIT_KHR: VkCompositeAlphaFlagBitsKHR = VkCompositeAlphaFlagBitsKHR(2);
pub const POST_MULTIPLIED_BIT_KHR: VkCompositeAlphaFlagBitsKHR = VkCompositeAlphaFlagBitsKHR(4);
pub const INHERIT_BIT_KHR: VkCompositeAlphaFlagBitsKHR = VkCompositeAlphaFlagBitsKHR(8);
#[inline]
pub fn contains(&self, other: Self) -> bool
{
return (self.0 & other.0) == other.0;
}
}
Bitflags are implemented in a very similar way to enums, but they contains a very useful contains
method for checking that a mask is contained in another one. They also implement additional traits like BitOr
, BitAnd
, BitXor
, and their -Assign
variants.
#[repr(C)]
#[derive(Copy, Clone)]
pub struct VkInstanceCreateInfo
{
pub s_type: VkStructureType,
pub p_next: *const core::ffi::c_void,
pub flags: VkInstanceCreateFlags,
pub p_application_info: *const VkApplicationInfo,
pub enabled_layer_count: u32,
pub pp_enabled_layer_names: *const *const u8,
pub enabled_extension_count: u32,
pub pp_enabled_extension_names: *const *const u8,
}
pub trait ExtendsInstanceCreateInfo
{
}
impl Default for VkInstanceCreateInfo
{
fn default() -> Self
{
Self {
s_type: VkStructureType::INSTANCE_CREATE_INFO,
p_next: core::ptr::null(),
flags: VkInstanceCreateFlags::default(),
p_application_info: core::ptr::null(),
enabled_layer_count: 0,
pp_enabled_layer_names: core::ptr::null(),
enabled_extension_count: 0,
pp_enabled_extension_names: core::ptr::null(),
}
}
}
Structures use the repr(C)
attribute for ABI compatibility. They implement the Default
trait, and we also define a Extends-
trait that must be implemented by all structures that can be added to the pNext
chain of this structure. For instance we have:
impl ExtendsInstanceCreateInfo for VkDebugUtilsMessengerCreateInfoEXT {}
pub type PfnVkEnumeratePhysicalDevices = extern "system" fn(
instance: VkInstance,
p_physical_device_count: *mut u32,
p_physical_devices: *mut VkPhysicalDevice,
) -> VkResult;
Both function pointers and commands define associated function pointers that will be used in the loader. They use raw types, but will not be used directly by the user.
pub struct VkInstanceCreateInfoBuilder<'a>
{
s: VkInstanceCreateInfo,
_p: core::marker::PhantomData<&'a ()>,
}
impl<'a> VkInstanceCreateInfoBuilder<'a>
{
pub fn new() -> Self
{
Self {
s: VkInstanceCreateInfo::default(),
_p: core::marker::PhantomData,
}
}
pub fn build(&self) -> VkInstanceCreateInfo
{
self.s.clone()
}
pub fn s_type(mut self, value: VkStructureType) -> VkInstanceCreateInfoBuilder<'a>
{
self.s.s_type = value;
self
}
pub fn push_next<T: ExtendsInstanceCreateInfo>(
mut self,
next: &'a mut T,
) -> VkInstanceCreateInfoBuilder<'a>
{
unsafe {
let last = get_last_base_out_struct_chain(next as *mut T as *mut VkBaseOutStructure);
(*last).p_next = self.s.p_next as _;
self.s.p_next = core::mem::transmute(next);
}
self
}
pub fn p_application_info(
mut self,
value: Option<&'a VkApplicationInfo>,
) -> VkInstanceCreateInfoBuilder<'a>
{
self.s.p_application_info = match value
{
Some(r) => r,
None => core::ptr::null(),
};
self
}
pub fn enabled_layer_count(mut self, value: u32) -> VkInstanceCreateInfoBuilder<'a>
{
self.s.enabled_layer_count = value;
self
}
pub fn pp_enabled_layer_names(
mut self,
values: &'a [*const u8],
) -> VkInstanceCreateInfoBuilder<'a>
{
self.s.enabled_layer_count = values.len() as _;
self.s.pp_enabled_layer_names = values.as_ptr();
self
}
// ...
}
impl<'a> core::ops::Deref for VkInstanceCreateInfoBuilder<'a>
{
type Target = VkInstanceCreateInfo;
fn deref(&self) -> &Self::Target
{
&self.s
}
}
Raw structures can be annoying to fill in because they use pointers, amongst other reasons. To prevent this pain, we define a builder for each structure.
Each field from the struct has an associated method to fill it in the raw struct. These methods use Rust-friendly types:
Option<&[mut] T>
instead of optional pointers.pp_enabled_layer_names
above).For structs that include a pNext
chain, the associated push_next
method checks the compatibility of the structure we want to push using the Extends-
trait at compile time (oh yeah!), and then automagically pushes it to the front of the chain.
In addition to all this, the builder also has a build
method that returns the underlying struct. The only caveat is that all lifetime information is then lost.
Deref
and DerefMut
are also implemented in order to be able to pass a builder as its underlying struct without having to call build
.
Note that most of this was inspired by Ash.
Now that we have the types sorted out, we need to load the functions pointers to all commands. This is done in several steps:
vkGetInstanceProcAddr
function from the Vulkan library (vulkan-1.dll
on Windows). This is the “static” category.vkGetInstanceProcAddr
with VK_NULL_HANDLE
as first parameter. This is the “entry” category.vkGetInstanceProcAddr
with the instance handle as first parameter. This is the “instance” category.vkGetDeviceProcAddr
with the device handle as first parameter. This is the “device” category.Each category has a struct containing the function pointers to all its commands. It has a new
method that takes in a closure used to load a command given its name as a null-terminated byte string. Each command also has a method to call it.
For example, to create the StaticCommands
struct, the user just needs to pass to it a closure that calls GetProcAddress
with the handle to the DLL previously loaded.
#[derive(Clone)]
pub struct EntryCommands
{
pfn_enumerate_instance_layer_properties: PfnVkEnumerateInstanceLayerProperties,
pfn_enumerate_instance_extension_properties: PfnVkEnumerateInstanceExtensionProperties,
pfn_create_instance: PfnVkCreateInstance,
}
impl EntryCommands
{
pub fn load(load_fn: impl Fn(&[u8]) -> PfnVkVoidFunction) -> Self
{
EntryCommands {
pfn_enumerate_instance_layer_properties: unsafe {
core::mem::transmute(load_fn(b"vkEnumerateInstanceLayerProperties\0"))
},
pfn_enumerate_instance_extension_properties: unsafe {
core::mem::transmute(load_fn(b"vkEnumerateInstanceExtensionProperties\0"))
},
pfn_create_instance: unsafe { core::mem::transmute(load_fn(b"vkCreateInstance\0")) },
}
}
#[inline]
pub unsafe fn enumerate_instance_layer_properties(
&self,
p_property_count: *mut u32,
p_properties: *mut VkLayerProperties,
) -> VkResult
{
(self.pfn_enumerate_instance_layer_properties)(p_property_count, p_properties)
}
}
The API is divided in three parts:
EntryPoint
stores static and entry commands. It is created by giving it a closure, just like in the loader. This one is responsible for loading vkGetInstanceProcAddr
from the OS. After loading the static commands, it also loads the entry commands.Instance
wraps a VkInstance
and the instance commands. It is created by giving it a VkInstance
handle and an EntryPoint
struct.Device
wraps a VkDevice
and the device commands. It is created by giving it a VkDevice
handle and an Instance
struct.Those structs contain a method for each of their command. Those methods are high level: they translate from idiomatic Rust to raw API calls. The next part will detail how they are generated.
#[derive(Clone)]
pub struct Device
{
handle: VkDevice,
d: DeviceCommands,
}
impl Device
{
pub fn new(device: VkDevice, instance: &Instance) -> Self
{
let commands = DeviceCommands::load(|fn_name| unsafe {
instance.i.get_device_proc_addr(device, fn_name.as_ptr())
});
Self {
handle: device,
d: commands,
}
}
}
The first step to making the Vulkan API more ergonomic in this context is not requiring to pass in the device or instance handle as first argument of the commands. For this purpose, the first VkInstance
argument of all instance commands is ommited and the one contained in the Instance
is given to the raw call. Same thing for VkDevice
in Device
.
Next, commands that return a VkResult
now return a Result<VkResult, VkResult>
. Whether it is Ok
or Err
depend on the success codes listed in the specs.
Commands that return a single value via a pointer now return it naturally. If such a command returns a VkResult
, the high level method returns a Result<(VkResult, T), VkResult>
instead, where T is the returned value.
impl Device
{
pub fn create_fence(
&self,
p_create_info: &VkFenceCreateInfo,
p_allocator: Option<&VkAllocationCallbacks>,
) -> Result<(VkResult, VkFence), VkResult>
{
// VkResult vkCreateFence(
// VkDevice device,
// const VkFenceCreateInfo* pCreateInfo,
// const VkAllocationCallbacks* pAllocator,
// VkFence* pFence);
// ...
}
}
The argument type ergonomics are the same used in structure builders so I won’t list them again. This includes optional pointers, slices, etc.
Finally commands are categorized to generate the most ergonomic version possible.
This is detected by finding a non-const pointer argument that doesn’t have a length
attribute. See the above example.
This is detected by finding pointer arguments that have a length
attribute pointing to an integer value.
pub fn wait_for_fences(
&self,
p_fences: &[VkFence],
wait_all: bool,
timeout: u64,
) -> Result<VkResult, VkResult>
{
let fence_count = p_fences.len() as _;
// ...
// VkResult vkWaitForFences(
// VkDevice device,
// uint32_t fenceCount,
// const VkFence* pFences,
// VkBool32 waitAll,
// uint64_t timeout);
}
pub fn allocate_command_buffers(
&self,
p_allocate_info: &VkCommandBufferAllocateInfo,
p_command_buffers: &mut [VkCommandBuffer],
) -> Result<VkResult, VkResult>
{
assert!(p_allocate_info.command_buffer_count as usize == p_command_buffers.len());
// ...
// VkResult vkAllocateCommandBuffers(
// VkDevice device,
// const VkCommandBufferAllocateInfo* pAllocateInfo,
// VkCommandBuffer* pCommandBuffers);
}
pub fn cmd_bind_vertex_buffers(
&self,
command_buffer: VkCommandBuffer,
first_binding: u32,
p_buffers: &[VkBuffer],
p_offsets: &[VkDeviceSize],
)
{
let binding_count = p_buffers.len() as _;
assert!(binding_count as usize == p_offsets.len());
// ...
// void vkCmdBindVertexBuffers(
// VkCommandBuffer commandBuffer,
// uint32_t firstBinding,
// uint32_t bindingCount,
// const VkBuffer* pBuffers,
// const VkDeviceSize* pOffsets);
}
This is detected by finding a non-const pointer argument with a length
attribute pointing to a non-const integer pointer. In this case we generate two methods: one that sets the data pointer to null and return only the returned length, and one that take in a slice and return values through it.
// VkResult vkEnumerateDeviceLayerProperties(
// VkPhysicalDevice physicalDevice,
// uint32_t* pPropertyCount,
// VkLayerProperties* pProperties);
pub fn enumerate_device_layer_properties_count(
&self,
physical_device: VkPhysicalDevice,
) -> Result<(VkResult, usize), VkResult>
{
// Here we set pProperties to null and return the value of pPropertyCount
// ...
}
pub fn enumerate_device_layer_properties(
&self,
physical_device: VkPhysicalDevice,
p_properties: &mut [VkLayerProperties],
) -> Result<VkResult, VkResult>
{
let mut p_property_count = p_properties.len() as _;
// ...
}
Any command that doesn’t fall into any of those categories is generated without any special ergonomic apart from the one listed at the beginning of this part.
And that’s it for the bindings generator. It was definitely a challenge to be able to handle everything the Vulkan API throws at us but in the end it makes is possible to write Vulkan code in a somewhat Rusty way. However I can’t really say that the generator code is elegant… Below is an example of initializing Vulkan.
The code for all of this is available in the VkXml repository. You can contact me on Twitter for any question or remark.
let hinstance = unsafe { GetModuleHandleA(0 as _) as HINSTANCE };
let vk_module = unsafe { LoadLibraryA(b"vulkan-1.dll\0".as_ptr()) };
let vk_entry = vk::EntryPoint::new(|fn_name| unsafe {
transmute(GetProcAddress(vk_module, fn_name.as_ptr()))
});
let instance_extensions = &[
VK_EXT_DEBUG_UTILS_EXTENSION_NAME__C.as_ptr(),
VK_KHR_SURFACE_EXTENSION_NAME__C.as_ptr(),
VK_KHR_WIN32_SURFACE_EXTENSION_NAME__C.as_ptr(),
];
let layers = &[b"VK_LAYER_LUNARG_standard_validation\0".as_ptr()];
let create_info = VkInstanceCreateInfoBuilder::new()
.pp_enabled_extension_names(instance_extensions)
.pp_enabled_layer_names(layers);
let vk_instance = vk_entry.create_instance(&create_info, None).unwrap().1;
let vk_instance = vk::Instance::new(vk_instance, &vk_entry);
let gpu_count = vk_instance.enumerate_physical_devices_count().unwrap().1;
println!("{} GPU(s)", gpu_count);
let gpus = {
let mut gpus = Array::new();
gpus.resize(gpu_count, VkPhysicalDevice::null());
vk_instance.enumerate_physical_devices(&mut gpus).unwrap();
gpus
};
for (index, gpu) in gpus.iter().enumerate()
{
let prps = vk_instance.get_physical_device_properties(*gpu);
let name = unsafe { CStr::from_bytes_null_terminated_unchecked(prps.device_name.as_ptr()) };
println!(" {}: {}", index, name.as_str().unwrap());
}
vk_instance.destroy_instance(None);