This post is a continuation of the previous post " Vulkan. Developer's Guide. Drawing a Triangle ", it is devoted to the translation of the chapter Validation layers.
Content
Validation layers
- What are validation layers?
- Using validation layers
- Interception of debug messages
- Debugging instance of Vulkan
- Testing
- Settings
What are validation layers?
The Vulkan API is designed around the idea of minimal driver workload, so by default the error detection capabilities are severely limited. Even such simple errors as incorrect values in enumerations or passing null pointers are usually not handled explicitly and lead to crashes or undefined behavior. Since Vulkan requires a detailed description of each action, such errors can occur quite often.
To solve this problem, Vulkan uses validation layers . Validation layers are optional components that can be plugged into function calls to perform additional operations. The following operations can be performed in the validation layers:
- Checking parameter values according to specification to detect errors
- Resource Leak Tracking
- Streaming safety check
- Logging of each call and its parameters
- Vulkan Call Tracking for Profiling and Replay
Below is an example of how a function could be implemented in the validation layer:
VkResult vkCreateInstance(
const VkInstanceCreateInfo* pCreateInfo,
const VkAllocationCallbacks* pAllocator,
VkInstance* instance) {
if (pCreateInfo == nullptr || instance == nullptr) {
log("Null pointer passed to required parameter!");
return VK_ERROR_INITIALIZATION_FAILED;
}
return real_vkCreateInstance(pCreateInfo, pAllocator, instance);
}
You can combine validation layers with each other to use all the debugging features you need. Also, validation layers can be enabled for debug builds and completely disabled for release builds, which is very convenient.
Vulkan doesn't have built-in validation layers, but LunarG's Vulkan SDK provides a good set of layers to track the most common bugs. All layers are open source , and you can always see what bugs they are tracking. Thanks to validation layers, you can avoid errors on different drivers associated with undefined behavior.
To use validation layers, they must be installed on the system. For example, LunarG's validation layers are only available if the Vulkan SDK is installed.
Previously, Vulkan had two types of validation layers: instance-specific and device-specific. The bottom line is that instance layers check for calls related to global Vulkan objects, while device layers only check for calls related to a specific GPU. At this point, the device layers are deprecated, so the instance validation layers are applied to all Vulkan calls. The specification continues to recommend the inclusion of device-level validation layers, including for interoperability, which is required for some implementations. We will specify the same layers for the instance and the logical device, which we will learn about a little later.
Using validation layers
In this section, we will look at how to connect the layers provided by the Vulkan SDK. As well as for extensions, we must specify the names of the layers to connect them. All the checks useful to us are collected in a layer named "
VK_LAYER_KHRONOS_validation
".
Let's add two configuration constants. The first one (validationLayers) will list which validation layers we want to include. The second one (enableValidationLayers) will allow connection depending on the build mode. This macro
NDEBUG
is part of the C ++ standard and stands for “not debug”.
const uint32_t WIDTH = 800;
const uint32_t HEIGHT = 600;
const std::vector<const char*> validationLayers = {
"VK_LAYER_KHRONOS_validation"
};
#ifdef NDEBUG
const bool enableValidationLayers = false;
#else
const bool enableValidationLayers = true;
#endif
Let's add a new function
checkValidationLayerSupport
that will check if all required layers are available. First, let's get a list of available layers using
vkEnumerateInstanceLayerProperties
. Its use is similar to the function
vkEnumerateInstanceExtensionProperties
we looked at earlier.
bool checkValidationLayerSupport() {
uint32_t layerCount;
vkEnumerateInstanceLayerProperties(&layerCount, nullptr);
std::vector<VkLayerProperties> availableLayers(layerCount);
vkEnumerateInstanceLayerProperties(&layerCount, availableLayers.data());
return false;
After that, check if all layers from
validationLayers
are present in
availableLayers
. You may need to connect
<cstring>
for
strcmp
.
for (const char* layerName : validationLayers) {
bool layerFound = false;
for (const auto& layerProperties : availableLayers) {
if (strcmp(layerName, layerProperties.layerName) == 0) {
layerFound = true;
break;
}
}
if (!layerFound) {
return false;
}
}
return true;
The function can now be used in
createInstance
:
void createInstance() {
if (enableValidationLayers && !checkValidationLayerSupport()) {
throw std::runtime_error("validation layers requested, but not available!");
}
...
}
Run the program in debug mode and make sure there are no errors.
In the structure,
VkInstanceCreateInfo
specify the names of the connected validation layers:
if (enableValidationLayers) {
createInfo.enabledLayerCount = static_cast<uint32_t>(validationLayers.size());
createInfo.ppEnabledLayerNames = validationLayers.data();
} else {
createInfo.enabledLayerCount = 0;
}
If our check was passed,
vkCreateInstance
it should not return an error
VK_ERROR_LAYER_NOT_PRESENT
, but it is better to verify this by running the program.
Interception of debug messages
By default, validation layers send debug messages to standard output, but you can handle them yourself by providing a callback function. This will allow you to filter the messages that you would like to receive, as not all of them contain error warnings. If you want to skip this step, skip straight to the last section of the chapter.
To connect a callback function to process messages, you need to configure a debug messenger using the
VK_EXT_debug_utils
.
First, let's add a function
getRequiredExtensions
that will return the required list of extensions depending on whether validation layers are connected or not.
std::vector<const char*> getRequiredExtensions() {
uint32_t glfwExtensionCount = 0;
const char** glfwExtensions;
glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount);
std::vector<const char*> extensions(glfwExtensions, glfwExtensions + glfwExtensionCount);
if (enableValidationLayers) {
extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
}
return extensions;
}
GLFW extensions are required, and the debug messenger extension is added based on conditions. Please note that we are using a macro
VK_EXT_DEBUG_UTILS_EXTENSION_NAME
to avoid typos.
We can now use this function in
createInstance
:
auto extensions = getRequiredExtensions();
createInfo.enabledExtensionCount = static_cast<uint32_t>(extensions.size());
createInfo.ppEnabledExtensionNames = extensions.data();
Run the program to check if we received an error
VK_ERROR_EXTENSION_NOT_PRESENT
.
Now let's see what the callback function itself is. Let's add a new static method with a prototype
PFN_vkDebugUtilsMessengerCallbackEXT
.
VKAPI_ATTR
and
VKAPI_CALL
make sure that the method has the correct signature.
static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback(
VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity,
VkDebugUtilsMessageTypeFlagsEXT messageType,
const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData,
void* pUserData) {
std::cerr << "validation layer: " << pCallbackData->pMessage << std::endl;
return VK_FALSE;
}
The first parameter determines the severity of the messages, which are:
VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT
: diagnostic messageVK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT
: informational message, for example, about creating a resourceVK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT
: a message about behavior that is not necessarily incorrect, but most likely indicates an errorVK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT
: message about incorrect behavior that could lead to a crash
The values for the enumeration are chosen in such a way that you can use the comparison operation to filter out messages above or below some threshold, for example:
if (messageSeverity >= VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) {
// Message is important enough to show
}
The parameter
messageType
can have the following values:
VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT
: the event that occurred is not related to spec or performanceVK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT
: the event occurred violates the specification or indicates a possible errorVK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT
: Vulkan may not be used optimally
The parameter
pCallbackData
refers to a structure
VkDebugUtilsMessengerCallbackDataEXT
that contains the details of the message. The most important members of the structure are:
pMessage
: debug message as a null-terminated stringpObjects
: an array of descriptors of objects related to the messageobjectCount
: number of objects in the array
The parameter
pUserData
contains the pointer passed during the setup of the callback function.
The callback function returns a
VkBool32
type. The result indicates whether to terminate the call that generated the message. If the callback function returns
VK_TRUE
, the call is aborted and an error code is returned
VK_ERROR_VALIDATION_FAILED_EXT
. As a rule, this happens only when testing the validation layers themselves, in our case, you need to return
VK_FALSE
.
It remains to tell Vulkan about the callback function. Surprisingly, even controlling a debug callback function in Vulkan requires a descriptor that must be explicitly created and destroyed. This callback function is part of the debug messenger, and their number is unlimited. Add a class member for the descriptor after
instance
:
VkDebugUtilsMessengerEXT debugMessenger;
Now add a function
setupDebugMessenger
that will be called from
initVulkan
right after
createInstance
:
void initVulkan() {
createInstance();
setupDebugMessenger();
}
void setupDebugMessenger() {
if (!enableValidationLayers) return;
}
We need to fill the structure with details about the messenger and its callback functions:
VkDebugUtilsMessengerCreateInfoEXT createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;
createInfo.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT;
createInfo.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT;
createInfo.pfnUserCallback = debugCallback;
createInfo.pUserData = nullptr; // Optional
The field
messageSeverity
allows you to specify the severity for which the callback function will be called. We set all degrees, except
VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT
to be notified of possible problems and not to clutter the console with detailed debugging information.
Similarly, the field
messageType
allows you to filter messages by type. We have selected all types, but you can always disable unnecessary ones.
A
pfnUserCallback
pointer to the callback function is passed to the field . Optionally, you can pass a pointer to the field
pUserData
, it will be passed to the callback function through a parameter
pUserData
.
Note that there are other ways to customize validation layer messages and debug callbacks, but this is the best way to get started with Vulkan. For more information on other methods, refer to the extension specification .
The structure must be passed to the function
vkCreateDebugutilsMessengerEXT
to create the object
VkDebugUtilsMessengerEXT
. This is an extension feature, so it is not automatically loaded. You need to find its address yourself using
vkGetInstanceProcAddr
. We will create our own proxy function that will do this internally. Add it before the class definition
HelloTriangleApplication
.
VkResult CreateDebugUtilsMessengerEXT(VkInstance instance, const VkDebugUtilsMessengerCreateInfoEXT* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkDebugUtilsMessengerEXT* pDebugMessenger) {
auto func = (PFN_vkCreateDebugUtilsMessengerEXT) vkGetInstanceProcAddr(instance, "vkCreateDebugUtilsMessengerEXT");
if (func != nullptr) {
return func(instance, pCreateInfo, pAllocator, pDebugMessenger);
} else {
return VK_ERROR_EXTENSION_NOT_PRESENT;
}
}
We use this function to create a messenger:
if (CreateDebugUtilsMessengerEXT(instance, &createInfo, nullptr, &debugMessenger) != VK_SUCCESS) {
throw std::runtime_error("failed to set up debug messenger!");
}
The penultimate parameter is optional, this is the callback function of the allocator, which we will specify as
nullptr
. The rest of the parameters are pretty simple. Since the messenger is used for a specific Vulkan instance (and its validation layers), a pointer to this instance must be passed as the first argument. We will come across this pattern for other child objects.
The object
VkDebugUtilsMessengerEXT
must be destroyed by calling
vkDestroyDebugUtilsMessengerEXT
. As well as for
vkCreateDebugUtilsMessengerEXT
, we have to load this function explicitly.
Then
CreateDebugUtilsMessengerEXT
create another proxy function:
void DestroyDebugUtilsMessengerEXT(VkInstance instance, VkDebugUtilsMessengerEXT debugMessenger, const VkAllocationCallbacks* pAllocator) {
auto func = (PFN_vkDestroyDebugUtilsMessengerEXT) vkGetInstanceProcAddr(instance, "vkDestroyDebugUtilsMessengerEXT");
if (func != nullptr) {
func(instance, debugMessenger, pAllocator);
}
}
Check that this function is either a static function of the class or a function outside the class. After that, it can be called in a function
cleanup
:
void cleanup() {
if (enableValidationLayers) {
DestroyDebugUtilsMessengerEXT(instance, debugMessenger, nullptr);
}
vkDestroyInstance(instance, nullptr);
glfwDestroyWindow(window);
glfwTerminate();
}
Debugging instance of Vulkan
We've added debugging with validation layers, but there is still a little more. A
vkCreateDebugUtilsMessengerEXT
valid instance is
vkDestroyDebugUtilsMessengerEXT
required to call , and must be called before the instance is destroyed. Therefore, we are not yet able to debug in
vkCreateInstance
and
vkDestroyInstance
.
However, if you read the specification carefully , you will see that it is possible to create a separate debug messenger for these two functions. To do this, you need to set the
pNext
structure pointer
VkInstanceCreateInfo
to the structure
VkDebugUtilsMessengerCreateInfoEXT
. First, let's move the filling out
VkDebugUtilsMessengerCreateInfoEXT
into a separate method:
void populateDebugMessengerCreateInfo(VkDebugUtilsMessengerCreateInfoEXT& createInfo) {
createInfo = {};
createInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;
createInfo.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT;
createInfo.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT;
createInfo.pfnUserCallback = debugCallback;
}
...
void setupDebugMessenger() {
if (!enableValidationLayers) return;
VkDebugUtilsMessengerCreateInfoEXT createInfo;
populateDebugMessengerCreateInfo(createInfo);
if (CreateDebugUtilsMessengerEXT(instance, &createInfo, nullptr, &debugMessenger) != VK_SUCCESS) {
throw std::runtime_error("failed to set up debug messenger!");
}
}
We can reuse it in a function
createInstance
:
void createInstance() {
...
VkInstanceCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
createInfo.pApplicationInfo = &appInfo;
...
VkDebugUtilsMessengerCreateInfoEXT debugCreateInfo;
if (enableValidationLayers) {
createInfo.enabledLayerCount = static_cast<uint32_t>(validationLayers.size());
createInfo.ppEnabledLayerNames = validationLayers.data();
populateDebugMessengerCreateInfo(debugCreateInfo);
createInfo.pNext = (VkDebugUtilsMessengerCreateInfoEXT*) &debugCreateInfo;
} else {
createInfo.enabledLayerCount = 0;
createInfo.pNext = nullptr;
}
if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) {
throw std::runtime_error("failed to create instance!");
}
}
The variable
debugCreateInfo
is outside the if statement so that it is not destroyed before being called
vkCreateInstance
. Creating an additional debug messenger in this way allows you to automatically use it in
vkCreateInstance
and
vkDestroyInstance
, after which it will be destroyed.
Testing
Let's deliberately make a mistake to see the validation layers in action.
Temporarily remove the call
DestroyDebugUtilsMessengerEXT
in the function
cleanup
and run the program. You should end up with something like this:
To find out which call resulted in the message being sent, add a breakpoint to the message's callback function and look at the call stack.
Settings
There are many more settings that define the behavior of the validation levels, beyond those specified in the structure
VkDebugUtilsMessengerCreateInfoEXT
. Go to Vulkan SDK and open the directory
Config
. There you will find a file
vk_layer_settings.txt
that explains how to set up layers.
To set up the layers, copy the file to the directory
Debug
and
Release
and follow the instructions to configure the desired behavior. However, throughout the rest of this manual, it will be assumed that you are using the default settings.
In the future, we will deliberately make mistakes to show you how convenient and effective it is to use validation layers to track them.
C ++ code