Simple and efficient Vulkan loading with flextGL

Play­ing with Vulkan but don’t want to in­clude thou­sands lines of var­i­ous head­ers just to call a few func­tions? FlextGL just learned Vulkan sup­port and it’s here to speed up your turn­around times.

If you don’t know what flextGL is, it’s a func­tion load­er gen­er­a­tor for OpenGL, OpenGL ES and now al­so Vulkan. In com­par­i­son to GLEW, GL3W, GLAD, glLoad­Gen and all oth­er func­tion point­er load­ers it al­lows you to pro­vide a tem­plate and a whitelist of ver­sions, ex­ten­sions and func­tions to load, so you can load what you want, how­ev­er you want.

Chances are you’re us­ing flextGL for func­tion point­er load­ing in your GL / GLES code, so now you can use the same tool for your Vulkan back­end as well.

How?

FlextGL con­tains a builtin Vulkan tem­plate that you can use to gen­er­ate a ba­sic load­er. In ad­di­tion you need Python 3 and Wheezy Tem­plate:

pip3 install wheezy.template
git clone git://github.com/mosra/flextGL --branch vulkan

De­sired Vulkan ver­sion, ex­ten­sions and an op­tion­al func­tion white- or black­list is spec­i­fied us­ing a pro­file file:

version 1.0 vulkan

# Extensions to include on top of the core functionality. The VK_ prefix
# is omitted.
extension KHR_swapchain optional
extension KHR_maintenance1 optional
# ...

# Function whitelist. If you omit this section, all functions from the
# above version and extensions will be pulled in. The vk prefix is omitted.
begin functions
    CreateInstance
    EnumeratePhysicalDevices
    GetPhysicalDeviceProperties
    GetPhysicalDeviceQueueFamilyProperties
    GetPhysicalDeviceMemoryProperties
    # ...
end functions

# You can also choose to have a function blacklist instead, delimited by
# begin functions blacklist and end functions blacklist.

With a pro­file file you can then gen­er­ate the Vulkan load­er head­er + source file like this. The generated/ di­rec­to­ry will then con­tain a pair of flextVk.h and flextVk.cpp files:

./flextGLgen.py -D generated -t vulkan profile.txt

Now you can just in­clude it and use. The ac­tu­al func­tion point­er load­ing is done by call­ing flextVkInit() and af­ter that all Vulkan func­tions are avail­able glob­al­ly:

#include "flextVk.h"

int main() {
    /* Create an instance, load function pointers */
    VkInstance instance;
    {
        VkInstanceCreateInfo info{};
        info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
        vkCreateInstance(&info, nullptr, &instance);
    }
    flextVkInit(instance);

    VkPhysicalDevice physicalDevices[5];
    std::uint32_t count = 5;
    vkEnumeratePhysicalDevices(instance, &count, &physicalDevice);
    ...
}

Why both­er?

Com­pared to OpenGL, Vulkan is still do­ing ba­by steps, how­ev­er the amount of avail­able ex­ten­sions is grow­ing at an alarm­ing rate and soon the size of stock “all you can eat” head­ers will have a sig­nif­i­cant im­pact on your build times. Be­cause Vulkan API is more about var­i­ous types than just func­tion point­ers, flextGL en­sures that on­ly the struc­tures, enums and de­fines that are ac­tu­al­ly ref­er­enced by func­tions are pulled in, to shrink head­er sizes even fur­ther.

So, let’s have some mea­sure­ments!

Head­er sizes

The fol­low­ing ta­ble com­pares raw line count and line count of pre­pro­cessed out­put when us­ing var­i­ous Vulkan load­ers, gen­er­at­ed by the fol­low­ing two com­mands us­ing GCC 7.3.1 for Vulkan 1.1.74:

wc -l /path/to/header

echo "#include <header>" | g++ -std=c++11 -E -x c++ - | wc -l
Head­er Line count Af­ter pre­pro­cess­ing
#include "flextVk.h" 1 1 710 1 929
#include <MagnumExternal/Vulkan/flextVk.h> 2 3 577 3 592
#include "volk.h" 837 3 6 352
#include <vulkan/vulkan.h> 4 7 470 7 363
#include <vulkan/vulkan.hpp> 5 42 544 ! 83 530 !!
#include <GL/glew.h> (for com­par­i­son) 23 686 ?! 7 464
1
A min­i­mal gen­er­at­ed head­er whitelist­ing on­ly func­tions re­quired to build my First Tri­an­gle in Vulkan. The pro­file file used to gen­er­ate the head­er is in­clud­ed in the gist.
2
Ex­per­i­men­tal Vulkan head­er in­clud­ed in lat­est Mag­num mas­ter, in­clud­ing ev­ery­thing from Vulkan 1.1 + all ex­ten­sions that were pro­mot­ed to 1.1 for back­wards com­pat­i­bil­i­ty
3
The Volk meta-load­er. While small on its own, it de­pends on the stock vulkan.h for all type and enum def­i­ni­tions
4
The stock Vulkan head­er pro­vides on­ly func­tion point­er type­defs, not ac­tu­al func­tions, so can’t be used as-is. The vulkan.h head­er it­self has on­ly 79 lines, this counts lines of vulkan_core.h.
5
vulkan.hpp, aim­ing to pro­vide C++11 head­er-on­ly Vulkan “bind­ings” with bet­ter type safe­ty. But, look at those num­bers, se­ri­ous­ly, don’t use this thing. Please.

Com­pile times

I abused Cor­rade::Test­Suite and std::system() a bit to bench­mark how long it takes GCC to com­pile each case from the above ta­ble in­to an ex­e­cutable that cre­ates the Vulkan in­stance and pop­u­lates func­tion point­ers us­ing giv­en load­er. On­ly com­pi­la­tion of the ac­tu­al main file is mea­sured, ex­clud­ing time need­ed to com­pile ex­tra *.cpp, *.c or *.so files, be­cause their cost is usu­al­ly amor­tized in the project. Here are the re­sults (hov­er over the bars to get the con­crete val­ues):

62.69 ± 0.84 ms 69.98 ± 2.04 ms 74.78 ± 3.65 ms 76.76 ± 3.34 ms 719.71 ± 6.95 ms 0 100 200 300 400 500 600 700 ms flextVk minimal flextVk Magnum Volk vulkan.h vulkan.hpp 1929 lines 3592 lines 6352 lines 7363 lines 83530 lines Compile time

As ex­pect­ed, vulkan.hpp takes an in­sane amount of time to com­pile — ten times as much as the oth­ers, al­most a sec­ond — and this is for ev­ery file that (tran­si­tive­ly) in­cludes it! The com­pile time rough­ly cor­re­sponds to pre­pro­cessed line count from the above ta­ble, with flextGL-gen­er­at­ed head­ers be­ing the small­est and fastest to com­pile.

As is usu­al, the head­ers usu­al­ly get tran­si­tive­ly in­clud­ed in­to ma­jor­i­ty of a project, so sav­ing 15 mil­lisec­onds per file when go­ing from stock head­ers to flextGL-gen­er­at­ed ones can save you 15 sec­onds in mod­er­ate­ly sized project hav­ing 1000 tar­gets. And this gap will be in­creas­ing as more ex­ten­sions get added to the stock head­ers.

Run­time cost

Be­cause flextGL loads on­ly the func­tions you ac­tu­al­ly re­quest­ed in­stead of ev­ery­thing that any­body could ev­er need, it has al­so some im­pact on start­up time. The fol­low­ing bench­mark mea­sures the time it takes to call load­er-spe­cif­ic ini­tial­iza­tion func­tions. The vulkan.h and vulkan.hpp head­ers aren’t in­clud­ed, be­cause these re­ly on ex­ter­nal func­tion point­er load­ing and don’t do any on their own.

15.09 ± 0.74 µs 84.98 ± 3.65 µs 197.13 ± 9.45 µs 934.27 ± 25.66 µs 0 200 400 600 800 1000 µs flextVk minimal flextVk Magnum Volk vkCreateInstance() 49 ptrs 192 ptrs 302 ptrs (for comparison) Runtime cost

Again, the mea­sured time cor­re­sponds to ac­tu­al amount of load­ed func­tion point­ers. The Vulkan Tri­an­gle needs just 49 func­tion point­ers, Mag­num loads ev­ery­thing from Vulkan 1.1 to­geth­er with com­mand alias­es from pro­mot­ed ex­ten­sions, while Volk adds al­so all known ex­ten­sions. How­ev­er, note that these are mi­crosec­onds — and com­pared to time that’s need­ed to cre­ate a Vulkan in­stance (last mea­sure­ment), the sav­ings are on­ly very mi­nor.

Vulkan load­ing in Mag­num

As of mosra/mag­num@b137703, Mag­num ships flextGL-gen­er­at­ed Vulkan head­ers. To save on del­e­ga­tion over­head, the de­ci­sion was to load per-de­vice func­tion point­ers in­stead of go­ing through per-in­stance func­tion point­ers for ev­ery­thing — that’s al­so what Volk does with great suc­cess, sav­ing as much as 5% to 10% of driv­er over­head, de­pend­ing on the work­flow.

Be­sides that, load­ed Vulkan func­tions are not glob­al by de­fault in or­der to sup­port mul­ti­ple co­ex­ist­ing Vulkan in­stances:

#include <MagnumExternal/Vulkan/flextVk.h>

int main() {
    /* Create an instance */
    VkInstance instance;
    {
        VkInstanceCreateInfo info{};
        info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
        // ...
        vkCreateInstance(&info, nullptr, &instance);
    }

    /* Load per-instance function pointers */
    FlextVkInstance i;
    flextVkInitInstance(instance, &i);

    /* Create a device */
    VkPhysicalDevice physicalDevice;
    {
        uint32_t count = 1;
        i.EnumeratePhysicalDevices(instance, &count, &physicalDevice);
    }
    VkDevice device;
    {
        VkDeviceCreateInfo info{};
        info.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
        // ...
        i.CreateDevice(physicalDevice, &info, nullptr, &device);
    }

    /* Load per-device function pointers */
    FlextVkDevice d;
    flextVkInitDevice(device, &d, i.GetDeviceProcAddr);

    // ...
}

In the above snip­pet, the i and d struc­tures con­tain all load­ed func­tion point­ers. So in­stead of vkCreateBuffer(device, ...) you’d write d.Createbuffer(device, ), for ex­am­ple. While this is prop­er­ly de­cou­pled, it might get in the way when just play­ing around or adapt­ing sam­ple code. For that rea­son, Mag­num pro­vides opt-in glob­al func­tion point­ers as well — just in­clude flextVkGlobal.h in­stead of flextVk.h and load your point­ers glob­al­ly:

#include <MagnumExternal/Vulkan/flextVkGlobal.h>

int main() {
    /* Create an instance */
    VkInstance instance;
    {
        VkInstanceCreateInfo info{};
        info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
        // ...
        vkCreateInstance(&info, nullptr, &instance);
    }

    /* Load per-instance function pointers globally */
    flextVkInitInstance(instance, &flextVkInstance);

    /* Create a device */
    VkPhysicalDevice physicalDevice;
    {
        uint32_t count = 1;
        vkEnumeratePhysicalDevices(instance, &count, &physicalDevice);
    }
    VkDevice device;
    {
        VkDeviceCreateInfo info{};
        info.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
        // ...
        vkCreateDevice(physicalDevice, &info, nullptr, &device);
    }

    /* Load per-device function pointers globally */
    flextVkInitDevice(device, &flextVkDevice, vkGetDeviceProcAddr);

    // ...
}

In this case flextVkInitInstance() and flextVkInitDevice() will load the point­ers in­to glob­al flextVkInstance and flextVkDevice struc­tures, which then are alias­es to glob­al vk*() func­tions.

Both ap­proach­es can co­ex­ist, just be sure that you call in­stance-/de­vice-spe­cif­ic func­tions on the in­stance/de­vice that they were queried from and ev­ery­thing will work well.

~ ~ ~

And that’s it! Check Vulkan sup­port in flextGL out and please re­port bugs, if you find any. Thanks for read­ing, I’ll be back soon!

Magnum 2018.02 released

The new Mag­num mile­stone brings We­bGL 2.0 and We­bAssem­bly, VR sup­port, lots of niceties for Win­dows users, iOS port, new ex­per­i­men­tal UI li­brary, im­proved test­ing ca­pa­bil­i­ties, sup­port for over 80 new as­set for­mats, new ex­am­ples and much more.

Introducing Guest Posts

One of the goals while build­ing the new Mag­num web­site was to low­er the bar­ri­er for con­tribut­ing con­tent. With Git and GitHub it’s al­ready very easy to con­trib­ute code to the project it­self, so why not ex­tend that to the web­site as well?

page 1 | older articles »