如何测量Vulkan管线的执行时间

7

摘要

我希望能够在GPU上测量整个图形管道的毫秒级已经流逝的时间,目的是为了在优化代码之前/之后保存基准测试,以查看改进。在OpenGL中实现这一点非常简单,但我对Vulkan很新,并需要一些帮助。

我已经浏览了相关的现有答案(这里这里),但它们并没有多大帮助。我无法在任何地方找到代码示例,所以我敢在这里问。

通过文档页面,我发现了一些我认为应该使用的函数,因此我已经像这样放置了一些内容:

1:创建查询池

void CreateQueryPool()
{
    VkQueryPoolCreateInfo createInfo{};
    createInfo.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO;
    createInfo.pNext = nullptr; // Optional
    createInfo.flags = 0; // Reserved for future use, must be 0!

    createInfo.queryType = VK_QUERY_TYPE_TIMESTAMP;
    createInfo.queryCount = mCommandBuffers.size() * 2; // REVIEW

    VkResult result = vkCreateQueryPool(mDevice, &createInfo, nullptr, &mTimeQueryPool);
    if (result != VK_SUCCESS)
    {
        throw std::runtime_error("Failed to create time query pool!");
    }
}

我想到了queryCount = mCommandBuffers.size() * 2的主意,以便在渲染之前和之后有单独的查询时间戳空间,但我不知道这个假设是否正确。

2:记录命令缓冲区

// recording command buffer i:
vkCmdWriteTimestamp(mCommandBuffers[i], VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, mTimeQueryPool, i);
// render pass ...
vkCmdWriteTimestamp(mCommandBuffers[i], VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, mTimeQueryPool, i);

vkCmdCopyQueryPoolResults(/* many parameters here */);

我想要澄清一下几个问题:
  • 向同一个查询索引写入的后果是什么?我需要两个单独的查询池-一个用于渲染前,另一个用于渲染后吗?
  • 我应该如何处理同步?我假设每个命令缓冲区都有一个单独的查询。
  • 对于包含查询结果的目标缓冲区,将其存储在具有“主机可见位”(host visible bit)的位置是否足够好,还是需要“仅设备可见”的分段内存?我对这个也有点迷茫。

我没有找到任何关于如何测量渲染时间的在线示例,但我认为这是如此常见的任务,一定有类似的示例存在。


“下一步将是纹理的mipmapping。” 除非您在GPU上生成了纹理,或者该纹理明显不像图像,否则您应该始终对纹理进行mipmap处理。 - Nicol Bolas
@NicolBolas 只是在跟着教程走。决定在进入Mipmap章节之前实现计时,因为我很好奇它真正能提升多少性能。 - alexpanter
2个回答

8

感谢 @karlschultz 的帮助,我成功实现了一些功能。为了帮助其他人寻找相同的答案,我决定在这里发布我的发现。对于 Vulkan 专家,请让我知道是否犯了明显的错误,我将在此更正!

查询池创建

我按照我的问题描述填写了一个 VkQueryPoolCreateInfo 结构,并让它的 queryCount 字段等于命令缓冲区数量的两倍,以存储渲染前后的查询空间。

重要的是,在使用查询之前重置查询池中的所有条目,并且在写入查询之后重置查询。这需要进行一些更改:

1) 查询图形队列是否支持时间戳

在选择图形队列家族时,结构体 VkQueueFamilyProperties 有一个字段 timestampValidBits,必须大于 0,否则该队列族无法用于时间戳查询!

2) 确定时间戳周期

物理设备包含一个特殊值,指示增加 1 个时间戳查询花费的纳秒数。这是解释查询结果(如纳秒或毫秒)所必需的。该值是一个 float,可以通过调用 vkGetPhysicalDeviceProperties 并查看字段 VkPhysicalDeviceProperties.limits.timestampPeriod 来检索。

3) 请求查询重置支持

在创建逻辑设备期间,必须填写一个结构并将其添加到 pNext 链中以启用主机查询重置功能:

VkDeviceCreateInfo createInfo{};
VkPhysicalDeviceHostQueryResetFeatures resetFeatures;
resetFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_HOST_QUERY_RESET_FEATURES;
resetFeatures.pNext = nullptr;
resetFeatures.hostQueryReset = VK_TRUE;

createInfo.pNext = &resetFeatures;

4) 记录命令缓冲区

时间戳查询应该在渲染过程之外进行,如下所示。由于管线阶段可能存在时间上的重叠,因此无法测量单个着色器(例如片段着色器)的运行时间,只能测量整个管线或渲染过程之外的部分。

vkCmdWriteTimestamp(mCommandBuffers[i], VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, mTimeQueryPool, i * 2);

vkCmdBeginRenderPass(/* ... */);

// render here...

vkCmdEndRenderPass(mCommandBuffers[i]);

vkCmdWriteTimestamp(mCommandBuffers[i], VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, mTimeQueryPool, i * 2 + 1);

5) 检索查询结果

我们有两种方法可以实现: vkCmdCopyQueryPoolResultsvkGetQueryPoolResults。我选择使用后者,因为它极大地简化了设置,并且不需要与 GPU 缓冲同步。

假设我有一个交换链索引(在我的场景中与命令缓冲索引相同!),我有一个如下的设置:

void FetchRenderTimeResults(uint32_t swapchainIndex)
{
    uint64_t buffer[2];

    VkResult result = vkGetQueryPoolResults(mDevice, mTimeQueryPool, swapchainIndex * 2, 2, sizeof(uint64_t) * 2, buffer, sizeof(uint64_t),
    VK_QUERY_RESULT_64_BIT);
    if (result == VK_NOT_READY)
    {
        return;
    }
    else if (result == VK_SUCCESS)
    {
        mTimeQueryResults[swapchainIndex] = buffer[1] - buffer[0];
    }
    else
    {
        throw std::runtime_error("Failed to receive query results!");
    }

    // Queries must be reset after each individual use.
    vkResetQueryPool(mDevice, mTimeQueryPool, swapchainIndex * 2, 2);
}

变量mTimeQueryResults是指一个包含每个交换链结果的std::vector<uint64_t>,我使用它来计算每秒的平均渲染时间,通过在第二步确定的时间戳周期进行计算。

不要忘记调用vkDestroyQueryPool清理查询池。

这里省略了许多细节,对于像我这样的Vulkan新手来说,这个设置令人恐惧,并且需要花费几天时间才能弄清楚。希望这可以为其他人节省头痛。

更多信息请参见文档


2
这是一篇不错的写作。作为(3)的替代方案,你可以在命令缓冲区中使用vkCmdResetQueryPool命令重置查询池,在编写时间戳之前放置。这些与查询相关的命令存在隐式执行依赖关系,因此在写入时间戳之前会完成重置。两种方法之间的选择可能取决于应用程序的其他方面,但采用基于设备的重置时,您无需启用设备功能并考虑使用vkResetQueryPool进行主机同步。 - Karl Schultz
那其实是一个更好的解决方案。我之前认为vkCmdResetQueryPool需要命令缓冲区复制命令,但我只能想象主机查询重置也会更慢。再次感谢您的建议。 - alexpanter

1
在同一查询索引中进行写操作是不好的,因为您会用“后”时间戳覆盖“前”时间戳。您可能需要更改写时间戳调用中的最后一个参数,对于“前”调用,请将其更改为i * 2,对于“后”调用,请将其更改为i * 2 + 1。您已经为每个命令缓冲区分配了2个时间戳,但只使用了一半。此方案最终会为每个命令缓冲区i生成一对前/后时间戳。
我没有使用vkCmdCopyQueryPoolResults()的经验。如果可以使您的队列空闲,则在空闲后调用vkGetQueryPoolResults(),这可能更容易实现您在此处所做的工作。它将查询结果复制回主机内存,您无需处理将写入同步到另一个缓冲区,然后映射/读取它。

非常感谢!我在任何地方都找不到代码示例,这真的是不可能的。我会尝试这个。+1 - alexpanter

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接