GLSL Break and Continue Rules and Limitations

In the OpenGL Shading Language (GLSL), the break and continue statements alter loop execution flow, but their usage is governed by strict scope rules, target specification constraints, and hardware-level execution characteristics. While modern desktop GLSL versions treat these jump statements similarly to standard C, older mobile profiles (like GLSL ES 1.00) enforce strict compiler limitations, and non-uniform branch exits across GPU threads can trigger subtle bugs in operations that rely on implicit derivatives.

Syntactic Scope and Placement

The break and continue statements must adhere to strict structural constraints within shader code:

  • Enclosing Structure: A continue statement is only valid inside the body of an iteration statement (for, while, or do-while). Using it anywhere else results in a compilation error.
  • Switch Interaction: The break statement can appear inside iteration statements or switch blocks. When placed inside a switch contained within a loop, a break only terminates the innermost switch block, not the loop itself.
  • Innermost Scope: Both statements only affect the immediate, innermost loop in which they reside. GLSL does not support labeled breaks or multi-level loop exits.

Profile and Version-Specific Restrictions (GLSL ES 1.00)

Older embedded environments, particularly GLSL ES 1.00 (commonly used in WebGL 1.0 and OpenGL ES 2.0), introduced severe restrictions in Appendix A ("Loop Limitations") due to early GPU hardware lacking dynamic branch support:

  • Ban on continue: In the strict profile of GLSL ES 1.00, the continue statement is entirely forbidden inside loop bodies.
  • Conditional break Limitations: A break statement was permitted only if enclosed within a simple if condition. The loop index comparison was heavily restricted to ensure the loop could be statically unrolled at compile time.
  • Dynamic Loop Bounds: Breaking out of a loop dynamically based on uniform or attribute variables often failed to compile on strict hardware, requiring loops to feature constant iteration counts and fixed exit conditions.

Modern desktop GLSL (versions 1.30 and later) and GLSL ES 3.00+ have largely lifted these syntactic limitations, allowing dynamic branch decisions and full continue support.

Thread Divergence and Implicit Derivatives

GPUs process fragment and compute shaders in parallel groups of threads, often called warps or wavefronts. When some threads execute a break or continue while others do not, execution diverges:

  • Undefined Derivatives: Executing a dynamic break or continue before functions that require implicit screen-space derivatives (such as texture(), textureLod(), dFdx(), or dFdy()) causes undefined behavior. These functions require values from adjacent 2x2 fragment quads to calculate gradients. If quad lanes exit the loop prematurely via break or skip iterations via continue, the hardware cannot determine the correct mipmap level or rate of change.
  • Workaround: Texture samples inside loops with dynamic exits must explicitly specify derivatives or levels of detail using functions such as textureGrad() or textureLod().

Performance and Static Unrolling

Early loop exits prevent compiler optimizations:

  • Impaired Loop Unrolling: Shaders frequently unroll loops to eliminate counter overhead and expose instruction-level parallelism. A non-constant break or continue prevents full compile-time unrolling, forcing the shader compiler to emit dynamic branching instructions.
  • Execution Masking: GPUs handle dynamic exits within a warp by masking out finished threads rather than immediately advancing to the next instruction block. A warp containing even a single thread that did not execute a break will continue executing the loop iterations for all threads, negating potential performance gains from early termination.