This is a library to reduce the amount of boilerplate code in Java Vulkan projects,
as well as adding several optional abstractions. This library intentionally
does not hide any of its handles (e.g. the VkDevice
) from you, to ensure
that it does not take any power away from you.
The functionality of vk-boiler
can be subdivided in roughly 3
topics:
This is best illustrated with an example. Consider the task of creating a one-time-submit command pool and command buffer. Without any boilerplate reduction, it would probably look like this:
try (var stack = stackPush()) {
var ciCommandPool = VkCommandPoolCreateInfo.calloc(stack);
ciCommandPool.sType$Default();
ciCommandPool.flags(flags);
ciCommandPool.queueFamilyIndex(queueFamilyIndex);
var pCommandPool = stack.callocLong(1);
assertVkSuccess(vkCreateCommandPool(
instance.vkDevice(), ciCommandPool, null, pCommandPool
));
var commandPool = pCommandPool.get(0);
var aiCommandBuffer = VkCommandBufferAllocateInfo.calloc(stack);
aiCommandBuffer.sType$Default();
aiCommandBuffer.commandPool(commandPool);
aiCommandBuffer.level(VK_COMMAND_BUFFER_LEVEL_PRIMARY);
aiCommandBuffer.commandBufferCount(1);
var pCommandBuffer = stack.callocPointer(amount);
assertVkSuccess(vkAllocateCommandBuffers(
instance.vkDevice(), aiCommandBuffer, pCommandBuffer
));
var commandBuffer = new VkCommandBuffer(pCommandBuffer.get(0), vkDevice)
}
Instead of writing all this, you could also write this:
var commandPool = boiler.commands.createPool(
0, boiler.queueFamilies().graphics().index(), "Copy"
);
var commandBuffer = boiler.commands.createPrimaryBuffers(
commandPool, 1, "Copy"
)[0];
Or this
var commands = new SingleTimeCommands(boiler);
This library provides plenty of such convenience methods. By using them all whenever possible, you can dramatically reduce the amount of code you need (at least, I did when I started using this in my own projects).
vk-boiler
doesn't have a convenience method for everything that is possible with
Vulkan. For instance, when
- I couldn't find a way to simplify them in a meaningful way (e.g. render passes)
- The use case is niche (e.g. 1d and 3d images)
- When I simply didn't encounter the need in my own projects (pull requests are welcome in this case)
When this library doesn't provide a nice method, you will need to do it the old way:
write all the code yourself. This should normally not be a problem since vk-boiler
doesn't hide your Vulkan handles from you.
Bootstrapping Vulkan applications can be very verbose (easily hundreds of lines of code). You need to:
- Query supported instance extensions and layers
- Create the instance
- Query physical devices, their properties, limits, extensions...
- Choose the best physical device (or abort execution)
- Query queue families
- Choose which queues you want to create
- Create the (logical) device
- And if you want to create a window as well, you need to take special care in several of these steps...
Most of this work is very similar for almost all applications. To improve this
situation, vk-boiler
provides a BoilerBuilder
class that can be used to
do all of this with potentially very little code. It could be as simple as
this:
var boiler = new BoilerBuilder(
VK_API_VERSION_1_1, "SimpleRingApproximation", VK_MAKE_VERSION(0, 2, 0)
)
.validation()
.enableDynamicRendering()
.addWindow(new WindowBuilder(1000, 8000, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT))
.build();
See the complete initialization documentation for more information.
Almost every Vulkan application needs to do swapchain management, but the code to accomplish this is usually the same. Furthermore, swapchain management can be quite complicated (for instance, when can you safely destroy your swapchain? and did you know that Wayland swapchain images never become out of date?).
vk-boiler
provides a swapchain management system
that you can use to avoid rewriting swapchain management for every single
application, and to avoid some of the aforementioned complexity.
Note that the whole swapchain manager is lazy, so it won't allocate/create anything if you don't use it.
This repository has a samples
folder containing example applications that
demonstrate how you can use vk-boiler
effectively. (In fact, many code
snippets in this README
are copied from these projects.) The basic usage
comes down to this:
var boiler = new BoilerBuilder(
VK_API_VERSION_1_0, "Just an example", VK_MAKE_VERSION(1, 0, 0)
)
// Configure window, validation, features, and extensions
.build();
// Use the convenience methods like boiler.buffers.create(...)
// Render loop, for instance SimpleWindowRenderLoop
boiler.destroyInitialObjects()
To learn about the rest of the features, just check the code completion suggestions :)
This project requires Java 17 or later (Java 17 and 21 are tested in CI).
While this project is compiled against LWJGL, it does not bundle LWJGL, so you
still need to declare the LWJGL dependencies yourself
(hint: use LWJGL customizer). This approach
allows you to control which version of LWJGL you want to use (as long as its
compatible with the version used by vk-boiler
).
...
repositories {
...
maven { url 'https://jitpack.io' }
}
...
dependencies {
...
implementation 'com.github.knokko:vk-boiler:v4.4.0'
}
...
<repositories>
...
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
...
<dependency>
<groupId>com.github.knokko</groupId>
<artifactId>vk-boiler</artifactId>
<version>v4.4.0</version>
</dependency>
Most vk-boiler
methods use VulkanFailureException.assertVkSuccess(...)
to test
the return values of all Vulkan function calls. This will throw a
VulkanFailureException
when the return value is not VK_SUCCESS
(or another
acceptable return code).
The build()
method of BoilerBuilder
is a special method that can throw
several other exception types as well. See its doc comments for more information.
All exceptions provided by vk-boiler
extend RuntimeException
, so handling them
is completely optional. If you want a resilient application, you should probably
catch at least VulkanFailureException
and try to recreate the boiler
when an
unrecoverable error (like VK_ERROR_DEVICE_LOST
) occurs. If you don't want anything
fancy, you can also just catch it, save user progress, and exit.