Vulkan descriptor heap

Изначально вся система работы с дескрипторами в Vulkan была довольно сложной и неудобной. Считалось, что она даст гибкость, но на практике быстро стало ясно, что это было довольно неудачным решением.

При этом дескрипторы это на самом деле это просто небольшие блоки памяти (обычно 16-64 байта). Проблема в том, что для разных GPU не только строение этих блоков, но и даже их размер оказались разными. Было несколько попыток облегчить работу с ресурсами - push descriptors и descriptor buffers.

По буферам дескрипторов есть статья на сайте (Расширение VK_EXT_descriptor_buffer и новый способ работы с дескрипторами в Vulkan) и готовый пример кода. Хотя сам подход был явно шагом в правильном направлении, но были и проблемы, в частности оказалось на GPU от NVidia это работает довольно неэффективно.

Более подробно о том, как на самом деле устроены дескрипторы на разных GPU /можно прочитать здесь:

Поэтому совсем недавно было предложено новое решение, похожее на descriptor buffers, но не основанное на нем и полностью его заменяющее - расширение VK_ext_descriptor_heap. Кроме того, из материалов Khronos следует, что именно в эту сторону и будет двигаться Vulkan в будущем.

Основная идея такая же как и в descriptor buffers - раз дескрипторы это просто блоки памяти, то давайте будем хранить их в специальных буферах, получивших название descriptor heap. Мы для командного буфера задаем эти буфера, причем задание буфера является довольно дорогой операцией, поэтому желательно сделать это один раз и больше не менять.

По аппаратным соображениям есть два типа таких буферов - отдельный буфер для сэмплеров и отдельный буфер для всего остального (ресурсы). При этом необходимость в объектах-сэмплерах (VkSampler) вообще пропадает, мы храним в соответствующем буфере те данные, по которым мы раньше строили объекты-сэмплеры.

Во втором буфере мы храним данные для буферов и текстур. Также пропадает необходимость использования объектов VkImageView - мы явно передаем необходимую информацию при записи в буфер. Обратите внимание, что в этом способе не поддерживается объект, состоящий сразу и из текстуры и из сэмплера - мы должны передавать их по-отдельности.

Если мы хотим поменять передаваемые ресурсы, то мы просто записываем новые данные в соответствующий буфер. Для этого введены специальные команды и необходимые структуры данных. Мы можем получить объем памяти, требуемый для различных дескрипторов, чтобы распределить пространство внутри буфера. И, кроме, того, внутри буфера нужно выделить некоторый зарезервированный объем для самого Vulkan. Требуемый объем для этого также можно получить.

Кроме того, введен новый механизм, позволяющий задавать (для конвейеров и shader objects) отображение (set, binding) в смещение внутри такого буфера. Таким образом можно легко использовать старые шейдеры, без необходимости их переписывания, но мы при этом задаем откуда внутри буфера, надо брать какой дескриптор (по set и binding).

Ниже приводятся основные шаги, которые нужно выполнить для использования данного расширения:

Обратите внимание, что descriptor set-ы (как и их пулы) вообще перестают быть нужны - про них можно забыть как про страшный сон. И это заметно облегчает нашу жизнь.

Как уже писалось выше, каждый дескриптор - это просто небольшой кусочек памяти, но при этом и его размер и что и как хранится внутри, полностью определяется драйвером. Мы можем (и должны) получить размеры для разных типов дескрипторов, но само их внутреннее устройство полностью скрыто от нас - для нас это просто кусочек памяти и у нас есть функции, которые в него записывают данные заданного дескриптора (или группы дескрипторов).

При этом нет взаимно-однозначного соответствия между дескрипторами и ресурсами. Так вы можете создать один большой буфер для всего и поделить его на части, каждая его часть будет задаваться своим дескриптором и восприниматься в шейдере как отдельный буфер.

Для uniform/storage буферов мы создаем VkBuffer. После чего мы используем vkGetBufferDeviceAddress для получения адреса буфера в памяти GPU и передаем диапазон памяти буфера (адрес и размер в байтах) как дескриптор в функцию vkWriteResourceDescriptorsEXT. Тем самым мы полностью задаем буфер для доступа к нему их шейдера.

С текстурами все несколько сложнее (но все равно проще чем было раньше). Мы используем функцию vkWriteResourceDescriptorsEXT для записи данных о текстуре. Как уже отмечалось для текстур нам теперь вообще не нужен VkImageView. Вместо этого мы передаем VkImageViewCreateInfo при вызове vkWriteResourceDescriptorsEXT.

Фактически целью расширения VK_EXT_descriptor_heap является предоставление приложению информации, где именно хранятся данные дескриптора, с тем чтобы можно было просто копировать данные по правильному адресу внутри буфера (heap).

Также когда вы используете descriptor heap, то вам больше не нужны не только множества дескрипторов (descriptor set), но их размещения (descriptor set layout).

Давайте рассмотрим это поподробнее.

Выделение буфера

При создании буфера (descriptor heap) мы должны задать флаги VK_BUFFER_USAGE_DESCRIPTOR_HEAP_BIT_EXT иVK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT в поле usage.

Для удобства мы мы будем использовать в качестве буферов экземпляры класса PersistentBuffer, тем самым мы сразу получим функционал по постоянному отображению в память CPU.

Размеры дескрипторов и зарезервированной области памяти

У каждого типа дескриптора есть свой размер в байтах (зависящий от GPU и драйвера), задаваемые в полях структуры VkPhysicalDeviceDescriptorHeapPropertiesEXT:

В Niagara (GitHub - zeux/niagara: A Vulkan renderer written from scratch on stream · GitHub) используется простейший подход - взять максимум из этих размеров и выделять память под дескрипторы по этому максимальному размеру.

Кроме того, есть некоторый объем места, которое нужно зарезервировать в буфере для драйвера, оно задается в следующих полях:

Кроме того, более подробную информацию об этом можно получить через функцию vkGetPhysicalDeviceDescriptorSizeEXT.

Запись дескриптора в буфер

Для записи дескриптора по заданному адресу в памяти CPU служат две функции - vkWriteResourceDescriptorsEXT и vkWriteSamplerDescriptorsEXT. Обычно в качестве адреса в памяти CPU выступает адрес отображенной (mapped) памяти буфера (heap) - это самый простой способ и мы будем далее пользоваться именно им.

VkResult vkWriteSamplerDescriptorsEXT (
    VkDevice                         device,
    uint32_t                         samplerCount,
    const VkSamplerCreateInfo*       pSamplers,
    const VkHostAddressRangeEXT*     pDescriptors );

VkResult vkWriteResourceDescriptorsEXT (
    VkDevice                           device,
    uint32_t                           resourceCount,
    const VkResourceDescriptorInfoEXT* pResources,
    const VkHostAddressRangeEXT*       pDescriptors );

В этих вызовах device это логическое устройство, для которого предназначены эти дескрипторы. Параметр samplerCount/resourceCount - это число дескрипторов, которые мы хотим записать за один раз. Через параметр pDescriptors мы передаем информацию о том, куда именно нужно записать эти дескрипторы. И параметр pResources/pSamplers задает массив информации о самих записываемых дескрипторах.

Место в памяти CPU, куда будет производится запись очередного дескриптора, задается при помощи структуры VkHostAddressRangeEXT. В ней поле address задает сам адрес, а поле size - размер диапазона (фактически размер дескриптора).

typedef struct VkHostAddressRangeEXT 
{
    void*     address;
    size_t    size;
} VkHostAddressRangeEXT;

Дескриптор с индексом i (pResources [i] или pSamplers [i]) будет записан в память по адресу pDescriptors [i].address.

Для задания ресурсов служат следующие структуры:

typedef struct VkResourceDescriptorInfoEXT 
{
    VkStructureType                sType;
    const void*                    pNext;
    VkDescriptorType               type;
    VkResourceDescriptorDataEXT    data;
} VkResourceDescriptorInfoEXT;

Для задания ресурса конкретного типа используется union из нескольких структур, каждая их которых соответствует определенному типу ресурса. Мы рассмотрим только два - буфер и текстуру (изображение). Первый задается через VkDeviceAddressRangeEXT, а второй - через VkImageDescriptorInfoEXT.

typedef union VkResourceDescriptorDataEXT 
{
    const VkImageDescriptorInfoEXT*          pImage;
    const VkTexelBufferDescriptorInfoEXT*    pTexelBuffer;
    const VkDeviceAddressRangeEXT*           pAddressRange;
    const VkTensorViewCreateInfoARM*         pTensorARM;
} VkResourceDescriptorDataEXT;
typedef struct VkImageDescriptorInfoEXT 
{
    VkStructureType                 sType;
    const void*                     pNext;
    const VkImageViewCreateInfo*    pView;
    VkImageLayout                   layout;
} VkImageDescriptorInfoEXT;
typedef struct VkDeviceAddressRangeEXT 
{
    VkDeviceAddress    address;
    VkDeviceSize       size;
} VkDeviceAddressRangeEXT;

Ниже приводится как это было сделано в Nanite - для этого есть три перегруженные функции. Каждая из них записывает свой вид дескриптора по заданному адресу внутри буфера (в отображенную память). Последние два параметра задает адрес в отображенной памяти и размер дескриптора (как уже отмечалось в Nanite берется один максимальный из всех трех).

void getDescriptor(VkDevice device, VkImage image, VkFormat format, uint32_t mipLevel, uint32_t levelCount, VkDescriptorType type, void* descriptor, size_t descriptorSize)
{
    VkImageAspectFlags aspectMask = (format == VK_FORMAT_D32_SFLOAT) ? VK_IMAGE_ASPECT_DEPTH_BIT : VK_IMAGE_ASPECT_COLOR_BIT;

    VkImageViewCreateInfo viewInfo = getImageViewInfo(image, format, mipLevel, levelCount);

    VkImageDescriptorInfoEXT imageInfo = { VK_STRUCTURE_TYPE_IMAGE_DESCRIPTOR_INFO_EXT };
    imageInfo.pView = &viewInfo;
    imageInfo.layout = VK_IMAGE_LAYOUT_GENERAL;

    VkResourceDescriptorInfoEXT resourceInfo = { VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT };
    resourceInfo.type = type;
    resourceInfo.data.pImage = &imageInfo;

    VkHostAddressRangeEXT descriptorRange = { descriptor, descriptorSize };

    vkWriteResourceDescriptorsEXT(device, 1, &resourceInfo, &descriptorRange);
}

void getDescriptor(VkDevice device, VkDeviceAddress address, VkDeviceSize size, VkDescriptorType type, void* descriptor, size_t descriptorSize)
{
    VkDeviceAddressRangeEXT addressRange = { address, size };

    VkResourceDescriptorInfoEXT resourceInfo = { VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT };
    resourceInfo.type = type;
    resourceInfo.data.pAddressRange = &addressRange;

    VkHostAddressRangeEXT descriptorRange = { descriptor, descriptorSize };

    vkWriteResourceDescriptorsEXT(device, 1, &resourceInfo, &descriptorRange);
}

void getDescriptor(VkDevice device, VkFilter filter, VkSamplerMipmapMode mipmapMode, VkSamplerAddressMode addressMode, VkSamplerReductionModeEXT reductionMode, void* descriptor, size_t descriptorSize)
{
    VkSamplerCreateInfo createInfo = getSamplerInfo(filter, mipmapMode, addressMode);

    VkSamplerReductionModeCreateInfoEXT reductionInfo = { VK_STRUCTURE_TYPE_SAMPLER_REDUCTION_MODE_CREATE_INFO_EXT };
    reductionInfo.reductionMode = reductionMode;

    if (reductionMode != VK_SAMPLER_REDUCTION_MODE_WEIGHTED_AVERAGE_EXT)
        createInfo.pNext = &reductionInfo;

    VkHostAddressRangeEXT descriptorRange = { descriptor, descriptorSize };

    vkWriteSamplerDescriptorsEXT(device, 1, &createInfo, &descriptorRange);
}

Подключение к командному буферу

Чтобы использовать descriptor heap как источник дескрипторов нужно подключить его к командному буферу. Для этого есть две почти одинаковых функции (по одной для каждого типа heap) - vkCmdBindSamplerHeapEXT и vkCmdBindResourceHeapEXT.

void vkCmdBindSamplerHeapEXT (
    VkCommandBuffer           commandBuffer,
    const VkBindHeapInfoEXT*  pBindInfo );

void vkCmdBindResourceHeapEXT (
    VkCommandBuffer           commandBuffer,
    const VkBindHeapInfoEXT*  pBindInfo );

Каждая из них получает на вход сам командный буфер и информацию об области буфера, используемой в качестве heap.

typedef struct VkBindHeapInfoEXT 
{
    VkStructureType            sType;
    const void*                pNext;
    VkDeviceAddressRangeEXT    heapRange;
    VkDeviceSize               reservedRangeOffset;
    VkDeviceSize               reservedRangeSize;
} VkBindHeapInfoEXT;

Поле heapRange задает подключаемую область буфера. При этом также есть и зарезервированная часть буфера, которую также нужно задать - она будет использоваться драйвером для своих целей. Мы можем сами выбрать ее место внутри буфера и здесь мы должны ее передать. Для этого служат поля - minResourceHeapReservedRange и minSamplerHeapReservedRange.

Обратите внимание, что эти вызовы очень дорогостоящие и не надо часто переключаться между различными частями heap.

    // Bind the heap containing resources (buffers and images)
VkBindHeapInfoEXT bindHeapInfoRes{
    .sType = VK_STRUCTURE_TYPE_BIND_HEAP_INFO_EXT,
    .heapRange{
        .address = descriptorHeapResources.deviceAddress,
        .size = descriptorHeapResources.size
    },
    // Put the reserved range after our descriptors, simplifies some calculations
    .reservedRangeOffset = descriptorHeapResources.size - descriptorHeapProperties.minResourceHeapReservedRange,
    .reservedRangeSize = descriptorHeapProperties.minResourceHeapReservedRange,
};
vkCmdBindResourceHeapEXT(cmdBuffer, &bindHeapInfoRes);

    // Bind the heap containing samplers
VkBindHeapInfoEXT bindHeapInfoSamplers{
    .sType = VK_STRUCTURE_TYPE_BIND_HEAP_INFO_EXT,
    .heapRange{
        .address = descriptorHeapSamplers.deviceAddress,
        .size = descriptorHeapSamplers.size
    },
    // Put the reserved range after our descriptors, simplifies some calculations
    .reservedRangeOffset = descriptorHeapSamplers.size - descriptorHeapProperties.minSamplerHeapReservedRange,
    .reservedRangeSize = descriptorHeapProperties.minSamplerHeapReservedRange
    };
vkCmdBindSamplerHeapEXT(cmdBuffer, &bindHeapInfoSamplers);

Задание отображение (set, binding) на адреса внутри буфера

Соответствие между (set. binding) и местом внутри буфера задается отдельно для каждого конвейера (VkPipeline) или каждого шейдерного объекта (VkShaderObjectEXT). Схема задания одинаковая, но мы рассмотрим ее на примере шейдерных объектов.

При его создании мы выставляем поле pNext на VkShaderDescriptorSetAndBindingMappingInfoEXT, внутри него живет массив VkDescriptorSetAndBindingMappingEXT.

typedef struct VkShaderDescriptorSetAndBindingMappingInfoEXT 
{
    VkStructureType                               sType;
    const void*                                   pNext;
    uint32_t                                      mappingCount;
    const VkDescriptorSetAndBindingMappingEXT*    pMappings;
} VkShaderDescriptorSetAndBindingMappingInfoEXT;
typedef struct VkDescriptorSetAndBindingMappingEXT 
{
    VkStructureType                     sType;
    const void*                         pNext;
    uint32_t                            descriptorSet;
    uint32_t                            firstBinding;
    uint32_t                            bindingCount;
    VkSpirvResourceTypeFlagsEXT         resourceMask;
    VkDescriptorMappingSourceEXT        source;
    VkDescriptorMappingSourceDataEXT    sourceData;
} VkDescriptorSetAndBindingMappingEXT;

Есть несколько различных режимов задания соответствия и мы рассмотрим только самый простой. При этом режиме resourceMask равен VK_SPIRV_RESOURCE_TYPE_ALL_EXT; и source равен VK_DESCRIPTOR_MAPPING_SOURCE_HEAP_WITH_CONSTANT_OFFSET_EXT. А место ресурса будет задаваться полем constantOffset в sourceData.

typedef union VkDescriptorMappingSourceDataEXT 
{
    VkDescriptorMappingSourceConstantOffsetEXT        constantOffset;
    VkDescriptorMappingSourcePushIndexEXT             pushIndex;
    VkDescriptorMappingSourceIndirectIndexEXT         indirectIndex;
    VkDescriptorMappingSourceIndirectIndexArrayEXT    indirectIndexArray;
    VkDescriptorMappingSourceHeapDataEXT              heapData;
    uint32_t                                          pushDataOffset;
    uint32_t                                          pushAddressOffset;
    VkDescriptorMappingSourceIndirectAddressEXT       indirectAddress;
    VkDescriptorMappingSourceShaderRecordIndexEXT     shaderRecordIndex;
    uint32_t                                          shaderRecordDataOffset;
    uint32_t                                          shaderRecordAddressOffset;
} VkDescriptorMappingSourceDataEXT;
typedef struct VkDescriptorMappingSourceConstantOffsetEXT 
{
    uint32_t                      heapOffset;
    uint32_t                      heapArrayStride;
    const VkSamplerCreateInfo*    pEmbeddedSampler;
    uint32_t                      samplerHeapOffset;
    uint32_t                      samplerHeapArrayStride;
} VkDescriptorMappingSourceConstantOffsetEXT;

Здесь поле heapOffset задает смещение в байтах от начала heap. Поле heapoArrayStride задает шаг (расстояние) в байтах для элементов массивов.

Поле samplerHeapOffset используется при задании в качестве ресурса совмещенную текстуру + сэмплер, через него задается положение соответствующего сэмплера. И поле samplerHeapArrayStride используется при работе с массивами.

Для расчета смещения внутри heap используется следующая формула

shaderIndex = (Binding - firstBinding) + arrayIndex offset = heapOffset + (shaderIndex * heapArrayStride)

Здесь Binding соответствует binding в шейдере, arrayIndex это индекс в массиве, если соответствующий дескриптор в шейдере объявлен как массив.

Ниже приводится фрагмент кода, для использования descriptor heap при использовании описанной выше техники для задания связи между ресурсами в шейдерах и исходным кодом на С++.

void    createHeaps ( int nResources, int nSamplers )
{ 
    descriptorSize = std::max ( descriptorHeapProperties.samplerDescriptorSize, descriptorHeapProperties.bufferDescriptorSize );
    descriptorSize = std::max ( descriptorSize, descriptorHeapProperties.imageDescriptorSize );
    numResources   = nResources;
    numSamplers    = nSamplers;

    VkDeviceSize    samplerHeapSize  = descriptorHeapProperties.minSamplerHeapReservedRange  + nSamplers  * descriptorSize;
    VkDeviceSize    resourceHeapSize = descriptorHeapProperties.minResourceHeapReservedRange + nResources * descriptorSize;
    uint32_t        usage            = VK_BUFFER_USAGE_DESCRIPTOR_HEAP_BIT_EXT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT;

    resourcesBuffer.create ( device, resourceHeapSize, usage, Buffer::hostWrite, descriptorHeapProperties.resourceHeapAlignment );
    samplersBuffer.create  ( device, samplerHeapSize,  usage, Buffer::hostWrite, descriptorHeapProperties.samplerHeapAlignment  );
}

void    addMapping ( uint32_t set, uint32_t binding, VkDeviceSize heapOffset, uint32_t count = 1, VkDeviceSize arrayStride = 0, uint32_t mask = VK_SPIRV_RESOURCE_TYPE_ALL_EXT )
{
    VkDescriptorSetAndBindingMappingEXT m { VK_STRUCTURE_TYPE_DESCRIPTOR_SET_AND_BINDING_MAPPING_EXT };

    m.descriptorSet                             = set;
    m.firstBinding                              = binding;
    m.bindingCount                              = count;
    m.resourceMask                              = mask;
    m.source                                    = VK_DESCRIPTOR_MAPPING_SOURCE_HEAP_WITH_CONSTANT_OFFSET_EXT;
    m.sourceData.constantOffset.heapOffset      = uint32_t ( heapOffset  );
    m.sourceData.constantOffset.heapArrayStride = uint32_t ( arrayStride );

    mappings.push_back ( m );

    mappingInfo.sType        = VK_STRUCTURE_TYPE_SHADER_DESCRIPTOR_SET_AND_BINDING_MAPPING_INFO_EXT;
    mappingInfo.mappingCount = uint32_t ( mappings.size () );
    mappingInfo.pMappings    = mappings.data ();
}

void    writeDescriptor ( uint32_t index, Texture& tex )
{
    uint8_t           * descriptor = resourceHeapReservedRange + index * descriptorSize + (uint8_t *) resourcesBuffer.getPtr ();
    auto                format     = tex.getImage ().getFormat    ();
    auto                levelCount = tex.getImage ().getMipLevels ();
    VkImageAspectFlags  aspectMask = (format == VK_FORMAT_D32_SFLOAT) ? VK_IMAGE_ASPECT_DEPTH_BIT : VK_IMAGE_ASPECT_COLOR_BIT;
    bool                depth      = (aspectMask == VK_IMAGE_ASPECT_DEPTH_BIT);

    VkImageViewCreateInfo       viewInfo  = getImageViewInfo ( tex.getImage ().getHandle (), format, depth, 0, levelCount );
    VkImageDescriptorInfoEXT    imageInfo = { VK_STRUCTURE_TYPE_IMAGE_DESCRIPTOR_INFO_EXT };

    imageInfo.pView  = &viewInfo;
    imageInfo.layout = VK_IMAGE_LAYOUT_GENERAL;

    VkResourceDescriptorInfoEXT resourceInfo = { VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT };

    resourceInfo.type        = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE;
    resourceInfo.data.pImage = &imageInfo;

    VkHostAddressRangeEXT descriptorRange = { descriptor, descriptorSize };

    vkWriteResourceDescriptorsEXT ( device.getDevice (), 1, &resourceInfo, &descriptorRange );
}

void    writeDescriptor ( uint32_t index, Buffer& buf, VkDeviceSize size, VkDeviceSize offset = 0 )
{
    VkDeviceAddressRangeEXT     addressRange = { buf.getDeviceAddress () + offset, size };
    VkResourceDescriptorInfoEXT resourceInfo = { VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT };

    resourceInfo.type               = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
    resourceInfo.data.pAddressRange = &addressRange;

    uint8_t             * descriptor      = resourceHeapReservedRange + index * descriptorSize + (uint8_t *) resourcesBuffer.getPtr ();
    VkHostAddressRangeEXT descriptorRange = { descriptor, descriptorSize };

    vkWriteResourceDescriptorsEXT ( device.getDevice (), 1, &resourceInfo, &descriptorRange );
}

void writeDescriptor ( uint32_t index, 
                       VkFilter filter                         = VK_FILTER_LINEAR, 
                       VkSamplerMipmapMode mipmapMode          = VK_SAMPLER_MIPMAP_MODE_LINEAR, 
                       VkSamplerAddressMode addressMode        = VK_SAMPLER_ADDRESS_MODE_REPEAT, 
                       VkSamplerReductionModeEXT reductionMode = VK_SAMPLER_REDUCTION_MODE_WEIGHTED_AVERAGE_EXT )
{
    uint8_t                           * descriptor    = samplerHeapReservedRange + index * descriptorSize + (uint8_t *) samplersBuffer.getPtr ();
    VkSamplerCreateInfo                 createInfo    = getSamplerInfo ( filter, mipmapMode, addressMode );
    VkSamplerReductionModeCreateInfoEXT reductionInfo = { VK_STRUCTURE_TYPE_SAMPLER_REDUCTION_MODE_CREATE_INFO_EXT };

    reductionInfo.reductionMode = reductionMode;

    if ( reductionMode != VK_SAMPLER_REDUCTION_MODE_WEIGHTED_AVERAGE_EXT )
        createInfo.pNext = &reductionInfo;

    VkHostAddressRangeEXT descriptorRange = { descriptor, descriptorSize };

    vkWriteSamplerDescriptorsEXT ( device.getDevice (), 1, &createInfo, &descriptorRange );
}

Использование untyped-bindings

Есть еще один подход, для установления соответствия между ресурсами и приложением - использование untyped bindings. Здесь подход крайне прост - мы добавляем описатель layout (descriptor_heap) для используемых шейдеров.

layout (descriptor_heap) uniform texture2D images [];
layout (descriptor_heap) uniform sampler samplers  [];

layout (descriptor_heap) uniform Camera 
{
    mat4    proj;
    mat4    view;
} camera [];

layout (descriptor_heap) uniform Model 
{
    mat4    model;
} model [];

При этом мы рассматриваем ресурсы как массивы, в которые мы индексируемся. Важно понимать, что при этом все ресурсы живут в общих буферах.

То есть все сэмплеры лежат в одном буфере. Точно так же все буфера и текстуры лежат тоже в одном буфере (resource heap). Важно понимать что все массивы буферов и текстур считаются лежащими в resource heap с самого начала.

Для примера выше это значит, что описания images, camera и model на самом деле считаются к одной и той же области resource heap, т.е. данные для ресурсов images [0], camera [0] и model [0] ссылаются на начала resource heap.

Для разработчика это означает, что при записи их в resource heap нужно очень аккуратно записывать описатели ресурсов. В примере ниже мы сначала записываем в resource heap описатели буферов (сначала один буфер Camera, а потом три буфера Model, а потом, с правильным смещением, записываем описатели текстур).

На что здесь нужно обратить внимание, что довольно часто размер дескрипторов для буфера и текстуры часто различаются (например у меня 16 и 32 байта соответственно). Это значит, что нужно размещать текстуры с правильным выравниванием, которое отличается для буферов и текстур.

В этом коде этим смещением является imageHeapIndexOffset (смещение в байтах для первой текстуры от начала heap) и imageHeapOffset (индекс первой текстуры)

DescriptorHeapWindow ( int w, int h, const std::string& t, bool depth, DevicePolicy * p, VkPhysicalDeviceDescriptorHeapPropertiesEXT& descHeapProps ) : VulkanWindow ( w, h, t, depth, p ), descriptorHeapProperties ( descHeapProps )
{
    loadExtensions ();
    setController  ( new RotateController ( this, glm::vec3 ( 2.0f, 2.0f, 2.0f ) ) );

    mesh = std::unique_ptr<Mesh> ( createKnot ( device, 0.2f, 0.13f, 120, 30 ) );

        // prepare buffers and textures
    createUniformBuffers ();
    createTextures       ();

        // create heap buffers
    createHeaps ( 1 + int ( modelUbos.size () ), 6, 1 );

        // instead of uploadBuffer we store buffer to descriptor heap
        // buffers [0] = camera
    writeDescriptor ( 0, cameraUbo, cameraUbo.getSize () );

        // upload other Ubos and textures
        // buffers  [1..] = modelUbo
    for ( int i = 0; i < modelUbos.size (); i++ )
        writeDescriptor ( 1 + i, modelUbos [i], modelUbos [i].getSize () );

        // textures [imageHeapIndexOffset + 0..] = texture
    for ( int i = 0; i < 6; i++ )
        writeDescriptor ( i, textures [i] );

        // upload sampler
        // samplers [0] = default sampler
    writeDescriptor ( 0 );

        // create pipeline
    createPipelines ();
}

void    createHeaps ( int nBuffers, int nTextures, int nSamplers )
{ 
    numBuffers  = nBuffers;
    numTextures = nTextures;
    numSamplers = nSamplers;

    bufferDescriptorSize      = aligned ( descriptorHeapProperties.bufferDescriptorSize,  descriptorHeapProperties.bufferDescriptorAlignment  );
    imageDescriptorSize       = aligned ( descriptorHeapProperties.imageDescriptorSize,   descriptorHeapProperties.imageDescriptorAlignment   );
    samplerDescriptorSize     = aligned ( descriptorHeapProperties.samplerDescriptorSize, descriptorHeapProperties.samplerDescriptorAlignment );
    imageHeapOffset           = aligned ( numBuffers * bufferDescriptorSize,              descriptorHeapProperties.imageDescriptorAlignment   );
    imageHeapIndexOffset      = static_cast<int32_t> ( imageHeapOffset / imageDescriptorSize );
    samplersHeapSize          = aligned ( descriptorHeapProperties.minSamplerHeapReservedRange  + numSamplers  * samplerDescriptorSize,                descriptorHeapProperties.samplerDescriptorAlignment );
    resourcesHeapSize         = aligned ( descriptorHeapProperties.minResourceHeapReservedRange + imageHeapOffset + numTextures * imageDescriptorSize, descriptorHeapProperties.imageDescriptorAlignment   );

    uint32_t    usage = VK_BUFFER_USAGE_DESCRIPTOR_HEAP_BIT_EXT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT;

    resourcesBuffer.create ( device, resourcesHeapSize, usage, Buffer::hostWrite, descriptorHeapProperties.resourceHeapAlignment );
    samplersBuffer.create  ( device, samplersHeapSize,  usage, Buffer::hostWrite, descriptorHeapProperties.samplerHeapAlignment  );
}

void  * textureHeapPtr ( uint32_t index ) const
{ 
        // use built-in offset so textures not override buffers in resource heap
    return imageHeapOffset + index * imageDescriptorSize + (byte *) resourcesBuffer.getPtr ();
}

void  * bufferHeapPtr ( uint32_t index ) const
{
    return index * bufferDescriptorSize + (byte *) resourcesBuffer.getPtr ();
}

void * samplerHeapPtr ( uint32_t index ) const
{
        // samplers are in a separate heap
    return index * samplerDescriptorSize + (byte *) samplersBuffer.getPtr ();
}

void    writeDescriptor ( uint32_t index, Texture& tex, bool depth = false )
{
    void  * descriptor = textureHeapPtr ( index );
    auto    levelCount = tex.getImage ().getMipLevels ();

    VkImageViewCreateInfo       viewInfo  = getImageViewInfo ( tex.getImage ().getHandle (), tex.getImage ().getFormat (), depth, 0, levelCount );
    VkImageDescriptorInfoEXT    imageInfo = { VK_STRUCTURE_TYPE_IMAGE_DESCRIPTOR_INFO_EXT };

    imageInfo.pView  = &viewInfo;
    imageInfo.layout = VK_IMAGE_LAYOUT_GENERAL;

    VkHostAddressRangeEXT       descriptorRange = { descriptor, imageDescriptorSize };
    VkResourceDescriptorInfoEXT resourceInfo    = { VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT };

    resourceInfo.type        = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE;
    resourceInfo.data.pImage = &imageInfo;

    vkWriteResourceDescriptorsEXT ( device.getDevice (), 1, &resourceInfo, &descriptorRange );
}

void    writeDescriptor ( uint32_t index, Buffer& buf, VkDeviceSize size, VkDeviceSize offset = 0, VkDescriptorType type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER )
{
    VkDeviceAddressRangeEXT     addressRange = { buf.getDeviceAddress () + offset, size };
    VkResourceDescriptorInfoEXT resourceInfo = { VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT };

    resourceInfo.type               = type;
    resourceInfo.data.pAddressRange = &addressRange;

    void                * descriptor      = bufferHeapPtr ( index );
    VkHostAddressRangeEXT descriptorRange = { descriptor, bufferDescriptorSize };

    vkWriteResourceDescriptorsEXT ( device.getDevice (), 1, &resourceInfo, &descriptorRange );
}

void writeDescriptor ( uint32_t index, 
           VkFilter filter                         = VK_FILTER_LINEAR, 
           VkSamplerMipmapMode mipmapMode          = VK_SAMPLER_MIPMAP_MODE_LINEAR, 
           VkSamplerAddressMode addressMode        = VK_SAMPLER_ADDRESS_MODE_REPEAT, 
           VkSamplerReductionModeEXT reductionMode = VK_SAMPLER_REDUCTION_MODE_WEIGHTED_AVERAGE_EXT )
{
    void                              * descriptor    = samplerHeapPtr ( index );
    VkSamplerCreateInfo                 createInfo    = getSamplerInfo ( filter, mipmapMode, addressMode );
    VkSamplerReductionModeCreateInfoEXT reductionInfo = { VK_STRUCTURE_TYPE_SAMPLER_REDUCTION_MODE_CREATE_INFO_EXT };

    reductionInfo.reductionMode = reductionMode;

    if ( reductionMode != VK_SAMPLER_REDUCTION_MODE_WEIGHTED_AVERAGE_EXT )
        createInfo.pNext = &reductionInfo;

    VkHostAddressRangeEXT descriptorRange = { descriptor, samplerDescriptorSize };

    vkWriteSamplerDescriptorsEXT ( device.getDevice (), 1, &createInfo, &descriptorRange );
}

А вот как выглядят обращение к ресурсам из шейдеров. Обратите внимание, что при обращении к буферам, к буферам моделей

#version 450

#extension GL_EXT_descriptor_heap: require
#extension GL_EXT_nonuniform_qualifier : require

layout ( location = 0 ) in vec3 pos;
layout ( location = 1 ) in vec2 texCoord;

layout ( push_constant ) uniform PushConsts 
{
    int modelIndex;
    int frameIndex;
    int imageHeapIndexOffset;
} pushConsts;

layout (descriptor_heap) uniform Camera 
{
    mat4    proj;
    mat4    view;
} camera [];

layout (descriptor_heap) uniform Model 
{
    mat4    model;
} model [];

layout ( location = 0 ) out vec2 tex;

void main() 
{
    gl_Position  = camera [0].proj * camera [0].view * model [1 + pushConsts.modelIndex].model * vec4 ( pos, 1.0 );

    tex          = texCoord;
}
#version 450

#extension GL_EXT_descriptor_heap: require
#extension GL_EXT_nonuniform_qualifier : require

layout (descriptor_heap) uniform texture2D images   [];
layout (descriptor_heap) uniform sampler   samplers [];

layout ( location = 0 ) in  vec2 tex;
layout ( location = 0 ) out vec4 color;

layout ( push_constant ) uniform PushConsts 
{
    int modelIndex;
    int frameIndex;
    int imageHeapIndexOffset;   // смещение для первой текстуры
} pushConsts;


void main() 
{
    color = texture ( sampler2D ( images [pushConsts.imageHeapIndexOffset + pushConsts.modelIndex], samplers [0] ), tex * vec2 ( 1, 6 ) );
}

Полный код примеров на использование descriptor heap можно найти на github по адресу https://github.com/steps3d/vulkan-with-classes.git