steps3D - Tutorials - Расширение VK_EXT_debug_utils

Расширение VK_EXT_debug_utils

Это одно из довольно старых расширений (апрель 2020 года) и одну из предоставляемых им возможностей (задание своего обработчика ошибок) я уже использую в своем коде. Но на самом деле там есть много других полезных возможностей и он довольно хорошо умеет взаимодействовать с тулами вроде RenderDoc. Собственно его основная задача это именно облегчение отладки приложения на Vulkan.

Данное расширение предоставляет следующий функционал:

Обратите внимание, что это расширение уровня экземпляра (instance extension), поэтому для всех вводимых им функций необходимо явно получить их адреса (или воспользоваться библиотекой volk). Ниже приводятся фрагменты класса VulkanWindow, делающие именно это.

PFN_vkDebugUtilsMessengerCallbackEXT    VulkanWindow::vkDebugUtilsMessengerCallbackEXT = {};
PFN_vkCreateDebugUtilsMessengerEXT      VulkanWindow::vkCreateDebugUtilsMessengerEXT   = {};
PFN_vkDestroyDebugUtilsMessengerEXT     VulkanWindow::vkDestroyDebugUtilsMessengerEXT  = {};

if ( !vkCreateDebugUtilsMessengerEXT )
{
    vkDebugUtilsMessengerCallbackEXT = (PFN_vkDebugUtilsMessengerCallbackEXT) vkGetInstanceProcAddr ( instance, "vkDebugUtilsMessengerCallbackEXT" );
    vkCreateDebugUtilsMessengerEXT   = (PFN_vkCreateDebugUtilsMessengerEXT)   vkGetInstanceProcAddr ( instance, "vkCreateDebugUtilsMessengerEXT");
    vkDestroyDebugUtilsMessengerEXT  = (PFN_vkDestroyDebugUtilsMessengerEXT)  vkGetInstanceProcAddr ( instance, "vkDestroyDebugUtilsMessengerEXT" );
}

Debug Messenger Callback

Одной из самых удобных функций данного расширение является возможность задания своей функции-обработчика различных ошибок (в первую очередь от слоев валидации). Можно задать для каких случаев она будет вызываться. При вызове эта функция получает большой объем крайне полезной информации. Ниже приводится прототип функции-обработчика.

typedef VkBool32 (VKAPI_PTR *PFN_vkDebugUtilsMessengerCallbackEXT)(
    VkDebugUtilsMessageSeverityFlagBitsEXT       messageSeverity,
    VkDebugUtilsMessageTypeFlagsEXT              messageType,
    const VkDebugUtilsMessengerCallbackDataEXT * pCallbackData,
    void *                                       pUserData
);

Первый ее параметр (messageSeverity) является числом, задающим серьезность ситуации. Возможные значения для данного поля перечислены ниже, их смысл сразу понятен. Скорее всего, имеет смысл игнорировать все кроме ошибок и (может быть) предупреждений.

typedef enum VkDebugUtilsMessageSeverityFlagBitsEXT
{
    VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT = 0x00000001,
    VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT    = 0x00000010,
    VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT = 0x00000100,
    VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT   = 0x00001000,
} VkDebugUtilsMessageSeverityFlagBitsEXT;

Далее идет поле (messageType), задающее тип (класс) возникшей ошибки. Допустимые ошибки приведены ниже.

typedef enum VkDebugUtilsMessageTypeFlagBitsEXT
{
    VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT     = 0x00000001,
    VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT  = 0x00000002,
    VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT = 0x00000004,
} VkDebugUtilsMessageTypeFlagBitsEXT;

Последний параметр (pUserData) - это передаваемый программистом при создании функции-обработчика указатель, чаще всего это просто указатель на некоторый объект (у меня на VulkanWindow), метод которого и выполняет обработку.

Более интересным параметром является третий (pCallbackData). Это указатель на структуру VkDebugUtilsMessengerCallbackDataEXT, которая содержит как само сообщение (pMessage), так и кучу всякой полезной информации, включая имена объектов (pObjects, смотря далее) и метки (pQueueLabels, смотря далее).

typedef struct VkDebugUtilsMessengerCallbackDataEXT
{
    VkStructureType                           sType;
    const void *                              pNext;
    VkDebugUtilsMessengerCallbackDataFlagsEXT flags;
    const char *                              pMessageIdName;
    int32_t                                   messageIdNumber;
    const char *                              pMessage;
    uint8_t                                   queueLabelCount;
    VkDebugUtilsLabelEXT *                    pQueueLabels;
    uint8_t                                   cmdBufLabelCount;
    VkDebugUtilsLabelEXT *                    pCmdBufLabels;
    uint8_t                                   objectCount;
    VkDebugUtilsObjectNameInfoEXT *           pObjects;
} VkDebugUtilsMessengerCallbackDataEXT;

Сам обработчик сообщений идентифицируется при помощи значения типа VkDebugUtilsMessengerEXT, данный обработчик нужно создать вначале и уничтожить в конце работы. Для создания обработчика служит функция vkCreateDebugUtilsMessengerEXT.

VkResult vkCreateDebugUtilsMessengerEXT (
    VkInstance instance,
    const VkDebugUtilsMessengerCreateInfoEXT* pCreateInfo,
    const VkAllocationCallbacks* pAllocator,
    VkDebugUtilsMessengerEXT* pMessenger
);

Вся необходимая для создания обработчика информация передается через структуру VkDebugUtilsMessengerCreateInfoEXT. Поля pNext и flags обычно просто равны нулю. Через поле messageSeverity можно задать какие уровни серьезности (severity) должны обрабатываться - это просто битовая маска из значений VkDebugUtilsMessageSeverityFlagBitsEXT.

Аналогично поле messageType задает какие типы сообщений необходимо обрабатывать. В поле pfnUserCallback передается указатель на саму функцию-обработчик, а в поле pUserData - передаваемый в обработчик указатель.

typedef struct VkDebugUtilsMessengerCreateInfoEXT
{
    VkStructureType                      sType;
    const void *                         pNext;
    VkDebugUtilsMessengerCreateFlagsEXT  flags;
    VkDebugUtilsMessageSeverityFlagsEXT  messageSeverity;
    VkDebugUtilsMessageTypeFlagsEXT      messageType;
    PFN_vkDebugUtilsMessengerCallbackEXT pfnUserCallback;
    void *                               pUserData;
} VkDebugUtilsMessengerCreateInfoEXT;

Ниже приводится метод из класса VulkanWindow для заполнения данной структуры.

void    VulkanWindow::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;
}

Для уничтожения обработчика служит функция vkDestroyDebugUtilsMessengerEXT.

void vkDestroyDebugUtilsMessengerEXT (
    VkInstance instance,
    VkDebugUtilsMessengerEXT messenger,
    const VkAllocationCallbacks* pAllocator
);

Ниже приводится расширенный обработчик ошибок, выдающий отладочную информацию, которую мы будем задавать далее.

VKAPI_ATTR VkBool32 VKAPI_CALL VulkanWindow::debugCallback ( VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, 
        VkDebugUtilsMessageTypeFlagsEXT messageType, 
        const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData, void* pUserData )
{
    if ( messageSeverity >= VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT )       // display only warning or higher
    {
        if ( messageSeverity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT )
            log () << "VERBOSE : ";
        else if ( messageSeverity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT )
            log () <<  "INFO : ";
        else if ( messageSeverity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT )
            log () << "WARNING : ";
        else if ( messageSeverity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT )
            log () << "ERROR : ";
    
        log () << "validation layer: " << pCallbackData->pMessage << Log::endl;

        if ( pCallbackData -> objectCount > 0 )
        {
            log () << "Objects - " << std::endl;

            for ( uint32_t object = 0; object < pCallbackData -> objectCount; ++object )
            {
                const char * name = pCallbackData->pObjects[object].pObjectName;

                log () << " Object[" << object << "] - Type " << pCallbackData->pObjects[object].objectType << 
                          " Value " << (void *)pCallbackData->pObjects[object].objectHandle << 
                          " Name \"" << (name ? name : "") << "\'\n" << std::endl;
            }
        }

        if ( pCallbackData->cmdBufLabelCount > 0 )
        {
            log () << "\n Command Buffer Labels -" << std::endl;

            for ( uint32_t label = 0; label < pCallbackData->cmdBufLabelCount; ++label )
            {
                const char * name = pCallbackData->pCmdBufLabels[label].pLabelName;

                log () << " Label[" << label << "d] - " << (name ? name : "") << std::endl;
            }
        }
    }

    return VK_FALSE;
}

Именование объектов Vulkan

Обычно при возникновении какой-то ошибки мы просто получаем хэндлы связанных объектов. Это крайне не информативно и не удобно - тяжело понять с каким реальным объектом связан тот или иной хэндл. Поэтому расширение VK_EXT_debug_utils позволяет связать с любым объектом Vulkan некоторое имя - обычную завершенную нулем строку (на самом деле это строка в кодировке UTF-8).

Для задания имени служит функция vkSetDebugUtilsObjectNameEXT, всю необходимую информацию она получает через поля структуры VkDebugUtilsObjectNameInfoEXT.

VkResult vkSetDebugUtilsObjectNameEXT (
    VkDevice device,
    const VkDebugUtilsObjectNameInfoEXT* pNameInfo );
 
typedef struct VkDebugUtilsObjectNameInfoEXT
{
    VkStructureType sType;
    const void *    pNext;
    VkObjectType    objectType;
    uint64_t        objectHandle;
    const char *    pObjectName;
} VkDebugUtilsObjectNameInfoEXT;

Поле objectType задает тип объекта и является одной из констант VK_OBJECT_TYPE_*. Поле objectHandle - это хэндл объекта, переведенный в тип uint64_t (т.е. из указателя перевели в беззнаковое целое число с тем тем же числом битов).

Ниже приводится пример методов, облегчающих работу с данным функционалом.

void setName ( VkObjectType type, const void * handle, const char * name )
{
    VkDebugUtilsObjectNameInfoEXT   info = {};      // zero it

    info.sType        = VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT;
    info.objectType   = type;
    info.objectHandle = uint64_t ( handle );
    info.pObjectName  = name;

    vkSetDebugUtilsObjectNameEXT ( device.getDevice (), &info );
}

void setName ( Texture& texture, const std::string& name )
{
    setName ( VK_OBJECT_TYPE_IMAGE, texture.getImage ().getHandle (), name.c_str () );
}

template <class T>
void setName ( T& object, const std::string& name )
{
    setName ( VkObjectType(T::type_id), object.getHandle (), name.c_str () );
}

template <class T>
void setName ( const T& object, const std::string& name )
{
    setName ( VkObjectType(T::type_id), object.getHandle (), name.c_str () );
}

Тэггирование объектов Vulkan

Кроме имени с каждый объектом Vulkan можно связать некоторое целое число (тэг) и блок бинарных данных. Для этого служит функция vkSetDebugUtilsObjectTagEXT, получающая информацию через поля структуры VkDebugUtilsObjectTagInfoEXT.

VkResult vkSetDebugUtilsObjectTagEXT (
    VkDevice device,
    const VkDebugUtilsObjectTagInfoEXT* pTagInfo
);
    
    
typedef struct VkDebugUtilsObjectTagInfoEXT
{
    VkStructureType sType;
    const void *    pNext;
    VkObjectType    objectType;
    uint64_t        objectHandle;
    uint64_t        tagName;
    size_t          tagSize;
    const void *    pTag;
} VkDebugUtilsObjectTagInfoEXT;

Ниже приводится вариант "обертки" над этой возможностью.

template <typename VT, typename DT>
void setTag ( VT& object, uint64_t tag, DT& data )
{
    VkDebugUtilsObjectTagInfoEXT    info = {};

    info.sType        = VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_TAG_INFO_EXT;
    info.objectType   = VT::type_id;
    info.objectHandle = uint64_t ( object.getHandle () );
    info.tagName      = tag;
    info.tagSize      = sizeof ( DT );
    info.pTag         = &data;

    vkSetDebugUtilsObjectTagEXT ( device.getHandle (), &info );
}

Добавление меток

Хотя добавление имен и облегчает отладку, но иногда этого бывает недостаточно и хочется понять не только с каким объектом связана ошибка, но и в каком месте кода она происходит. Для этого расширение VK_EXT_debug_utils добавило возможность задания метод (label). При этом метки можно использовать как для обозначения каких-то областей кода (т.е. у метки есть начало и конец), так и просто вставлять их в код, обозначая какое-то действие.

При этом метки можно вставлять как в очередь, так и в командные буфера - у каждой из соответствующих команд есть версия для очереди и версия для командного буфера. Каждая метка имеет текстовое имя и RGBA-цвет - он может использоваться при анализе приложения в RenderDoc или других подобных инструментах.

Информация о метке передается через поля структуры VkDebugUtilsLabelEXT

typedef struct VkDebugUtilsLabelEXT
{
    VkStructureType sType;
    const void *    pNext;
    const char *    pLabelName;
    float           color [4];
} VkDebugUtilsLabelEXT;

Для задания меток, обозначающих области кода, служат функции vkQueueBeginDebugUtilsLabelEXT и vkQueueEndDebugUtilsLabelEXT.

void vkQueueBeginDebugUtilsLabelEXT (
    VkQueue queue,
    const VkDebugUtilsLabelEXT* pLabelInfo
);
 
void vkQueueEndDebugUtilsLabelEXT ( VkQueue queue );
 
void vkCmdBeginDebugUtilsLabelEXT (
    VkCommandBuffer commandBuffer,
    const VkDebugUtilsLabelEXT* pLabelInfo
);
 
void vkCmdEndDebugUtilsLabelEXT ( VkCommandBuffer commandBuffer );

Для вставки обычных меток, не привязанных к какой-либо области кода, служит функция vkQueueInsertDebugUtilsLabelEXT.

void insertLabel ( VkQueue queue, const char * text, const glm::vec4& color = glm::vec4 ( 1.0f ) )
{
    VkDebugUtilsLabelEXT    info = {};

    info.sType      = VK_STRUCTURE_TYPE_DEBUG_UTILS_LABEL_EXT;
    info.pLabelName = text;
    info.color [0]  = color [0];
    info.color [1]  = color [1];
    info.color [2]  = color [2];
    info.color [3]  = color [3];

    vkQueueInsertDebugUtilsLabelEXT ( queue, &info );
}

void insertLabel ( CommandBuffer& where, const char * text, const glm::vec4& color = glm::vec4 ( 1.0f ) )
{
    VkDebugUtilsLabelEXT    info = {};

    info.sType      = VK_STRUCTURE_TYPE_DEBUG_UTILS_LABEL_EXT;
    info.pLabelName = text;
    info.color [0]  = color [0];
    info.color [1]  = color [1];
    info.color [2]  = color [2];
    info.color [3]  = color [3];

    vkCmdInsertDebugUtilsLabelEXT ( where.getHandle (), &info);
}

class   Label
{
    VkQueue         queue = VK_NULL_HANDLE;
    VkCommandBuffer cb    = VK_NULL_HANDLE;
public:
    Label ( VkQueue where, const char * text, const glm::vec4& color = glm::vec4 ( 1.0f ) )
    {
        VkDebugUtilsLabelEXT    info = {};

        queue           = where;
        info.sType      = VK_STRUCTURE_TYPE_DEBUG_UTILS_LABEL_EXT;
        info.pLabelName = text;
        info.color [0]  = color [0];
        info.color [1]  = color [1];
        info.color [2]  = color [2];
        info.color [3]  = color [3];

        vkQueueBeginDebugUtilsLabelEXT ( queue, &info );
    }

    Label ( const CommandBuffer& where, const char * text, const glm::vec4& color = glm::vec4 ( 1.0f ) )
    {
        VkDebugUtilsLabelEXT    info = {};

        cb              = where.getHandle ();
        info.sType      = VK_STRUCTURE_TYPE_DEBUG_UTILS_LABEL_EXT;
        info.pLabelName = text;
        info.color [0]  = color [0];
        info.color [1]  = color [1];
        info.color [2]  = color [2];
        info.color [3]  = color [3];

        vkCmdBeginDebugUtilsLabelEXT ( where.getHandle (), &info );
    }

    ~Label ()
    {
        if ( queue )
            vkQueueEndDebugUtilsLabelEXT ( queue );
        else
            vkCmdEndDebugUtilsLabelEXT ( cb );
    }
};

Примеры использования

// in window c-tor
setName ( texture, "Sample image" );

    // in createPipeline
setName ( pipeline.getVertexShader   (), "my vertex shader"      );
setName ( pipeline.getFragmentShader (), "my fragment shader"    );
setName ( pipeline,                      "my rendering pipeline" );

Ниже приводится скриншоты запуска тестового приложения в RenderDoc, обратите внимание на метку "Rendering" и ее цвет.

Соответствующий код можно скачать в репозитории на github - Vulkan with classes.

Полезные ссылки

Vulkan Debug Utilities

Vulkan Debug Utilities Extension

Vulkan Debug Utilities Extension