Shader Compiler
The Tellusim Clay Shader Compiler is a core component of all graphics and compute programs. It follows the GLSL-based shader specification and supports cross-compilation into various shading languages, including HLSL, MSL, WGSL, and CUDA. This allows you to write all your shaders in GLSL, and the SDK will automatically translate them to the target platform shading language. More information about the Clay Shader Compiler can be found here.
In this guide, we focus on the unique shader features available in the Tellusim SDK.
References
The GLSL language specification does not include pointers or reference objects. This limitation complicates the development of complex systems that need to access different structures from multiple memory locations. The Tellusim Clay Shader Compiler adds reference types that map buffers into user-defined structures.
Declare structures as usual (nested reference types are also supported):
struct Vector4 {
float x, y, z, w;
};
struct Data {
uint size;
uint u;
float f;
vec3 v3;
Vector4 v4;
mat4 m4;
};
Create a reference proxy variable by providing the target storage buffer and the index of the first element:
layout(std430, binding = 0) buffer ScalarBuffer { uint scalar_buffer[]; };
layout(std430, binding = 1) buffer VectorBuffer { vec4 vector_buffer[]; };
{
// Somewhere inside code
Data &data_0 = Data(scalar_buffer, element_index_0);
Data &data_1 = Data(vector_buffer, element_index_1);
}
The element_index used as array index, not byte offset.
Access reference members:
data_0.size = data_1.u;
data_0.f = data_1.f + 1.0f;
data_1 = data_0;
References are supported on all target platforms and incur no performance penalty.
- Comprehensive SDK example demonstrating reference usage: samples/platform/reference/.
- Live WebGPU demo: WebGPU Example.
Extensions
The Tellusim Clay Shader Compiler includes several useful language extensions:
assert()- Ensures a compile-time condition is true; otherwise produces a compilation error.sizeof()- Returns the size in bytes of a type or variable.offsetof()- Returns the byte offset of a structure member.
These extensions are useful for validating data sizes and structure layouts:
struct Parameters {
mat4 matrix;
vec4 vector;
float scalar;
};
// Ensure type sizes
assert(sizeof(float) == 4);
assert(sizeof(vec4) == sizeof(float) * 4);
assert(sizeof(mat4) == sizeof(vec4) * 4);
assert(sizeof(Parameters) == sizeof(mat4) + sizeof(vec4) + sizeof(float));
// Ensure structure layout
assert(offsetof(Parameters, vector) == sizeof(mat4));
assert(offsetof(Parameters, scalar) == sizeof(mat4) + sizeof(vec4));
Preprocessor
The shader preprocessor is essential for efficient shader development. Even simple enhancements can significantly improve productivity.
All preprocessor directives are also available for native shader types (HLSL, MSL, WGSL, CUDA).
#include
The standard GLSL approach requires the GL_ARB_shading_language_include extension:
#extension GL_ARB_shading_language_include : require
This must be placed at the beginning of each *.glsl file to use #include.
However, this extension is not part of core GLSL and does not allow intercepting file loading without modifying the source code.
With Tellusim, you can use #include without additional declarations.
Moreover, file loading can be intercepted using Source callbacks,
allowing shaders to be embedded or autogenerated without creating actual files.
Named file sources can be provided through the Tellusim API:
String file_source = ...;
Shader::setInclude("my_file.h", file_source);
This enables dynamic shader code injection instead of relying on physical files.
#define
Macro definitions are an excellent tool for efficient shader branching based on external arguments.
They are more efficient than GLSL if() { } else { } statements because branching happens before compilation and optimization.
This skips complex compilation steps, enables early-stage shader optimization, and reduces application loading time.
Avoiding unnecessary dynamic shader compilation also prevents runtime slowdowns and stuttering in complex shaders.
However, duplicate macro declarations may produce warnings:
#define MY_MACRO 1
// ...
#define MY_MACRO 1
// Shader::preprocessor(): "MY_MACRO" macro redefined
// main.shader:10 #define MY_MACRO 2
You could suppress these warnings using #ifdef/#endif or #pragma, but Tellusim provides a cleaner solution:
#assign acts like #define but does not produce warnings for repeated definitions.
#assign MY_MACRO 1
// ...
#assign MY_MACRO 1
No warnings will be generated during compilation.
Application-defined global macro declarations can be provided through the Tellusim API:
Shader::setMacro("MY_MACRO", 42);
#pragma
The #pragma directive uses the form #pragma name or #pragma name(arg1,arg2,...) with comma-separated arguments.
It is not a standard macro definition but is very useful for controlling compilation, debugging, and exporting in Tellusim.
General pragmas
#pragma nodebug- Excludes debug information from the final shader.#pragma nounused- Suppresses warnings about unused variables and functions.#pragma noreserved- Ignores errors caused by reserved language keywords.#pragma quiet- Omits all macro definitions from the error output.#pragma dynamic- Disables binary caching for dynamic or runtime-generated shaders.
Export and preprocessor
#pragma export- Prints the final shader source (HLSL, MSL, WGSL, CUDA) to the Log output.#pragma export(my_file.hlsl)- Writes the final shader source to the specified file.#pragma preprocessor- Prints the preprocessed shader source to the Log output.#pragma preprocessor(my_file.glsl)- Writes the preprocessed shader source to the specified file.
Direct3D12 pragmas
#pragma entry(name)- Overrides the default main entry point with the specified function.#pragma target(name)- Sets the compilation target (e.g.,vs_6_0,ps_6_8,lib_6_8).#pragma flags(arguments)- Passes command-line flags todxcompilercompiler:Od- Disables optimizations (-Od).O0-O3- Sets optimization level (-O0to-O3).Zi- Enables debug information (-Zi).internal- Enables internal validator (-select-validator internal).
Direct3D11 pragmas
#pragma entry(name)- Overrides the default main entry point with the specified function.#pragma target(name)- Sets the compilation target (e.g.,vs_5_0,ps_5_0,cs_5_0).#pragma flags(arguments)- Passes compilation flags tod3dcompilercompiler:O0-O3- Sets optimization level (D3DCOMPILE_OPTIMIZATION_LEVEL0toD3DCOMPILE_OPTIMIZATION_LEVEL3).Zi- Enables debug information (D3DCOMPILE_DEBUG).
SDK example demonstrating #pragma usage: samples/native/d3d12_work_graph/.
printf() System
The shader printf() system is useful for debugging.
Tellusim supports printf() on all platforms, including WebGPU.
It relies on a macro-based system for encoding strings and arguments.
String embedding
The @ modifier converts a literal string into hexadecimal integers:
#define TO_STRING(VALUE) @VALUE
TO_STRING("Hello World!\n")
Produces:
0x6c6c6548, 0x6f57206f, 0x21646c72, 0x0000000a
Arguments
__VA_ARGC__- Returns the number of arguments passed via....__VA_ARGM__(MACRO)- AppliesMACROto each variadic argument. TheMACROmust accept two arguments: value and index.
#define PRINTV(VALUE, INDEX) (VALUE, INDEX),
#define PRINTF(...) __VA_ARGM__(PRINTV)
PRINTF("A", "B", "C", "D")
Produces:
("A", 0u), ("B", 1u), ("C", 2u), ("D", 3u),
Combined into a macro-based shader printf()
// Buffer to output printf() messages
// The first element is an atomic counter with the string length
layout(std430, binding = 0) buffer ScalarBuffer { uint scalar_buffer[]; };
// Argument-specialized functions for correct format conversion
inline uint printv(uint value) { return value; }
inline uint printv(float value) { return floatBitsToUint(value); }
// Outputs argument into its specific position
#define PRINTV(VALUE, INDEX) scalar_buffer[i + (INDEX + 1u)] = printv(VALUE);
// Printf macro that writes the message into the scalar_buffer
#define PRINTF(ARGC, ...) { \
uint i = atomicAdd(scalar_buffer[0u], __VA_ARGC__ + 1u) + 1u; \
scalar_buffer[i] = __VA_ARGC__ | ((ARGC) << 16u); \
__VA_ARGM__(PRINTV) \
}
#define printf(FORMAT, ...) PRINTF(__VA_ARGC__, @FORMAT, __VA_ARGS__)
SDK example demonstrating printf() usage: samples/platform/printf/.
Live WebGPU demo: WebGPU Example.
Predefined Macro Values
The following predefined macro values are available depending on the target API, platform, or vendor.
API-specific macros
CLAY_GLSL=1- Present when using Clay Shader Compiler.CLAY_VK=1- Present on the Vulkan backend.CLAY_GL=1- Present on the OpenGL backend.CLAY_GLES=1- Present on the OpenGLES backend.CLAY_HLSL=1- Present on the Direct3D backends.CLAY_D3D12=1- Present on the Direct3D12 backend.CLAY_D3D11=1- Present on the Direct3D11 backend.CLAY_MSL=1- Present on the Metal backend.CLAY_MTL=1- Present on the Metal backend.CLAY_WGSL=1- Present on the WebGPU backend.CLAY_WG=1- Present on the WebGPU backend.CLAY_CU=1- Present on the CUDA backend.CLAY_HIP=1- Present on the HIP backend.
Platform-specific macros
_WIN32=1- Present on the Windows platform._LINUX=1- Present on the Linux platform._MACOS=1- Present on the macOS platform._ANDROID=1- Present on the Android platform._IOS=1- Present on the iOS platform._TVOS=1- Present on the tvOS platform.
Vendor-specific macros
_ARM=1- Present on ARM Mali GPUs._AMD=1- Present on AMD GPUs._APPLE=1- Present on Apple GPUs._INTEL=1- Present on Intel GPUs._NVIDIA=1- Present on NVIDIA GPUs._QUALCOMM=1- Present on Qualcomm GPUs.