What's the correct two-call pattern for vkEnumerate*?

Viewed 165

I've found two variants of the two-call pattern for vkEnumerate*. I'm wondering what the merit of the second solution over the first is.

The first solution:

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

What this solution doesn't take into account is that vkEnumerateInstanceLayerProperties and vkEnumerateInstanceExtensionProperties may change at any time (see 37.4.1 of Vulkan Specification 1.2.151):

The list of available layers may change at any time due to actions outside of the Vulkan implementation, so two calls to vkEnumerateInstanceLayerProperties with the same parameters may return different results, or retrieve different pPropertyCount values or pProperties contents. Once an instance has been created, the layers enabled for that instance will continue to be enabled and valid for the lifetime of that instance, even if some of them become unavailable for future instances.

(Similar for vkEnumerateInstanceExtensionProperties.) If count is larger than before, you're not getting all the info; if count is smaller than before, the tail of results contains invalid data.

Note: The results of the other vkEnumerate* commands seem to have infinite life time, according to 2.5.1 Lifetime of Retrieved Results of Vulkan Specification 1.2.151, so for the other vkEnumerate* commands, the first solution is sufficient:

Unless otherwise specified for an individual command, the results are invariant; that is, they will remain unchanged when retrieved again by calling the same command with the same parameters, so long as those parameters themselves all remain valid.

The latter problem can be solved by extending the solution above (VK_INCOMPLETE is returned if count is smaller than the number of properties available):

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

The second variant (taken from Vulkan-Tools; f is vkEnumerateTs, init serves as type sentinel for template deduction and default value) seems to serve to solve the first problem (not getting all the info):

// 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;
}

But what's the use of even trying to get "all" the info, if the amount of available info may change at any time? Why keep iterating through the while loop to get "complete" information if the moment you leave the while loop to return the results, it may already be incomplete again? Even worse, you may (theoretically) get stuck in this while loop forever if the value of count after the first call to f is always smaller than its value after the second call.

Am I misinterpreting the situation? Does the second variant have any merit over the first (with the modification I propose)?

1 Answers

OK, this is largely becomming opinion-based or esthetical question. I feel you are overthinking something that largely might not matter.

There is nothing wrong with either version per-se. There is also another common version that creates large enough array on the stack by estimate, and simply calls the Vulkan query once. Speaking about vkEnumerateInstanceLayerProperties, another option would be to skip it altogether and rely on the VK_ERROR_LAYER_NOT_PRESENT only.


Queries are not the interesting parts of the code. And by all accounts what is the deal here is to a large part C++ification (std::vector, and templating to satisfy DRY principle) of the C API rather than coding something useful.

My argument here would be that the implementation that is least "smart" and least surprising is the best. Basically something that you can scan over quick and say "yep, that does what I intend and seems to properly cover all the cornercases", without thinking twice about it.

If you already got VK_INCOMPLETE you know for a fact the data already are incomplete, so why return data that you know for a fact are incomplete? OK, it is a fluke that would happen in 0.0001 % of cases. And OK, the data might be changed later anyway. But the question is other way around. What does the first version offer over the second version? To me it seems it only tries to be smart by skipping dealing with one of the error codes using a clever analysis of the situation. By which it gets what? Two lines of code less? The smarts and cleverness is not really warranted for this and better be used elsewhere, and the reader of the code might not expect clever stuff in mundane code.

The infinite loop case is indeed theoretical (rather I should say non-existent) problem. It would require malicious driver to actively create that situation. In which case you have bigger problems.

Related