Initialize debug functionality

In order to obtain debugging information from the debug layers, we will have to interface with the VK_EXT_debug_report extension to specify a callback function. The functions required are not part of the normal set of Vulkan functions, as the debug report functionality is part of an extension.

As a result, we will have to obtain the functions from the extensions before we are able to use this functionality. While the functions exist in vulkan.h, using these functions directly will result in a linker error, due to their optional nature.

Obtaining functions from instance-level extensions is done through vkGetInstanceProcAddr. This will return a pointer to the function, which will need to be casted to the calling signature of that function.

Function signatures are defined in vulkan.h, and are consistent in naming. Function signatures start with PFN_ followed by the function name. So for instance: vkCreateDebugReportCallbackEXT will have a call signature of PFN_vkCreateDebugReportCallbackEXT. Casting the result of vkGetInstanceProcAddr to a variable of this type will allow you to call the function by “calling” the variable.

The system used in the example below is a little bit more sophisticated in order to make adding extra functions as simple as possible, but the basic principe is nothing more than casting the result of vkGetInstanceProcAddr to the appropriate calling signature.

struct
{
    const char* funcName;
    PFN_vkVoidFunction* ptrDest;
} instanceFuncs[] = {
#define DEF_FUNC(x) { .funcName = #x, .ptrDest = (PFN_vkVoidFunction*)&(renderer->vkInstanceVtbl.##x) }

#if !NO_DEBUG_REPORT
    // Debug report functions
    DEF_FUNC(vkCreateDebugReportCallbackEXT),
    DEF_FUNC(vkDestroyDebugReportCallbackEXT),
    DEF_FUNC(vkDebugReportMessageEXT),
#endif

#undef DEF_FUNC
};

uint32_t failedFuncPtrCount = 0;
for ( uint32_t i = 0; i < STATIC_ARRAY_SIZE ( instanceFuncs ); i++ )
{
    *(instanceFuncs[i].ptrDest) = vkGetInstanceProcAddr(renderer->instance, instanceFuncs[i].funcName);
    if ( !*(instanceFuncs[i].ptrDest) )
    {
        LOG("ERROR", "Could not find instance function \"%s\"", instanceFuncs[i].funcName );
        failedFuncPtrCount++;
    }
}

if ( failedFuncPtrCount )
    RETURN_ERROR(-1,"Not all required instance functions were found; Aborting...");

The snippet here composes a list of all function names and output locations for the query code to fill. All function variables are located in renderer->vkInstanceVtbl with the function name as variable name, and the appropriate calling signature. For more information, refer to the Vulkan examples provided with this guide.

With the functions queried, we now simply need to use the function vkCreateDebugReportCallbackEXT in order to register a function of ours as the function to be called if anything is wrong in our use of the Vulkan API.

#if !NO_DEBUG_REPORT
result = renderer->vkInstanceVtbl.vkCreateDebugReportCallbackEXT (
    renderer->instance,
    &(VkDebugReportCallbackCreateInfoEXT){
        .sType       = VK_STRUCTURE_TYPE_DEBUG_REPORT_CREATE_INFO_EXT,
        .pNext       = NULL,
        .flags       = VK_DEBUG_REPORT_ERROR_BIT_EXT | VK_DEBUG_REPORT_WARNING_BIT_EXT,
        .pfnCallback = VulkanDebugCallback,
        .pUserData   = NULL,
    },
    NULL,
    &renderer->debugReportCallback
);

if ( result != VK_SUCCESS )
    RETURN_ERROR(-1,"vkCreateDebugReportCallbackEXT failed (0x%08X)", (uint32_t)result);
#endif

The VkDebugReportCallbackEXT created by this function call is not necessary unless you intend to delete the debug report callback at a later time.

This will specify VulkanDebugCallback as the function to handle our Vulkan debug messages, as noted in pfnCallback. In flags we specify the types of messages we are interested in. In our case, we are just interested in errors and warnings.

The debug callback as in the examples is specified as follows:

VKAPI_ATTR VkBool32 VKAPI_CALL
VulkanDebugCallback(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 )
		LOG("ERR ", "[%s] [Vk#%d]: %s", pLayerPrefix, msgCode, pMsg );
	else if ( msgFlags & VK_DEBUG_REPORT_WARNING_BIT_EXT )
		LOG("WARN", "[%s] [Vk#%d]: %s", pLayerPrefix, msgCode, pMsg );
	else if ( msgFlags & VK_DEBUG_REPORT_DEBUG_BIT_EXT )
		LOG("DBG ", "[%s] [Vk#%d]: %s", pLayerPrefix, msgCode, pMsg );
	else if ( msgFlags & VK_DEBUG_REPORT_PERFORMANCE_WARNING_BIT_EXT )
		LOG("PERF", "[%s] [Vk#%d]: %s", pLayerPrefix, msgCode, pMsg );
	else if ( msgFlags & VK_DEBUG_REPORT_INFORMATION_BIT_EXT )
		LOG("INFO", "[%s] [Vk#%d]: %s", pLayerPrefix, msgCode, pMsg );
	return 0;
}

Returning 0 from this function will tell the debug layer that triggered the message we are ignoring the message. Whereas ignoring the message is not recommended, it will cause the runtime to generally behave as though the validation layers were not there in the first place. The best course of action will generally be to return 0, but break on the return statement in the debugger as soon as the issue takes place. This not only allows errors to be ignored temporarily, but also allows for graphics debuggers such as Renderdoc. Renderdoc commonly causes warnings when doing a frame capture, so returning 0 is the best way to preserve behaviour while capturing.