Using OpenGL uniforms/attributes/outputs in more efficient way

If few last episodes Casey has been struggling to use uniform locations/outputs in more efficient way. I want to describe here how this can be done in modern OpenGL (meaning GL3 and up).

Let's say we have simple vertex shader with two attributes like this:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// vertex shader
#version 330 core
uniform mat4 uPosTransform;
uniform mat2 uTexTransform;
in vec4 aPos;
in vec2 aTex;
out vec2 vTex; // varying passed to fragment shader
void main()
{
  vTex = uTexTransform * aTex;
  gl_Position = uPosTransform * aPos;
}

and simple fragment shader like this:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// fragment shader
#version 330 core
in vec2 vTex; // varying from vertex shader
uniform sampler2D sTex;

out vec4 oColor;
out vec2 oTex; // lets output texture coordinates, just for fun
void main()
{
  oTex = vTex;
  oColor = texture(sTex, vTex);
}

Most older OpenGL tutorials recommend querying positions on vertex attributes, uniforms and output locations like this:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// before linking we bind output locations to specific indexes
glBindFragDataLocation(prog, 0, "oColor");
glBindFragDataLocation(prog, 1, "oTex");

glLinkProgram(prog);

// after linking program we can query
GLuint uPosTransformLocation = glGetUniformLocation(prog, "uPosTransform");
GLuint uTexTransformLoGLuint aPosLocation = glGetUniformLocation(prog, "aPos");
GLuint sTexLocation = glGetUniformLocation(prog, "sTex");
GLuint aPosLocation = glGetAttribLocation(prog, "aPos");
GLuint aTexLocation = glGetAttribLocation(prog, "aTex");

You can see that we explicitly want to have index 0 for color output buffer, and index 1 for texture coordinate output buffer.
But all the attributes and uniforms are queried dynamically - index returned needs to be used with glVertexAttribPointer and glUniform* functions. That's not very nice. Querying state always should be avoided as much as possible.

For vertex attributes there is alternative to also bind them explicitly before linking program (this is available since at least OpenGL version 2):
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// before linking we bind vertex shader attributes and fragment shader outputs
glBindFragDataLocation(prog, 0, "oColor");
glBindFragDataLocation(prog, 1, "oTex");
glBindAttribLocation(prog, 0, "aPos");
glBindAttribLocation(prog, 1, "aTex");

glLinkProgram(prog);

// after linking program we can query uniforms
GLuint uPosTransformLocation = glGetUniformLocation(prog, "uPosTransform");
GLuint uTexTransformLoGLuint aPosLocation = glGetUniformLocation(prog, "aPos");
GLuint sTexLocation = glGetUniformLocation(prog, "sTex");

Now only uniform locations are queried. Why this is good? Because this allows you to share vertex attribute pointer bindings between different programs. If you know that location 0 is position and location 1 is texture coordinate, then there's no need to call glVertexAttribPointer if you need to use draw call with exactly same data but just with different shader. With glGetAttribLocation there is no such guarantee.

There is actually better way - you can put these attribute and fragment output locations directly in shader, this way you accidentally won't forget to bind or query them:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// vertex shader
#version 330 core
uniform mat4 uPosTransform;
uniform mat2 uTexTransform;
layout(location=0) in vec4 aPos;
layout(location=1) in vec2 aTex;
out vec2 vTex;
void main()
{
  vTex = uTexTransform * aTex;
  gl_Position = uPosTransform * aPos;
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// fragment shader
#version 330 core
in vec2 vTex;
uniform sampler2D sTex;

layout(location=0) out vec4 oColor;
layout(location=1) out vec2 oTex;
void main()
{
  oTex = vTex;
  oColor = texture(sTex, vTex);
}

This is nice! Our C/C++ code is now much shorter:
1
2
3
4
5
6
glLinkProgram(prog);

// after linking program we can query uniforms
GLuint uPosTransformLocation = glGetUniformLocation(prog, "uPosTransform");
GLuint uTexTransformLoGLuint aPosLocation = glGetUniformLocation(prog, "aPos");
GLuint sTexLocation = glGetUniformLocation(prog, "sTex");

This feature - locations for attributes and fragment outputs is available in GL version 3.3. Or for previous versions ARB_explicit_attrib_location extension.

So what to do with uniforms?

There is a way to specify explicit locations for uniforms. Unfortunately this is is not in GL version 3. It is available only in OpenGL version 4.3. Or it requires ARB_explicit_uniform_location extension. As far as I know - most GL3 capable GPU's support this extension. That's a good news. It means that you pretty much need to install latest GPU driver and you're good.

Here's how shader code would look like:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// vertex shader
#version 330 core
#extension GL_ARB_explicit_uniform_location : require
layout(location=0) uniform mat4 uPosTransform;
layout(location=1) uniform mat2 uTexTransform;
layout(location=0) in vec4 aPos;
layout(location=1) in vec2 aTex;
out vec2 vTex;
void main()
{
  vTex = uTexTransform * aTex;
  gl_Position = uPosTransform * aPos;
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// fragment sahder
#version 330 core
in vec2 vTex;
uniform sampler2D sTex;
layout(location=0) out vec4 oColor;
layout(location=1) out vec2 oTex;
void main()
{
  oTex = vTex;
  oColor = texture(sTex, vTex);
}

This is nice! Our C/C++ code is now much shorter:
1
2
3
4
glLinkProgram(prog);

// after linking program we can query uniforms for samplers
GLuint sTexLocation = glGetUniformLocation(prog, "sTex");

Much better, right? Only thing left is a sampler. Unfortunately this extension doesn't apply to sampler uniforms. Those you'll need to query and assign with glUniform calls.

But there is another extension that helps - ARB_shading_language_420pack. This extension is in OpenGL version 4.2, but same as before - all GL3 capable GPUs support it, so it's only question of using latest driver. That's again good news. The fragment shader now will look like this:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// fragment shader
#version 330 core
#extension GL_ARB_shading_language_420pack : require
in vec2 vTex;
layout(binding=0) uniform sampler2D sTex; // be careful - it's "binding", not "location"
layout(location=0) out vec4 oColor;
layout(location=1) out vec2 oTex;
void main()
{
  oTex = vTex;
  oColor = texture(sTex, vTex);
}

Now C/C++ code requires only linking, no querying at all:
1
glLinkProgram(prog);

This is pretty nice. All locations are explicitly set in GLSL shader, and we'll know what is exactly where in our C/C++ code.

There is a way to avoid ARB_explicit_uniform_location extension, and just use ARB_shading_language_420pack. To do that we'll need to use uniform buffer that was introduced in GL version 3. These buffers are similar to vertex/index buffers. Instead of submitting data bit by bit, you simply upload your data to your buffer (glBufferData/glBufferSubData/glMapBuffer) and then simply bind to shader. It would look like this:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#version 330 core
#extension GL_ARB_explicit_uniform_location : require // if you don't have this then remove binding=0 on next line
layout(std140, binding=0) uniform uTransforms
{
    mat4 Pos; // be careful about layout, each member is aligned to 16-bytes
    mat4 Tex; // so not exactly same as in C language
};
layout(location=0) in vec4 aPos;
layout(location=1) in vec2 aTex;
out vec2 vTex;
void main()
{
  vTex = uTransforms.Pos * aTex;
  gl_Position = uTransforms.Tex * aPos;
}

The code to use create buffer and pass the data looks like this:
1
2
3
4
5
6
7
GLuint ubuf;
glGenBuffers(&ubuf, 1);
glBindBuffer(GL_UNIFORM_BUFFER, ubuf);
GLuint bufferSlot = 0; // similar stuff to glActiveTexture(GL_TEXTURE0)
GLuint bufferLocation = 0; // this is value from binding attribute in GLSL shader
glUniformBlockBinding(prog, bufferLocation, bufferSlot);
glBindBufferBase(GL_UNIFORM_BUFFER, bufferLocation, ubuf);

glUniformBlockBinding function is something similar to glActiveTexture call - it sets to which slot (bufferSlot, arbitrary value, at least 24 guaranteed) you want to bind specific uniform buffer binding (bufferLocation). Then glBindBufferBase binds actual buffer object (ubuf) to bufferLocation binding. Double indirection fun.

Now whenever you need to update data in buffer you just upload new data:
1
2
3
struct UniformData { mat4 pos; mat4 tex; };
UniformData data = ...;
glBufferData(GL_UNIFORM_BUFFER, sizeof(data), &data, GL_DYNAMIC_DRAW); // adjust GL_DYNAMIC_DRAW as needed

Make sure the order of members is the same as in GLSL source, and take care of alignment.
Basically shaders now have four different type of bindings (which are independent, meaning index 0 means different thing for each type) - vertex attributes, uniform buffers, uniform samplers, fragment outputs.

This approach reduces amount of functions you need to call - you can group uniforms you want to update, and just call one function per group. You can split uniforms in multiple buffers - one for things that doesn't change or change super rare (window size, projection matrix), some which change every few frames (mouse position?) and some that changes every frame (player position). And upload data only when they change, not every frame.

You cannot put samplers in uniform buffers, they need to be defined outside and location needs to be bound with help of ARB_shading_language_420pack extension, or queried with glGetUniformLocation.

Unfortunately Casey's GPU driver doesn't support ARB_explicit_uniform_location and also no ARB_shading_language_420pack :(

If you don't have ARB_shading_language_420pack extension, then this approach is still better. Because with uniform buffers you can query location only once per buffer, not per every uniform. You do that with glGetUniformBlockIndex function:
1
2
3
4
5
glLinkProgam(prog);

// query uniform locations after linking
GLuint bufferLocation = glGetUniformBlockIndex(prog, "uTransforms"); // uniform buffer
GLuint sTexLocation = glGetUniformLocation(prog, "sTex"); // sampler

Using this function will require no extension outside of OpenGL 3.3 spec.

In summary:
* specify vertex attribute locations explicitly in GLSL vertex shader
* specify fragment attribute locations explicitly in GLSL fragment shader
* if you don't like specifying locations for vertex attributes or fragment outputs in GLSL shader source, bind them before linking program. Do not query them after linking.

Doing that allows to use vertex data for multiple shaders without specifying pointers/framebuffer outputs multiple times.

* if you are OK with GL_ARB_explicit_uniform_location and ARB_shading_language_420pack location - specify uniform locations and sampler bindings in GLSL shader
* or better - use uniform buffers and just ARB_shading_language_420pack extension to specify only sampler and uniform buffer bindings
* if you want to use no extensions, use uniform buffers, and query only for sampler locations, and bind uniform buffer locations before linking GLSL program

Using uniform buffers should allow you to update uniforms in bulk.

Bonus tips&tricks - so now when you have all/most of locations explicitly set in GLSL shader, you think you'll need to duplicate number in GLSL code and in C/C++ code for glVertexAttribPointer or glUniform* calls. You can avoid that by simply using #define's when compiling shader. Here's example for vertex attributes

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// GLSL vertex shader, in C++11 string literal
const char* vertex_shader = R"(
  layout(location=V_ATTRIB_POS) in vec4 aPos;
  layout(location=V_ATTRIB_TEX) in vec2 aTex;
  ...
)";

// C/C++ code

// some constant section
#define V_ATTRIB_POS 0
#define V_ATTRIB_TEX 1

// C stringify macro nonsense
#define STR2(x) #x
#define STR(x) STR2(x)

// source for GLSL compiler
// you can prepare this array at runtime to support multiple shaders with different attributes
const char* glsl_source[] = {
    "#extension 330 core\n",
    "#define V_ATTRIB_POS " STR(V_ATTRIB_POS) "\n",
    "#define V_ATTRIB_TEX " STR(V_ATTRIB_TEX) "\n",
    vertex_shader,
};
glShaderSource(vsh, 4, glsl_source, NULL);
...

// somewhere later in code
glVertexAttribPointer(V_ATTRIB_POS, 4, GL_FLOAT, ...);

Now you won't mistake by using wrong index for glVertexAttribPointer calls. And all these indices are compile-time constants. No need to query them in variables and read variables every time you need to bind vertex pointers. You can do same thing with uniforms.

Edited by Mārtiņš Možeiko on
Good write up. Thanks for laying this info out. I hope we can get things like archived someday if that feature request comes through.

For now clipping this to my Evernotes.

In my engine as well I had started to move to this style its good to see this info all in one place.