Vulkan. Developer's guide. Validation layers

I am a translator from CG Tribe in Izhevsk, and here I am sharing a translation of the Vulkan API manual. Source link - vulkan-tutorial.com .



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
1.



2.



3.



4.





  1. (pipeline)


5.



  1. Staging


6. Uniform-



  1. layout
  2. sets


7.



  1. Image view image sampler
  2. image sampler


8.



9.



10. -



11. Multisampling



FAQ







Validation layers







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 message
  • VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT



    : informational message, for example, about creating a resource
  • VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT



    : a message about behavior that is not necessarily incorrect, but most likely indicates an error
  • VK_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 performance
  • VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT



    : the event occurred violates the specification or indicates a possible error
  • VK_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 string
  • pObjects



    : an array of descriptors of objects related to the message
  • objectCount



    : 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



All Articles