andersch.dev

<2024-05-29>

Hot-Reloadable, Embedded Shader Code in C/C++

How to include a GLSL shader as a string inside your code (and still make it hot-reloadable)

Most OpenGL tutorials that start you out on shaders will tell you to include your first shaders as string literals by writing out the GLSL code out like this:

const char* vertex_shader_source = "#version 330 core\n"
    "layout(location = 0) in vec3 aPos;\n"
    "void main() {\n"
    "    gl_Position = vec4(aPos, 1.0);\n"
    "}\0";

This is done because it has the benefit of being able to pass over file IO & parsing code and instead focus on teaching actual OpenGL/GLSL specific concepts. It would also be useful for fast prototyping with more complex shaders, if it wasn't for the fact that…

  • Writing out "...\n" for every line is tedious and easy to forget
  • You don't get any syntax highlighting for the GLSL code
  • It makes shader hot-reloading impossible (or does it?)

I was exploring better ways on how to include the shader without having to write some bespoke shader management code that loads in files, allocates memory for the string, appends null terminators, watches for file changes and so on. To my surprise, I stumbled upon my now preferred way of hot-reloading shaders.

A Better Way of Embedding Shader Code

My requirements for including the GLSL code:

  • It shouldn't rely on an additional preprocessing step (e.g. invoking a tool in your CMakeLists.txt or adding something to your build script)
  • I should have the option of having the shader in a separate file
  • Preserving (some) syntax highlighting would be nice
  • It should be as portable as possible across both C and C++

In C++11 (and apparently some versions of gcc via GNU extensions), you can have raw string literals of the following form:

const char* fragment_shader_source = R"(
    #version 330
    int main()
    {
        // ...
    }
)";

This is a step up, but it is not fully portable and the editor will still highlight it as just a string. Instead, all following solutions build on a stringify macro that includes the #version directive by default. This needs to be done because the # character will always be interpreted as a C preprocessor directive when it is the first non-whitespace character in a source file.

#define SHADER_STRINGIFY(x) "#version 330\n" #x

Using this macro, you have the option of specifying shader code inline without having to write out quotes or newlines:

// first option: shader src code as an inlined string
const char* inline_shader = SHADER_STRINGIFY(
    uniform mat4 u_mvp;
    in vec3 in_pos;
    void main()
    {
        int foo = 5;
        gl_Position = u_mvp * vec4(in_pos, 1);
    }
);

As you see, the C syntax highlighting should apply for this code and - depending on your colorscheme (mine isn't that colorful, admittedly) - can do a decent job of highlighting GLSL code.

As a second option, we can write out the shader code in its own file and wrap it inside the stringify macro:

SHADER_STRINGIFY(
uniform mat4 u_mvp;
in vec3 in_pos;
void main()
{
    int foo = 5;
    gl_Position = u_mvp * vec4(in_pos, 1);
}
)

We can then #include this file in our C/C++ source code like so:

// second option: shader src code as an included file
const char* file_shader =
  #include "shader.vert"
;

If you prefer not to have a dangling semicolon, you can instead write it out at the end of the shader file. However, using the version without the semicolon at the end lets you write code using initializers:

typedef struct shader_t
{
    const char* name; // shader name
    const char* code; // shader source code as a string
} shader_t;

shader_t shader =
{
    "Shader Name",
    #include "shader.vert"
};

If you like to have both your fragment and vertex shader in the same .glsl file, you can do that as well. This time, we can use the fact that any line starting with a # is interpreted as a preprocessor directive, so that we can write our shader.glsl like so:

SHADER_STRINGIFY(
#if defined(VERT_SHADER)
    // vertex shader code
#undef VERT_SHADER
#elif defined(FRAG_SHADER)
    // fragment shader code
#undef FRAG_SHADER
#endif
)

To include the shaders in your code:

#define VERT_SHADER
const char* vertex_shader_source =
      #include "shader.glsl"
    ;

#define FRAG_SHADER
const char* fragment_shader_source =
      #include "shader.glsl"
    ;

Limitations

Shader code will be stored with all newline characters missing (except the one after the #version directive):

#version 330
in vec3 aPos; void main() { gl_Position = vec4(aPos, 1); }

This means you won't get matching line numbers from the shader compiler in case of an error. If you are using C++11 or higher, you can use the R"()" method for your included GLSL files, which will preserve line numbers.1

R"(
#version 330

in vec3 aPos;

void main()
{
    gl_Position = vec4(aPos, 1);
}
)"

Hot-Reloading Embedded Shaders

Usually, you wouldn't be able to hot-reload shaders that are included in your source code. After all, they are baked into the executable. But what if we just reload the entirety of our code using DLL-based code hot-reloading? This way, we get a buffer to the string of a new shader anytime we recompile.

Instead of having code that checks several shader files for modifications times or setting up file watchers, we only check the .dll or .so for changes.

All you need to do is to recompile and link the shader program again after you have loaded in the new DLL.

void* dll_handle = dlopen("code.dll", RTLD_NOW);

if (dll_handle == NULL) { printf("Opening DLL failed. Trying again...\n"); }
while (dll_handle  == NULL)
{
    dll_handle = dlopen(DLL_FILENAME, RTLD_NOW);
}

// fill function pointers
create_shaders = (void (*)(state_t*)) dlsym(dll_handle, "create_shaders");

// reload all shaders
create_shaders(&state);

If you don't want to compile all shaders again and instead only the ones that have changed, you could either compare all old source code strings against the new ones 2 or check for new file modification timestamps and only reload the corresponding shaders. But at that point, you would probably be better of implementing a conventional shader management system that loads in shaders as real text files.

Footnotes:

1

Make sure to include no GLSL code on the very first line, only R"(

2

Make sure to exclude SHADER_STRINGIFY(...) from the string_equals() check if comparing at the file level