Атомарные счетчики в OPenGL. Расширение ARB_atomic_counters

Данное расширение (вошедшее в core OpenGL начиная с версии 4.2) добавляет в OpenGL поддержку атомарных счетчиков. Для этих счетчиков поддерживаются операции взятия текущего значения, атомарного увеличения и атомарного уменьшения значения счетчика на единицу.

Атомарность счетчика гарантирует, что если одновренно будет произведено несколько операций по изменению значения данного счетчика, то все эти запросы будут обработаны корректно. Обратите внимание, что для многопоточного приложения (а шейдеры можно рассматривать именно как многопоточное приложения, исполняемое на GPU) обычная операция инкремента или декремента не является атомарной. Это связано с тем, что обычно подобная операция состоит из трех шагов - прочесть значения переменной в регистр, изменить значение регистра на еденицу и записать значение из регистра обратно в переменную.

Если одновременно происходит сразу несколько подобных операций над одним и тем же счетчиком и переключения между нитями произошло после чтения значения в регистр но до записи итогового значения, то в результате записано будет некорректное значение. Атомарные счетчики лишены подобного недостатка - в них операция инкремента и декремента происходят атомарно и внутри такой операции не может быть переключения нитей.

Расширения ARB_atomic_counters вводит в GLSL новый тип данных - atomic_uint, являющийся непрозрачной оберткой (handle) для32-битового беззнакового целочисленного счетчика. Переменные этого типа описываются как uniform, однако память под эти переменные предоставляется вершинными буферами нового типа - GL_ATOMIC_COUNTER_BUFFER.

При этом атомарный счетчик привязывается (при помощи команд glBindBufferBase и glBindBufferRange) к точке привязки и смещению внутри буфера. Один буфер может использоваться для хранения сразу нескольких атомарных счетчиков.

При описании атомарного счетчика в теле шейдера при помощи директивы layout осуществляется привязка к конкретной точке привязки и смещению внутри буфера.

layout ( binding = 2, offset = 4 ) uniform atomic_uint myCounter;

При линковке шейдеров все шейдеры разделяют общее пространство uniform-переменных, таким образом шейдеры разных типов (например вершинный и фрагментный) могут использовать одни и те же атомарные счетчики.

Для изменения значения атомарного счетчика используются встроенные функции atomicCounterIncrement и atomicCounterDecrement, изменяющие значение счетчика на +1 и -1 соответственно.

uint atomicCounterIncrement ( atomic_uint counter );
uint atomicCounterDecrement ( atomic_uint counter );

Функция atomicCounterIncrement возвращает значение атомарноо счетчика до операции инкремента, а функция atomicCounterDecrement возврашает значение счетчика после декремента.

При помощи функции atomicCounter можнр получить значение атомарного счетчика не изменяя его.

uint atomicCounter ( atomic_uint counter );

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

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

#version 420 core

uniform float       scale;

in vec3 n;
in vec3 v;
in vec3 l;
in vec3 h;

out vec4 color;

layout(binding=0, offset=0)  uniform atomic_uint    c0;
layout(binding=0, offset=4)  uniform atomic_uint    c1;
layout(binding=0, offset=8)  uniform atomic_uint    c2;
layout(binding=0, offset=12) uniform atomic_uint    total;

const vec3 lum = vec3 ( 0.27, 0.67, 0.06 );
const vec4 clr = vec4 ( 0.902, 0.694, 0.49, 1.0 );

void main (void)
{
    const vec4  specColor = vec4 ( 0.7, 0.7, 0.0, 1.0 );
    const float specPower = 70.0;

    vec3    n2   = normalize ( n );
    vec3    l2   = normalize ( l );
    vec3    h2   = normalize ( h );
    vec4    diff = vec4 ( max ( dot ( n2, l2 ), 0.0 ) + 0.3 );
    vec4    spec = specColor * pow ( max ( dot ( n2, h2 ), 0.0 ), specPower );
        
    atomicCounterIncrement ( total );           // total # of fragments
    
    color = diff * clr + spec;

    float   l = dot ( lum, color.rgb );
    
    if ( l < 0.5 )
        atomicCounterIncrement ( c0 );
    else
    if ( l < 1.0 )
        atomicCounterIncrement ( c1 );
    else
        atomicCounterIncrement ( c2 );
}

Здесь атомарные счетчики c0, c1, c2 и total испольуются для подсчета числа фрагментов. Ниже приодится соответствующий код на С++.

#include    <GL/glew.h>

#ifdef  _WIN32
    #include    <GL/wglew.h>
#else
    #include    <GL/glxew.h>
#endif

#include    "GlutWindow.h"
#include    "Program.h"
#include    "mat4.h"
#include    "vec2.h"
#include    "Texture.h"
#include    "BasicMesh.h"
#include    "glUtilities.h"
#include    "VertexBuffer.h"

#define NUM_VERTICES    3
#define VERTEX_SIZE     (5*sizeof(float))

int     mouseOldX = 0;
int     mouseOldY = 0;
float   angle     = 0;
vec3    rot   ( 0.0f );
vec3    eye   ( 7, 7, 7 );
vec3    light ( 7, 7, 7 );
float   scale = 9.0;

class   MyWindow : public GlutWindow
{
    Program      program;
    BasicMesh  * mesh;
    VertexBuffer counterBuf;

public:
    MyWindow () : GlutWindow ( 200, 100, 400, 400, "ARB_shader_atomic_counters" )
    {
        glClearColor ( 0.5, 0.5, 0.5, 1.0 );
        glEnable     ( GL_DEPTH_TEST );
        glDepthFunc  ( GL_LEQUAL );
    
        const char * err = getGlErrorString ();
        
        if ( !program.loadProgram ( "atomic-counters.glsl" ) )
        {
            printf ( "Error building program: %s\n", program.getLog ().c_str () );
            
            exit ( 2 );
        }
        else
            printf ( "Shader loaded:\n%s\n", program.getLog ().c_str () );
    
        mesh = createTorus ( 2, 4, 30, 30 );
        
        counterBuf.create ();
        counterBuf.bindBase ( GL_ATOMIC_COUNTER_BUFFER, 0 );    // unbind ???
    }

    virtual void    redisplay ()
    {
        static  GLuint  buf [] = { 0, 0, 0, 0 };
        static  GLuint  counters [4];
        
        counterBuf.setData ( sizeof ( buf ), buf, GL_DYNAMIC_DRAW );
        
        glClear ( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

        mat4    mv = mat4 :: rotateZ ( toRadians ( rot.z ) ) * mat4 :: rotateY ( toRadians ( rot.y ) ) * mat4 :: rotateX ( toRadians ( rot.x ) );
        mat3    nm = normalMatrix ( mv );
        
        program.bind ();
        program.setUniformMatrix ( "mv",  mv );
        program.setUniformMatrix ( "nm",  nm );

        mesh -> render ();

        program.unbind ();
        
        glFinish ();
        
        counterBuf.getSubData ( 0, sizeof ( buf ), counters );
        
        printf ( "%4d %4d %d %d\n", counters [0], counters [1], counters [2], counters [3] );
    }

    virtual void    reshape ( int w, int h )
    {
        GlutWindow::reshape ( w, h );
        
        glViewport ( 0, 0, (GLsizei)w, (GLsizei)h );
       
        mat4 proj = perspective ( 60.0f, (float)w / (float)h, 0.5f, 20.0f ) * lookAt ( eye, vec3 :: zero, vec3 ( 0, 0, 1 ) );

        program.bind ();
        program.setUniformMatrix ( "proj",  proj );
        program.setUniformVector ( "eye",   eye );
        program.setUniformVector ( "light", light );
        program.unbind ();  
    }
    
    virtual void    mouseMotion ( int x, int y ) 
    {
        rot.y += ((mouseOldY - y) * 180.0f) / 200.0f;
        rot.z += ((mouseOldX - x) * 180.0f) / 200.0f;
        rot.x  = 0;

        if ( rot.z > 360 )
            rot.z -= 360;

        if ( rot.z < -360 )
            rot.z += 360;

        if ( rot.y > 360 )
            rot.y -= 360;

        if ( rot.y < -360 )
            rot.y += 360;

        mouseOldX = x;
        mouseOldY = y;
    }
    
    virtual void    mouseClick ( int button, int state, int modifiers, int x, int y ) 
    {
        if ( state == GLUT_DOWN )
        {
            mouseOldX = x;
            mouseOldY = y;
        }
    }

    virtual void    keyTyped ( unsigned char key, int modifiers, int x, int y )
    {
        if ( key == 27 || key == 'q' || key == 'Q' )    //  quit requested
            exit ( 0 );
    }
    
    virtual void    idle () 
    {
        angle  = 4 * getTime ();

        light.x = 8*cos ( angle );
        light.y = 8*sin ( 1.4 * angle );
        light.z = 8 + 0.5 * sin ( angle / 3 );

        program.bind ();
        program.setUniformVector ( "eye",   eye   );
        program.setUniformVector ( "light", light );
        program.unbind ();

        GlutWindow::idle ();        // for glutPostRedisplay ();
    }
};

int main ( int argc, char * argv [] )
{
                                // initialize glut
    GlutWindow::init( argc, argv, 4, 2 );
    
    MyWindow    win;
    
    GlutWindow::run ();
                                
    return 0;
}

При помощи команды glGetActiveAtomicCounterBufferiv можно получить информацию об атомарных счетчиках используемых данной шейдерной программой.

void glGetActiveAtomicCounterBufferiv( GLuint program, GLuint bufferIndex, 
                                       GLenum pname, GLint * params );

Параметры program и bufferIndex задают шейдерную программу и номер буфера. Максимальное значение для индекса буфера можно получить при помощи команды glGetIntegerv с параметром GL_ACTIVE_ATOMIC_COUNTER_BUFFERSю

Параметр pname задает возвращаемое свойство и может принимать одно из следующих значений:

Максимальное число атомарных счетчиок, допустимых для конкретного типа шейдера, можно узнать при помощи команды glGetIntegerv где параметр принимает одно из следующих значений - GL_MAX_VERTEX_ATOMIC_COUNTERS, GL_MAX_TESS_CONTROL_ATOMIC_COUNTERS, GL_MAX_TESS_EVALUATION_ATOMIC_COUNTERS, GL_MAX_GEOMETRY_ATOMIC_COUNTERS и GL_MAX_FRAGMENT_ATOMIC_COUNTERS. Максимальное общее число атомарных счетчиков можно узнать через команду glGetIntegerv с параметром GL_MAX_COMBINED_ATOMIC_COUNTERS.


По этой ссылке можно скачать весь исходный код к этой статье. Также доступны для скачивания откомпилированные версии для M$ Windows и Linux.