16 min read

In this article by Parminder Singh, author of Learning Vulkan, we learn Vulkan debugging in order to avoid unpleasant mistakes.

Vulkan allows you to perform debugging through validation layers. These validation layer checks are optional and can be injected into the system at runtime. Traditional graphics APIs perform validation right up front using some sort of error-checking mechanism, which is a mandatory part of the pipeline. This is indeed useful in the development phase, but actually, it is an overhead during the release stage because the validation bugs might have already been fixed at the development phase itself. Such compulsory checks cause the CPU to spend a significant amount of time in error checking.

On the other hand, Vulkan is designed to offer maximum performance, where the optional validation process and debugging model play a vital role. Vulkan assumes the application has done its homework using the validation and debugging capabilities available at the development stage, and it can be trusted flawlessly at the release stage.

In this article, we will learn the validation and debugging process of a Vulkan application. We will cover the following topics:

  • Peeking into Vulkan debugging
  • Understanding LunarG validation layers and their features
  • Implementing debugging in Vulkan

(For more resources related to this topic, see here.)

Peeking into Vulkan debugging

Vulkan debugging validates the application implementation. It not only surfaces the errors, but also other validations, such as proper API usage. It does so by verifying each parameter passed to it, warning about the potentially incorrect and dangerous API practices in use and reporting any performance-related warnings when the API is not used optimally. By default, debugging is disabled, and it’s the application’s responsibility to enable it. Debugging works only for those layers that are explicitly enabled at the instance level at the time of the instance creation (VkInstance).

When debugging is enabled, it inserts itself into the call chain for the Vulkan commands the layer is interested in. For each command, the debugging visits all the enabled layers and validates them for any potential error, warning, debugging information, and so on.

Debugging in Vulkan is simple. The following is an overview that describes the steps required to enable it in an application:

  1. Enable the debugging capabilities by adding the VK_EXT_DEBUG_REPORT_EXTENSION_NAME extension at the instance level.
  2. Define the set of the validation layers that are intended for debugging. For example, we are interested in the following layers at the instance and device level. For more information about these layer functionalities, refer to the next section:
    • VK_LAYER_GOOGLE_unique_objects
    • VK_LAYER_LUNARG_api_dump
    • VK_LAYER_LUNARG_core_validation
    • VK_LAYER_LUNARG_image
    • VK_LAYER_LUNARG_object_tracker
    • VK_LAYER_LUNARG_parameter_validation
    • VK_LAYER_LUNARG_swapchain
    • VK_LAYER_GOOGLE_threading
  3. The Vulkan debugging APIs are not part of the core command, which can be statically loaded by the loader. These are available in the form of extension APIs that can be retrieved at runtime and dynamically linked to the predefined function pointers. So, as the next step, the debug extension APIs vkCreateDebugReportCallbackEXT and vkDestroyDebugReportCallbackEXT are queried and linked dynamically. These are used for the creation and destruction of the debug report.
  4. Once the function pointers for the debug report are retrieved successfully, the former API (vkCreateDebugReportCallbackEXT) creates the debug report object. Vulkan returns the debug reports in a user-defined callback, which has to be linked to this API.
  5. Destroy the debug report object when debugging is no more required.

Understanding LunarG validation layers and their features

The LunarG Vulkan SDK supports the following layers for debugging and validation purposes. In the following points, we have described some of the layers that will help you understand the offered functionalities:

  • VK_LAYER_GOOGLE_unique_objects: Non-dispatchable handles are not required to be unique; a driver may return the same handle for multiple objects that it considers equivalent. This behavior makes the tracking of the object difficult because it is not clear which object to reference at the time of deletion. This layer packs the Vulkan objects into a unique identifier at the time of creation and unpacks them when the application uses it. This ensures there is proper object lifetime tracking at the time of validation. As per LunarG’s recommendation, this layer must be last in the chain of the validation layer, making it closer to the display driver.
  • VK_LAYER_LUNARG_api_dump: This layer is helpful in knowing the parameter values passed to the Vulkan APIs. It prints all the data structure parameters along with their values.
  • VK_LAYER_LUNARG_core_validation: This is used for validating and printing important pieces of information from the descriptor set, pipeline state, dynamic state, and so on. This layer tracks and validates the GPU memory, object binding, and command buffers. Also, it validates the graphics and compute pipelines.
  • VK_LAYER_LUNARG_image: This layer can be used for validating texture formats, rendering target formats, and so on. For example, it verifies whether the requested format is supported on the device. It validates whether the image view creation parameters are reasonable for the image that the view is being created for.
  • VK_LAYER_LUNARG_object_tracker: This keeps track of object creation along with its use and destruction, which is helpful in avoiding memory leaks. It also validates that the referenced object is properly created and is presently valid.
  • VK_LAYER_LUNARG_parameter_validation: This validation layer ensures that all the parameters passed to the API are correct as per the specification and are up to the required expectation. It checks whether the value of a parameter is consistent and within the valid usage criteria defined in the Vulkan specification. Also, it checks whether the type field of a Vulkan control structure contains the same value that is expected for a structure of that type.
  • VK_LAYER_LUNARG_swapchain: This layer validates the use of the WSI swapchain extensions. For example, it checks whether the WSI extension is available before its functions could be used. Also, it validates that an image index is within the number of images in a swapchain.
  • VK_LAYER_GOOGLE_threading: This is helpful in the context of thread safety. It checks the validity of multithreaded API usage. This layer ensures the simultaneous use of objects using calls running under multiple threads. It reports threading rule violations and enforces a mutex for such calls. Also, it allows an application to continue running without actually crashing, despite the reported threading problem.
  • VK_LAYER_LUNARG_standard_validation: This enables all the standard layers in the correct order.

For more information on validation layers, visit LunarG’s official website. Check out https://vulkan.lunarg.com/doc/sdk and specifically refer to the Validation layer details section for more details.

Implementing debugging in Vulkan

Since debugging is exposed by validation layers, most of the core implementation of the debugging will be done under the VulkanLayerAndExtension class (VulkanLED.h/.cpp). In this section, we will learn about the implementation that will help us enable the debugging process in Vulkan:

The Vulkan debug facility is not part of the default core functionalities. Therefore, in order to enable debugging and access the report callback, we need to add the necessary extensions and layers:

  • Extension: Add the VK_EXT_DEBUG_REPORT_EXTENSION_NAME extension to the instance level. This will help in exposing the Vulkan debug APIs to the application:
          vector<const char *> instanceExtensionNames = {
          	. . . . // other extensios
          	VK_EXT_DEBUG_REPORT_EXTENSION_NAME,
          };
  • Layer: Define the following layers at the instance level to allow debugging at these layers:
     vector<const char *> layerNames = {
          	"VK_LAYER_GOOGLE_threading",     
          	"VK_LAYER_LUNARG_parameter_validation",
          	"VK_LAYER_LUNARG_device_limits", 
          	"VK_LAYER_LUNARG_object_tracker",
          	"VK_LAYER_LUNARG_image",         
          	"VK_LAYER_LUNARG_core_validation",
          	"VK_LAYER_LUNARG_swapchain",  
          	“VK_LAYER_GOOGLE_unique_objects”   
          };

In addition to the enabled validation layers, the LunarG SDK provides a special layer called VK_LAYER_LUNARG_standard_validation. This enables basic validation in the correct order as mentioned here. Also, this built-in metadata layer loads a standard set of validation layers in the optimal order. It is a good choice if you are not very specific when it comes to a layer.

a) VK_LAYER_GOOGLE_threading

b) VK_LAYER_LUNARG_parameter_validation

c) VK_LAYER_LUNARG_object_tracker

d) VK_LAYER_LUNARG_image

e) VK_LAYER_LUNARG_core_validation

f) VK_LAYER_LUNARG_swapchain

g) VK_LAYER_GOOGLE_unique_objects

These layers are then supplied to the vkCreateInstance() API to enable them:

VulkanApplication* appObj = VulkanApplication::GetInstance();
appObj->createVulkanInstance(layerNames, 
			instanceExtensionNames, title);

// VulkanInstance::createInstance()
VkResult VulkanInstance::createInstance(vector<const char *>& 
	layers, std::vector<const char *>& extensionNames,
	char const*const appName)
{

    . . .
    VkInstanceCreateInfo instInfo	= {};

    // Specify the list of layer name to be enabled.
    instInfo.enabledLayerCount	= layers.size();
    instInfo.ppEnabledLayerNames	= layers.data();
	
    // Specify the list of extensions to
    // be used in the application.
    instInfo.enabledExtensionCount   	= extensionNames.size();
    instInfo.ppEnabledExtensionNames 	= extensionNames.data();
    . . .

    vkCreateInstance(&instInfo, NULL, &instance);
}

The validation layer is very specific to the vendors and SDK version. Therefore, it is advisable to first check whether the layers are supported by the underlying implementation before passing them to the vkCreateInstance() API. This way, the application remains portable throughout when ran against another driver implementation. The areLayersSupported() is a user-defined utility function that inspects the incoming layer names against system-supported layers. The unsupported layers are informed to the application and removed from the layer names before feeding them into the system:

// VulkanLED.cpp

 VkBool32 VulkanLayerAndExtension::areLayersSupported
		(vector<const     char *> &layerNames)
{
    uint32_t checkCount = layerNames.size();
    uint32_t layerCount = layerPropertyList.size();
    std::vector<const char*> unsupportLayerNames;
    for (uint32_t i = 0; i < checkCount; i++) {
      VkBool32 isSupported = 0;
      for (uint32_t j = 0; j < layerCount; j++) {
      if (!strcmp(layerNames[i], layerPropertyList[j]. properties.layerName)) {
              isSupported = 1;
          }
      }

      if (!isSupported) {
          std::cout << "No Layer support found, removed”
	“ from layer: "<< layerNames[i] << endl;
          unsupportLayerNames.push_back(layerNames[i]);
      }
    else {
      cout << "Layer supported: " << layerNames[i] << endl;
     }
  }

   for (auto i : unsupportLayerNames) {
        auto it = std::find(layerNames.begin(), 
				layerNames.end(), i);
       if (it != layerNames.end()) layerNames.erase(it);
    }

   return true;
  }

The debug report is created using the vkCreateDebugReportCallbackEXT API. This API is not a part of Vulkan’s core commands; therefore, the loader is unable to link it statically. If you try to access it in the following manner, you will get an undefined symbol reference error:

vkCreateDebugReportCallbackEXT(instance, NULL, NULL, NULL);

All the debug-related APIs need to be queried using the vkGetInstanceProcAddr() API and linked dynamically. The retrieved API reference is stored in a corresponding function pointer called PFN_vkCreateDebugReportCallbackEXT. The VulkanLayerAndExtension::createDebugReportCallback() function retrieves the create and destroy debug APIs, as shown in the following implementation:

/********* VulkanLED.h *********/
// Declaration of the create and destroy function pointers
	PFN_vkCreateDebugReportCallbackEXT dbgCreateDebugReportCallback;
PFN_vkDestroyDebugReportCallbackEXT dbgDestroyDebugReportCallback;


/********* VulkanLED.cpp *********/
VulkanLayerAndExtension::createDebugReportCallback(){
  . . . 

  // Get vkCreateDebugReportCallbackEXT API
dbgCreateDebugReportCallback=(PFN_vkCreateDebugReportCallbackEXT) 
vkGetInstanceProcAddr(*instance,"vkCreateDebugReportCallbackEXT");
	
  if (!dbgCreateDebugReportCallback) {
		std::cout << "Error: GetInstanceProcAddr unable to locate
 vkCreateDebugReportCallbackEXT function.n";
		return VK_ERROR_INITIALIZATION_FAILED;
	  }
  
  // Get vkDestroyDebugReportCallbackEXT API
  dbgDestroyDebugReportCallback= 
 (PFN_vkDestroyDebugReportCallbackEXT)vkGetInstanceProcAddr
 (*instance, "vkDestroyDebugReportCallbackEXT");

  if (!dbgDestroyDebugReportCallback) {
	std::cout << "Error: GetInstanceProcAddr unable to locate 
		vkDestroyDebugReportCallbackEXT function.n";
	return VK_ERROR_INITIALIZATION_FAILED;
  }
  . . . 
 }

The vkGetInstanceProcAddr() API obtains the instance-level extensions dynamically; these extensions are not exposed statically on a platform and need to be linked through this API dynamically. The following is the signature of this API:

PFN_vkVoidFunction vkGetInstanceProcAddr(
                             VkInstance     instance,
                             const char*    name);

The following table describes the API fields:

Parameters

Description

instance

This is a VkInstance variable. If this variable is NULL, then the name must be one of these: vkEnumerateInstanceExtensionProperties, vkEnumerateInstanceLayerProperties, or vkCreateInstance.

name

This is the name of the API that needs to be queried for dynamic linking.

 

Using the dbgCreateDebugReportCallback()function pointer, create the debugging report object and store the handle in debugReportCallback. The second parameter of the API accepts a VkDebugReportCallbackCreateInfoEXT control structure. This data structure defines the behavior of the debugging, such as what should the debug information include—errors, general warnings, information, performance-related warning, debug information, and so on. In addition, it also takes the reference of a user-defined function (debugFunction); this helps filter and print the debugging information once it is retrieved from the system. Here’s the syntax for creating the debugging report:

struct VkDebugReportCallbackCreateInfoEXT {
    	VkStructureType                 type;
    	const void*                     next;
    	VkDebugReportFlagsEXT           flags;
    	PFN_vkDebugReportCallbackEXT    fnCallback;
    	void*                           userData;
};

The following table describes the purpose of the mentioned API fields:

Parameters

Description

type

This is the type information of this control structure. It must be specified as VK_STRUCTURE_TYPE_DEBUG_REPORT_CREATE_INFO_EXT.

flags

This is to define the kind of debugging information to be retrieved when debugging is on; the next table defines these flags.

fnCallback

This field refers to the function that filters and displays the debug messages.

The VkDebugReportFlagBitsEXT control structure can exhibit a bitwise combination of the following flag values:

Insert table here

The createDebugReportCallback function implements the creation of the debug report. First, it creates the VulkanLayerAndExtension control structure object and fills it with relevant information. This primarily includes two things: first, assigning a user-defined function (pfnCallback) that will print the debug information received from the system (see the next point), and second, assigning the debugging flag (flags) in which the programmer is interested:

/********* VulkanLED.h *********/
// Handle of the debug report callback
VkDebugReportCallbackEXT debugReportCallback;
	
	      // Debug report callback create information control structure
VkDebugReportCallbackCreateInfoEXT dbgReportCreateInfo = {};


/********* VulkanLED.cpp *********/
VulkanLayerAndExtension::createDebugReportCallback(){
   . . . 
    // Define the debug report control structure, 
   // provide the reference of 'debugFunction',
   // this function prints the debug information on the console.
   dbgReportCreateInfo.sType	= VK_STRUCTURE_TYPE_DEBUG_REPORT_CREATE_INFO_EXT;
   dbgReportCreateInfo.pfnCallback = debugFunction;
   dbgReportCreateInfo.pUserData = NULL;
   dbgReportCreateInfo.pNext	   = NULL;
   dbgReportCreateInfo.flags	   =
                    VK_DEBUG_REPORT_WARNING_BIT_EXT |
                    VK_DEBUG_REPORT_PERFORMANCE_WARNING_BIT_EXT |
                    VK_DEBUG_REPORT_ERROR_BIT_EXT |
                    VK_DEBUG_REPORT_DEBUG_BIT_EXT;

   // Create the debug report callback and store the handle
   // into 'debugReportCallback'
   result = dbgCreateDebugReportCallback
       (*instance, &dbgReportCreateInfo, NULL, &debugReportCallback);

   if (result == VK_SUCCESS) {
 	cout << "Debug report callback object created successfullyn";
    }
         return result;
}

Define the debugFunction() function that prints the retrieved debug information in a user-friendly way. It describes the type of debug information along with the reported message:

VKAPI_ATTR VkBool32 VKAPI_CALL
VulkanLayerAndExtension::debugFunction( VkFlags msgFlags,
             VkDebugReportObjectTypeEXT objType, uint64_t srcObject, 
          size_t location, int32_t msgCode, const char *pLayerPrefix,
          const char *pMsg, void *pUserData){
	
   if (msgFlags & VK_DEBUG_REPORT_ERROR_BIT_EXT) {
std::cout << "[VK_DEBUG_REPORT] ERROR: ["      <<layerPrefix<<"]
                                   Code" << msgCode << ":" << msg << std::endl;

    }
    	    else if (msgFlags & VK_DEBUG_REPORT_WARNING_BIT_EXT) {
		std::cout << "[VK_DEBUG_REPORT] WARNING: ["<<layerPrefix<<"]
                            Code" << msgCode << ":" << msg << std::endl;
	    }
	    else if (msgFlags & VK_DEBUG_REPORT_INFORMATION_BIT_EXT) {
std::cout<<"[VK_DEBUG_REPORT] INFORMATION:[" <<layerPrefix<<"]
                              Code" << msgCode << ":" << msg << std::endl;

    }
	    else if(msgFlags& VK_DEBUG_REPORT_PERFORMANCE_WARNING_BIT_EXT){
		cout <<"[VK_DEBUG_REPORT] PERFORMANCE: ["<<layerPrefix<<"]
                              Code" << msgCode << ":" << msg << std::endl;
	    }
	    else if (msgFlags & VK_DEBUG_REPORT_DEBUG_BIT_EXT) {
		cout << "[VK_DEBUG_REPORT] DEBUG: ["<<layerPrefix<<"]
                        Code" << msgCode << ":" << msg <<       std::endl;
	    }
	    else {
	return VK_FALSE;
	    }

	    return VK_SUCCESS;
     }

The following table describes the various fields from the debugFunction()callback:

Parameters

Description

msgFlags

This specifies the type of debugging event that has triggered the call, for example, an error, warning, performance warning, and so on.

objType

This is the type object that is manipulated by the triggering call.

srcObject

This is the handle of the object that’s being created or manipulated by the triggered call.

location

This refers to the place of the code describing the event.

msgCode

This refers to the message code.

layerPrefix

This is the layer responsible for triggering the debug event.

msg

This field contains the debug message text.

userData

Any application-specific user data is specified to the callback using this field.

 The debugFunction callback has a Boolean return value. The true return value indicates the continuation of the command chain to subsequent validation layers even after an error is occurred.

However, the false value indicates the validation layer to abort the execution when an error occurs. It is advisable to stop the execution at the very first error.

Having an error itself indicates that something has occurred unexpectedly; letting the system run in these circumstances may lead to undefined results or further errors, which could be completely senseless sometimes. In the latter case, where the execution is aborted, it provides a better chance for the developer to concentrate and fix the reported error. In contrast, it may be cumbersome in the former approach, where the system throws a bunch of errors, leaving the developers in a confused state sometimes.

In order to enable debugging at vkCreateInstance, provide dbgReportCreateInfo to the VkInstanceCreateInfo’spNext field:

VkInstanceCreateInfo instInfo	= {};
     	    . . . 
    instInfo.pNext = &layerExtension.dbgReportCreateInfo;
    vkCreateInstance(&instInfo, NULL, &instance);

Finally, once the debug is no longer in use, destroy the debug callback object:

void VulkanLayerAndExtension::destroyDebugReportCallback(){
	  VulkanApplication* appObj = VulkanApplication::GetInstance();
	  dbgDestroyDebugReportCallback(instance,debugReportCallback,NULL);
       }

The following is the output from the implemented debug report. Your output may differ from this based on the GPU vendor and SDK provider. Also, the explanation of the errors or warnings reported are very specific to the SDK itself. But at a higher level, the specification will hold; this means you can expect to see a debug report with a warning, information, debugging help, and so on, based on the debugging flag you have turned on.

Summary

This article was short, precise, and full of practical implementations. Working on Vulkan without debugging capabilities is like shooting in the dark. We know very well that Vulkan demands an appreciable amount of programming and developers make mistakes for obvious reasons; they are humans after all. We learn from our mistakes, and debugging allows us to find and correct these errors. It also provides insightful information to build quality products.

Let’s do a quick recap. We learned the Vulkan debugging process. We looked at the various LunarG validation layers and understood the roles and responsibilities offered by each one of them. Next, we added a few selected validation layers that we were interested to debug. We also added the debug extension that exposes the debugging capabilities; without this, the API’s definition could not be dynamically linked to the application. Then, we implemented the Vulkan create debug report callback and linked it to our debug reporting callback; this callback decorates the captured debug report in a user-friendly and presentable fashion. Finally, we implemented the API to destroy the debugging report callback object.

Resources for Article:


Further resources on this subject:


LEAVE A REPLY

Please enter your comment!
Please enter your name here