Shaders

#include <safi/render/shader.h>

Shaders are authored once in HLSL and compiled at build time into per-stage SPIR-V (.spv) and MSL (.msl) artifacts by cmake/SafiShaders.cmake (via glslangValidator + spirv-cross). At runtime, safi_shader_load inspects SDL_GetGPUShaderFormats() and picks the artifact matching the active backend — the caller only knows the logical shader name.

Functions

safi_shader_load

SDL_GPUShader *safi_shader_load(SafiRenderer    *r,
                                const char      *shader_dir,
                                const char      *name,
                                const char      *entrypoint,
                                SafiShaderStage  stage,
                                uint32_t         num_samplers,
                                uint32_t         num_uniform_buffers,
                                uint32_t         num_storage_buffers,
                                uint32_t         num_storage_textures);

Loads <shader_dir>/<name>.<vert|frag>.<spv|msl> based on the device's supported formats. Returns a ready-to-bind SDL_GPUShader * or NULL on failure.

The four trailing counts tell SDL_gpu how many of each resource the shader will bind at draw time — they must match the register() declarations in your HLSL.

safi_shader_create_blob

SDL_GPUShader *safi_shader_create_blob(SafiRenderer      *r,
                                       const void        *bytes,
                                       size_t             size,
                                       SDL_GPUShaderFormat format,
                                       const char        *entrypoint,
                                       SafiShaderStage    stage,
                                       uint32_t           num_samplers,
                                       uint32_t           num_uniform_buffers,
                                       uint32_t           num_storage_buffers,
                                       uint32_t           num_storage_textures);

Low-level: creates a GPU shader directly from a pre-compiled byte blob of a known format. Prefer safi_shader_load unless you are shipping embedded bytecode.

Stages

typedef enum SafiShaderStage {
    SAFI_SHADER_STAGE_VERTEX,
    SAFI_SHADER_STAGE_FRAGMENT,
} SafiShaderStage;

Compute shaders aren't wrapped yet — call SDL_CreateGPUComputePipeline directly if you need them.

HLSL binding layout (SDL_gpu rules)

SDL_gpu maps HLSL register spaces to resource slots:

ResourceHLSL register
Vertex uniform buffer Nregister(bN, space1)
Fragment texture Nregister(tN, space2)
Fragment sampler Nregister(sN, space2)
Fragment uniform buffer Nregister(bN, space3)

For the engine's unlit material (1 vertex uniform, 1 fragment texture + sampler):

cbuffer MVPBlock : register(b0, space1) {
    float4x4 mvp;
};

Texture2D    base_tex : register(t0, space2);
SamplerState base_smp : register(s0, space2);

VSOutput vs_main(VSInput input) { ... }
float4   fs_main(VSOutput input) : SV_Target0 { ... }

Build pipeline

The CMake helper safi_compile_shader() (in cmake/SafiShaders.cmake) runs two steps per stage:

  1. HLSL → SPIR-V via glslangValidator -V -D -S <stage> -e <entry>
  2. SPIR-V → MSL via spirv-cross --msl --msl-version 20000

Generated artifacts land in ${CMAKE_BINARY_DIR}/shaders/. See examples/gltf_viewer/assets/shaders/unlit.hlsl and engine/src/ui/shaders/microui.hlsl for working examples.

Example

SDL_GPUShader *vs = safi_shader_load(
    &app.renderer, SAFI_DEMO_SHADER_DIR,
    "unlit", "vs_main", SAFI_SHADER_STAGE_VERTEX,
    /*samplers*/ 0, /*ubo*/ 1, /*ssbo*/ 0, /*stex*/ 0);

SDL_GPUShader *fs = safi_shader_load(
    &app.renderer, SAFI_DEMO_SHADER_DIR,
    "unlit", "fs_main", SAFI_SHADER_STAGE_FRAGMENT,
    /*samplers*/ 1, /*ubo*/ 0, /*ssbo*/ 0, /*stex*/ 0);

See also

  • SafiMaterial — builds a graphics pipeline from a vs_main + fs_main pair
  • Render overview — frame lifecycle and coordinate conventions