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)

Loading instance-level functions

We have created a Vulkan Instance object. The next step is to enumerate physical devices, choose one of them, and create a logical device from it. These operations are performed with instance-level functions, of which we need to acquire the addresses.

How to do it...

  1. Take the handle of a created Vulkan Instance. Provide it in a variable of type VkInstance named instance.
  2. Choose the name (denoted as <function name>) of an instance-level function you want to load.
  3. Create a variable of type PFN_<function name> named <function name>.
  4. Call vkGetInstanceProcAddr( instance, "<function name>" ). Provide a handle for the created Instance in the first parameter and a function name in the second. Cast the result of this operation onto a PFN_<function name> type and store it in a <function name> variable.
  5. Confirm that this operation succeeded by checking if a value of a <function name> variable is not equal to nullptr.

How it works...

Instance-level functions are used mainly for operations on physical devices. There are multiple instance-level functions, with vkEnumeratePhysicalDevices(), vkGetPhysicalDeviceProperties(), vkGetPhysicalDeviceFeatures(), vkGetPhysicalDeviceQueueFamilyProperties(), vkCreateDevice(), vkGetDeviceProcAddr(), vkDestroyInstance() or vkEnumerateDeviceExtensionProperties() among them. However, this list doesn't include all instance-level functions.

How can we tell if a function is instance- or device-level? All device-level functions have their first parameter of type VkDevice, VkQueue, or VkCommandBuffer. So, if a function doesn't have such a parameter and is not from the global level, it is from an instance level. As mentioned previously, instance-level functions are used for manipulating with physical devices, checking their properties, abilities and, creating logical devices.

Remember that extensions can also introduce new functions. You need to add their functions to the function loading code in order to be able to use the extension in the application. However, you shouldn't load functions introduced by a given extension without enabling the extension first during Instance creation. If these functions are not supported on a given platform, loading them will fail (it will return a null pointer).

So, in order to load instance-level functions, we should update the ListOfVulkanFunctions.inl file as follows:

#ifndef INSTANCE_LEVEL_VULKAN_FUNCTION 
#define INSTANCE_LEVEL_VULKAN_FUNCTION( function ) 
#endif 

INSTANCE_LEVEL_VULKAN_FUNCTION( vkEnumeratePhysicalDevices ) 
INSTANCE_LEVEL_VULKAN_FUNCTION( vkGetPhysicalDeviceProperties ) 
INSTANCE_LEVEL_VULKAN_FUNCTION( vkGetPhysicalDeviceFeatures ) 
INSTANCE_LEVEL_VULKAN_FUNCTION( vkCreateDevice ) 
INSTANCE_LEVEL_VULKAN_FUNCTION( vkGetDeviceProcAddr ) 
//... 

#undef INSTANCE_LEVEL_VULKAN_FUNCTION 

// 

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

INSTANCE_LEVEL_VULKAN_FUNCTION_FROM_EXTENSION( vkGetPhysicalDeviceSurfaceSupportKHR, VK_KHR_SURFACE_EXTENSION_NAME ) 
INSTANCE_LEVEL_VULKAN_FUNCTION_FROM_EXTENSION( vkGetPhysicalDeviceSurfaceCapabilitiesKHR, VK_KHR_SURFACE_EXTENSION_NAME ) 
INSTANCE_LEVEL_VULKAN_FUNCTION_FROM_EXTENSION( vkGetPhysicalDeviceSurfaceFormatsKHR, VK_KHR_SURFACE_EXTENSION_NAME ) 

#ifdef VK_USE_PLATFORM_WIN32_KHR 
INSTANCE_LEVEL_VULKAN_FUNCTION_FROM_EXTENSION( vkCreateWin32SurfaceKHR, VK_KHR_WIN32_SURFACE_EXTENSION_NAME ) 
#elif defined VK_USE_PLATFORM_XCB_KHR 
INSTANCE_LEVEL_VULKAN_FUNCTION_FROM_EXTENSION( vkCreateXcbSurfaceKHR, VK_KHR_XLIB_SURFACE_EXTENSION_NAME ) 
#elif defined VK_USE_PLATFORM_XLIB_KHR 
INSTANCE_LEVEL_VULKAN_FUNCTION_FROM_EXTENSION( vkCreateXlibSurfaceKHR, VK_KHR_XCB_SURFACE_EXTENSION_NAME ) 
#endif 

#undef INSTANCE_LEVEL_VULKAN_FUNCTION_FROM_EXTENSION

In the preceding code, we added the names of several (but not all) instance-level functions. Each of them is wrapped into an INSTANCE_LEVEL_VULKAN_FUNCTION or an INSTANCE_LEVEL_VULKAN_FUNCTION_FROM_EXTENSION macro, and is placed between #ifndef and the #undef preprocessor definitions.

To implement the instance-level functions loading recipe using the preceding macros, we should write the following code:

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

#include "ListOfVulkanFunctions.inl" 

return true;

The preceding macro calls a vkGetInstanceProcAddr() function. It's the same function used to load global-level functions, but this time, the handle of a Vulkan Instance is provided in the first parameter. This way, we can load functions that can work properly only when an Instance object is created.

This function returns a pointer to the function whose name is provided in the second parameter. The returned value is of type void*, which is why it is then cast onto a type appropriate for a function we acquire the address of.

The type of a given function's prototype is defined based on its name, with a PFN_ before it. So, in the example, the type of the vkEnumeratePhysicalDevices() function's prototype will be defined as PFN_vkEnumeratePhysicalDevices.

If the vkGetInstanceProcAddr() function cannot find an address of the requested procedure, it returns nullptr. That's why we should perform a check and log the appropriate message in case of any problems.

The next step is to load functions that are introduced by extensions. Our function loading code acquires pointers of all functions that are specified with a proper macro in the ListOfVulkanFunctions.inl file, but we can't provide extension-specific functions in the same way, because they can be loaded only when appropriate extensions are enabled. When we don't enable any extension, only the core Vulkan API functions can be loaded. That's why we need to distinguish core API functions from extension-specific functions. We also need to know which extensions are enabled and which function comes from which extension. That's why a separate macro is used for functions introduced by extensions. Such a macro specifies a function name, but also the name of an extension in which a given function is specified. To load such functions, we can use the following code:

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

#include "ListOfVulkanFunctions.inl" 

return true;

enabled_extensions is a variable of type std::vector<char const *>, which contains the names of all enabled instance-level extensions. We iterate over all its elements and check whether the name of a given extension matches the name of an extension that introduces the provided function. If it does, we load the function in the same way as a normal core API function. Otherwise, we skip the pointer-loading code. If we don't enable the given extension, we can't load functions introduced by it.

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 device-level functions