Confusion with Descriptor set and pools

Hello. I’ve been struggling to understand how descriptor sets and pools work. My understanding is that DescriptorSetLayouts are like structs/classes in C++, and that an instance would be a DescriptorSet, allocated from a pool. For a single draw object following the various tutorials this mental model works fine.

However, when I wish to create a few more objects (eg. another 2 triangles with a different uniform transform data), just like in C++, I create an instance (DescriptorSet) of the same DescriptorSetLayout. The mental model I have is that I allocate a new DescriptorSet from a pool, however this is where I run into trouble. This will only work if DescriptorPoolSize.descriptorCount = 3 (or higher) for each DescriptorPoolSize.type::eUniformBuffer., and the DescriptorPoolCreateInfo.maxSets must also be 3 (or higher).

From this experiment, my mental model is assuming that DescriptorPoolSize.descriptorCount is the max pool size. If I had thousands of scenes to draw, I’d have 1000’s of uniform buffers (with transform). Both DescriptorPoolSize.descriptorCount and DescriptorPoolCreateInfo.maxSets should then bet 1000? Is this mental model correct?

To illustrate my problem, I have the following 3 scenes:

struct Uniform {Matrix4 mvp};
void RenderManager :: DescriptorPoolInit()
{
	std::array<vk::DescriptorPoolSize, 1> pool_sizes{};
	//	Uniform buffer (transform MVP)
	pool_sizes[0].type = vk::DescriptorType::eUniformBuffer;
	pool_sizes[0].descriptorCount = 3;

	vk::DescriptorPoolCreateInfo pool_info;
	pool_info.poolSizeCount = static_cast<uint32_t>(pool_sizes.size());
	pool_info.pPoolSizes = pool_sizes.data();
	pool_info.maxSets = 3;
	fVulkanDescriptorPool = fVulkanDevice.createDescriptorPool(pool_info);
}  

then further down:

void RenderManager :: CreateDescriptorSet(RenderNode *node, const size_t size)
{
	std::array<vk::DescriptorSetLayout, 1> layouts = { node->mRenderPipeline->mDescriptorSetLayout};
	vk::DescriptorSetAllocateInfo alloc_info;
	alloc_info.descriptorPool = fVulkanDescriptorPool;
	alloc_info.descriptorSetCount = static_cast<uint32_t>(layouts.size());
	alloc_info.pSetLayouts = layouts.data();

	node->mRenderPipeline->mDescriptorSets = fVulkanDevice.allocateDescriptorSets(alloc_info);

vk::DescriptorBufferInfo buffer_info;
buffer_info.buffer = node->mUniformBuffers[i].mBuffer;
buffer_info.range = size;

std::array<vk::WriteDescriptorSet, 1> descriptor_writes{};
//	Uniform
descriptor_writes[0].dstSet = node->mRenderPipeline->mDescriptorSets[i];
descriptor_writes[0].descriptorType = vk::DescriptorType::eUniformBuffer;
descriptor_writes[0].descriptorCount = 1;
descriptor_writes[0].pBufferInfo = &buffer_info;

fVulkanDevice.updateDescriptorSets(static_cast<uint32_t>(descriptor_writes.size()), descriptor_writes.data(), 0, nullptr);

}

If I dont specify the following:

pool_sizes[0].descriptorCount = 3;
...
pool_info.maxSets = 3;

I get out of memory errors on vkAllocateDescriptorSets()

Let’s keep using your analogy here: descriptor set layouts are to structs as descriptor sets are to objects. OK, but… where does the memory for those objects come from?

That’s the descriptor pool; in our analogy, it’s like new and delete for descriptor sets. However, unlike new/delete, it has two major differences:

  1. You have to tell it the maximum number of objects (sets) you’re going to allocate from that pool up-front.

  2. You have to tell it the maximum number of variables within all of those sets you will allocate from that pool up-front. In our analogy, imagine if all of your struct members were pointers to a type rather than the actual type itself. So each member has to have its memory separately allocated.

So you have to be up-front about how much stuff you’re going to use. And if you need to exceed those limits, then you’re going to have to create additional descriptor pools.

Thanks Alfonse for a quick and valuable response.

It has been challenging to mentally comprehend the reason why there are multiple layers for descriptor set layout metadata. I’m sure that the GPU designers have special registers which get bounded from separate memory pools. The documentation is very helpful here, lots of people (on the internet) that are also learning Vulkan are stumbling onto the same problem when they modify the tutorials to work with multiple scenes.

Regarding point 2 in your answer, I’m assuming you mean the following (using C code):

class AbstractSet;
AbstractSet *all_sets = new AbstractSet[maxSets];

Uniform **all_uniforms[descriptorCount_uniforms];
Sampler **all_samplers[descriptorCount_samplers];

class CustomLayout1
{
    Uniform   *uniform;
    Sampler *sampler;
};
class CustomLayout2
{
    Uniform   *uniform;
};

When I call VkAllocateDescriptorSet, it consumes a slot from all_sets. The allocated descripter set needs the vk::WriteDescriptorSet.:pBufferInfo to point to a CustomLayout1 instance. The bound vk::WriteDescriptorSet() for a uniform must be less than descriptorCount_uniforms.

Does this sound right?