Skip to content

Instantly share code, notes, and snippets.

@but0n
Forked from graphitemaster/T0.md
Created July 20, 2018 02:39
Show Gist options
  • Save but0n/e45aca652d97f9037c1cef2d359d4fc1 to your computer and use it in GitHub Desktop.
Save but0n/e45aca652d97f9037c1cef2d359d4fc1 to your computer and use it in GitHub Desktop.
Vulkan Tutorial

Tutorial 0

What is Vulkan

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.

Tutorial 1 (Instance creation)

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 resources
vkDestroyInstance(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.

Tutorial 2 (Physical Devices Enumeration)

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 system
uint32_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 present
if (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.

// Enumerate all physical devices
vkPhysicalDeviceProperties deviceProperties;
for (uint32_i = 0; i < deviceCount; i++) {
    memset(&deviceProperties, 0, sizeof deviceProperties);
    vkGetPhysicalDeviceProperties(devices[i], &deviceProperties);
    printf("Driver Version: %d\n", deviceProperties.driverVersion);
    printf("Device Name:    %s\n", deviceProperties.deviceName);
    printf("Device Type:    %d\n", deviceProperties.deviceType);
    printf("API Version:    %d.%d.%d\n",
        // See note below regarding this:
        (deviceProperties.apiVersion>>22)&0x3FF,
        (deviceProperties.apiVersion>>12)&0x3FF,
        (deviceProperties.apiVersion&0xFFF));
}

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.

#define VK_VER_MAJOR(X) ((((uint32_t)(X))>>22)&0x3FF)
#define VK_VER_MINOR(X) ((((uint32_t)(X))>>12)&0x3FF)
#define VK_VER_PATCH(X) (((uint32_t)(X)) & 0xFFF)

Tutorial 3 (Device Queues)

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 families
for (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");
    }
}

Tutorial 4 (Device Creation)

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 queue
float 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.

Tutorial 5 (Getting a Surface)

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:

vector<const char *> enabledExtensions;
enabledExtensions.push_back(VK_KHR_SURFACE_EXTENSION_NAME);
#if defined(_WIN32)
enabledExtensions.push_back(VK_KHR_WIN32_SURFACE_EXTENSION_NAME);
#else
enabledExtensions.push_back(VK_KHR_XCB_SURFACE_EXTENSION_NAME);
#endif
applicationInfo.enabledExtensionCount = enabledExtensions.size();
applicationInfo.ppEnabledExtensionNames = &enabledExtensions[0];

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);
#endif
if (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 returned
if (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.

Tutorial 6 (Setting up a Swap chain)

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.

struct SwapChainBuffer {
    VkImage image;
    VkImageView view;
};

Swapchain

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.

struct SwapChain {
    VkInstance instance;
    VkDevice device;
    VkPhysicalDevice physicalDevice;
    VkSurfaceKHR surface;
    VkFormat colorFormat;
    VkColorSpaceKHR colorSpace;
    VkSwapchainKHR swapChain;
    vector<VkImage> images;
    vector<SwapChainBuffer> buffers;
    size_t nodeIndex;
    PFN_vkGetPhysicalDeviceSupportKHR fpGetPhysicalDeviceSurfaceSupportKHR;
    PFN_vkGetPhysicalDeviceSurfaceCapabilitiesKHR fpGetPhysicalDeviceSurfaceCapabilitiesKHR;
    PFN_vkGetPhysicalDeviceSurfaceFormatsKHR fpGetPhysicalDeviceSurfaceFormatsKHR;
    PFN_vkGetPhysicalDeviceSurfacePresentModesKHR fpGetPhysicalDeviceSurfacePresentModesKHR;
    PFN_vkCreateSwapchainKHR fpCreateSwapchainKHR;
    PFN_vkDestroySwapchainKHR fpDestroySwapchainKHR;
    PFN_vkGetSwapchainImagesKHR fpGetSwapchainImagesKHR;
    PFN_vkAcquireNExtImageKHR fpAcquireNextImageKHR;
    PFN_vkQueuePresentKHR fpQueuePresentKHR;
};

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, const char *name) {
    return vkGetInstanceProcAddr(swapChain->instance, name);
}
void *swapChainGetDeviceProc(SwapChain *swapChain, const char *name) {
    return vkGetDeviceProcAddr(swapChain->device, name);
}

#define GET_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); \
            return false; \
        } \
    } while (0)

#define GET_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); \
            return false; \
        } \
    } 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.

bool swapChainConnect(SwapChain *swapChain,
                      VkInstance instance,
                      VkPhysicalDevice physicalDevice,
                      VkDevice device)
{
    swapChain->instance = instance;
    swapChain->physicalDevice = physicalDevice;
    swapChain->device = device;

    // Get some instance local procedures
    GET_IPROC(GetPhysicalDeviceSurfaceSupportKHR);
    GET_IPROC(GetPhysicalDeviceSurfaceCapabilitiesKHR);
    GET_IPROC(GetPhysicalDeviceSurfaceFormatsKHR);
    GET_IPROC(GetPhysicalDeviceSurfacePrresentModesKHR);

    // Get some device local procedures
    GET_DPROC(CreateSwapchainKHR);
    GET_DPROC(DestroySwapchainKHR);
    GET_DPROC(GetSwapchainImagesKHR);
    GET_DPROC(AcquireNextImageKHR);
    GET_DPROC(QueuePresentKHR);

    return true;
}

Now we reiterate on the previous tutorial for connecting a platform handle to Vulkan.

#if defined(_WIN32)
bool swapChainPlatformConnect(SwapChain *swapChain,
                              HINSTANCE handle,
                              HWND window)
#elif defined(__ANDROID__)
bool swapChainPlatformConnect(SwapChain *swapChain,
                              ANativeWindow *window)
#else
bool swapChainPlatformConnect(SwapChain *swapChain,
                              xcb_connection_t *connection,
                              xcb_window_t *window)
#endif
{
    VkResult result;
#if defined(_WIN32)
    VkWin32SurfaceCreateInfoKHR surfaceCreateInfo;
    surfaceCreateInfo.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR;
    surfaceCreateInfo.hinstance = handle;
    surfaceCreateInfo.hwnd = window;
    VkResult result = vkCreateWin32SurfaceKHR(swapChain->instance,
                                              &surfaceCreateInfo,
                                              NULL,
                                              &swapChain->surface);
#elif defined(__ANDROID__)
    VkAndroidSurfaceCreateInfoKHR surfaceCreateInfo;
    surfaceCreateInfo.sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR;
    surfaceCreateInfo.window = window;
    VkResult result = vkCreateAndroidSurfaceKHR(swapChain->instance,
                                                &surfaceCreateInfo,
                                                NULL,
                                                &swapChain->surface);
#else
    VkXcbSurfaceCreateInfoKHR surfaceCreateInfo;
    surfaceCreateInfo.sType = VK_STRUCTURE_TYPE_XCB_SURFACE_CREATE_INFO_KHR;
    surfaceCreateInfo.connection = connection;
    surfaceCreateInfo.window = window;
    VkResult result = vkCreateXcbSurfaceKHR(swapChain->instance,
                                            &surfaceCreateInfo,
                                            NULL,
                                            &swapChain->surface);
#endif
    return result == VK_SUCCESS;
}

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)
bool swapChainInit(SwapChain *swapChain,
                   HINSTANCE handle,
                   HWND window)
#elif defined(__ANDROID__)
bool swapChainInit(SwapChain *swapChain,
                   ANativeWindow *window)
#else
bool swapChainInit(SwapChain *swapChain,
                   xcb_connection_t *connection,
                   xcb_window_t *window)
#endif
{
#if defined(_WIN32)
    if (!swapChainPlatformConnect(swapChain, handle, window))
        return false;
#elif defined(__ANDROID__)
    if (!swapChainPlatformConnect(swapChain, window))
        return false;
#else
    if (!swapChainPlatformConnect(swapChain, connection, window))
        return false;
#endif

    uint32_t queueCount;
    vkGetPhysicalDeviceQueueFamilyProperties(swapChain->physicalDevice,
                                             &queueCount,
                                             NULL);
    if (queueCount == 0)
        return false;

    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
    // render
    if (graphicIndex == UINT32_MAX || presentIndex == UINT32_MAX)
        return false;

    // In a future tutorial we'll look at supporting a separate graphics
    // and presenting queue
    if (graphicIndex != presentIndex) {
        fprintf(stderr, "Not supported\n");
        return false;
    }

    swapChain->nodeIndex = graphicIndex;

    // Now get a list of supported surface formats, as covered in the
    // previous tutorial
    uint32_t formatCount;
    if (swapChain->fpGetPhysicalDeviceSurfaceFormatsKHR(swapChain->physicalDevice,
                                                        swapChain->surface,
                                                        &formatCount,
                                                        NULL) != VK_SUCCESS)
        return false;
    if (formatCount == 0)
        return false;
    vector<VkSurfaceFormatKHR> surfaceFormats(formatCount);
    if (swapChain->fpGetPhysicalDeviceSurfaceFormatsKHR(swapChain->physicalDevice,
                                                        swapChain->surface,
                                                        &formatCount,
                                                        &surfaceFormats[0]) != VK_SUCCESS)
        return false;

    // 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 returned
    if (formatCount == 1 && surfaceFormats[0].format == VK_FORMAT_UNDEFINED)
        swapChain->colorFormat = VK_FORMAT_B8G8R8A8_UNORM;
    else {
        if (formatCount == 0)
            return false;
        swapChain->colorFormat = surfaceFormats[0].format;
    }
    swapChain->colorSpace = surfaceFormats[0].colorSpace;
    return true;
}

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.
void setImageLayout(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 finished
    if (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 finished
    if (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 finished
    if (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 finished
    if (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 finished
    if (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 finished
    if (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 finished
    if (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 finished
    if (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 finished
    if (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 buffer
    vkCmdPipelineBarrier(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.

bool swapChainCreate(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)
        return false;

    // 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)
        return false;
    if (presentModeCount == 0)
        return false;
    vector<VkPresentModeKHR> presentModes(presentModeCount);
    if (swapChain->fpGetPhysicalDeviceSurfacePresentModesKHR(swapChain->physicalDevice,
                                                             swapChain->surface,
                                                             &presentModeCount,
                                                             &presentModes[0]) != VK_SUCCESS)
        return false;

    // 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 supported
    for (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 chain
    uint32_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)
        return false;

    // If an existing swap chain is recreated, destroy the old one. This
    // also cleans up all presentable images
    if (oldSwapChain != VK_NULL_HANDLE) {
        swapChain->fpDestroySwapchainKHR(swapChain->device,
                                         oldSwapChain,
                                         NULL);
    }

    // Now get the presentable images from the swap chain
    uint32_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);
        return false;
    }

    // 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 layout
        setImageLayout(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 view
        if (vkCreateImageView(swapChain->device,
                              &colorAttachmentView,
                              NULL,
                              &swapChain->buffers[i].view) != VK_SUCCESS)
            goto failed;
    }

    return true;
}

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.

VkResult swapChainAcquireNext(SwapChain *swapChain,
                              VkSemaphore presentCompleteSemaphore,
                              uint32_t *currentBuffer)
{
    return swapChain->fpAcquireNextImageKHR(swapChain->device,
                                            swapChain->swapChain,
                                            UINT64_MAX,
                                            presentCompleteSemaphore,
                                            (VkFence)0,
                                            currentBuffer);
}

VkResult swapChainQueuePresent(SwapChain *swapChain,
                               VkQueue queue,
                               uint32_t currentBuffer)
{
    VkPresentInfoKHR = presentInfo = { };
    presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
    presentInfo.pNext = NULL;
    presentInfo.swapchainCount = 1;
    presentInfo.pSwapchains = &swapChain->swapChain;
    presentInfo.pImaghheIndices = &currentBuffer;
    return swapChain->fpQueuePresentKHR(queue, &presentInfo);
}

Finally we need a destructor to free resources we've allocated for our swap chain

void swapChainCleanup(SwapChain *swapChain) {
    for (auto &it : swapChain->buffers)
        vkDestroyImageView(swapChain->device, it.view, NULL);
    swapChain->fpDestroySwapchainKHR(swapChain->device,
                                     swapChain->swapChain,
                                     NULL);
    vkDestroySurfaceKHR(swapChain->instance,
                        swapChain->surface,
                        NULL);
}

With that we're now done our framework. In the next tutorial we can begin rendering!

A toolkit and Resizing

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)

It's available here

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.

VkSurfaceCapabilitiesKHR surfaceCapabilities;
vkGetPhysicalDeviceSurfaceCapabilitiesKHR(m_physicalDevice, m_surface, &surfaceCapabilities);
if (surfaceCapabilities.currentExtent.width == 0 || surfaceCapabilities.currentExtent.height == 0)
    return false;

if (m_swapchainCreateInfo.imageExtent.width == surfaceCapabilities.currentExtent.width &&
    m_swapchainCreateInfo.imageExtent.height == surfaceCapabilities.currentExtent.height)
{
    return true;
}

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)
    return false;

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.

vkDeviceWaitIdle(m_device);
vkDestroySwapchainKHR(m_device, m_swapchainCreateInfo.oldSwapchain, nullptr);

Now we can construct our new swap chain images

uint32_t imagesCount = 0;
vkGetSwapchainImagesKHR(m_device, m_swapchain, &imagesCount, nullptr);
vkGetSwapchainImagesKHR(m_device, m_swapchain, &imagesCount, m_presentImages);

Then we construct a command buffer to make these changes present

VkCommandBufferAllocateInfo setupBufferInfo = {};
setupBufferInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
setupBufferInfo.commandPool = m_commandPool;
setupBufferInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
setupBufferInfo.commandBufferCount = 1;

VkCommandBuffer setupBuffer;
result = vkAllocateCommandBuffers(m_device, &setupBufferInfo, &setupBuffer);
if (result != VK_SUCCESS)
    return false;

VkCommandBufferBeginInfo commandBufferBeginInfo;
commandBufferBeginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;

result = vkBeginCommandBuffer(setupBuffer, &commandBufferBeginInfo);
if (result != VK_SUCCESS)
  return false;

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 first
  vkCmdPipelineBarrier(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

VkSubmitInfo submitInfo = {};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &setupBuffer;

vkQueueSubmit(m_queue, 1, &submitInfo, VK_NULL_HANDLE);

vkQueueWaitIdle(m_queue);

It's now immediately active, we'll no longer need this command buffer; so let us not leak resources

vkFreeCommandBuffers(m_device, m_commandPool, 1, &setupBuffer);

return true;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment