Vulkan Layers

Unlike typical graphics APIs, Vulkan groups possible error scenarios into two distinct categories:

Validity Errors
These are error conditions resulting from incorrect API usage. Essentially, the application acts against the API usage rules necessary to obtain well-defined behaviour from issued commands. These rules can be found in the specification for all API commands.
Run-time Errors
These are error conditions that occur during the execution of applications that use the API correctly. One example would be running out of memory. These errors are reported as result codes. The API sepcification lists the possible result codes each command may return individually and feature descriptions of the situations which may result in each particular result code.

Whilst the majority of the Vulkan API commands return a result code, these codes are only used to indicate run-time errors and status information about certain operations or objects. The result codes do not report information about respecting valid usage conditions. This allows release builds of an application to run at peak performance. This is also due to driver implementations not spending CPU cycles checking for potential violations of the specification rules.

Driver implementations do not check valid usage, as they expect all calls to be valid as standard; due to this, running applications that use the API incorrectly may result in unexpected behaviour. This could lead to corrupted rendering or even crashing the application. Most commonly, the consequences of invalid parameters being passed to an API command may only manifest when executing later commands.

Validation and Debug Layers

For developers to actually detect validity errors ahead of time, Vulkan comes with a set of validation and debug layers as part of the Vulkan SDK. There are over a dozen layers dedicated to validating certain aspects of API usage and for providing debugging tools for developers, such as an API call logger. When these layers are enabled, they insert themselves into every call chain of each Vulkan API call issued by the application.

The advantage of having validation layers as opposed to the approach taken by more traditional APIs is that applications only spend time on extensive error checking when explicitly ordered to during development and when using debug builds of the application. As well as this, because the validation layers that come with the SDK work across driver implementations, this approach doesn't suffer from the fragmentation issues associated with the error-checking behaviour of traditional APIs. As a result, developers can be confident that the same validation errors will be reported in all cases, no matter which driver implementation is being used.

Validation layers don't simply look for violations of the permitted API usage; they can also report warnings about potentailly incorrect or dangerous uses of the API and can report performance warnings that let developers identify where the API is being used inefficiently. For example, a potential performance warning could be related to binding resources that aren't actually being used or using a sub-optimal layout for an image.

Preparing an Instance for Validation

The code example below shows how applications should enable the VK_LAYER-LUNARG_standard_validation layer and the VK_EXT_debug_report extension at instance creation time in their debug builds in C++:

std::vector enabledInstanceLayers;
std::vector enabledInstanceExtensions;

#ifdef MY_DEBUG_BUILD_MACRO
/* Enable validation layers in debug builds to detect validation errors */
enabledInstanceLayers.push_back("VK_LAYER_LUNARG_standard_validation");
#endif

/* Enable instance extensions used in all build types */
enabledInstanceExtensions.push_back("VK_KHR_surface");

...

#ifdef MY_DEBUG_BUILD_MACRO
/* Enable debug report extension in debug builds to be able to consume 
validation errors */
enabledInstanceExtensions.push_back("VK_EXT_debug_report");
#endif

/* Setup instance creation information */
VkInstanceCreateInfo instanceCreateInfo = {};

...

instanceCreateInfo.enabledLayerCount = 
static_cast<uint32_t>(enabledInstanceLayers.size());
    
instanceCreateInfo.ppEnabledLayerNames = &enabledInstanceLayers[0];
    
instanceCreateInfo.enabledExtensionCount =  
    static_cast<uint32_t>(enabledInstanceExtensions.size());
        
instanceCreateInfo.ppEnabledExtensionNames = &enabledInstanceExtensions[0];
        
/* Create the instance */
VkInstance instance = VK_NULL_HANDLE;
VkResult result = vkCreateInstance(&instanceCreateInfo, nullptr, &instance);

A resilient application first checks for the presence of used instance layers and extensions before passing them to vkCreateInstance by using the ckEnumerateInstanceLayerProperties and ckEnumerateInstanceExtensionProperties commands, respectively. After a successful instance creation, the validation layers and debug report extension are active for the instance.

As the VK_EXT_debug_report extension is not a core feature, its entry points have to be obtained by using the vkGetInstanceProcAddr as shown below:

#ifdef MY_DEBUG_BUILD_MACRO

/* Load VK_EXT_debug_report entry points in debug builds */
PFN_vkCreateDebugReportCallbackEXT vkCreateDebugReportCallbackEXT =
reinterpret_cast<PFN_vkCreateDebugReportCallbackEXT>
(vkGetInstanceProcAddr(instance, "vkCreateDebugReportCallbackEXT"));

PFN_vkDebugReportMessageEXT vkDebugReportMessageEXT =
reinterpret_cast<PFN_vkDebugReportMessageEXT>
(vkGetInstanceProcAddr(instance, "vkDebugReportMessageEXT"));

PFN_vkDestroyDebugReportCallbackEXT vkDestroyDebugReportCallbackEXT =
reinterpret_cast<PFN_vkDestroyDebugReportCallbackEXT>
(vkGetInstanceProcAddr(instance, "vkDestroyDebugReportCallbackEXT"));

#endif

Debug Report Callback

An application can register any number of debug report callbacks. These only need to match the signature defined by PFN_vkDebugReportCallbackEXT. Below is an example of a debug report callback which directs all incoming debug messages to stderr:

VKAPI_ATTR VkBool32 VKAPI_CALL MyDebugReportCallback(
    VkDebugReportFlagsEXT       flags,
    VkDebugReportObjectTypeEXT  objectType,
    uint64_t                    object,
    size_t                      location,
    int32_t                     messageCode,
    const char*                 pLayerPrefix,
    const char*                 pMessage,
    void*                       pUserData)
{
    std::cerr << pMessage << std::endl;
    return VK_FALSE;
}

The parameters passed to the callback provide information on where and what type of validation event has triggered the call. The information includes not only the type of event, but also the type and handle of the object being created or manipulated by the command that triggered the call. The code and text message describing the event and the parameter to supply application-specific user data to the callback are also provided. By having a breakpoint in the callback, developers can access the complete callstack to accurately determine the location of the erroneous API call.

The return of the callback is a boolean value that tells the validation layers whether the API call that triggered the debug report callback should be aborted or not. Developers should be aware that when an error is reported by one of the validation layers, it is an indication that something invalid was attempted by the application, and so any operation following the error may lead to undefined behaviour or a crash. It is therefore recommended that developers stop at the first error and attempt to resolve that before making any assumptions about the behaviour of following operations.

When registering a debug report callback, the type of event to be notified about can be specified. The normal types that are of interest are errors, warnings, and performance warnings. The following code snippet registers a callback with such a configuration.

#ifdef MY_DEBUG_BUILD_MACRO
/* Setup callback creation information */
VkDebugReportCallbackCreateInfoEXT callbackCreateInfo;

callbackCreateInfo.sType       = VK_STRUCTURE_TYPE_DEBUG_REPORT_CREATE_INFO_EXT;
callbackCreateInfo.pNext       = nullptr;
callbackCreateInfo.flags       = VK_DEBUG_REPORT_ERROR_BIT_EXT |
VK_DEBUG_REPORT_WARNING_BIT_EXT |
VK_DEBUG_REPORT_PERFORMANCE_WARNING_BIT_EXT;

callbackCreateInfo.pfnCallback = &MyDebugReportCallback;
callbackCreateInfo.pUserData   = nullptr;

/* Register the callback */
VkDebugReportCallbackEXT callback;
VkResult result = 
    vkCreateDebugReportCallbackEXT(instance, &callbackCreateInfo, nullptr, &callback);
#endif

A registered callback can then be unregistered by destroying the callback object like any other API object using the corresponding command vkDestroyDebugReportCallbackEXT. Developers should make sure to unregister their debug report callbacks before destroying the instance, otherwise they will continue to receive notifications about erroneous behaviour during any debug report callback that is still registered.

The last remaining entry point of the debug report extension is vkDebugReportMessageEXT. It can be used to generate debug report messages from application code, which can be useful to mark points of the execution of the application or to report application-specific information to the same stream as the validation messages.

Device-Level Operations

The architecture of Vulkan distinguishes between instance-level and device-level functionality and has separate layers and extensions for each. When the validation layers are enabled at instance creation time, only validation of instance-level operations are requested. To establish the validation of device-level operations, developers have to enable the VK_LAYER_LUNARG_standard_validation layer as in the following example:

std::vector enabledDeviceLayers;
std::vector enabledDeviceExtensions;

#ifdef MY_DEBUG_BUILD_MACRO
    /* Enable validation layers in debug builds to detect validation errors */
    enabledDeviceLayers.push_back("VK_LAYER_LUNARG_standard_validation");
#endif

/* Enable device extensions used in all build types */
enabledDeviceExtensions.push_back("VK_KHR_swapchain");

...

/* Setup device creation information */
VkDeviceCreateInfo deviceCreateInfo = {};

...

deviceCreateInfo.enabledLayerCount = static_cast<uint32_t>(enabledDeviceLayers.size());
deviceCreateInfo.ppEnabledLayerNames = &enabledDeviceLayers[0];
deviceCreateInfo.enabledExtensionCount = 
    static_cast<uint32_t>(enabledDeviceExtensions.size());
deviceCreateInfo.ppEnabledExtensionNames = &enabledDeviceExtensions[0];

...

/* Create the device */
VkDevice device = VK_NULL_HANDLE;
VkResult result = vkCreateDevice(physicalDevice, &deviceCreateInfo, nullptr,&device);

Just like for instance-level layers and extensions, the application should first check the presence of the requested layers and extensions using the vkEnumerateDeviceLayerProperties and vkEnumerateDeviceExtensionsProperties commands. Keep in mind that unlike the validation layers, which have separate instance-level and device-level versions and therefore have to be enabled at both levels, the VK_EXT_debug_report extension only needs to be enabled at instance-level due to being an instance-level only extension.

Forcing Validation Externally

The approach presented so far is the recommended method of validating an application because it allows developers to enable validation based on the type of build, on some application setting, or any other mechanism that can be specified. In addition to this, the debug report callback enables fine-grained control regarding which validation events should be captured and how they should be reported.

In some cases, however, it is possible that modifying or rebuilding the application to enable validation programmatically is inconvenient or simply not a viable option. This includes cases such as validating release builds of applications that don't reproduce the issue in debug builds or validating third-party applications or libraries that cannot be rebuilt due to lack of access to the source code.

The solution for such an instance involves the instance and device layers being enabled through the variables VK_INSTANCE_LAYERS and VK_DEVICE_LAYERS respectively. These variables accept a list of layer names to enable, separated by semicolons on Windows and colons on Linux. The following commands enable all standard validation layers when working on Windows:

  • set VK_INSTANCE_LAYERS=VK_LAYER_LUNARG_standard_validation
  • set VK_DEVICE_LAYERS+VK_LAYER_LUNARG_standard_validation

When using this approach to enable validation, the reporting mechanism must be configured for each layer through a settings file, otherwise the activated layers will produce no output. This setting file must be named vk_layer_settings.txt and has to be located in the working directory of the application. A sample layer settings file is provided as part of the Vulkan SDK under the config folder, which will simply output all error, warning, and performance warning messages to stdout. These can be changed to output a different subset of the validation messages and can be redirected to files instead of console output, which may be necessary to capture the output of applications without a console. Instructions on how to change the various configuration options are included in the sample settings file.