What we’ll build here

It doesn’t look like much huh? Well, after seeing the amount of setup we have to do with Vulkan to get there, your appreciation for this result just might increase significantly! Believe me! We’ll be using Vulkan 1.3, which makes things much less cumbersome compared to the Vulkan 1.0 era, but be prepared, it’s still a long journey ahead!
This post doesn’t require you to have any prior experience with Vulkan whatsoever, however it does assume that you’re already familiar with the GLFW library, and C++.
Just want the source code? Find it here.
Setup
You’ll need:
- The Vulkan SDK (for the Vulkan headers, loader and validation layers)
- GLFW (I used 3.5.1, but any recent 3.x release will do)
- A C++20 compiler. We use designated initializers (the
.sType = ...style struct initialization) a lot, which is a C++20 feature
The whole sample lives in a single main.cpp, which starts with these includes:
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <cassert>
#include <algorithm>
#include <functional>
#include <memory>
#include <set>
#include <string>
#include <utility>
#include <vector>
#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>
Defining GLFW_INCLUDE_VULKAN before including GLFW makes it include the Vulkan header (vulkan/vulkan.h) for us, and enables GLFW’s Vulkan related functions such as glfwCreateWindowSurface.
NOTE: In the file, the SampleVulkan class (which we’ll build up throughout this post) needs to be declared before the SampleApp class, since SampleApp holds it in a std::unique_ptr, which needs the complete type to be able to destroy it. The full source code has everything in the right order if you get lost.
Entry point & App class
We’ll start by creating a class to manage our app state:
class SampleApp
{
static constexpr uint32_t SCREEN_WIDTH = 800;
static constexpr uint32_t SCREEN_HEIGHT = 600;
public:
int launch(int argc, char *argv[]);
~SampleApp();
private:
GLFWwindow *m_window{};
std::unique_ptr<SampleVulkan> m_vulkan{}; // We'll get to what this "SampleVulkan" class is in a minute!
};
and so our entry point (main) simplifies to:
int main(int argc, char *argv[])
{
SampleApp app;
return app.launch(argc, argv);
}
In the destructor of our app class, we handle the cleanup for GLFW and our Vulkan class (Described in the next section):
SampleApp::~SampleApp()
{
m_vulkan.reset();
if (m_window)
glfwDestroyWindow(m_window);
glfwTerminate();
}
Now the meaty one of the app class, the launch function.
Don’t worry if you don’t understand what m_vulkan->initialize and m_vulkan->render functions do yet, we’ll get to them in the coming sections.
I, however, do assume that you’re already familiar with GLFW and you understand the GLFW code below. Since the focus of this post is Vulkan, I won’t be going into details on how to setup or use GLFW. In a nutshell though, GLFW is a cross-platform windowing library.
Don’t let the lambda function passed into the m_vulkan->initialize here scare you, I’ll explain what it does and why we do it that way in the next section.
int SampleApp::launch(int argc, char *argv[])
{
if (!glfwInit())
{
printf("Failed to initialize GLFW\n");
return -1;
}
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
m_window = glfwCreateWindow(SCREEN_WIDTH, SCREEN_HEIGHT, "i-a-s.me Vulkan Sample", nullptr, nullptr);
if (!m_window)
{
printf("Failed to create GLFW window\n");
return -1;
}
m_vulkan = std::make_unique<SampleVulkan>();
if (!m_vulkan->initialize([window = m_window](VkInstance instance, uint32_t &out_width, uint32_t &out_height,
VkSurfaceKHR &out_surface) -> bool {
if (glfwCreateWindowSurface(instance, window, nullptr, &out_surface) != VK_SUCCESS)
{
printf("Failed to create Vulkan surface\n");
return false;
}
int32_t fb_width{}, fb_height{};
glfwGetFramebufferSize(window, &fb_width, &fb_height);
while ((fb_width == 0) || (fb_height == 0))
{
glfwWaitEvents();
glfwGetFramebufferSize(window, &fb_width, &fb_height);
}
out_width = (uint32_t) fb_width;
out_height = (uint32_t) fb_height;
return true;
}))
{
printf("Failed to initialize Vulkan\n");
return -1;
}
glfwSetWindowUserPointer(m_window, m_vulkan.get());
glfwSetFramebufferSizeCallback(m_window, [](GLFWwindow *window, int width, int height) {
int32_t fb_width{}, fb_height{};
glfwGetFramebufferSize(window, &fb_width, &fb_height);
if ((fb_width == 0) || (fb_height == 0))
return;
static_cast<SampleVulkan *>(glfwGetWindowUserPointer(window))->resize(fb_width, fb_height);
});
while (!glfwWindowShouldClose(m_window))
{
if (glfwGetKey(m_window, GLFW_KEY_ESCAPE))
glfwSetWindowShouldClose(m_window, true);
glfwPollEvents();
// Don't render while the window is minimized (its framebuffer is 0x0 then)
int32_t fb_width{}, fb_height{};
glfwGetFramebufferSize(m_window, &fb_width, &fb_height);
if ((fb_width == 0) || (fb_height == 0))
{
glfwWaitEvents();
continue;
}
if (!m_vulkan->render())
break;
}
return 0;
}
Vulkan Class
class SampleVulkan
{
public:
SampleVulkan() = default;
// I don't want to stray too far from our topic at hand here and explain
// why we delete the copy constructor and copy assignment, but for those of you who are curious,
// this is because the instances of this class will be owning the Vulkan
// handles.
//
// Don't worry if you don't understand what that means, focus here is not C++.
SampleVulkan(const SampleVulkan &) = delete;
SampleVulkan &operator=(const SampleVulkan &) = delete;
~SampleVulkan(); // This is where we'll carry out our cleanup
// ------------------------------------------
// API exposed by our Vulkan class
// ------------------------------------------
bool initialize(std::function<bool(VkInstance, uint32_t &, uint32_t &, VkSurfaceKHR &)> surface_creation_callback);
bool render();
void resize(uint32_t fb_width, uint32_t fb_height);
private:
/*
We'll put our private function declarations here,
for example like create_instance(), create_device_and_swapchain() etc.
*/
private:
/*
We'll put our private member variables here, mostly Vulkan handles.
*/
};
Let’s now walk through each of the functions in our class API:
-
render: This will tell our SampleVulkan class to render a frame. This function is to be called from the main application loop every frame. -
resize: This will tell our class that the window was resized, so that it can resize its draw surface (the swapchain images) to match. We should call this whenever the window is resized. -
initialize: This function initializes the class instance. It takes a callback function that will:
- Take in a VkInstance handle (needed for creating the Vulkan Surface from GLFW)
- Output initial width and height for the draw surface
- Create the GLFW window Vulkan Surface and output that
THEORY: Vulkan API Shape
Creating most Vulkan objects follows the pattern:
// 1. We define the thing handle
VkThing thing_handle{};
// 2. We populate a CreateInfo structure for the thing
VkThingCreateInfo create_info{
.sType = VK_STRUCTURE_TYPE_THING_CREATE_INFO,
// .field_1 = value,
// .field_2 = value,
// .field_3 = value,
};
// 3. We invoke the thing create function, passing the earlier populated CreateInfo structure
const auto result = vkCreateThing(
device, // the parent object the thing is created from (usually the VkDevice)
&create_info, // populated create info structure
nullptr, // a custom allocator parameter (we can use `nullptr` to indicate we do not wish to use custom allocation logic)
&thing_handle // a pointer to where the handle to the created 'thing' will be stored
);
// Check the `result` to make sure it is `VK_SUCCESS`.
// If it isn't, value of `result` most of the time indicates what went wrong.
and, once we’re done with the thing, we destroy it with the matching vkDestroyThing(device, thing_handle, nullptr) function.
The instance, which we’ll create first, is the one exception to the “parent object” part: it’s the root of everything, so vkCreateInstance has no parent parameter.
Enumerating things
Another pattern we’ll run into a lot, is getting a list of things from Vulkan (e.g. the available GPUs, their queue families, supported surface formats etc.). These functions are called twice: first with a nullptr array to get the count, and then again with an array of that size to actually get the elements:
uint32_t count{};
vkEnumeratePhysicalDevices(instance, &count, nullptr); // 1. Get the count
std::vector<VkPhysicalDevice> devices(count);
vkEnumeratePhysicalDevices(instance, &count, devices.data()); // 2. Get the elements
Since we’ll do this quite a few times, let’s make a small helper template for it:
template<typename ElementT, typename FuncT, typename... Args> std::vector<ElementT> vkEnumCall(FuncT f, Args... args)
{
uint32_t _count{};
f(args..., &_count, nullptr);
std::vector<ElementT> result(_count);
f(args..., &_count, result.data());
return result;
}
so that the above simply becomes:
const auto devices = vkEnumCall<VkPhysicalDevice>(vkEnumeratePhysicalDevices, instance);
Instance Creation
We’ll start by declaring a member variable to hold the Vulkan instance handle:
private:
VkInstance m_instance{};
and a function to house the instance creation logic:
private:
bool create_instance();
Note: We give them a return type of bool instead of void so we can indicate whether the function succeeded or not by returning true/false.
This is what the code to create the instance actually looks like:
bool SampleVulkan::create_instance()
{
std::vector<const char *> extensions;
// GLFW extensions contain platform specific extensions needed for
// using the window surface.
// We do not need to include these extensions if we don't want to
// create or use window surfaces (i.e. off screen rendering and other headless apps).
uint32_t glfw_ext_count{};
const auto glfw_ext = glfwGetRequiredInstanceExtensions(&glfw_ext_count);
extensions.insert(extensions.end(), glfw_ext, glfw_ext + glfw_ext_count);
VkApplicationInfo app_info{
.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO,
.pApplicationName = "i-a-s.me Vulkan 1.3 Sample",
.applicationVersion = VK_MAKE_VERSION(1, 0, 0),
.pEngineName = "i-a-s.me Vulkan 1.3 Sample",
.engineVersion = VK_MAKE_VERSION(1, 0, 0),
.apiVersion = VK_API_VERSION_1_3,
};
VkInstanceCreateInfo create_info{
.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
.pApplicationInfo = &app_info,
.enabledExtensionCount = (uint32_t) extensions.size(),
.ppEnabledExtensionNames = extensions.data(),
};
if (vkCreateInstance(&create_info, nullptr, &m_instance) != VK_SUCCESS)
{
printf("Failed to create Vulkan instance\n");
return false;
}
return true;
}
We can optionally (optional but STRONGLY recommended for development) enable validation layers. While we’re at it, we’ll also add macOS support (explained below). First, a couple more declarations for our class:
static constexpr const char *VALIDATION_LAYER_NAME = "VK_LAYER_KHRONOS_validation";
private:
VkDebugUtilsMessengerEXT m_debug_messenger{};
and then the updated create_instance:
bool SampleVulkan::create_instance()
{
// In production code we would have a global switch (a constexpr or a traditional preprocessor definition)
// to enable or disable validation layers. Here, we always enable them if they're available.
const auto instance_layer_props = vkEnumCall<VkLayerProperties>(vkEnumerateInstanceLayerProperties);
const bool enable_validation_layers =
std::any_of(instance_layer_props.begin(), instance_layer_props.end(),
[](VkLayerProperties p) { return strcmp(p.layerName, VALIDATION_LAYER_NAME) == 0; });
VkDebugUtilsMessengerCreateInfoEXT debug_messenger_create_info{
.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT,
.messageSeverity =
VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT,
.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT |
VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT,
.pfnUserCallback = [](VkDebugUtilsMessageSeverityFlagBitsEXT severity, VkDebugUtilsMessageTypeFlagsEXT type,
const VkDebugUtilsMessengerCallbackDataEXT *callback_data, void *user_data) -> VkBool32 {
printf("[VK_VALIDATION_LAYER]: %s\n", callback_data->pMessage);
return VK_FALSE;
},
};
std::vector<const char *> extensions;
{ // GLFW Extensions
uint32_t glfw_ext_count{};
const auto glfw_ext = glfwGetRequiredInstanceExtensions(&glfw_ext_count);
extensions.insert(extensions.end(), glfw_ext, glfw_ext + glfw_ext_count);
}
if (enable_validation_layers)
extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
// macOS support: MoltenVK is a "portability" implementation (it doesn't support all
// of Vulkan), so the loader only exposes it to us if we explicitly opt in.
const auto instance_ext_props = vkEnumCall<VkExtensionProperties>(vkEnumerateInstanceExtensionProperties, nullptr);
const bool enable_portability =
std::any_of(instance_ext_props.begin(), instance_ext_props.end(), [](VkExtensionProperties p) {
return strcmp(p.extensionName, VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME) == 0;
});
if (enable_portability)
extensions.push_back(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME);
VkApplicationInfo app_info{
.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO,
.pApplicationName = "i-a-s.me Vulkan 1.3 Sample",
.applicationVersion = VK_MAKE_VERSION(1, 0, 0),
.pEngineName = "i-a-s.me Vulkan 1.3 Sample",
.engineVersion = VK_MAKE_VERSION(1, 0, 0),
.apiVersion = VK_API_VERSION_1_3,
};
VkInstanceCreateInfo create_info{
.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
.pNext = enable_validation_layers ? &debug_messenger_create_info : nullptr,
.flags = enable_portability ? (VkInstanceCreateFlags) VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR : 0u,
.pApplicationInfo = &app_info,
.enabledLayerCount = enable_validation_layers ? 1u : 0u,
.ppEnabledLayerNames = enable_validation_layers ? &VALIDATION_LAYER_NAME : nullptr,
.enabledExtensionCount = (uint32_t) extensions.size(),
.ppEnabledExtensionNames = extensions.data(),
};
if (vkCreateInstance(&create_info, nullptr, &m_instance) != VK_SUCCESS)
{
printf("Failed to create Vulkan instance\n");
return false;
}
const auto debug_messenger_create_func =
(PFN_vkCreateDebugUtilsMessengerEXT) vkGetInstanceProcAddr(m_instance, "vkCreateDebugUtilsMessengerEXT");
if (enable_validation_layers && debug_messenger_create_func)
{
if (debug_messenger_create_func(m_instance, &debug_messenger_create_info, nullptr, &m_debug_messenger) !=
VK_SUCCESS)
{
printf("Failed to create Vulkan debug messenger\n");
return false;
}
}
return true;
}
Validation layers sit between our code and the driver, and check every Vulkan call we make for mistakes (wrong parameters, missing synchronization, leaked objects etc.). Vulkan drivers themselves do very little error checking for the sake of performance, so without the validation layers, a mistake might just crash, or worse, seem to work fine on your machine and break on someone else’s. They come with the Vulkan SDK, which is why we first check whether they’re available.
A few things to note here:
- The debug messenger (
VkDebugUtilsMessengerEXT) is how the validation layers report messages back to us. Here, we just print them to the console. - The debug messenger comes from the
VK_EXT_debug_utilsextension, which isn’t part of core Vulkan. So we have to enable the extension, and get the address ofvkCreateDebugUtilsMessengerEXTourselves withvkGetInstanceProcAddr. - We also pass the debug messenger create info through the
pNextfield ofVkInstanceCreateInfo. The debug messenger we create after the instance can’t report problems that happen duringvkCreateInstanceitself (orvkDestroyInstance), so chaining the create info like this gives us a temporary messenger that covers those calls too.
macOS support
There’s no native Vulkan driver on macOS. Instead, the Vulkan SDK ships with MoltenVK, which implements Vulkan on top of Apple’s Metal (and recent versions of MoltenVK support Vulkan 1.3). Since Metal can’t do everything Vulkan can, MoltenVK is a portability implementation: it supports almost all of Vulkan, minus a few features. To make sure apps don’t accidentally run on such an implementation without knowing it, the Vulkan loader hides them unless we explicitly opt in, by:
- Enabling the
VK_KHR_portability_enumerationinstance extension, and - Setting the
VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHRflag in the instance create info
which is exactly what the code above does, if the extension is available. Without this, vkCreateInstance on macOS fails with VK_ERROR_INCOMPATIBLE_DRIVER. On other platforms, the extension is usually available too, and enabling it is harmless. There’s one more piece to this, which we’ll get to when we create the logical device.
Calling it from initialize
Finally, let’s add the create_instance call to our initialize method:
bool SampleVulkan::initialize(
std::function<bool(VkInstance, uint32_t &, uint32_t &, VkSurfaceKHR &)>
surface_creation_callback)
{
if (!create_instance())
return false;
// ... more to come
return true;
}
Surface Creation
We’ll start by adding the surface handle declaration to our class:
private:
VkInstance m_instance{};
VkSurfaceKHR m_surface{}; // Surface Handle
In the SampleApp class we already filled out the logic for creating a GLFW surface, given a Vulkan instance handle. Now that we have the instance handle, we can simply invoke that logic using the surface_creation_callback callback function.
bool SampleVulkan::initialize(
std::function<bool(VkInstance, uint32_t &, uint32_t &, VkSurfaceKHR &)>
surface_creation_callback)
{
if (!create_instance())
return false;
uint32_t surface_width{}, surface_height{}; // Temporary place to store the surface dimensions
// given by the surface_creation_callback
if (!surface_creation_callback(m_instance, surface_width, surface_height,
m_surface))
{
printf("Failed to create Vulkan surface\n");
return false;
}
// ... more to come
return true;
}
NOTE: We’re gonna leave the surface creation at that without getting into the nitty gritty. If you’re curious, explore more on GLFW surface creation.
THEORY: Queues and Queue Families
We don’t tell the GPU to do things by calling functions that execute right away. Instead, we record work into command buffers and submit them to a queue. The GPU then picks up the work from the queue and executes it in its own time.
A GPU usually has several queues, and they’re grouped into queue families. All queues in a family have the same capabilities, which are described by the family’s queueFlags:
VK_QUEUE_GRAPHICS_BIT: Can do graphics work (drawing, and clearing attachments like we’ll do here)VK_QUEUE_COMPUTE_BIT: Can run compute shadersVK_QUEUE_TRANSFER_BIT: Can do copy operations- …and a few others
There’s one more capability we need that isn’t in queueFlags: being able to present images to our window’s surface. Since presenting is tied to a specific surface, we have to ask for it separately, per family, using vkGetPhysicalDeviceSurfaceSupportKHR.
So for our coloured window, we need to find two queue families on the GPU:
- A graphics family, which we’ll submit our rendering work to
- A present family, which we’ll use to present the rendered images to the window
On most hardware, the same family can do both, but that’s not guaranteed, so we’ll handle them separately (and it costs us almost nothing to do so).
Here’s how we find them. Given a physical device (a GPU, more on that in the next section), we go through its queue families, noting down any graphics capable and present capable family we come across, until we’ve found one of each:
std::pair<uint32_t, uint32_t> find_queue_families(VkPhysicalDevice p)
{
uint32_t graphics_family_index{UINT32_MAX}, present_family_index{UINT32_MAX};
const auto queue_families = vkEnumCall<VkQueueFamilyProperties>(vkGetPhysicalDeviceQueueFamilyProperties, p);
for (uint32_t i = 0; i < (uint32_t) queue_families.size(); i++)
{
const auto &f = queue_families[i];
if (f.queueFlags & VK_QUEUE_GRAPHICS_BIT)
graphics_family_index = i;
VkBool32 has_present_support{};
if ((vkGetPhysicalDeviceSurfaceSupportKHR(p, i, m_surface, &has_present_support) == VK_SUCCESS) &&
has_present_support)
present_family_index = i;
if ((graphics_family_index != UINT32_MAX) && (present_family_index != UINT32_MAX))
break;
}
return {graphics_family_index, present_family_index};
};
We use UINT32_MAX to mean “not found”. Since we check both capabilities on every family, a family that can do both will be picked for both (as long as we haven’t already found both elsewhere), which is exactly what we’d want. Note that this function needs access to m_surface, so in the next section, we’ll put it inside select_physical_device as a lambda.
Selecting a Physical Device
A physical device (VkPhysicalDevice) represents a single Vulkan capable GPU in the system. You might have more than one of them (a laptop with both integrated and discrete GPUs, for example), and not all of them might be suitable for what we want to do. So we’ll go through all of them and pick one that:
- Supports Vulkan 1.3 (since we’re relying on Vulkan 1.3 core features like dynamic rendering)
- Has both graphics and present capable queue families
- Is preferably a discrete GPU (these are usually much faster than integrated ones)
We’ll start by adding the class declarations:
private:
VkPhysicalDevice m_physical_device{};
std::string m_physical_device_name{}; // OPTIONAL but it is useful to have the human
// readable name of the physical device handy
uint32_t m_graphics_queue_family_index{};
uint32_t m_present_queue_family_index{};
private:
bool select_physical_device();
bool SampleVulkan::select_physical_device()
{
const auto find_queue_families = [&](VkPhysicalDevice p) -> std::pair<uint32_t, uint32_t> {
uint32_t graphics_family_index{UINT32_MAX}, present_family_index{UINT32_MAX};
const auto queue_families = vkEnumCall<VkQueueFamilyProperties>(vkGetPhysicalDeviceQueueFamilyProperties, p);
for (uint32_t i = 0; i < (uint32_t) queue_families.size(); i++)
{
const auto &f = queue_families[i];
if (f.queueFlags & VK_QUEUE_GRAPHICS_BIT)
graphics_family_index = i;
VkBool32 has_present_support{};
if ((vkGetPhysicalDeviceSurfaceSupportKHR(p, i, m_surface, &has_present_support) == VK_SUCCESS) &&
has_present_support)
present_family_index = i;
if ((graphics_family_index != UINT32_MAX) && (present_family_index != UINT32_MAX))
break;
}
return {graphics_family_index, present_family_index};
};
m_physical_device = VK_NULL_HANDLE;
const auto physical_devices = vkEnumCall<VkPhysicalDevice>(vkEnumeratePhysicalDevices, m_instance);
for (const auto &p : physical_devices)
{
VkPhysicalDeviceProperties2 props{
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2,
};
vkGetPhysicalDeviceProperties2(p, &props);
if (props.properties.apiVersion < VK_API_VERSION_1_3)
continue; // If this physical device does not support at least Vulkan 1.3, we cannot use it. We use Vulkan 1.3
// core features in this sample.
const auto queue_family_indices = find_queue_families(p);
if ((queue_family_indices.first == UINT32_MAX) || (queue_family_indices.second == UINT32_MAX))
continue; // This physical device isn't what we want, if it lacks either graphics or present queue families
// We could (and should) do more suitability checks in a real application,
// but for this sample, this is just ok.
m_physical_device = p;
m_graphics_queue_family_index = queue_family_indices.first;
m_present_queue_family_index = queue_family_indices.second;
m_physical_device_name = props.properties.deviceName;
if (props.properties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU)
break; // If we find a discrete GPU, then prefer it
}
if (m_physical_device == VK_NULL_HANDLE)
{
printf("Failed to find a good physical device\n");
return false;
}
printf("Using physical device \"%s\"\n", m_physical_device_name.c_str());
return true;
}
Notice that we use vkGetPhysicalDeviceProperties2 rather than vkGetPhysicalDeviceProperties. The 2 versions of Vulkan functions and structures were added in later versions of Vulkan, and can be extended through the pNext chain. We don’t chain anything here, but it’s a good habit to get into.
Also note how we don’t break out of the loop as soon as we find a suitable device. We keep the last suitable device we’ve seen, unless we come across a discrete GPU, in which case we take it and stop looking.
Device & Swapchain
The logical device
The VkPhysicalDevice we just picked represents the actual GPU, but we don’t talk to it directly. Instead, we create a logical device (VkDevice) out of it. The logical device is our “connection” to the GPU, and while creating it we have to tell Vulkan up front exactly what we’re going to use:
- Which queues we want (and from which queue families)
- Which device extensions we want enabled. For us, that’s
VK_KHR_swapchain, since presenting to a window is not part of core Vulkan (plusVK_KHR_portability_subseton macOS, more on that below) - Which features we want enabled. We want two Vulkan 1.3 features,
dynamicRenderingandsynchronization2. They’re part of core Vulkan 1.3, but core does not mean enabled, we still have to explicitly ask for them!
As usual, the class declarations first:
static constexpr const char *SWAPCHAIN_DEVICE_EXTENSION_NAME = "VK_KHR_swapchain"; // Same as the VK_KHR_SWAPCHAIN_EXTENSION_NAME macro
// Only needed on macOS. Its macro lives in vulkan_beta.h, so we just spell out the name
static constexpr const char *PORTABILITY_SUBSET_DEVICE_EXTENSION_NAME = "VK_KHR_portability_subset";
private:
bool create_device_and_swapchain();
private:
VkDevice m_device{};
VkQueue m_graphics_queue{};
VkQueue m_present_queue{};
and then the first half of create_device_and_swapchain:
bool SampleVulkan::create_device_and_swapchain()
{
const float priority = 1.0f;
std::vector<VkDeviceQueueCreateInfo> queue_create_infos;
// passing through std::set effectively dedups family indices.
// not really necessary to use std::set here as we only have to consider
// 2 families (graphics and present), so we could have just checked (family_index_1 == family_index_2),
// but this std::set approach generalizes to higher orders (imagine doing if/else checks for combinations of 3 or 4)
for (const auto index : std::set<uint32_t>{m_graphics_queue_family_index, m_present_queue_family_index})
queue_create_infos.push_back(VkDeviceQueueCreateInfo{
.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
.queueFamilyIndex = index,
.queueCount = 1,
.pQueuePriorities = &priority,
});
std::vector<const char *> device_extensions{SWAPCHAIN_DEVICE_EXTENSION_NAME};
// macOS support: if the device is a portability implementation (i.e. MoltenVK),
// it exposes VK_KHR_portability_subset, and the spec requires us to enable it.
const auto device_ext_props =
vkEnumCall<VkExtensionProperties>(vkEnumerateDeviceExtensionProperties, m_physical_device, nullptr);
if (std::any_of(device_ext_props.begin(), device_ext_props.end(), [](VkExtensionProperties p) {
return strcmp(p.extensionName, PORTABILITY_SUBSET_DEVICE_EXTENSION_NAME) == 0;
}))
device_extensions.push_back(PORTABILITY_SUBSET_DEVICE_EXTENSION_NAME);
VkPhysicalDeviceFeatures enabled_features{};
VkPhysicalDeviceVulkan13Features vk_13_features{
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES,
.synchronization2 = VK_TRUE,
.dynamicRendering = VK_TRUE,
};
VkDeviceCreateInfo device_create_info{
.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,
.pNext = &vk_13_features,
.queueCreateInfoCount = (uint32_t) queue_create_infos.size(),
.pQueueCreateInfos = queue_create_infos.data(),
.enabledLayerCount = 0,
.ppEnabledLayerNames = nullptr,
.enabledExtensionCount = (uint32_t) device_extensions.size(),
.ppEnabledExtensionNames = device_extensions.data(),
.pEnabledFeatures = &enabled_features,
};
if (vkCreateDevice(m_physical_device, &device_create_info, nullptr, &m_device) != VK_SUCCESS)
{
printf("Failed to create Vulkan device\n");
return false;
}
vkGetDeviceQueue(m_device, m_graphics_queue_family_index, 0, &m_graphics_queue);
vkGetDeviceQueue(m_device, m_present_queue_family_index, 0, &m_present_queue);
// ... Swapchain stuff goes here, see below
}
A few things worth pointing out here:
- Queue priorities: Every queue we request needs a priority between
0.0and1.0. It only matters relative to other queues on the same device, and since we only have one queue per family, we just go with1.0. - The
pNextchain: Remember thepNextfield we used to sneak the debug messenger create info into the instance creation? Same trick here.VkDeviceCreateInfoonly has a field for the old Vulkan 1.0 features (pEnabledFeatures), so newer feature structs such asVkPhysicalDeviceVulkan13Featuresare chained throughpNext. You’ll see this pattern everywhere in Vulkan: when the API needs to grow, new structures get chained onto old ones throughpNext. - Device layers (
enabledLayerCount/ppEnabledLayerNames): These are deprecated and ignored by modern Vulkan implementations. Layers are an instance level thing now, so we leave them empty. - Getting the queues: We don’t create queues, they get created along with the device. We just ask for handles to them with
vkGetDeviceQueue. If the graphics and present families happen to be the same (which is very common), bothm_graphics_queueandm_present_queuewill end up being the same queue, and that’s perfectly fine. VK_KHR_portability_subset: This is the other half of our macOS support. A portability implementation (like MoltenVK) exposes this device extension to describe which Vulkan features it can’t support, and the spec requires us to enable it whenever the device exposes it. So we check the device’s extensions withvkEnumerateDeviceExtensionProperties, and enable it only if it’s there. Its name macro lives in thevulkan_beta.hheader (which isn’t included by default), so we just spell out the name ourselves.
THEORY: What is a swapchain?
We can’t just draw straight onto the window. Instead, the window system (through the surface we created earlier) owns a small set of images, which we call the swapchain. Every frame, the flow looks like this:
- Acquire an image from the swapchain (
vkAcquireNextImageKHR), this gives us the index of an image we’re allowed to render into - Render into that image
- Present the image (
vkQueuePresentKHR), handing it back to the window system to be shown on the screen
While one image is being shown on screen, we can be rendering into another one. That’s the “swap” in the swapchain.
When creating a swapchain, there are a few things we need to decide on:
- Surface format: The pixel format (e.g.
VK_FORMAT_B8G8R8A8_SRGB) and colour space of the images - Present mode: How images are queued up for display.
VK_PRESENT_MODE_FIFO_KHRis basically V-Sync, and it’s the only mode guaranteed to be supported everywhere.VK_PRESENT_MODE_MAILBOX_KHRis also V-Synced (no tearing), but instead of blocking us when the queue is full, it replaces the waiting image with the newer one, which gives lower latency. We’ll prefer mailbox when it’s available, and fall back to FIFO otherwise. - Extent: The resolution of the images, which should match our window’s framebuffer size
- Image count: How many images the swapchain holds
Let’s add the swapchain related members to our class:
private:
bool recreate_swapchain();
private:
VkSwapchainKHR m_swapchain{};
std::vector<VkImage> m_swapchain_images{};
std::vector<VkImageView> m_swapchain_image_views{};
uint32_t m_swapchain_image_count{};
VkSurfaceFormatKHR m_swapchain_format{};
VkPresentModeKHR m_swapchain_present_mode{};
VkSurfaceTransformFlagBitsKHR m_swapchain_pre_transform{};
VkExtent2D m_swapchain_extent{1, 1}; // The extent our current swapchain was created with
VkExtent2D m_pending_swapchain_extent{1, 1}; // The extent we *want* the swapchain to have (updated on resize)
bool m_swapchain_is_out_of_date{};
std::vector<VkSemaphore> m_render_done_semaphores; // We'll talk about these in the synchronization section!
The format and the present mode don’t change when the window gets resized, so we pick them once, right after creating the device. So here is the second half of create_device_and_swapchain:
bool SampleVulkan::create_device_and_swapchain()
{
// ... Device creation code from above
const auto surface_formats =
vkEnumCall<VkSurfaceFormatKHR>(vkGetPhysicalDeviceSurfaceFormatsKHR, m_physical_device, m_surface);
const auto surface_present_modes =
vkEnumCall<VkPresentModeKHR>(vkGetPhysicalDeviceSurfacePresentModesKHR, m_physical_device, m_surface);
assert(surface_formats.size());
assert(surface_present_modes.size());
m_swapchain_format = surface_formats[0];
m_swapchain_present_mode = (std::find(surface_present_modes.begin(), surface_present_modes.end(),
VK_PRESENT_MODE_MAILBOX_KHR) != surface_present_modes.end())
? VK_PRESENT_MODE_MAILBOX_KHR
: VK_PRESENT_MODE_FIFO_KHR;
return recreate_swapchain();
}
NOTE: We simply take the first surface format the surface reports. That’s fine for a clear colour sample, but in a real application you’d want to look for a specific one (commonly VK_FORMAT_B8G8R8A8_SRGB with VK_COLOR_SPACE_SRGB_NONLINEAR_KHR). Whether the format is sRGB or not changes how your colours look on screen, so if the clear colour looks a bit off for you compared to the screenshot, that’s probably why!
(Re)creating the swapchain
Everything else about the swapchain depends on the window size, which means we need to be able to throw the swapchain away and build a new one whenever the window gets resized. So instead of writing a create_swapchain function and a recreate_swapchain function, we write just the latter, and call it for the initial creation too:
bool SampleVulkan::recreate_swapchain()
{
// 1. Make sure the GPU isn't using any of the resources we're about to destroy
vkDeviceWaitIdle(m_device);
// 2. Query what the surface is capable of (image counts, extents, transforms etc.)
VkSurfaceCapabilitiesKHR surface_caps{};
if (vkGetPhysicalDeviceSurfaceCapabilitiesKHR(m_physical_device, m_surface, &surface_caps) != VK_SUCCESS)
{
printf("Failed to query physical device surface caps\n");
return false;
}
// A minimized window has a 0x0 surface, and a 0x0 swapchain is not allowed. So we
// keep the current swapchain, and try again later (once the window is restored).
if ((surface_caps.maxImageExtent.width == 0) || (surface_caps.maxImageExtent.height == 0))
{
m_swapchain_is_out_of_date = true;
return true;
}
// 3. Decide on an image count. One more than the minimum, so we don't have to wait on the
// presentation engine as often. A maxImageCount of 0 means "no maximum".
m_swapchain_image_count = std::min(surface_caps.minImageCount + 1,
(surface_caps.maxImageCount == 0) ? UINT32_MAX : surface_caps.maxImageCount);
// 4. Decide on the extent. It must be within the limits reported by the surface.
m_swapchain_pre_transform = surface_caps.currentTransform;
m_pending_swapchain_extent.width = std::clamp(m_pending_swapchain_extent.width, surface_caps.minImageExtent.width,
surface_caps.maxImageExtent.width);
m_pending_swapchain_extent.height = std::clamp(m_pending_swapchain_extent.height, surface_caps.minImageExtent.height,
surface_caps.maxImageExtent.height);
m_swapchain_extent = m_pending_swapchain_extent;
// 5. Destroy the old swapchain (if we have one)
for (const auto &view : m_swapchain_image_views)
vkDestroyImageView(m_device, view, nullptr);
if (m_swapchain)
vkDestroySwapchainKHR(m_device, m_swapchain, nullptr);
m_swapchain_image_views.clear();
m_swapchain = VK_NULL_HANDLE;
// 6. Create the new swapchain
VkSwapchainCreateInfoKHR swapchain_create_info{
.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR,
.surface = m_surface,
.minImageCount = m_swapchain_image_count,
.imageFormat = m_swapchain_format.format,
.imageColorSpace = m_swapchain_format.colorSpace,
.imageExtent = m_swapchain_extent,
.imageArrayLayers = 1,
.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT,
.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE,
.preTransform = m_swapchain_pre_transform,
.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR,
.presentMode = m_swapchain_present_mode,
.clipped = VK_TRUE,
};
uint32_t queue_family_indices[] = {m_graphics_queue_family_index, m_present_queue_family_index};
if (m_graphics_queue_family_index != m_present_queue_family_index)
{
swapchain_create_info.imageSharingMode = VK_SHARING_MODE_CONCURRENT;
swapchain_create_info.queueFamilyIndexCount = 2;
swapchain_create_info.pQueueFamilyIndices = queue_family_indices;
}
if (vkCreateSwapchainKHR(m_device, &swapchain_create_info, nullptr, &m_swapchain) != VK_SUCCESS)
{
printf("Failed to create swapchain\n");
return false;
}
// 7. Get the swapchain images, and (re)create one "render done" semaphore per image
m_swapchain_images = vkEnumCall<VkImage>(vkGetSwapchainImagesKHR, m_device, m_swapchain);
// Note that we use the *actual* number of images here, which can be
// more than the minImageCount we asked for.
for (auto semaphore : m_render_done_semaphores)
vkDestroySemaphore(m_device, semaphore, nullptr);
m_render_done_semaphores.assign(m_swapchain_images.size(), VK_NULL_HANDLE);
VkSemaphoreCreateInfo semaphore_create_info{
.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO,
};
for (auto &semaphore : m_render_done_semaphores)
{
if (vkCreateSemaphore(m_device, &semaphore_create_info, nullptr, &semaphore) != VK_SUCCESS)
{
printf("Failed to create a semaphore\n");
return false;
}
}
// 8. Create an image view for each swapchain image
m_swapchain_image_views.reserve(m_swapchain_images.size());
for (const auto &image : m_swapchain_images)
{
VkImageViewCreateInfo create_info{
.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
.image = image,
.viewType = VK_IMAGE_VIEW_TYPE_2D,
.format = m_swapchain_format.format,
.components =
{
.r = VK_COMPONENT_SWIZZLE_IDENTITY,
.g = VK_COMPONENT_SWIZZLE_IDENTITY,
.b = VK_COMPONENT_SWIZZLE_IDENTITY,
.a = VK_COMPONENT_SWIZZLE_IDENTITY,
},
.subresourceRange =
{
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
.baseMipLevel = 0,
.levelCount = 1,
.baseArrayLayer = 0,
.layerCount = 1,
},
};
VkImageView view{};
if (vkCreateImageView(m_device, &create_info, nullptr, &view) != VK_SUCCESS)
{
printf("Failed to create a swapchain image view\n");
return false;
}
m_swapchain_image_views.push_back(view);
}
m_swapchain_is_out_of_date = false;
return true;
}
That’s a big one, so let’s go over the less obvious bits:
vkDeviceWaitIdle: This blocks the CPU until the GPU has finished all of its work. It’s a heavy-handed way of making sure nothing we’re about to destroy is still in use. Resizing doesn’t happen often, so this is totally acceptable here.minImageCount: Note that this is a minimum, the driver is allowed to create more images than we ask for. That’s why we query the actual images withvkGetSwapchainImagesKHRafterwards instead of assuming there are exactlym_swapchain_image_countof them, and why we create the render done semaphores only after that, one for each image we actually got. (If we created one per requested image instead, and the driver gave us more,renderwould index past the end ofm_render_done_semaphores!)- The 0x0 check: When the window is minimized (on Windows, at least), the surface reports a maximum extent of 0x0, and creating a swapchain with a zero width or height isn’t allowed. So in that case, we keep the swapchain we have, leave
m_swapchain_is_out_of_dateset, and try again later. imageUsage: We’re only going to render into these images as colour attachments, soVK_IMAGE_USAGE_COLOR_ATTACHMENT_BITis all we need.imageSharingMode: If the graphics and present queues come from different families, the images get used by both of them.VK_SHARING_MODE_CONCURRENTlets us do that without having to transfer the ownership of the images between queue families ourselves. It’s slightly slower, but it’s much simpler, and on most hardware the two families will be the same anyway.preTransform: Some displays (mostly on mobile) are rotated. By passingcurrentTransform, we’re saying “don’t apply any extra transformation”.compositeAlpha: We don’t want our window to blend with other windows behind it, so we go opaque.clipped: We don’t care about the colours of pixels that are hidden (for example, behind another window), so we let the driver skip them.- Image views: We almost never use
VkImages directly in Vulkan. Instead, we access them through aVkImageView, which describes how to look at an image (which format, which mip levels, which array layers etc.). Our swapchain images are plain 2D colour images, so the views are pretty boring.
NOTE: VkSwapchainCreateInfoKHR also has an oldSwapchain field that can be used to hand over the old swapchain while creating the new one, which lets the transition happen more smoothly. We’re simply waiting for the device to go idle and destroying the old one first, to keep things simple.
Handling resizes
Remember the framebuffer size callback we registered in SampleApp::launch? It calls our resize function. You might be tempted to call recreate_swapchain directly from there, but GLFW can call that callback quite a lot while the user is dragging the window’s edge. So instead, we just take a note of the new size, and let our render function recreate the swapchain when it gets the chance:
void SampleVulkan::resize(uint32_t fb_width, uint32_t fb_height)
{
m_pending_swapchain_extent = {fb_width, fb_height};
m_swapchain_is_out_of_date = true;
}
What about when the window gets minimized? The framebuffer becomes 0x0 then, and there’s nothing to render into. That’s why the main loop in SampleApp::launch checks the framebuffer size before calling render, and if it’s 0x0, it waits for events (with glfwWaitEvents) instead of rendering. Once the window is restored, GLFW calls our framebuffer size callback, and the swapchain gets recreated on the next render. The 0x0 check in recreate_swapchain is a second line of defence, in case the swapchain gets recreated right as the window is being minimized.
Also, now that we have a proper place to store the initial surface dimensions (m_pending_swapchain_extent), we can get rid of the temporary variables in initialize and pass it to the surface_creation_callback directly. Let’s also call the two functions we’ve just written:
bool SampleVulkan::initialize(
std::function<bool(VkInstance, uint32_t &, uint32_t &, VkSurfaceKHR &)>
surface_creation_callback)
{
if (!create_instance())
return false;
if (!surface_creation_callback(m_instance, m_pending_swapchain_extent.width, m_pending_swapchain_extent.height,
m_surface))
{
printf("Failed to create Vulkan surface\n");
return false;
}
if (!select_physical_device())
return false;
if (!create_device_and_swapchain())
return false;
return true;
}
THEORY: Synchronization Basics
Here’s the thing that trips up pretty much everyone coming to Vulkan from OpenGL: The GPU runs asynchronously to the CPU, and Vulkan does (almost) nothing to keep them in sync for you. When we submit work to a queue, vkQueueSubmit returns right away, long before the GPU has actually done that work. It’s our job to make sure things happen in the correct order, and Vulkan gives us a few tools for that:
-
Semaphores (
VkSemaphore): Used for GPU ↔ GPU synchronization, between queue operations. One operation signals the semaphore when it’s done, and another one waits on it before it starts. The CPU never waits on these (well, not the kind of semaphores we’re using here). -
Fences (
VkFence): Used for GPU → CPU synchronization. We hand a fence to a queue submission, the GPU signals it when that submission finishes, and the CPU can wait on it withvkWaitForFences. Once signaled, a fence stays signaled until we reset it withvkResetFences. -
Pipeline Barriers: Used for synchronization within a queue, between commands. We’ll see these in the image layouts section.
Our frame, in terms of synchronization
For every frame, we need the following to happen in order:
- The swapchain image we acquired must be actually ready for us (the presentation engine might still be reading it to show a previous frame!) before we render into it. The acquire operation will signal an “image available” semaphore when that’s the case, and our queue submission will wait on it.
- Our rendering must be finished before the image gets presented. Our submission will signal a “render done” semaphore, and the present operation will wait on it.
- The CPU must not reuse a command buffer (or any other per frame resource) while the GPU is still working on it. Our submission will signal an “in flight” fence and the CPU will wait on it before reusing those resources.
Frames in flight
If we only had one set of the above resources, the CPU would have to sit idle waiting for the GPU to completely finish a frame, before it could even start recording the next one. Instead, we keep multiple sets of per frame resources (NUM_CONCURRENT_FRAMES of them, we’ll use 3), so the CPU can record the next frame while the GPU is still busy with previous ones:
CPU: [record F0][record F1][record F2][wait for F0 fence][record F0][record F1]...
GPU: [execute F0 ][execute F1 ][execute F2 ][execute F0]...
Why are the “render done” semaphores per swapchain image?
You might have noticed that we created the render done semaphores in recreate_swapchain, one per swapchain image, and not one per frame in flight like the other two. This is subtle, but important:
When we present, we hand the render done semaphore over to the presentation engine, and there’s no fence to tell us when it’s done waiting on it. If we reused that semaphore for a later frame, we could end up signaling it again while the presentation engine is still waiting on it from the previous present, which is an error (the validation layers will happily point this out). However, a swapchain image cannot be acquired again until the presentation engine has finished with it, so by tying the render done semaphore to the swapchain image index, we know it’s safe to reuse whenever we get that same image back.
The image available semaphore on the other hand is used before we know which image we’ll get (we pass it to vkAcquireNextImageKHR), so it has to be per frame in flight. It’s protected by the in flight fence of that frame.
Draw Loop
Per frame resources
Let’s add a struct to hold the resources of a single frame in flight, along with the rest of the declarations:
class SampleVulkan
{
static constexpr uint32_t NUM_CONCURRENT_FRAMES = 3;
struct FrameResourceSet
{
VkFence fen_in_flight{};
VkSemaphore sm_image_available{};
VkCommandBuffer cmd_buffer{};
};
// ...
private:
bool create_frame_resources();
private:
uint32_t m_current_frame_index{};
VkCommandPool m_frame_resource_command_pool{};
FrameResourceSet m_frames[NUM_CONCURRENT_FRAMES];
};
Wait, what’s a command buffer? In Vulkan, we don’t call functions that immediately draw stuff. Instead, we record commands (vkCmd* functions) into a command buffer, and then submit the whole command buffer to a queue for the GPU to execute. Command buffers are allocated from a command pool, which is tied to a single queue family. Since we’ll be submitting our command buffers to the graphics queue, our pool uses the graphics queue family index.
bool SampleVulkan::create_frame_resources()
{
VkCommandPoolCreateInfo commmand_pool_create_info{
.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO,
.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT,
.queueFamilyIndex = m_graphics_queue_family_index,
};
if (vkCreateCommandPool(m_device, &commmand_pool_create_info, nullptr, &m_frame_resource_command_pool) != VK_SUCCESS)
{
printf("Failed to create command pool\n");
return false;
}
VkCommandBufferAllocateInfo command_buffer_alloc_info{
.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,
.commandPool = m_frame_resource_command_pool,
.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY,
.commandBufferCount = 1,
};
for (uint32_t i = 0; i < NUM_CONCURRENT_FRAMES; i++)
{
if (vkAllocateCommandBuffers(m_device, &command_buffer_alloc_info, &m_frames[i].cmd_buffer) != VK_SUCCESS)
{
printf("Failed to allocate frame command buffer\n");
return false;
}
VkSemaphoreCreateInfo semaphore_create_info{
.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO,
};
if (vkCreateSemaphore(m_device, &semaphore_create_info, nullptr, &m_frames[i].sm_image_available) != VK_SUCCESS)
{
printf("Failed to create a semaphore\n");
return false;
}
VkFenceCreateInfo fence_create_info{
.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO,
.flags = VK_FENCE_CREATE_SIGNALED_BIT,
};
if (vkCreateFence(m_device, &fence_create_info, nullptr, &m_frames[i].fen_in_flight) != VK_SUCCESS)
{
printf("Failed to create a fence\n");
return false;
}
}
return true;
}
Two flags here deserve a mention:
VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT: Allows us to reset individual command buffers allocated from this pool (withvkResetCommandBuffer), so we can re-record them every frame.VK_FENCE_CREATE_SIGNALED_BIT: The first thing we do every frame is wait on that frame’s fence. On the very first frame, nothing has ever been submitted, so nothing would ever signal the fence, and we’d wait forever! Creating the fences already signaled solves that.
And, the final version of our initialize function:
bool SampleVulkan::initialize(
std::function<bool(VkInstance, uint32_t &, uint32_t &, VkSurfaceKHR &)> surface_creation_callback)
{
if (!create_instance())
return false;
if (!surface_creation_callback(m_instance, m_pending_swapchain_extent.width, m_pending_swapchain_extent.height,
m_surface))
{
printf("Failed to create Vulkan surface\n");
return false;
}
if (!select_physical_device())
return false;
if (!create_device_and_swapchain())
return false;
if (!create_frame_resources())
return false;
return true;
}
The render function
Now we finally get to the function that SampleApp calls every frame. It ties together everything from the synchronization section:
bool SampleVulkan::render()
{
const auto ¤t_frame = m_frames[m_current_frame_index];
// 1. Wait until the GPU is done with the last submission that used this frame's resources
vkWaitForFences(m_device, 1, ¤t_frame.fen_in_flight, VK_TRUE, UINT64_MAX);
// 2. Acquire a swapchain image to render into
uint32_t image_index{};
switch (vkAcquireNextImageKHR(m_device, m_swapchain, UINT64_MAX, current_frame.sm_image_available, VK_NULL_HANDLE,
&image_index))
{
case VK_ERROR_OUT_OF_DATE_KHR:
if (!recreate_swapchain())
return false;
return true;
case VK_SUCCESS:
case VK_SUBOPTIMAL_KHR:
break;
default:
printf("Failed to acquire swapchain image\n");
return false;
}
// 3. Record the command buffer
VkCommandBufferBeginInfo cmd_begin_info{
.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
};
vkResetCommandBuffer(current_frame.cmd_buffer, 0);
if (vkBeginCommandBuffer(current_frame.cmd_buffer, &cmd_begin_info) != VK_SUCCESS)
{
printf("Failed to start recording command buffer\n");
return false;
}
if (!transition_image_layout(current_frame.cmd_buffer, m_swapchain_images[image_index], VK_IMAGE_LAYOUT_UNDEFINED,
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL))
{
printf("Failed to transition swapchain image\n");
return false;
}
draw_frame(current_frame.cmd_buffer, m_swapchain_image_views[image_index]);
if (!transition_image_layout(current_frame.cmd_buffer, m_swapchain_images[image_index],
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR))
{
printf("Failed to transition swapchain image\n");
return false;
}
if (vkEndCommandBuffer(current_frame.cmd_buffer) != VK_SUCCESS)
{
printf("Failed to finish recording command buffer\n");
return false;
}
// 4. Submit the command buffer to the graphics queue
VkPipelineStageFlags submit_pipeline_wait_stage{VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT};
VkSubmitInfo submit_info{
.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
.waitSemaphoreCount = 1,
.pWaitSemaphores = ¤t_frame.sm_image_available,
.pWaitDstStageMask = &submit_pipeline_wait_stage,
.commandBufferCount = 1,
.pCommandBuffers = ¤t_frame.cmd_buffer,
.signalSemaphoreCount = 1,
.pSignalSemaphores = &m_render_done_semaphores[image_index],
};
vkResetFences(m_device, 1, ¤t_frame.fen_in_flight);
if (vkQueueSubmit(m_graphics_queue, 1, &submit_info, current_frame.fen_in_flight) != VK_SUCCESS)
{
printf("Failed to submit frame command buffer\n");
return false;
}
// 5. Present the image
VkPresentInfoKHR present_info{
.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR,
.waitSemaphoreCount = 1,
.pWaitSemaphores = &m_render_done_semaphores[image_index],
.swapchainCount = 1,
.pSwapchains = &m_swapchain,
.pImageIndices = &image_index,
};
switch (vkQueuePresentKHR(m_present_queue, &present_info))
{
case VK_ERROR_OUT_OF_DATE_KHR:
case VK_SUBOPTIMAL_KHR:
if (!recreate_swapchain())
return false;
break;
case VK_SUCCESS:
break;
default:
printf("Failed to present swapchain\n");
return false;
}
// 6. Recreate the swapchain if the window got resized
if (m_swapchain_is_out_of_date && !recreate_swapchain())
return false;
// 7. Move on to the next set of frame resources
m_current_frame_index = (m_current_frame_index + 1) % NUM_CONCURRENT_FRAMES;
return true;
}
Let’s walk through it step by step:
-
Wait for the fence: Before we touch this frame’s command buffer or semaphore, we need the GPU to be done with the previous submission that used them. With 3 frames in flight, that’s the submission from 3 frames ago, which is most likely done already, so this usually returns immediately.
-
Acquire an image:
vkAcquireNextImageKHRgives us the index of the next swapchain image, and signals our image available semaphore once that image is actually ready. There are a couple of special return values to handle here:
VK_ERROR_OUT_OF_DATE_KHR: The swapchain no longer matches the surface (usually because the window was resized) and can’t be used anymore. We recreate it and skip this frame.VK_SUBOPTIMAL_KHR: The swapchain still works but doesn’t match the surface exactly. We still got an image (and the semaphore will be signaled), so we go ahead and render this frame, and deal with it after presenting.
-
Record the command buffer: We reset the command buffer and record our commands into it: transition the image into a layout suitable for rendering, draw (clear, in our case), and then transition it into a layout suitable for presenting. We’ll look at what these layout transitions are about in the next section.
-
Submit: We submit the command buffer to the graphics queue, telling it to wait on the image available semaphore, and to signal the render done semaphore and the in flight fence when done. Notice the
pWaitDstStageMask: we’re saying that only the colour attachment output stage of the pipeline needs to wait for the image. Any work that comes before that stage (which we don’t have yet, but will once we start drawing geometry) can start right away without waiting for the image to become available. -
Present: We queue the image for presentation on the present queue, waiting on the render done semaphore.
-
Handle resizes: If the present told us the swapchain is out of date or suboptimal, or our
resizefunction was called, we recreate the swapchain. -
Advance the frame index: So that the next call to
renderuses the next set of frame resources.
NOTE: Pay attention to where we reset the fence: right before the submit, and not right after waiting on it. If we reset it right after waiting, and then bailed out early because the swapchain was out of date, the fence would be left unsignaled with no submission to ever signal it, and the next time we come around to this frame, we’d wait on it forever!
THEORY: Image Layouts & Transitions
On the GPU, images are not necessarily stored as a simple row after row array of pixels. Depending on what an image is being used for, the GPU might prefer to arrange (or even compress) its memory in completely different ways. Vulkan exposes this through image layouts. An image is always in some layout, and before using an image for something, we have to make sure it’s in a layout that supports that use. The ones we care about for now are:
VK_IMAGE_LAYOUT_UNDEFINED: “I don’t care what’s in this image”. Transitioning from this layout is allowed to throw away the image contents.VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL: Optimal for rendering into, as a colour attachment.VK_IMAGE_LAYOUT_PRESENT_SRC_KHR: Required for presenting a swapchain image.
So every frame, our swapchain image goes UNDEFINED → COLOR_ATTACHMENT_OPTIMAL → PRESENT_SRC_KHR. We can start from UNDEFINED every time because we clear the whole image anyway, so we don’t care about what was in it before.
Pipeline barriers
Layout transitions are done with image memory barriers, which are a kind of pipeline barrier. On top of changing the layout, a barrier also synchronizes the commands that come before it with the commands that come after it. Thanks to the synchronization2 feature we enabled, we describe a barrier with a VkImageMemoryBarrier2, which has these important bits:
srcStageMaskandsrcAccessMask: Which pipeline stages (and which kinds of memory accesses within them) from before the barrier must be finished and made available, before the transition happens.dstStageMaskanddstAccessMask: Which pipeline stages (and memory accesses) from after the barrier must wait for the transition to be done.oldLayoutandnewLayout: The layout transition itself.subresourceRange: Which part of the image (aspect, mip levels, array layers) the barrier applies to.
Let’s look at the two transitions we do each frame:
-
UNDEFINED→COLOR_ATTACHMENT_OPTIMAL: The source stage here is the colour attachment output stage, with no source access. This might look odd since nothing before the barrier writes to the image, but this is what connects the barrier to the image available semaphore. Remember, our submission waits on that semaphore at the colour attachment output stage. By using that same stage as our source stage, we’re saying “do the transition only after the image is actually available”. If we usedVK_PIPELINE_STAGE_2_NONEhere instead, the transition could happen while the presentation engine is still reading the image! On the destination side, colour attachment writes (our clear) must wait for the transition. -
COLOR_ATTACHMENT_OPTIMAL→PRESENT_SRC_KHR: The source is our colour attachment writes, which must be done before the transition. The destination is nothing, since there are no more commands after this that use the image. Presentation happens outside of the pipeline, and the render done semaphore takes care of making sure the present waits for all this to finish.
Since we’ll be doing a lot of these transitions (and many more kinds of them later on in the series), we wrap them up in a helper function:
private:
bool transition_image_layout(VkCommandBuffer cmd, VkImage image, VkImageLayout old_layout, VkImageLayout new_layout,
uint32_t base_mip_level = 0, uint32_t level_count = 1, uint32_t base_array_layer = 0,
uint32_t layer_count = 1);
bool SampleVulkan::transition_image_layout(VkCommandBuffer cmd, VkImage image, VkImageLayout old_layout,
VkImageLayout new_layout, uint32_t base_mip_level, uint32_t level_count,
uint32_t base_array_layer, uint32_t layer_count)
{
VkPipelineStageFlags2 src_stage{}, dst_stage{};
VkAccessFlags2 src_access{}, dst_access{};
VkImageAspectFlags aspect{VK_IMAGE_ASPECT_COLOR_BIT};
#define _is_layout_pair(o, n) ((old_layout == o) && (new_layout == n))
if (_is_layout_pair(VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL))
{
src_stage = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;
src_access = VK_ACCESS_2_NONE;
dst_stage = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;
dst_access = VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT;
}
else if (_is_layout_pair(VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL))
{
// Wait for any depth writes from a previous use of this image (e.g. the previous frame)
src_stage = VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_2_LATE_FRAGMENT_TESTS_BIT;
src_access = VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
dst_stage = VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_2_LATE_FRAGMENT_TESTS_BIT;
dst_access = VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
aspect = VK_IMAGE_ASPECT_DEPTH_BIT;
}
else if (_is_layout_pair(VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR))
{
src_stage = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;
src_access = VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT;
dst_stage = VK_PIPELINE_STAGE_2_NONE;
dst_access = VK_ACCESS_2_NONE;
}
else
{
printf("Unknown layout transition\n");
return false;
}
#undef _is_layout_pair
VkImageMemoryBarrier2 barrier{
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2,
.srcStageMask = src_stage,
.srcAccessMask = src_access,
.dstStageMask = dst_stage,
.dstAccessMask = dst_access,
.oldLayout = old_layout,
.newLayout = new_layout,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.image = image,
.subresourceRange =
{
.aspectMask = aspect,
.baseMipLevel = base_mip_level,
.levelCount = level_count,
.baseArrayLayer = base_array_layer,
.layerCount = layer_count,
},
};
VkDependencyInfo dep_info{
.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO,
.imageMemoryBarrierCount = 1,
.pImageMemoryBarriers = &barrier,
};
vkCmdPipelineBarrier2(cmd, &dep_info);
return true;
}
NOTE: The UNDEFINED → DEPTH_ATTACHMENT_OPTIMAL case isn’t used in this post, but we’ll need it once we get to depth buffers, so I’ve included it here already. Notice that its source isn’t empty: a depth image usually gets reused every frame, so the transition has to wait for the depth writes from the previous frame to finish. Otherwise, clearing it for the new frame could race with the previous frame still writing to it! On the destination side, depth tests both read and write the depth buffer, so we include both accesses.
srcQueueFamilyIndex and dstQueueFamilyIndex are used for transferring image ownership between queue families. We don’t do that (our swapchain images use concurrent sharing when the families differ), so we set both to VK_QUEUE_FAMILY_IGNORED.
Drawing Frames
We’re finally here! After all that setup, the actual drawing is almost anticlimactic.
In the Vulkan 1.0 days, before we could draw anything, we would have had to create a VkRenderPass describing all of our attachments and how they’re used, and then a VkFramebuffer for each swapchain image, tying the image views to that render pass (and recreate all of those framebuffers whenever the swapchain gets recreated). With Vulkan 1.3’s dynamic rendering, we skip all of that, and just describe our attachments right when we begin rendering:
private:
void draw_frame(VkCommandBuffer cmd, VkImageView color_attachment);
void SampleVulkan::draw_frame(VkCommandBuffer cmd, VkImageView color_attachment)
{
VkRenderingAttachmentInfo color_attachment_info{
.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO,
.imageView = color_attachment,
.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR,
.storeOp = VK_ATTACHMENT_STORE_OP_STORE,
.clearValue =
{
.color =
{
0.2f,
0.4f,
0.8f,
1.0f,
},
},
};
VkRenderingInfo rendering_info{
.sType = VK_STRUCTURE_TYPE_RENDERING_INFO,
.renderArea =
{
{},
m_swapchain_extent,
},
.layerCount = 1,
.viewMask = 0,
.colorAttachmentCount = 1,
.pColorAttachments = &color_attachment_info,
};
vkCmdBeginRendering(cmd, &rendering_info);
vkCmdEndRendering(cmd);
}
imageLayout: The layout the image will be in while we render into it. This is why we transitioned the image toCOLOR_ATTACHMENT_OPTIMALbefore callingdraw_frame.loadOp: What happens to the attachment’s contents when rendering begins.VK_ATTACHMENT_LOAD_OP_CLEARclears it withclearValue. This is where our colour actually comes from! (Other options areLOAD, which keeps the existing contents, andDONT_CARE, which leaves them undefined.)storeOp: What happens to what we’ve rendered when rendering ends. We want to present it, so we definitely want toSTOREit.clearValue: Our lovely blue, as RGBA values between0.0and1.0. Go ahead and change it to your favourite colour!renderArea: The region of the attachments we’re rendering to. We want the whole image, so we start at{0, 0}(the{}) and use the full swapchain extent.
Notice that there’s nothing between vkCmdBeginRendering and vkCmdEndRendering. The clear happens because of the loadOp alone, so we don’t need to draw anything to get our coloured window. Later on in the series, this is exactly where our draw commands will go.
Cleanup
Last but not least, we need to clean up after ourselves. Unlike OpenGL, Vulkan won’t clean anything up for us, and every object we created must be destroyed, in the reverse order of creation (objects created from the device before the device itself, and so on). The validation layers will complain loudly if we forget something!
SampleVulkan::~SampleVulkan()
{
if (m_device)
{
// Make sure the GPU is done with everything before we start destroying things
vkDeviceWaitIdle(m_device);
for (uint32_t i = 0; i < NUM_CONCURRENT_FRAMES; i++)
{
vkDestroyFence(m_device, m_frames[i].fen_in_flight, nullptr);
vkDestroySemaphore(m_device, m_frames[i].sm_image_available, nullptr);
}
for (size_t i = 0; i < m_render_done_semaphores.size(); i++)
vkDestroySemaphore(m_device, m_render_done_semaphores[i], nullptr);
m_render_done_semaphores.clear();
// Destroying the command pool also frees all the command buffers allocated from it
vkDestroyCommandPool(m_device, m_frame_resource_command_pool, nullptr);
for (const auto &view : m_swapchain_image_views)
vkDestroyImageView(m_device, view, nullptr);
// The swapchain images themselves are owned by the swapchain, so we don't destroy them
vkDestroySwapchainKHR(m_device, m_swapchain, nullptr);
vkDestroyDevice(m_device, nullptr);
}
if (m_instance)
{
if (m_surface)
vkDestroySurfaceKHR(m_instance, m_surface, nullptr);
{
const auto debug_messenger_destroy_func =
(PFN_vkDestroyDebugUtilsMessengerEXT) vkGetInstanceProcAddr(m_instance, "vkDestroyDebugUtilsMessengerEXT");
if (debug_messenger_destroy_func && m_debug_messenger)
debug_messenger_destroy_func(m_instance, m_debug_messenger, nullptr);
}
vkDestroyInstance(m_instance, nullptr);
}
}
Note that the physical device and the queues are not destroyed, since we never created them, we just retrieved handles to them. They go away along with the instance and the device respectively.
Wrapping up
And that’s it! Run it, and you should be greeted by a window cleared with a nice shade of blue. Resize it, and the swapchain gets recreated to match. If you’ve got the Vulkan SDK installed, the validation layers are watching over everything we do, so keep an eye on the console for any [VK_VALIDATION_LAYER] messages.
Let’s recap what it took to get here:
- Created a Vulkan instance (with validation layers and a debug messenger)
- Created a surface for our GLFW window
- Picked a physical device that supports Vulkan 1.3, with graphics and present capable queue families
- Created a logical device with the queues, extensions and features we need
- Created a swapchain (and image views for its images), and handled recreating it on resize
- Created per frame resources (command buffers, semaphores and fences) for multiple frames in flight
- Every frame: acquired an image, transitioned its layout, cleared it using dynamic rendering, transitioned it again, submitted our commands and presented the image
Now you know why I said you might appreciate a blue window a lot more by the end of this! The good news is, most of this is a one time setup. From here on in the series, we’ll be building on top of this foundation, and things will start to get a lot more visual.
Next up: Coloured Triangle
In the next post, we’ll finally draw some actual geometry: the classic coloured triangle! We’ll write our first shaders, upload our vertex data to the GPU with vertex and index buffers, and create a graphics pipeline to tie it all together. Each vertex will carry its own colour, and we’ll let the GPU interpolate those colours across the triangle. Remember that empty space between vkCmdBeginRendering and vkCmdEndRendering? That’s where our draw commands will go!
References
- Vulkan Tutorial
- VkGuide
- The Vulkan specification (version 1.4.364 at the time of writing)
How this post was written
While the sample’s source code was hand-written by I-A-S and he designed the article and its structure, we did use Claude Opus 5.5 to write the textual contents of this article. Claude also helped proof-read and review the code for any human mistakes, of which Claude did find a few and Claude also added macOS support. All of which are fixed in the code presented here.
Full Source Code
The complete sample in a single file, around 900 lines.
Show full source (main.cpp)
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <cassert>
#include <algorithm>
#include <functional>
#include <memory>
#include <set>
#include <string>
#include <utility>
#include <vector>
#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>
class SampleVulkan
{
static constexpr uint32_t NUM_CONCURRENT_FRAMES = 3;
static constexpr const char *VALIDATION_LAYER_NAME = "VK_LAYER_KHRONOS_validation";
static constexpr const char *SWAPCHAIN_DEVICE_EXTENSION_NAME = "VK_KHR_swapchain";
static constexpr const char *PORTABILITY_SUBSET_DEVICE_EXTENSION_NAME = "VK_KHR_portability_subset";
struct FrameResourceSet
{
VkFence fen_in_flight{};
VkSemaphore sm_image_available{};
VkCommandBuffer cmd_buffer{};
};
public:
SampleVulkan() = default;
SampleVulkan(const SampleVulkan &) = delete;
SampleVulkan &operator=(const SampleVulkan &) = delete;
~SampleVulkan();
bool initialize(std::function<bool(VkInstance, uint32_t &, uint32_t &, VkSurfaceKHR &)> surface_creation_callback);
bool render();
void resize(uint32_t fb_width, uint32_t fb_height);
private:
bool create_instance();
bool select_physical_device();
bool create_device_and_swapchain();
bool create_frame_resources();
void draw_frame(VkCommandBuffer cmd, VkImageView color_attachment);
bool recreate_swapchain();
bool transition_image_layout(VkCommandBuffer cmd, VkImage image, VkImageLayout old_layout, VkImageLayout new_layout,
uint32_t base_mip_level = 0, uint32_t level_count = 1, uint32_t base_array_layer = 0,
uint32_t layer_count = 1);
private:
VkInstance m_instance{};
VkDebugUtilsMessengerEXT m_debug_messenger{};
VkSurfaceKHR m_surface{};
std::string m_physical_device_name{};
VkPhysicalDevice m_physical_device{};
uint32_t m_graphics_queue_family_index{};
uint32_t m_present_queue_family_index{};
uint32_t m_swapchain_image_count{};
std::vector<VkSemaphore> m_render_done_semaphores;
VkSurfaceFormatKHR m_swapchain_format{};
VkPresentModeKHR m_swapchain_present_mode{};
VkExtent2D m_swapchain_extent{1, 1};
VkExtent2D m_pending_swapchain_extent{1, 1};
bool m_swapchain_is_out_of_date{};
VkSurfaceTransformFlagBitsKHR m_swapchain_pre_transform{};
VkDevice m_device{};
VkQueue m_graphics_queue{};
VkQueue m_present_queue{};
VkSwapchainKHR m_swapchain{};
std::vector<VkImage> m_swapchain_images{};
std::vector<VkImageView> m_swapchain_image_views{};
uint32_t m_current_frame_index{};
VkCommandPool m_frame_resource_command_pool{};
FrameResourceSet m_frames[NUM_CONCURRENT_FRAMES];
};
class SampleApp
{
static constexpr uint32_t SCREEN_WIDTH = 800;
static constexpr uint32_t SCREEN_HEIGHT = 600;
public:
int launch(int argc, char *argv[]);
~SampleApp();
private:
GLFWwindow *m_window{};
std::unique_ptr<SampleVulkan> m_vulkan{};
};
int main(int argc, char *argv[])
{
SampleApp app;
return app.launch(argc, argv);
}
// -----------------------------------------------------------------------------
int SampleApp::launch(int argc, char *argv[])
{
if (!glfwInit())
{
printf("Failed to initialize GLFW\n");
return -1;
}
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
m_window = glfwCreateWindow(SCREEN_WIDTH, SCREEN_HEIGHT, "i-a-s.me Vulkan Sample", nullptr, nullptr);
if (!m_window)
{
printf("Failed to create GLFW window\n");
return -1;
}
m_vulkan = std::make_unique<SampleVulkan>();
if (!m_vulkan->initialize([window = m_window](VkInstance instance, uint32_t &out_width, uint32_t &out_height,
VkSurfaceKHR &out_surface) -> bool {
if (glfwCreateWindowSurface(instance, window, nullptr, &out_surface) != VK_SUCCESS)
{
printf("Failed to create Vulkan surface\n");
return false;
}
int32_t fb_width{}, fb_height{};
glfwGetFramebufferSize(window, &fb_width, &fb_height);
while ((fb_width == 0) || (fb_height == 0))
{
glfwWaitEvents();
glfwGetFramebufferSize(window, &fb_width, &fb_height);
}
out_width = (uint32_t) fb_width;
out_height = (uint32_t) fb_height;
return true;
}))
{
printf("Failed to initialize Vulkan\n");
return -1;
}
glfwSetWindowUserPointer(m_window, m_vulkan.get());
glfwSetFramebufferSizeCallback(m_window, [](GLFWwindow *window, int width, int height) {
int32_t fb_width{}, fb_height{};
glfwGetFramebufferSize(window, &fb_width, &fb_height);
if ((fb_width == 0) || (fb_height == 0))
return;
static_cast<SampleVulkan *>(glfwGetWindowUserPointer(window))->resize(fb_width, fb_height);
});
while (!glfwWindowShouldClose(m_window))
{
if (glfwGetKey(m_window, GLFW_KEY_ESCAPE))
glfwSetWindowShouldClose(m_window, true);
glfwPollEvents();
// Don't render while the window is minimized (its framebuffer is 0x0 then)
int32_t fb_width{}, fb_height{};
glfwGetFramebufferSize(m_window, &fb_width, &fb_height);
if ((fb_width == 0) || (fb_height == 0))
{
glfwWaitEvents();
continue;
}
if (!m_vulkan->render())
break;
}
return 0;
}
SampleApp::~SampleApp()
{
m_vulkan.reset();
if (m_window)
glfwDestroyWindow(m_window);
glfwTerminate();
}
// -----------------------------------------------------------------------------
template<typename ElementT, typename FuncT, typename... Args> std::vector<ElementT> vkEnumCall(FuncT f, Args... args)
{
uint32_t _count{};
f(args..., &_count, nullptr);
std::vector<ElementT> result(_count);
f(args..., &_count, result.data());
return result;
}
SampleVulkan::~SampleVulkan()
{
if (m_device)
{
vkDeviceWaitIdle(m_device);
for (uint32_t i = 0; i < NUM_CONCURRENT_FRAMES; i++)
{
vkDestroyFence(m_device, m_frames[i].fen_in_flight, nullptr);
vkDestroySemaphore(m_device, m_frames[i].sm_image_available, nullptr);
}
for (size_t i = 0; i < m_render_done_semaphores.size(); i++)
vkDestroySemaphore(m_device, m_render_done_semaphores[i], nullptr);
m_render_done_semaphores.clear();
vkDestroyCommandPool(m_device, m_frame_resource_command_pool, nullptr);
for (const auto &view : m_swapchain_image_views)
vkDestroyImageView(m_device, view, nullptr);
vkDestroySwapchainKHR(m_device, m_swapchain, nullptr);
vkDestroyDevice(m_device, nullptr);
}
if (m_instance)
{
if (m_surface)
vkDestroySurfaceKHR(m_instance, m_surface, nullptr);
{
const auto debug_messenger_destroy_func =
(PFN_vkDestroyDebugUtilsMessengerEXT) vkGetInstanceProcAddr(m_instance, "vkDestroyDebugUtilsMessengerEXT");
if (debug_messenger_destroy_func && m_debug_messenger)
debug_messenger_destroy_func(m_instance, m_debug_messenger, nullptr);
}
vkDestroyInstance(m_instance, nullptr);
}
}
bool SampleVulkan::initialize(
std::function<bool(VkInstance, uint32_t &, uint32_t &, VkSurfaceKHR &)> surface_creation_callback)
{
if (!create_instance())
return false;
if (!surface_creation_callback(m_instance, m_pending_swapchain_extent.width, m_pending_swapchain_extent.height,
m_surface))
{
printf("Failed to create Vulkan surface\n");
return false;
}
if (!select_physical_device())
return false;
if (!create_device_and_swapchain())
return false;
if (!create_frame_resources())
return false;
return true;
}
bool SampleVulkan::render()
{
const auto ¤t_frame = m_frames[m_current_frame_index];
vkWaitForFences(m_device, 1, ¤t_frame.fen_in_flight, VK_TRUE, UINT64_MAX);
uint32_t image_index{};
switch (vkAcquireNextImageKHR(m_device, m_swapchain, UINT64_MAX, current_frame.sm_image_available, VK_NULL_HANDLE,
&image_index))
{
case VK_ERROR_OUT_OF_DATE_KHR:
if (!recreate_swapchain())
return false;
return true;
case VK_SUCCESS:
case VK_SUBOPTIMAL_KHR:
break;
default:
printf("Failed to acquire swapchain image\n");
return false;
}
VkCommandBufferBeginInfo cmd_begin_info{
.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
};
vkResetCommandBuffer(current_frame.cmd_buffer, 0);
if (vkBeginCommandBuffer(current_frame.cmd_buffer, &cmd_begin_info) != VK_SUCCESS)
{
printf("Failed to start recording command buffer\n");
return false;
}
if (!transition_image_layout(current_frame.cmd_buffer, m_swapchain_images[image_index], VK_IMAGE_LAYOUT_UNDEFINED,
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL))
{
printf("Failed to transition swapchain image\n");
return false;
}
draw_frame(current_frame.cmd_buffer, m_swapchain_image_views[image_index]);
if (!transition_image_layout(current_frame.cmd_buffer, m_swapchain_images[image_index],
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR))
{
printf("Failed to transition swapchain image\n");
return false;
}
if (vkEndCommandBuffer(current_frame.cmd_buffer) != VK_SUCCESS)
{
printf("Failed to finish recording command buffer\n");
return false;
}
VkPipelineStageFlags submit_pipeline_wait_stage{VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT};
VkSubmitInfo submit_info{
.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
.waitSemaphoreCount = 1,
.pWaitSemaphores = ¤t_frame.sm_image_available,
.pWaitDstStageMask = &submit_pipeline_wait_stage,
.commandBufferCount = 1,
.pCommandBuffers = ¤t_frame.cmd_buffer,
.signalSemaphoreCount = 1,
.pSignalSemaphores = &m_render_done_semaphores[image_index],
};
vkResetFences(m_device, 1, ¤t_frame.fen_in_flight);
if (vkQueueSubmit(m_graphics_queue, 1, &submit_info, current_frame.fen_in_flight) != VK_SUCCESS)
{
printf("Failed to submit frame command buffer\n");
return false;
}
VkPresentInfoKHR present_info{
.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR,
.waitSemaphoreCount = 1,
.pWaitSemaphores = &m_render_done_semaphores[image_index],
.swapchainCount = 1,
.pSwapchains = &m_swapchain,
.pImageIndices = &image_index,
};
switch (vkQueuePresentKHR(m_present_queue, &present_info))
{
case VK_ERROR_OUT_OF_DATE_KHR:
case VK_SUBOPTIMAL_KHR:
if (!recreate_swapchain())
return false;
break;
case VK_SUCCESS:
break;
default:
printf("Failed to present swapchain\n");
return false;
}
if (m_swapchain_is_out_of_date && !recreate_swapchain())
return false;
m_current_frame_index = (m_current_frame_index + 1) % NUM_CONCURRENT_FRAMES;
return true;
}
void SampleVulkan::draw_frame(VkCommandBuffer cmd, VkImageView color_attachment)
{
VkRenderingAttachmentInfo color_attachment_info{
.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO,
.imageView = color_attachment,
.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR,
.storeOp = VK_ATTACHMENT_STORE_OP_STORE,
.clearValue =
{
.color =
{
0.2f,
0.4f,
0.8f,
1.0f,
},
},
};
VkRenderingInfo rendering_info{
.sType = VK_STRUCTURE_TYPE_RENDERING_INFO,
.renderArea =
{
{},
m_swapchain_extent,
},
.layerCount = 1,
.viewMask = 0,
.colorAttachmentCount = 1,
.pColorAttachments = &color_attachment_info,
};
vkCmdBeginRendering(cmd, &rendering_info);
vkCmdEndRendering(cmd);
}
bool SampleVulkan::transition_image_layout(VkCommandBuffer cmd, VkImage image, VkImageLayout old_layout,
VkImageLayout new_layout, uint32_t base_mip_level, uint32_t level_count,
uint32_t base_array_layer, uint32_t layer_count)
{
VkPipelineStageFlags2 src_stage{}, dst_stage{};
VkAccessFlags2 src_access{}, dst_access{};
VkImageAspectFlags aspect{VK_IMAGE_ASPECT_COLOR_BIT};
#define _is_layout_pair(o, n) ((old_layout == o) && (new_layout == n))
if (_is_layout_pair(VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL))
{
src_stage = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;
src_access = VK_ACCESS_2_NONE;
dst_stage = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;
dst_access = VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT;
}
else if (_is_layout_pair(VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL))
{
// Wait for any depth writes from a previous use of this image (e.g. the previous frame)
src_stage = VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_2_LATE_FRAGMENT_TESTS_BIT;
src_access = VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
dst_stage = VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_2_LATE_FRAGMENT_TESTS_BIT;
dst_access = VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
aspect = VK_IMAGE_ASPECT_DEPTH_BIT;
}
else if (_is_layout_pair(VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR))
{
src_stage = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;
src_access = VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT;
dst_stage = VK_PIPELINE_STAGE_2_NONE;
dst_access = VK_ACCESS_2_NONE;
}
else
{
printf("Unknown layout transition\n");
return false;
}
#undef _is_layout_pair
VkImageMemoryBarrier2 barrier{
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2,
.srcStageMask = src_stage,
.srcAccessMask = src_access,
.dstStageMask = dst_stage,
.dstAccessMask = dst_access,
.oldLayout = old_layout,
.newLayout = new_layout,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.image = image,
.subresourceRange =
{
.aspectMask = aspect,
.baseMipLevel = base_mip_level,
.levelCount = level_count,
.baseArrayLayer = base_array_layer,
.layerCount = layer_count,
},
};
VkDependencyInfo dep_info{
.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO,
.imageMemoryBarrierCount = 1,
.pImageMemoryBarriers = &barrier,
};
vkCmdPipelineBarrier2(cmd, &dep_info);
return true;
}
bool SampleVulkan::create_instance()
{
// This is a sample, not production code, so we always enable validation layers,
// unless they're unavailable
const auto instance_layer_props = vkEnumCall<VkLayerProperties>(vkEnumerateInstanceLayerProperties);
const bool enable_validation_layers =
std::any_of(instance_layer_props.begin(), instance_layer_props.end(),
[](VkLayerProperties p) { return strcmp(p.layerName, VALIDATION_LAYER_NAME) == 0; });
VkDebugUtilsMessengerCreateInfoEXT debug_messenger_create_info{
.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT,
.messageSeverity =
VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT,
.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT |
VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT,
.pfnUserCallback = [](VkDebugUtilsMessageSeverityFlagBitsEXT severity, VkDebugUtilsMessageTypeFlagsEXT type,
const VkDebugUtilsMessengerCallbackDataEXT *callback_data, void *user_data) -> VkBool32 {
printf("[VK_VALIDATION_LAYER]: %s\n", callback_data->pMessage);
return VK_FALSE;
},
};
std::vector<const char *> extensions;
{ // GLFW Extensions
uint32_t glfw_ext_count{};
const auto glfw_ext = glfwGetRequiredInstanceExtensions(&glfw_ext_count);
extensions.insert(extensions.end(), glfw_ext, glfw_ext + glfw_ext_count);
}
if (enable_validation_layers)
extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
// macOS support: MoltenVK is a "portability" implementation (it doesn't support all
// of Vulkan), so the loader only exposes it to us if we explicitly opt in.
const auto instance_ext_props = vkEnumCall<VkExtensionProperties>(vkEnumerateInstanceExtensionProperties, nullptr);
const bool enable_portability =
std::any_of(instance_ext_props.begin(), instance_ext_props.end(), [](VkExtensionProperties p) {
return strcmp(p.extensionName, VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME) == 0;
});
if (enable_portability)
extensions.push_back(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME);
VkApplicationInfo app_info{
.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO,
.pApplicationName = "i-a-s.me Vulkan 1.3 Sample",
.applicationVersion = VK_MAKE_VERSION(1, 0, 0),
.pEngineName = "i-a-s.me Vulkan 1.3 Sample",
.engineVersion = VK_MAKE_VERSION(1, 0, 0),
.apiVersion = VK_API_VERSION_1_3,
};
VkInstanceCreateInfo create_info{
.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
.pNext = enable_validation_layers ? &debug_messenger_create_info : nullptr,
.flags = enable_portability ? (VkInstanceCreateFlags) VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR : 0u,
.pApplicationInfo = &app_info,
.enabledLayerCount = enable_validation_layers ? 1u : 0u,
.ppEnabledLayerNames = enable_validation_layers ? &VALIDATION_LAYER_NAME : nullptr,
.enabledExtensionCount = (uint32_t) extensions.size(),
.ppEnabledExtensionNames = extensions.data(),
};
if (vkCreateInstance(&create_info, nullptr, &m_instance) != VK_SUCCESS)
{
printf("Failed to create Vulkan instance\n");
return false;
}
const auto debug_messenger_create_func =
(PFN_vkCreateDebugUtilsMessengerEXT) vkGetInstanceProcAddr(m_instance, "vkCreateDebugUtilsMessengerEXT");
if (enable_validation_layers && debug_messenger_create_func)
{
if (debug_messenger_create_func(m_instance, &debug_messenger_create_info, nullptr, &m_debug_messenger) !=
VK_SUCCESS)
{
printf("Failed to create Vulkan debug messenger\n");
return false;
}
}
return true;
}
bool SampleVulkan::select_physical_device()
{
const auto find_queue_families = [&](VkPhysicalDevice p) -> std::pair<uint32_t, uint32_t> {
uint32_t graphics_family_index{UINT32_MAX}, present_family_index{UINT32_MAX};
const auto queue_families = vkEnumCall<VkQueueFamilyProperties>(vkGetPhysicalDeviceQueueFamilyProperties, p);
for (uint32_t i = 0; i < (uint32_t) queue_families.size(); i++)
{
const auto &f = queue_families[i];
if (f.queueFlags & VK_QUEUE_GRAPHICS_BIT)
graphics_family_index = i;
VkBool32 has_present_support{};
if ((vkGetPhysicalDeviceSurfaceSupportKHR(p, i, m_surface, &has_present_support) == VK_SUCCESS) &&
has_present_support)
present_family_index = i;
if ((graphics_family_index != UINT32_MAX) && (present_family_index != UINT32_MAX))
break;
}
return {graphics_family_index, present_family_index};
};
m_physical_device = VK_NULL_HANDLE;
const auto physical_devices = vkEnumCall<VkPhysicalDevice>(vkEnumeratePhysicalDevices, m_instance);
for (const auto &p : physical_devices)
{
VkPhysicalDeviceProperties2 props{
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2,
};
vkGetPhysicalDeviceProperties2(p, &props);
if (props.properties.apiVersion < VK_API_VERSION_1_3)
continue; // If this physical device does not support at least Vulkan 1.3, we cannot use it. We use Vulkan 1.3
// core features in this sample.
const auto queue_family_indices = find_queue_families(p);
if ((queue_family_indices.first == UINT32_MAX) || (queue_family_indices.second == UINT32_MAX))
continue; // This physical device isn't what we want, if it lacks either graphics or present queue families
// We could (and should) do more suitability checks in a real application,
// but for this sample, this is just ok.
m_physical_device = p;
m_graphics_queue_family_index = queue_family_indices.first;
m_present_queue_family_index = queue_family_indices.second;
m_physical_device_name = props.properties.deviceName;
if (props.properties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU)
break; // If we find a discrete GPU, then prefer it
}
if (m_physical_device == VK_NULL_HANDLE)
{
printf("Failed to find a good physical device\n");
return false;
}
printf("Using physical device \"%s\"\n", m_physical_device_name.c_str());
return true;
}
void SampleVulkan::resize(uint32_t fb_width, uint32_t fb_height)
{
m_pending_swapchain_extent = {fb_width, fb_height};
m_swapchain_is_out_of_date = true;
}
bool SampleVulkan::create_device_and_swapchain()
{
const float priority = 1.0f;
std::vector<VkDeviceQueueCreateInfo> queue_create_infos;
// passing through std::set effectively dedups family indices.
// not really necessary to use std::set here as we only have to consider
// 2 families (graphics and present), so we could have just checked (family_index_1 == family_index_2),
// but this std::set approach generalizes to higher orders (imagine doing if/else checks for combinations of 3 or 4)
for (const auto index : std::set<uint32_t>{m_graphics_queue_family_index, m_present_queue_family_index})
queue_create_infos.push_back(VkDeviceQueueCreateInfo{
.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
.queueFamilyIndex = index,
.queueCount = 1,
.pQueuePriorities = &priority,
});
std::vector<const char *> device_extensions{SWAPCHAIN_DEVICE_EXTENSION_NAME};
// macOS support: if the device is a portability implementation (i.e. MoltenVK),
// it exposes VK_KHR_portability_subset, and the spec requires us to enable it.
const auto device_ext_props =
vkEnumCall<VkExtensionProperties>(vkEnumerateDeviceExtensionProperties, m_physical_device, nullptr);
if (std::any_of(device_ext_props.begin(), device_ext_props.end(), [](VkExtensionProperties p) {
return strcmp(p.extensionName, PORTABILITY_SUBSET_DEVICE_EXTENSION_NAME) == 0;
}))
device_extensions.push_back(PORTABILITY_SUBSET_DEVICE_EXTENSION_NAME);
VkPhysicalDeviceFeatures enabled_features{};
VkPhysicalDeviceVulkan13Features vk_13_features{
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES,
.synchronization2 = VK_TRUE,
.dynamicRendering = VK_TRUE,
};
VkDeviceCreateInfo device_create_info{
.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,
.pNext = &vk_13_features,
.queueCreateInfoCount = (uint32_t) queue_create_infos.size(),
.pQueueCreateInfos = queue_create_infos.data(),
.enabledLayerCount = 0,
.ppEnabledLayerNames = nullptr,
.enabledExtensionCount = (uint32_t) device_extensions.size(),
.ppEnabledExtensionNames = device_extensions.data(),
.pEnabledFeatures = &enabled_features,
};
if (vkCreateDevice(m_physical_device, &device_create_info, nullptr, &m_device) != VK_SUCCESS)
{
printf("Failed to create Vulkan device\n");
return false;
}
vkGetDeviceQueue(m_device, m_graphics_queue_family_index, 0, &m_graphics_queue);
vkGetDeviceQueue(m_device, m_present_queue_family_index, 0, &m_present_queue);
const auto surface_formats =
vkEnumCall<VkSurfaceFormatKHR>(vkGetPhysicalDeviceSurfaceFormatsKHR, m_physical_device, m_surface);
const auto surface_present_modes =
vkEnumCall<VkPresentModeKHR>(vkGetPhysicalDeviceSurfacePresentModesKHR, m_physical_device, m_surface);
assert(surface_formats.size());
assert(surface_present_modes.size());
m_swapchain_format = surface_formats[0];
m_swapchain_present_mode = (std::find(surface_present_modes.begin(), surface_present_modes.end(),
VK_PRESENT_MODE_MAILBOX_KHR) != surface_present_modes.end())
? VK_PRESENT_MODE_MAILBOX_KHR
: VK_PRESENT_MODE_FIFO_KHR;
return recreate_swapchain();
}
bool SampleVulkan::create_frame_resources()
{
VkCommandPoolCreateInfo commmand_pool_create_info{
.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO,
.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT,
.queueFamilyIndex = m_graphics_queue_family_index,
};
if (vkCreateCommandPool(m_device, &commmand_pool_create_info, nullptr, &m_frame_resource_command_pool) != VK_SUCCESS)
{
printf("Failed to create command pool\n");
return false;
}
VkCommandBufferAllocateInfo command_buffer_alloc_info{
.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,
.commandPool = m_frame_resource_command_pool,
.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY,
.commandBufferCount = 1,
};
for (uint32_t i = 0; i < NUM_CONCURRENT_FRAMES; i++)
{
if (vkAllocateCommandBuffers(m_device, &command_buffer_alloc_info, &m_frames[i].cmd_buffer) != VK_SUCCESS)
{
printf("Failed to allocate frame command buffer\n");
return false;
}
VkSemaphoreCreateInfo semaphore_create_info{
.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO,
};
if (vkCreateSemaphore(m_device, &semaphore_create_info, nullptr, &m_frames[i].sm_image_available) != VK_SUCCESS)
{
printf("Failed to create a semaphore\n");
return false;
}
VkFenceCreateInfo fence_create_info{
.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO,
.flags = VK_FENCE_CREATE_SIGNALED_BIT,
};
if (vkCreateFence(m_device, &fence_create_info, nullptr, &m_frames[i].fen_in_flight) != VK_SUCCESS)
{
printf("Failed to create a fence\n");
return false;
}
}
return true;
}
bool SampleVulkan::recreate_swapchain()
{
vkDeviceWaitIdle(m_device);
VkSurfaceCapabilitiesKHR surface_caps{};
if (vkGetPhysicalDeviceSurfaceCapabilitiesKHR(m_physical_device, m_surface, &surface_caps) != VK_SUCCESS)
{
printf("Failed to query physical device surface caps\n");
return false;
}
// A minimized window has a 0x0 surface, and a 0x0 swapchain is not allowed. So we
// keep the current swapchain, and try again later (once the window is restored).
if ((surface_caps.maxImageExtent.width == 0) || (surface_caps.maxImageExtent.height == 0))
{
m_swapchain_is_out_of_date = true;
return true;
}
m_swapchain_image_count = std::min(surface_caps.minImageCount + 1,
(surface_caps.maxImageCount == 0) ? UINT32_MAX : surface_caps.maxImageCount);
m_swapchain_pre_transform = surface_caps.currentTransform;
m_pending_swapchain_extent.width = std::clamp(m_pending_swapchain_extent.width, surface_caps.minImageExtent.width,
surface_caps.maxImageExtent.width);
m_pending_swapchain_extent.height = std::clamp(m_pending_swapchain_extent.height, surface_caps.minImageExtent.height,
surface_caps.maxImageExtent.height);
m_swapchain_extent = m_pending_swapchain_extent;
for (const auto &view : m_swapchain_image_views)
vkDestroyImageView(m_device, view, nullptr);
if (m_swapchain)
vkDestroySwapchainKHR(m_device, m_swapchain, nullptr);
m_swapchain_image_views.clear();
m_swapchain = VK_NULL_HANDLE;
VkSwapchainCreateInfoKHR swapchain_create_info{
.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR,
.surface = m_surface,
.minImageCount = m_swapchain_image_count,
.imageFormat = m_swapchain_format.format,
.imageColorSpace = m_swapchain_format.colorSpace,
.imageExtent = m_swapchain_extent,
.imageArrayLayers = 1,
.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT,
.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE,
.preTransform = m_swapchain_pre_transform,
.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR,
.presentMode = m_swapchain_present_mode,
.clipped = VK_TRUE,
};
uint32_t queue_family_indices[] = {m_graphics_queue_family_index, m_present_queue_family_index};
if (m_graphics_queue_family_index != m_present_queue_family_index)
{
swapchain_create_info.imageSharingMode = VK_SHARING_MODE_CONCURRENT;
swapchain_create_info.queueFamilyIndexCount = 2;
swapchain_create_info.pQueueFamilyIndices = queue_family_indices;
}
if (vkCreateSwapchainKHR(m_device, &swapchain_create_info, nullptr, &m_swapchain) != VK_SUCCESS)
{
printf("Failed to create swapchain\n");
return false;
}
m_swapchain_images = vkEnumCall<VkImage>(vkGetSwapchainImagesKHR, m_device, m_swapchain);
// Note that we use the *actual* number of images here, which can be
// more than the minImageCount we asked for.
for (auto semaphore : m_render_done_semaphores)
vkDestroySemaphore(m_device, semaphore, nullptr);
m_render_done_semaphores.assign(m_swapchain_images.size(), VK_NULL_HANDLE);
VkSemaphoreCreateInfo semaphore_create_info{
.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO,
};
for (auto &semaphore : m_render_done_semaphores)
{
if (vkCreateSemaphore(m_device, &semaphore_create_info, nullptr, &semaphore) != VK_SUCCESS)
{
printf("Failed to create a semaphore\n");
return false;
}
}
m_swapchain_image_views.reserve(m_swapchain_images.size());
for (const auto &image : m_swapchain_images)
{
VkImageViewCreateInfo create_info{
.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
.image = image,
.viewType = VK_IMAGE_VIEW_TYPE_2D,
.format = m_swapchain_format.format,
.components =
{
.r = VK_COMPONENT_SWIZZLE_IDENTITY,
.g = VK_COMPONENT_SWIZZLE_IDENTITY,
.b = VK_COMPONENT_SWIZZLE_IDENTITY,
.a = VK_COMPONENT_SWIZZLE_IDENTITY,
},
.subresourceRange =
{
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
.baseMipLevel = 0,
.levelCount = 1,
.baseArrayLayer = 0,
.layerCount = 1,
},
};
VkImageView view{};
if (vkCreateImageView(m_device, &create_info, nullptr, &view) != VK_SUCCESS)
{
printf("Failed to create a swapchain image view\n");
return false;
}
m_swapchain_image_views.push_back(view);
}
m_swapchain_is_out_of_date = false;
return true;
}