Descriptors

2021/02/23

Descriptors

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 descriptor behind UNIFORM_BUFFER and STORAGE_BUFFER 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.

Dynamic buffer descriptors are similar, with a small difference in that they conceptually live in the command buffer (in case of radv, they compete for space with push constants 2) as opposed to descriptor pool.

Similarly, the descriptor behind SAMPLED_IMAGE is a 16-word tuple containing an address, a format, extent, number of samples, mip levels, layers, and other bits found in the VkImageView 3.

A sampler descriptor is an odd one in that most sampler descriptors are pure fat, but some keep an index into a stash of samplers' extra bits. That is, in radv, 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 4. Anv and turnip are similar 5 6.

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.

Descriptor Sets

Descriptors are grouped into descriptor sets, not unlike variables are composed into structures in C, with descriptor set layouts being akin to type definitions and vkUpdateDescriptorSets akin to member writes and copies. Let’s conceive an arbitrary descriptor set

// 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 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 7. 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 the descriptor set is 640192 bytes.

Memory

There’s 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 8. 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.

Dirty Details

It’s important to note that on some implementations, some descriptors differ significantly from the model presented. Updates of descriptors of such types may modify some structures not encapsulated in VkDescriptorSet.

A possible example of such are SAMPLED_IMAGE and SAMPLER descriptor types on Nvidia hardware. Starting with NV50 (GeForce 8 series), all textures and samplers that are expected to be accessed are stored in two big arrays, referred to as texture image control (TIC) and texture sampler control (TSC) blocks, respectively 9 10. Switching TxCBs 11 12 has to happen when shaders aren’t running, so it is desirable to have as few TxCBs as possible to minimize switches and associated waiting. Descriptors SAMPLED_IMAGE and SAMPLER would then be indices into respective TxCB.

Future Directions

As of writing, descriptors are treated as special entities, separate from data. This is unfortunate for structures that wish to refer to the objects descriptors point to. A correspondence between some data (for example, an integer) and a descriptor has to be introduced, which has an impression of being unnecessary.

For some types of descriptors, this indirection can be avoided. For example, buffer device address replaces storage buffer descriptors with familiar pointers. The experimental VK_NVX_image_view_handle extension in turn addresses images and combined image-samplers. Although the interface assumes specific hardware, the assumption is easily relaxed by modifying vkGetImageViewHandleNVX to return multiple uint32_t values required to describe an object, allowing for straightforward implementations on other hardware. It is obvious how this could be extended to other types of descriptors that don’t require special treatment. Let’s hope this materializes at one point.


  1. https://gitlab.freedesktop.org/mesa/mesa/-/blob/04be7934df765eea0623360f748249870487baee/src/amd/vulkan/radv_descriptor_set.c#L982 ↩︎

  2. https://gitlab.freedesktop.org/mesa/mesa/-/blob/04be7934df765eea0623360f748249870487baee/src/amd/vulkan/radv_cmd_buffer.c#L2809 ↩︎

  3. https://gitlab.freedesktop.org/mesa/mesa/-/blob/04be7934df765eea0623360f748249870487baee/src/amd/vulkan/radv_image.c#L1675 ↩︎

  4. https://gitlab.freedesktop.org/mesa/mesa/-/blob/04be7934df765eea0623360f748249870487baee/src/amd/vulkan/radv_device.c#L7486 ↩︎

  5. https://gitlab.freedesktop.org/mesa/mesa/-/blob/04be7934df765eea0623360f748249870487baee/src/intel/vulkan/genX_state.c#L515 ↩︎

  6. https://gitlab.freedesktop.org/mesa/mesa/-/blob/04be7934df765eea0623360f748249870487baee/src/freedreno/vulkan/tu_device.c#L1857 ↩︎

  7. https://gitlab.freedesktop.org/mesa/mesa/-/blob/04be7934df765eea0623360f748249870487baee/src/amd/vulkan/radv_descriptor_set.c#L196 ↩︎

  8. https://gitlab.freedesktop.org/mesa/mesa/-/blob/04be7934df765eea0623360f748249870487baee/src/amd/vulkan/radv_descriptor_set.c#L721 ↩︎

  9. https://github.com/envytools/envytools/blob/39d90be897f41434d67277ebdf244d6bd419ecd9/rnndb/graph/g80_texture.xml ↩︎

  10. https://github.com/envytools/envytools/blob/39d90be897f41434d67277ebdf244d6bd419ecd9/rnndb/graph/gm200_texture.xml ↩︎

  11. https://gitlab.freedesktop.org/mesa/mesa/-/blob/94ed2ab3a117bdd8f4f4d0e888c4e6d0f99a7bfa/src/gallium/drivers/nouveau/nvc0/nvc0_screen.c#L1376 ↩︎

  12. https://gitlab.freedesktop.org/mesa/mesa/-/blob/94ed2ab3a117bdd8f4f4d0e888c4e6d0f99a7bfa/src/gallium/drivers/nouveau/nvc0/nvc0_screen.c#L1389 ↩︎