Book Image

Vulkan Cookbook

By : Pawel Lapinski
Book Image

Vulkan Cookbook

By: Pawel Lapinski

Overview of this book

Vulkan is the next generation graphics API released by the Khronos group. It is expected to be the successor to OpenGL and OpenGL ES, which it shares some similarities with such as its cross-platform capabilities, programmed pipeline stages, or nomenclature. Vulkan is a low-level API that gives developers much more control over the hardware, but also adds new responsibilities such as explicit memory and resources management. With it, though, Vulkan is expected to be much faster. This book is your guide to understanding Vulkan through a series of recipes. We start off by teaching you how to create instances in Vulkan and choose the device on which operations will be performed. You will then explore more complex topics such as command buffers, resources and memory management, pipelines, GLSL shaders, render passes, and more. Gradually, the book moves on to teach you advanced rendering techniques, how to draw 3D scenes, and how to improve the performance of your applications. By the end of the book, you will be familiar with the latest advanced techniques implemented with the Vulkan API, which can be used on a wide range of platforms.
Table of Contents (13 chapters)

Creating a logical device

The logical device is one the most important objects created in our application. It represents real hardware, along with all the extensions and features enabled for it and all the queues requested from it:

The logical device allows us to perform almost all the work typically done in rendering applications, such as creating images and buffers, setting the pipeline state, or loading shaders. The most important ability it gives us is recording commands (such as issuing draw calls or dispatching computational work) and submitting them to queues, where they are executed and processed by the given hardware. After such execution, we acquire the results of the submitted operations. These can be a set of values calculated by compute shaders, or other data (not necessarily an image) generated by draw calls. All this is performed on a logical device, so now we will look at how to create one.

Getting ready

In this recipe, we will use a variable of a custom structure type. The type is called QueueInfo and is defined as follows:

struct QueueInfo { 
  uint32_t           FamilyIndex; 
  std::vector<float> Priorities; 
};

In a variable of this type, we will store information about the queues we want to request for a given logical device. The data contains an index of a family from which we want the queues to be created, the total number of queues requested from this family, and the list of priorities assigned to each queue. As the number of priorities must be equal to the number of queues requested from a given family, the total number of queues we request from a given family is equal to the number of elements in the Priorities vector.

How to do it...

  1. Based on the features, limits, available extensions and supported types of operations, choose one of the physical devices acquired using the vkEnumeratePhysicalDevices() function call (refer to Enumerating available physical devices recipe). Take its handle and store it in a variable of type VkPhysicalDevice called physical_device.
  2. Prepare a list of device extensions you want to enable. Store the names of the desired extensions in a variable of type std::vector<char const *> named desired_extensions.
  1. Create a variable of type std::vector<VkExtensionProperties> named available_extensions. Acquire the list of all available extensions and store it in the available_extensions variable (refer to Checking available device extensions recipe).
  2. Make sure that the name of each extension from the desired_extensions variable is also present in the available_extensions variable.
  3. Create a variable of type VkPhysicalDeviceFeatures named desired_features.
  4. Acquire a set of features supported by a physical device represented by the physical_device handle and store it in the desired_features variable (refer to Getting features and properties of a physical device recipe).
  5. Make sure that all the required features are supported by a given physical device represented by the physical_device variable. Do that by checking if the corresponding members of the acquired desired_features structure are set to one. Clear the rest of the desired_features structure members (set them to zero).
  6. Based on the properties (supported types of operations), prepare a list of queue families, from which queues should be requested. Prepare a number of queues that should be requested from each selected queue family. Assign a priority for each queue in a given family: A floating point value from 0.0f to 1.0f (multiple queues may have the same priority value). Create a std::vector variable named queue_infos with elements of a custom type QueueInfo. Store the indices of queue families and a list of priorities in the queue_infos vector, the size of Priorities vector should be equal to the number of queues from each family.
  7. Create a variable of type std::vector<VkDeviceQueueCreateInfo> named queue_create_infos. For each queue family stored in the queue_infos variable, add a new element to the queue_create_infos vector. Assign the following values for members of a new element:
    1. VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO value for sType.
    2. nullptr value for pNext.
    3. 0 value for flags.
    4. Index of a queue family for queueFamilyIndex.
    5. Number of queues requested from a given family for queueCount.
    6. Pointer to the first element of a list of priorities of queues from a given family for pQueuePriorities.
  1. Create a variable of type VkDeviceCreateInfo named device_create_info. Assign the following values for members of a device_create_info variable:
    1. VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO value for sType.
    2. nullptr value for pNext.
    3. 0 value for flags.
    4. Number of elements of the queue_create_infos vector variable for queueCreateInfoCount.
    5. Pointer to the first element of the queue_create_infos vector variable in pQueueCreateInfos.
    6. 0 value for enabledLayerCount.
    7. nullptr value for ppEnabledLayerNames.
    8. Number of elements of the desired_extensions vector variable in enabledExtensionCount.
    9. Pointer to the first element of the desired_extensions vector variable (or nullptr if it is empty) in ppEnabledExtensionNames.
    10. Pointer to the desired_features variable in pEnabledFeatures.
  2. Create a variable of type VkDevice named logical_device.
  3. Call vkCreateDevice( physical_device, &device_create_info, nullptr, &logical_device ). Provide a handle of the physical device in the first argument, a pointer to the device_create_info variable in the second argument, a nullptr value in the third argument, and a pointer to the logical_device variable in the final argument.
  4. Make sure the operation succeeded by checking that the value returned by the vkCreateDevice() function call is equal to VK_SUCCESS.

How it works...

To create a logical device, we need to prepare a considerable amount of data. First we need to acquire the list of extensions that are supported by a given physical device, and then we need check that all the extensions we want to enable can be found in the list of supported extensions. Similar to Instance creation, we can't create a logical device with extensions that are not supported. Such an operation will fail:

std::vector<VkExtensionProperties> available_extensions; 
if( !CheckAvailableDeviceExtensions( physical_device, available_extensions ) ) { 
  return false; 
} 

for( auto & extension : desired_extensions ) { 
  if( !IsExtensionSupported( available_extensions, extension ) ) { 
    std::cout << "Extension named '" << extension << "' is not supported by a physical device." << std::endl; 
    return false; 
  } 
}

Next we prepare a vector variable named queue_create_infos that will contain information about queues and queue families we want to request for a logical device. Each element of this vector is of type VkDeviceQueueCreateInfo. The most important information it contains is an index of the queue family and the number of queues requested for that family. We can't have two elements in the vector that refer to the same queue family.

In the queue_create_infos vector variable, we also provide information about queue priorities. Each queue in a given family may have a different priority: A floating-point value between 0.0f and 1.0f, with higher values indicating higher priority. This means that hardware will try to schedule operations performed on multiple queues based on this priority, and may assign more processing time to queues with higher priorities. However, this is only a hint and it is not guaranteed. It also doesn't influence queues from other devices:

std::vector<VkDeviceQueueCreateInfo> queue_create_infos; 

for( auto & info : queue_infos ) { 
  queue_create_infos.push_back( { 
    VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO, 
    nullptr, 
    0, 
    info.FamilyIndex, 
    static_cast<uint32_t>(info.Priorities.size()), 
    info.Priorities.size() > 0 ? &info.Priorities[0] : nullptr 
  } ); 
};

The queue_create_infos vector variable is provided to another variable of type VkDeviceCreateInfo. In this variable, we store information about the number of different queue families from which we request queues for a logical device, number and names of enabled layers, and extensions we want to enable for a device, and also features we want to use.

Layers and extensions are not required for the device to work properly, but there are quite useful extensions, which must be enabled if we want to display Vulkan-generated images on screen.

Features are also not necessary, as the core Vulkan API gives us plenty of features to be able to generate beautiful images or perform complicated calculations. If we don't want to enable any feature, we can provide a nullptr value for the pEnabledFeatures member, or provide a variable filled with zeros. However, if we want to use more advanced features, such as geometry or tessellation shaders, we need to enable them by providing a pointer to a proper variable, previously acquiring the list of supported features, and making sure the ones we need are available. Unnecessary features can (and even should) be disabled, because there are some features that may impact performance. This situation is very rare, but it's good to bear this in mind. In Vulkan, we should do and use only those things that need to be done and used:

VkDeviceCreateInfo device_create_info = { 
  VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, 
  nullptr, 
  0, 
  static_cast<uint32_t>(queue_create_infos.size()), 
  queue_create_infos.size() > 0 ? &queue_create_infos[0] : nullptr, 
  0, 
  nullptr, 
  static_cast<uint32_t>(desired_extensions.size()), 
  desired_extensions.size() > 0 ? &desired_extensions[0] : nullptr, 
  desired_features 
};

The device_create_info variable is provided to the vkCreateDevice() function, which creates a logical device. To be sure that the operation succeeded, we need to check that the value returned by the vkCreateDevice() function call is equal to VK_SUCCESS. If it is, the handle of a created logical device is stored in the variable pointed to by the final argument of the function call:

VkResult result = vkCreateDevice( physical_device, &device_create_info, nullptr, &logical_device ); 
if( (result != VK_SUCCESS) || 
    (logical_device == VK_NULL_HANDLE) ) { 
  std::cout << "Could not create logical device." << std::endl; 
  return false; 
} 

return true;

See also

The following recipes in this chapter:

  • Enumerating available physical devices
  • Checking available device extensions
  • Getting features and properties of a physical device
  • Checking available queue families and their properties
  • Selecting the index of a queue family with the desired capabilities
  • Destroying a logical device