Why Screen-Space Derivatives Fail in Divergent GLSL
In GLSL, screen-space derivative functions like dFdx,
dFdy, and implicit LOD texture lookups become invalid or
undefined when executed inside divergent branches. This limitation stems
from the underlying GPU hardware architecture, which processes fragment
shaders in locked 2x2 pixel quads to compute finite differences. When
neighboring fragments within a quad take different execution paths, the
synchronized data sharing required to calculate these derivatives breaks
down, leading to visual artifacts or undefined behavior.
The Mechanics of Screen-Space Derivatives
GPUs do not execute fragment shaders in complete isolation. Instead, they process pixels in 2x2 blocks known as quads. Screen-space derivatives rely on this grouping to approximate the rate of change of a variable relative to screen coordinates.
dFdx(v)subtracts the value of variablevin the left pixel from the value in the right pixel of the quad.dFdy(v)subtracts the value of variablevin the bottom pixel from the value in the top pixel of the quad.
Because these operations use finite differences across adjacent hardware threads, calculating a derivative requires simultaneous access to the register values of neighboring threads inside the same quad.
The Problem with Divergent Branches
Branching divergence occurs when fragments within the same 2x2 quad
evaluate a dynamic condition differently—for example, when two pixels
execute the if block while the other two execute the
else block.
GPUs handle conditional branching using SIMD (Single Instruction, Multiple Data) or SIMT (Single Instruction, Multiple Threads) execution masks. When divergence occurs, threads that do not take the current path are masked off (disabled). If a derivative instruction executes inside that conditional path:
- Missing Neighbor Data: The active threads require register values from the disabled threads to compute the finite difference.
- Undefined Registers: The disabled threads never executed the instructions leading up to that point in the branch, meaning their corresponding register values are uninitialized, stale, or completely invalid.
- Implicit Sync Failure: Hardware cannot synchronize derivative values between active threads and inactive threads.
Because the GPU cannot retrieve valid neighboring data, the result of the derivative function becomes undefined.
Affected GLSL Functions
The restriction does not only apply to explicit derivative operations. It affects any function that relies on quad-based differences:
- Explicit derivatives:
dFdx,dFdy,dFdxFine,dFdyFine,dFdxCoarse,dFdyCoarse, andfwidth. - Implicit level-of-detail (LOD) sampling: Standard
texture sampling functions like
texture(),textureProj(), andtextureCube(). These rely on screen-space derivatives of the texture coordinates to determine which mipmap level to sample. When called inside divergent flow control, mipmapping fails, often causing noisy seams or rendering the lowest-resolution mip level.
How to Fix Derivative Errors in Branches
To avoid undefined behavior caused by divergent derivatives, use one of the following approaches:
- Hoist calculations outside the branch: Compute the
derivative or sample the texture before the
ifstatement, store the result in a variable, and access that variable inside the branch. - Use explicit LOD sampling: Replace standard
sampling functions with variants that do not rely on implicit
derivatives, such as
textureLod(), which takes an explicit mipmap level. - Supply explicit gradients: Use
textureGrad(), which allows you to compute gradients outside the conditional block and pass them explicitly to the texture sampler inside the branch.