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