Skip to content

Shader Compilation

CF uses SDL_Gpu under the hood for rendering. CF ships its own shader compiler, cute_spirv, which compiles a well-defined subset of GLSL 450 (see GLSL Support) directly to SPIR-V with zero external dependencies -- nothing to fetch, nothing to install. The SPIR-V is then translated to whatever the active backend needs (DXBC for D3D12, MSL for Metal, GLSL ES 300 for GLES/WebGL2).

Runtime Shader Compilation

CF compiles shaders of the format GLSL 450 (see GLSL Support for the supported subset). Runtime compilation is always available on every platform, including web builds -- the compiler is tiny, fast, and part of CF itself, so there is no build option to disable it and nothing to install. You can compile a shader by calling cf_make_shader_from_source.

Precompiling Shaders

CF also provides an offline compiler called cute-shaderc. Precompiling is purely optional -- runtime compilation covers most projects -- but it shaves shader load time to zero at startup, which can matter for games with many shaders.

Note

By default (when using CMake) CF_CUTE_SHADERC is set to ON which will output cute-shaderc, an executable for precompiling shaders, in the same directory as where the cute library is placed when building CF. You may freely take a copy of cute-shaderc and place/use it wherever you like to support precompiled shaders.

Usage: cute-shaderc [options] <input>
Compile GLSL into SPIRV bytecode and/or generate a C header for embedding.

--help             Print this message.
-I<dir>            Add directory to #include search path.
-type=<type>       The shader type. Valid values are:
                   * draw (default): Draw shader for `cf_make_draw_shader_from_bytecode`.
                   * vertex: Standalone vertex shader for `cf_make_shader_from_bytecode`.
                   * fragment: Standalone fragment shader for `cf_make_shader_from_bytecode`.
-oheader=<file>    Where to write the C header file.
                   Also requires -varname.
-varname=<file>    The variable name inside the C header.
-obytecode=<file>  (Optional) Where to write the raw SPIRV blob.
-nogles            Omit the GLSL ES 300 output from generated headers. Smaller
                   headers; the bytecode then cannot be used on GLES3/WebGL2.
-verbose           Embed the preprocessed shader source as a comment in
                   generated headers (for debugging).

Example (compiles my_shader.glsl to a C header):
cute-shaderc -I./my_shaders -type=draw -oheader=my_shader.h -varname=my_shader my_shader.shd

-oheader= indicates where you want to output the header file.

-varname= indicates the name of the static variable of the type CF_ShaderBytecode. This variable will be defined in the generated header. It can be passed to related shader functions (explained below).

The -type= flag indicates which type of shader you want to compile:

The -I flag will be explained in the "Shader inclusion" section below.

In case you need the raw SPIRV blob, -obytecode= can also be used. Take note that this is only available for shaders of type vertex or fragment.

Working with cute-shaderc

A common pattern is to compile shaders at runtime during development (edit the .shd file, rerun, done) and switch to precompiled bytecode when shipping. Use your own build flag to pick:

#ifdef SHIP_IT // Your own build flag.
#include "custom_draw_shd.h" // Include the header generated by cute-shaderc
#endif

int main() {
#ifndef SHIP_IT
    // Load and compile the shader from disk at runtime.
    cf_shader_directory("/shaders");
    CF_Shader custom_draw_shader = cf_make_draw_shader("custom_draw.shd");
#else
    // Use the precompiled shader.
    CF_Shader custom_draw_shader = cf_make_draw_shader_from_bytecode(s_custom_draw_shd);
#endif
}

(For backwards compatibility CF still defines the CF_RUNTIME_SHADER_COMPILATION macro -- it is now always defined, since runtime compilation is always available.)

In order to keep the generated header in sync, add the following into your CMakeLists.txt:

if (SHIP_IT)
    add_custom_command(
        # Replace with where you want to output the header
        OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/src/custom_draw_shd.h
        COMMAND cute-shaderc  # The path is set automatically by CMake
            # Replace with your shader directory
            -I${CMAKE_CURRENT_SOURCE_DIR}/shaders
            # Other types are possible
            -type=draw
            # What you want the variable name to be
            -varname=s_flash_shd_bytecode
            # Repeat the path above
            -oheader=${CMAKE_CURRENT_SOURCE_DIR}/src/custom_draw_shd.h
            # Replace with path to the shader file
            ${CMAKE_CURRENT_SOURCE_DIR}/shaders/custom_draw.shd
        DEPENDS shaders/custom_draw.shd  # Rebuild when the shader source change
        DEPENDS cute-shaderc  # Rebuild when the compiler is updated
    )
endif ()

# Make sure that your program depends on the generated header
set(SOURCES
    # Other source files
    src/main.c
    # Generated headers
    src/custom_draw_shd.h
)
add_executable(my_game SOURCES)

Shader Inclusion #include

To make reusable utility functions, CF supports shaders including each other with the #include directive.

"Include guard", usually seen in C/C++, is not needed (e.g. #pragma once). Each file will only be included once and subsequent inclusions are ignored.

With online compilation, the include directory must be set with cf_shader_directory. For example: cf_shader_directory("/shaders"). Take note that this is a path in the VFS, hence, the leading slash ('/'). When shaders inclusions occur they always search relative to this shader directory, and never search outside of it. You may organize your shaders within the shader directory however you like, but they cannot exist outside the shader directory.

With offline compilation, the include directory is set with the -I flag. You run the shader compiler on the command line, after building the shader compiler scute-shaderc. For example: cute-shaderc -Ishaders -o src/my_shader_shd.h my_shader.shd. Take note that this is a path in your actual filesystem, and not a path in the VFS. The include directory is relative to wherever you run the command.

When using CMake, prefix the path with ${CMAKE_CURRENT_SOURCE_DIR} to make it independent of the build directory.

CF also provides several builtin utility modules: gamma.shd, distance.shd, smooth_uv.shd, blend.shd. These can always be #include-d by your shader without setting the include path. You can view these files by looking at CF's source code to see what sort of extra helper functions are available for use in your shaders.

Resource Set Layout

All shaders compiled by CF (whether at runtime or precompiled) must assign resources to specific descriptor sets. These sets are dictated by SDL_GPU's SPIR-V binding convention and vary by shader stage.

Graphics Shaders (Vertex + Fragment)

Resource type Vertex shader Fragment shader
Sampled textures, storage textures, storage buffers set = 0 set = 2
Uniform buffers set = 1 set = 3

Example vertex shader:

layout (set = 0, binding = 0) uniform sampler2D u_image;
layout (set = 1, binding = 0) uniform uniform_block {
    vec2 u_texture_size;
};

Example fragment shader:

layout (set = 2, binding = 0) uniform sampler2D u_image;
layout (set = 3, binding = 0) uniform uniform_block {
    vec2 u_texture_size;
};

Compute Shaders

Resource type Set
Sampled textures, then readonly storage textures, then readonly storage buffers set = 0
Read-write storage textures, then read-write storage buffers set = 1
Uniform buffers set = 2

Example compute shader:

layout (set = 0, binding = 0) uniform sampler2D u_input;
layout (set = 1, binding = 0, rgba8) uniform writeonly image2D u_output;
layout (set = 2, binding = 0) uniform uniform_block {
    float u_time;
};
layout (local_size_x = 16, local_size_y = 16, local_size_z = 1) in;

When multiple resources share the same set, they are ordered by binding index. For example, if a compute shader has both a sampled texture and a readonly storage texture on set 0:

layout (set = 0, binding = 0) uniform sampler2D u_sampled;
layout (set = 0, binding = 1) readonly uniform image2D u_storage;

Barrier Safety

When using shared memory with barrier() in compute shaders, all threads in a workgroup must reach each barrier() call. Do not use return to exit threads before a barrier. Instead, let all threads participate in the loop/barrier and guard side-effects (like imageStore) behind a bounds check at the end.

Error Reporting

Errors in custom draw shaders loaded from disk are reported under the shader's own path with correct line numbers (e.g. my_shader.shd:12: error: ...). Shaders compiled from an in-memory string report as shader_stub.shd since there is no path to name.

The most recent compile error is always available from cf_shader_compile_error. You can also register a callback with cf_shader_on_error to be invoked whenever any shader compile fails -- including failures during automatic live-reload -- which makes it easy to display shader errors on-screen while iterating.

Bytecode Stability

The CF_ShaderBytecode struct, whether coming from cf_compile_shader_to_bytecode or the cute-shaderc compiler, should be treated as opaque. It should not be modified in any way and only passed verbatim to related functions: cf_make_shader_from_bytecode and cf_make_draw_shader_from_bytecode.

The signatures of these functions are stable, but the inner structure of the bytecode may change between CF versions -- recompile your shaders with the matching cute-shaderc when updating CF.