Skip to content

Instantly share code, notes, and snippets.

@huhlig
Forked from graphitemaster/T0.md
Created February 25, 2016 14:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save huhlig/56d4bd018f2fa0afe1d1 to your computer and use it in GitHub Desktop.
Save huhlig/56d4bd018f2fa0afe1d1 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.

vkApplication 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&0x3FF));
}

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) (((X)>>22)&0x3FF)
#define VK_VER_MINOR(X) (((X)>>12)&0x3FF)
#define VK_VER_PATCH(X) ((X) & 0x3FF)

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;

// 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];

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;
VkGetPhysicalDeviceSurfaceCapabilitiesKHR(physicalDevice, surface, &formatCount, NULL);
vector<VkSurfaceFormatKHR> surfaceFormats(formatCount);
VkGetPhysicalDeviceSurfaceCapabilitiesKHR(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.

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