Expand description
Maia
Safe, low-level Vulkan bindings. The general properties of this library are
- Memory safe on the CPU. No safe operation can cause memory corruption or data races in host* memory.
- Lock-free. Thread safety is handled at compile time with
&mut
rather than with mutexes, to avoid performance surprises. - Low-level. Close to 1-1 correspondance with Vulkan API calls. Calls which don’t allocate in Vulkan also don’t allocate in Maia.
- Selective. Maia intentionally omits Vulkan features that are not performant, not useful, or are rarely supported. However, APIs are provided to allow extension in downstream crates.
- As ergonomic as possible. In particular, nearly everything is
Send
andSync
.
Setup
Maia dynamically links to the system’s Vulkan loader, so one must be installed. Instructions for specific systems follow.
To begin using the API, create an instance object with vk::Instance::new
.
To enable validation layers for debugging, set the environment variable VK_INSTANCE_LAYERS="VK_LAYER_KHRONOS_validation"
or use the Configurator GUI.
On Linux
To build, install your distro’s Vulkan development libaries (eg for Debian, sudo apt install libvulkan-dev
). You will also probably want to install the validation layers, either from the distro (eg sudo apt install vulkan-validationlayers
) or by installing the Vulkan SDK.
To run, a Vulkan-compatible graphics driver should suffice.
On MacOS
To build, install the Vulkan SDK, and enable the “System Global Files” option during installation.
To run, you will probably want to include the Vulkan loader and MoltenVK into your .app bundle. Full instructions are available here, and an example can be found in the demo/
directory.
On Windows
A Vulkan-compatible graphics driver is sufficient to build and run. You will probably want to install the Vulkan SDK for validation layers, though.
To run the demos
To compile shaders in the demos, either CMake or the Vulkan SDK must be installed. (CMake appears to be the easier approach on Linux, and the Vulkan SDK on other systems.)
Using Vulkan
This documentation assumes that you already know how Vulkan works. If you’re just getting started, I can recommend the Vulkan Guide, the Vulkan Tutorial, or the older, but more detailed API without Secrets. The code that they walk you through will look very similar to the demo
and hello-triangle
examples in this repo, so you can look at those alongside the walkthroughs to see what the corresponding Rust functions are called.
Safety
Maia does not try to protect the contents of your buffers, images, and shader variables, since these values don’t have invalid bit patterns and in particular don’t contain pointers. Instead, it prevents incorrect API usage where the consequences could “escape” into the rest of your program, for example use-after-free of Vulkan objects, or out of bounds accesses in RAM buffers. In this way, Maia’s memory safety experience is akin to talking to a C program on the other end of a network connection. The implication of this is that Maia does not enforce every “the application must not” statement in the Vulkan spec, since the spec does not distinguish between these different kinds of misuse. The safety it provides is instead in regards to the behavior of actual Vulkan implementations.
Index
Some quick links to the main object types:
- vk::Instance
- vk::PhysicalDevice
- vk::Device
- vk::Queue
- vk::CommandPool
- vk::DescriptorSetLayout
- vk::PipelineLayout
- vk::Pipeline
- vk::Buffer
- vk::Image
- vk::Sampler
- vk::DeviceMemory
- vk::DescriptorSet
- vk::RenderPass
- vk::Semaphore
- vk::Fence
Hello Triangle
Here is a complete, minimal example showing all the boilerplate neccesary to put a triangle onscreen. Note that it does skip over some things you will probably want to do, such as recreate the swapchain, use descriptor sets, and so on. The example in the demo/ directory demonstrates some of these features.
// Copyright 2022 Google LLC
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
use maia::vk;
use std::io::Write;
fn main() -> vk::Result<()> {
let event_loop = winit::event_loop::EventLoop::new();
let window = winit::window::Window::new(&event_loop).unwrap();
let mut instance_exts = vec![];
// Get the instance extensions needed to create a surface for the window
instance_exts
.extend(maia::window::required_instance_extensions(&window)?.iter());
// Get the instance extension needed by MoltenVK, if neccesary
instance_exts.extend(required_instance_extensions()?.iter());
// Create the instance
let inst = vk::Instance::new(&vk::InstanceCreateInfo {
enabled_extension_names: vk::slice(&instance_exts),
..Default::default()
})?;
// Create a surface with the appropriate platform extension
let surf = maia::window::create_surface(&inst, &window)?;
// Pick a suitable physical device
let phy = pick_physical_device(&inst.enumerate_physical_devices()?);
// Pick a queue family that supports presenting to the surface
let queue_family = pick_queue_family(&phy, &surf, &window)?;
// Make sure the surface format we want is supported
assert!(surf.surface_formats(&phy)?.iter().any(|f| {
f == &vk::SurfaceFormatKHR {
format: vk::Format::B8G8R8A8_SRGB,
color_space: vk::ColorSpaceKHR::SRGB_NONLINEAR_KHR,
}
}));
// We need the swapchain extension, plus the extension needed by MoltenVK
let device_extensions = required_device_extensions(&phy)?;
// Create the virtual device
let (device, mut queues) = vk::Device::new(
&phy,
&vk::DeviceCreateInfo {
// Create one queue
queue_create_infos: vk::slice(&[vk::DeviceQueueCreateInfo {
queue_family_index: queue_family,
queue_priorities: vk::slice(&[1.0]),
..Default::default()
}]),
enabled_extension_names: vk::slice(device_extensions),
enabled_features: Some(&vk::PhysicalDeviceFeatures {
// Allow the use of in-RAM vertex buffers
robust_buffer_access: vk::True,
..Default::default()
}),
..Default::default()
},
)?;
// Create the swapchain
let window_size = window.inner_size();
let window_extent =
vk::Extent2D { width: window_size.width, height: window_size.height };
let mut swapchain = vk::ext::SwapchainKHR::new(
&device,
vk::CreateSwapchainFrom::Surface(surf),
vk::SwapchainCreateInfoKHR {
min_image_count: 3,
image_format: vk::Format::B8G8R8A8_SRGB,
image_extent: window_extent,
image_usage: vk::ImageUsageFlags::COLOR_ATTACHMENT,
..Default::default()
},
)?;
// We need one framebuffer per image, so put them in a hashmap
let mut framebuffers = std::collections::HashMap::new();
// Create the render pass
let render_pass = vk::RenderPass::new(
&device,
&vk::RenderPassCreateInfo {
attachments: vk::slice(&[vk::AttachmentDescription {
format: vk::Format::B8G8R8A8_SRGB,
load_op: vk::AttachmentLoadOp::CLEAR,
store_op: vk::AttachmentStoreOp::STORE,
initial_layout: vk::ImageLayout::UNDEFINED,
final_layout: vk::ImageLayout::PRESENT_SRC_KHR,
..Default::default()
}]),
subpasses: vk::slice(&[vk::SubpassDescription {
color_attachments: &[vk::AttachmentReference {
attachment: 0,
layout: vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL,
}],
..Default::default()
}
.try_into()?]),
..Default::default()
},
)?;
// Create the shader modules and graphics pipeline
let vertex_shader = vk::ShaderModule::new(
&device,
inline_spirv::inline_spirv!(
r#" #version 450
layout(location = 0) in vec4 i_position;
layout(location = 1) in vec4 i_color;
layout(location = 0) out vec4 o_color;
void main() {
gl_Position = i_position;
o_color = i_color;
} "#,
glsl,
vert
),
)?;
let fragment_shader = vk::ShaderModule::new(
&device,
inline_spirv::inline_spirv!(
r#" #version 450
layout(location = 0) in vec4 i_color;
layout(location = 0) out vec4 o_Color;
void main() { o_Color = i_color;} "#,
glsl,
frag
),
)?;
let pipeline =
vk::Pipeline::new_graphics(&vk::GraphicsPipelineCreateInfo {
stages: &[
vk::PipelineShaderStageCreateInfo::vertex(&vertex_shader),
vk::PipelineShaderStageCreateInfo::fragment(&fragment_shader),
],
vertex_input_state: &vk::PipelineVertexInputStateCreateInfo {
vertex_binding_descriptions: vk::slice(&[
vk::VertexInputBindingDescription {
binding: 0,
stride: std::mem::size_of_val(&VERTEX_DATA[0]) as u32,
input_rate: vk::VertexInputRate::VERTEX,
},
]),
vertex_attribute_descriptions: vk::slice(&[
vk::VertexInputAttributeDescription {
location: 0,
binding: 0,
format: vk::Format::R32G32B32A32_SFLOAT,
offset: 0,
},
vk::VertexInputAttributeDescription {
location: 1,
binding: 0,
format: vk::Format::R32G32B32A32_SFLOAT,
offset: 16,
},
]),
..Default::default()
},
input_assembly_state: &vk::PipelineInputAssemblyStateCreateInfo {
topology: vk::PrimitiveTopology::TRIANGLE_STRIP,
..Default::default()
},
tessellation_state: None,
viewport_state: &Default::default(),
rasterization_state: &Default::default(),
multisample_state: &Default::default(),
depth_stencil_state: None,
color_blend_state: &Default::default(),
// It's good practice to specify the viewport as dynamic state, so
// we don't need to recompile every pipeline when the window is
// resized
dynamic_state: Some(&vk::PipelineDynamicStateCreateInfo {
dynamic_states: vk::slice(&[
vk::DynamicState::VIEWPORT,
vk::DynamicState::SCISSOR,
]),
..Default::default()
}),
layout: &vk::PipelineLayout::new(
&device,
Default::default(),
vec![],
vec![],
)?,
render_pass: &render_pass,
subpass: 0,
cache: None,
})?;
// Create the vertex buffer and fill it with data
let vertex_buffer = vk::BufferWithoutMemory::new(
&device,
&vk::BufferCreateInfo {
size: std::mem::size_of_val(&VERTEX_DATA) as u64,
usage: vk::BufferUsageFlags::VERTEX_BUFFER,
..Default::default()
},
)?;
let memory = vk::DeviceMemory::new(
&device,
vertex_buffer.memory_requirements().size,
memory_type(&phy),
)?;
let vertex_buffer = vk::Buffer::new(vertex_buffer, &memory, 0)?;
let mut mapped = memory.map(0, std::mem::size_of_val(&VERTEX_DATA))?;
mapped.write_at(0).write_all(bytemuck::bytes_of(&VERTEX_DATA)).unwrap();
// Create the remaining required objects
let mut cmd_pool = vk::CommandPool::new(&device, queue_family)?;
// Create the command buffer
let mut cmd_buf = Some(cmd_pool.allocate()?);
let mut queue = queues.remove(0).remove(0);
// Since we wait between frames we only need one acquire semaphore
let mut acquire_sem = vk::Semaphore::new(&device)?;
let mut fence = Some(vk::Fence::new(&device)?);
let mut redraw = move || -> vk::Result<()> {
let (img, _subopt) =
swapchain.acquire_next_image(&mut acquire_sem, u64::MAX)?;
// We want one framebuffer and present semaphore per image
if !framebuffers.contains_key(&img) {
// Create them if we haven't already
let img_view = vk::ImageView::new(
&img,
&vk::ImageViewCreateInfo {
format: vk::Format::B8G8R8A8_SRGB,
..Default::default()
},
)?;
let fb = vk::Framebuffer::new(
&render_pass,
Default::default(),
vec![img_view],
window_extent.into(),
)?;
let sem = vk::Semaphore::new(&device)?;
framebuffers.insert(img.clone(), (fb, sem));
}
let (framebuffer, present_sem) = framebuffers.get_mut(&img).unwrap();
// Command buffer recoding uses a builder pattern
let mut pass = cmd_pool
.begin(cmd_buf.take().unwrap())?
// The render pass is recorded on a builder that wraps the main one
.begin_render_pass(
&render_pass,
&framebuffer,
&vk::Rect2D { extent: window_extent, ..Default::default() },
&[vk::ClearValue {
color: vk::ClearColorValue { f32: [0.1, 0.2, 0.3, 1.0] },
}],
)?;
pass.bind_pipeline(&pipeline);
pass.set_viewport(&vk::Viewport {
x: 0.0,
y: 0.0,
width: window_extent.width as f32,
height: window_extent.height as f32,
min_depth: 0.0,
max_depth: 1.0,
});
pass.set_scissor(&vk::Rect2D {
extent: window_extent,
..Default::default()
});
pass.bind_vertex_buffers(0, &[(&vertex_buffer, 0)])?;
pass.draw(3, 1, 0, 0)?;
// End the render pass and extract the command buffer from the builder
cmd_buf = Some(pass.end()?.end()?);
// Submit the command buffer
let pending_fence = queue.submit_with_fence(
&mut [vk::SubmitInfo {
wait: &mut [(
&mut acquire_sem,
vk::PipelineStageFlags::TOP_OF_PIPE,
)],
commands: &mut [cmd_buf.as_mut().unwrap()],
signal: &mut [present_sem],
}],
fence.take().unwrap(),
)?;
swapchain.present(&mut queue, &img, present_sem)?;
// Wait for the execution to finish. Otherwise resetting the command
// pool will return an error.
fence = Some(pending_fence.wait()?);
cmd_pool.reset(Default::default())?;
Ok(())
};
event_loop.run(move |event, _, control_flow| {
use winit::event::{Event, WindowEvent};
use winit::event_loop::ControlFlow;
*control_flow = ControlFlow::Wait;
match event {
Event::WindowEvent {
event: WindowEvent::CloseRequested, ..
} => *control_flow = ControlFlow::Exit,
Event::RedrawRequested(_) => {
if let Err(e) = redraw() {
println!("{:?}", e);
*control_flow = ControlFlow::Exit;
}
}
_ => (),
}
})
}
const VERTEX_DATA: [[[f32; 4]; 2]; 3] = [
[[0.0, -0.7, 0., 1.], [0., 0., 1., 1.]],
[[-0.7, 0.7, 0., 1.], [1., 0., 0., 1.]],
[[0.7, 0.7, 0., 1.], [0., 1., 0., 1.]],
];
// Add extension required by MoltenVK, if available
fn required_instance_extensions() -> vk::Result<&'static [vk::Str<'static>]> {
let exts = vk::instance_extension_properties()?;
const FOR_MVK: vk::Str<'static> = vk::ext::GET_PHYSICAL_DEVICE_PROPERTIES2;
if exts.iter().any(|e| e.extension_name == FOR_MVK) {
Ok(&[FOR_MVK])
} else {
Ok(&[])
}
}
// Add extension required by MoltenVK, if available
fn required_device_extensions(
phy: &vk::PhysicalDevice,
) -> vk::Result<&'static [vk::Str<'static>]> {
let exts = phy.device_extension_properties()?;
const FOR_MVK: vk::Str<'static> = vk::ext::PORTABILITY_SUBSET;
if exts.iter().any(|e| e.extension_name == FOR_MVK) {
Ok(&[FOR_MVK, vk::ext::SWAPCHAIN])
} else {
Ok(&[vk::ext::SWAPCHAIN])
}
}
// Pick an appropriate physical device
fn pick_physical_device(phys: &[vk::PhysicalDevice]) -> vk::PhysicalDevice {
let discr = vk::PhysicalDeviceType::DISCRETE_GPU;
let int = vk::PhysicalDeviceType::INTEGRATED_GPU;
phys.iter()
.find(|p| p.properties().device_type == discr)
.or_else(|| phys.iter().find(|p| p.properties().device_type == int))
.unwrap_or(&phys[0])
.clone()
}
// Pick an appropriate queue family
fn pick_queue_family(
phy: &vk::PhysicalDevice, surf: &vk::ext::SurfaceKHR,
window: &winit::window::Window,
) -> vk::Result<u32> {
for (num, props) in phy.queue_family_properties().iter().enumerate() {
if !(props.queue_flags & vk::QueueFlags::GRAPHICS).is_empty()
&& surf.support(phy, num as u32)?
&& maia::window::presentation_support(phy, num as u32, window)
{
return Ok(num as u32);
}
}
panic!("No usable queue!")
}
fn memory_type(phy: &vk::PhysicalDevice) -> u32 {
let desired = vk::MemoryPropertyFlags::HOST_VISIBLE
| vk::MemoryPropertyFlags::HOST_COHERENT;
for (num, props) in phy.memory_properties().memory_types.iter().enumerate()
{
if props.property_flags & desired == desired {
return num as u32;
}
}
panic!("No host visible memory!")
}
Modules
Vulkan extensions
MacOS-specific Instructions
Vulkan core functionality.
window
Wrappers that integrate with raw_window_handle and the platform’s wsi extensions. This module is disabled by default because it requires additional dependencies.