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)

Checking available queue families and their properties

In Vulkan, when we want to perform operations on hardware, we submit them to queues. The operations within a single queue are processed one after another, in the same order they were submitted--that's why it's called a queue. However, operations submitted to different queues are processed independently (if we need, we can synchronize them):

Different queues may represent different parts of the hardware, and thus may support different kinds of operations. Not all operations may be performed on all queues.

Queues with the same capabilities are grouped into families. A device may expose any number of queue families, and each family may contain one or more queues. To check what operations can be performed on the given hardware, we need to query the properties of all queue families.

How to do it...

  1. Take one of the physical device handles returned by the vkEnumeratePhysicalDevices() function and store it in a variable of type VkPhysicalDevice called physical_device.
  2. Prepare a variable of type uint32_t named queue_families_count.
  3. Call vkGetPhysicalDeviceQueueFamilyProperties( physical_device, &queue_families_count, nullptr ). Provide the handle of a physical device in the first parameter; the second parameter should point to the queue_families_count variable, and the final parameter should be set to nullptr.
  4. After the successful call, the queue_families_count variable will contain the number of all queue families exposed by a given physical device.
  5. Prepare a storage for the list of queue families and their properties. A very convenient solution is to use a variable of type std::vector. Its elements must be of type VkQueueFamilyProperties. Call the variable queue_families.
  6. Resize the vector to be able to hold at least the queue_families_count elements.
  7. Call vkGetPhysicalDeviceQueueFamilyProperties( physical_device, &queue_families_count, &queue_families[0] ). The first and second argument should be the same as in the previous call; the last parameter should point to the first element of the queue_families vector.
  8. To be sure that everything went okay, check that the queue_families_count variable is greater than zero. If successful, the properties of all queue families will be stored in the queue_families vector.

How it works...

The implementation of the preceding recipe, similarly to other queries, can be divided into two stages: Firstly, we acquire information about the total number of queue families available on a given physical device. This is done by calling a vkGetPhysicalDeviceQueueFamilyProperties() function, with the last argument set to nullptr:

uint32_t queue_families_count = 0; 

vkGetPhysicalDeviceQueueFamilyProperties( physical_device, &queue_families_count, nullptr ); 
if( queue_families_count == 0 ) { 
  std::cout << "Could not get the number of queue families." << std::endl; 
  return false; 
}

Secondly, when we know how many queue families there are, we can prepare sufficient memory to be able to store the properties of all of them. In the presented example, we create a variable of type std::vector with VkQueueFamilyProperties elements and resize it to the value returned by the first query. After that, we perform a second vkGetPhysicalDeviceQueueFamilyProperties() function call, with the last parameter pointing to the first element of the created vector. In this vector, the parameters of all available queue families will be stored.

queue_families.resize( queue_families_count ); 
vkGetPhysicalDeviceQueueFamilyProperties( physical_device, &queue_families_count, &queue_families[0] ); 
if( queue_families_count == 0 ) { 
  std::cout << "Could not acquire properties of queue families." << std::endl; 
  return false; 
} 

return true;

The most important information we can get from properties is the types of operations that can be performed by the queues in a given family. Types of operations supported by queues are divided into:

  • Graphics: For creating graphics pipelines and drawing
  • Compute: For creating compute pipelines and dispatching compute shaders
  • Transfer: Used for very fast memory-copying operations
  • Sparse: Allows for additional memory management features

Queues from the given family may support more than one type of operation. There may also be a situation where different queue families support exactly the same types of operation.

Family properties also inform us about the number of queues that are available in the given family, about the timestamp support (for time measurements), and the granularity of image transfer operations (how small parts of image can be specified during copy/blit operations).

With the knowledge of the number of queue families, their properties, and the available number of queues in each family, we can prepare for logical device creation. All this information is needed, because we don't create queues by ourselves. We just request them during logical device creation, for which we must specify how many queues are needed and from which families. When a device is created, queues are created automatically along with it. We just need to acquire the handles of all requested queues.

See also

  • The following recipes in this chapter:
    • Selecting index of a queue family with desired capabilities
    • Creating a logical device
    • Getting a device queue
    • Creating a logical device with geometry shaders, graphics, and compute queues
  • The following recipe in Chapter 2, Image Presentation:
    • Selecting a queue family that supports presentation to a given surface