Search icon CANCEL
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Vulkan Cookbook

You're reading from   Vulkan Cookbook Work through recipes to unlock the full potential of the next generation graphics API—Vulkan

Arrow left icon
Product type Paperback
Published in Apr 2017
Publisher Packt
ISBN-13 9781786468154
Length 700 pages
Edition 1st Edition
Languages
Arrow right icon
Author (1):
Arrow left icon
Pawel Lapinski Pawel Lapinski
Author Profile Icon Pawel Lapinski
Pawel Lapinski
Arrow right icon
View More author details
Toc

Table of Contents (13) Chapters Close

Preface 1. Instance and Devices FREE CHAPTER 2. Image Presentation 3. Command Buffers and Synchronization 4. Resources and Memory 5. Descriptor Sets 6. Render Passes and Framebuffers 7. Shaders 8. Graphics and Compute Pipelines 9. Command Recording and Drawing 10. Helper Recipes 11. Lighting 12. Advanced Rendering Techniques

Loading device-level functions

We have created a logical device on which we can perform any desired operations, such as rendering a 3D scene, calculating collisions of objects in a game, or processing video frames. These operations are performed with device-level functions, but they are not available until we acquire them.

How to do it...

  1. Take the handle of a created logical device object. Store it in a variable of type VkDevice named logical_device.
  2. Choose the name (denoted as <function name>) of a device-level function you want to load.
  3. For each device-level function that will be loaded, create a variable of type PFN_<function name> named <function name>.
  4. Call vkGetDeviceProcAddr( device, "<function name>" ), in which you provide the handle of created logical device in the first argument and the name of the function in the second argument. Cast the result of this operation onto a PFN_<function name> type and store it in a <function name> variable.
  5. Confirm that the operation succeeded by checking that the value of a <function name> variable is not equal to nullptr.

How it works...

Almost all the typical work done in 3D rendering applications is performed using device-level functions. They are used to create buffers, images, samplers, or shaders. We use device-level functions to create pipeline objects, synchronization primitives, framebuffers, and many other resources. And, most importantly, they are used to record operations that are later submitted (using device-level functions too) to queues, where these operations are processed by the hardware. This all is done with device-level functions.

Device-level functions, like all other Vulkan functions, can be loaded using the vkGetInstanceProcAddr() function, but this approach is not optimal. Vulkan is designed to be a flexible API. It gives the option to perform operations on multiple devices in a single application, but when we call the vkGetInstanceProcAddr() function, we can't provide any parameter connected with the logical device. So, the function pointer returned by this function can't be connected with the device on which we want to perform the given operation. This device may not even exist at the time the vkGetInstanceProcAddr() function is called. That's why the vkGetInstanceProcAddr() function returns a dispatch function which, based on its arguments, calls the implementation of a function, that is proper for a given logical device. However, this jump has a performance cost: It's very small, but it nevertheless takes some processor time to call the right function.

If we want to avoid this unnecessary jump and acquire function pointers corresponding directly to a given device, we should do that by using a vkGetDeviceProcAddr(). This way, we can avoid the intermediate function call and improve the performance of our application. Such an approach also has some drawbacks: We need to acquire function pointers for each device created in an application. If we want to perform operations on many different devices, we need a separate list of function pointers for each logical device. We can't use functions acquired from one device to perform operations on a different device. But using C++ language's preprocessor, it is quite easy to acquire function pointers specific to a given device:

How do we know if a function is from the device-level and not from the global or instance-level? The first argument of device-level functions is of type VkDevice, VkQueue, or VkCommandBuffer. Most of the functions that will be introduced from now on are from the device level.

To load device-level functions, we should update the ListOfVulkanFunctions.inl file as follows:

#ifndef DEVICE_LEVEL_VULKAN_FUNCTION 
#define DEVICE_LEVEL_VULKAN_FUNCTION( function ) 
#endif 

DEVICE_LEVEL_VULKAN_FUNCTION( vkGetDeviceQueue ) 
DEVICE_LEVEL_VULKAN_FUNCTION( vkDeviceWaitIdle ) 
DEVICE_LEVEL_VULKAN_FUNCTION( vkDestroyDevice ) 

DEVICE_LEVEL_VULKAN_FUNCTION( vkCreateBuffer ) 
DEVICE_LEVEL_VULKAN_FUNCTION( vkGetBufferMemoryRequirements ) 
// ... 

#undef DEVICE_LEVEL_VULKAN_FUNCTION 

// 

#ifndef DEVICE_LEVEL_VULKAN_FUNCTION_FROM_EXTENSION 
#define DEVICE_LEVEL_VULKAN_FUNCTION_FROM_EXTENSION( function, extension ) 
#endif 

DEVICE_LEVEL_VULKAN_FUNCTION_FROM_EXTENSION( vkCreateSwapchainKHR, VK_KHR_SWAPCHAIN_EXTENSION_NAME ) 
DEVICE_LEVEL_VULKAN_FUNCTION_FROM_EXTENSION( vkGetSwapchainImagesKHR, VK_KHR_SWAPCHAIN_EXTENSION_NAME ) 
DEVICE_LEVEL_VULKAN_FUNCTION_FROM_EXTENSION( vkAcquireNextImageKHR, VK_KHR_SWAPCHAIN_EXTENSION_NAME ) 
DEVICE_LEVEL_VULKAN_FUNCTION_FROM_EXTENSION( vkQueuePresentKHR, VK_KHR_SWAPCHAIN_EXTENSION_NAME ) 
DEVICE_LEVEL_VULKAN_FUNCTION_FROM_EXTENSION( vkDestroySwapchainKHR, VK_KHR_SWAPCHAIN_EXTENSION_NAME ) 

#undef DEVICE_LEVEL_VULKAN_FUNCTION_FROM_EXTENSION

In the preceding code, we added, names of multiple device-level functions. Each of them is wrapped into a DEVICE_LEVEL_VULKAN_FUNCTION macro (if it is defined in the core API) or a DEVICE_LEVEL_VULKAN_FUNCTION_FROM_EXTENSION macro (if it is introduced by an extension), and is placed between proper #ifndef and #undef preprocessor directives. The list is, of course, incomplete, as there are too many functions to present them all here.

Remember that we shouldn't load functions introduced by a given extension without first enabling the extension during the logical device creation. If an extension is not supported, its functions are not available and the operation of loading them will fail. That's why, similarly to loading instance-level functions, we need to divide function-loading code into two blocks.

First, to implement the device-level core API functions loading using the preceding macros, we should write the following code:

#define DEVICE_LEVEL_VULKAN_FUNCTION( name )                      \
name = (PFN_##name)vkGetDeviceProcAddr( device, #name );          \
if( name == nullptr ) {                                           \
  std::cout << "Could not load device-level Vulkan function named: " #name << std::endl;                                      \
  return false;                                                   \
} 

#include "ListOfVulkanFunctions.inl" 

return true;

In this code sample, we create a macro that, for each occurrence of a DEVICE_LEVEL_VULKAN_FUNCTION() definition found in the ListOfVulkanFunctions.inl file, calls a vkGetDeviceProcAddr() function and provides the name of a procedure we want to load. The result of this operation is cast onto an appropriate type and stored in a variable with exactly the same name as the name of the acquired function. Upon failure, any additional information is displayed on screen.

Next, we need to load functions introduced by extensions. These extensions must have been enabled during logical device creation:

#define DEVICE_LEVEL_VULKAN_FUNCTION_FROM_EXTENSION( name, 
extension )                                                         \         
for( auto & enabled_extension : enabled_extensions ) {              \         
  if( std::string( enabled_extension ) == std::string( extension )
 ) {                                                                \  
    name = (PFN_##name)vkGetDeviceProcAddr( logical_device, #name );\                                                          
    if( name == nullptr ) {                                         \        
      std::cout << "Could not load device-level Vulkan function named: "                                                                   \  
        #name << std::endl;                                         \         
      return false;                                                 \         
    }                                                               \         
  }                                                                 \         
} 

#include "ListOfVulkanFunctions.inl" 

return true;

In the preceding code, we define the macro which iterates over all enabled extensions. They are defined in a variable of type std::vector<char const *> named enabled_extensions. In each loop iteration, the name of the enabled extension from the vector is compared with the name of an extension specified for a given function. If they match, the function pointer is loaded; if not, the given function is skipped as we can't load functions from un-enabled extensions.

See also

The following recipes in this chapter:

  • Preparing for loading Vulkan API functions
  • Loading function exported from a Vulkan Loader library
  • Loading global-level functions
  • Loading instance-level functions
You have been reading a chapter from
Vulkan Cookbook
Published in: Apr 2017
Publisher: Packt
ISBN-13: 9781786468154
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at €18.99/month. Cancel anytime