Правильное использование буферов в вычислительных шейдерах OpenGL

Aug 19 2020

Я переписываю алгоритм, который я сначала написал с использованием матричных / векторных операций в ядрах OpenGL, чтобы попытаться максимизировать производительность.

У меня есть базовые знания OpenGL, поэтому я смог заставить все работать, но у меня много проблем, когда дело доходит до выбора различных вариантов, предлагаемых OpenGL, особенно параметров буфера, которые, я думаю, имеют огромное влияние в моем случае где я читаю и записываю много данных.

Я называю три ядра последовательно:

Первый :

/* Generated constants (for all three shaders): 
 *   #version 430
 *   const vec3 orig
 *   const float vx
 *   const ivec2 size
 *   const uint projections
 *   const uint subIterations
 */
layout(local_size_x = 1, local_size_y = 1) in;

layout(std430, binding = 0) buffer bufferA { //GL_SHADER_STORAGE_BUFFER, GL_DYNAMIC_READ
    uint bufferProjection[]; //Written and read (AtomicAdd) by this shader, read by the second kernel
};
layout(std430, binding = 1) readonly buffer bufferB { //GL_SHADER_STORAGE_BUFFER, GL_DYNAMIC_READ
    uint layer[]; //Written and read by the third kernel, read by this shader and by glGetNamedBufferSubData
};
layout(std140) uniform bufferMat { //GL_UNIFORM_BUFFER, GL_STATIC_DRAW
    mat4 proj_mat[projections*subIterations]; //Read only by this shader and the third
};
layout(location = 0) uniform int z;
layout(location = 1) uniform int subit;

void main() {
    vec4 layer_coords = vec4(orig,1.0) + vec4(gl_GlobalInvocationID.x, z, gl_GlobalInvocationID.y, 0.0)*vx;
    uint val = layer[gl_GlobalInvocationID.y*size.x + gl_GlobalInvocationID.x];
    for(int i = 0; i < projections; ++i) {
        vec4 proj_coords = proj_mat[subit+i*subIterations]*layer_coords;
        ivec2 tex_coords = ivec2(floor((proj_coords.xy*size)/(2.0*proj_coords.w)) + size/2);
        bool valid = all(greaterThanEqual(tex_coords, ivec2(0,0))) && all(lessThan(tex_coords, size));
        atomicAdd(bufferProjection[tex_coords.y*size.x+tex_coords.x+i*(size.x*size.y)], valid?val:0);
    }
}

Второй:

layout(local_size_x = 1, local_size_y = 1) in;

layout(std430, binding = 0) buffer bufferA { //GL_SHADER_STORAGE_BUFFER, GL_DYNAMIC_READ
    float updateProjection[]; //Written by this shader, read by the third kernel
};
layout(std430, binding = 1) readonly buffer bufferB { //GL_SHADER_STORAGE_BUFFER, GL_DYNAMIC_READ
    uint bufferProjection[]; //Written by the first, read by this shader
};
layout(std430, binding = 2) readonly buffer bufferC { //GL_SHADER_STORAGE_BUFFER, GL_DYNAMIC_READ
    uint originalProjection[]; //Only modified by glBufferSubData, read by this shader
};

void main() {
    for(int i = 0; i < projections; ++i) {
        updateProjection[gl_GlobalInvocationID.x+i*(size.x*size.y)] = float(originalProjection[gl_GlobalInvocationID.x+i*(size.x*size.y)])/float(bufferProjection[gl_GlobalInvocationID.x+i*(size.x*size.y)]);
    }
}

В третьих:

layout(local_size_x = 1, local_size_y = 1) in;

layout(std430, binding = 0) readonly buffer bufferA { //GL_SHADER_STORAGE_BUFFER, GL_DYNAMIC_READ
    float updateProjection[]; //Written by the second kernel, read by this shader
};
layout(std430, binding = 1) buffer bufferB { //GL_SHADER_STORAGE_BUFFER, GL_DYNAMIC_READ
    uint layer[]; //Written and read by this shader, read by the first kernel and by glGetNamedBufferSubData
};
layout(std140) uniform bufferMat { //GL_UNIFORM_BUFFER, GL_STATIC_DRAW
    mat4 proj_mat[projections*subIterations]; //Read only by this shader and and the first
};
layout(location = 0) uniform int z;
layout(location = 1) uniform int subit;
layout(location = 2) uniform float weight;

void main() {
    vec4 layer_coords = vec4(orig,1.0) + vec4(gl_GlobalInvocationID.x, z, gl_GlobalInvocationID.y, 0.0)*vx;
    float acc = 0;
    for(int i = 0; i < projections; ++i) {
        vec4 proj_coords = proj_mat[subit+i*subIterations]*layer_coords;
        ivec2 tex_coords = ivec2(floor((proj_coords.xy*size)/(2.0*proj_coords.w)) + size/2);
        bool valid = all(greaterThanEqual(tex_coords, ivec2(0,0))) && all(lessThan(tex_coords, size));
        acc += valid?updateProjection[tex_coords.y*size.x+tex_coords.x+i*(size.x*size.y)]:0;
    }
    float val = pow(float(layer[gl_GlobalInvocationID.y*size.x + gl_GlobalInvocationID.x])*(acc/projections), weight);
    layer[gl_GlobalInvocationID.y*size.x + gl_GlobalInvocationID.x] = uint(val);
}

Что я придумал, прочитав документ OpenGL:

  • Некоторое значение, одинаковое на протяжении всего алгоритма, генерируется как const перед компиляцией шейдера. Особенно полезно для границы цикла
  • bufferMat, который очень мал по сравнению с другими буферами, помещается в UBO, который должен иметь лучшую производительность, чем SSBO. Могу ли я улучшить производительность события, сделав его константой времени компиляции? Маленький, но все же несколько сотен мат4
  • Остальные буферы, которые читаются и записываются несколько раз, должны быть лучше как SSBO
  • Мне сложно понять, какое значение может быть лучшим для параметров использования буфера. Все буферы записываются и читаются несколько раз, я не уверен, что сюда поставить.
  • Если я правильно понимаю, local_size полезен только при совместном использовании данных между вызовами, поэтому я должен сохранить его на одном уровне?

Я с радостью приму любые рекомендации или подсказки о том, где можно оптимизировать эти ядра!

Ответы

1 NicolBolas Aug 19 2020 at 21:51

Могу ли я улучшить производительность события, сделав его константой времени компиляции?

Вам придется его профилировать. При этом «несколько сотен mat4» - это не «маленький» .

Мне сложно понять, какое значение может быть лучшим для параметров использования буфера. Все буферы записываются и читаются несколько раз, я не уверен, что сюда поставить.

Во-первых, параметры использования касаются вашего использования буферного объекта, а не использования OpenGL памяти, стоящей за ними. То есть, они говорят о функциях , как glBufferSubData, glMapBufferRangeи так далее. READозначает, что ЦП будет читать из буфера, но не писать в него. DRAWозначает, что ЦП будет писать в буфер, но не читать из него.

Во-вторых ... тебе все равно. Подсказки по использованию ужасны, плохо определены и используются настолько неправильно, что многие реализации полностью игнорируют их. Реализация NVIDIA GL, вероятно, относится к ним наиболее серьезно.

Вместо этого используйте неизменяемые буферы хранения . Эти «подсказки по использованию» не являются подсказками ; это требования API. Если вы не используете GL_DYNAMIC_STORAGE_BIT, то вы не можете писать в буфер через glBufferSubData. И так далее.

Если я правильно понимаю, local_size полезен только при совместном использовании данных между вызовами, поэтому я должен сохранить его на одном уровне?

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