Правильное использование буферов в вычислительных шейдерах OpenGL
Я переписываю алгоритм, который я сначала написал с использованием матричных / векторных операций в ядрах 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 полезен только при совместном использовании данных между вызовами, поэтому я должен сохранить его на одном уровне?
Я с радостью приму любые рекомендации или подсказки о том, где можно оптимизировать эти ядра!
Ответы
Могу ли я улучшить производительность события, сделав его константой времени компиляции?
Вам придется его профилировать. При этом «несколько сотен mat4» - это не «маленький» .
Мне сложно понять, какое значение может быть лучшим для параметров использования буфера. Все буферы записываются и читаются несколько раз, я не уверен, что сюда поставить.
Во-первых, параметры использования касаются вашего использования буферного объекта, а не использования OpenGL памяти, стоящей за ними. То есть, они говорят о функциях , как glBufferSubData
, glMapBufferRange
и так далее. READ
означает, что ЦП будет читать из буфера, но не писать в него. DRAW
означает, что ЦП будет писать в буфер, но не читать из него.
Во-вторых ... тебе все равно. Подсказки по использованию ужасны, плохо определены и используются настолько неправильно, что многие реализации полностью игнорируют их. Реализация NVIDIA GL, вероятно, относится к ним наиболее серьезно.
Вместо этого используйте неизменяемые буферы хранения . Эти «подсказки по использованию» не являются подсказками ; это требования API. Если вы не используете GL_DYNAMIC_STORAGE_BIT
, то вы не можете писать в буфер через glBufferSubData
. И так далее.
Если я правильно понимаю, local_size полезен только при совместном использовании данных между вызовами, поэтому я должен сохранить его на одном уровне?
Нет. На самом деле, никогда не используйте 1. Если все вызовы делают свои собственные вещи, без каких-либо барьеров для выполнения и т.п., тогда вам следует выбрать локальный размер, который эквивалентен размеру волнового фронта оборудования, на котором вы работаете. . Очевидно, это зависит от реализации, но 32 определенно лучше по умолчанию, чем 1.