/liblava

Modern and easy-to-use library for Vulkan

Primary LanguageC++MIT LicenseMIT

Table of Contents


A modern and easy-to-use library for the Vulkan® API


version License CodeFactor Discord Donate Twitter Follow

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

core util file base resource asset frame block app engine



Demos

deferred shading + offscreen rendering uniform buffer + camera
light spawn
light spawn

push constants ➜ shader generating primitives
lamp shapes
lamp shapes

float, double & int meshes unique classic mesh
generics triangle
generics triangle


Projects

raytraced reflecting cubes free download on itch.io ➜ demo collection
demo
rt cubes lava demo


In a nutshell

  • 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...

Introduction


Tutorial

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


1. frame

int main(int argc, char* argv[]) {

    lava::frame frame( {argc, argv} );
    
    return frame.ready() ? 0 : error::not_ready;
}

This is how to initialize frame with command line arguments.


2. run loop

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.


3. window input

Here is another example that shows how to create window and handle 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) {
    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...


4. clear color

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.


Ok, it's time for block


5. color 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.get_buffers());

And call the renderer with our recorded command buffers.


Don't forget to clean it up when the run ends:

block.destroy();

6. imgui demo

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.on_draw = []() {

        ImGui::ShowDemoWindow();
    };

    return app.run();
}


What's next? ➜ Check some demo or use the template to try it out!



Guide

  1. Lifetime of an Object
  2. Making Meshes
  3. Modules
  4. Reference
  5. Test
  6. Keyboard Shortcuts
  7. Command-Line Arguments

1. Lifetime of an Object

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:

  1. make   Use constructor or factory method (static function to get a shared pointer)
  2. create   Build the respective object
  3. destroy   Discard it after your use

The destructor calls the destroy method if it was not called before.


Example: buffer object

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;
}

2. Making Meshes

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:

  1. Color
  2. Normal
  3. 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.


3. Modules

lava engine

require app

engine producer property

lava app

require block + frame + asset

app camera forward_shading

benchmark config imgui

lava block

require base

attachment block descriptor render_pass subpass

compute_pipeline graphics_pipeline pipeline pipeline_layout

lava frame

require resource

argh driver frame gamepad input

render_target renderer swapchain window

lava asset

require resource + file

image_loader load_mesh load_texture write_image

require base

buffer mesh primitive

format image texture

lava base

require util

base instance profile memory

platform device physical_device queue

lava file

require util

file file_system file_utils json_file json

lava util

require core

log random telegram thread utility

lava core

data hex id math time types version


4. Reference

To generate the documentation with Doxygen run:

doxygen tool/doxygen.conf

Here you can find the latestdoc.lava-block.com


5. Test

Run the lava executable to test our Tutorial examples ➜ so called stages.


List all stages

lava -ls

lava --stages
  1. frame
  2. run loop
  3. window input
  4. clear color
  5. color block
  6. imgui demo
  7. forward shading
  8. gamepad

Here you can find the complete source code - The last stages in this list are further examples


Run example window input

lava -s=3

lava --stage=3

If you run lava without arguments - the stage driver is started.


Unit testing

In addition run lava-test to check some unit tests with Catch2


Template

Put your code in the src folder and begin to code in main.cpp


You can change the project name in CMakeLIBLAVA_TEMPLATE_NAME

cmake -DLIBLAVA_TEMPLATE_NAME="My-Project" ..

6. Keyboard Shortcuts

app 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;

7. Command-Line Arguments

app

--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

Benchmark

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

frame

You need the Vulkan SDK installed for debugging.


--debug, -d

--utils, -u

--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



Build

CMake (Linux, Windows)

Requirements

  • 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

Install

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})

Package setup

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

Installing and using Vcpkg

Vcpkg integration with 2 options ➜ use this registry and port


Conan Package Manager

If you are familiar with Conan ➜ build this package recipe



Collaborate

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:

  1. Fork the official repository
  2. Apply your changes to your fork
  3. Submit a pull request describing the changes you have made

Support


paypal GitHub Stars   Twitter URL
help maintenance and further development every star and follow motivates


Third-Party

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

    argh

  • 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

    frame gamepad input window

  • gli   OpenGL Image (GLI)   MIT

    load_texture

  • glm   OpenGL Mathematics (GLM)   MIT

    math

  • 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

  • imgui   Dear ImGui - Bloat-free Graphical User interface for C++ with minimal dependencies   MIT

    imgui

  • json   JSON for Modern C++   MIT

    json

  • physfs   A portable, flexible file i/o abstraction   zlib

    file file_system

  • PicoSHA2   A header-file-only SHA256 hash generator in C++   MIT

    utility

  • shaderc   A collection of tools, libraries, and tests for Vulkan shader compilation   Apache 2.0

    producer

  • spdlog   Fast C++ logging library   MIT

    log

  • SPIRV-Headers   SPIRV Headers   MIT

  • SPIRV-Tools   SPIRV Tools   Apache 2.0

  • stb   Single-file public domain libraries for C/C++   MIT

    image_loader load_texture write_image

  • tinyobjloader   Tiny but powerful single file wavefront obj loader   MIT

    load_mesh

  • volk   Meta loader for Vulkan API   MIT

    base

  • Vulkan-Headers   Vulkan Header files and API registry   Apache 2.0

    base window

  • Vulkan-Profiles   Vulkan Profiles Tools   Apache 2.0

    profile

  • VulkanMemoryAllocator   Easy to integrate Vulkan memory allocation library   MIT

    memory


Demo

You can find the demonstration projects in the liblava-demo folder.




License

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


Contributors