Главная Статьи Ссылки Скачать Скриншоты Юмор Почитать Tools Проекты Обо мне Гостевая Форум |
Расширение VK_KHR_buffer_device_address (и несколько связанных с ним расширений GLSL),
вошедшее в Vulkan 1.2 Core, дают очень полезную и удобную возможность - адреса в памяти GPU.
Под адресом понимается 64-битовое значение, указывающее внутрь буфера (не обязательно на его начало).
Этот адрес (как значение типа uint64_t
или uvec2
) можно свободно передавать в любой шейдер самыми разными способами
(например через push constants или как содержимое другого буфера), а потом использовать прямо внутри шейдера как
адрес, по которому можно обращаться напрямую к памяти GPU, т.е. трактовать как ссылку внутрь буфера.
В частности это означает, что многие буфера с различными данными теперь не обязательно передавать в шейдер через descriptor set'ы - можно просто через push constants передать сразу адрес и использовать его как ссылку на данные.
Если вы используете 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 - их надо явно включить в начале шейдера. В первую очередь это расширение 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