Search code examples
c++opengl-esglslwebgl2

How to use multiple Uniform Buffer Objects


In my OpenGL ES 3.0 program I need to have two separate Uniform Buffer Objects (UBOs). With just one UBO, things work as expected. The code for that case looks as follows:

GLSL vertex shader:

version 300 es

layout (std140) uniform MatrixBlock
{
  mat4 matrix[200];
};

C++ header file member variables:

GLint  _matrixBlockLocation;
GLuint _matrixBuffer;

static constexpr GLuint _matrixBufferBindingPoint = 0;

glm::mat4 _matrixBufferContent[200];

C++ code to initialze the UBO:

_matrixBlockLocation = glGetUniformBlockIndex(_program, "MatrixBlock");

glGenBuffers(1, &_matrixBuffer);
glBindBuffer(GL_UNIFORM_BUFFER, _matrixBuffer);
glBufferData(GL_UNIFORM_BUFFER, 200 * sizeof(glm::mat4), _matrixBufferContent, GL_DYNAMIC_DRAW);
glBindBufferBase(GL_UNIFORM_BUFFER, _matrixBufferBindingPoint, _matrixBuffer);
glUniformBlockBinding(_program, _matrixBlockLocation, _matrixBufferBindingPoint);

To update the content of the UBO I modify the _matrixBufferContent array and then call

glBufferSubData(GL_UNIFORM_BUFFER, 0, 200 * sizeof(glm::mat4), _matrixBufferContent);

This works as I expect it. In the vertex shader I can access the matrices and the resulting image is as it should be.


The OpenGL ES 3.0 specification defines that the maximum available storage per UBO is 16K (GL_MAX_UNIFORM_BLOCK_SIZE). Because the size of my matrix array comes close to that limit I want to create a second UBO that stores additional data. But as soon as I add that second UBO I encounter problems. Here's the code to create the two UBOs:

GLSL vertex shader:

version 300 es

layout (std140) uniform MatrixBlock
{
  mat4 matrix[200];
};

layout (std140) uniform HighlightingBlock
{
  int highlighting[200];
};

C++ header file member variables:

GLint  _matrixBlockLocation;
GLint  _highlightingBlockLocation;
GLuint _uniformBuffers[2];

static constexpr GLuint _matrixBufferBindingPoint       = 0;
static constexpr GLuint _highlightingBufferBindingPoint = 1;

glm::mat4 _matrixBufferContent[200];
int32_t   _highlightingBufferContent[200];

C++ code to initialize both UBOs:

_matrixBlockLocation       = glGetUniformBlockIndex(_program, "MatrixBlock");
_highlightingBlockLocation = glGetUniformBlockIndex(_program, "HighlightingBlock");

glGenBuffers(2, _uniformBuffers);
glBindBuffer(GL_UNIFORM_BUFFER, _uniformBuffers[0]);
glBufferData(GL_UNIFORM_BUFFER, 200 * sizeof(glm::mat4), _matrixBufferContent, GL_DYNAMIC_DRAW);
glBindBuffer(GL_UNIFORM_BUFFER, _uniformBuffers[1]);
glBufferData(GL_UNIFORM_BUFFER, 200 * sizeof(int32_t), _highlightingBufferContent, GL_DYNAMIC_DRAW);
glBindBufferBase(GL_UNIFORM_BUFFER, _matrixBufferBindingPoint, _uniformBuffers[0]);
glBindBufferBase(GL_UNIFORM_BUFFER, _highlightingBufferBindingPoint, _uniformBuffers[1]);
glUniformBlockBinding(_program, _matrixBlockLocation, _matrixBufferBindingPoint);
glUniformBlockBinding(_program, _highlightingBlockLocation, _highlightingBufferBindingPoint);

To update the first UBO I still modify the _matrixBufferContent array but then call

glBindBuffer(GL_UNIFORM_BUFFER, _uniformBuffers[0]);
glBufferSubData(GL_UNIFORM_BUFFER, 0, 200 * sizeof(glm::mat4), _matrixBufferContent);

To update the second UBO I modify the content of the _highlightingBufferContent array and then call

glBindBuffer(GL_UNIFORM_BUFFER, _uniformBuffers[1]);
glBufferSubData(GL_UNIFORM_BUFFER, 0, 200 * sizeof(int32_t), _highlightingBufferContent);

From what I see, the first UBO still works as expected. But the data that I obtain in the vertex shader is not what I originally put into _highlightingBufferContent. If I run this code as WebGL 2.0 code I'm getting the following warning in Google Chrome:

GL_INVALID_OPERATION: It is undefined behaviour to use a uniform buffer that is too small.

In Firefox I'm getting the following:

WebGL warning: drawElementsInstanced: Buffer for uniform block is smaller than UNIFORM_BLOCK_DATA_SIZE.

So, somehow the second UBO is not properly mapped somehow. But I'm failing to see where things go wrong. How do I create two separate UBOs and use both of them in the same vertex shader?


Edit

Querying the value behind GL_UNIFORM_BLOCK_DATA_SIZE that is expected by OpenGL reveals that it needs to be 4 times bigger than it is now. Here's how I query the values:

GLint matrixBlock       = 0;
GLint highlightingBlock = 0;
glGetActiveUniformBlockiv(_program, _matrixBlockLocation, GL_UNIFORM_BLOCK_DATA_SIZE, &matrixBlock);
glGetActiveUniformBlockiv(_program, _highlightingBlockLocation, GL_UNIFORM_BLOCK_DATA_SIZE, &highlightingBlock);

Essentially, this means that the buffer size must be

200 * sizeof(int32_t) * 4

and not just

200 * sizeof(int32_t)

However, it is not clear to me why that it. I'm putting 32-bit integers into that array which I'd expect to be 4 byte in size but they seem to be 16 bytes long somehow. Not sure yet what is going on.


Solution

  • As hinted to by the edit section of the question and by Beko's comment, there are specific alignment rules associated with OpenGL's std140 layout. The OpenGL ES 3.0 standard specifies the following:

    1. If the member is a scalar consuming N basic machine units, the base alignment is N.
    2. If the member is a two- or four-component vector with components consuming N basic machine units, the base alignment is 2N or 4N, respectively.
    3. If the member is a three-component vector with components consuming N basic machine units, the base alignment is 4N.
    4. If the member is an array of scalars or vectors, the base alignment and array stride are set to match the base alignment of a single array element, according to rules (1), (2), and (3), and rounded up to the base alignment of a vec4. The array may have padding at the end; the base offset of the member following the array is rounded up to the next multiple of the base alignment.

    Note the emphasis "rounded up to the base alignment of a vec4". This means every integer in the array does not simply occupy 4 bytes but instead occupies the size of a vec4 which is 4 times larger.

    Therefore, the array must be 4 times the original size. In addition, it is necessary to pad each integer to the corresponding size before copying the array content using glBufferSubData. If that is not done, the data is misaligned and hence gets misinterpreted by the GLSL shader.