steps3D - Tutorials - Расширение VK_KHR_buffer_device_address или для буферов наконец завезли адреса

Расширение VK_KHR_buffer_device_address или для буферов наконец завезли адреса

Расширение VK_KHR_buffer_device_address (и несколько связанных с ним расширений GLSL), вошедшее в Vulkan 1.2 Core, дают очень полезную и удобную возможность - адреса в памяти GPU. Под адресом понимается 64-битовое значение, указывающее внутрь буфера (не обязательно на его начало). Этот адрес (как значение типа uint64_t или uvec2) можно свободно передавать в любой шейдер самыми разными способами (например через push constants или как содержимое другого буфера), а потом использовать прямо внутри шейдера как адрес, по которому можно обращаться напрямую к памяти GPU, т.е. трактовать как ссылку внутрь буфера.

В частности это означает, что многие буфера с различными данными теперь не обязательно передавать в шейдер через descriptor set'ы - можно просто через push constants передать сразу адрес и использовать его как ссылку на данные.

Включение поддержки в Vulkan

Если вы используете Vulkan версии ниже, чем 1.2, то надо надо явно задать поддержку расширения VK_KHR_buffer_device_address. Кроме того, при создании устройства необходимо через структуру VkPhysicalDeviceVulkan12Features задать поддержку получения адресов буферов выставив значение поля bufferDeviceAddress в VK_TRUE.

int main ( int argc, const char * argv [] ) 
{
    DevicePolicy                     policy;
    VkPhysicalDeviceVulkan12Features features12 = { VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES };

    features12.bufferDeviceAddress = VK_TRUE;
    policy.addFeatures ( &features12 );

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

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

Кроме приведенных выше шагов, необходимо также явно задать поддержку получения адреса при выделении памяти. При использовании стандартного механизма выделения памяти GPU в Vulkan (vkAllocMemory) необходимо передать флаг VK_MEMORY_ALLOCATE_DEVICE_ADDRESS_BIT_KHR как показано ниже.

bool    alloc ( Device& _device, VkMemoryRequirements memRequirements, VkMemoryPropertyFlags properties, bool addressable = false )
{
    VkMemoryAllocateInfo        allocInfo = { VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO };
    VkMemoryAllocateFlagsInfo   flagsInfo = { VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_FLAGS_INFO };
        
    device                    = &_device;
    size                      = memRequirements.size;
    allocInfo.allocationSize  = size;
    allocInfo.memoryTypeIndex = findMemoryType ( memRequirements.memoryTypeBits, properties );

    if ( addressable )      // if VK_KHR_buffer_device_address requested
    {
        flagsInfo.flags = VK_MEMORY_ALLOCATE_DEVICE_ADDRESS_BIT_KHR;
        allocInfo.pNext = &flagsInfo;
    }

    return vkAllocateMemory ( device->getDevice (), &allocInfo, nullptr, &memory ) == VK_SUCCESS;
}

Если же вы для выделения памяти GPU используете библиотеку VMA, то при вызове функции vmaCreateAllocator необходимо передать флаг VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT.

VmaAllocatorCreateInfo  allocatorCreateInfo = {};
VmaVulkanFunctions      vulkanFuncs         = {};

vulkanFuncs.vkGetInstanceProcAddr = &vkGetInstanceProcAddr;
vulkanFuncs.vkGetDeviceProcAddr   = &vkGetDeviceProcAddr;

allocatorCreateInfo.vulkanApiVersion = VK_API_VERSION_1_3;
allocatorCreateInfo.physicalDevice   = physicalDevice;
allocatorCreateInfo.device           = device;
allocatorCreateInfo.instance         = instance;
allocatorCreateInfo.flags            = VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT;

if ( vmaCreateAllocator ( &allocatorCreateInfo, &allocator ) != VK_SUCCESS )
    fatal () << "vmaCreateAllocator failure" << std::endl;

Также необходимо при создании самого буфера задать поддержку получения его адреса передав в структуре VkBufferCreateInfo параметр usage, содержащий бит VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT.

bool    create ( Device& dev, VkDeviceSize sz, VkBufferUsageFlags usage, int mappable )
{
    VkBufferCreateInfo      bufferInfo = { VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO };

    bufferInfo.size        = sz;
    bufferInfo.usage       = usage | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT;
    bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
    device                 = &dev;
    size                   = sz;

Получение адреса буфера

Для получения адреса начала буфера служит функция vkGetBufferDeviceAddress, как показано ниже.

uint64_t    getDeviceAddress () const
{
    VkBufferDeviceAddressInfo addressInfo = { VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO };

    addressInfo.buffer = buffer;

    return vkGetBufferDeviceAddress ( device->getDevice (), &addressInfo );
}

Полученный адрес это просто 64-битовое беззнаковое целочисленное значение (uint64_t), над которым можно выполнять стандартные арифметические операции, складывать в буфера и передавать как push constants.

Поддержка адресов в GLSL

Есть несколько расширений для работы с адресами в GLSL - их надо явно включить в начале шейдера. В первую очередь это расширение GL_EXT_buffer_reference. Это расширение вводит в GLSL синтаксис для описания ссылок (reference) в память GPU. Кроме того, еще несколько дополнительных расширений оказываются очень полезными.

Одним из этих дополнительных расширений является GL_EXT_buffer_reference_uvec2. Оно вводит возможность преобразования типа между адресами в памяти GPU (как значений типа uvec2) и собственно ссылками, используемыми для обращения по этим адресам. Расширение GL_EXT_buffer_reference2 добавляет фактически поддержку адресной арифметики для ссылок в память GPU без явного преобразования в uint64_t. Кроме того, это расширение позволяет узнать размер структуры в памяти GPU.

Для того, чтобы получить поддержку типа uint64_t (вместе с возможностью преобразования типа в/из ссылки) нужны следующие два расширения - GL_ARB_gpu_shader_int64 и GL_EXT_shader_explicit_arithmetic_types_int64. При помощи этого расширения можно в GLSL реализовать sizeof (отсутствующий в GLSL по стандарту) как показано ниже.

#define sizeof(Type) (uint64_t(Type(uint64_t(0))+1))

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

struct  Instance
{
    vec4    offs;
    vec4    color;
};

layout ( buffer_reference, std430, buffer_reference_align=16) buffer BufferPtr
{
    Instance instances [];
};

layout( push_constant ) uniform constants
{
    BufferPtr   ptr;
} push;

После этой директивы если у нас есть переменная типа MyReference, то мы можем использовать ее для обращения к данными в памяти GPU. Ниже приводится вершинный шейдер, используемый в примере к данной статье.

void main() 
{
    gl_Position  = ubo.proj * ubo.view * ubo.model * vec4 ( 0.3 * inPosition + push.ptr.instances [gl_InstanceIndex].offs.xyz, 1.0 );
    fragTexCoord = inTexCoord;
    fragColor    = push.ptr.instances [gl_InstanceIndex].color;
}

Пример использования

Давайте рассмотрим следующий пример: у нас есть набор фиксированных данных, т.е. то, что обычно передают через uniform-буфера и мы хотим получить доступ к этим данным без явной передачи его посредством descriptor set'ов.

Для этого мы вводим в шейдерах соответствующий тип - ссылки на эти данные. Кроме этого нам нужно как-то передать указатель на эти данные. Для этого очень хорошо подходят push constants. Основной их недостаток - небольшой размер данных, которые можно таким образом передать. Но мы гарантированно можем передать адрес, являющийся 8-байтовым значением.

layout ( buffer_reference, std430, buffer_reference_align=16) buffer BufferPtr
{
    Instance instances [];
};

layout( push_constant ) uniform constants
{
    BufferPtr   ptr;
} push;

После этого для доступа к данным нам останется только перевести 64-битовое значение из числа в ссылку на данные и обратиться к этим данным по ссылке.

#version 450

#extension  GL_EXT_buffer_reference : require

layout(binding = 0) uniform UniformBufferObject 
{
    mat4 model;
    mat4 view;
    mat4 proj;
} ubo;

struct  Instance
{
    vec4    offs;
    vec4    color;
};

layout ( buffer_reference, std430, buffer_reference_align=16) buffer BufferPtr
{
    Instance instances [];
};

layout( push_constant ) uniform constants
{
    BufferPtr   ptr;
} push;

layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec2 inTexCoord;

layout(location = 0) out vec2 fragTexCoord;
layout(location = 1) out vec4 fragColor;

void main() 
{
    gl_Position  = ubo.proj * ubo.view * ubo.model * vec4 ( 0.3 * inPosition + 
                   push.ptr.instances [gl_InstanceIndex].offs.xyz, 1.0 );
    fragTexCoord = inTexCoord;
    fragColor    = push.ptr.instances [gl_InstanceIndex].color;
}

Таким образом, можно вообще уйти от необходимости передачи буферов через descriptor set'ы , а передавать адреса через push constants. Обратите внимание, что мы можем легко создать сложную структуру в памяти GPU, содержащую ссылки на отдельные свои части, например дерево или граф.

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

struct Ubo 
{
    glm::mat4 model;
    glm::mat4 view;
    glm::mat4 proj;
    glm::mat3 nm;
};

struct Instance
{
    glm::vec4   offset;
    glm::vec4   color;
};

struct BufferRec
{
    Instance    instances [64];
};

struct PushData
{
    uint64_t    bufferPtr;
};

class   ExampleWindow : public VulkanWindow
{
    std::vector<CommandBuffer>      commandBuffers;
    std::vector<DescriptorSet>      descriptorSets;
    std::vector<Uniform<Ubo>>       uniformBuffers;
    GraphicsPipeline                pipeline;
    Renderpass                      renderPass;
    Texture                         texture;
    Sampler                         sampler;
    std::unique_ptr<Mesh>           mesh;
    Buffer                          buffer;

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

        mesh = std::unique_ptr<Mesh> ( loadMesh ( device, "../../Models/teapot.3ds", 0.04f ) );

        sampler.create  ( device );     // use default options
        texture.load    ( device, "../../Textures/Fieldstone.dds", false );
        createBuffer    ();
        createPipelines ();
    }

    void    createBuffer ()
    {
        std::vector<Instance>   instances ( 64 );

        for ( int i = 0; i < 64; i++ )
        {
            instances [i].offset = glm::vec4 ( i % 8 - 4, i / 8 - 4, 0, 0 );
            instances [i].color  = glm::vec4 ( (i / 8) / 8.0f, 1 - (i % 8) / 8.0f, 0, 1 );
        }

        buffer.create ( device, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, instances, 
         Buffer::hostWrite );
    }

    void    createUniformBuffers ()
    {
        uniformBuffers.resize ( swapChain.imageCount() );
        
        for ( size_t i = 0; i < swapChain.imageCount (); i++ )
            uniformBuffers [i].create ( device );
    }

    void    freeUniformBuffers ()
    {
        uniformBuffers.clear ();
    }

    void    createDescriptorSets ()
    {
        descriptorSets.resize ( swapChain.imageCount () );

        for ( uint32_t i = 0; i < swapChain.imageCount (); i++ )
        {
            descriptorSets  [i]
                .setLayout        ( device, descAllocator, pipeline.getDescLayout () )
                .addUniformBuffer ( 0, uniformBuffers [i], 0, sizeof ( Ubo ) )
                .addImage         ( 1, texture, sampler )
                .create           ();
        }
    }
    
    virtual void    createPipelines () override 
    {
        createUniformBuffers    ();
        createDefaultRenderPass ( renderPass );

        pipeline.setDevice ( device )
                .setVertexShader   ( "shaders/shader-buffer-address.vert.spv" )
                .setFragmentShader ( "shaders/shader-buffer-address.frag.spv" )
                .setSize           ( swapChain.getExtent () )
                .addVertexBinding  ( sizeof ( BasicVertex ) )
                .addVertexAttributes <BasicVertex> ()
                .addDescLayout     ( 0, DescSetLayout ()
                    .add ( 0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,         VK_SHADER_STAGE_VERTEX_BIT )
                    .add ( 1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT ) )
            .addPushConstRange (  VK_SHADER_STAGE_VERTEX_BIT, sizeof ( PushData ) )
            .setCullMode       ( VK_CULL_MODE_NONE )
            .setDepthTest      ( true )
            .setDepthWrite     ( true )
            .create            ( renderPass );          

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

        createDescriptorSets ();
        createCommandBuffers ( renderPass );
    }

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

    void    createCommandBuffers ( Renderpass& renderPass )
    {
        auto    framebuffers = swapChain.getFramebuffers ();

        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 () ).clearColor ().clearDepthStencil () )
                .pipeline          ( pipeline )
                .addDescriptorSets ( { descriptorSets[i] } );

            PushData    data = { buffer.getDeviceAddress () };

            commandBuffers [i]
                .pushConstants ( pipeline.getLayout (), VK_SHADER_STAGE_VERTEX_BIT, data )
                .renderInstanced   ( mesh.get (), 64 );     // draw 64 instances

            commandBuffers [i].end ();
        }
    }

    void updateUniformBuffer ( uint32_t currentImage )
    {
        uniformBuffers [currentImage]->model = controller->getModelView  ();
        uniformBuffers [currentImage]->view  = glm::mat4 ( 1 );
        uniformBuffers [currentImage]->proj  = controller->getProjection ();
    }
};

int main ( int argc, const char * argv [] ) 
{
    DevicePolicy                     policy;
    VkPhysicalDeviceVulkan12Features features12 = { VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES };

    features12.bufferDeviceAddress = VK_TRUE;
    policy.addFeatures ( &features12 );

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

Несколько полезных ссылок:

VK_KHR_buffer_device_address(3) Manual Page

Buffer device addresses in Vulkan and VMA

Bindless descriptor sets - на самом деле кроме bindless еще рассматривается и адреса буферов

New game changing Vulkan extensions for mobile: Buffer Device Address