пятница, 7 декабря 2012 г.

OpenGL 4.3. Shader Storage Buffer Object

В OpenGL 4.3 появился давно ожидаемый новый тип буфера - Shader Storage Buffer Object (сокращенно SSBO), являющийся по существу массивом для произвольного чтения/записи в шейдере, исключающий необходимость использовать для этих целей текстурные буферы совместно с расширением Image Load Store. Вкратце об этом буфере написано в этой статье, здесь я привожу ее перевод с некоторыми дополнениями (взятыми из спецификации).

Shader Storage Buffer Object (SSBO) - это объект буфера (Buffer Object), который используется для хранения и извлечения данных в GLSL.
Этот тип буфера объявлен в расширении ARB_shader_storage_buffer_object которое вошло в ядро OpenGL 4.3.

SSBO во многом похож на Uniform Buffer Objects. В шейдере блоки определяются практически идентично блокам юниформ, с тем отличием что SSBO связано со своим точкам бинда, а  UBO со своими.
Основные различия между ними: 
  1. SSBO обычно значительно больше. Максимальный размер буферов зависит от реализации, при этом наименьший максимальный размер для UBO составляет 16Кб, в то время как максимальный размер SSBO уже как минимум 16Мб.
  2. SSBO доступно для записи, даже атомарно, в то время как  UBO является юниформой. SSBO имеет тот же тип доступа к памяти как и операции Image Load Store, потому для гарантированного чтения/записи нуждаются в соответствующих "барьерах" (memory barriers).
  3. У SSBO  размер может варьироваться от 0 до размера буфера, в то время как размер UBO фиксирован. Это означает, что у вас в SSBO может быть массив произвольной длины. Фактический размер массива определяется размером прикрепленного буфера и может быть запрошен в шейдере через функцию "length".

Говоря о функциональности, SSBO можно рассматривать как более удобный интерфейс доступа к текстурным буферам (Buffer Textures) чем через предоставляемый Image Load Store.  

Атомарные операции (Atomic operations).

Существуют специальные атомарные функции, которые могут быть применены к переменным буфера (и для разделяемых (shared) переменных в вычислительном шейдере). Эти функции принимают только целочисленные типы (uint и int), но они могут быть членами структур, массивов и векторов.


Все атомарные функции возвращают значение до выполнения операции.
Вместо "nint" может быть int или uint.
Список доступных атомарных функций:
nint atomicAdd(inout nint mem​, nint data​)
nint atomicMin(inout nint mem​, nint data​)
nint atomicMax(inout nint mem​, nint data​)
nint atomicAnd (inout nint mem​, nint data​)
nint atomicOr(inout nint mem​, nint data​)
nint atomicXor(inout nint mem​, nint data​)
nint atomicExchange(inout nint mem​, nint data​)
nint atomicCompSwap(inout nint mem​, nint compare​, nint data​)
За исключением последней, назначение всех функций понятно из названия. Последняя функция (atomicCompSwap) меняет значение mem с data, если compare равно текущему значению mem.

                                      


Пару важных дополнений к указанной выше статье, взятые из спецификации:
1. Что происходит при попытке записи за пределы размера SSBO? В спецификации дан такой ответ: "Для SSBO используются те же правила что и для блоков юниформ, которые не предоставляют никаких проверок выхода за границы диапазона. Если активный блок юниформ не подкреплен буфером с достаточным объемом, то результат работы шейдера будет неопределенным и может привести к прерыванию работы GL".

2. В GLSL 430 появился новый квалификатор "std430", в отличии от старого "std140", у которого выравнивание по границе границе 16 байт, "std430" позволяет более плотно упаковывать данные, так для  "float", "int", и "uint" выравнивание будет только на границу в 4 байта. Однако, "vec3" все так же выравнивается на границу в 16 байт.
Квалификтор "std430" может быть применим только для блоков SSBO.

3. Узнать максимально допустимые размеры SSBO для каждого типа шейдера  можно воспользовавшись функцией "glGetInteger64v" с одним из параметров:

        MAX_VERTEX_SHADER_STORAGE_BLOCKS                0x90D6
        MAX_GEOMETRY_SHADER_STORAGE_BLOCKS              0x90D7
        MAX_TESS_CONTROL_SHADER_STORAGE_BLOCKS          0x90D8
        MAX_TESS_EVALUATION_SHADER_STORAGE_BLOCKS       0x90D9
        MAX_FRAGMENT_SHADER_STORAGE_BLOCKS              0x90DA
        MAX_COMPUTE_SHADER_STORAGE_BLOCKS               0x90DB
        MAX_COMBINED_SHADER_STORAGE_BLOCKS              0x90DC
        MAX_SHADER_STORAGE_BUFFER_BINDINGS              0x90DD
        MAX_SHADER_STORAGE_BLOCK_SIZE                   0x90DE
 
4. Бинд SSBO возможен с использованием новой функции:
"void ShaderStorageBlockBinding(uint program, uint storageBlockIndex, uint storageBlockBinding);"
Или с использованием старых функций "BindBuffer, BufferData, BufferSubData, MapBuffer, UnmapBuffer, GetBufferSubData, GetBufferPointerv", указав в качестве назначения буфера (Target) константу:
        SHADER_STORAGE_BUFFER                           0x90D2
 
Пример использования SSBO (пример взят из из спецификации).
В этом примере, при растеризации примитивов, в буфер записывается список координат фрагментов (x,y) и их цвет. Соответствующий фрагментный шейдер представлен ниже:

      #extension GL_ARB_shader_storage_buffer_object : require

      // Use an atomic counter to keep a running count of the number of
      // fragments recorded in the shader storage buffer.
      layout(binding=0, offset=0) uniform atomic_uint fragmentCounter;

      // Keep a uniform with the number of fragments that can be recorded in
      // the buffer.
      uniform uint maxFragmentCount;

      // Structure with the per-fragment information to record.
      struct FragmentData {
        ivec2 position;
        vec4 color;
      };

      // Shader storage block holding an array <fragments> declared without
      // a fixed size.  Application code should determine how many fragments
      // it wants to record and allocate a buffer appropriately.  With the 
      // "std140" layout, each FragmentData record will take 32B.  With other
      // layouts, the stride of the array is implementation-dependent.  The
      // "binding=2" layout qualifier says that the block <Fragments> should
      // be associated with shader storage buffer binding point #2.
      layout(std140, binding=2) buffer Fragments {
        FragmentData fragments[];
      };

      in vec4 color;

      void main()
      {
        uint fragmentNumber = atomicCounterIncrement(fragmentCounter);
        if (fragmentNumber < maxFragmentCount) {
          fragments[fragmentNumber].position = ivec2(gl_FragCoord.xy);
          fragments[fragmentNumber].color    = color;
        }
      }

Код приложения:
      #define NFRAGMENTS        100000
      #define FRAGMENT_SIZE     32  // known due to "std140" usage

      GLuint fragmentBuffer, counterBuffer;

      // Generate, bind, and specify the data store to hold fragments.  The
      // NULL pointer in BufferData says that the intial buffer contents are
      // undefined.  They will be filled in by the fragment shader code.
      glGenBuffers(1, &amp;fragmentBuffer);
      glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, fragmentBuffer);
      glBufferData(GL_SHADER_STORAGE_BUFFER, NFRAGMENTS*FRAGMENT_SIZE,
                   NULL, GL_DYNAMIC_DRAW);

      // Generate, bind, and specify the data store for the atomic counter.
      glGenBuffers(1, &amp;counterBuffer);
      glBindBufferBase(GL_ATOMIC_COUNTER_BUFFER, 0, counterBuffer);
      glBufferData(GL_ATOMIC_COUNTER_BUFFER, sizeof(GLuint), NULL, 
                   GL_DYNAMIC_DRAW);

      // Reset the atomic counter to zero, then draw stuff.  This will record
      // values into the shader storage buffer as fragments are generated.
      GLuint zero = 0;
      glBufferSubData(GL_ATOMIC_COUNTER_BUFFER, 0, sizeof(GLuint), &amp;zero);
      glUseProgram(program);
      glDrawElements(GL_TRIANGLES, ...);

      // You could inspect the contents with a call such as:
      void *ptr = glMapBuffer(GL_SHADER_STORAGE_BUFFER, GL_READ_ONLY);
      ...
      glUnmapBuffer(GL_SHADER_STORAGE_BUFFER);

      // You could also use the storage buffer contents for vertex pulling.
      // The glMemoryBarrier() command ensures that the data writes to the
      // storage buffer complete prior to vertex pulling.
      glMemoryBarrier(GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT);
      glBindBuffer(GL_ARRAY_BUFFER, fragmentBuffer);
      glVertexAttribIPointer(0, 2, GL_INT, GL_FALSE, FRAGMENT_SIZE, 
                             (void*)0);
      glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, FRAGMENT_SIZE,
                            (void*)16);
      glEnableVertexAttribArray(0);
      glEnableVertexAttribArray(1);
      glDrawArrays(GL_POINTS, ...); 

2 комментария:

  1. Получается, что теперь в шейдере можно одновременно применять к объекту некий эффект и тут же записывать результат в буфер, который дальше можно будет использовать для создания пост-эффектов? Можно ли параллельно автивировать аттачмент FBO и вести запись в 2 буфера(хотя пока слабо представляю, зачем мне такое нужно :) )? Или лучше не забивать гвозди микроскопом и использовать его именно для хранения промежуточных данных? Пока на ум прходят только мысли о записи информации о техиках освещения в light pre-pass рендере, да и те довольно невнятные...

    ОтветитьУдалить
  2. Да, почитай следующую статью:
    http://vbomesh.blogspot.com/2012/12/opengl-43-fbo.html
    Там как раз рассматривается вариант полной замены FBO на SSBO.

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

    Относительно FBO, при использовании в качестве текстуры, оно конечно немного медленнее, но это с лихвой окупается предоставляемыми возможностями.

    ОтветитьУдалить