Skip to content

Blender Shader Language

Introduction

Historically, Blender's shaders were written in GLSL targeting only OpenGL.

With the addition of the Vulkan and Metal we needed to find a way to feed the same codebase to different backends. We opted to keep the GLSL syntax and make it compatible with the target API native language. This is for multiple reasons: * Lack of time to rewrite the codebase in another language. * Keeping compatibility with OpenGL 4.3 (no Spir-V extension). * Needed access to some MSL-only features.

As time went on, the complexity of our shader codebase increased. The lack of modern feature of GLSL is starting to make authoring of shaders and managing the shader codebase harder.

To bridge this gap, we started investing in tools. But quickly, our shaders tools became were very opaque and hard to use. Only a few people in the team had knowledge on how to use them and even less how they work.

For this reason, we are trying to convert these tools into our own shading language.

Targets

The goals of this shading language: * C++ compatible for host side code reuse. * Everything defined as code (no concept of implicit code injection). * Every code path statically checked.

What the language should be: * be easy to translate to the target shader languages (GLSL, MSL). * be translated at compile time. This mean translation should be done per file.

What the language should not be: * be directly compiled to Spir-V or other machine code. We do not want to write a compiler. * be 100% compliant with the C++ standard. It is not something that we have the capacity to do.

This language is based on the GLSL specification for historic reason. Features are expressed as C++ construct with some limitation to stick to the above rules.

Implementation

The shader files are translated at compile time into an intermediate language which doesn't have the C++ constructs. Some metadata are extracted from each original file and a GPUSource is created for each of the file. The intermediate language is even closer to GLSL and is also used to support user GLSL shader through from the python API. At runtime this intermediate language is then compiled by the different backend using different compatibility macros.

flowchart LR
    Source["Blender Shader Source"]
    SourcePy["Python Shader Source"]
    SourceMod["Intermediate Language"]
    GPUSource
    CompiledOGL["OpenGL GLSL Compiler"]
    CompiledMTL["MSL Compiler"]
    CompiledVLK["Vulkan GLSL Compiler"]

    Source --> |Blender Compilation|SourceMod
    SourcePy --> |pyGPU API|SourceMod
    SourceMod --> |Runtime|GPUSource

    GPUSource --> |Runtime Shader Compilation|CompiledOGL
    GPUSource --> |Runtime Shader Compilation|CompiledMTL
    GPUSource --> |Runtime Shader Compilation|CompiledVLK

Features

In this section we explain the language's features and their differences with C++.

Include System

The #include directives act similarly to slang's __include. It is a one time include of a file, with no propagation of preprocessor state.

This behavior is enforced when compiling shader files to C++ by always adding #pragma once to each header file and by putting all include statements before any other code.

Only the syntax #include "my_file.hh" is supported (no support for #include <my_file.hh> as it is against our codestyle). It is forbidden to include C++ libraries in shader files.

For legacy reasons, most files currently includes shader info files *info.hh. These are omitted from the final shader source and are only there for compiling the shader using C++.

Types

Types use the same conventions as BLI math API. Vectors are floatX,intX,uintX,shortX,ushortX,charX and ucharX where X is the component count between 2 and 4 included. Smaller types like half, char and short can be used but are not given to be the same size on all implementation. Their use is forbidden in interface structs. They are likely to be promoted to 32 bit integers.

Enum

Enums are supported when defined in namespace or global scope.

Underlying type should always be set.

enum axes : uchar {}; // Works
enum axes {}; // Doesn't work
All values must be explicitly set using integers literals. Do not forget u suffix for unsigned literal.
enum axes : uchar { // Works
  AXES_X = 1u,
  AXES_Y = 2u,
  AXES_Z = 3u,
};
enum axes : uchar { 
  AXES_X, // Doesn't work
  AXES_Y,
  AXES_Z,
};

Enum class are not supported but could be in the future.

Note that they are defined as global constant in GLSL and as native enum in MSL.

Reference

References in function scope are supported but have limitations: * Their definition must not have side effect or function call. * Array subscript index must be const qualified.

int &a = int_buffer[i++]; // Not ok, i++ has side effect
const int j = i++;
int &a = int_buffer[i]; // Works, j is const qualified
These references are not native concepts are are implemented as copy pasting of their definition statement.

References inside function arguments are supported and are transformed into inout qualifiers.

void func(int &i) {}

Assert

The function assert(int) can be used just like in C. Note that these only have effects if the cmake option WITH_GPU_SHADER_ASSERT is on.

Printf

printf can be used inside shader code to debug variables. Note that the formatting options are limited to %u, %d, %f, %x without extra parameters.

The output is flushed only when printf_end is called on the host side. The buffer is limited in the number of calls. So care must be taken to only call printf from a limited number of threads.

Strings support are only added for printf and they cannot be manipulated.

Bug

There is currently a bug preventing the use of formats starting with % (will result in crash). Using a space before it will fix the issue.

Unroll

The [[gpu::unroll]] attribute can be added in front of for loops to manually unroll a loop. Be aware that this is a lot more dumb than a real compiler option.

[[gpu::unroll]] for (int x = -1; x <= 1; x++) {
    x += texelFetch(img, x);
}
Note that the above syntax only works if the number of iteration can be detected.

The number of iteration can be set manually for loop with conditions referencing external variables.

[[gpu::unroll(3)]] for (; x < y;) {
    x += texelFetch(img, x);
}
break and continue statements are forbidden inside an unrolled loop.

Be aware that this expansion happens at compile time and that the resulting copy pasted text is store in the final blender executable.

Namespace

Namespaces works like in C++ but are much more limited.

Automatic namespace elision is only supported inside the scope where the definition occurs.

namespace A {
void func() {}
void test() {
  func(); // Works
}
} // A
namespace A {
void test2() {
  func(); // Doesn't work
  A::func(); // Works
  using A::func;
  func(); // Works
}
} // A

The syntax using namespace is not supported. All needed symbols from a namespace need to be mane visible manually.

void func() {
  using namespace A; // Doesn't work
  using A::func; // Works
  using T = A::T; // Works
}

The using syntaxes using X and using X = Y are allowed at both function and namespace scopes. However, when used in namespace scope, it can only bring symbols from the same namespace.

namespace B {
void test() {}
} // B
namespace A {
void func() {}
} // A
namespace A {
using A::func; // Works
using B::test; // Doesn't work
} // A
Namespaces cannot be nested.
namespace A {
namespace B { /* invalid */
} // B
} // A
But the following is supported:
namespace A {
} // A
namespace A::B {
} // A::B

Default Arguments

Default arguments are supported on functions.

void func(int a = 0) {}
These are implemented as function overloads and will expand to something similar as:
void func(int a) {}
void func() { func(0) }

Template

Templates must use explicit instantiation:

template<typename VecT> bool is_zero(VecT vec)
{
  return all(equal(vec, VecT(0.0f)));
}
template bool is_zero<float2>(float2);
template bool is_zero<float3>(float3);
template bool is_zero<float4>(float4);

Template explicit instantiation must specify all parameters explicitly:

template bool is_zero(float4); // Not OK
template bool is_zero<float4>(float4); // OK

Templates having all arguments present in function signature must to be called with template argument deduction:

template<typename T>
void func(T a) {}

func(2.0f); // OK
func<float>(2.0f); // Not OK

On the other side, templates not having all arguments present in function signature must to be called fully explicit:

template<typename T, int i> // i not in function arguments
void func(T a) {}

func<2>(2.0f); // Not OK
func<float, 2>(2.0f); // OK

Template calling templates are supported.

template<typename T, int i> // i not in function arguments
void func(T a) {
  test<T>(a); // OK
}

Template arguments can only be typename, integers or enums.

Template of template are not supported.

Template default arguments are not supported.

template<typename T = int>
void func(T a) {} // Not OK

Templated types are not supported.

template<typename T>
struct A { // Not OK
  T a;
}; 

Variadic templates are not supported.

template<typename... T>

Implementation is currently done using macros, so be aware of different symbol types having the same names.