A good way to think about a buffer or an image descriptor is to imagine it as a very fat pointer. This is, in fact, not too far removed from reality, as we shall see.
Taking a peek at radv, we find the uniform buffer and storage buffer descriptors to be a 4-word tuple, where the first two words make up the address, followed by length in bytes for bounds checking and an extra word, which holds format information and bounds checking behavior 1.
Similarly, the sampled image descriptor is a 16-word tuple containing an
address, a format, extent, number of samples, mip levels, layers, and other bits
the user provides when creating an VkImageView
2.
The sampler descriptor is an odd one out in that in radv most sampler descriptors are pure fat but some keep an index into a stash of samplers' extra bits. That is, a sampler descriptor is a 4-word tuple, which holds all of the sampler bits, unless custom border color is used, in which case the last word also maintains an index into an array of custom border colors 3.
Combining these bits of knowledge, it is easy to guess that a combined image-sampler descriptor is, in fact, a sampled image and a sampler descriptors glued together.
Descriptors are grouped into descriptor sets, not unlike variables are composed into structures in C, with descriptor set layout being akin to a type definition. Let's conceive an arbitrary descriptor set layout
// A list of VkDescriptorSetLayoutBindings making up an "everything"
// descriptor set. For simplicity, all stages can access all bindings.
{0, VK_DESCRIPTOR_TYPE_INLINE_UNIFORM_BLOCK_EXT, 128, VK_SHADER_STAGE_ALL, NULL}, // camera
{1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_ALL, NULL}, // transforms
{2, VK_DESCRIPTOR_TYPE_SAMPLER, 2, VK_SHADER_STAGE_ALL, NULL}, // samplers
{3, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 10000, VK_SHADER_STAGE_ALL, NULL}, // manyimages
In our C analogy, such a descriptor set layout would be written as follows
struct DescriptorSetOfEverything {
char camera[128];
StorageBuffer transforms;
Sampler samplers[2];
SampledImage manyimages[10000];
};
with a slight caveat that the offsets are unspecified and are hidden inside
VkDescriptorSetLayout
4. Nevertheless, let's put on shoes of radv and
calculate the descriptor offsets and the size of the descriptor set. First, we
shall familiarize ourselves with size and alignment of each descriptor
Descriptor | Size | Alignment |
---|---|---|
sampler | 16 | 16 |
storage buffer | 16 | 16 |
sampled image | 64 | 32 |
inline uniform block | 1 | 16 |
Then let there be a sequence nᵢ, n₀ = 0, nᵢ₊₁ = roundup(nᵢ, aᵢ) + kᵢmᵢ, where aᵢ, mᵢ are, respectively, the alignment and size of i-th binding's descriptor, kᵢ is i-th binding's descriptor count and roundup(x, y) = min {yn | yn ≧ x, n ∈ ℤ}. For each binding i, roundup(nᵢ, aᵢ) is the offset of the binding's first descriptor and given the number of bindings p, nₚ is the descriptor set's size. Writing out this sequence, we get 0, 128, 144, 176, 640192. The offsets of each binding's first descriptor are thus 0, 128, 144, 192 and the size of a descriptor set of this layout would be 640192 bytes.
Note: VK_EXT_descriptor_buffer
removes descriptor management APIs, leaving
descriptor management entirely up to the user and making this section
irrelevant. Fast forward to Push Descriptors.
Prior to descriptor buffers, there was no malloc
for descriptor sets and, in
fact, no good analogy that a reader would be familiar with appears to exist.
Descriptor pools can be confusing. Read the following closely, lest you will
find your program only works on your computer.
A good starting point for reasoning about descriptor pools is to pretend that a
VkDescriptorPool
is a VkDeviceMemory
for descriptor sets. The list of
descriptor pool sizes taken by vkCreateDescriptorPool
specifies the size of
the underlying VkDeviceMemory
as a sum of each descriptor size times
descriptor count 5. For a concrete example, let's consider the following list
of pool sizes
{VK_DESCRIPTOR_TYPE_SAMPLER, 1},
{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 100},
In radv, sampler and combined image-sampler descriptors take up 32 and 96 bytes
respectively, thus the VkDeviceMemory
inside such descriptor pool will be
32⋅1 + 96⋅100 = 9632 bytes. This is plenty to allocate a descriptor set of 200
UNIFORM_BUFFER
descriptors and such a vkAllocateDescriptorSets
call will
indeed succeed on radv, where a buffer descriptor takes 32 bytes. This behavior
is to be exploited, but not to be relied upon.
A simple method to deal with the allocation is to use a very large capacity
descriptor pool and allocate descriptor sets until VK_ERROR_OUT_OF_POOL_MEMORY
is returned. In the out of pool memory case, the pool becomes a zombie and when
the descriptor sets it backs are not needed any more, the pool can be freed.
VkDescriptorPoolSize poolSizes[] = {
{VK_DESCRIPTOR_TYPE_SAMPLER, 1000},
{VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1000},
{VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1000},
{VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1000},
{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1000},
};
// ...
if ((r = vkCreateDescriptorPool(device, &(VkDescriptorPoolCreateInfo) {
.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO,
.maxSets = 10000,
.poolSizeCount = nelem(poolSizes),
.pPoolSizes = poolSizes,
}, NULL, &descriptorPool)) != VK_SUCCESS) {
// Handle error.
}
While this method is simple, it can waste significant amounts of memory for some applications, unless some tuning is done.
This inefficiency may be remedied by creating a descriptor pool per descriptor set layout, which will accomodate some number of descriptor sets of this layout.
If a lot of descriptor sets have the same lifetime such as in cases, for example, when the application allocates all descriptor sets during initialization, it's possible to compute optimal pool size and use a single pool, side-stepping descriptor pool cycling headaches entirely.
TODO
VK_EXT_descriptor_buffer
removes the descriptor management APIs, letting the
user manage descriptors almost like any other data. Special treatment still
applies, such as stricter memory type requirements and the way descriptors are
accessed in the shader is different from accessing ordinary data. Structures
that wish to refer to the objects that descriptors represent need to establish
correspondence between some non-opaque data (for example, an integer) and a
descriptor. Section Solution Space in VK_EXT_descriptor_buffer
6, option 5
presents a more flexible way of accessing descriptors, providing an idea of what
shader descriptor access could look like in the future.
Footnotes
-
https://gitlab.freedesktop.org/mesa/mesa/-/blob/04be7934df765eea0623360f748249870487baee/src/amd/vulkan/radv_descriptor_set.c#L982 ↩
-
https://gitlab.freedesktop.org/mesa/mesa/-/blob/04be7934df765eea0623360f748249870487baee/src/amd/vulkan/radv_image.c#L1675 ↩
-
https://gitlab.freedesktop.org/mesa/mesa/-/blob/04be7934df765eea0623360f748249870487baee/src/amd/vulkan/radv_device.c#L7486 ↩
-
https://gitlab.freedesktop.org/mesa/mesa/-/blob/04be7934df765eea0623360f748249870487baee/src/amd/vulkan/radv_descriptor_set.c#L196 ↩
-
https://gitlab.freedesktop.org/mesa/mesa/-/blob/04be7934df765eea0623360f748249870487baee/src/amd/vulkan/radv_descriptor_set.c#L721 ↩
-
https://github.com/KhronosGroup/Vulkan-Docs/blob/main/proposals/VK_EXT_descriptor_buffer.adoc#2-solution-space ↩