GLSL Invariant Keyword in Multi-Pipeline Rendering
The invariant keyword in GLSL guarantees that the
compiler generates identical floating-point results for specified output
variables across different shader programs. In modern rendering
architectures utilizing multi-pipeline configurations—such as depth
pre-passes, shadow mapping, and deferred lighting executed via Separate
Shader Objects (SSO)—subtle differences in compiler optimizations can
cause depth disparities and severe visual artifacts like z-fighting.
This article explains how the invariant qualifier interacts
with separate compilation units and outlines the conditions necessary to
maintain strict invariance across independent pipeline objects.
The Purpose of Invariance
Modern optimizing compilers reorganize, vectorize, and fuse floating-point instructions (such as utilizing Fused Multiply-Add, or FMA) to maximize execution speed. Because floating-point arithmetic is non-associative, altering the instruction order or precision alters the numerical outcome.
When two separate pipeline programs calculate the same
value—typically gl_Position across a depth-only pass and a
forward shading pass—divergent optimizations will yield marginally
different coordinates. The invariant keyword instructs the
driver’s compiler backend to forgo specific optimizations for that
variable, ensuring deterministic computation across shader
boundaries.
Interaction with Separate Compilation Units
In standard monolithic GLSL linking, the compiler can inspect both
the vertex and fragment stages simultaneously, potentially culling
unused code across stage boundaries. With separate compilation
units—such as those created using
GL_ARB_separate_shader_objects or
glCreateShaderProgramv—each pipeline stage or module is
compiled in total isolation from the others.
This isolation heightens the risk of computational divergence:
- Dead Code Elimination Differences: A vertex shader compiled alone might retain instructions that would have been stripped if compiled alongside a specific fragment shader, altering register allocation and instruction scheduling.
- Context-Free Optimization: Without the global context of a monolithic program, the compiler chooses optimizations solely based on local heuristics, which vary depending on the active stage outputs.
Applying the invariant qualifier to an output variable
in an isolated compilation unit establishes a strict compilation
boundary. It signals the independent compiler instance that the marked
variable must adhere to a standardized, unoptimized evaluation pattern
that can be matched identically by any other unit implementing the same
math.
Requirements for Cross-Pipeline Determinism
The invariant qualifier does not automatically guarantee
identical output on its own; it only guarantees identical evaluation of
identical code. To preserve invariance across distinct pipeline objects,
you must satisfy four prerequisites:
- Exact Code Replication: The calculations contributing to the invariant variable must be written identically in both compilation units. Changing sub-expression order, variable naming that affects AST generation, or operator groupings breaks the guarantee.
- Identical Input Values: Every input consumed during the calculation (uniforms, vertex attributes, and push constants) must receive bitwise identical values in both pipelines.
- Consistent State and Precision: Compilation
settings, floating-point precision qualifiers (such as
highpin GLSL ES), and driver-level flags (e.g., fast-math toggles) must be congruent across all compilation runs. - Matching Declarations: The output variable must explicitly be declared invariant in every compilation unit where parity is required.
// Output declared as invariant in both separate shader objects
invariant out vec4 gl_Position;
uniform mat4 u_ModelViewProjection;
layout(location = 0) in vec4 a_Position;
void main() {
// This calculation must match bit-for-bit in all passes
gl_Position = u_ModelViewProjection * a_Position;
}Alternatively, setting #pragma STDGL invariant(all) at
the top of the compilation unit forces global invariance on all outputs,
but this can significantly restrict the driver's ability to optimize the
shader stage.
Invariant vs. Precise
In modern GLSL (4.0+) and Vulkan-targeted SPIR-V, the
precise keyword is often used alongside or instead of
invariant. While invariant is primarily
concerned with cross-program output matching (preventing multi-pass
variance), precise specifically disables optimizations like
FMA fusion for intermediate calculations to prevent algorithmic
instability within a single algorithm (such as tessellation cracking).
For multi-pipeline parity across separate compilation units,
invariant remains the dedicated mechanism for enforcing
multi-pass consistency at the output interface.