Validation layers
What are validation layers?
The Vulkan API is designed around the idea of minimal driver overhead, and one of the manifestations of that goal is that there is very limited error checking in the API by default. Even mistakes as simple as setting enumerations to incorrect values or passing null pointers to required parameters are generally not explicitly handled beyond c++ type checking, and will simply result in crashes or undefined behavior. Because Vulkan requires you to be very explicit about everything you’re doing, it’s easy to make many small mistakes like using a new GPU feature and forgetting to request it at logical device creation time.
However, that doesn’t mean that these checks can’t be added to the API. Vulkan introduces an elegant system for this known as validation layers. Validation layers are optional components that hook into Vulkan function calls to apply additional operations. Common operations in validation layers are:
-
Checking the values of parameters against the specification to detect misuse
-
Tracking the creation and destruction of objects to find resource leaks
-
Checking thread safety by tracking the threads that calls originate from
-
Logging every call and its parameters to the standard output
-
Tracing Vulkan calls for profiling and replaying
Here’s an example of what the implementation of a function in a diagnostics validation layer could look like:
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);
}
These validation layers can be freely stacked to include all the debugging functionality that you’re interested in. You can enable validation layers for debug builds and completely disable them for release builds, which gives you the best of both worlds!
Vulkan does not come with any validation layers built-in, but the LunarG Vulkan SDK provides a nice set of layers that check for common errors. They’re also completely open source, so you can check which kind of mistakes they check for and contribute. Using the validation layers is the best way to avoid your application breaking on different drivers by accidentally relying on undefined behavior.
Validation layers can only be used if they have been installed onto the system. For example, the LunarG validation layers are only available on PCs with the Vulkan SDK installed.
Other layers exist, and we recommend making use of them. We’ll leave it as an exercise to the reader to discover and try out the other layers. However, we recommend looking at BestPractices for more help and suggestions about how to use Vulkan in an updated way.
There were formerly two different types of validation layers in Vulkan: instance and device specific. The idea was that instance layers would only check calls related to global Vulkan objects like instances, and device-specific layers would only check calls related to a specific GPU. Device-specific layers have now been deprecated, which means that instance validation layers apply to all Vulkan calls. The specification document still recommends that you enable validation layers at device level as well for compatibility, which is required by some implementations. We’ll simply specify the same layers as the instance at logical device level, which we’ll see later on.
Using validation layers
In this section, we’ll see how to enable the standard diagnostics layers provided
by the Vulkan SDK. Just like extensions, validation layers need to be enabled by
specifying their name. All the useful standard validation is bundled into a
layer included in the SDK that is known as VK_LAYER_KHRONOS_validation
.
Let’s first add two configuration variables to the program to specify the layers
to enable and whether to enable them or not. We’ve chosen to base that value on
whether the program is being compiled in debug mode or not. The NDEBUG
macro
is part of the c++ standard and means "not debug".
constexpr uint32_t WIDTH = 800;
constexpr uint32_t HEIGHT = 600;
const std::vector validationLayers = {
"VK_LAYER_KHRONOS_validation"
};
#ifdef NDEBUG
constexpr bool enableValidationLayers = false;
#else
constexpr bool enableValidationLayers = true;
#endif
We’ll check if all the requested layers are available. We need to iterate
through the requested layers and validate that all the required layers are
supported by the Vulkan implementation. This check is performed directly in the
createInstance
function:
void createInstance() {
constexpr vk::ApplicationInfo appInfo{ .pApplicationName = "Hello Triangle",
.applicationVersion = VK_MAKE_VERSION( 1, 0, 0 ),
.pEngineName = "No Engine",
.engineVersion = VK_MAKE_VERSION( 1, 0, 0 ),
.apiVersion = vk::ApiVersion14 };
// Get the required layers
std::vector<char const*> requiredLayers;
if (enableValidationLayers) {
requiredLayers.assign(validationLayers.begin(), validationLayers.end());
}
// Check if the required layers are supported by the Vulkan implementation.
auto layerProperties = context.enumerateInstanceLayerProperties();
if (std::ranges::any_of(requiredLayers, [&layerProperties](auto const& requiredLayer) {
return std::ranges::none_of(layerProperties,
[requiredLayer](auto const& layerProperty)
{ return strcmp(layerProperty.layerName, requiredLayer) == 0; });
}))
{
throw std::runtime_error("One or more required layers are not supported!");
}
...
}
Now run the program in debug mode and ensure that the error does not occur. If it does, then have a look at the FAQ.
Finally, modify the VkInstanceCreateInfo
struct instantiation to include the
validation layer names if they are enabled:
vk::InstanceCreateInfo createInfo{
.pApplicationInfo = &appInfo,
.enabledLayerCount = static_cast<uint32_t>(requiredLayers.size()),
.ppEnabledLayerNames = requiredLayers.data(),
.enabledExtensionCount = 0,
.ppEnabledExtensionNames = nullptr };
If the check was successful then vkCreateInstance
should not ever return a
VK_ERROR_LAYER_NOT_PRESENT
error, but you should run the program to make sure.
Message callback
The validation layers will print debug messages to the standard output by default, but we can also handle them ourselves by providing an explicit callback in our program. This will also allow you to decide which kind of messages you would like to see, because not all are necessarily (fatal) errors. If you don’t want to do that right now, then you may skip to the last section in this chapter.
To set up a callback in the program to handle messages and the associated
details, we have to set up a debug messenger with a callback using the
VK_EXT_debug_utils
extension.
We’ll first create a getRequiredExtensions
function that will return the
required list of extensions based on whether validation layers are enabled or
not:
std::vector<const char*> getRequiredExtensions() {
uint32_t glfwExtensionCount = 0;
auto glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount);
std::vector extensions(glfwExtensions, glfwExtensions + glfwExtensionCount);
if (enableValidationLayers) {
extensions.push_back(vk::EXTDebugUtilsExtensionName );
}
return extensions;
}
The extensions specified by GLFW are always required, as we’re working with
the GLFW dependency for windowing, but the debug messenger extension is
conditionally added. Note that We’ve used the
VK_EXT_DEBUG_UTILS_EXTENSION_NAME
macro here which is equal to the literal
string "VK_EXT_debug_utils". Using this macro lets you avoid typos.
We can now use this function in createInstance
:
auto extensions = getRequiredExtensions();
vk::InstanceCreateInfo createInfo({}, &appInfo, enabledLayers, extensions);
Run the program to make sure you don’t receive a
VK_ERROR_EXTENSION_NOT_PRESENT
error. We don’t really need to check for the
existence of this extension because it should be implied by the availability of
the validation layers.
Now let’s see what a debug callback function looks like. Add a new static member
function called debugCallback
with the PFN_vkDebugUtilsMessengerCallbackEXT
prototype. The VKAPI_ATTR
and VKAPI_CALL
ensure that the function has the
right signature for Vulkan to call it.
static VKAPI_ATTR vk::Bool32 VKAPI_CALL debugCallback(vk::DebugUtilsMessageSeverityFlagBitsEXT severity, vk::DebugUtilsMessageTypeFlagsEXT type, const vk::DebugUtilsMessengerCallbackDataEXT* pCallbackData, void*) {
std::cerr << "validation layer: type " << to_string(type) << " msg: " << pCallbackData->pMessage << std::endl;
return vk::False;
}
The first parameter specifies the severity of the message, which is one of the following flags:
-
VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT
: Diagnostic message -
VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT
: Informational message like the creation of a resource -
VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT
: Message about behavior that is not necessarily an error, but very likely a bug in your application -
VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT
: Message about behavior that is invalid and may cause crashes
The values of this enumeration are set up in such a way that you can use a comparison operation to check if a message is equal or worse compared to some level of severity, for example:
if (messageSeverity >= vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning) {
// Message is important enough to show
}
The messageType
parameter can have the following values:
-
VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT
: Some event has happened that is unrelated to the specification or performance -
VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT
: Something has happened that violates the specification or indicates a possible mistake -
VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT
: Potential non-optimal use of Vulkan
The pCallbackData
parameter refers to a VkDebugUtilsMessengerCallbackDataEXT
struct containing the details of the message itself, with the most important members being:
-
pMessage
: The debug message as a null-terminated string -
pObjects
: Array of Vulkan object handles related to the message -
objectCount
: Number of objects in the array
Finally, the pUserData
parameter contains a pointer specified during the
setup of the callback and allows you to pass your own data to it.
The callback returns a boolean that indicates if the Vulkan call that triggered
the validation layer message should be aborted. If the callback returns true,
then the call is aborted with the VK_ERROR_VALIDATION_FAILED_EXT
error. This
is normally only used to test the validation layers themselves, so you should
always return VK_FALSE
.
All that remains now is telling Vulkan about the callback function. Such a
callback is part of a debug messenger, and you can have as many of them as
you want. Add a class member for this handle right under instance
:
vk::raii::DebugUtilsMessengerEXT debugMessenger = nullptr;
Now add a function setupDebugMessenger
to be called from initVulkan
right
after createInstance
:
void initVulkan() {
createInstance();
setupDebugMessenger();
}
void setupDebugMessenger() {
if (!enableValidationLayers) return;
}
We’ll need to fill in a structure with details about the messenger and its callback:
vk::DebugUtilsMessageSeverityFlagsEXT severityFlags( vk::DebugUtilsMessageSeverityFlagBitsEXT::eVerbose | vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning | vk::DebugUtilsMessageSeverityFlagBitsEXT::eError );
vk::DebugUtilsMessageTypeFlagsEXT messageTypeFlags( vk::DebugUtilsMessageTypeFlagBitsEXT::eGeneral | vk::DebugUtilsMessageTypeFlagBitsEXT::ePerformance | vk::DebugUtilsMessageTypeFlagBitsEXT::eValidation );
vk::DebugUtilsMessengerCreateInfoEXT debugUtilsMessengerCreateInfoEXT{
.messageSeverity = severityFlags,
.messageType = messageTypeFlags,
.pfnUserCallback = &debugCallback
};
debugMessenger = instance.createDebugUtilsMessengerEXT(debugUtilsMessengerCreateInfoEXT);
The messageSeverity
field allows you to specify all the types of
severities you would like your callback to be called for. We’ve specified
all types except for VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT
here to
receive notifications about possible problems while leaving out verbose
general debug info.
Similarly, the messageType
field lets you filter which types of messages
your callback is notified about. We’ve simply enabled all types here. You
can always disable some if they’re not useful to you.
Finally, the pfnUserCallback
field specifies the pointer to the callback
function. You can optionally pass a pointer to the pUserData
field which
will be passed along to the callback function via the pUserData
parameter.
You could use this to pass a pointer to the HelloTriangleApplication
class, for example.
Note that there are many more ways to configure validation layer messages and debug callbacks, but this is a good setup to get started with for this tutorial. See the extension specification for more info about the possibilities.
We can now re-use this in the createInstance
function:
void createInstance() {
constexpr vk::ApplicationInfo appInfo{ .pApplicationName = "Hello Triangle",
.applicationVersion = VK_MAKE_VERSION( 1, 0, 0 ),
.pEngineName = "No Engine",
.engineVersion = VK_MAKE_VERSION( 1, 0, 0 ),
.apiVersion = vk::ApiVersion14 };
// Get the required layers
std::vector<char const*> requiredLayers;
if (enableValidationLayers) {
requiredLayers.assign(validationLayers.begin(), validationLayers.end());
}
// Check if the required layers are supported by the Vulkan implementation.
auto layerProperties = context.enumerateInstanceLayerProperties();
if (std::ranges::any_of(requiredLayers, [&layerProperties](auto const& requiredLayer) {
return std::ranges::none_of(layerProperties,
[requiredLayer](auto const& layerProperty)
{ return strcmp(layerProperty.layerName, requiredLayer) == 0; });
}))
{
throw std::runtime_error("One or more required layers are not supported!");
}
// Get the required extensions.
auto requiredExtensions = getRequiredExtensions();
// Check if the required extensions are supported by the Vulkan implementation.
auto extensionProperties = context.enumerateInstanceExtensionProperties();
for (auto const & requiredExtension : requiredExtensions)
{
if (std::ranges::none_of(extensionProperties,
[requiredExtension](auto const& extensionProperty)
{ return strcmp(extensionProperty.extensionName, requiredExtension) == 0; }))
{
throw std::runtime_error("Required extension not supported: " + std::string(requiredExtension));
}
}
vk::InstanceCreateInfo createInfo{
.pApplicationInfo = &appInfo,
.enabledLayerCount = static_cast<uint32_t>(requiredLayers.size()),
.ppEnabledLayerNames = requiredLayers.data(),
.enabledExtensionCount = static_cast<uint32_t>(requiredExtensions.size()),
.ppEnabledExtensionNames = requiredExtensions.data() };
instance = vk::raii::Instance(context, createInfo);
}
Configuration
There are a lot more settings for the behavior of validation layers than just
the flags specified in the VkDebugUtilsMessengerCreateInfoEXT
struct. Browse
to the Vulkan SDK and go to the Config
directory. There you will find a
vk_layer_settings.txt
file that explains how to configure the layers.
To configure the layer settings for your own application, copy the file to the
Debug
and Release
directories of your project and follow the instructions to
set the desired behavior. However, for the remainder of this tutorial, We will
assume that you’re using the default settings.
Throughout this tutorial, we will be making a couple of intentional mistakes to show you how helpful the validation layers are with catching them and to teach you how important it is to know exactly what you’re doing with Vulkan. Now it’s time to look at Vulkan devices in the system.