Table of Contents
A modern and easy-to-use library for the Vulkan® API
lava
is a lean framework that provides essentials for low-level graphics - specially well suited for prototyping, tooling, profiling and education. This library is written in modern C++20 and strives for a modular rolling release as far as possible. We don't want to promise too much, but it runs really smoothly on Windows and Linux.
➜ Download • Introduction • Guide • Build • Collaborate • Third-Party
Modules
deferred shading + offscreen rendering | uniform buffer + camera |
---|---|
push constants ➜ shader | generating primitives |
---|---|
float, double & int meshes | unique classic mesh |
---|---|
raytraced reflecting cubes | free download on itch.io ➜ demo collection |
---|---|
- written in modern C++ with latest Vulkan support
- run loop abstraction for window and input handling
- plain renderer and command buffer model
- texture and mesh loading from virtual file system
- runtime shader compilation included
- camera, imgui, logger and more...
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.
New to Vulkan? ➜ Take a look at this Vulkan Guide
Check Awesome Vulkan ecosystem for tutorials, samples and books.
#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 with command line arguments.
lava::frame frame(argh);
if (!frame.ready())
return error::not_ready;
ui32 count = 0;
frame.add_run([&](id::ref run) {
sleep(one_second);
count++;
log()->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 and handle
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) {
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_p device = frame.platform.create_device();
if (!device)
return error::create_failed;
lava::render_target::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) {
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 and command buffers for each frame of the render target
- And set each command buffer to clear the frame image with some random color
clean_cmd_bufs
frees all buffers and destroys the command pool.
In case of swap chain restoration we simply recreate command buffers with a new random color - This happens for example on
window
resize.
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 - And therefore it is a very static example.
Vulkan supports a more dynamic and common usage by resetting a command pool before recording new commands.
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.get_buffers());
And call the renderer
with our recorded command buffers.
Don't forget to clean it up when the run ends:
block.destroy();
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.on_draw = []() {
ImGui::ShowDemoWindow();
};
return app.run();
}
What's next? ➜ Check some demo or use the template to try it out!
- Lifetime of an Object
- Making Meshes
- Modules
- Reference
- Test
- Keyboard Shortcuts
- Command-Line Arguments
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::ptr use_buffer_on_heap() {
auto buf = make_buffer();
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::ptr my_mesh = make_mesh();
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::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>::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>::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.
lava engine
require app
lava app
lava block
require base
lava frame
require resource
lava asset
lava resource
require base
lava base
require util
lava file
require util
lava util
require core
lava core
To generate the documentation with Doxygen run:
doxygen tool/doxygen.conf
Here you can find the latest ➜ doc.lava-block.com
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
Here you can find the complete source code - The last stages in this list are further examples
lava -s=3
lava --stage=3
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" ..
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 + 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
--physical_device={n}, -pd={n}
- n physical device index default: n = 0
--identification={str}, -id={str}
- str config save name supports punctuation marks
--paused={0|1}, -p={0|1}
- 0 running
- 1 paused
--delta={n}, -d={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
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
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
Need help? Please feel free to ask us on ➜ Discord
- C++20 compatible compiler
- CMake 3.22+
- 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
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.7.3 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
Use the issue tracker to report any bug or compatibility issue.
❤️ Thanks to all contributors making liblava flow...
If you want to contribute - we suggest the following:
- Fork the official repository
- Apply your changes to your fork
- Submit a pull request describing the changes you have made
help maintenance and further development | every star and follow motivates |
You can update all external modules by running the script:
python tool/update.py > tool/version.json
-
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
-
Vulkan-Profiles Vulkan Profiles Tools Apache 2.0
-
VulkanMemoryAllocator Easy to integrate Vulkan memory allocation library MIT
You can find the demonstration projects in the liblava-demo
folder.
- Roboto ➜ Website • GitHub Apache License, Version 2.0 Roboto-Regular.ttf
- Font Awesome ➜ Website • GitHub Font Awesome Free License fa-solid-900.ttf
- Barbarella ➜ Website Shader by Weyland Yutani lamp.frag
- Spawn Model ➜ Website CC BY-SA 3.0 lava-spawn-game.mtl • lava-spawn-game.obj
- Mationi - Colored Border ➜ Website Shader by juanpetrik demo.frag
liblava is licensed under MIT License which allows you to use the software
for any purpose you might like - including commercial and for-profit use.
However - this library includes several Third-Party libraries which are licensed under their own respective Open Source licenses ➜ They all allow static linking with closed source software.
All copies of liblava must include a copy of the MIT License terms and the copyright notice.
Vulkan and the Vulkan logo are trademarks of the Khronos Group Inc.
Copyright (c) 2018-present • Lava Block OÜ and contributors