steps3D - Tutorials - Выравнивание данных в памяти буферов и push-констант в Vulkan и расширение GL_EXT_scalar_buffer_layout

Выравнивание данных в памяти буферов и push-констант в Vulkan и расширение GL_EXT_scalar_buffer_layout

При передаче различных буферов в шейдер из приложения возникает закономерный вопрос - а как именно данные должны быть расположены внутри буфера с точки зрения 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

Наиболее жестким способом является std140. При его использовании размер и выравнивание элементов определяются по следующим правилам:

Рассмотрим несколько примеров.

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
};

Выравнивание std430

Здесь размер и выравнивание элементов определяются по похожим правилам:

Рассмотрим несколько примеров.

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.

Выравнивание scalar

Это самые простые и компактные (с точки зрения памяти) правила выравнивания.

Давайте рассмотрим как изменятся размещение в памяти для трех ранее рассмотренных буферов.

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