-
Book Overview & Buying
-
Table Of Contents
Vulkan 3D Graphics Rendering Cookbook - Second Edition
By :
As some readers may recall from the first edition of our book, the Vulkan API is significantly more verbose than OpenGL. To make things more manageable, we’ve broken down the process of creating our first graphical demo apps into a series of smaller, focused recipes. In this recipe, we’ll cover how to create a Vulkan instance, enumerate all physical devices in the system capable of 3D graphics rendering, and initialize one of these devices to create a window with an attached surface.
We recommend starting with beginner-friendly Vulkan books, such as The Modern Vulkan Cookbook by Preetish Kakkar and Mauricio Maurer (published by Packt) or Vulkan Programming Guide: The Official Guide to Learning Vulkan by Graham Sellers and John Kessenich (Addison-Wesley Professional).
The most challenging aspect of transitioning from OpenGL to Vulkan—or to any similar modern graphics API—is the extensive amount of explicit code required to set up the rendering process, which, fortunately, only needs to be done once. It’s also helpful to familiarize yourself with Vulkan’s object model. A great starting point is Adam Sawicki’s article, Understanding Vulkan Objects https://gpuopen.com/understanding-vulkan-objects. In the recipes that follow, our goal is to start rendering 3D scenes with the minimal setup needed, demonstrating how modern bindless Vulkan can be wrapped into a more user-friendly API.
All our Vulkan recipes rely on the LightweightVK library, which can be downloaded from https://github.com/corporateshark/lightweightvk using the provided Bootstrap snippet. This library implements all the low-level Vulkan wrapper classes, which we will discuss in detail throughout this book.
{
"name": "lightweightvk",
"source": {
"type": "git",
"url" : "https://github.com/corporateshark/lightweightvk.git",
"revision": "v1.3"
}
}
The complete Vulkan example for this recipe can be found in Chapter02/01_Swapchain.
Before diving into the actual implementation, let’s take a look at some scaffolding code that makes debugging Vulkan backends a bit easier. We will begin with error-checking facilities.
VK_ASSERT() and VK_ASSERT_RETURN() macros, which check the results of Vulkan operations. When starting a new Vulkan implementation from scratch, having something like this in place from the beginning can be very helpful.
#define VK_ASSERT(func) { \
const VkResult vk_assert_result = func; \
if (vk_assert_result != VK_SUCCESS) { \
LLOGW("Vulkan API call failed: %s:%i\n %s\n %s\n", \
__FILE__, __LINE__, #func, \
ivkGetVulkanResultString(vk_assert_result)); \
assert(false); \
} \
}
VK_ASSERT_RETURN() macro is very similar and returns the control to the calling code.
#define VK_ASSERT_RETURN(func) { \
const VkResult vk_assert_result = func; \
if (vk_assert_result != VK_SUCCESS) { \
LLOGW("Vulkan API call failed: %s:%i\n %s\n %s\n", \
__FILE__, __LINE__, #func, \
ivkGetVulkanResultString(vk_assert_result)); \
assert(false); \
return getResultFromVkResult(vk_assert_result); \
} \
}
Now we can start creating our first Vulkan application. Let’s explore what is going on in the sample application Chapter02/01_Swapchain which creates a window, a Vulkan instance and device together with a Vulkan swapchain, which will be explained in a few moments. The application code is very simple:
lvk::createVulkanContextWithSwapchain() helper function, which we will examine shortly.
int main(void) {
minilog::initialize(nullptr, { .threadNames = false });
int width = 960;
int height = 540;
GLFWwindow* window = lvk::initWindow(
"Simple example", width, height);
std::unique_ptr<lvk::IContext> ctx =
lvk::createVulkanContextWithSwapchain(
window, width, height, {});
while (!glfwWindowShouldClose(window)) {
glfwPollEvents();
glfwGetFramebufferSize(window, &width, &height);
if (!width || !height) continue;
lvk::ICommandBuffer& buf = ctx->acquireCommandBuffer();
ctx->submit(buf, ctx->getCurrentSwapchainTexture());
}
IDevice object should be destroyed before the GLFW window.
ctx.reset();
glfwDestroyWindow(window);
glfwTerminate();
return 0;
}
The application should render an empty black window as in the following screenshot:

Figure 2.1: The main loop and swapchain
Let’s explore lvk::createVulkanContextWithSwapchain() and take a sneak peek at its implementation. As before, we will skip most of the error checking in the book text where it doesn’t contribute to the overall understanding:
std::unique_ptr<lvk::IContext> createVulkanContextWithSwapchain(
GLFWwindow* window, uint32_t width, uint32_t height,
const lvk::vulkan::VulkanContextConfig& cfg,
lvk::HWDeviceType preferredDeviceType)
{
std::unique_ptr<vulkan::VulkanContext> ctx;
#if defined(_WIN32)
ctx = std::make_unique<VulkanContext>(cfg,
(void*)glfwGetWin32Window(window));
#elif defined(__linux__)
#if defined(LVK_WITH_WAYLAND)
wl_surface* waylandWindow = glfwGetWaylandWindow(window);
if (!waylandWindow) {
LVK_ASSERT_MSG(false, "Wayland window not found");
return nullptr;
}
ctx = std::make_unique<VulkanContext>(cfg,
(void*)waylandWindow, (void*)glfwGetWaylandDisplay());
#else
ctx = std::make_unique<VulkanContext>(cfg,
(void*)glfwGetX11Window(window), (void*)glfwGetX11Display());
#endif // LVK_WITH_WAYLAND
#else
# error Unsupported OS
#endif
HWDeviceDesc device;
uint32_t numDevices =
ctx->queryDevices(preferredDeviceType, &device, 1);
if (!numDevices) {
if (preferredDeviceType == HWDeviceType_Discrete) {
numDevices =
ctx->queryDevices(HWDeviceType_Integrated, &device);
} else if (preferredDeviceType == HWDeviceType_Integrated) {
numDevices =
ctx->queryDevices(HWDeviceType_Discrete, &device);
}
}
VulkanContext::initContext(), which creates all Vulkan and LightweightVK internal data structures.
if (!numDevices) return nullptr;
Result res = ctx->initContext(device);
if (!res.isOk()) return nullptr;
if (width > 0 && height > 0) {
res = ctx->initSwapchain(width, height);
if (!res.isOk()) return nullptr;
}
return std::move(ctx);
}
That covers the high-level code. Now, let’s dive deeper and explore the internals of LightweightVK to see how the actual Vulkan interactions work.
There are several helper functions involved in getting Vulkan up and running. It all begins with the creation of a Vulkan instance in VulkanContext::createInstance(). Once the Vulkan instance is created, we can use it to acquire a list of physical devices with the required properties.
const char* kDefaultValidationLayers[] =
{"VK_LAYER_KHRONOS_validation"};
void VulkanContext::createInstance() {
vkInstance_ = VK_NULL_HANDLE;
uint32_t numLayerProperties = 0;
vkEnumerateInstanceLayerProperties(
&numLayerProperties, nullptr);
std::vector<VkLayerProperties>
layerProperties(numLayerProperties);
vkEnumerateInstanceLayerProperties(
&numLayerProperties, layerProperties.data());
VulkanContextConfig::enableValidation accordingly if none are found.
[this, &layerProperties]() -> void {
for (const VkLayerProperties& props : layerProperties) {
for (const char* layer : kDefaultValidationLayers) {
if (!strcmp(props.layerName, layer)) return;
}
}
config_.enableValidation = false;
}();
VK_KHR_surface and another platform-specific extension which takes an OS window handle and attaches a rendering surface to it. On Linux, we support both libXCB-based window creation and the Wayland protocol. Here is how Wayland support was added to LightweightVK by Roman Kuznetsov: https://github.com/corporateshark/lightweightvk/pull/13.
std::vector<const char*> instanceExtensionNames = {
VK_KHR_SURFACE_EXTENSION_NAME,
VK_EXT_DEBUG_UTILS_EXTENSION_NAME,
#if defined(_WIN32)
VK_KHR_WIN32_SURFACE_EXTENSION_NAME,
#elif defined(VK_USE_PLATFORM_ANDROID_KHR)
VK_KHR_ANDROID_SURFACE_EXTENSION_NAME,
#elif defined(__linux__)
#if defined(VK_USE_PLATFORM_WAYLAND_KHR)
VK_KHR_WAYLAND_SURFACE_EXTENSION_NAME,
#else
VK_KHR_XLIB_SURFACE_EXTENSION_NAME,
#endif // VK_USE_PLATFORM_WAYLAND_KHR
#endif
};
VK_EXT_validation_features when validation features are requested and available. Additionally, a headless rendering extension, VK_EXT_headless_surface, can also be added here together with all custom instance extensions from VulkanContextConfig::extensionsInstance[].
if (config_.enableValidation)
instanceExtensionNames.push_back(
VK_EXT_VALIDATION_FEATURES_EXTENSION_NAME);
if (config_.enableHeadlessSurface)
instanceExtensionNames.push_back(
VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME);
for (const char* ext : config_.extensionsInstance) {
if (ext) instanceExtensionNames.push_back(ext);
}
VkValidationFeatureEnableEXT validationFeaturesEnabled[] = {
VK_VALIDATION_FEATURE_ENABLE_GPU_ASSISTED_EXT,
VK_VALIDATION_FEATURE_ENABLE_GPU_ASSISTED_
RESERVE_BINDING_SLOT_EXT,
};
const VkValidationFeaturesEXT features = {
.sType = VK_STRUCTURE_TYPE_VALIDATION_FEATURES_EXT,
.enabledValidationFeatureCount = config_.enableValidation ?
(uint32_t)LVK_ARRAY_NUM_ELEMENTS(validationFeaturesEnabled) : 0u,
.pEnabledValidationFeatures = config_.enableValidation ?
validationFeaturesEnabled : nullptr,
};
VkBool32 gpuav_descriptor_checks = VK_FALSE;
VkBool32 gpuav_indirect_draws_buffers = VK_FALSE;
VkBool32 gpuav_post_process_descriptor_indexing = VK_FALSE;
#define LAYER_SETTINGS_BOOL32(name, var) \
VkLayerSettingEXT { \
.pLayerName = kDefaultValidationLayers[0], \
.pSettingName = name, \
.type = VK_LAYER_SETTING_TYPE_BOOL32_EXT, \
.valueCount = 1, \
.pValues = var }
const VkLayerSettingEXT settings[] = {
LAYER_SETTINGS_BOOL32("gpuav_descriptor_checks",
&gpuav_descriptor_checks),
LAYER_SETTINGS_BOOL32("gpuav_indirect_draws_buffers",
&gpuav_indirect_draws_buffers),
LAYER_SETTINGS_BOOL32(
"gpuav_post_process_descriptor_indexing",
&gpuav_post_process_descriptor_indexing),
};
#undef LAYER_SETTINGS_BOOL32
const VkLayerSettingsCreateInfoEXT layerSettingsCreateInfo = {
.sType = VK_STRUCTURE_TYPE_LAYER_SETTINGS_CREATE_INFO_EXT,
.pNext = config_.enableValidation ? &features : nullptr,
.settingCount = (uint32_t)LVK_ARRAY_NUM_ELEMENTS(settings),
.pSettings = settings
};
VK_API_VERSION_1_3.
const VkApplicationInfo appInfo = {
.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO,
.pApplicationName = "LVK/Vulkan",
.applicationVersion = VK_MAKE_VERSION(1, 0, 0),
.pEngineName = "LVK/Vulkan",
.engineVersion = VK_MAKE_VERSION(1, 0, 0),
.apiVersion = VK_API_VERSION_1_3,
};
VkInstance object, we need to populate the VkInstanceCreateInfo structure. We use pointers to the previously mentioned appInfo constant and layerSettingsCreateInfo we created earlier. We also use a list of requested Vulkan layers stored in the global variable kDefaultValidationLayers[], which will allow us to enable debugging output for every Vulkan call. The only layer we use in this book is the Khronos validation layer, VK_LAYER_KHRONOS_validation. Then, we use the Volk library to load all instance-related Vulkan functions for the created VkInstance.Note
Volk is a meta-loader for Vulkan. It allows you to dynamically load entry points required to use Vulkan without linking to vulkan-1.dll or statically linking the Vulkan loader. Volk simplifies the use of Vulkan extensions by automatically loading all associated entry points. Besides that, Volk can load Vulkan entry points directly from the driver which can increase performance by skipping loader dispatch overhead. https://github.com/zeux/volk
const VkInstanceCreateInfo ci = {
.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
.pNext = &layerSettingsCreateInfo,
.pApplicationInfo = &appInfo,
.enabledLayerCount = config_.enableValidation ?
(uint32_t)LVK_ARRAY_NUM_ELEMENTS(kDefaultValidationLayers) : 0u,
.ppEnabledLayerNames = config_.enableValidation ?
kDefaultValidationLayers : nullptr,
.enabledExtensionCount =
(uint32_t)instanceExtensionNames.size(),
.ppEnabledExtensionNames = instanceExtensionNames.data(),
};
VK_ASSERT(vkCreateInstance(&ci, nullptr, &vkInstance_));
volkLoadInstance(vkInstance_);
vkEnumerateInstanceExtensionProperties() is called twice: first to get the number of available extensions, and second to retrieve information about them.
uint32_t count = 0;
vkEnumerateInstanceExtensionProperties(
nullptr, &count, nullptr);
std::vector<VkExtensionProperties>
allInstanceExtensions(count);
vkEnumerateInstanceExtensionProperties(
nullptr, &count, allInstanceExtensions.data()));
LLOGL("\nVulkan instance extensions:\n");
for (const VkExtensionProperties& extension : allInstanceExtensions)
LLOGL(" %s\n", extension.extensionName);
}
Note
If you’ve looked at the actual source code in VulkanClasses.cpp, you’ll have noticed that we skipped the Debug Messenger initialization code here. It will be covered later in the recipe Setting up Vulkan debugging capabilities.
Once we’ve created a Vulkan instance, we can access the list of Vulkan physical devices, which are necessary to continue setting up our Vulkan context. Here’s how we can enumerate Vulkan physical devices and choose a suitable one:
vkEnumeratePhysicalDevices() is called twice: first to get the number of available physical devices and allocate std::vector storage for it, and second to retrieve the actual physical device data.
uint32_t lvk::VulkanContext::queryDevices(
HWDeviceType deviceType,
HWDeviceDesc* outDevices,
uint32_t maxOutDevices)
{
uint32_t deviceCount = 0;
vkEnumeratePhysicalDevices(vkInstance_, &deviceCount, nullptr);
std::vector<VkPhysicalDevice> vkDevices(deviceCount);
vkEnumeratePhysicalDevices(
vkInstance_, &deviceCount, vkDevices.data());
convertVulkanDeviceTypeToLVK() converts a Vulkan enum, VkPhysicalDeviceType, into a LightweightVK enum, HWDeviceType.More information
enum HWDeviceType {
HWDeviceType_Discrete = 1,
HWDeviceType_External = 2,
HWDeviceType_Integrated = 3,
HWDeviceType_Software = 4,
};
const HWDeviceType desiredDeviceType = deviceType;
uint32_t numCompatibleDevices = 0;
for (uint32_t i = 0; i < deviceCount; ++i) {
VkPhysicalDevice physicalDevice = vkDevices[i];
VkPhysicalDeviceProperties deviceProperties;
vkGetPhysicalDeviceProperties(
physicalDevice, &deviceProperties);
const HWDeviceType deviceType =
convertVulkanDeviceTypeToLVK(deviceProperties.deviceType);
if (desiredDeviceType != HWDeviceType_Software &&
desiredDeviceType != deviceType) continue;
if (outDevices && numCompatibleDevices < maxOutDevices) {
outDevices[numCompatibleDevices] =
{.guid = (uintptr_t)vkDevices[i], .type = deviceType};
strncpy(outDevices[numCompatibleDevices].name,
deviceProperties.deviceName,
strlen(deviceProperties.deviceName));
numCompatibleDevices++;
}
}
return numCompatibleDevices;
}
Once we’ve selected a suitable Vulkan physical device, we can create a logical representation of a single GPU, or more precisely, a device VkDevice. We can think of Vulkan devices as collections of queues and memory heaps. To use a device for rendering, we need to specify a queue capable of executing graphics-related commands, along with a physical device that has such a queue. Let’s explore LightweightVK and some parts of the function VulkanContext::initContext(), which, among many other things we’ll cover later, detects suitable queue families and creates a Vulkan device. As before, most of the error checking will be omitted here in the text.
VulkanContext::initContext() is retrieve all supported extensions of the physical device we selected earlier and the Vulkan driver. We store them in allDeviceExtensions to later decide which features we can enable. Note how we iterate over the validation layers to check which extensions they bring in.
lvk::Result VulkanContext::initContext(const HWDeviceDesc& desc)
{
vkPhysicalDevice_ = (VkPhysicalDevice)desc.guid;
std::vector<VkExtensionProperties> allDeviceExtensions;
getDeviceExtensionProps(
vkPhysicalDevice_, allDeviceExtensions);
if (config_.enableValidation) {
for (const char* layer : kDefaultValidationLayers)
getDeviceExtensionProps(
vkPhysicalDevice_, allDeviceExtensions, layer);
}
vkGetPhysicalDeviceFeatures2(
vkPhysicalDevice_, &vkFeatures10_);
vkGetPhysicalDeviceProperties2(
vkPhysicalDevice_, &vkPhysicalDeviceProperties2_);
vkFeatures10_, vkFeatures11_, vkFeatures12_, and vkFeatures13_ are declared in VulkanClasses.h and correspond to the Vulkan features for Vulkan versions 1.0 to 1.3. These structures are chained together using their pNext pointers as follows:
// lightweightvk/lvk/vulkan/VulkanClasses.h
VkPhysicalDeviceVulkan13Features vkFeatures13_ = {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES};
VkPhysicalDeviceVulkan12Features vkFeatures12_ = {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES,
.pNext = &vkFeatures13_};
VkPhysicalDeviceVulkan11Features vkFeatures11_ = {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_FEATURES,
.pNext = &vkFeatures12_};
VkPhysicalDeviceFeatures2 vkFeatures10_ = {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2,
.pNext = &vkFeatures11_};
// ...
initContext() and print some information related to the Vulkan physical device and a list of all supported extensions. This is very useful for debugging.
const uint32_t apiVersion =
vkPhysicalDeviceProperties2_.properties.apiVersion;
LLOGL("Vulkan physical device: %s\n",
vkPhysicalDeviceProperties2_.properties.deviceName);
LLOGL(" API version: %i.%i.%i.%i\n",
VK_API_VERSION_MAJOR(apiVersion),
VK_API_VERSION_MINOR(apiVersion),
VK_API_VERSION_PATCH(apiVersion),
VK_API_VERSION_VARIANT(apiVersion));
LLOGL(" Driver info: %s %s\n",
vkPhysicalDeviceDriverProperties_.driverName,
vkPhysicalDeviceDriverProperties_.driverInfo);
LLOGL("Vulkan physical device extensions:\n");
for (const VkExtensionProperties& ext : allDeviceExtensions) {
LLOGL(" %s\n", ext.extensionName);
}
VkDevice object, we need to find the queue family indices and create queues. This code block creates one or two device queues—graphical and compute—based on the actual queue availability on the provided physical device. The helper function lvk::findQueueFamilyIndex(), implemented in lvk/vulkan/VulkanUtils.cpp, returns the first dedicated queue family index that matches the requested queue flag. It’s recommended to take a look at it to see how it ensures the selection of dedicated queues first.Note
In Vulkan, queueFamilyIndex is the index of the queue family to which the queue belongs. A queue family is a collection of Vulkan queues with similar properties and functionality. Here deviceQueues_ is member field of VulkanContext holding a structure with queues information:
struct DeviceQueues {
const static uint32_t INVALID = 0xFFFFFFFF;
uint32_t graphicsQueueFamilyIndex = INVALID;
uint32_t computeQueueFamilyIndex = INVALID;
VkQueue graphicsQueue = VK_NULL_HANDLE;
VkQueue computeQueue = VK_NULL_HANDLE;
};
deviceQueues_.graphicsQueueFamilyIndex =
lvk::findQueueFamilyIndex(vkPhysicalDevice_,
VK_QUEUE_GRAPHICS_BIT);
deviceQueues_.computeQueueFamilyIndex =
lvk::findQueueFamilyIndex(vkPhysicalDevice_,
VK_QUEUE_COMPUTE_BIT);
const float queuePriority = 1.0f;
const VkDeviceQueueCreateInfo ciQueue[2] = {
{ .sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
.queueFamilyIndex = deviceQueues_.graphicsQueueFamilyIndex,
.queueCount = 1,
.pQueuePriorities = &queuePriority, },
{ .sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
.queueFamilyIndex = deviceQueues_.computeQueueFamilyIndex,
.queueCount = 1,
.pQueuePriorities = &queuePriority, },
};
const uint32_t numQueues =
ciQueue[0].queueFamilyIndex == ciQueue[1].queueFamilyIndex ?
1 : 2;
VulkanContextConfig::extensionsDevice[].
std::vector<const char*> deviceExtensionNames = {
VK_KHR_SWAPCHAIN_EXTENSION_NAME,
};
for (const char* ext : config_.extensionsDevice) {
if (ext) deviceExtensionNames.push_back(ext);
}
1.0–1.3 features we’ll be using in our Vulkan implementation. The most important features are descriptor indexing from Vulkan 1.2 and dynamic rendering from Vulkan 1.3, which we’ll discuss in subsequent chapters. Take a look at how to request these and other features we’ll be using.Note
Descriptor indexing is a set of Vulkan 1.2 features that enable applications to access all of their resources and select among them using integer indices in shaders.
Dynamic rendering is a Vulkan 1.3 feature that allows applications to render directly into images without the need to create render pass objects or framebuffers.
VkPhysicalDeviceFeatures deviceFeatures10 = {
.geometryShader = vkFeatures10_.features.geometryShader,
.sampleRateShading = VK_TRUE,
.multiDrawIndirect = VK_TRUE,
// ...
};
pNext pointers. Note how we access the vkFeatures10_ through vkFeatures13_ structures here to enable optional features only if they are actually supported by the physical device. The complete list is quite long, so we skip some parts of it here.
VkPhysicalDeviceVulkan11Features deviceFeatures11 = {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_FEATURES,
.pNext = config_.extensionsDeviceFeatures,
.storageBuffer16BitAccess = VK_TRUE,
.samplerYcbcrConversion = vkFeatures11_.samplerYcbcrConversion,
.shaderDrawParameters = VK_TRUE,
};
VkPhysicalDeviceVulkan12Features deviceFeatures12 = {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES,
.pNext = &deviceFeatures11,
.drawIndirectCount = vkFeatures12_.drawIndirectCount,
// ...
.descriptorIndexing = VK_TRUE,
.shaderSampledImageArrayNonUniformIndexing = VK_TRUE,
.descriptorBindingSampledImageUpdateAfterBind = VK_TRUE,
.descriptorBindingStorageImageUpdateAfterBind = VK_TRUE,
.descriptorBindingUpdateUnusedWhilePending = VK_TRUE,
.descriptorBindingPartiallyBound = VK_TRUE,
.descriptorBindingVariableDescriptorCount = VK_TRUE,
.runtimeDescriptorArray = VK_TRUE,
// ...
};
VkPhysicalDeviceVulkan13Features deviceFeatures13 = {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES,
.pNext = &deviceFeatures12,
.subgroupSizeControl = VK_TRUE,
.synchronization2 = VK_TRUE,
.dynamicRendering = VK_TRUE,
.maintenance4 = VK_TRUE,
};
VkDevice object. We check our list of requested device extensions against the list of available extensions. Any missing extensions are printed into the log, and the initialization function returns. This is very convenient for debugging.
std::string missingExtensions;
for (const char* ext : deviceExtensionNames) {
if (!hasExtension(ext, allDeviceExtensions))
missingExtensions += "\n " + std::string(ext);
}
if (!missingExtensions.empty()) {
MINILOG_LOG_PROC(minilog::FatalError,
"Missing Vulkan device extensions: %s\n",
missingExtensions.c_str());
return Result(Result::Code::RuntimeError);
}
std::string missingFeatures;
#define CHECK_VULKAN_FEATURE( \
reqFeatures, availFeatures, feature, version) \
if ((reqFeatures.feature == VK_TRUE) && \
(availFeatures.feature == VK_FALSE)) \
missingFeatures.append("\n " version " ." #feature);
#define CHECK_FEATURE_1_0(feature) \
CHECK_VULKAN_FEATURE(deviceFeatures10, vkFeatures10_.features, \
feature, "1.0 ");
CHECK_FEATURE_1_0(robustBufferAccess);
CHECK_FEATURE_1_0(fullDrawIndexUint32);
CHECK_FEATURE_1_0(imageCubeArray);
… // omitted a lot of other Vulkan 1.0 features here
#undef CHECK_FEATURE_1_0
#define CHECK_FEATURE_1_1(feature) \
CHECK_VULKAN_FEATURE(deviceFeatures11, vkFeatures11_, \
feature, "1.1 ");
CHECK_FEATURE_1_1(storageBuffer16BitAccess);
CHECK_FEATURE_1_1(uniformAndStorageBuffer16BitAccess);
CHECK_FEATURE_1_1(storagePushConstant16);
… // omitted a lot of other Vulkan 1.1 features here
#undef CHECK_FEATURE_1_1
#define CHECK_FEATURE_1_2(feature) \
CHECK_VULKAN_FEATURE(deviceFeatures12, vkFeatures12_, \
feature, "1.2 ");
CHECK_FEATURE_1_2(samplerMirrorClampToEdge);
CHECK_FEATURE_1_2(drawIndirectCount);
CHECK_FEATURE_1_2(storageBuffer8BitAccess);
… // omitted a lot of other Vulkan 1.2 features here
#undef CHECK_FEATURE_1_2
#define CHECK_FEATURE_1_3(feature) \
CHECK_VULKAN_FEATURE(deviceFeatures13, vkFeatures13_, \
feature, "1.3 ");
CHECK_FEATURE_1_3(robustImageAccess);
CHECK_FEATURE_1_3(inlineUniformBlock);
… // omitted a lot of other Vulkan 1.3 features here
#undef CHECK_FEATURE_1_3
if (!missingFeatures.empty()) {
MINILOG_LOG_PROC(minilog::FatalError,
"Missing Vulkan features: %s\n", missingFeatures.c_str());
return Result(Result::Code::RuntimeError);
}
const VkDeviceCreateInfo ci = {
.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,
.pNext = createInfoNext,
.queueCreateInfoCount = numQueues,
.pQueueCreateInfos = ciQueue,
.enabledExtensionCount = deviceExtensionNames.size(),
.ppEnabledExtensionNames = deviceExtensionNames.data(),
.pEnabledFeatures = &deviceFeatures10,
};
vkCreateDevice(vkPhysicalDevice_, &ci, nullptr, &vkDevice_);
volkLoadDevice(vkDevice_);
vkGetDeviceQueue(vkDevice_,
deviceQueues_.graphicsQueueFamilyIndex, 0,
&deviceQueues_.graphicsQueue);
vkGetDeviceQueue(vkDevice_,
deviceQueues_.computeQueueFamilyIndex, 0,
&deviceQueues_.computeQueue);
// ... other code in initContext() is unrelated to this recipe
}
The VkDevice object is now ready to be used, but the initialization of the Vulkan rendering pipeline is far from complete. The next step is to create a swapchain object. Let’s proceed to the next recipe to learn how to do this.
Change the font size
Change margin width
Change background colour