![]() |
Главная
![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() |
При передаче различных буферов в шейдер из приложения возникает закономерный вопрос - а как именно данные должны быть расположены внутри буфера с точки зрения Vulkan ? И соответственно как нам гарантировать, что данные и на стороне CPU и на стороне GPU будут размещены в памяти одинаковым образом (т.е. смещения всех элементов будут совпадать). Иначе мы легко можем прийти к ситуации, когда на одной стороне мы считаем что смещение второго элемента в приводимом ниже примере равно 12, а на другой - 16
layout ( bindidng = 0, st140 ) buffer Block
{
vec3 a;
float b;
};
На стороне CPU у нас все в порядке - у каждого компилятора имеется целый набор способ гарантировать заданное выравнивание данных в структуре, т.е. мы для заданной структуры можем явно задать требуемое выравнивание.
А вот на стороне GLSL с этим гораздо сложнее, мы не можем явно задать требуемое выравнивание.
Вместо этого мы для каждого буфера можем задать один из способов раскладки элементов в памяти.
В Vulkan в GLSL поддерживается три таких способа - std140
, std430
и scalar
.
Причем последний способ вводится расширением GL_EXT_scalar_buffer_layout, вошедшим в
состав Vulkan 1.2 Core.
Наиболее жестким способом является std140
.
При его использовании размер и выравнивание элементов определяются по следующим правилам:
uint
или float
), то его размер и
выравнивание равны размеру этого типа в байтах (для uint
или float
это будет 4).
vec2
) размер и выравнивание равны удвоенному размеру базового типа (для vec2
это будет 8).
vec3
) размер и выравнивание равны размеру базового типа умноженному на 4 (для vec3
и vec4
это будет 16).
Рассмотрим несколько примеров.
layout(binding = 0) buffer Block1
{
float a; // offset 0
vec2 b; // offset 8
vec2 c; // offset 16
};
layout(binding = 0) buffer Block2
{
vec3 a; // offset 0
vec2 b; // offset 16
vec4 c; // offset 32
};
layout(binding = 0) buffer Block3
{
vec2 a[4]; // array stride = 16, offset 0
vec4 b; // offset 64
};
Здесь размер и выравнивание элементов определяются по похожим правилам:
vec2
) размер и выравнивание равны удвоенному размеру базового типа (для vec2
это будет 8).
vec3
) размер и выравнивание равны размеру базового типа умноженному на 4 (для vec3
и vec4
это будет 16).
vec3
не происходит округления вверх до ближайшего множителя 16.
Рассмотрим несколько примеров.
layout(binding = 0) buffer Block1
{
float a; // offset 0
vec2 b; // offset 8
vec2 c; // offset 16
};
layout(binding = 0) buffer Block2
{
vec3 a; // offset 0
vec2 b; // offset 16
vec4 c; // offset 32
};
layout(binding = 0) buffer Block3
{
vec2 a[4]; // array stride = 8, offset 0
vec4 b; // offset 32
};
Обратите внимание, что для последнего примера расстояние между подряд идущими элементами массива vec3 изменилось с 16 (для std140) до 8, что привело к изменению смещения для члена b.
Это самые простые и компактные (с точки зрения памяти) правила выравнивания.
vecN
имеет выравнивание равное 4 и размер, равный 4*N.
float
выравнивание равно 4.
Давайте рассмотрим как изменятся размещение в памяти для трех ранее рассмотренных буферов.
layout(binding = 0) buffer Block1
{
float a; // offset 0
vec2 b; // offset 4
vec2 c; // offset 12
};
layout(binding = 0) buffer Block2
{
vec3 a; // offset 0
vec2 b; // offset 12
vec4 c; // offset 20
};
layout(binding = 0) buffer Block3
{
vec2 a[4]; // array stride = 8, offset 0
vec4 b; // offset 32
};
После того, как шейдер, содержащий определение буфера (или push-константы) был успешно скомпилирован в SPIR-V, его
можно дизассемблировать, т.е. перевести в удобочитаемый текстовый вид при помощи утилиты spirv-dis
(входящей в Vulkan SDK).
В получившемся листинге каждому блоку будет соответствовать несколько строк, в которых задаются смещения всех
его полей и array stride для массивов.
Так если мы возьмем следующее определение буфера.
layout ( binding = 0, std140 ) buffer Block140
{
vec2 a [4];
vec4 b;
};
Тогда в получившемся листинге ему будут соответствовать следующие три строки. Первая из них задает array stride, вторая задает смещение (offset) первого поля, третья - смещение второго поля.
OpDecorate %_arr_v2float_uint_4 ArrayStride 16 OpMemberDecorate %Block140 0 Offset 0 OpMemberDecorate %Block140 1 Offset 64
Несколько полезных ссылок:
Описание размещения в памяти данных
Описание расширения GL_EXT_scalar_block_layout
Пример дизассемблирования.
glslangValidator -V shader-140.vert -o shader-140.vert.spv spirv-dis.exe shader-140.vert.spv > shader-140.dis