Yamgine»Blog

Cross Compiling Shader Uniforms, OpenGL 4.2

Hello!

Over the past month I have been working on revamping the rendering system to being something that's actually usable, mainly making it easy to add new shaders and modify them over time, eventually without having to restart the game for a shader modification. My previous approach was the "OpenGL 2.x" style of using GetUniformLocation() to upload data. This ends up requiring a lot of specialized code for each shader you load. Newer OpenGL versions offer some shader reflection which might have made this less painful, but I think I've been able to transcend most of that by uploading all of the discrete uniforms as a block for each shader. I've accomplished this by defining a struct/UBO for each shader, in a file which gets compiled as both C++ and GLSL. Here's a bit of that file.

 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
//shader_uniforms.h

#ifdef CPP
#define layout(a,b)
#define mat4 matrix4
#define vec2 v2
#define vec3 v3
#define vec4 v4
#define uniform struct
#endif

#define YAM_TEXTURE0_LOCATION 16

layout(std140, binding=0) uniform global_uniforms
{
    mat4 View;
    mat4 Projection;
};

#ifdef SIMPLE_TEXTURED_RECT_SHADER
layout(std140, binding=1) uniform simple_textured_rect_uniforms
{
    mat4 Model;
    vec4 Color;
    vec4 UVRect;
};
#endif

// ... another struct per shader


So now on the game side, I can do something like

1
2
3
4
5
6
7
//game_rendering.cpp

simple_textured_rect_uniforms Uniforms = {};
Uniforms.Model = Model;
Uniforms.Color = v4_WHITE;
Uniforms.UVRect = RectMinDim(0,0,1,1);
UseShader(ShaderHandle, &Uniforms);


Very easy! This feels a lot like having a .h and a .cpp, but for shaders.

One snag in this is that samplers (AKA textures) cannot be a part of a UBO. So in each shader that uses textures I make sure to force the textures to bind to a YAM_TEXTUREXXX_LOCATION so I know where to bind to them on the game side. So after UseShader I have a UseTexture call.

This system does make shader compilation tricky because I have to cobble the shader together, with a preamble of

1
#define MY_SHADER

then the global_uniforms file,
then the actual shader file.

The define ensures that ONLY the UBO for that shader is compiled in.

This isn't too bad though, I'll take complexity in the start up code over having it in the runtime code.

A disclaimer, this wasn't by invention, I first ran into this style of data management in RenderDoc and decided to give it a try for myself.
Mārtiņš Možeiko, Edited by Mārtiņš Možeiko on
Be careful - std140 layout rules are different than C/C++ struct member layout rules. That means your variables will have different offsets than GL expects and nothing will work.
Yam
James Fulop,
Yes I have already had to deal with this, one of my other uniform blocks looks like
1
2
3
4
5
6
7
8
9
layout(std140, binding=1) uniform mesh_recieve_shadows_uniforms
{
...
    vec3 LightDir;
    int dummy1;
    
    vec3 LightColor;
    int dummy2;
};

do you know if there's some other setting that makes them both behave the same?
Mārtiņš Možeiko, Edited by Mārtiņš Možeiko on
There's no such GLSL layout qualifier that will match C layout exactly. There is std430 layout defined in ARB_shader_storage_buffer_object extension, that will do a bit tighter packing (aligned to 4 bytes), but that still won't match exactly to C layout, but its closer.

You could "fix" this on C side by specifying explicit alignment for all members:
1
2
3
4
struct s {
  ALIGN_MACRO(16) vec3 LighDir;
  ALIGN_MACRO(16) vec3 LigtColor;
};

where ALIGN_MACRO(X) is __declspec(align(X)) for MSVC and __attribute__((aligned(X))) for GCC/Clang.

Then compiler will insert automatic padding for unaligned members.

And I believe C++11 introduces alignas(X) that should work for all three - msvc/gcc/clang. And C11 has _Alignas(X), but that will work only in gcc/clang.
Yam
James Fulop,
Thanks for the info! That does seem like a cleaner solution since I will almost certainly forget the manual padding sometime in the future.