Ch3-8 查询

Vulkan中提供了一种用来取得Vulkan命令执行信息的机制,称之为查询(query)。

Query Pool

查询池(VkQueryPool)是用于管理查询的Vulkan对象。

创建查询池

vkCreateQueryPool(...)创建查询池:

VkResult VKAPI_CALL vkCreateQueryPool(...) 的参数说明

VkDevice device

逻辑设备的handle

const VkQueryPoolCreateInfo* pCreateInfo

指向VkQueryPool的创建信息

const VkAllocationCallbacks* pAllocator

VkQueryPool* pQueryPool

若执行成功,将查询池的handle写入*pQueryPool

struct VkQueryPoolCreateInfo 的成员说明

VkStructureType sType

结构体的类型,本处必须是VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO

const void* pNext

如有必要,指向一个用于扩展该结构体的结构体

VkQueryPoolCreateFlags flags

到Vulkan1.3为止没用

VkQueryType queryType

查询的类型

uint32_t queryCount

查询的个数

VkQueryPipelineStatisticFlags pipelineStatistics

如果查询的类型为管线统计查询,在此指定需要进行哪些统计

版本要求

VkQueryType 的枚举项

1.0

VK_QUERY_TYPE_OCCLUSION 表示遮挡查询

1.0

VK_QUERY_TYPE_PIPELINE_STATISTICS 表示管线统计查询

1.0

VK_QUERY_TYPE_TIMESTAMP 表示时间戳查询

开始和结束查询

在Vulkan核心功能提供的三种查询中,遮挡查询和管线统计查询基于一定范围发生,有开始和结束。

在命令缓冲区中,用vkCmdBeginQuery(...)标记查询范围的起始:

void VKAPI_CALL vkCmdBeginQuery(...) 的参数说明

VkCommandBuffer commandBuffer

命令缓冲区的handle

VkQueryPool queryPool

查询池的handle

uint32_t query

要开始的查询的索引

VkQueryControlFlags flags

自Vulkan1.0到1.3,VK_QUERY_CONTROL_PRECISE_BIT是唯一可用的bit,相关说明见后文的遮挡查询

在命令缓冲区中,用vkCmdEndQuery(...)标记查询范围的结束:

void VKAPI_CALL vkCmdEndQuery(...) 的参数说明

VkCommandBuffer commandBuffer

命令缓冲区的handle

VkQueryPool queryPool

查询池的handle

uint32_t query

要结束的查询的索引

  • 如果查询在渲染通道外开始,相应地得在渲染通道外结束。
    如果查询在渲染通道内开始,相应地得在该渲染通道的同一子通道内结束。

    获取查询结果

    在命令缓冲区中,用vkCmdCopyQueryPoolResults(...)将查询结果拷贝到数据缓冲区:

    void VKAPI_CALL vkCmdCopyQueryPoolResults(...) 的参数说明

    VkCommandBuffer commandBuffer

    命令缓冲区的handle

    VkQueryPool queryPool

    查询池的handle

    uint32_t firstQuery

    首个要被拷贝结果的查询的索引

    uint32_t queryCount

    要被拷贝结果的查询的个数

    VkBuffer dstBuffer

    目标缓冲区

    VkDeviceSize dstOffset

    查询结果要被拷贝到目标缓冲区中的位置,单位是字节,必须是4的倍数(或8的倍数,见后文)

    VkDeviceSize stride

    每组查询结果在目标缓冲区中的间距,单位是字节,必须是4的倍数(或8的倍数,见后文)

    VkQueryResultFlagBits flags

    • 若queryCount为1,stride可以为0。

    版本要求

    VkQueryResultFlagBits 的枚举项

    1.0

    VK_QUERY_RESULT_64_BIT 如不注明该bit,获取到的每个查询结果为4字节无符号整形,注明该bit时为8字节

    1.0

    VK_QUERY_RESULT_WAIT_BIT 表示在获取查询结果前,等待查询的完成

    1.0

    VK_QUERY_RESULT_WITH_AVAILABILITY_BIT 见后文

    1.0

    VK_QUERY_RESULT_PARTIAL_BIT 见后文

    • 注明VK_QUERY_RESULT_64_BIT时,dstOffset和stride也相应地须是8的倍数。

    • VK_QUERY_RESULT_WITH_AVAILABILITY_BIT 表示在每组查询结果尾端添加一个数值,用于表示是否已完成查询,未完成时为0,已完成则非零。
      该数值的字节数同查询结果一样,受VK_QUERY_RESULT_64_BIT影响。

    • 若注明VK_QUERY_RESULT_PARTIAL_BIT,查询尚未完成时获取到的结果,是查询过程中的中间结果。
      若没有注明VK_QUERY_RESULT_PARTIAL_BIT,且查询尚未完成,获取到的查询结果未定义(可能是任意不准确的数值)。

    • VK_QUERY_RESULT_PARTIAL_BIT不适用于时间戳查询(时间戳查询没有操作范围,“写入时间戳”是瞬间性的操作)。

    vkCmdCopyQueryPoolResults(...)适用于可以在GPU侧直接处理查询结果的情形,比如实现GPU-driven的遮挡剔除。
    对于管线统计查询和时间戳查询,你可能会更倾向于把查询结果拷贝到主机一侧(要将查询结果显示到屏幕,用计算着色器根据数值生成字符的顶点数据是可行的,不过大部分人应该会用开源GUI的功能来绘制吧)。

    在主机(CPU)一侧,用vkGetQueryPoolResults(...)获取查询结果:

    VkResult VKAPI_CALL vkGetQueryPoolResults(...) 的参数说明

    VkDevice device

    逻辑设备的handle

    VkQueryPool queryPool

    查询池的handle

    uint32_t firstQuery

    首个要被拷贝结果的查询的索引

    uint32_t queryCount

    要被拷贝结果的查询的个数

    size_t dataSize

    pData所指向的内存区的容量(虽然该参数如此命名,并不是要你填查询结果的大小),大小须能容纳queryCount个查询结果

    void* pData

    查询结果被拷贝到pData所指向的内存区

    VkDeviceSize stride

    每组查询结果在目标内存区中的间距,单位是字节,必须是4的倍数(或8的倍数,见前文)

    VkQueryResultFlagBits flags

    • 函数执行成功时,除了VK_SUCCESS还可能会返回VK_NOT_READY(值为1),表示查询尚未结束。

    重置查询

    开始查询前(包括首次开始),必须重置查询。

    在命令缓冲区中,用vkCmdResetQueryPool(...)重置查询:

    void VKAPI_CALL vkCmdResetQueryPool(...) 的参数说明

    VkCommandBuffer commandBuffer

    命令缓冲区的handle

    VkQueryPool queryPool

    查询池的handle

    uint32_t firstQuery

    首个要被重置的查询的索引

    uint32_t queryCount

    要被重置的查询的个数

    • 必须在渲染通道之外重置查询。

    自Vulkan1.2起,可以在主机(CPU)一侧,用vkResetQueryPool(...)重置查询:

    void VKAPI_CALL vkResetQueryPool(...) 的参数说明

    VkDevice device

    逻辑设备的handle

    VkQueryPool queryPool

    查询池的handle

    uint32_t firstQuery

    首个要被重置的查询的索引

    uint32_t queryCount

    要被重置的查询的个数

    封装为queryPool类

    接下来先对VkQueryPool进行封装,再逐一解说Vulkan核心功能中提供的三种查询类型及相关函数。
    VKBase.h,vulkan命名空间中添加以下代码:

    class queryPool {
        VkQueryPool handle = VK_NULL_HANDLE;
    public:
        queryPool() = default;
        queryPool(VkQueryPoolCreateInfo& createInfo) {
            Create(createInfo);
        }
        queryPool(VkQueryType queryType, uint32_t queryCount, VkQueryPipelineStatisticFlags pipelineStatistics = 0 /*VkQueryPoolCreateFlags flags*/) {
            Create(queryType, queryCount, pipelineStatistics);
        }
        queryPool(queryPool&& other) noexcept { MoveHandle; }
        ~queryPool() { DestroyHandleBy(vkDestroyQueryPool); }
        //Getter
        DefineHandleTypeOperator;
        DefineAddressFunction;
        //Const Function
        void CmdReset(VkCommandBuffer commandBuffer, uint32_t firstQueryIndex, uint32_t queryCount) const {
            vkCmdResetQueryPool(commandBuffer, handle, firstQueryIndex, queryCount);
        }
        void CmdBegin(VkCommandBuffer commandBuffer, uint32_t queryIndex, VkQueryControlFlags flags = 0) const {
            vkCmdBeginQuery(commandBuffer, handle, queryIndex, flags);
        }
        void CmdEnd(VkCommandBuffer commandBuffer, uint32_t queryIndex) const {
            vkCmdEndQuery(commandBuffer, handle, queryIndex);
        }
        void CmdWriteTimestamp(VkCommandBuffer commandBuffer, VkPipelineStageFlagBits pipelineStage, uint32_t queryIndex) const {
            vkCmdWriteTimestamp(commandBuffer, pipelineStage, handle, queryIndex);
        }
        void CmdCopyResults(VkCommandBuffer commandBuffer, uint32_t firstQueryIndex, uint32_t queryCount,
            VkBuffer buffer_dst, VkDeviceSize offset_dst, VkDeviceSize stride, VkQueryResultFlags flags = 0) const {
            vkCmdCopyQueryPoolResults(commandBuffer, handle, firstQueryIndex, queryCount, buffer_dst,offset_dst, stride, flags);
        }
        result_t GetResults(uint32_t firstQueryIndex, uint32_t queryCount, size_t dataSize, void* pData_dst, VkDeviceSize stride, VkQueryResultFlags flags = 0) const {
            VkResult result = vkGetQueryPoolResults(graphicsBase::Base().Device(), handle, firstQueryIndex, queryCount, dataSize, pData_dst, stride, flags);
            if (result)
                result > 0 ? //若返回值为VK_NOT_READY,则查询尚未结束,有查询结果尚不可获
                outStream << std::format("[ queryPool ] WARNING\nNot all queries are available!\nError code: {}\n", int32_t(result)) :
                outStream << std::format("[ queryPool ] ERROR\nFailed to get query pool results!\nError code: {}\n", int32_t(result));
            return result;
        }
        /*Provided by VK_API_VERSION_1_2*/
        void Reset(uint32_t firstQueryIndex, uint32_t queryCount) {
            vkResetQueryPool(graphicsBase::Base().Device(), handle, firstQueryIndex, queryCount);
        }
        //Non-const Function
        result_t Create(VkQueryPoolCreateInfo& createInfo) {
            createInfo.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO;
            VkResult result = vkCreateQueryPool(graphicsBase::Base().Device(), &createInfo, nullptr, &handle);
            if (result)
                outStream << std::format("[ queryPool ] ERROR\nFailed to create a query pool!\nError code: {}\n", int32_t(result));
            return result;
        }
        result_t Create(VkQueryType queryType, uint32_t queryCount, VkQueryPipelineStatisticFlags pipelineStatistics = 0 /*VkQueryPoolCreateFlags flags*/) {
            VkQueryPoolCreateInfo createInfo = {
                .queryType = queryType,
                .queryCount = queryCount,
                .pipelineStatistics = pipelineStatistics
            };
            return Create(createInfo);
        }
    };
    

    遮挡查询

    遮挡查询(occlusion query)可以统计绘制出的采样点的数量。

    没有通过深度测试和模板测试等早期/后期片段测试的、覆盖率(见多重采样gl_SampleMaskIngl_SampleMask)为0的、在片段着色器中被discard的,这些采样点都算是“被遮挡”,不计入查询结果。
    另外,管线混色状态中,若将VkPipelineColorBlendAttachmentState::colorWriteMask指定为0(即关闭对RGBA四通道的颜色写入),则查询结果的计数不会增加。
    先被绘制出来,之后被遮掉的采样点,当然计入查询结果(毕竟在计数的时候不知道后来会不会被遮掉)。

    创建遮挡查询

    要创建遮挡查询,只需要指定查询类型和查询个数即可,每组查询结果是一个整形数值,于是:

    class occlusionQueries {
    protected:
        queryPool queryPool;
        std::vector<uint32_t> passingSampleCounts;
    public:
        occlusionQueries() = default;
        occlusionQueries(uint32_t capacity) {
            Create(capacity);
        }
        //Getter
        operator VkQueryPool() const { return queryPool; }
        const VkQueryPool* Address() const { return queryPool.Address(); }
        uint32_t Capacity() const { return passingSampleCounts.size(); }
        uint32_t PassingSampleCount(uint32_t index) const { return passingSampleCounts[index]; }
        //Const Function
        /*待填充*/
        //Non-const Function
        void Create(uint32_t capacity) {
            passingSampleCounts.resize(capacity); //使passingSampleCounts的元素个数等于查询池容量
            queryPool.Create(VK_QUERY_TYPE_OCCLUSION, capacity);
        }
        result_t GetResults() {
            return queryPool.GetResults(Capacity());
        }
        result_t GetResults(uint32_t queryCount) {
            return queryPool.GetResults(
                0,                          //firstQuery
                queryCount,                 //queryCount
                queryCount * 4,             //dataSize,内存区大小
                passingSampleCounts.data(), //pData
                4                           //stride,每组查询结果在目标内存区中的间距,这里是uint32_t的大小,即是紧密排列
            );                              //使用该函数获取结果时应当有栅栏同步,无需注明VK_QUERY_RESULT_WAIT_BIT
        }
    };
    
    • 将查询结果获取为32位整数应该是足够了,除非你使用的硬件有夸张的运算性能(得绘制4096*4096大小的区域256次且没有采样点被丢弃,才能达到UINT32_MAX)。

    考虑需要变更查询池容量,重建查询池的情况:

    void Create(uint32_t capacity) {
        passingSampleCounts.resize(capacity);
        /*新增*/ passingSampleCounts.shrink_to_fit();
        queryPool.Create(VK_QUERY_TYPE_OCCLUSION, Capacity()); //capacity变为Capacity()
    }
    void Recreate(uint32_t capacity) {
        graphicsBase::Base().WaitIdle(); //重建查询池之前应确保物理设备没有在使用它
        queryPool.~queryPool();
        Create(capacity);
    }
    
    • 用std::vector::resize(...)重设一个vector容器的元素数时,即便重设到更小容量,容器底层的堆内存大小也不会减少。
      重设vector容器到更小容量后,用std::vector::shrink_to_fit()可以将底层的堆内存重新分配,以节省多余的容量。

    开始和结束遮挡查询

    若向vkCmdBeginQuery(...)的flags参数填入VK_QUERY_CONTROL_PRECISE_BIT(值为1),则查询结果为最终保留下来的采样点个数的精确值(计数方法为实现特定,不同Vulkan实现提供的数值间可能略有偏差)。

    否则,应当将查询结果视为二值的:在有采样点被保留下来时的查询结果为非零值(对具体数值无要求),所有采样点皆不被保留时的查询结果为0。
    注明VK_QUERY_CONTROL_PRECISE_BIT时的开销可能大于不注明该bit时的开销,在只需知晓是否会渲染出内容,而不关注具体的采样点数量时,不应注明该bit。

    void CmdBegin(VkCommandBuffer commandBuffer, uint32_t queryIndex, bool isPrecise = false) const {
        queryPool.CmdBegin(commandBuffer, queryIndex, isPrecise);
    }
    void CmdEnd(VkCommandBuffer commandBuffer, uint32_t queryIndex) const {
        queryPool.CmdEnd(commandBuffer, queryIndex);
    }
    

    封装

    完成剩余封装:

    class occlusionQueries {
    protected:
        queryPool queryPool;
        std::vector<uint32_t> passingSampleCounts;
    public:
        occlusionQueries() = default;
        occlusionQueries(uint32_t capacity) {
            Create(capacity);
        }
        //Getter
        operator VkQueryPool() const { return queryPool; }
        const VkQueryPool* Address() const { return queryPool.Address(); }
        uint32_t Capacity() const { return passingSampleCounts.size(); }
        uint32_t PassingSampleCount(uint32_t index) const { return passingSampleCounts[index]; }
        //Const Function
        /*重置整个查询池*/
        void CmdReset(VkCommandBuffer commandBuffer) const {
            queryPool.CmdReset(commandBuffer, 0, Capacity());
        }
        void CmdBegin(VkCommandBuffer commandBuffer, uint32_t queryIndex, bool isPrecise = false) const {
            queryPool.CmdBegin(commandBuffer, queryIndex, isPrecise);
        }
        void CmdEnd(VkCommandBuffer commandBuffer, uint32_t queryIndex) const {
            queryPool.CmdEnd(commandBuffer, queryIndex);
        }
        /*常用于GPU-driven遮挡剔除*/
        void CmdCopyResults(VkCommandBuffer commandBuffer, uint32_t firstQueryIndex, uint32_t queryCount,
            VkBuffer buffer_dst, VkDeviceSize offset_dst, VkDeviceSize stride) const {
            //需要等待查询结束以获取正确的数值,flags为VK_QUERY_RESULT_WAIT_BIT
            queryPool.CmdCopyResults(commandBuffer, firstQueryIndex, queryCount, buffer_dst, offset_dst,stride, VK_QUERY_RESULT_WAIT_BIT);
        }
        //Non-const Function
        void Create(uint32_t capacity) {
            passingSampleCounts.resize(capacity);
            passingSampleCounts.shrink_to_fit();
            queryPool.Create(VK_QUERY_TYPE_OCCLUSION, Capacity());
        }
        void Recreate(uint32_t capacity) {
            graphicsBase::Base().WaitIdle();
            queryPool.~queryPool();
            Create(capacity);
        }
        result_t GetResults() {
            return queryPool.GetResults(Capacity);
        }
        result_t GetResults(uint32_t queryCount) {
            return queryPool.GetResults(0, queryCount, queryCount * 4, timestamps.data(), 4);
        }
    };
    

    管线统计查询

    在Vulkan的核心功能中(即不涉及扩展的话),可通过管线统计查询(pipeline statistics query)进行以下统计:

    版本要求

    VkQueryPipelineStatisticFlagBits 的枚举项

    1.0

    VK_QUERY_PIPELINE_STATISTIC_INPUT_ASSEMBLY_VERTICES_BIT 表示输入的顶点个数,可能计算不构成完整图元的顶点

    1.0

    VK_QUERY_PIPELINE_STATISTIC_INPUT_ASSEMBLY_PRIMITIVES_BIT 表示输入的图元个数,可能计算不完整的图元

    1.0

    VK_QUERY_PIPELINE_STATISTIC_VERTEX_SHADER_INVOCATIONS_BIT 表示顶点着色器的执行次数

    1.0

    VK_QUERY_PIPELINE_STATISTIC_GEOMETRY_SHADER_INVOCATIONS_BIT 表示几何着色器的执行次数

    1.0

    VK_QUERY_PIPELINE_STATISTIC_GEOMETRY_SHADER_PRIMITIVES_BIT 表示几何着色器生成的图元个数

    1.0

    VK_QUERY_PIPELINE_STATISTIC_CLIPPING_INVOCATIONS_BIT 表示到达图元剪裁阶段的图元个数

    1.0

    VK_QUERY_PIPELINE_STATISTIC_CLIPPING_PRIMITIVES_BIT 表示图元剪裁阶段输出的图元个数,计数方法为实现特定

    1.0

    VK_QUERY_PIPELINE_STATISTIC_FRAGMENT_SHADER_INVOCATIONS_BIT 表示片段着色器的执行次数,辅助调用被计算在内

    1.0

    VK_QUERY_PIPELINE_STATISTIC_TESSELLATION_CONTROL_SHADER_PATCHES_BIT 表示细分控制着色器处理的patch的个数

    1.0

    VK_QUERY_PIPELINE_STATISTIC_TESSELLATION_EVALUATION_SHADER_INVOCATIONS_BIT 表示细分求值着色器的执行次数

    1.0

    VK_QUERY_PIPELINE_STATISTIC_COMPUTE_SHADER_INVOCATIONS_BIT 表示计算着色器的执行次数

    • 图元剪裁(primitive cliping)阶段指根据视见体、gl_ClipDistancegl_CullDistance,剪裁和丢弃图元的阶段。

    • 到达图元剪裁阶段的图元个数,与片段着色器前最后一个着色器阶段生成的图元个数一致。

    • 视见体(view volume)即齐次剪裁空间的可视范围,根据视见体进行剪裁,等价于只保留NDC坐标在可渲染范围内的片段。如果绘制的图元全部在范围之外,图元剪裁阶段输出的图元个数为0。

    管线统计查询可以被用于debug。
    比如,如果图元剪裁阶段输出的图元个数非0,而片段着色器执行次数为0,那么说明片段没有通过早期片段测试,进而检查相关参数来找出问题所在(注:对于这里所述的情况,如果开了深度测试,深度比较方式/深度清屏值的嫌疑很大)。

    创建管线统计查询

    不涉及扩展,VkQueryPipelineStatisticFlagBits的枚举项有11种,各个bit的总和是(1 << 11) - 1,即2047。
    若要进行这11个枚举项对应的所有统计,创建保存1组统计结果的查询池的代码如下:

    constexpr uint32_t statisticCount = 11;
    VkQueryPoolCreateInfo createInfo = {
        .queryType = VK_QUERY_TYPE_PIPELINE_STATISTICS,
        .queryCount = 1,
        .pipelineStatistics = (1 << statisticCount) - 1
    };
    queryPool queryPool(createInfo);
    

    根据你向VkQueryPoolCreateInfo::pipelineStatistics提供的数值,各项管线统计值依其对应的枚举项,按照前述VkQueryPipelineStatisticFlagBits枚举项表格中的顺序排列。
    特地去记忆哪个索引对应哪个统计值就太麻烦了,对于一个保存了全部11种统计值的数组,可用下述写法方便地进行访问:

    enum statisticName {
        //输入装配阶段
        vertexCount_ia,           0
        primitiveCount_ia,        1
        //顶点着色器阶段
        invocationCount_vs,       2
        //几何着色器阶段
        invocationCount_gs,       3
        primitiveCount_gs,        4
        //图元剔除阶段
        invocationCount_clipping, 5
        primitiveCount_clipping,  6
        //片段着色器阶段
        invocationCount_fs,       7
        //细分控制着色器阶段
        patchCount_tcs,           8
        //细分求值着色器阶段
        invocationCount_tes,      9
        //计算着色器阶段
        invocationCount_cs,       10
        //上述枚举项的总数
        statisticCount            11
    };
    uint32_t statistics[statisticCount] = {};
    outStream << statistics[invocationCount_cs] << '\n'; //输出计算着色器的执行次数
    
    • 这里使用了一个C++编程技巧:通过定义枚举,使用有意义的名称为索引来访问数组元素。在从0开始递增的枚举序列中,最后一个枚举项的数值会是先前所有枚举项的总数,因此还可以通过枚举来统计名称的个数。

    封装

    可对保存1组统计结果的查询池做如下封装以方便使用:

    class pipelineStatisticQuery {
    protected:
        enum statisticName {
            //Input Assembly
            vertexCount_ia,
            primitiveCount_ia,
            //Vertex Shader
            invocationCount_vs,
            //Geometry Shader
            invocationCount_gs,
            primitiveCount_gs,
            //Clipping
            invocationCount_clipping,
            primitiveCount_clipping,
            //Fragment Shader
            invocationCount_fs,
            //Tessellation
            patchCount_tcs,
            invocationCount_tes,
            //Compute Shader
            invocationCount_cs,
            statisticCount
        };
        //--------------------
        queryPool queryPool;
        uint32_t statistics[statisticCount] = {};
    public:
        pipelineStatisticQuery() {
            Create();
        }
        //Getter
        operator VkQueryPool() const { return queryPool; }
        const VkQueryPool* Address() const { return queryPool.Address(); }
        uint32_t     VertexCount_Ia() const { return statistics[vertexCount_ia]; }
        uint32_t  PrimitiveCount_Ia() const { return statistics[primitiveCount_ia]; }
        uint32_t InvocationCount_Vs() const { return statistics[invocationCount_vs]; }
        uint32_t InvocationCount_Gs() const { return statistics[invocationCount_gs]; }
        uint32_t  PrimitiveCount_Gs() const { return statistics[primitiveCount_gs]; }
        uint32_t InvocationCount_Clipping() const { return statistics[invocationCount_clipping]; }
        uint32_t  PrimitiveCount_Clipping() const { return statistics[primitiveCount_clipping]; }
        uint32_t InvocationCount_Fs() const { return statistics[invocationCount_fs]; }
        uint32_t      PatchCount_Tcs() const { return statistics[patchCount_tcs]; }
        uint32_t InvocationCount_Tes() const { return statistics[invocationCount_tes]; }
        uint32_t InvocationCount_Cs() const { return statistics[invocationCount_cs]; }
        //Const Function
        void CmdReset(VkCommandBuffer commandBuffer) const {
            queryPool.CmdReset(commandBuffer, 0, 1);
        }
        void CmdBegin(VkCommandBuffer commandBuffer) const {
            queryPool.CmdBegin(commandBuffer, 0);
        }
        void CmdEnd(VkCommandBuffer commandBuffer) const {
            queryPool.CmdEnd(commandBuffer, 0);
        }
        void CmdResetAndBegin(VkCommandBuffer commandBuffer) const {
            CmdReset(commandBuffer);
            CmdBegin(commandBuffer);
        }
        //Non-const Function
        void Create() {
            queryPool.Create(VK_QUERY_TYPE_PIPELINE_STATISTICS, 1, (1 << statisticCount) - 1);
        }
        result_t GetResults() {
            return queryPool.GetResults(
                0,                 //firstQuery
                1,                 //queryCount
                sizeof statistics, //dataSize,内存区大小
                statistics,        //pData
                sizeof statistics  //stride,每组查询结果在目标内存区中的间距,这里是紧密排列(因为就1组结果,可以写0)
            );
        }
    };
    
    • VertexCount_Ia()比Statistic(vertexCount_ia)短,所以就这么写了。

    时间戳查询

    在命令缓冲区的执行过程中,可写入时间戳到时间戳查询(timestamp query)。
    时间戳查询可用于性能测试:两个时间戳相减即表明两次写入的间隔时间,即其间命令大致的执行耗时。

    写入时间戳

    时间戳查询没有“开始”和“结束”,不对其使用vkCmdBeginQuery(...)和vkCmdEndQuery(...)。
    相应地,在命令缓冲区中,用vkCmdWriteTimestamp(...)写入时间戳:

    void VKAPI_CALL vkCmdWriteTimestamp(...) 的参数说明

    VkCommandBuffer commandBuffer

    命令缓冲区的handle

    VkPipelineStageFlagBits pipelineStage

    需要等待的管线阶段

    VkQueryPool queryPool

    查询池的handle

    uint32_t query

    被写入时间戳的查询的索引

    • 该命令定义执行依赖:于该命令前的命令到达pipelineStage所注明的(但凡到达得了的)阶段后,写入时间戳。
      注意该函数不保证写入时间戳的最晚阶段,Vulkan的实现可能在先前的命令到达pipelineStage所注明的阶段后的任何阶段写入时间戳。

    写入的时间戳的单位为一定量的纳秒,时间戳的数值间隔1,相当于时间上间隔VkPhysicalDeviceLimits::timestampPeriod个纳秒。

    写入的时间戳的有效位数由VkQueueFamilyProperties::timestampValidBits决定,若支持写入时间戳,则至少会有36个有效位数。
    虽然至少会有36个有效位数,不过查询结果通常是用于计算差值的,获取到的结果是32位就足够,就算数值溢出也能算得正确的差值(除非两个时间戳之间相差的时间很大,比如VkPhysicalDeviceLimits::timestampPeriod为1且两个时间戳之间相差4秒以上的情况)。

    两次写入之间存在隐式同步:对于两条vkCmdWriteTimestamp(...)命令,后录制的命令写入的数值所表明的时间,一定晚于先录制的命令(除非数值溢出)。

    封装

    每组时间戳查询的查询结果是一个整形数值,既然获取到32位就够用了,对其做如下封装:

    class timestampQueries {
    protected:
        queryPool queryPool;
        std::vector<uint32_t> timestamps;
    public:
        timestampQueries() = default;
        timestampQueries(uint32_t capacity) {
            Create(capacity);
        }
        //Getter
        operator VkQueryPool() const { return queryPool; }
        const VkQueryPool* Address() const { return queryPool.Address(); }
        uint32_t Capacity() const { return timestamps.size(); }
        uint32_t Timestamp(uint32_t index) const { return timestamp[index]; }
        /*计算两个时间戳之间的差值*/
        uint32_t Duration(uint32_t index) const { return timestamp[index + 1] - timestamp[index]; }
        //Const Function
        /*重置整个查询池*/
        void CmdReset(VkCommandBuffer commandBuffer) const {
            queryPool.CmdReset(commandBuffer, 0, Capacity());
        }
        void CmdWriteTimestamp(VkCommandBuffer commandBuffer, VkPipelineStageFlagBits pipelineStage, uint32_t queryIndex) const {
            queryPool.CmdWriteTimestamp(commandBuffer, pipelineStage, queryIndex);
        }
        //Non-const Function
        void Create(uint32_t capacity) {
            timestamps.resize(capacity);
            timestamps.shrink_to_fit();
            queryPool.Create(VK_QUERY_TYPE_TIMESTAMP, Capacity());
        }
        void Recreate(uint32_t capacity) {
            graphicsBase::Base().WaitIdle();
            queryPool.~queryPool();
            Create(capacity);
        }
        result_t GetResults() {
            return queryPool.GetResults(Capacity());
        }
        result_t GetResults(uint32_t queryCount) {
            return queryPool.GetResults(0, queryCount, queryCount * 4, timestamps.data(), 4);
        }
    };
    

    使用方式,类似这样:

    uint32_t timestampCounter = 0;
    commandBuffer.Begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT);
    
    timestampQueries.CmdReset(commandBuffer);
    timestampQueries.CmdWriteTimestamp(commandBuffer, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, timestampCounter++);
    /*某些命令A*/
    timestampQueries.CmdWriteTimestamp(commandBuffer, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, timestampCounter++);
    /*某些命令B*/
    timestampQueries.CmdWriteTimestamp(commandBuffer, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, timestampCounter++);
    
    commandBuffer.End();
    
    graphicsBase::Base().SubmitCommandBuffer_Graphics(commandBuffer, fence);
    fence.Wait();
    
    timestampQueries.GetResults(timestampCounter);
    //以毫秒单位输出时间间隔
    for (size_t i = 0; i < timestampCounter - 1; i++)
        outStream << timestampQueries.Duration(i) * graphicsBase::Base().PhysicalDeviceProperties().limits.timestampPeriod / 1000000.f << '\n';