steps3D - Tutorials - Механизм запросов (queries) в Vulkan

Механизм запросов (queries) в Vulkan

Как и в OpenGL в Vulkan есть поддержка запросов (queries) к GPU. Vulkan поддерживает на данный момент три типа таких запросов - видимости (occlusion query), статистики конвейера (pipeline statistics) и запись метки времени (timestamp query).

Каждому такому запросу соответствует некоторый объект в Vulkan, но он скрыт от разработчика. Эти объекты выделяются из специальных пулов и когда мы ссылаемся на такой объект, мы просто указываем пул и индекс в пуле.

Пулу запросов соответствует тип VkQueryPool Для создания и уничтожения таких пулов служат команды vkCreateQueryPool и vkDestroyQueryPool.

VkResult vkCreateQueryPool(
    VkDevice                                    device,
    const VkQueryPoolCreateInfo*                pCreateInfo,
    const VkAllocationCallbacks*                pAllocator,
    VkQueryPool*                                pQueryPool);
    
void vkDestroyQueryPool(
    VkDevice                                    device,
    VkQueryPool                                 queryPool,
    const VkAllocationCallbacks*                pAllocator);

При создании такого пула нужно передать информацию через поля структуры VkQueryPoolCreateInfo.

typedef struct VkQueryPoolCreateInfo {
    VkStructureType                  sType;
    const void*                      pNext;
    VkQueryPoolCreateFlags           flags;
    VkQueryType                      queryType;
    uint32_t                         queryCount;
    VkQueryPipelineStatisticFlags    pipelineStatistics;
} VkQueryPoolCreateInfo;

Здесь поле flags задает флаги, сейчас флагов нет, это поле зарезервировано для будущего применения и должно быть равно нулю. Поле queryType задает тип запросов, которые будут выделяться этим пулом. Каждый пул может выделять запросы только одного типа. Допустимыми значениями для этого поля являются VK_QUERY_TYPE_OCCLUSION, VK_QUERY_TYPE_PIPELINE_STATISTICS и VK_QUERY_TYPE_TIMESTAMP.

Параметр queryCount задает размер пула, т.е. максимальное число запросов, которые можно из него выделить (фактически все они выделяются сразу при создании). Параметр pipelineStatistics нужен только для запросов статистики конвейера. Это просто набор битов, каждый бит соответствует одному из шагов конвейера, статистику по которому нужно собирать. Допустимыми битами являются - VK_QUERY_PIPELINE_STATISTIC_INPUT_ASSEMBLY_VERTICES_BIT, VK_QUERY_PIPELINE_STATISTIC_INPUT_ASSEMBLY_PRIMITIVES_BIT, VK_QUERY_PIPELINE_STATISTIC_VERTEX_SHADER_INVOCATIONS_BIT, VK_QUERY_PIPELINE_STATISTIC_GEOMETRY_SHADER_INVOCATIONS_BIT, VK_QUERY_PIPELINE_STATISTIC_GEOMETRY_SHADER_PRIMITIVES_BIT, VK_QUERY_PIPELINE_STATISTIC_CLIPPING_INVOCATIONS_BIT, VK_QUERY_PIPELINE_STATISTIC_CLIPPING_PRIMITIVES_BIT, VK_QUERY_PIPELINE_STATISTIC_FRAGMENT_SHADER_INVOCATIONS_BIT, VK_QUERY_PIPELINE_STATISTIC_TESSELLATION_CONTROL_SHADER_PATCHES_BIT, VK_QUERY_PIPELINE_STATISTIC_TESSELLATION_EVALUATION_SHADER_INVOCATIONS_BIT и VK_QUERY_PIPELINE_STATISTIC_COMPUTE_SHADER_INVOCATIONS_BIT.

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

У каждого запроса есть свой статус и состояние. Статус запроса принимает всего два значения - доступно (available) или недоступно (unavailable). Состояние - это значение (результат) соответствующего запроса. Запрос (или сразу группу запросов) можно сбросить (reset), тогда его статус становится "недоступен", а состояние становится неопределенным.

Статус и состояние можно получить при помощи команд vkGetQueryPoolResults и vkCmdCopyQueryPoolResults. При создании пула все запросы находятся в неинициализированном состоянии и должны быть сброшены перед использованием. Также после завершения запроса, для того, чтобы его снова можно было послать его нужно сбросить. Для сброса запроса служит команда vkCmdResetQueryPool.

void vkCmdResetQueryPool(
    VkCommandBuffer                             commandBuffer,
    VkQueryPool                                 queryPool,
    uint32_t                                    firstQuery,
    uint32_t                                    queryCount);

Здесь commandBuffer это командный буфер, в который будет записана команда сброса запросов, queryPool это пул, содержащий сбрасываемые запросы, а firstQuery и queryCount задают группу подряд идущих запросов (с firstQuery до firstQuery + queryCount - 1), которые следует сбросить. Данная команда переводить статус всех заданных команд в состояние "недоступно".

Также группу запросов можно сбросить и на стороне CPU при помощи команды vkResetQueryPool.

void vkResetQueryPool(
    VkDevice                                    device,
    VkQueryPool                                 queryPool,
    uint32_t                                    firstQuery,
    uint32_t                                    queryCount);

Запросы типа видимости и статистики конвейера считает число событий (прошедших тесты сэмплов и вызовов определенных участков конвейера) между двумя заданными точками. Эти точки задаются при помощи пары функций - vkCmdBeginQuery и vkCmdEndQuery.

void vkCmdBeginQuery(
    VkCommandBuffer                             commandBuffer,
    VkQueryPool                                 queryPool,
    uint32_t                                    query,
    VkQueryControlFlags                         flags);
    
void vkCmdEndQuery(
    VkCommandBuffer                             commandBuffer,
    VkQueryPool                                 queryPool,
    uint32_t                                    query);

Параметры commandBuffer и queryPool задают командный буфер, в который будут записаны запросы и пул, из которого запросы будут браться. Параметр query задает номер (индекс) запроса в пуле. Параметр flags имеет смысл только для запросов видимости и может принимать единственное ненулевое значение - VK_QUERY_CONTROL_PRECISE_BIT.

Для получения результата запросов служат две команды - vkGetQueryPoolResults и vkCmdCopyQueryPoolResults.

VkResult vkGetQueryPoolResults(
    VkDevice                                    device,
    VkQueryPool                                 queryPool,
    uint32_t                                    firstQuery,
    uint32_t                                    queryCount,
    size_t                                      dataSize,
    void*                                       pData,
    VkDeviceSize                                stride,
    VkQueryResultFlags                          flags);
    
void vkCmdCopyQueryPoolResults(
    VkCommandBuffer                             commandBuffer,
    VkQueryPool                                 queryPool,
    uint32_t                                    firstQuery,
    uint32_t                                    queryCount,
    VkBuffer                                    dstBuffer,
    VkDeviceSize                                dstOffset,
    VkDeviceSize                                stride,
    VkQueryResultFlags                          flags);

Первая из них записывает результаты одно или нескольких запросов в оперативную память, а вторая - в заданный буфер в памяти GPU. Сами результаты записываются в виде 32- или 64-битовых беззнаковых целых чисел с шагом в stride байт. Можно задать, что кроме собственно самих результатов нужно записать признак доступности результата (ненулевое значение говорит о том, что результат уже доступен).

Запросы видимости записывают всего одно значение - число сэмплов, прошедших тесты. Запросы статистики конвейера записывают по одному значению на каждый каждый единичный бит в соответствующей маске.

Параметр flags является битовой маской и может включать следующие биты - VK_QUERY_RESULT_64_BIT, VK_QUERY_RESULT_WAIT_BIT, VK_QUERY_RESULT_WITH_AVAILABILITY_BIT и VK_QUERY_RESULT_PARTIAL_BIT.

Бит VK_QUERY_RESULT_64_BIT задает, что результаты запроса нужно записывать в виде 64-битовых целых чисел, иначе они будут записаны как 32-битовые. Бит VK_QUERY_RESULT_WAIT_BIT означает, что перед записью необходимо дождаться готовности результата. Бит VK_QUERY_RESULT_WITH_AVAILABILITY_BIT означает, что нужно помимо самих значений записать также признак их доступности. При помощи бита VK_QUERY_RESULT_PARTIAL_BIT можно задать что допустимо вернуть лишь частичный результат.

Запросы видимости

Запросы видимости (occlusion queries) отслеживают число сэмплов, прошедших пофрагментные тесты для заданных команд рендеринга геометрии. Отслеживаемые команды рендеринга должны находится между vkGetQueryPoolResults и vkCmdCopyQueryPoolResults соответственно.

Обратите внимание, что если флаг VK_QUERY_CONTROL_PRECISE_BIT не задан, то разрешается вернуть ненулевое значение на запрос видимости, если хотя бы один сэмпл прошел тесты. На некоторых реализациях Vulkan запрос без этого флага может быть эффективнее, чем с ним. Даже если флаг VK_QUERY_CONTROL_PRECISE_BIT установлен, то не гарантируется что другие реализации вернут то же самое число сэмплов.

Запрос статистики конвейера

Запросы этого типа позволяют получить значения внутренних счетчиков GPU, отслеживающих наступление заданных событий. Если данный вид запросов поддерживается конкретным GPU, то поле pipelineStatisticsQuery в структуре VkPhysicalDeviceFeatures будет не равно нулю.

Статистика возвращается по всем командам, заключенным между вызовами vkGetQueryPoolResults и vkCmdCopyQueryPoolResults. Обратите внимание, что если получение этой статистики не поддерживается или запрос относится к отсутствующей для данных команд стадии, то соответствующее возвращенное значение будет неопределенным.

Рассмотрим что значат отдельные биты для маски собираемой статистики.

Имейте в виду, что различные реализации Vulkan могут по-разному считать статистику. При получении результат для запроса данного типа будут записаны только те значения счетчиков, для которых был задан соответствующий бит в маске при создании пула.

Timestamp

Этот вид запросов позволяет отслеживать сколько времени заняло на GPU выполнение той или иной операции. С этими запросами не используются функции vkGetQueryPoolResults и vkCmdCopyQueryPoolResults. Вместо этого при помощи функций vkCmdWriteTimestamp и vkCmdWriteTimestamp2 в командный буфер помещается запрос на сохранение значения внутреннего таймера GPU. Полученное значение таймера может быть потом получено стандартным образом при помощи функций vkGetQueryPoolResults и vkCmdCopyQueryPoolResults соответственно.

Поле timestampValidBits структуры VkQueueFamilyProperties задает для данной очереди сколько битов из значения таймера являются значащими. Нулевое значение данного поля говорит о том, что для данной очереди запись метод времени не поддерживается. В поле timestampPeriod структуры VkPhysicalDeviceLimits содержится число наносекунд, через которое значение счетчика изменяется на единицу, т.е. его разрешение в наносекундах.

Helper classes

Для упрощения работы можно пулы запросов завернуть в классы на С++. Ниже приводятся реализации двух таких классов - для получения значений таймера GPU и для получения статистики конвейера GPU.

#pragma once
#include    "CommandBuffer.h"
#include    "Device.h"


class   TimestampPool
{
    Device        * device    = nullptr;
    VkQueryPool     queryPool = VK_NULL_HANDLE;
    uint32_t        count     = 0;

    TimestampPool () = default;
    ~TimestampPool ()
    {
        destroy ();
    }

        // create pool with cnt slots
    bool    create ( Device& dev, uint32_t cnt = 128 )
    {
        VkQueryPoolCreateInfo createInfo = { VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO };

        device                = &dev;
        count                 = cnt;
        createInfo.queryType  = VK_QUERY_TYPE_TIMESTAMP;
        createInfo.queryCount = count;

        return vkCreateQueryPool ( device->getDevice (), &createInfo, nullptr, &queryPool ) == VK_SUCCESS;
    }

    void    destroy ()
    {
        if ( queryPool && device )
            vkDestroyQueryPool ( device->getDevice (), queryPool, nullptr );
    }

        // reset all timestamps in the pool
    void    reset ( CommandBuffer& commandBuffer, uint32_t first = 0, uint32_t num = 0 )
    {
        if ( num < 1 )
            num = count;

        vkCmdResetQueryPool ( commandBuffer.getHandle (), queryPool, first, num );
    }

        // write timestamp with given no
    void    writeTimestamp ( CommandBuffer& commandBuffer, int no, VkPipelineStageFlagBits stageMask = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT )
    {
        vkCmdWriteTimestamp ( commandBuffer.getHandle (), stageMask, queryPool, no );
    }

        // get selected timestamps from GPU
    bool    getResults ( std::vector<uint64_t>& results, uint32_t first = 0, uint32_t num = 0 )
    {
        if ( num == 0 )
            num = count;

        if ( num > results.size () )
            num = uint32_t ( results.size () );

        return vkGetQueryPoolResults ( device->getDevice (), queryPool, first, num, uint32_t (results.size ()) * sizeof(uint64_t), results.data (), sizeof(uint64_t), VK_QUERY_RESULT_64_BIT ) == VK_SUCCESS;
    }

    uint64_t    getResult ( uint32_t index )
    {
        uint64_t    value;

        vkGetQueryPoolResults ( device->getDevice (), queryPool, index, 1, sizeof(uint64_t), &value, sizeof(uint64_t), VK_QUERY_RESULT_64_BIT );

        return value;
    }

        // convert  timestamp value to milliseconds
    double  convertToMs ( uint64_t value )
    {
        return double(value) * device->getProperties ().properties.limits.timestampPeriod * 1e-6;
    }

        // convert  timestamp value to seconds
    double  convertToSec ( uint64_t value )
    {
        return double(value) * device->getProperties ().properties.limits.timestampPeriod * 1e-9;
    }
};

#pragma once
#include    "CommandBuffer.h"
#include    "Device.h"

class   StatisticsPool
{
    Device                        * device     = nullptr;
    VkQueryPool                     queryPool  = VK_NULL_HANDLE;
    uint32_t                        count      = 0;
    VkQueryPipelineStatisticFlags   statistics = 0;

    StatisticsPool () = default;
    ~StatisticsPool ()
    {
        destroy ();
    }

        // create pool with cnt slots
    bool    create ( Device& dev, VkQueryPipelineStatisticFlags stats, uint32_t cnt )
    {
        VkQueryPoolCreateInfo createInfo = { VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO };

        device                        = &dev;
        count                         = cnt;
        statistics                    = stats;
        createInfo.queryType          = VK_QUERY_TYPE_PIPELINE_STATISTICS;
        createInfo.queryCount         = count;
        createInfo.pipelineStatistics = stats;

        return vkCreateQueryPool ( device->getDevice (), &createInfo, nullptr, &queryPool ) == VK_SUCCESS;
    }

    void    destroy ()
    {
        if ( queryPool && device )
            vkDestroyQueryPool ( device->getDevice (), queryPool, nullptr );
    }

        // reset all timestamps in the pool
    void    reset ( CommandBuffer& commandBuffer, uint32_t first = 0, uint32_t num = 0 )
    {
        if ( num < 1 )
            num = count;

        vkCmdResetQueryPool ( commandBuffer.getHandle (), queryPool, first, num );
    }

    void    begin ( CommandBuffer& commandBuffer, uint32_t index )
    {
        vkCmdBeginQuery ( commandBuffer.getHandle (), queryPool, index, 0 );
    }

    void    end ( CommandBuffer& commandBuffer, uint32_t index )
    {
        vkCmdEndQuery ( commandBuffer.getHandle (), queryPool, index );
    }

        // get selected timestamps from GPU
    std::vector<uint64_t>   getResults ( uint32_t first = 0, uint32_t num = 0 )
    {
        if ( num == 0 )
            num = count;

        std::vector<uint64_t>   results ( num );

        if (  vkGetQueryPoolResults ( device->getDevice (), queryPool, first, num, uint32_t (results.size ()) * sizeof(uint64_t), results.data (), sizeof(uint64_t), VK_QUERY_RESULT_64_BIT ) == VK_SUCCESS )
            return results;

        return {};
    }

    int getNumResults () const
    {
        int sum = 0;

        for ( int i = 0; i < sizeof ( statistics ) * 8; i++ )
            if ( statistics & (1 << i) )
                sum++;

        return sum;
    }

    int indexForFlag ( VkQueryPipelineStatisticFlags bit )
    {
        int pos = 0;

        for ( int i = 0; (1u << i) < bit; i++ )
            if ( statistics & (1 << i) )
                pos++;

        return pos;
    }
};

Исходный код этих классов можно найти в репозитории https://github.com/steps3d/vulkan-with-classes.git.