A modern and easy-to-use library for the Vulkan® API
Guide • Test • Keyboard Shortcuts • Command-Line Arguments • Benchmark • Build • Third-Party
Need help? Please feel free to ask us on ➜ Discord
New to Vulkan? Take a look at this ➜ Vulkan Guide
Check Awesome Vulkan ecosystem for tutorials, samples and books.
Latest Vulkan documentation: Specification and Proposals
Hello World in Vulkan? Let's go!
a simple app that renders a colored window
All we need is a window
+ device
and renderer
Vulkan is a low-level, verbose graphics API and such a program can take several hundred lines of code.
The good news is that liblava can help you...
#include "liblava/lava.hpp"
using namespace lava;
Here are a few examples to get to know lava
int main(int argc, char* argv[]) {
lava::frame frame( {argc, argv} );
return frame.ready() ? 0 : error::not_ready;
}
This is how to initialize lava frame with command line arguments.
lava::frame frame(argh);
if (!frame.ready())
return error::not_ready;
ui32 count = 0;
frame.add_run([&](id::ref run_id) {
sleep(one_second);
count++;
logger()->debug("{} - running {} sec",
count, frame.get_running_time_sec());
if (count == 3)
return frame.shut_down();
return run_continue;
});
return frame.run();
The last line performs a loop with the run we added before - If count reaches 3 that loop will exit.
Here is another example that shows how to create lava window and handle lava input.
lava::frame frame(argh);
if (!frame.ready())
return error::not_ready;
lava::window window;
if (!window.create())
return error::create_failed;
lava::input input;
window.assign(&input);
input.key.listeners.add([&](key_event::ref event) {
if (event.pressed(key::escape))
return frame.shut_down();
return input_ignore;
});
frame.add_run([&](id::ref run_id) {
input.handle_events();
if (window.close_request())
return frame.shut_down();
return run_continue;
});
return frame.run();
Straightforward ➜ With this knowledge in hand let's write our Hello World...
lava::frame frame(argh);
if (!frame.ready())
return error::not_ready;
lava::window window;
if (!window.create())
return error::create_failed;
lava::input input;
window.assign(&input);
input.key.listeners.add([&](key_event::ref event) {
if (event.pressed(key::escape))
return frame.shut_down();
return input_ignore;
});
lava::device::ptr device = frame.platform.create_device();
if (!device)
return error::create_failed;
lava::render_target::s_ptr render_target = create_target(&window, device);
if (!render_target)
return error::create_failed;
lava::renderer renderer;
if (!renderer.create(render_target->get_swapchain()))
return error::create_failed;
ui32 frame_count = render_target->get_frame_count();
VkCommandPool cmd_pool;
VkCommandBuffers cmd_bufs(frame_count);
auto build_cmd_bufs = [&]() {
if (!device->vkCreateCommandPool(device->graphics_queue().family, &cmd_pool))
return build_failed;
if (!device->vkAllocateCommandBuffers(cmd_pool, frame_count, cmd_bufs.data()))
return build_failed;
VkCommandBufferBeginInfo const begin_info{
.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
.flags = VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT,
};
VkClearColorValue const clear_color = {
random(1.f), random(1.f), random(1.f), 0.f
};
VkImageSubresourceRange const image_range{
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
.levelCount = 1,
.layerCount = 1,
};
for (auto i = 0u; i < frame_count; ++i) {
VkCommandBuffer cmd_buf = cmd_bufs[i];
VkImage frame_image = render_target->get_image(i);
if (failed(device->call().vkBeginCommandBuffer(cmd_buf, &begin_info)))
return build_failed;
insert_image_memory_barrier(device,
cmd_buf,
frame_image,
VK_ACCESS_MEMORY_READ_BIT,
VK_ACCESS_TRANSFER_WRITE_BIT,
VK_IMAGE_LAYOUT_UNDEFINED,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT,
image_range);
device->call().vkCmdClearColorImage(cmd_buf,
frame_image,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
&clear_color,
1,
&image_range);
insert_image_memory_barrier(device,
cmd_buf,
frame_image,
VK_ACCESS_TRANSFER_WRITE_BIT,
VK_ACCESS_MEMORY_READ_BIT,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,
image_range);
if (failed(device->call().vkEndCommandBuffer(cmd_buf)))
return build_failed;
}
return build_done;
};
auto clean_cmd_bufs = [&]() {
device->vkFreeCommandBuffers(cmd_pool, frame_count, cmd_bufs.data());
device->vkDestroyCommandPool(cmd_pool);
};
if (!build_cmd_bufs())
return error::create_failed;
render_target->on_swapchain_start = build_cmd_bufs;
render_target->on_swapchain_stop = clean_cmd_bufs;
frame.add_run([&](id::ref run_id) {
input.handle_events();
if (window.close_request())
return frame.shut_down();
if (window.resize_request())
return window.handle_resize();
optional_index current_frame = renderer.begin_frame();
if (!current_frame.has_value())
return run_continue;
return renderer.end_frame({ cmd_bufs[*current_frame] });
});
frame.add_run_end([&]() {
clean_cmd_bufs();
renderer.destroy();
render_target->destroy();
});
return frame.run();
Welcome on Planet Vulkan - That's a lot to display a colored window
Take a closer look at the build_cmd_bufs function:
- We create a command pool + command buffers for each frame of the render target.
- And set the command buffers to clear the frame image with some random color.
clean_cmd_bufs frees and destroys all buffers in the command pool.
In case of swap chain restoration we simply recreate command buffers with a new random color - This happens for example when the
window
gets resized.
The flag VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT specifies the usage of command buffers in such a way that they can no longer be changed - Therefore it is a very static example. Vulkan supports a more dynamic and common usage by resetting a command pool before recording new commands.
Ok, it's time for lava block.
lava::block block;
if (!block.create(device, frame_count, device->graphics_queue().family))
return error::create_failed;
block.add_command([&](VkCommandBuffer cmd_buf) {
VkClearColorValue const clear_color = {
random(1.f), random(1.f), random(1.f), 0.f
};
VkImageSubresourceRange const image_range{
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
.levelCount = 1,
.layerCount = 1,
};
VkImage frame_image = render_target->get_image(block.get_current_frame());
insert_image_memory_barrier(device,
cmd_buf,
frame_image,
VK_ACCESS_MEMORY_READ_BIT,
VK_ACCESS_TRANSFER_WRITE_BIT,
VK_IMAGE_LAYOUT_UNDEFINED,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT,
image_range);
device->call().vkCmdClearColorImage(cmd_buf,
frame_image,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
&clear_color,
1,
&image_range);
insert_image_memory_barrier(device,
cmd_buf,
frame_image,
VK_ACCESS_TRANSFER_WRITE_BIT,
VK_ACCESS_MEMORY_READ_BIT,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,
image_range);
});
This is much more simpler than before!
➜ We create a block
with a command
that clears the current frame image.
All we need to do now is to process that block
in the run loop:
if (!block.process(*current_frame))
return run_abort;
return renderer.end_frame(block.collect_buffers());
And call the renderer
with our recorded command buffers.
Don't forget to clean it up when the run ends:
block.destroy();
lava app supports Dear ImGui for tooling and easy prototyping.
int main(int argc, char* argv[]) {
lava::app app("demo", { argc, argv });
if (!app.setup())
return error::not_ready;
app.imgui.layers.add("demo window", []() {
ImGui::ShowDemoWindow();
});
return app.run();
}
What's next? ➜ Check some demo or use the template and start coding...
Before you create new objects or use existing ones, you should get familiar with the lifetime of objects.
It is basically possible to create all objects in liblava on the stack or on the heap.
But be careful. You have to take care of the lifetime yourself.
make ➜ create ➜ destroy
This is the general pattern that is used in this library:
- make Use constructor or factory method (static function to get a shared pointer)
- create Build the respective object
- destroy Discard it after your use
The destructor calls the destroy method if it was not called before.
void use_buffer_on_stack() {
buffer buf; // make
auto created = buf.create(device, data, size, usage);
if (created) {
// ...
buf.destroy();
}
}
Or look at this method where it is returned as a shared pointer:
buffer::s_ptr use_buffer_on_heap() {
auto buf = buffer::make();
if (buf->create(device, data, size, usage))
return buf;
return nullptr;
}
liblava provides a mesh
struct that contains a list of vertices and optionally a list of indices.
It is made this way:
mesh::s_ptr my_mesh = mesh::make();
my_mesh->add_data( /* Pass in a lava::mesh_data object */ );
my_mesh->create(device);
liblava prepares a create_mesh()
function to simplify the creation of primitives.
It takes a mesh_type
argument to specify what kind of primitive to build:
cube
triangle
quad
hexagon
none
The function is called in this way:
mesh::s_ptr cube;
cube = create_mesh(device, mesh_type::cube);
By default, vertices in a mesh
are of type vertex
which has the following layout:
struct vertex {
v3 position;
v4 color;
v2 uv;
v3 normal;
}
Meshes are templated and can represent any vertex struct definition, like here:
struct int_vertex {
std::array<i32, 3> position;
v4 color;
};
mesh_template<int_vertex>::s_ptr int_triangle;
create_mesh()
can generate primitives for arbitrary vertex structs too. Provided that the struct contains an array or vector member named position
:
int_triangle = create_mesh<int_vertex>(device, mesh_type::triangle);
create_mesh()
may also initialize Color, Normal, and UV data automatically.
However, it will only initialize these if there are corresponding color
, normal
, and/or uv
fields defined in the vertex struct.
By default, it will initialize everything automatically. But if generating any of this data is not desired, the fields can be individually disabled by template arguments in this order:
- Color
- Normal
- UV
struct custom_vertex {
v3 position;
v3 color;
v3 normal;
v2 uv;
};
mesh_template<custom_vertex>::s_ptr triangle;
// Generate three vertices with positions and uvs, but not colors or normals
triangle = create_mesh<custom_vertex, false, false, true>
(device, mesh_type::triangle);
Cubes generated this way have a special case. If they are initialized with normal data, they will be represented by 24 vertices. Otherwise, only 8 vertices will be initialized.
Run the lava
executable to test our Tutorial examples ➜ so called stages.
lava -ls
lava --stages
- frame
- run loop
- window input
- clear color
- color block
- imgui demo
- forward shading
- gamepad
The last stages in this list are further examples.
lava -st=6
lava --stage=6
If you run lava
without arguments - the stage driver is started.
In addition run lava-test
to check some unit tests with Catch2
Put your code in the src/
folder and begin to code in main.cpp
You can change the project name in CMake ➜ LIBLAVA_TEMPLATE_NAME
cmake -DLIBLAVA_TEMPLATE_NAME="My-Project" ..
lava defines some shortcuts for common actions:
shortcut | action | default | config.json |
---|---|---|---|
alt + enter | fullscreen | off | window/fullscreen |
alt + backspace | v-sync | off | app/v-sync |
control + tab | imgui | on | app/imgui |
control + space | pause | off | app/paused |
control + ^ | hud menu | off | |
control + b | benchmark | ||
control + p | screenshot | ||
control + q | quit |
You can disable these actions by simply turning them off:
app.config.handle_key_events = false;
--clean, -c
- clean preferences folder
--clean_cache, -cc
- clean cache folder
--v_sync={0|1}, -vs={0|1}
- 0 vertical sync off
- 1 vertical sync on
--triple_buffering={0|1}, -tb={0|1}
- 0 triple buffering off
- 1 triple buffering on
--fps_cap={n}, -fps={n}
- n frames per second cap disable: n = 0
--physical_device={n}, -pd={n}
- n physical device index default: n = 0
--identification={str}, -id={str}
- str config save name for example: "test profile"
--resource={str}, -res={str}
- str resource file or path (relative to app directory) for example: mod.zip
--paused={0|1}, -p={0|1}
- 0 running
- 1 paused
--delta={n}, -dt={n}
- n fixed delta in milliseconds disable: n = 0
--speed={n}, -s={n}
- n runtime speed default: n = 1.0
--imgui={0|1}, -ig={0|1}
- 0 hide imgui
- 1 show imgui
--fullscreen={0|1}, -wf={0|1}
- 0 windowed mode
- 1 fullscreen mode
--x_pos={n}, -wx={n}
- n window x position
--y_pos={n}, -wy={n}
- n window y position
--width={n}, -ww={n}
- n window width
--height={n}, -wh={n}
- n window height
--center, -wc
- center window on the monitor
--title, -wt
- show window save title
You need the Vulkan SDK installed for debugging.
--debug, -d
- enable validation layer VK_LAYER_KHRONOS_validation
--utils, -u
- enable debug utils extension VK_EXT_debug_utils
--renderdoc, -r
- enable RenderDoc capture layer VK_LAYER_RENDERDOC_Capture
--log={0|1|2|3|4|5|6}, -l={0|1|2|3|4|5|6}
- level 0 trace verbose logging
- level 1 debug
- level 2 info
- level 3 warn
- level 4 error
- level 5 critical
- level 6 off logging disabled
lava app writes frame times (durations in milliseconds) into a json
file to analyze them further for automated workflows like benchmarks:
{
"benchmark": {
"avg": 16.02839111337229,
"count": 622,
"max": 45,
"min": 12,
"offset": 5000,
"time": 10000
},
"frames": [
12,
14,
16,
16
],
"timestamps": [
5,
17,
31,
47,
63
]
}
--benchmark, -bm
- activate benchmark mode
--benchmark_time={n}, -bmt={n}
- n benchmark duration in milliseconds default: n = 10000 ms
--benchmark_offset={n}, -bmo={n}
- n warm up time in milliseconds default: n = 5000 ms
--benchmark_file={str}, -bmf={str}
- str output file default: str = benchmark.json
--benchmark_path={str}, -bmp={str}
- str output path default: preferences folder
--benchmark_exit={0|1}, -bmx={0|1}
- 0 keep running after benchmark
- 1 close app after benchmark default
--benchmark_buffer={n}, -bmb={n}
- n pre-allocated buffer size for results default: n = 100000
- C++23 compatible compiler
- CMake 3.27+
- Python 3 for utility scripts
- Vulkan SDK for debugging only
git clone https://github.com/liblava/liblava.git
cd liblava
mkdir build
cd build
cmake ..
cmake --build . --parallel
Problems building or running on Ubuntu? - Try this:
sudo apt-get install libxi-dev libatomic-ops-dev libatomic1
You can use liblava as a git submodule in your project:
git submodule add https://github.com/liblava/liblava.git
Add this to your CMakeLists.txt
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/liblava ${CMAKE_CURRENT_BINARY_DIR}/liblava)
...
target_link_libraries(${PROJECT_NAME} PRIVATE lava::engine ${LIBLAVA_ENGINE_LIBRARIES})
Alternatively ➜ compile and install a specific version for multiple projects:
mkdir build
cd build
cmake -D CMAKE_BUILD_TYPE=Release -D CMAKE_INSTALL_PREFIX=../lava-install ..
cmake --build . --config Release --target install --parallel
First find the package in your CMakeLists.txt
find_package(lava 0.8.1 REQUIRED)
...
target_link_libraries(${PROJECT_NAME} PRIVATE lava::engine ${LIBLAVA_ENGINE_LIBRARIES})
And then build your project with install path ➜ lava_DIR
mkdir build
cd build
cmake -D lava_DIR=path/to/lava-install/lib/cmake/lava ..
cmake --build . --parallel
Vcpkg integration with 2 options ➜ use this registry and port
If you are familiar with Conan ➜ build this package recipe
- argh Argh! A minimalist argument handler 3-clause BSD
- Catch2 A modern, C++-native, header-only, test framework for unit-tests, TDD and BDD BSL 1.0
- CPM.cmake A small CMake script for setup-free, cross-platform, reproducible dependency management MIT
- glfw A multi-platform library for OpenGL, OpenGL ES, Vulkan, window and input zlib
- gli OpenGL Image (GLI) MIT
- glm OpenGL Mathematics (GLM) MIT
- glslang Khronos-reference front end for GLSL/ESSL, partial front end for HLSL, and a SPIR-V generator 3-clause BSD
- IconFontCppHeaders C, C++ headers and C# classes for icon fonts zlib
- imgui Dear ImGui - Bloat-free Graphical User interface for C++ with minimal dependencies MIT
- json JSON for Modern C++ MIT
- physfs A portable, flexible file i/o abstraction zlib
- PicoSHA2 A header-file-only SHA256 hash generator in C++ MIT
- shaderc A collection of tools, libraries, and tests for Vulkan shader compilation Apache 2.0
- spdlog Fast C++ logging library MIT
- SPIRV-Headers SPIRV Headers MIT
- SPIRV-Tools SPIRV Tools Apache 2.0
- stb Single-file public domain libraries for C/C++ MIT
- tinyobjloader Tiny but powerful single file wavefront obj loader MIT
- volk Meta loader for Vulkan API MIT
- Vulkan-Headers Vulkan Header files and API registry Apache 2.0
- VulkanMemoryAllocator Easy to integrate Vulkan memory allocation library MIT
You can find the demonstration projects in the liblava-demo/
folder.
- Roboto ➜ Roboto-Regular.ttf Website • GitHub Apache License, Version 2.0
- Font Awesome ➜ fa-solid-900.ttf Website • GitHub Font Awesome Free License
- Barbarella ➜ lamp.frag Website Shader by Weyland Yutani
- Spawn Model ➜ lava-spawn-game.obj + lava-spawn-game.mtl Website CC BY-SA 3.0
- Mationi - Colored Border ➜ demo.frag Website Shader by juanpetrik
- Doxygen Generate documentation from source code GPL-2.0
- Doxybook2 Doxygen XML to Markdown (or JSON) MIT
- docsify A magical documentation site generator MIT
liblava Documentation is licensed under a Creative Commons Attribution 4.0 International License.