You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Vulkan is a low-overhead, cross-platform 3D graphics and compute API.
Vulkan targets
Vulkan targets high-performance realtime 3D graphics applications such as
games and interactive media across multiple platforms providing higher
performance and lower CPU usage.
Tutorial Structure
These tutorials assume you have the Vulkan SDK installed and a working
Vulkan driver.
There is no global state in Vulkan; all application state is stored in
a vkInstance object. Creating a vkInstance object initializes the
Vulkan library and allows application to pass information about itself
to the implementation.
To create an instance we also need a vkInstanceCreateInfo object
controlling the creation of the instance and a vkAllocationCallback to
control host memory allocation for the instance. For now we will ignore
vkAllocationCallback and use NULL which will use the system-wide allocator.
More on vkAllocationCallback later.
vkApplicationInfo applicationInfo;
vkInstanceCreateInfo instanceInfo;
vkInstance instance;
// Filling out application description:// sType is mandatory
applicationInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
// pNext is mandatory
applicationInfo.pNext = NULL;
// The name of our application
applicationInfo.pApplicationName = "Tutorial 1";
// The name of the engine (e.g: Game engine name)
applicationInfo.pEngineName = NULL;
// The version of the engine
applicationInfo.engineVersion = 1;
// The version of Vulkan we're using for this application
applicationInfo.apiVersion = VK_API_VERSION;
// Filling out instance description:// sType is mandatory
instanceInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
// pNext is mandatory
instanceInfo.pNext = NULL;
// flags is mandatory
instanceInfo.flags = 0;
// The application info structure is then passed through the instance
instanceInfo.pApplicationInfo = &applicationInfo;
// Don't enable and layer
instanceInfo.enabledLayerCount = 0;
instanceInfo.ppEnabledLayerNames = NULL;
// Don't enable any extensions
instanceInfo.enabledExtensionCount = 0;
instanceInfo.ppEnabledExtensionNames = NULL;
// Now create the desired instance
vkResult result = vkCreateInstance(&instanceInfo, NULL, &instance);
if (result != VK_SUCCESS) {
fprintf(stderr, "Failed to create instance: %d\n", result);
abort();
}
// To Come Later// ...// ...// Never forget to free resourcesvkDestroyInstance(instance, NULL);
sType is used to describe the type of the structure. It must be filled
out in every structure. pNext must be filled out too. The idea behind
pNext is to store pointers to extension-specific structures. Valid usage
currently is to assign it a value of NULL. The same goes for flags.
In the first chunk of code we setup an application description structure
which will be a required component for our instance info structure. In Vulkan
you're expected to describe what your application is, which engine it uses
(or NULL.) This is useful information for Vulkan to have as driver vendors
may want to apply engine or game specific features/fixes to the application
code. Traditionally this sort of technique was supported in much more
complicated and unsafe manners. Vulkan addresses the problem by requiring
upfront information.
The second part creates an instance description structure which will
be used to actually initialize an instance. This is where you'd request
extensions or layers. Extensions work in the same way as GL did extensions,
nothing has changed here and it should be familiar. A layer is a new
concept Vulkan has introduced. Layers are techniques you can enable that
insert themselves into the call chain for Vulkan commands the layer is
inserted in. They can be used to validate application behavior during
development. Think of them as decorators to commands. In our example
we don't bother with extensions or layers, but they must be filled out.
From here it's as trivial as calling vkCreateInstance to create an
instance. On success this function will return VK_SUCCESS. When we're
done with our instance we destroy it with vkDestroyInstance.
Now that we have an instance we need to a way to associate the instance
with the hardware. In Vulkan there is no notion of a singular GPU, instead
you enumerate physical devices and choose. This allows you to use multiple
physical devices at the same time for rendering or compute.
The vkEnumeratePhysicalDevices function allows you to both query the
count of physical devices present on the system and fill out an array of
vkPhysicalDevice structures representing the physical devices.
// Query how many devices are present in the systemuint32_t deviceCount = 0;
VkResult result = vkEnumeratePhysicalDevices(instance, &deviceCount, NULL);
if (result != VK_SUCCESS) {
fprintf(stderr, "Failed to query the number of physical devices present: %d\n", result);
abort();
}
// There has to be at least one device presentif (deviceCount == 0) {
fprintf(stderr, "Couldn't detect any device present with Vulkan support: %d\n", result);
abort();
}
// Get the physical devices
vector<VkPhysicalDevice> physicalDevices(deviceCount);
result = vkEnumeratePhysicalDevices(instance, &deviceCount, &physicalDevices[0]);
if (result != VK_SUCCESS) {
fprintf(stderr, "Faied to enumerate physical devices present: %d\n", result);
abort();
}
Once we have a physical device; we can fetch the properties of that
physical device using vkGetPhysicalDeviceProperties which will
fill out a vkPhysicalDeviceProperties structure.
In Vulkan the API version is encoded as a 32-bit integer with the major
and minor version being encoded into bits 31-22 and 21-12 respectively
(for 10 bits each.); the final 12-bits encode the patch version number.
These handy macros should help with fetching some human readable digits
from the encoded API integer.
Queues in Vulkan provide an interface to the execution engine of a device.
Commands are recorded into command buffers ahead of execution time. These
same buffers are then submitted to queues for execution. Each physical
devices provides a family of queues to choose from. The choice of the queue
depends on the task at hand.
A Vulkan queue can support one or more of the following operations
(in order of most common):
graphic VK_QUEUE_GRAPHICS_BIT
compute VK_QUEUE_COMPUTE_BIT
transfer VK_QUEUE_TRANSFER_BIT
sparse memory VK_QUEUE_SPARSE_BINDING_BIT
This is encoded in the queueFlags field of the VkQueueFamilyProperties
structures filled out by vkGetPhysicalDeviceQueueFamilyProperties. Which,
like vkEnumeratePhysicalDevices can also be used to query the count of
available queue families.
While the queue support bits are pretty straight forward; something must
be said about VK_QUEUE_SPARSE_BINDING_BIT. If this bit is set it indicates
that the queue family supports sparse memory management operations. Which
means you can submit operations that operate on sparse resources. If this
bit is not present, submitting operations with sparse resource is undefined.
Sparse resources will be covered in later tutorials as they are an
advanced topic.
uint32_t queueFamilyCount = 0;
vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &queueFamilyCount, NULL);
vector<VkQueueFamilyProperties> familyProperties(queueFamilyCount);
vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &queueFamilyCount &damilyProperties[0]);
// Print the familiesfor (uint32_t i = 0; i < deviceCount; i++) {
for (uint32_t j = 0; j < queueFamilyCount; j++) {
printf("Count of Queues: %d\n", familyProperties[j].queueCount);
printf("Supported operationg on this queue:\n");
if (familyProperties[j].queueFlags & VK_QUEUE_GRAPHICS_BIT)
printf("\t\t Graphics\n");
if (familyProperties[j].queueFlags & VK_QUEUE_COMPUTE_BIT)
printf("\t\t Compute\n");
if (familyProperties[j].queueFlags & VK_QUEUE_TRANSFER_BIT)
printf("\t\t Transfer\n");
if (familyProperties[j].queueFlags & VK_QUEUE_SPARSE_BINDING_BIT)
printf("\t\t Sparse Binding\n");
}
}
So far we have the ability to get the physical devices present on the
system, create an instance and query the queue families supported by the
physical devices. Vulkan does not operate directly on a VkPhysicalDevice.
Instead it operates on views of a VkPhysicalDevice which it represents
as a VkDevice and calls a logical device. This additional layer of
abstraction is what allows us to tie together everything into an abstract,
usable context.
Like the other structures we filled out previously, sType, pNext and
flags are mandatory here.
VkDeviceCreateInfo deviceInfo;
// Mandatory fields
deviceInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
deviceInfo.pNext = NULL;
deviceInfo.flags = 0;
// We won't bother with extensions or layers
deviceInfo.enabledLayerCount = 0;
deviceInfo.ppEnabledLayerNames = NULL;
deviceInfo.enabledExtensionCount = 0;
deviceInfo.ppEnabledExtensionNames = NULL;
// We don't want any any features,:the wording in the spec for DeviceCreateInfo// excludes that you can pass NULL to disable features, which GetPhysicalDeviceFeatures// tells you is a valid value. This must be supplied - NULL is legal here.
deviceInfo.pEnabledFeatures = NULL;
// Here's where we initialize our queues
VkDeviceQueueCreateInfo deviceQueueInfo;
deviceQueueInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
deviceQueueInfo.pNext = NULL;
deviceQueueInfo.flags = 0;
// Use the first queue family in the family list
deviceQueueInfo.queueFamilyIndex = 0;
// Create only one queuefloat queuePriorities[] = { 1.0f };
deviceQueueInfo.queueCount = 1;
deviceQueueInfo.pQueuePriorities = queuePriorities;
// Set queue(s) into the device
deviceInfo.queueCreateInfoCount = 1;
deviceInfo.pQueueCreateInfos = &deviceQueueInfo;
result = vkCreateDevice(physicalDevice, &deviceInfo, NULL, device);
if (result != VK_SUCCESS) {
fprintf(stderr, "Failed creating logical device: %d\n", result);
abort();
}
You can create many instances of the same queue family and set multiple
queues into a VkDeviceCreateInfo structure. Just be sure to set the
queueCount correctly. In Vulkan you can control the priority of each queue
with an array of normalized floats. A value of 1 has highest priority.
With that you should have a logical device setup from a physical device
with your associated queues containing your application-provided information.
From here we may now create the appropriate swap chains and begin rendering.
What we have now is sufficient enough to create a swap chain with and
begin rendering, for Vulkan does not require a surface be present
to render into. Chances are you want to get something on screen; so to
do that we need to make a surface. If you are interested in doing window-less
rendering this tutorial may be skipped.
Creating a surface is platform-specific: think WGL, AGL, GLX, etc. However
the amount of platform-specific code has gone down tremendously in Vulkan;
which now provides some easy to use extensions.
We must now go back to when we created our application info structure
and add the appropriate extensions:
Like OpenGL the Vulkan headers don't expose extensions directly to the
implementation as prototypes. Instead you must use the function pointer
types and get their function addresses.
There is one big difference however; In Vulkan there is two types of
function pointers: instance local and device local. Instance local
addresses are for procedures based on a Vulkan instance while device local
addresses are for procedures based on a Vulkan device. For now we'll only
cover the Vulkan instance one which is appropriately named
vkGetInstanceProcAddr. The next tutorial we'll see use of Vulkan device
function: vkGetDeviceProcAddr.
PFN_vkGetPhysicalDeviceSurfaceFormatsKHR vkGetPhysicalDeviceSurfaceFormatsKHR = NULL;
// somewhere in initialization code
*(void **)&vkGetPhysicalDeviceSurfaceFormatsKHR = vkGetInstanceProcAddr(instance, "vkGetPhysicalDeviceSurfaceFormatsKHR");
if (!vkPhysicalDeviceSurfaceFormatsKHR) {
abort();
}
Now we can begin to use the extensions to get a surface to render into.
VkSurfaceKHR surface;
#if defined(_WIN32)
VkWin32SurfaceCreateInfoKHR surfaceCreateInfo;
surfaceCreateInfo.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR;
surfaceCreateInfo.hinstance = (HINSTANCE)platformHandle; // provided by the platform code
surfaceCreateInfo.hwnd = (HWND)platformWindow; // provided by the platform code
VkResult result = vkCreateWin32SurfaceKHR(instance, &surfaceCreateInfo, NULL, &surface);
#elif defined(__ANDROID__)
VkAndroidSurfaceCreateInfoKHR surfaceCreateInfo;
surfaceCreateInfo.sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR;
surfaceCreateInfo.window = window; // provided by the platform code
VkResult result = vkCreateAndroidSurfaceKHR(instance, &surfaceCreateInfo, NULL, &surface);
#else
VkXcbSurfaceCreateInfoKHR surfaceCreateInfo;
surfaceCreateInfo.sType = VK_STRUCTURE_TYPE_XCB_SURFACE_CREATE_INFO_KHR;
surfaceCreateInfo.connection = connection; // provided by the platform code
surfaceCreateInfo.window = window; // provided by the platform code
VkResult result = vkCreateXcbSurfaceKHR(instance, &surfaceCreateInfo, NULL, &surface);
#endifif (result != VK_SUCCESS) {
fprintf(stderr, "Failed to create Vulkan surface: %d\n", result);
abort();
}
Once we have the surface obtained the next step is to get the physical
device surface properties and formats
VkFormat colorFormat;
uint32 formatCount = 0;
VkGetPhysicalDeviceSurfaceFormatsKHR(physicalDevice, surface, &formatCount, NULL);
vector<VkSurfaceFormatKHR> surfaceFormats(formatCount);
VkGetPhysicalDeviceSurfaceFormatsKHR(physicalDevice, surface, &formatCount, &surfaceFormats[0]);
// If the format list includes just one entry of VK_FORMAT_UNDEFINED,// the surface has no preferred format. Otherwise, at least one// supported format will be returnedif (formatCount == 1 && surfaceFormats[0].format == VK_FORMAT_UNDEFINED)
colorFormat = VK_FORMAT_B8G8R8A8_UNORM;
else {
assert(formatCount >= 1);
colorFormat = surfaceFormats[0].format;
}
colorSpace = surfaceFormats[0].colorSpace;
You can iterate this list to find better formats for your application but
the first entry is usually acceptable for most uses. Take note that Vulkan
differentiates between a color format and color space. The format
can be thought of like the data format while the latter is the way that
data is to be interpreted by the implementation. We record this information
for it will be useful for later rendering.
Up to now everything we have done has been setting up a rendering context
that is suitable for rendering. We're now going to be using the results of
the previous tutorials to build our swap chain.
The swap chain is effectively a circular buffer of images (and optional
image views to make them present for our window.) It is what allows us to
specify how to buffer the render set (choice of double, triple, etc buffering.);
as well as the synchronization mode (swap tear, vertical synchronization, etc.);
It's ultimately at a high level our rendering context. The only thing we
really care about. Everything up to now has been ground work to get us
here.
This tutorial will contain a full usable high level framework for creating
a context of which we can begin rendering into. So mind the size for we'll
be reiterating on previous tutorials.
Buffer
A swap chain can be thought of as a collection of buffers. So let's
separate concerns.
This is where we will begin to write our tutorials more as a framework
than pieces of random code, as we'll need to be reiterating on some stuff
from previous tutorials. We're going to be needing a few more extensions
too so we'll be encapsulating them in the swap chain structure as well.
We'll need a simple way to fetch addresses of procedures for a Vulkan
instance as well as a Vulkan device as covered in the previous tutorial.
void *swapChainGetInstanceProc(SwapChain *swapChain, constchar *name) {
returnvkGetInstanceProcAddr(swapChain->instance, name);
}
void *swapChainGetDeviceProc(SwapChain *swapChain, constchar *name) {
returnvkGetDeviceProcAddr(swapChain->device, name);
}
#defineGET_IPROC(NAME) \
do { \
*(void **)&swapChain->fp##NAME = swapChainGetInstanceProc(swapChain, "vk" #NAME); \
if (!swapChain->fp##NAME) { \
fprintf(stderr, "Failed to get Vulkan instance procedure: `%s'\n", #NAME); \
returnfalse; \
} \
} while (0)
#defineGET_DPROC(NAME) \
do { \
*(void **)&swapChain->fp##NAME = swapChainGetDeviceProc(swapChain, "vk" #NAME); \
if (!swapChain->fp##NAME) { \
fprintf(stderr, "Failed to get Vulkan device procedure: `%s'\n", #NAME); \
returnfalse; \
} \
} while (0)
Now we need a simple constructor that lets us connect the instance and
device to our swap chain and fetch the appropriate function addresses
we will need using the two helper functions and macros we just wrote.
boolswapChainConnect(SwapChain *swapChain,
VkInstance instance,
VkPhysicalDevice physicalDevice,
VkDevice device)
{
swapChain->instance = instance;
swapChain->physicalDevice = physicalDevice;
swapChain->device = device;
// Get some instance local proceduresGET_IPROC(GetPhysicalDeviceSurfaceSupportKHR);
GET_IPROC(GetPhysicalDeviceSurfaceCapabilitiesKHR);
GET_IPROC(GetPhysicalDeviceSurfaceFormatsKHR);
GET_IPROC(GetPhysicalDeviceSurfacePrresentModesKHR);
// Get some device local proceduresGET_DPROC(CreateSwapchainKHR);
GET_DPROC(DestroySwapchainKHR);
GET_DPROC(GetSwapchainImagesKHR);
GET_DPROC(AcquireNextImageKHR);
GET_DPROC(QueuePresentKHR);
returntrue;
}
Now we reiterate on the previous tutorial for connecting a platform
handle to Vulkan.
Now a generic constructor which will also find a suitable device queue
for rendering as covered in Tutorial 3. This function must be called
after swapChainConnect
#if defined(_WIN32)
boolswapChainInit(SwapChain *swapChain,
HINSTANCE handle,
HWND window)
#elif defined(__ANDROID__)
boolswapChainInit(SwapChain *swapChain,
ANativeWindow *window)
#elseboolswapChainInit(SwapChain *swapChain,
xcb_connection_t *connection,
xcb_window_t *window)
#endif
{
#if defined(_WIN32)
if (!swapChainPlatformConnect(swapChain, handle, window))
returnfalse;
#elif defined(__ANDROID__)
if (!swapChainPlatformConnect(swapChain, window))
returnfalse;
#elseif (!swapChainPlatformConnect(swapChain, connection, window))
returnfalse;
#endifuint32_t queueCount;
vkGetPhysicalDeviceQueueFamilyProperties(swapChain->physicalDevice,
&queueCount,
NULL);
if (queueCount == 0)
returnfalse;
vector<VkQueueFamilyProperties> queueProperties(queueCount);
vkGetPhysicalDeviceQueueFamilyProperties(swapChain->physicalDevice,
&queueCount,
&queueProperties[0]);
// In previous tutorials we just picked which ever queue was readily// available. The problem is not all queues support presenting. Here// we make use of vkGetPhysicalDeviceSurfaceSupportKHR to find queues// with present support.
vector<VkBool32> supportsPresent(queueCount);
for (uint32_t i = 0; i < queueCount; i++) {
swapChain->fpGetPhysicalDeviceSurfaceSupportKHR(swapChain->physicalDevice,
i,
swapChain->surface,
&supportsPresent[i]);
}
// Now we have a list of booleans for which queues support presenting.// We now must walk the queue to find one which supports// VK_QUEUE_GRAPHICS_BIT and has supportsPresent[index] == VK_TRUE// (indicating it supports both.)uint32_t graphicIndex = UINT32_MAX;
uint32_t presentIndex = UINT32_MAX;
for (uint32_t i = 0; i < queueCount; i++) {
if ((queueProperties[i].queueFlags & VK_QUEUE_GRAPHICS_BIT)) {
if (graphicIndex == UINT32_MAX)
graphicIndex = i;
if (supportsPresent[i] == VK_TRUE) {
graphicIndex = i;
presentIndex = i;
break;
}
}
}
if (presentIndex == UINT32_MAX) {
// If there is no queue that supports both present and graphics;// try and find a separate present queue. They don't necessarily// need to have the same index.for (uint32_t i = 0; i < queueCount; i++) {
if (supportsPresent[i] != VK_TRUE)
continue;
presentIndex = i;
break;
}
}
// If neither a graphics or presenting queue was found then we cannot// renderif (graphicIndex == UINT32_MAX || presentIndex == UINT32_MAX)
returnfalse;
// In a future tutorial we'll look at supporting a separate graphics// and presenting queueif (graphicIndex != presentIndex) {
fprintf(stderr, "Not supported\n");
returnfalse;
}
swapChain->nodeIndex = graphicIndex;
// Now get a list of supported surface formats, as covered in the// previous tutorialuint32_t formatCount;
if (swapChain->fpGetPhysicalDeviceSurfaceFormatsKHR(swapChain->physicalDevice,
swapChain->surface,
&formatCount,
NULL) != VK_SUCCESS)
returnfalse;
if (formatCount == 0)
returnfalse;
vector<VkSurfaceFormatKHR> surfaceFormats(formatCount);
if (swapChain->fpGetPhysicalDeviceSurfaceFormatsKHR(swapChain->physicalDevice,
swapChain->surface,
&formatCount,
&surfaceFormats[0]) != VK_SUCCESS)
returnfalse;
// If the format list includes just one entry of VK_FORMAT_UNDEFINED,// the surface has no preferred format. Otherwise, at least one// supported format will be returnedif (formatCount == 1 && surfaceFormats[0].format == VK_FORMAT_UNDEFINED)
swapChain->colorFormat = VK_FORMAT_B8G8R8A8_UNORM;
else {
if (formatCount == 0)
returnfalse;
swapChain->colorFormat = surfaceFormats[0].format;
}
swapChain->colorSpace = surfaceFormats[0].colorSpace;
returntrue;
}
First we're going to implement a generic function for setting the
image layout for an image. This will be needed when setting the layout
of our present images. It's a bit involved because it's generic and
needs to protect the whole thing with a memory barrier as the image being
changed could be in use. We'll cover more of it in later tutorials for
it does a lot of things we're not going to discuss for awhile.
// This lovely piece of code is from SaschaWillems, check out his Vulkan repositories on// GitHub if you're feeling adventurous.voidsetImageLayout(VkCommandBuffer commandBuffer,
VkImage image,
VkImageAspectFlags aspectMask,
vkImageLayout oldImageLayout,
vkImageLayout newImageLayout)
{
VkImageMemoryBarrier imageMemoryBarrier = // :E
imageMemoryBarrier.oldLayout = oldImageLayout;
imageMemoryBarrier.newLayout = newImageLayout;
imageMemoryBarrier.image = image;
imageMemoryBarrier.subresourceRange.aspectMask = aspectMask;
imageMemoryBarrier.subresourceRange.baseMipLevel = 0;
imageMemoryBarrier.subresourceRange.levelCount = 1;
imageMemoryBarrier.subresourceRange.layerCount = 1;
// Undefined layout:// Note: Only allowed as initial layout!// Note: Make sure any writes to the image have been finishedif (oldImageLayout == VK_IMAGE_LAYOUT_UNDEFINED)
imageMemoryBarrier.srcAccessMask = VK_ACCESS_HOST_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT;
// Old layout is color attachment:// Note: Make sure any writes to the color buffer have been finishedif (oldImageLayout == VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL)
imageMemoryBarrier.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
// Old layout is transfer source:// Note: Make sure any reads from the image have been finishedif (oldImageLayout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL)
imageMemoryBarrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
// Old layout is shader read (sampler, input attachment):// Note: Make sure any shader reads from the image have been finishedif (oldImageLayout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
imageMemoryBarrier.srcAccessMask = VK_ACCESS_SHADER_READ_BIT;
// New layout is transfer destination (copy, blit):// Note: Make sure any copyies to the image have been finishedif (newImageLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL)
imageMemoryBarrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
// New layout is transfer source (copy, blit):// Note: Make sure any reads from and writes to the image have been finishedif (newImageLayout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL) {
imageMemoryBarrier.srcAccessMask = imageMemoryBarrier.srcAccessMask | VK_ACCESS_TRANSFER_READ_BIT;
imageMemoryBarrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
}
// New layout is color attachment:// Note: Make sure any writes to the color buffer hav been finishedif (newImageLayout == VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) {
imageMemoryBarrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
imageMemoryBarrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
}
// New layout is depth attachment:// Note: Make sure any writes to depth/stencil buffer have been finishedif (newImageLayout == VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL)
imageMemoryBarrier.dstAccessMask = imageMemoryBarrier.dstAccessMask | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
// New layout is shader read (sampler, input attachment):// Note: Make sure any writes to the image have been finishedif (newImageLayout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) {
imageMemoryBarrier.srcAccessMask = VK_ACCESS_HOST_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT;
imageMemoryBarrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
}
// Put barrier inside the setup command buffervkCmdPipelineBarrier(commandBuffer,
// Put the barriers for source and destination on// top of the command buffer
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
0,
0, NULL,
0, NULL,
1, &imageMemoryBarrier);
}
We're almost there, now all that's left is the creation function for
actually creating the swap chain and making it active for a VkCommandBuffer
We'll be exploring for the first time how to utilize the physical device
surface properties and formats to guide our creation of the swap chain.
Like all creation functions in Vulkan we will be filling out a creation
info structure VkSwapchainCreateInfoKHR to actually create our swap
chain.
But before we do that we need to touch on presentation modes. In Vulkan
you get to choose how to present frames to the swap chain. There is a
couple presentation modes to choose from.
VK_PRESENT_MODE_MAILBOX_KHR
VK_PRESENT_MODE_IMMEDIATE_KHR
VK_PRESENT_MODE_FIFO_RELAXED_KHR
VK_PRESENT_MODE_FIFO_KHR
All compliant implementations of Vulkan must support VK_PRESENT_MODE_FIFO_KHR.
The others are optional.
VK_PRESENT_MODE_MAILBOX_KHR Optimized v-sync technique, will not screen-tear.
Has more latency than tearing. Generally speaking this is the preferred
presentation mode if supported for it is the lowest latency non-tearing
presentation mode.
VK_PRESENT_MODE_IMMEDIATE_KHR does not vertical synchronize and will
screen-tear if a frame is late. This is basically useless unless you're
doing one-off rendering to a surface where the result is not immediately
needed.
VK_PRESENT_MODE_FIFO_RELAXED_KHR normally vertical synchronizes but will
screen-tear if a frame is late. This is the same as "late swap tearing"
extensions in GL/D3D. The idea behind this is to allow late swaps to
occur without synchronization to the video frame. It does reduce visual
stuffer on late frames and reduces the stall on subsequent frames.
VK_PRESENT_MODE_FIFO_KHR will vertical synchronizes and won't screen-tear.
This is your standard vertical synchronization.
boolswapChainCreate(SwapChain *swapChain,
VkCommandBuffer commandBuffer,
uint32_t *width,
uint32_t *height)
{
VkSwapChain oldSwapChain = swapChain->swapChain;
// Get physical device surface properties and formats. This was not// covered in previous tutorials. Effectively does what it says it does.// We will be using the result of this to determine the number of// images we should use for our swap chain and set the appropriate// sizes for them.
VkSurfaceCapabilitiesKHR surfaceCapabilities;
if (swapChain->fpGetPhysicalDeviceSurfaceCapabilitiesKHR(swapChain->physicalDevice,
swapChain->surface,
&surfaceCapabilities) != VK_SUCCESS)
returnfalse;
// Also not covered in previous tutorials: used to get the available// modes for presentation.uint32_t presentModeCount;
if (swapChain->fpGetPhysicalDeviceSurfacePresentModesKHR(swapChain->physicalDevice,
swapChain->surface,
&presentModeCount,
NULL)) != VK_SUCCESS)
returnfalse;
if (presentModeCount == 0)
returnfalse;
vector<VkPresentModeKHR> presentModes(presentModeCount);
if (swapChain->fpGetPhysicalDeviceSurfacePresentModesKHR(swapChain->physicalDevice,
swapChain->surface,
&presentModeCount,
&presentModes[0]) != VK_SUCCESS)
returnfalse;
// When constructing a swap chain we must supply our surface resolution.// Like all things in Vulkan there is a structure for representing this
VkExtent swapChainExtent = { };
// The way surface capabilities work is rather confusing but width// and height are either both -1 or both not -1. A size of -1 indicates// that the surface size is undefined, which means you can set it to// effectively any size. If the size however is defined, the swap chain// size *MUST* match.if (surfaceCapabilities.currentExtent.width == -1) {
swapChainExtent.width = *width;
swapChainExtent.height = *height;
} else {
swapChainExtent = surfaceCapabilities.currentExtent;
*width = surfaceCapabilities.width;
*height = surfaceCapabilities.height;
}
// Prefer mailbox mode if present
VkPresentModeKHR presentMode = VK_PRESENT_MODE_FIFO_KHR; // always supportedfor (uint32_t i = 0; i < presentModeCount; i++) {
if (presentModes[i] == VK_PRESENT_MODE_MAILBOX_KHR) {
presentMode = VK_PRESENT_MODE_MAILBOX_KHR;
break;
}
if (presentMode != VK_PRESENT_MODE_MAILBOX_KHR
&& presentMode != VK_PRESENT_MODE_IMMEDIATE_KHR)
{
// The horrible fallback
presentMode = VK_PRESENT_MODE_IMMEDIATE_KHR;
}
}
// Determine the number of images for our swap chainuint32_t desiredImages = surfaceCapabilites.minImageCount + 1;
if (surfaceCapabilities.maxImageCount > 0
&& desiredImages > surfaceCapabilities.maxImageCount)
{
desiredImages = surfaceCapabilities.maxImageCount;
}
// This will be covered in later tutorials
VkSurfaceTransformFlagsKHR preTransform = surfaceCapabilities.currentTransform;
if (surfaceCapabilities.supportedTransforms & VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR)
preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
vkSwapchainCreateInfoKHR swapChainCreateInfo = { };
// Mandatory fields
swapChainCreateInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
swapChainCreateInfo.pNext = NULL;
swapChainCreateInfo.surface = swapChain->surface;
swapChainCreateInfo.minImageCount = desiredImages;
swapChainCreateInfo.imageFormat = swapChain->colorFormat;
swapChainCreateInfo.imageColorSpace = swapChain->colorSpace;
swapChainCreateInfo.imageExtent = { swapChainExtent.width, swapChainExtent.height };
// This is literally the same as GL_COLOR_ATTACHMENT0
swapChainCreateInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
swapChainCreateInfo.preTransform = (VkSurfaceTransformFlagBitsKHR)preTransform;
swapChainCreateInfo.imageArrayLayers = 1; // Only one attachment
swapChainCreateInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; // No sharing
swapChainCreateInfo.queueFamilyIndexCount = 0; // Covered in later tutorials
swapChainCreateInfo.pQueueFamilyIndices = NULL; // Covered in later tutorials
swapChainCreateInfo.presentMode = presentMode;
swapChainCreateInfo.clipped = true; // If we want clipping outside the extents// Alpha on the window surface should be opaque:// If it was not we could create transparent regions of our window which// would require support from the Window compositor. You can totally do// that if you wanted though ;)
swapChainCreateInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
if (swapChain->fpCreateSwapchainKHR(swapChain->device,
&swapChainCreateInfo,
NULL,
&swapChain->swapChain) != VK_SUCCESS)
returnfalse;
// If an existing swap chain is recreated, destroy the old one. This// also cleans up all presentable imagesif (oldSwapChain != VK_NULL_HANDLE) {
swapChain->fpDestroySwapchainKHR(swapChain->device,
oldSwapChain,
NULL);
}
// Now get the presentable images from the swap chainuint32_t imageCount;
if (swapChain->fpGetSwapchainImagesKHR(swapChain->device,
swapChain->swapChain,
&imageCount,
NULL) != VK_SUCCESS)
goto failed;
swapChain->images.resize(imageCount);
if (swapChain->fpGetSwapchainImagesKHR(swapChain->device,
swapChain->swapChain,
&imageCount,
&swapChain->images[0]) != VK_SUCCESS) {
failed:
swapChain->fpDestroySwapchainKHR(swapChain->device,
swapChain->swapChain,
NULL);
returnfalse;
}
// Create the image views for the swap chain. They will all be single// layer, 2D images, with no mipmaps.// Check the VkImageViewCreateInfo structure to see other views you// can potentially create.
swapChain->buffers.resize(imageCount);
for (uint32_t i = 0; i < imageCount; i++) {
VkImageViewCreateInfo colorAttachmentView = { };
colorAttachmentView.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
colorAttachmentView.pNext = NULL;
colorAttachmentView.format = swapChain->colorFormat;
colorAttachmentView.components = {
VK_COMPONENT_SWIZZLE_R,
VK_COMPONENT_SWIZZLE_G,
VK_COMPONENT_SWIZZLE_B,
VK_COMPONENT_SWIZZLE_A
};
colorAttachmentView.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
colorAttachmentView.subresourceRange.baseMipLevel = 0;
colorAttachmentView.subresourceRange.levelCount = 1;
colorAttachmentView.subresourceRange.baseArrayLayer = 0;
colorAttachmentView.subresourceRange.layerCount = 1;
colorAttachmentView.viewType = VK_IMAGE_VIEW_TYPE_2D;
colorattachmentView.flags = 0; // mandatory// Wire them up
swapChain->buffers[i].image = swapChain->images[i];
// Transform images from the initial (undefined) layer to// present layoutsetImageLayout(commandBuffer,
swapChain->buffers[i].image,
VK_IMAGE_ASPECT_COLOR_BIT,
VK_IMAGE_LAYOUT_UNDEFINED,
VK_IMAGE_LAYOUT_PRESET_SRC_KHR);
colorAttachmentView.image = swapChain->buffers[i].image;
// Create the viewif (vkCreateImageView(swapChain->device,
&colorAttachmentView,
NULL,
&swapChain->buffers[i].view) != VK_SUCCESS)
goto failed;
}
returntrue;
}
Now we need a simple way to acquire the next image in the swap chain and
a way to present the image into the render queue.
It can get very messy very fast to follow all of this when there is no proper
code organizational structure since Vulkan has such high overhead. So I've taken the
time to write a toolkit that should help people get started with Vulkan. It lowers
the barrier to entry for people by providing you with a rendering context which you
start using.
There are assumptions made by it to simplify a lot of things; these assumptions are
provided here and may be changed in the future:
Double buffered only
VSync always on (Will try lower latency MAILBOX_MODE if supported)
First physical device found is the one used for rendering
Single command buffer (There is a high level API for making more though)
I'll try to keep these tutorials up to date as new features are added to vktoolkit;
similarly I'll be keeping vktoolkit heavily documented (public facing API side.)
The current feature implemented in vktoolkit which we have not covered here yet
is resizing. The remainder of this tutorial will explain how that works.
Resizing
First of all we want to prevent resizing if we're already the correct size or if the
surface capabilities are reporting a size of zero for the current extent.
We'll be creating a new swap chain; the way this works is we need to set oldSwapchain in the
swap chain creation structure.
m_swapchainCreateInfo.imageExtent = surfaceCapabilities.currentExtent;
m_swapchainCreateInfo.oldSwapchain = m_swapchain;
VkResult result;
result = vkCreateSwapchainKHR(m_device, &m_swapchainCreateInfo, nullptr, &m_swapchain);
if (result != VK_SUCCESS)
returnfalse;
It's important to wait for the device to go idle before destroying the old swapchain
since we don't want to destroy the swapchain that we're still rendering into.
vkDeviceWaitIdle will do this for us and block until the device has gone idle.
We'll also need to make some memory barriers for the new swapchain images
for (uint32_t i = 0; i < imagesCount; ++i) {
VkImageMemoryBarrier memoryBarrier = {};
memoryBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
memoryBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
memoryBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
memoryBarrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
memoryBarrier.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
memoryBarrier.image = m_presentImages[i];
memoryBarrier.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 };
// we need to put the barrier at the top of the pipeline since we want it to// occur firstvkCmdPipelineBarrier(setupBuffer,
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
0,
0,
nullptr,
0,
nullptr,
1,
&memoryBarrier);
}
Now we can complete the command buffer setup
vkEndCommandBuffer(setupBuffer);
Then finally submit the whole thing to be processed