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 variable v in the left pixel from the value in the right pixel of the quad.
  • dFdy(v) subtracts the value of variable v in 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:

  1. Missing Neighbor Data: The active threads require register values from the disabled threads to compute the finite difference.
  2. 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.
  3. 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, and fwidth.
  • Implicit level-of-detail (LOD) sampling: Standard texture sampling functions like texture(), textureProj(), and textureCube(). 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 if statement, 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.