Validation layers
Code: main.rs
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 are generally not explicitly handled 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 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 (in C):
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 simply 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.
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.
Before we get started, we'll need some new imports for this chapter:
use std::collections::HashSet;
use std::ffi::CStr;
use std::os::raw::c_void;
use vulkanalia::vk::ExtDebugUtilsExtension;
HashSet
will be used for storing and querying supported layers and the other imports will be used in the function we will be writing to log messages from the validation layer with the exception of vk::ExtDebugUtilsExtension
which provides the command wrappers for managing debugging functionality.
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 of 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. I've chosen to base that value on whether the program is being compiled in debug mode or not.
const VALIDATION_ENABLED: bool =
cfg!(debug_assertions);
const VALIDATION_LAYER: vk::ExtensionName =
vk::ExtensionName::from_bytes(b"VK_LAYER_KHRONOS_validation");
We'll add some new code to our create_instance
function that collects the supported instance layers into a HashSet
, checks that the validation layer is available, and creates a list of layer names containing the validation layer. This code should go right below where the vk::ApplicationInfo
struct is built:
let available_layers = entry
.enumerate_instance_layer_properties()?
.iter()
.map(|l| l.layer_name)
.collect::<HashSet<_>>();
if VALIDATION_ENABLED && !available_layers.contains(&VALIDATION_LAYER) {
return Err(anyhow!("Validation layer requested but not supported."));
}
let layers = if VALIDATION_ENABLED {
vec![VALIDATION_LAYER.as_ptr()]
} else {
Vec::new()
};
Then you'll need to specify the requested layers in vk::InstanceCreateInfo
by adding a call to the enabled_layer_names
builder method:
let info = vk::InstanceCreateInfo::builder()
.application_info(&application_info)
.enabled_layer_names(&layers)
.enabled_extension_names(&extensions)
.flags(flags);
Now run the program in debug mode and ensure that the Validation layer requested but not supported.
error does not occur. If it does, then have a look at the FAQ. If you get past that check, then create_instance
should never return a vk::ErrorCode::LAYER_NOT_PRESENT
error code but you should still run the program to be 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 add some more code to our create_instance
function. This time we'll modify the extensions
list to be mutable and then add the debug utilities extension to the list when the validation layer is enabled:
let mut extensions = vk_window::get_required_instance_extensions(window)
.iter()
.map(|e| e.as_ptr())
.collect::<Vec<_>>();
if VALIDATION_ENABLED {
extensions.push(vk::EXT_DEBUG_UTILS_EXTENSION.name.as_ptr());
}
vulkanalia
provides a collection of metadata for each Vulkan extension. In this case we just need the name of the extension to load, so we add the value of the name
field of the vk::EXT_DEBUG_UTILS_EXTENSION
struct constant to our list of desired extension names.
Run the program to make sure you don't receive a vk::ErrorCode::EXTENSION_NOT_PRESENT
error code. 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 extern "system"
function called debug_callback
that matches the vk::PFN_vkDebugUtilsMessengerCallbackEXT
prototype. The extern "system"
is necessary to allow Vulkan to call our Rust function.
extern "system" fn debug_callback(
severity: vk::DebugUtilsMessageSeverityFlagsEXT,
type_: vk::DebugUtilsMessageTypeFlagsEXT,
data: *const vk::DebugUtilsMessengerCallbackDataEXT,
_: *mut c_void,
) -> vk::Bool32 {
let data = unsafe { *data };
let message = unsafe { CStr::from_ptr(data.message) }.to_string_lossy();
if severity >= vk::DebugUtilsMessageSeverityFlagsEXT::ERROR {
error!("({:?}) {}", type_, message);
} else if severity >= vk::DebugUtilsMessageSeverityFlagsEXT::WARNING {
warn!("({:?}) {}", type_, message);
} else if severity >= vk::DebugUtilsMessageSeverityFlagsEXT::INFO {
debug!("({:?}) {}", type_, message);
} else {
trace!("({:?}) {}", type_, message);
}
vk::FALSE
}
The first parameter specifies the severity of the message, which is one of the following flags:
vk::DebugUtilsMessageSeverityFlagsEXT::VERBOSE
– Diagnostic messagevk::DebugUtilsMessageSeverityFlagsEXT::INFO
– Informational message like the creation of a resourcevk::DebugUtilsMessageSeverityFlagsEXT::WARNING
– Message about behavior that is not necessarily an error, but very likely a bug in your applicationvk::DebugUtilsMessageSeverityFlagsEXT::ERROR
– 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 which we use here to decide on which log
macro is appropriate to use when logging the message.
The type_
parameter can have the following values:
vk::DebugUtilsMessageTypeFlagsEXT::GENERAL
– Some event has happened that is unrelated to the specification or performancevk::DebugUtilsMessageTypeFlagsEXT::VALIDATION
– Something has happened that violates the specification or indicates a possible mistakevk::DebugUtilsMessageTypeFlagsEXT::PERFORMANCE
– Potential non-optimal use of Vulkan
The data
parameter refers to a vk::DebugUtilsMessengerCallbackDataEXT
struct containing the details of the message itself, with the most important members being:
message
– The debug message as a null-terminated string (*const c_char
)objects
– Array of Vulkan object handles related to the messageobject_count
– Number of objects in array
Finally, the last parameter, here ignored as _
, contains a pointer that was specified during the setup of the callback and allows you to pass your own data to it.
The callback returns a (Vulkan) 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::ErrorCode::VALIDATION_FAILED_EXT
error code. 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. Perhaps somewhat surprisingly, even the debug callback in Vulkan is managed with a handle that needs to be explicitly created and destroyed. Such a callback is part of a debug messenger and you can have as many of them as you want. Add a field to the AppData
struct:
struct AppData {
messenger: vk::DebugUtilsMessengerEXT,
}
Now modify the signature and end of the create_instance
function to look like this:
unsafe fn create_instance(
window: &Window,
entry: &Entry,
data: &mut AppData
) -> Result<Instance> {
// ...
let instance = entry.create_instance(&info, None)?;
if VALIDATION_ENABLED {
let debug_info = vk::DebugUtilsMessengerCreateInfoEXT::builder()
.message_severity(vk::DebugUtilsMessageSeverityFlagsEXT::all())
.message_type(
vk::DebugUtilsMessageTypeFlagsEXT::GENERAL
| vk::DebugUtilsMessageTypeFlagsEXT::VALIDATION
| vk::DebugUtilsMessageTypeFlagsEXT::PERFORMANCE,
)
.user_callback(Some(debug_callback));
data.messenger = instance.create_debug_utils_messenger_ext(&debug_info, None)?;
}
Ok(instance)
}
Note: Calling the
all
static method on a set of Vulkan flags (e.g.,vk::DebugUtilsMessageSeverityFlagsEXT::all()
as in the above code) will, as the name implies, return a set of flags containing all of the flags of that type known byvulkanalia
. A complete set of flags may contain flags that are only valid when certain extensions are enabled or flags added by a newer version of Vulkan than the one you are using/targeting.In the above code we've explicitly listed the
vk::DebugUtilsMessageTypeFlagsEXT
flags we want because that set of flags contains a flag (vk::DebugUtilsMessageTypeFlagsEXT::DEVICE_ADDRESS_BINDING
) that is only valid when a certain extension is enabled.In most cases using unsupported flags shouldn't cause any errors or changes in the behavior of your application, but it definitely will result in validation errors if you have the validation layers enabled (as we are aiming to do in this chapter).
We have first extracted our Vulkan instance out of the return expression so we can use it to add our debug callback.
Then we construct a vk::DebugUtilsMessengerCreateInfoEXT
struct which provides information about our debug callback and how it will be called.
The message_severity
field allows you to specify all the types of severities you would like your callback to be called for. I've requested that messages of all severity be included. This would normally produce a lot of verbose general debug info but we can filter that out using a log level when we are not interested in it.
Similarly the message_type
field lets you filter which types of messages your callback is notified about. I've simply enabled all types here. You can always disable some if they're not useful to you.
Finally, the user_callback
field specifies the callback function. You can optionally pass a mutable reference to the user_data
field which will be passed along to the callback function via the final parameter. You could use this to pass a pointer to the AppData
struct, for example.
Lastly we call create_debug_utils_messenger_ext
to register our debug callback with the Vulkan instance.
Since our create_instance
function takes an AppData
reference now, we'll also need to update App
and App::create
:
Note:
AppData::default()
will use the implementation of theDefault
trait generated by the presence of#[derive(Default)]
on theAppData
struct. This will result in containers likeVec
being initialized to empty lists and Vulkan handles likevk::DebugUtilsMessengerEXT
being initialized to null handles. If Vulkan handles are not initialized properly before they are used, the validation layers we are enabling in this chapter should let us know exactly what we missed.
struct App {
entry: Entry,
instance: Instance,
data: AppData,
}
impl App {
unsafe fn create(window: &Window) -> Result<Self> {
// ...
let mut data = AppData::default();
let instance = create_instance(window, &entry, &mut data)?;
Ok(Self { entry, instance, data })
}
}
The vk::DebugUtilsMessengerEXT
object we created needs to cleaned up before our app exits. We'll do this in App::destroy
before we destroy the instance:
unsafe fn destroy(&mut self) {
if VALIDATION_ENABLED {
self.instance.destroy_debug_utils_messenger_ext(self.data.messenger, None);
}
self.instance.destroy_instance(None);
}
Debugging instance creation and destruction
Although we've now added debugging with validation layers to the program we're not covering everything quite yet. The create_debug_utils_messenger_ext
call requires a valid instance to have been created and destroy_debug_utils_messenger_ext
must be called before the instance is destroyed. This currently leaves us unable to debug any issues in the create_instance
and destroy_instance
calls.
However, if you closely read the extension documentation, you'll see that there is a way to create a separate debug utils messenger specifically for those two function calls. It requires you to simply pass a pointer to a vk::DebugUtilsMessengerCreateInfoEXT
struct in the next
extension field of vk::InstanceCreateInfo
. Before we do this, let's first discuss how extending structs works in Vulkan.
The s_type
field that is present on many Vulkan structs was briefly mentioned in the Builders section of the Overview chapter. It was said that this field must be set to the vk::StructureType
variant indicating the type of the struct (e.g., vk::StructureType::APPLICATION_INFO
for a vk::ApplicationInfo
struct).
You may have wondered what the purpose of this field is: doesn't Vulkan already know the type of structs passed to its commands? The purpose of this field is wrapped up with the purpose of the next
field that always accompanies the s_type
field in Vulkan structs: the ability to extend a Vulkan struct with other Vulkan structs.
The next
field in a Vulkan struct may be used to specify a structure pointer chain. next
can be either be null or a pointer to a Vulkan struct that is permitted by Vulkan to extend the struct. Each struct in this chain of structs is used to provide additional information to the Vulkan command the root structure is passed to. This feature of Vulkan allows for extending the functionality of Vulkan commands without breaking backwards compabilitity.
When you pass such a chain of structs to a Vulkan command, it must iterate through the structs to collect all of the information from the structs. Because of this, Vulkan can't know the type of each structure in the chain, hence the need for the s_type
field.
The builders provided by vulkanalia
allow for easily building these pointer chains in a type-safe manner. For example, take a look at the vk::InstanceCreateInfoBuilder
builder, specifically the push_next
method. This method allows adding any Vulkan struct for which the vk::ExtendsInstanceCreateInfo
trait is implemented for to the pointer chain for a vk::InstanceCreateInfo
.
One such struct is vk::DebugUtilsMessengerCreateInfoEXT
, which we will now use to extend our vk::InstanceCreateInfo
struct to set up our debug callback. To do this we'll continue to modify our create_instance
function. This time we'll make the info
struct mutable so we can modify its pointer chain before moving the debug_info
struct, now also mutable, below it so we can push it onto info
's pointer chain:
let mut info = vk::InstanceCreateInfo::builder()
.application_info(&application_info)
.enabled_layer_names(&layers)
.enabled_extension_names(&extensions)
.flags(flags);
let mut debug_info = vk::DebugUtilsMessengerCreateInfoEXT::builder()
.message_severity(vk::DebugUtilsMessageSeverityFlagsEXT::all())
.message_type(
vk::DebugUtilsMessageTypeFlagsEXT::GENERAL
| vk::DebugUtilsMessageTypeFlagsEXT::VALIDATION
| vk::DebugUtilsMessageTypeFlagsEXT::PERFORMANCE,
)
.user_callback(Some(debug_callback));
if VALIDATION_ENABLED {
info = info.push_next(&mut debug_info);
}
Note: It might seem redundant to use the same debug info with the same severity, type, and callback to both call
create_debug_utils_messenger_ext
and add as an extension to thevk::InstanceCreateInfo
instance. However, these two usages serve different purposes. The usage here (adding the debug info tovk::InstanceCreateInfo
) sets up debugging during the creation and destruction of the instance. Callingcreate_debug_utils_messenger_ext
sets up persistent debugging for everything else. See the paragraph starting "To capture events that occur while creating or destroying an instance" in the relevant chapter of the Vulkan specification.
debug_info
needs to be defined outside of the conditional since it needs to live until we are done calling create_instance
. Fortunately we can rely on the Rust compiler to protect us from pushing a struct that doesn't live long enough onto a pointer chain due to the lifetimes defined for the vulkanalia
builders.
Now we should be able to run our program and see logs from our debug callback, but first we'll need to set the RUST_LOG
environment variable so that pretty_env_logger
will enable the log levels we are interested in. Initially set the log level to debug
so we can be sure it is working, here is an example on Windows (PowerShell):
If everything is working you shouldn't see any warning or error messages. Going forward you will probably want to increase the minimum log level to info
using RUST_LOG
to reduce the verbosity of the logs unless you are trying to debug an error.
Configuration
There are a lot more settings for the behavior of validation layers than just the flags specified in the vk::DebugUtilsMessengerCreateInfoEXT
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 working directory of your project's executable and follow the instructions to set the desired behavior. However, for the remainder of this tutorial I'll assume that you're using the default settings.
Throughout this tutorial I'll 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.