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。
版本要求 |
|
---|---|
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_SampleMaskIn、gl_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)进行以下统计:
版本要求 |
|
---|---|
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_ClipDistance、gl_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';