Can You Use Recursion in GLSL Shaders?
The OpenGL Shading Language (GLSL) strictly forbids recursion in all shader functions, whether direct or indirect. This article explores the official GLSL specifications prohibiting recursive function calls, the hardware and architectural limitations of graphics processing units (GPUs) that necessitate this rule, how shader compilers enforce the restriction, and practical iterative techniques developers use to implement recursive-style algorithms.
The GLSL Specification on Recursion
The core GLSL specification across virtually all versions explicitly defines recursion as illegal. Under the function calling rules of the specification, functions may call other functions, but a function cannot call itself directly, nor can it call a sequence of other functions that ultimately leads back to itself.
// Illegal: Direct recursion
int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1); // Compilation error
}
// Illegal: Indirect recursion
void functionB();
void functionA() {
functionB(); // Compilation error: cycle detected
}
void functionB() {
functionA();
}If a GLSL compiler detects any recursive invocation path, it is required to reject the shader and emit a compile-time or link-time error.
Architectural Reasons for Disallowing Recursion
The prohibition of recursion is rooted in the physical design and execution model of graphics hardware:
- Massive Parallelism without Dynamic Stacks: GPUs execute thousands of threads concurrently using Single Instruction, Multiple Threads (SIMT) architectures. Providing a traditional, dynamically expanding stack frame per thread would drastically increase memory overhead and reduce cache efficiency.
- Predictable Register and Memory Allocation: GPU compilers statically analyze register pressure and local memory allocation before executing shader code. Disallowing recursion ensures that function call trees have a known, finite depth, allowing the compiler to inline functions or assign registers without needing dynamic runtime memory management.
- Execution Divergence: In SIMT execution, branching paths across threads in the same warp or wavefront cause serialization (warp divergence). Unbounded recursion would create severe control flow divergence, degrading performance significantly.
How Compilers Detect Recursion
GLSL compilers analyze shader source code by constructing a static call graph where each node represents a function and each directed edge represents a function call.
Before generating intermediate representation (such as SPIR-V) or native GPU machine code, the compiler performs cycle detection algorithms (such as Tarjan's or depth-first search) on the call graph. If a cycle exists within the directed graph, the compiler flags the cycle as recursive and halts compilation.
Common Workarounds for Recursive Algorithms
Certain algorithms—such as ray marching, bounding volume hierarchy (BVH) traversal, tree parsing, and fractal rendering—are naturally recursive. Developers translate these algorithms into non-recursive GLSL constructs through several established patterns.
1. Iterative Loops
Algorithms that follow tail recursion can be converted directly into
iterative loops using standard for or while
statements.
int factorialIterative(int n) {
int result = 1;
for (int i = 2; i <= n; ++i) {
result *= i;
}
return result;
}2. Explicit Fixed-Size Emulated Stacks
For non-linear traversals, such as navigating a binary tree or BVH during ray tracing, developers allocate a fixed-size local array to serve as an explicit stack.
#define STACK_CAPACITY 32
void traverseHierarchy(int rootNodeIndex) {
int stack[STACK_CAPACITY];
int stackPointer = 0;
// Push root
stack[stackPointer++] = rootNodeIndex;
while (stackPointer > 0) {
// Pop current node
int currentNode = stack[--stackPointer];
// Process node and push child nodes if capacity allows
int leftChild = getLeftChild(currentNode);
int rightChild = getRightChild(currentNode);
if (rightChild != -1 && stackPointer < STACK_CAPACITY) {
stack[stackPointer++] = rightChild;
}
if (leftChild != -1 && stackPointer < STACK_CAPACITY) {
stack[stackPointer++] = leftChild;
}
}
}3. State-Based State Machines
Complex recursive logic can also be structured as an explicit
finite-state machine contained inside a single while loop,
where an index or enum variable manages transitions between conceptual
call frames.