Content
1.
2.
3.
4.
5.
6. Uniform-
7.
8.
9.
10. -
11. Multisampling
FAQ
2.
3.
4.
-
-
- Window surface
- Swap chain
- Image views
- (pipeline)
5.
- Staging
6. Uniform-
- layout
- sets
7.
- Image view image sampler
- image sampler
8.
9.
10. -
11. Multisampling
FAQ
Swap chain
- Checking swap chain support
- Connecting extensions
- Swap chain support information request
- Choosing settings for swap chain
- Swap chain creation
- Getting an image from a swap chain
Vulkan has no such thing as a default framebuffer, so it needs an infrastructure with buffers to render images to before being displayed. This infrastructure is called a swap chain and must be explicitly created in Vulkan. Swap chain is a queue of images waiting to be displayed on the screen. The program first asks for an object
image(VkImage)
to draw into, and after rendering, sends it back to the queue. Exactly how the queue works depends on the settings, but the main task of the swap chain is to synchronize the display of images with the screen refresh rate.
Checking swap chain support
Some specialized video cards do not have display outputs and therefore cannot display images on the screen. In addition, screen mapping is tied to the window system and is not part of the Vulkan core. Therefore, we need to connect the extension
VK_KHR_swapchain
.
First
isDeviceSuitable
, let's change the function to check if the extension is supported. We have already worked with the list of supported extensions before, so there shouldn't be any difficulties. Note that the Vulkan header file provides a handy macro
VK_KHR_SWAPCHAIN_EXTENSION_NAME
that is defined as "
VK_KHR_swapchain
". The advantage of this macro is that if you make a spelling mistake, the compiler will warn you about it.
Let's start by declaring a list of required extensions.
const std::vector<const char*> deviceExtensions = {
VK_KHR_SWAPCHAIN_EXTENSION_NAME
};
For additional verification, let's create a new function
checkDeviceExtensionSupport
called from
isDeviceSuitable
:
bool isDeviceSuitable(VkPhysicalDevice device) {
QueueFamilyIndices indices = findQueueFamilies(device);
bool extensionsSupported = checkDeviceExtensionSupport(device);
return indices.isComplete() && extensionsSupported;
}
bool checkDeviceExtensionSupport(VkPhysicalDevice device) {
return true;
}
Let's change the body of the function to check if all the extensions we need are in the list of supported ones.
bool checkDeviceExtensionSupport(VkPhysicalDevice device) {
uint32_t extensionCount;
vkEnumerateDeviceExtensionProperties(device, nullptr, &extensionCount, nullptr);
std::vector<VkExtensionProperties> availableExtensions(extensionCount);
vkEnumerateDeviceExtensionProperties(device, nullptr, &extensionCount, availableExtensions.data());
std::set<std::string> requiredExtensions(deviceExtensions.begin(), deviceExtensions.end());
for (const auto& extension : availableExtensions) {
requiredExtensions.erase(extension.extensionName);
}
return requiredExtensions.empty();
}
Here I used
std::set<std::string>
to store the names of the required but not yet confirmed extensions. You can also use a nested loop like in a function
checkValidationLayerSupport
. The performance difference is not significant.
Now let's run the program and make sure that our video card is suitable for creating a swap chain. Note that the presence of a display queue already implies support for the swap chain extension. However, it is best to make sure of this explicitly.
Connecting extensions
To use the swap chain, you first need to enable the extension
VK_KHR_swapchain
. To do this, let's slightly change the padding
VkDeviceCreateInfo
when creating the logical device:
createInfo.enabledExtensionCount = static_cast<uint32_t>(deviceExtensions.size());
createInfo.ppEnabledExtensionNames = deviceExtensions.data();
Swap chain support information request
Checking alone to see if the swap chain is available is not enough. The creation of the swap chain involves a lot more configuration, so we need to ask for more information.
In total, you need to check 3 types of properties:
- Basic capabilities of the surface, such as min / max number of images in the swap chain, min / max width and height of images
- Surface format (pixel format, color space)
- Available operating modes
To work with this data, we will use the structure:
struct SwapChainSupportDetails {
VkSurfaceCapabilitiesKHR capabilities;
std::vector<VkSurfaceFormatKHR> formats;
std::vector<VkPresentModeKHR> presentModes;
};
Now let's create a function
querySwapChainSupport
that fills this structure.
SwapChainSupportDetails querySwapChainSupport(VkPhysicalDevice device) {
SwapChainSupportDetails details;
return details;
}
Let's start with surface capabilities. They are easy to query and return to the structure
VkSurfaceCapabilitiesKHR
.
vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device, surface, &details.capabilities);
This function accepts the previously created
VkPhysicalDevice
and
VkSurfaceKHR
. Each time we ask for supported functionality, these two parameters will be the first, since they are key components of the swap chain.
The next step is to query the supported surface formats. To do this, let's perform the already familiar ritual with a double function call:
uint32_t formatCount;
vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, nullptr);
if (formatCount != 0) {
details.formats.resize(formatCount);
vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, details.formats.data());
}
Make sure you allocate enough space in the vector to get all the available formats.
In the same way, we request the supported modes of operation using the function
vkGetPhysicalDeviceSurfacePresentModesKHR
:
uint32_t presentModeCount;
vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, nullptr);
if (presentModeCount != 0) {
details.presentModes.resize(presentModeCount);
vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, details.presentModes.data());
}
When all the necessary information is in the structure, add the function
isDeviceSuitable
to check if the swap chain is supported. For the purposes of this tutorial, we will assume that if there is at least one supported image format and one supported mode for the window surface, then the swap chain is supported.
bool swapChainAdequate = false;
if (extensionsSupported) {
SwapChainSupportDetails swapChainSupport = querySwapChainSupport(device);
swapChainAdequate = !swapChainSupport.formats.empty() && !swapChainSupport.presentModes.empty();
}
You only need to request swap chain support after you have verified that the extension is available.
The last line of the function changes to:
return indices.isComplete() && extensionsSupported && swapChainAdequate;
Choosing settings for swap chain
If
swapChainAdequate
true, the swap chain is supported. But the swap chain can have several modes. Let's write a few functions to find the appropriate settings for creating the most efficient swap chain.
In total, let's highlight 3 types of settings:
- surface format (color depth)
- operating mode (conditions for changing frames on the screen)
- swap extent (resolution of images in the swap chain)
For each setting, we will look for some "ideal" value, and if it is not available, we will use some logic to choose from what is.
Surface format
Let's add a function to select a format:
VkSurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector<VkSurfaceFormatKHR>& availableFormats) {
}
Later, we will pass a member
formats
from the structure
SwapChainSupportDetails
as an argument.
Each element
availableFormats
contains members
format
and
colorSpace
. The field
format
defines the number and types of channels. For example, it
VK_FORMAT_B8G8R8A8_SRGB
means that we have B, G, R and alpha channels of 8 bits each, for a total of 32 bits per pixel. A flag
VK_COLOR_SPACE_SRGB_NONLINEAR_KHR
in the field
colorSpace
indicates whether the SRGB color space is supported. Note that in an earlier version of the specification this flag was called
VK_COLORSPACE_SRGB_NONLINEAR_KHR
.
We will use SRGB as the color space. SRGB is a standard for representing colors in images, and it better reproduces perceived colors. That is why we will also use one of the SRGB formats as a color format -
VK_FORMAT_B8G8R8A8_SRGB
.
Let's go through the list and check if the combination we need is available:
for (const auto& availableFormat : availableFormats) {
if (availableFormat.format == VK_FORMAT_B8G8R8A8_SRGB && availableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) {
return availableFormat;
}
}
If not, we can sort the available formats from more suitable to less suitable, but in most cases we can simply take the first one from the list.
VkSurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector<VkSurfaceFormatKHR>& availableFormats) {
for (const auto& availableFormat : availableFormats) {
if (availableFormat.format == VK_FORMAT_B8G8R8A8_SRGB && availableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) {
return availableFormat;
}
}
return availableFormats[0];
}
Working hours
The mode of operation is perhaps the most important setting for the swap chain, since it determines the conditions for changing frames on the screen.
There are four modes available in Vulkan:
VK_PRESENT_MODE_IMMEDIATE_KHR
: , , , .VK_PRESENT_MODE_FIFO_KHR
: . , . , . , .VK_PRESENT_MODE_FIFO_RELAXED_KHR
: , . . .VK_PRESENT_MODE_MAILBOX_KHR
: this is another variation of the second mode. Instead of blocking the program when the queue is full, images in the queue are replaced with new ones. This mode is suitable for implementing triple buffering. With it, you can avoid the appearance of artifacts with low latency.
Only the mode is guaranteed to be available
VK_PRESENT_MODE_FIFO_KHR
, so again we will have to write a function to find the best available mode:
VkPresentModeKHR chooseSwapPresentMode(const std::vector<VkPresentModeKHR>& availablePresentModes) {
return VK_PRESENT_MODE_FIFO_KHR;
}
Personally, I find it best to use triple buffering. It avoids artifacts with low latency.
So let's go through the list to check the available modes:
VkPresentModeKHR chooseSwapPresentMode(const std::vector<VkPresentModeKHR>& availablePresentModes) {
for (const auto& availablePresentMode : availablePresentModes) {
if (availablePresentMode == VK_PRESENT_MODE_MAILBOX_KHR) {
return availablePresentMode;
}
}
return VK_PRESENT_MODE_FIFO_KHR;
}
Swap extent
It remains to configure the last property. To do this, add a function:
VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities) {
}
Swap extent is the resolution of the images in the swap chain, which almost always matches the resolution of the window (in pixels) where the images are rendered. We got the allowed range in the structure
VkSurfaceCapabilitiesKHR
. Vulkan tells us what resolution we should set using a field
currentExtent
(matches the size of the window). However, some window managers allow for different resolutions. For this, a special value for the width and height
currentExtent
is specified - the maximum value of the type
uint32_t
. In this case, from the interval between
minImageExtent
and,
maxImageExtent
we will choose the resolution that best matches the window resolution. The main thing is to specify the units of measurement correctly.
GLFW uses two units of measurement: pixels and screen coordinates . So, the resolution
{WIDTH, HEIGHT}
that we specified when creating the window is measured in screen coordinates. But since Vulkan works with pixels, the swap chain resolution must also be specified in pixels. If you are using a high-resolution display (such as Apple's Retina display), screen coordinates do not match pixels: Due to the higher pixel density, the window resolution is higher in pixels than in screen coordinates. Since Vulkan won't fix the swap chain permission for us, we can't use the original permission
{WIDTH, HEIGHT}
. Instead, we should use
glfwGetFramebufferSize
to query the window's resolution in pixels before mapping it to the minimum and maximum image resolutions.
#include <cstdint> // Necessary for UINT32_MAX
...
VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities) {
if (capabilities.currentExtent.width != UINT32_MAX) {
return capabilities.currentExtent;
} else {
int width, height;
glfwGetFramebufferSize(window, &width, &height);
VkExtent2D actualExtent = {
static_cast<uint32_t>(width),
static_cast<uint32_t>(height)
};
actualExtent.width = std::max(capabilities.minImageExtent.width, std::min(capabilities.maxImageExtent.width, actualExtent.width));
actualExtent.height = std::max(capabilities.minImageExtent.height, std::min(capabilities.maxImageExtent.height, actualExtent.height));
return actualExtent;
}
}
Function
max
and
min
is used to limit the values
width
and
height
within the available resolutions. Don't forget to include the header file
<algorithm>
to use the functions.
Swap chain creation
We now have all the information we need to create a suitable swap chain.
Let's create a function
createSwapChain
and call it from
initVulkan
after creating the logical device.
void initVulkan() {
createInstance();
setupDebugMessenger();
createSurface();
pickPhysicalDevice();
createLogicalDevice();
createSwapChain();
}
void createSwapChain() {
SwapChainSupportDetails swapChainSupport = querySwapChainSupport(physicalDevice);
VkSurfaceFormatKHR surfaceFormat = chooseSwapSurfaceFormat(swapChainSupport.formats);
VkPresentModeKHR presentMode = chooseSwapPresentMode(swapChainSupport.presentModes);
VkExtent2D extent = chooseSwapExtent(swapChainSupport.capabilities);
}
Now you need to decide how many image objects should be in the swap chain. The implementation specifies the minimum amount required for work:
uint32_t imageCount = swapChainSupport.capabilities.minImageCount;
However, if you only use this minimum, you sometimes have to wait for the driver to finish internal operations to get the next image. Therefore, it is better to request at least one more than the specified minimum:
uint32_t imageCount = swapChainSupport.capabilities.minImageCount + 1;
It is important not to exceed the maximum amount. A value
0
indicates that no maximum is specified.
if (swapChainSupport.capabilities.maxImageCount > 0 && imageCount > swapChainSupport.capabilities.maxImageCount) {
imageCount = swapChainSupport.capabilities.maxImageCount;
}
The swap chain is a Vulkan object, so you need to populate the structure to create it. The beginning of the structure is already familiar to us:
VkSwapchainCreateInfoKHR createInfo{}; createInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR; createInfo.surface = surface;
First, the surface is specified, to which the swap chain is attached, then - information for creating image objects:
createInfo.minImageCount = imageCount;
createInfo.imageFormat = surfaceFormat.format;
createInfo.imageColorSpace = surfaceFormat.colorSpace;
createInfo.imageExtent = extent;
createInfo.imageArrayLayers = 1;
createInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
In
imageArrayLayers
specifies the number of layers that each image consists of. There will always be value here
1
, unless, of course, these are stereo images. The bit field
imageUsage
indicates what operations the images obtained from the swap chain will be used for. In the tutorial, we'll be rendering directly to them, but you can render to a separate image first, for example for post-processing. In this case, use the value
VK_IMAGE_USAGE_TRANSFER_DST_BIT
, and for the transfer, use the memory operation.
QueueFamilyIndices indices = findQueueFamilies(physicalDevice);
uint32_t queueFamilyIndices[] = {indices.graphicsFamily.value(), indices.presentFamily.value()};
if (indices.graphicsFamily != indices.presentFamily) {
createInfo.imageSharingMode = VK_SHARING_MODE_CONCURRENT;
createInfo.queueFamilyIndexCount = 2;
createInfo.pQueueFamilyIndices = queueFamilyIndices;
} else {
createInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
createInfo.queueFamilyIndexCount = 0; // Optional
createInfo.pQueueFamilyIndices = nullptr; // Optional
}
Next, you need to specify how to handle images objects that are used across multiple queue families. This is true for cases where the graphics family and the display family are different families. We will render to images in the graphics queue and then send them to the display queue.
There are two ways to process images with access from multiple queues:
VK_SHARING_MODE_EXCLUSIVE
: The object belongs to one queue family and ownership must be transferred explicitly before using it in another queue family. This method provides the highest performance.
VK_SHARING_MODE_CONCURRENT
: Objects can be used across multiple queue families without explicitly transferring ownership.
If we have multiple queues, we will use
VK_SHARING_MODE_CONCURRENT
. This method requires you to specify in advance between which queue families the ownership will be shared. This can be done using the parameters
queueFamilyIndexCount
and
pQueueFamilyIndices
. If the graphics queue family and display queue family are the same, which is more common, use
VK_SHARING_MODE_EXCLUSIVE
.
createInfo.preTransform = swapChainSupport.capabilities.currentTransform;
You can specify that images in the swap chain are applied with any of the supported transformations (
supportedTransforms
in
capabilities
), for example, rotate 90 degrees clockwise or flip horizontally. To not apply any transformations, just leave
currentTransform
.
createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
The field
compositeAlpha
indicates whether to use the alpha channel for blending with other windows in the windowing system. You probably won't need an alpha channel, so leave it
VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR
.
createInfo.presentMode = presentMode; createInfo.clipped = VK_TRUE;
The field
presentMode
speaks for itself. If we put it
VK_TRUE
in the field
clipped
, then we are not interested in hidden pixels (for example, if part of our window is covered by another window). You can always turn clipping off if you need to read the pixels, but for now let's leave clipping on.
createInfo.oldSwapchain = VK_NULL_HANDLE;
The last field remains -
oldSwapChain
. If the swap chain becomes invalid, for example, due to the resizing of the window, it will need to be recreated from scratch and in the field
oldSwapChain
specify a link to the old swap chain. This is a complex topic that we will cover in a later chapter. For now, let's say we only have one swap chain.
Let's add a class member to store the object
VkSwapchainKHR
:
VkSwapchainKHR swapChain;
Now you just need to call
vkCreateSwapchainKHR
to create the swap chain:
if (vkCreateSwapchainKHR(device, &createInfo, nullptr, &swapChain) != VK_SUCCESS) {
throw std::runtime_error("failed to create swap chain!");
}
The following parameters are passed to the function: logical device, swap chain information, an optional custom allocator and a pointer to write the result. No surprises. The swap chain must be destroyed using
vkDestroySwapchainKHR
before the device is destroyed:
void cleanup() {
vkDestroySwapchainKHR(device, swapChain, nullptr);
...
}
Now let's run the program to make sure the swap chain was created successfully. If you receive an error message or a message like
ยซ vkGetInstanceProcAddress SteamOverlayVulkanLayer.dllยป
, go to the FAQ section .
Let's try to remove the line
createInfo.imageExtent = extent;
with the validation layers enabled. One of the validation levels will immediately detect the error and notify us:
Getting an image from a swap chain
Now that the swap chain has been created, it remains to get the VkImages descriptors . Let's add a class member for storing descriptors:
std::vector<VkImage> swapChainImages;
Image objects from the swap chain will be destroyed automatically after the swap chain itself is destroyed, so there is no need to add any cleanup code.
Immediately after the call,
vkCreateSwapchainKHR
add the code to get the descriptors. Remember that we have specified only the minimum number of images in the swap chain, which means that there may be more of them. Therefore, we first request the real number of images using the function
vkGetSwapchainImagesKHR
, then allocate the necessary space in the container and call it again
vkGetSwapchainImagesKHR
to get the descriptors.
vkGetSwapchainImagesKHR(device, swapChain, &imageCount, nullptr);
swapChainImages.resize(imageCount);
vkGetSwapchainImagesKHR(device, swapChain, &imageCount, swapChainImages.data());
And the last thing - save the format and resolution of the swap chain images into class variables. We will need them in the future.
VkSwapchainKHR swapChain;
std::vector<VkImage> swapChainImages;
VkFormat swapChainImageFormat;
VkExtent2D swapChainExtent;
...
swapChainImageFormat = surfaceFormat.format;
swapChainExtent = extent;
We now have an image to draw and display. In the next chapter, we'll show you how to set up an image to be used as render targets, and get started with the graphics pipeline and drawing commands!
C ++