steps3D - Tutorials - Расширение VK_EXT_descriptor_buffer и новый способ работы с дескрипторами в Vulkan

Расширение VK_EXT_descriptor_buffer и новый способ работы с дескрипторами в Vulkan

Стандартным способом передачи различных ресурсов (дескрипторов - буферов и текстур) в шейдеры в Vulkan является использование descriptor set'ов. При использовании данного способа мы должны сперва создать descriptor pool, потом мы из него выделяем нужные нам descriptor set'ы через vkAllocateDescriptorSets, далее мы записываем в них конкретные ресурсы через vkUpdateDesriptorSets и потом передаем их в шейдер через vkCmdBindDescriptorSets.

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

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

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

Для подключения данного расширения мы создаем экземпляр структуры VkPhysicalDeviceDescriptorBufferFeaturesEXT, выставляем у нее поле descriptorBuffer в VK_TRUE и подключаем к списку требуемых возможностей (features), передаваемому при создании логического устройства.

DevicePolicy    policy;

    // we require VK_EXT_DESCRIPTOR_BUFFER_EXTENSION_NAME device extension
policy.addDeviceExtension ( VK_EXT_DESCRIPTOR_BUFFER_EXTENSION_NAME );

VkPhysicalDeviceDescriptorBufferFeaturesEXT   descriptorBufferFeatures     = { VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_BUFFER_FEATURES_EXT   };
VkPhysicalDeviceDescriptorBufferPropertiesEXT descriptorBufferProperties   = { VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_BUFFER_PROPERTIES_EXT };

descriptorBufferFeatures.descriptorBuffer = VK_TRUE;

policy.addFeatures   ( &descriptorBufferFeatures    );
policy.addProperties ( &descriptorBufferProperties  );

Далее нужно получить для всех дескрипторов это требуемое выравнивание в буфере. Вводится новая структура VkPhysicalDeviceDescriptorBufferPropertiesEXT (подключаемое через поле pNext к VkPhysicalDeviceProeprties2KHR), содержащая информацию о поддержке буферов дескрипторов. За выравнивание в этой структуре отвечает поле descriptorBufferOffsetAlignment См. код выше).

Данное расширение различает два типа дескрипторов - дескрипторы буферов (UBO, SSBO) и дескрипторы текстур (изображений, сэмплеров). Соответственно эти дескрипторы должны размещаться в разных буферах. Буфера дескрипторов создаются стандартным путем, только необходимо добавить обязательный флаг VK_BUFFER_USAGE_RESOURCE_DESCRIPTOR_BUFFER_BIT_EXT в поле usage. Если буфер будет содержать дескрипторы текстур, то также необходимо добавить еще и флаг VK_BUFFER_USAGE_SAMPLER_DESCRIPTOR_BUFFER_BIT_EXT. Для непосредственного задания дескрипторов служит тип VkDescriptorDataEXT, представляющий собой просто union из указателей на структуры для конкретных типов ресурсов.

void    createDescriptorBuffer ( PersistentBuffer& buf, const DescSetLayout& descLayout, bool isBuffer, int sizeMultiplier, 
                                 uint32_t binding, VkDeviceSize& size, VkDeviceSize& offset )
{
        // get size & offset
    vkGetDescriptorSetLayoutSizeEXT          ( device.getDevice (), descLayout.getHandle (), &size );
    vkGetDescriptorSetLayoutBindingOffsetEXT ( device.getDevice (), descLayout.getHandle (), binding, &offset );

        // force size alignment
    VkDeviceSize allocSize = sizeMultiplier * alignedSize<VkDeviceSize> ( size, descriptorBufferProperties.descriptorBufferOffsetAlignment );

    uint32_t    usage;

    if ( isBuffer )
        usage = VK_BUFFER_USAGE_RESOURCE_DESCRIPTOR_BUFFER_BIT_EXT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT;
    else
        usage = VK_BUFFER_USAGE_RESOURCE_DESCRIPTOR_BUFFER_BIT_EXT | VK_BUFFER_USAGE_SAMPLER_DESCRIPTOR_BUFFER_BIT_EXT;

    buf.create ( device, allocSize, usage, Buffer::hostWrite );
}

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

void vkGetDescriptorSetLayoutSizeEXT (
    VkDevice                                    device,
    VkDescriptorSetLayout                       layout,
    VkDeviceSize*                               pLayoutSizeInBytes );
    
void vkGetDescriptorSetLayoutBindingOffsetEXT (
    VkDevice                                    device,
    VkDescriptorSetLayout                       layout,
    uint32_t                                    binding,
    VkDeviceSize*                               pOffset );

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

void vkGetDescriptorEXT (
    VkDevice                                    device,
    const VkDescriptorGetInfoEXT*               pDescriptorInfo,
    size_t                                      dataSize,
    void*                                       pDescriptor );

Здесь параметр pDescriptor это указатель на нужное место уже созданного буфера дескрипторов, куда и будет производиться запись. В dataSize передается размер дескриптора в байтах, а через pDescriptorInfo передается указатель на VkDescriptorGetInfoEXT, которая и задает параметры дескриптора.

typedef struct VkDescriptorGetInfoEXT
{
    VkStructureType        sType;
    const void*            pNext;
    VkDescriptorType       type;
    VkDescriptorDataEXT    data;
} VkDescriptorGetInfoEXT;

Поле type задает тип дескриптора и принимает стандартные значения для типа дескриптора за исключением следующих значений - VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC и VK_DESCRIPTOR_TYPE_INLINE_UNIFORM_BLOCK.

typedef union VkDescriptorDataEXT
 {
    const VkSampler*                     pSampler;
    const VkDescriptorImageInfo*         pCombinedImageSampler;
    const VkDescriptorImageInfo*         pInputAttachmentImage;
    const VkDescriptorImageInfo*         pSampledImage;
    const VkDescriptorImageInfo*         pStorageImage;
    const VkDescriptorAddressInfoEXT*    pUniformTexelBuffer;
    const VkDescriptorAddressInfoEXT*    pStorageTexelBuffer;
    const VkDescriptorAddressInfoEXT*    pUniformBuffer;
    const VkDescriptorAddressInfoEXT*    pStorageBuffer;
    VkDeviceAddress                      accelerationStructure;
} VkDescriptorDataEXT;

Если параметр type задает буфер, то соответствующее поле в data должно быть указателем на структуру VkDescriptorAddressInfoEXT, а если он задает текстуру/сэмплер, то соответствующее поле в data указывает на структуру VkDescriptorImageInfo.

typedef struct VkDescriptorImageInfo
{
    VkSampler        sampler;
    VkImageView      imageView;
    VkImageLayout    imageLayout;
} VkDescriptorImageInfo;
 
typedef struct VkDescriptorAddressInfoEXT
{
    VkStructureType    sType;
    void*              pNext;
    VkDeviceAddress    address;
    VkDeviceSize       range;
    VkFormat           format;
} VkDescriptorAddressInfoEXT;

Если посмотреть на то, как задаются буфера (UBO, SSBO), то видно, что поскольку они задаются как область буфера, то мы можем в одном большом буфере разместить сразу много "виртуальный" буферов. При этом каждый такой "виртуальный" буфер будет иметь свое смещение и размер внутри основного буфера.

Также видно текстуру мы задаем набором (sampler, view, memoryLayout), а для задания буфера мы передаем адрес начала и длину участка в байтах.

Теперь нам нужно подключить буфер (или буфера) дескрипторов к командному буферу при помощи команды vkCmdBindDescriptorBuffersEXT:

void vkCmdBindDescriptorBuffersEXT (
    VkCommandBuffer                             commandBuffer,
    uint32_t                                    bufferCount,
    const VkDescriptorBufferBindingInfoEXT*     pBindingInfos );

Мы одной командой можем подключить сразу много буферов, задав их все в массиве структур VkDescriptorBufferBindingInfoEXT. Эта команда задает какие именно буфера дескрипторов мы собираемся использовать далее. Но сами смещения для буфер мы будем задавать позже.

typedef struct VkDescriptorBufferBindingInfoEXT
{
    VkStructureType       sType;
    void*                 pNext;
    VkDeviceAddress       address;
    VkBufferUsageFlags    usage;
} VkDescriptorBufferBindingInfoEXT;

Команда vkCmdBindDescriptorBuffersEXT просто задает набор буферов дескрипторов, но не их привязку к конкретным множествам дескрипторов. Обратите внимание что данную команду не рекомендуется использовать часто. Для того, чтобы задать как конкретное множество дескрипторов отображается внутрь буферов дескрипторов служит еще однав команда vkCmdSetDescriptorBufferOffsetsEXT.

void vkCmdSetDescriptorBufferOffsetsEXT(
    VkCommandBuffer                             commandBuffer,
    VkPipelineBindPoint                         pipelineBindPoint,
    VkPipelineLayout                            layout,
    uint32_t                                    firstSet,
    uint32_t                                    setCount,
    const uint32_t*                             pBufferIndices,
    const VkDeviceSize*                         pOffsets);

Эта команда задает как множества дескрипторов с номерами от firstSet до firstSet + setCount - 1 должны отображаться внутрь подключенных ранее буферов дескрипторов. Для каждого множества дескрипторов firstSet + i в pBufferIndices[i] храниться индекс буфера в массиве буферов переданных ранее через команду vkCmdBindDescriptorBuffersEXT. В pOffsets [i] задается смещение в байтах от начала буфера. Таким образом мы можем внутри одного буфера дескрипторов задать сразу много различных множеств дескрипторов и потом для перехода между множествами дескрипторов просто задавать новые смещения.

for ( size_t i = 0; i < commandBuffers.size(); i++ )
{
    commandBuffers [i]
        .begin             ()
        .beginRenderPass   ( RenderPassInfo ( renderPass ).framebuffer ( framebuffers [i] )
                                 .extent ( swapChain.getExtent ().width, swapChain.getExtent ().height )
                                 .clearColor (0,0,0,1).clearDepthStencil () )
        .pipeline          ( pipeline );

        VkDeviceSize    bufferOffset     = 0;
        uint32_t        bufferIndexUbo   = 0;
        uint32_t        bufferIndexImage = 1;

            // set descriptor buffer bindings to our descriptor buffers
        vkCmdBindDescriptorBuffersEXT ( commandBuffers [i].getHandle (), 2, bindings );

            // camera ubo (set 0)
        vkCmdSetDescriptorBufferOffsetsEXT ( commandBuffers [i].getHandle (), VK_PIPELINE_BIND_POINT_GRAPHICS, 
                                             pipeline.getLayout (), 0, 1, &bufferIndexUbo, &bufferOffset );

        for ( int j = 0; j < 3; j++ )
        {
                // model ubo (set 1)
            bufferOffset = (j + 1) * buffersDescriptorSize;
            vkCmdSetDescriptorBufferOffsetsEXT ( commandBuffers [i].getHandle (), VK_PIPELINE_BIND_POINT_GRAPHICS, 
                                                 pipeline.getLayout (), 1, 1, &bufferIndexUbo, &bufferOffset );

                // texture (set 2)
            bufferOffset = j * texturesDescriptorSize;
            vkCmdSetDescriptorBufferOffsetsEXT ( commandBuffers [i].getHandle (), VK_PIPELINE_BIND_POINT_GRAPHICS, 
                                                 pipeline.getLayout (), 2, 1, &bufferIndexImage, &bufferOffset );
                
                // render mesh
            commandBuffers [i].render ( mesh.get () );
        }
        commandBuffers [i].end ();
}

В качестве примера давайте рассмотрим случай, когда у нас выводится три одинаковых объекта (тора в нашем случае), причем каждый из них обладает своим UBO и своей текстурой. Кроме того, также есть еще и один глобальный UBO.

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

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

Также удобно вынести запись в буфер дескрипторов для буферов и текстур в отдельные методы:

                  // set entry index in buffers descriptor buffer to given buffer and offset in it
void    uploadBuffer ( Buffer& buffer, uint32_t index, VkDeviceSize offset = 0 )
{
    VkDescriptorGetInfoEXT      descriptorInfo        = { VK_STRUCTURE_TYPE_DESCRIPTOR_GET_INFO_EXT     };
    VkDescriptorAddressInfoEXT  descriptorAddressInfo = { VK_STRUCTURE_TYPE_DESCRIPTOR_ADDRESS_INFO_EXT };
    char*                       descriptorBufPtr      = (char*)buffersDescriptorBuffer.getPtr ();

    descriptorAddressInfo.address = buffer.getDeviceAddress () + offset;
    descriptorAddressInfo.range   = buffer.getSize ();      //buffersDescriptorSize;
    descriptorAddressInfo.format  = VK_FORMAT_UNDEFINED;

    descriptorInfo.type                       = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
    descriptorInfo.data.pCombinedImageSampler = nullptr;
    descriptorInfo.data.pUniformBuffer        = &descriptorAddressInfo;

    vkGetDescriptorEXT ( device.getDevice (), &descriptorInfo, descriptorBufferProperties.uniformBufferDescriptorSize, 
                         descriptorBufPtr + index * buffersDescriptorSize + buffersDescriptorOffset );
}

void    uploadImage ( Texture& texture, Sampler& sampler, uint32_t index, 
                      VkImageLayout imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL )
{
    VkDescriptorGetInfoEXT  descriptorInfo   = { VK_STRUCTURE_TYPE_DESCRIPTOR_GET_INFO_EXT };
    VkDescriptorImageInfo   imageDescriptor  = {};
    char*                   descriptorBufPtr = (char*)texturesDescriptorBuffer.getPtr ();

    imageDescriptor.sampler     = sampler.getHandle    ();
    imageDescriptor.imageView   = texture.getImageView ();
    imageDescriptor.imageLayout = imageLayout;

    descriptorInfo.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
    descriptorInfo.data.pCombinedImageSampler = &imageDescriptor;

    vkGetDescriptorEXT ( device.getDevice (), &descriptorInfo, descriptorBufferProperties.combinedImageSamplerDescriptorSize,
                         descriptorBufPtr + index * texturesDescriptorSize + texturesDescriptorOffset );
}

При создании объекта конвейера нам будет нужно descriptor set layout для всех трех множеств дескрипторов.

void    createDescriptorSets ()
{
    descSetLayoutBuffers .add ( 0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_VERTEX_BIT   )
                         .create ( device.getDevice (), VK_DESCRIPTOR_SET_LAYOUT_CREATE_DESCRIPTOR_BUFFER_BIT_EXT );
    descSetLayoutTextures.add ( 0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT )
                         .create ( device.getDevice (), VK_DESCRIPTOR_SET_LAYOUT_CREATE_DESCRIPTOR_BUFFER_BIT_EXT );
}
    
virtual void    createPipelines () override 
{
    createUniformBuffers    ();
    createDefaultRenderPass ( renderPass );

    pipeline.setDevice ( device )
        .setVertexShader   ( "shaders/shader-descriptor-buffer.vert.spv" )
        .setFragmentShader ( "shaders/shader-descriptor-buffer.frag.spv" )
        .setSize           ( swapChain.getExtent () )
        .addVertexBinding  ( sizeof ( BasicVertex ) )
        .addVertexAttributes <BasicVertex> ()
        .addDescLayout ( 0, descSetLayoutBuffers  )
        .addDescLayout ( 1, descSetLayoutBuffers  )
        .addDescLayout ( 2, descSetLayoutTextures )
        .setCullMode       ( VK_CULL_MODE_NONE  )
        .setDepthTest      ( true )
        .setDepthWrite     ( true )
        .create            ( renderPass, VK_PIPELINE_CREATE_DESCRIPTOR_BUFFER_BIT_EXT );            

            // create before command buffers
    swapChain.createFramebuffers ( renderPass, depthTexture.getImageView () );

    createCommandBuffers ( renderPass );
}

Ниже приводятся используемые в примере вершинный и фрагментный шейдеры.

#version 450

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

layout ( set = 0, binding = 0 ) uniform Camera 
{
    mat4    proj;
    mat4    view;
} camera;

layout ( set = 1, binding = 0 ) uniform Model 
{
    mat4    model;
} model;

layout ( location = 0 ) out vec2 tex;

void main() 
{
    gl_Position  = camera.proj * camera.view * model.model * vec4 ( pos, 1.0 );
    tex          = texCoord;
}

#version 450

layout ( set = 2, binding = 0 ) uniform sampler2D image;

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

void main() 
{
    color = texture ( image, tex );
}

Ниже идет полный исходный код на С++ соответствующего примера.

#include    <ctype.h>
#include    <memory>
#include    "VulkanWindow.h"
#include    "Buffer.h"
#include    "DescriptorSet.h"
#include    "Mesh.h"
#include    "Controller.h"

struct UboCamera 
{
    glm::mat4 proj;
    glm::mat4 view;
};

struct UboModel
{
    glm::mat4 model;
};

class   ExampleWindow : public VulkanWindow
{
    std::vector<CommandBuffer>      commandBuffers;
    GraphicsPipeline                pipeline;
    Renderpass                      renderPass;
    std::vector<Texture>            textures;
    Sampler                         sampler;
    std::unique_ptr<Mesh>           mesh;
    Uniform<UboCamera>              cameraUbo;                  // camera uniform buffer
    std::vector<Uniform<UboModel>>  modelUbos;                  // models uniform buffers

    DescSetLayout                   descSetLayoutBuffers;       // descriptor set layout for descriptor buffer with buffers (Ubo)
    DescSetLayout                   descSetLayoutTextures;      // descriptor set layout for descriptor buffer with textures
    PersistentBuffer                buffersDescriptorBuffer;    // descriptor buffer with buffers (Ubo)
    VkDeviceSize                    buffersDescriptorOffset;
    VkDeviceSize                    buffersDescriptorSize;
    PersistentBuffer                texturesDescriptorBuffer;   // descriptor buffer with textures
    VkDeviceSize                    texturesDescriptorOffset;
    VkDeviceSize                    texturesDescriptorSize;
                                                                // we need properties for buffer alignment
    VkPhysicalDeviceDescriptorBufferPropertiesEXT& descriptorBufferProperties;

                                                                // function pointers to new commands
    PFN_vkGetDescriptorSetLayoutSizeEXT vkGetDescriptorSetLayoutSizeEXT                           = {};
    PFN_vkGetDescriptorSetLayoutBindingOffsetEXT vkGetDescriptorSetLayoutBindingOffsetEXT         = {};
    PFN_vkCmdBindDescriptorBuffersEXT vkCmdBindDescriptorBuffersEXT                               = {};
    PFN_vkCmdSetDescriptorBufferOffsetsEXT vkCmdSetDescriptorBufferOffsetsEXT                     = {};
    PFN_vkGetDescriptorEXT vkGetDescriptorEXT                                                     = {};
    PFN_vkCmdBindDescriptorBufferEmbeddedSamplersEXT vkCmdBindDescriptorBufferEmbeddedSamplersEXT = {};

public:
    ExampleWindow ( int w, int h, const std::string& t, bool depth, DevicePolicy * p, 
                    VkPhysicalDeviceDescriptorBufferPropertiesEXT& descBufferProps ) : 
                    VulkanWindow ( w, h, t, depth, p ), descriptorBufferProperties ( descBufferProps )
    {
        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 ) );

        createDescriptorSets ();                // create descriptor sets for descriptor buffers

            // create buffers
        createDescriptorBuffer ( buffersDescriptorBuffer,  descSetLayoutBuffers,  true,  4, 0, buffersDescriptorSize,  buffersDescriptorOffset  );
        createDescriptorBuffer ( texturesDescriptorBuffer, descSetLayoutTextures, false, 3, 0, texturesDescriptorSize, texturesDescriptorOffset );

        sampler.create       ( device );        // use default options
        createTextures       ();
        createPipelines      ();
        uploadBuffer         ( cameraUbo, 0, 0 );

        for ( int i = 0; i <  modelUbos.size (); i++ )
        {
            uploadBuffer ( modelUbos [i], i + 1, 0 );
            uploadImage  ( textures [i], sampler, i );
        }
    }

    void    createTextures ()
    {
        std::string                 texPath = "../../Textures/";
        std::vector<const char *>   texs    = { "Fieldstone.dds", "16.jpg", "flower.png", "lena.png", "block.jpg", 
                                                "brick.tga", "oak.jpg", "rockwall.jpg" };

        textures.resize ( texs.size () );
        for ( size_t i = 0; i < texs.size (); i++ )
            if ( !textures [i].load ( device, texPath + texs [i] ) )
                fatal () << "Error loading texture " << texs [i] << std::endl;
    }

    void    createDescriptorBuffer ( PersistentBuffer& buf, const DescSetLayout& descLayout, bool isBuffer, 
                                     int sizeMultiplier, uint32_t binding, VkDeviceSize& size, VkDeviceSize& offset )
    {
            // get size & offset
        vkGetDescriptorSetLayoutSizeEXT          ( device.getDevice (), descLayout.getHandle (), &size );
        vkGetDescriptorSetLayoutBindingOffsetEXT ( device.getDevice (), descLayout.getHandle (), binding, &offset );

            // force size alignment
        VkDeviceSize allocSize = sizeMultiplier * alignedSize<VkDeviceSize> ( size, 
                                                       descriptorBufferProperties.descriptorBufferOffsetAlignment );

        uint32_t    usage;

        if ( isBuffer )
            usage = VK_BUFFER_USAGE_RESOURCE_DESCRIPTOR_BUFFER_BIT_EXT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT;
        else
            usage = VK_BUFFER_USAGE_RESOURCE_DESCRIPTOR_BUFFER_BIT_EXT | VK_BUFFER_USAGE_SAMPLER_DESCRIPTOR_BUFFER_BIT_EXT;

        buf.create ( device, allocSize, usage, Buffer::hostWrite );
    }

        // set entry index in buffers descriptor buffer to given buffer and offset in it
    void    uploadBuffer ( Buffer& buffer, uint32_t index, VkDeviceSize offset = 0 )
    {
        VkDescriptorGetInfoEXT      descriptorInfo        = { VK_STRUCTURE_TYPE_DESCRIPTOR_GET_INFO_EXT     };
        VkDescriptorAddressInfoEXT  descriptorAddressInfo = { VK_STRUCTURE_TYPE_DESCRIPTOR_ADDRESS_INFO_EXT };
        char*                       descriptorBufPtr      = (char*)buffersDescriptorBuffer.getPtr ();

        descriptorAddressInfo.address = buffer.getDeviceAddress () + offset;
        descriptorAddressInfo.range   = buffer.getSize ();      //buffersDescriptorSize;
        descriptorAddressInfo.format  = VK_FORMAT_UNDEFINED;

        descriptorInfo.type                       = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
        descriptorInfo.data.pCombinedImageSampler = nullptr;
        descriptorInfo.data.pUniformBuffer        = &descriptorAddressInfo;

        vkGetDescriptorEXT ( device.getDevice (), &descriptorInfo, descriptorBufferProperties.uniformBufferDescriptorSize, 
                             descriptorBufPtr + index * buffersDescriptorSize + buffersDescriptorOffset );
    }

    void    uploadImage ( Texture& texture, Sampler& sampler, uint32_t index, 
                          VkImageLayout imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL )
    {
        VkDescriptorGetInfoEXT  descriptorInfo   = { VK_STRUCTURE_TYPE_DESCRIPTOR_GET_INFO_EXT };
        VkDescriptorImageInfo   imageDescriptor  = {};
        char*                   descriptorBufPtr = (char*)texturesDescriptorBuffer.getPtr ();

        imageDescriptor.sampler     = sampler.getHandle    ();
        imageDescriptor.imageView   = texture.getImageView ();
        imageDescriptor.imageLayout = imageLayout;

        descriptorInfo.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
        descriptorInfo.data.pCombinedImageSampler = &imageDescriptor;

        vkGetDescriptorEXT ( device.getDevice (), &descriptorInfo, descriptorBufferProperties.combinedImageSamplerDescriptorSize, 
                             descriptorBufPtr + index * texturesDescriptorSize + texturesDescriptorOffset );
    }

    void    createUniformBuffers ()
    {
        cameraUbo.create ( device );
        modelUbos.resize ( 3 );
        
        for ( size_t i = 0; i < 3; i++ )
            modelUbos [i].create ( device );
    }

    void    freeUniformBuffers ()
    {
        cameraUbo.clean ();
        modelUbos.clear ();
    }

    void    createDescriptorSets ()
    {
        descSetLayoutBuffers .add ( 0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,         VK_SHADER_STAGE_VERTEX_BIT   )
                             .create ( device.getDevice (), VK_DESCRIPTOR_SET_LAYOUT_CREATE_DESCRIPTOR_BUFFER_BIT_EXT );
        descSetLayoutTextures.add ( 0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT )
                             .create ( device.getDevice (), VK_DESCRIPTOR_SET_LAYOUT_CREATE_DESCRIPTOR_BUFFER_BIT_EXT );
    }
    
    virtual void    createPipelines () override 
    {
        createUniformBuffers    ();
        createDefaultRenderPass ( renderPass );

        pipeline.setDevice ( device )
            .setVertexShader   ( "shaders/shader-descriptor-buffer.vert.spv" )
            .setFragmentShader ( "shaders/shader-descriptor-buffer.frag.spv" )
            .setSize           ( swapChain.getExtent () )
            .addVertexBinding  ( sizeof ( BasicVertex ) )
            .addVertexAttributes <BasicVertex> ()
            .addDescLayout ( 0, descSetLayoutBuffers  )
            .addDescLayout ( 1, descSetLayoutBuffers  )
            .addDescLayout ( 2, descSetLayoutTextures )
            .setCullMode       ( VK_CULL_MODE_NONE  )
            .setDepthTest      ( true )
            .setDepthWrite     ( true )
            .create            ( renderPass, VK_PIPELINE_CREATE_DESCRIPTOR_BUFFER_BIT_EXT );            

                // create before command buffers
        swapChain.createFramebuffers ( renderPass, depthTexture.getImageView () );

        createCommandBuffers ( renderPass );
    }

    virtual void    freePipelines () override
    {
        commandBuffers.clear ();
        pipeline.clean       ();
        renderPass.clean     ();
        freeUniformBuffers   ();
        descAllocator.clean  ();
    }
    
    virtual void    submit ( uint32_t imageIndex ) override 
    {
        updateUniformBuffers ( imageIndex );
        defaultSubmit        ( commandBuffers [imageIndex] );
    }

    void    createCommandBuffers ( Renderpass& renderPass )
    {
        auto    framebuffers = swapChain.getFramebuffers ();
        VkDescriptorBufferBindingInfoEXT    bindings [2] {};

        bindings [0].sType   = VK_STRUCTURE_TYPE_DESCRIPTOR_BUFFER_BINDING_INFO_EXT;
        bindings [0].address = buffersDescriptorBuffer.getDeviceAddress ();
        bindings [0].usage   = VK_BUFFER_USAGE_RESOURCE_DESCRIPTOR_BUFFER_BIT_EXT;  
                                                                                   
        bindings [1].sType   = VK_STRUCTURE_TYPE_DESCRIPTOR_BUFFER_BINDING_INFO_EXT;
        bindings [1].address = texturesDescriptorBuffer.getDeviceAddress ();
        bindings [1].usage   = VK_BUFFER_USAGE_SAMPLER_DESCRIPTOR_BUFFER_BIT_EXT | VK_BUFFER_USAGE_RESOURCE_DESCRIPTOR_BUFFER_BIT_EXT;

        commandBuffers = device.allocCommandBuffers ( (uint32_t)framebuffers.size ());

        for ( size_t i = 0; i < commandBuffers.size(); i++ )
        {
            commandBuffers [i]
                .begin             ()
                .beginRenderPass   ( RenderPassInfo ( renderPass ).framebuffer ( framebuffers [i] )
                                            .extent ( swapChain.getExtent ().width, swapChain.getExtent ().height )
                                            .clearColor (0,0,0,1).clearDepthStencil () )
                .pipeline          ( pipeline );

                VkDeviceSize    bufferOffset     = 0;
                uint32_t        bufferIndexUbo   = 0;
                uint32_t        bufferIndexImage = 1;

                    // set descriptor buffer bindings to our descriptor buffers
                vkCmdBindDescriptorBuffersEXT ( commandBuffers [i].getHandle (), 2, bindings );

                    // camera ubo (set 0)
                vkCmdSetDescriptorBufferOffsetsEXT ( commandBuffers [i].getHandle (), VK_PIPELINE_BIND_POINT_GRAPHICS, 
                                                     pipeline.getLayout (), 0, 1, &bufferIndexUbo, &bufferOffset );

                for ( int j = 0; j < 3; j++ )
                {
                        // model ubo (set 1)
                    bufferOffset = (j + 1) * buffersDescriptorSize;
                    vkCmdSetDescriptorBufferOffsetsEXT ( commandBuffers [i].getHandle (), VK_PIPELINE_BIND_POINT_GRAPHICS, 
                                                         pipeline.getLayout (), 1, 1, &bufferIndexUbo, &bufferOffset );

                        // texture (set 2)
                    bufferOffset = j * texturesDescriptorSize;
                    vkCmdSetDescriptorBufferOffsetsEXT ( commandBuffers [i].getHandle (), VK_PIPELINE_BIND_POINT_GRAPHICS, 
                                                         pipeline.getLayout (), 2, 1, &bufferIndexImage, &bufferOffset );
                
                        // render mesh
                    commandBuffers [i].render ( mesh.get () );
                }

                commandBuffers [i].end ();
        }
    }

    void updateUniformBuffers ( uint32_t )
    {
        float   t     = float ( getTime () );

        //t = 0;

        cameraUbo->view  = controller->getModelView ();
        cameraUbo->proj  = controller->getProjection ();

        modelUbos [0]->model = glm::translate ( glm::mat4(1), glm::vec3 ( -1, -1, 0 ) ) * glm::rotate ( glm::mat4(1), t, glm::vec3 ( 1, 0, 0 ) );
        modelUbos [1]->model = glm::translate ( glm::mat4(1), glm::vec3 ( 0, 0, 1 ) )   * glm::rotate ( glm::mat4(1), t, glm::vec3 ( 0, 1, 0 ) );
        modelUbos [2]->model = glm::translate ( glm::mat4(1), glm::vec3 ( 1, 0, 0 ) )   * glm::rotate ( glm::mat4(1), t, glm::vec3 ( 0, 1, 1 ) );
    }

    void    loadExtensions ()
    {
        vkGetDescriptorSetLayoutSizeEXT              = reinterpret_cast<PFN_vkGetDescriptorSetLayoutSizeEXT>             
                                        (vkGetDeviceProcAddr(device.getDevice (), "vkGetDescriptorSetLayoutSizeEXT"));
        vkGetDescriptorSetLayoutBindingOffsetEXT     = reinterpret_cast<PFN_vkGetDescriptorSetLayoutBindingOffsetEXT>    
                                        (vkGetDeviceProcAddr(device.getDevice (), "vkGetDescriptorSetLayoutBindingOffsetEXT"));
        vkCmdBindDescriptorBuffersEXT                = reinterpret_cast<PFN_vkCmdBindDescriptorBuffersEXT>               
                                        (vkGetDeviceProcAddr(device.getDevice (), "vkCmdBindDescriptorBuffersEXT"));
        vkGetDescriptorEXT                           = reinterpret_cast<PFN_vkGetDescriptorEXT>                          
                                        (vkGetDeviceProcAddr(device.getDevice (), "vkGetDescriptorEXT"));
        vkCmdBindDescriptorBufferEmbeddedSamplersEXT = reinterpret_cast<PFN_vkCmdBindDescriptorBufferEmbeddedSamplersEXT>
                                        (vkGetDeviceProcAddr(device.getDevice (), "vkCmdBindDescriptorBufferEmbeddedSamplersEXT"));
        vkCmdSetDescriptorBufferOffsetsEXT           = reinterpret_cast<PFN_vkCmdSetDescriptorBufferOffsetsEXT>          
                                        (vkGetDeviceProcAddr(device.getDevice (), "vkCmdSetDescriptorBufferOffsetsEXT"));
    }
};

int main ( int argc, const char * argv [] ) 
{
    DevicePolicy    policy;

        // we require VK_EXT_DESCRIPTOR_BUFFER_EXTENSION_NAME device extension
    policy.addDeviceExtension ( VK_EXT_DESCRIPTOR_BUFFER_EXTENSION_NAME );

    VkPhysicalDeviceDescriptorBufferFeaturesEXT   descriptorBufferFeatures     = { VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_BUFFER_FEATURES_EXT   };
    VkPhysicalDeviceDescriptorBufferPropertiesEXT descriptorBufferProperties   = { VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_BUFFER_PROPERTIES_EXT };

    descriptorBufferFeatures.descriptorBuffer = VK_TRUE;

    policy.addFeatures   ( &descriptorBufferFeatures    );
    policy.addProperties ( &descriptorBufferProperties  );

    return ExampleWindow ( 800, 600, "Vulkan descriptor buffer", true,  &policy, descriptorBufferProperties ).run ();
}

Весь исходный код уже доступен в репозитории на github.

Полезные ссылки -

Vulkan Documentation: Descriptor buffers

VK_EXT_descriptor_buffer