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 with geometry shaders, graphics, and compute queues

In Vulkan, when we create various objects, we need to prepare many different structures that describe the creation process itself, but they may also require other objects to be created.

A logical device is no different: We need to enumerate physical devices, check their properties and supported queue families, and prepare a VkDeviceCreateInfo structure that requires much more information.

To organize these operations, we will present a sample recipe that creates a logical device from one of the available physical devices that support geometry shaders, and both graphics and compute queues.

How to do it...

  1. Prepare a variable of type VkDevice named logical_device.
  2. Create two variables of type VkQueue, one named graphics_queue and one named compute_queue.
  1. Create a variable of type std::vector<VkPhysicalDevice> named physical_devices.
  2. Get the list of all physical devices available on a given platform and store it in the physical_devices vector (refer to the Enumerating available physical devices recipe).
  3. For each physical device from the physical_devices vector:
    1. Create a variable of type VkPhysicalDeviceFeatures named device_features.
    2. Acquire the list of features supported by a given physical device and store it in the device_features variable.
    3. Check whether the geometryShader member of the device_features variable is equal to VK_TRUE (is not 0). If it is, reset all the other members of the device_features variable (set their values to zero); if it is not, start again with another physical device.
    4. Create two variables of type uint32_t named graphics_queue_family_index and compute_queue_family_index.
    5. Acquire indices of queue families that support graphics and compute operations, and store them in the graphics_queue_family_index and compute_queue_family_index variables, respectively (refer to the Selecting index of a queue family with desired capabilities recipe). If any of these operations is not supported, search for another physical device.
    6. Create a variable of type std::vector with elements of type QueueInfo (refer to Creating a logical device recipe). Name this variable requested_queues.
    7. Store the graphics_queue_family_index variable and one-element vector of floats with a 1.0f value in the requested_queues variable. If a value of the compute_queue_family_index variable is different than the value of the graphics_queue_family_index variable, add another element to the requested_queues vector, with the compute_queue_family_index variable and a one-element vector of floats with 1.0f value.
    8. Create a logical device using the physical_device, requested_queues, device_features and logical_device variables (refer to the Creating a logical device recipe). If this operation failed, repeat the preceding operations with another physical device.
    1. If the logical device was successfully created, load the device-level functions (refer to the Loading device-level functions recipe). Get the handle of the queue from the graphics_queue_family_index family and store it in the graphics_queue variable. Get the queue from the compute_queue_family_index family and store it in the compute_queue variable.

How it works...

To start the process of creating a logical device, we need to acquire the handles of all physical devices available on a given computer:

std::vector<VkPhysicalDevice> physical_devices; 
EnumerateAvailablePhysicalDevices( instance, physical_devices );

Next we need to loop through all available physical devices. For each such device, we need to acquire its features. This will give us the information about whether a given physical device supports geometry shaders:

for( auto & physical_device : physical_devices ) { 
  VkPhysicalDeviceFeatures device_features; 
  VkPhysicalDeviceProperties device_properties; 
  GetTheFeaturesAndPropertiesOfAPhysicalDevice( physical_device, device_features, device_properties ); 

  if( !device_features.geometryShader ) { 
    continue; 
  } else { 
    device_features = {}; 
    device_features.geometryShader = VK_TRUE; 
  }

If geometry shaders are supported, we can reset all the other members of a returned list of features. We will provide this list during the logical device creation, but we don't want to enable any other feature. In this example, geometry shaders are the only additional feature we want to use.

Next we need to check if a given physical device exposes queue families that support graphics and compute operations. This may be just one single family or two separate families. We acquire the indices of such queue families:

  uint32_t graphics_queue_family_index; 
  if( !SelectIndexOfQueueFamilyWithDesiredCapabilities( physical_device, VK_QUEUE_GRAPHICS_BIT, graphics_queue_family_index ) ) { 
    continue; 
  } 

  uint32_t compute_queue_family_index; 
  if( !SelectIndexOfQueueFamilyWithDesiredCapabilities( physical_device, VK_QUEUE_COMPUTE_BIT, compute_queue_family_index ) ) { 
    continue; 
  }

Next, we need to prepare a list of queue families, from which we want to request queues. We also need to assign priorities to each queue from each family:

  std::vector<QueueInfo> requested_queues = { { graphics_queue_family_index, { 1.0f } } }; 
  if( graphics_queue_family_index != compute_queue_family_index ) { 
    requested_queues.push_back( { compute_queue_family_index, { 1.0f } } ); 
  }

If graphics and compute queue families have the same index, we request only one queue from one queue family. If they are different, we need to request two queues: One from the graphics family and one from the compute family.

We are ready to create a logical device for which we provide the prepared data. Upon success, we can the load device-level functions and acquire the handles of the requested queues:

  if( !CreateLogicalDevice( physical_device, requested_queues, {}, &device_features, logical_device ) ) { 
    continue; 
  } else { 
    if( !LoadDeviceLevelFunctions( logical_device, {} ) ) { 
      return false; 
    } 
    GetDeviceQueue( logical_device, graphics_queue_family_index, 0, graphics_queue ); 
    GetDeviceQueue( logical_device, compute_queue_family_index, 0, compute_queue ); 
    return true; 
  } 
} 
return false;

See also

The following recipes in this chapter:

  • Enumerating available physical devices
  • Getting features and properties of a physical device
  • Selecting the index of a queue family with the desired capabilities
  • Creating a logical device
  • Loading device-level functions
  • Getting a device queue
  • Destroying a logical device