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:
(For more resources related to this topic, see here.)
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:
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:
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.
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:
vector<const char *> instanceExtensionNames = {
. . . . // other extensios
VK_EXT_DEBUG_REPORT_EXTENSION_NAME,
};
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.
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.
Further resources on this subject: