Vulkan Object Wrapper: another Rust renderer engine implementation
čŻč¨€ď˝ślanguage
简体ä¸ć–‡ | Chinglish
Usage
Add this crate to your project
cargo add vkobject-rs
Rendering strategy
- If you want to use vkobject-rs with GLFW, the simplest way is to use
create_vulkan_context() with your GLFW window.
- Build pipelines for you to draw.
- A pipeline is an object that gathers all of the data from all of your buffers into a shader, runs the shader, and then wires the output to the render target attachments.
- To build a pipeline, you will have to create these things:
- Mesh. A mesh describes the polygon you want to render.
- Shaders. The shaders define how to process polygons to make pixels and put the pixels into which target attachments.
- The data for the shader's inputs. A uniform buffer, storage buffer, push constants, textures, or texel buffers could do this.
- See the implementation of
pub fn draw() in the Example code section.
- Resource clean-ups were done automatically when your objects go out of scope, appreciated by the RAII rule of the Rust language.
The mainly used objects
Buffers: to hold your polygons, draw instances, draw commands, and shader storages
There are some kinds of buffers in this crate:
BufferWithType<T>: A wrapper for Buffer, mainly for the data that won't be modified frequently, so its staging buffer could be discarded.
BufferVec<T>: A wrapper for Buffer, provides an interface that is like a Vec<T>, call flush() could upload data to GPU with a command buffer.
- The
flush() will only upload the modified part of the data to the GPU. The data updation is incremental, minimizing the bandwidth usage of CPU-GPU data transfer.
- The staging buffer of this buffer is not discardable.
UniformBuffer<T>: A wrapper for Buffer, whose data on the CPU side could be dereferenced into a structure, and you can modify the structure members freely.
- The
flush() will upload the whole structure to the GPU side.
- This buffer is commonly used for shader inputs.
GenericUniformBuffer: A wrapper for UniformBuffer<T> that erases the generic type <T>.
StorageBuffer<T>: A wrapper for Buffer and the usage is the same as UniformBuffer<T> except it's for the shader's storage buffer inputs.
- The shaders could modify the storage buffers freely, while they can't modify uniform buffers.
GenericStorageBuffer: A wrapper for StorageBuffer<T> that erases the generic type <T>.
StagingBuffer: The staging buffer that's totally transparent to your CPU, you can have its data pointer and modify the data for it to upload to the GPU.
- Safety: You should have to know how to manipulate raw pointers correctly.
Buffer: The buffer that's transparent to the GPU, and has its own staging buffer for uploading data to the GPU.
- Transfer data to its staging buffer, then call
upload_staging_buffer() to enqueue an upload command into a command buffer.
- After the upload command is executed, the data is transferred into the GPU, then you call
discard_staging_buffer() to save some system memory if you wish.
- This thing is raw; you don't want to use this.
VulkanBuffer: The most low-level buffer wrapping object, the Buffer and StagingBuffer are implemented by using this object.
- This thing is super raw; you don't want to use this.
Mesh: to hold your polygons, draw instances, draw commands
A mesh has 4 buffers. According to the usage, they are:
- vertex buffer
- index buffer (optional)
- instance buffer (optional)
- indirect draw command buffer (optional)
The buffers for a mesh have two types:
- For static draw usage, there is
BufferWithType<T>
- The data in the buffer is once initialized, and then never changes.
- For dynamic update usage, there is
BufferVec<T>
- You can modify its data frequently like a
Vec<T>, then call flush() to apply changes to the GPU buffer.
#[derive(Debug, Clone)]
pub struct Mesh<BV, V, BE, E, BI, I, BC, C>
where
BV: BufferForDraw<V>,
BE: BufferForDraw<E>,
BI: BufferForDraw<I>,
BC: BufferForDraw<C>,
V: BufferVecStructItem,
E: BufferVecItem + 'static,
I: BufferVecStructItem,
C: BufferVecStructItem {
pub primitive_type: VkPrimitiveTopology,
pub vertices: BV,
pub indices: Option<BE>,
pub instances: Option<BI>,
pub commands: Option<BC>,
vertex_type: V,
element_type: E,
instance_type: I,
command_type: C,
}
#[derive(Default, Debug, Clone, Copy, Iterable)]
pub struct UnusedBufferItem {}
pub type UnusedBufferType = BufferWithType<UnusedBufferItem>;
pub fn buffer_unused() -> Option<UnusedBufferType> {
None
}
impl<BV, V, BE, E, BI, I, BC, C> Mesh<BV, V, BE, E, BI, I, BC, C>
where
BV: BufferForDraw<V>,
BE: BufferForDraw<E>,
BI: BufferForDraw<I>,
BC: BufferForDraw<C>,
V: BufferVecStructItem,
E: BufferVecItem + 'static,
I: BufferVecStructItem,
C: BufferVecStructItem {
pub fn new(primitive_type: VkPrimitiveTopology, vertices: BV, indices: Option<BE>, instances: Option<BI>, commands: Option<BC>) -> Self {
Self {
primitive_type,
vertices,
indices,
instances,
commands,
vertex_type: V::default(),
element_type: E::default(),
instance_type: I::default(),
command_type: C::default(),
}
}
pub fn flush(&mut self, cmdbuf: VkCommandBuffer) -> Result<(), VulkanError> {
filter_no_staging_buffer(self.vertices.flush(cmdbuf))?;
if let Some(ref mut indices) = self.indices {filter_no_staging_buffer(indices.flush(cmdbuf))?;}
if let Some(ref mut instances) = self.instances {filter_no_staging_buffer(instances.flush(cmdbuf))?;}
if let Some(ref mut commands) = self.commands {filter_no_staging_buffer(commands.flush(cmdbuf))?;}
Ok(())
}
pub fn discard_staging_buffers(&mut self) {
self.vertices.discard_staging_buffer();
if let Some(ref mut indices) = self.indices {indices.discard_staging_buffer();}
if let Some(ref mut instances) = self.instances {instances.discard_staging_buffer();}
if let Some(ref mut commands) = self.commands {commands.discard_staging_buffer();}
}
}
Shaders
The shaders in this crate can compile GLSL or HLSL code to SPIR-V intermediate language. Also, they could be loaded from a binary file instead of the source code.
let draw_shaders = Arc::new(DrawShaders::new(
Arc::new(VulkanShader::new_from_source_file_or_cache(device.clone(), ShaderSourcePath::VertexShader(PathBuf::from("shaders/test.vsh")), false, "main", OptimizationLevel::Performance, false)?),
None,
None,
None,
Arc::new(VulkanShader::new_from_source_file_or_cache(device.clone(), ShaderSourcePath::FragmentShader(PathBuf::from("shaders/test.fsh")), false, "main", OptimizationLevel::Performance, false)?),
));
Texture
The VulkanTexture is the wrapper for you to use textures.
DescriptorProps
The descriptor properties are for the shader inputs; they define which descriptor set and binding has a uniform buffer, or texture, samplers, etc.
- The shader inputs were made as
Vec<T> since this could help to provide data for array-type inputs of the shaders.
- For a single variable input, simply providing one element of the array could work.
#[derive(Debug)]
pub enum DescriptorProp {
Samplers(Vec<Arc<VulkanSampler>>),
Images(Vec<TextureForSample>),
StorageBuffers(Vec<Arc<dyn GenericStorageBuffer>>),
UniformBuffers(Vec<Arc<dyn GenericUniformBuffer>>),
StorageTexelBuffers(Vec<VulkanBufferView>),
UniformTexelBuffers(Vec<VulkanBufferView>),
}
#[derive(Default, Debug, Clone)]
pub struct DescriptorProps {
pub sets: HashMap<u32 , HashMap<u32 , Arc<DescriptorProp>>>,
}
Pipeline
The pipeline wires mesh, texture, uniform buffers, storage buffers, shaders, output images, all together, and defines all of the rendering options.
let pipeline = ctx.create_pipeline_builder(mesh, draw_shaders, desc_props.clone())?
.set_cull_mode(VkCullModeFlagBits::VK_CULL_MODE_NONE as VkCullModeFlags)
.set_depth_test(false)
.set_depth_write(false)
.build()?;
On draw:
let scene = ctx.begin_scene(0, None)?;
scene.set_viewport_swapchain(0.0, 1.0)?;
scene.set_scissor_swapchain()?;
scene.begin_renderpass(Vec4::new(0.0, 0.0, 0.2, 1.0), 1.0, 0)?;
pipeline.draw(scene.get_cmdbuf())?;
scene.end_renderpass()?;
scene.finish();
Example code
use glfw::*;
use crate::prelude::*;
use std::{
collections::HashMap,
ffi::CStr,
path::PathBuf,
slice::from_raw_parts_mut,
sync::{
Arc,
Mutex,
RwLock,
atomic::{
AtomicBool,
Ordering,
}
},
thread,
time::Duration,
};
const TEST_TIME: f64 = 10.0;
#[derive(Debug)]
pub struct AppInstance {
pub ctx: Arc<RwLock<VulkanContext>>,
pub window: PWindow,
pub events: GlfwReceiver<(f64, WindowEvent)>,
pub glfw: Glfw,
}
impl AppInstance {
pub fn new(width: u32, height: u32, title: &str, window_mode: glfw::WindowMode) -> Result<Self, VulkanError> {
static GLFW_LOCK: Mutex<u32> = Mutex::new(0);
let glfw_lock = GLFW_LOCK.lock().unwrap();
let mut glfw = glfw::init(glfw::fail_on_errors).unwrap();
glfw.window_hint(WindowHint::ClientApi(ClientApiHint::NoApi));
let (mut window, events) = glfw.create_window(width, height, title, window_mode).expect("Failed to create GLFW window.");
drop(glfw_lock);
window.set_key_polling(true);
let device_requirement = DeviceRequirement {
can_graphics: true,
can_compute: false,
name_subtring: "",
};
let ctx = Arc::new(RwLock::new(create_vulkan_context(&window, device_requirement, PresentInterval::VSync, 1, false)?));
let ctx_lock = ctx.read().unwrap();
for gpu in VulkanGpuInfo::get_gpu_info(&ctx_lock.vkcore)?.iter() {
println!("Found GPU: {}", unsafe{CStr::from_ptr(gpu.properties.deviceName.as_ptr())}.to_str().unwrap());
}
println!("Chosen GPU name: {}", unsafe{CStr::from_ptr(ctx_lock.device.get_gpu().properties.deviceName.as_ptr())}.to_str().unwrap());
println!("Chosen GPU type: {:?}", ctx_lock.device.get_gpu().properties.deviceType);
drop(ctx_lock);
Ok(Self {
glfw,
window,
events,
ctx,
})
}
pub fn get_time(&self) -> f64 {
glfw_get_time()
}
pub fn set_time(&self, time: f64) {
glfw_set_time(time)
}
pub fn run(&mut self,
test_time: Option<f64>,
mut on_render: impl FnMut(&mut VulkanContext, f64) -> Result<(), VulkanError> + Send + 'static
) -> Result<(), VulkanError> {
let exit_flag = Arc::new(AtomicBool::new(false));
let exit_flag_cloned = exit_flag.clone();
let start_time = self.glfw.get_time();
let ctx = self.ctx.clone();
let renderer_thread = thread::spawn(move || {
let mut num_frames = 0;
let mut time_in_sec: u64 = 0;
let mut num_frames_prev: u64 = 0;
while !exit_flag_cloned.load(Ordering::Relaxed) {
let cur_frame_time = glfw_get_time();
let run_time = cur_frame_time - start_time;
on_render(&mut ctx.write().unwrap(), run_time).unwrap();
num_frames += 1;
let new_time_in_sec = run_time.floor() as u64;
if new_time_in_sec > time_in_sec {
let fps = num_frames - num_frames_prev;
println!("FPS: {fps}\tat {new_time_in_sec}s");
time_in_sec = new_time_in_sec;
num_frames_prev = num_frames;
}
}
});
while !self.window.should_close() {
let run_time = glfw_get_time() - start_time;
thread::sleep(Duration::from_millis(1));
self.glfw.poll_events();
for (_, event) in glfw::flush_messages(&self.events) {
match event {
glfw::WindowEvent::Key(Key::Escape, _, Action::Press, _) => {
self.window.set_should_close(true);
}
_ => {}
}
}
if let Some(test_time) = test_time {
if run_time >= test_time {
self.window.set_should_close(true);
}
}
}
exit_flag.store(true, Ordering::Relaxed);
renderer_thread.join().unwrap();
println!("End of the test");
Ok(())
}
}
unsafe impl Send for AppInstance {}
unsafe impl Sync for AppInstance {}
fn main() {
derive_vertex_type! {
pub struct VertexType {
pub position: Vec2,
}
}
derive_uniform_buffer_type! {
pub struct UniformInput {
resolution: Vec3,
time: f32,
}
}
struct Resources {
uniform_input: Arc<dyn GenericUniformBuffer>,
pipeline: Pipeline,
}
impl Resources {
pub fn new(ctx: &mut VulkanContext) -> Result<Self, VulkanError> {
let device = ctx.device.clone();
let draw_shaders = Arc::new(DrawShaders::new(
Arc::new(VulkanShader::new_from_source_file_or_cache(device.clone(), ShaderSourcePath::VertexShader(PathBuf::from("shaders/test.vsh")), false, "main", OptimizationLevel::Performance, false)?),
None,
None,
None,
Arc::new(VulkanShader::new_from_source_file_or_cache(device.clone(), ShaderSourcePath::FragmentShader(PathBuf::from("shaders/test.fsh")), false, "main", OptimizationLevel::Performance, false)?),
));
let uniform_input: Arc<dyn GenericUniformBuffer> = Arc::new(UniformBuffer::<UniformInput>::new(device.clone())?);
let desc_props = Arc::new(DescriptorProps::default());
desc_props.new_uniform_buffer(0, 0, uniform_input.clone());
let pool_in_use = ctx.cmdpools[0].use_pool(None)?;
let vertices_data = vec![
VertexType {
position: Vec2::new(-1.0, -1.0),
},
VertexType {
position: Vec2::new( 1.0, -1.0),
},
VertexType {
position: Vec2::new(-1.0, 1.0),
},
VertexType {
position: Vec2::new( 1.0, 1.0),
},
];
let vertices = Arc::new(RwLock::new(BufferWithType::new(device.clone(), &vertices_data, pool_in_use.cmdbuf, VkBufferUsageFlagBits::VK_BUFFER_USAGE_VERTEX_BUFFER_BIT as VkBufferUsageFlags)?));
let mesh = Arc::new(GenericMeshWithMaterial::new(Arc::new(Mesh::new(VkPrimitiveTopology::VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, vertices, buffer_unused(), buffer_unused(), buffer_unused())), "", None));
mesh.geometry.flush(pool_in_use.cmdbuf)?;
drop(pool_in_use);
ctx.cmdpools[0].wait_for_submit(u64::MAX)?;
mesh.geometry.discard_staging_buffers();
let pipeline = ctx.create_pipeline_builder(mesh, draw_shaders, desc_props)?
.set_cull_mode(VkCullModeFlagBits::VK_CULL_MODE_NONE as VkCullModeFlags)
.set_depth_test(false)
.set_depth_write(false)
.build()?;
Ok(Self {
uniform_input,
pipeline,
})
}
pub fn draw(&self, ctx: &mut VulkanContext, run_time: f64) -> Result<(), VulkanError> {
let scene = ctx.begin_scene(0, None)?;
let cmdbuf = scene.get_cmdbuf();
let extent = scene.get_rendertarget_extent();
let ui_data = unsafe {from_raw_parts_mut(self.uniform_input.get_staging_buffer_address()? as *mut UniformInput, 1)};
ui_data[0] = UniformInput {
resolution: Vec3::new(extent.width as f32, extent.height as f32, 1.0),
time: run_time as f32,
};
self.uniform_input.flush(cmdbuf)?;
scene.set_viewport_swapchain(0.0, 1.0)?;
scene.set_scissor_swapchain()?;
scene.begin_renderpass(Vec4::new(0.0, 0.0, 0.2, 1.0), 1.0, 0)?;
self.pipeline.draw(cmdbuf)?;
scene.end_renderpass()?;
scene.finish();
Ok(())
}
}
let mut inst = Box::new(AppInstance::new(1024, 768, "Vulkan test", glfw::WindowMode::Windowed).unwrap());
let resources = Resources::new(&mut inst.ctx.write().unwrap()).unwrap();
inst.run(Some(TEST_TIME),
move |ctx: &mut VulkanContext, run_time: f64| -> Result<(), VulkanError> {
resources.draw(ctx, run_time)
}).unwrap();
}
FAQ
Question: When building the dependent shaderc, it fails and shows the following information:
warning: shaderc-sys@0.10.1: shaderc: requested to build from source
error: failed to run custom build command for `shaderc-sys v0.10.1`
Caused by:
process didn't exit successfully: `C:\your\path\to\your\crate\target\release\build\shaderc-sys-03dfa106721f22d5\build-script-build` (exit code: 101)
--- stdout
cargo:warning=shaderc: requested to build from source
--- stderr
thread 'main' panicked at C:\Users\your_name\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\shaderc-sys-0.10.1\build\cmd_finder.rs:55:13:
couldn't find required command: "ninja"
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
warning: build failed, waiting for other jobs to finish...
- Answer: Install ninja, then build it again.
- For Windows: run
winget install Ninja-build.Ninja
- For Debian/Ubuntu: run
sudo apt install ninja-build
- For Fedora/RHEL: run
sudo dnf install ninja-build
- For MacOS: run
brew install ninja
See: https://github.com/ninja-build/ninja/releases`
Question: When the validation_layer feature was enabled, it failed to run. The error information says:
called `Result::unwrap()` on an `Err` value: VkError(VkErrorLayerNotPresent("vkCreateInstance"))
- Answer: This is because your GPU driver doesn't support the Vulkan validation layer. You can only debug your Vulkan code without it.
- Buying an NVIDIA GPU device could resolve this problem, as its driver supports the Vulkan validation layer.