【问题标题】:What's the correct two-call pattern for vkEnumerate*?vkEnumerate* 的正确两次调用模式是什么?
【发布时间】:2020-12-25 04:00:45
【问题描述】:

我发现vkEnumerate* 的两次调用模式的两种变体。我想知道第二个解决方案比第一个解决方案的优点是什么。

第一种解决方案:

uint32_t count = 0;
vkEnumerateTs(ts..., &count, nullptr);
std::vector<T> results(count);
auto error = vkEnumerateTs(args..., &count, results.data());

这个解决方案没有考虑到vkEnumerateInstanceLayerPropertiesvkEnumerateInstanceExtensionProperties 可能随时更改(请参阅 Vulkan 规范 1.2.151 的 37.4.1):

由于 Vulkan 之外的操作,可用层列表可能随时更改 实现,因此对vkEnumerateInstanceLayerProperties 的两次调用可能具有相同的参数 返回不同的结果,或检索不同的pPropertyCount 值或pProperties 内容。创建实例后,为该实例启用的层将继续 在该实例的生命周期内启用并有效,即使其中一些变为 对未来的实例不可用。

(类似于vkEnumerateInstanceExtensionProperties。) 如果count 比以前大,您将无法获得所有信息; 如果count 比以前小,则results 的尾部包含无效数据。

注意:其他vkEnumerate* 命令的结果似乎有无穷大 life time,根据 Vulkan Specification 1.2.151 的 2.5.1 Lifetime of Retrieved Results,所以对于其他 vkEnumerate* 命令,第一个解决方案就足够了:

除非对单个命令另有规定,否则结果是不变的;也就是说,当通过使用相同参数调用相同命令再次检索它们时,它们将保持不变,只要这些参数本身都保持有效。

后一个问题可以通过扩展上面的解决方案来解决 (如果count 小于可用属性的数量,则返回VK_INCOMPLETE):

// Check if too much space was allocated.
if (error != VK_INCOMPLETE)
{
    results.resize(count);
}

第二个变体(取自 Vulkan-Tools; fvkEnumerateTsinit 用作模板的类型标记 扣除和默认值)似乎可以解决第一个问题(没有得到所有信息):

// Helper for robustly executing the two-call pattern
template <typename T, typename F, typename... Ts>
auto GetVectorInit(const char *func_name, F &&f, T init, Ts &&... ts) -> std::vector<T> {
    uint32_t count = 0;
    std::vector<T> results;
    VkResult err;
    do {
        err = f(ts..., &count, nullptr);
        if (err) THROW_VK_ERR(func_name, err);
        results.resize(count, init);
        err = f(ts..., &count, results.data());
        results.resize(count);
    } while (err == VK_INCOMPLETE);
    if (err) THROW_VK_ERR(func_name, err);
    return results;
}

但是,即使尝试获取“所有”信息又有什么用,如果 可用信息可能随时更改?为什么要不断迭代 如果您离开的那一刻,while 循环以获取“完整”信息 while 循环返回结果,它可能已经不完整了? 更糟糕的是,你可能(理论上)永远卡在这个 while 循环中 如果第一次调用fcount 的值总是更小 比第二次调用后的值。

我是否误解了情况?是否第二个变体 比第一个有什么优点(我提出的修改)?

【问题讨论】:

  • 你只是想多了。不确定问题到底是什么,因为您已经列出了惯用方案。第二个版本的优点是它不会触发强迫症,看起来它可以很好地处理所有事情,而且只长了2 LOC。换句话说,让读者好奇和思考的代码通常是正确的。想要对事物“聪明”并过早地尝试优化某些事物的代码通常是错误的。
  • @krOoze:我不确定我是否理解你的观点。 OP 的重点是规范声明这些特定枚举函数返回的值不可靠。他们可以随时改变。如果它们可能不断变化,为什么还要麻烦循环呢?循环只是创造了它永远不会结束的可能性。枚举值最终会稳定到某个特定值的想法吗?因为规范没有要求。
  • @NicolBolas 我的意思是,我不确定 OP 试图解决什么问题,因为惯用版本非常好、健壮且难以使用错误。在这里寻找替代品的原因是什么?除非驱动程序是积极恶意的,否则它永远不会结束的可能性为零,在这种情况下,您会遇到更严重的问题。在 99.999 % 的情况下,它会在第一次迭代后结束。
  • @krOoze:“惯用的版本非常好,健壮,而且很难用错”并且被隐藏了。该函数不是 Vulkan API 的一部分;它在一个补充 API 中。许多人不使用该补充 API,因此自然会默认以正常方式使用。这就是成语的问题。它们要求您知道它们的存在。问题的重点是,如果现有方法也同样有效,那么成语的意义何在?如果现有方法不可靠,那么该成语如何更可靠,因为规范没有说明确保其可靠性?
  • @NicolBolas 他问的是模式,而不是 util 函数。 util 函数的重点是能够交换所有vkEnumarate*vkGet* 命令,而不必以容易出错的方式手动为每个命令编写专门化。但他没有问这个。至于模式,大多数人都得出了相同的结论。有时,如果很确定它是不变的,可以编写较短的版本,但这很容易出错并且需要思考,以及为什么要进行另一种专门化以将 jmp 保存在热点之外。

标签: c++ vulkan


【解决方案1】:

好的,这主要是基于意见或审美的问题。我觉得你在想一些在很大程度上可能无关紧要的事情。

这两个版本本身都没有问题。还有另一个常见的版本,它通过估计在堆栈上创建足够大的数组,并且只需调用一次 Vulkan 查询。谈到vkEnumerateInstanceLayerProperties,另一种选择是完全跳过它,只依赖VK_ERROR_LAYER_NOT_PRESENT


查询不是代码中有趣的部分。众所周知,这里的交易是 C API 的大部分 C++ 化(std::vector,以及满足 DRY 原则的模板),而不是编写有用的代码。

我的论点是,最不“聪明”且最不令人惊讶的实现是最好的。基本上,您可以快速浏览并说“是的,这符合我的意图,并且似乎可以正确覆盖所有角落”,而无需三思而后行。

如果您已经得到VK_INCOMPLETE,那么您知道数据已经不完整,那么为什么要返回您知道事实不完整的数据呢?好的,0.0001% 的情况下会发生侥幸。好吧,无论如何,数据可能会在以后更改。但问题是另一种方式。第一个版本比第二个版本提供什么?在我看来,它似乎只是通过巧妙地分析情况而跳过处理其中一个错误代码来尝试变得聪明。它通过它得到什么?少了两行代码?聪明和聪明并不能真正保证这一点,最好在其他地方使用,代码的读者可能不会期望平凡的代码中有聪明的东西。

无限循环的情况确实是理论上的(我应该说不存在)问题。这将需要恶意驱动程序主动创建这种情况。在这种情况下,您会遇到更大的问题。

【讨论】:

  • “好的,这主要是基于意见或审美的问题。” - 真的。感谢您撰写答案!
猜你喜欢
  • 2014-06-28
  • 2023-03-07
  • 1970-01-01
  • 2023-03-21
  • 1970-01-01
  • 1970-01-01
  • 2013-09-03
  • 1970-01-01
相关资源
最近更新 更多