Early Depth Testing vs Discard in GLSL
Early fragment depth testing (often referred to as Early-Z) is a
crucial GPU optimization designed to eliminate invisible fragments
before running expensive fragment shader calculations. However, using
the discard statement in a GLSL fragment shader disrupts
this optimization by introducing conditional fragment termination. This
article examines the architectural conflict between early depth testing
and discard, how modern graphics drivers automatically
resolve it, the resulting performance implications, and how developers
can manually control this behavior using GLSL layout qualifiers.
Understanding Early Fragment Depth Testing
In a standard conceptual graphics pipeline, the depth test occurs late—after the fragment shader has computed the final color and output attributes. Because executing complex fragment shaders on surfaces that will eventually fail the depth test is wasteful, modern GPUs implement early fragment tests.
Early-Z moves the depth and stencil tests to a stage before the fragment shader executes. If an incoming fragment is obscured by previously rendered geometry, it is discarded immediately, preventing unnecessary shading operations and saving significant computational bandwidth.
The Conflict
Introduced by the discard Instruction
The GLSL discard keyword unconditionally halts the
processing of the current fragment, ensuring that it writes neither
color nor depth data to the framebuffer.
This introduces a pipeline hazard for early depth testing:
- Depth Buffer Corruption: If an early depth test
updates the depth buffer before the fragment shader runs, and the shader
subsequently encounters a
discardstatement, the depth buffer has already been altered. The discarded fragment will leave an invisible "footprint" in the depth buffer, incorrectly occluding any geometry rendered behind it in subsequent draw calls. - Side Effects: If early tests only read the depth buffer without writing to it, the GPU must still decide when to write the updated depth value.
Because the GPU cannot know whether a fragment will execute
discard until the shader runs, it cannot safely perform
both early depth testing and early depth writing by default.
Default Hardware and Driver Behavior
To preserve visual correctness, OpenGL drivers implement conservative
fallbacks when they detect a discard instruction (or any
modification to gl_FragDepth) in the fragment shader:
- Disabling Early Depth Writes: Most modern architectures retain early depth testing (reading the depth buffer to cull fragments behind existing geometry), but they defer depth writing until after the fragment shader finishes executing and confirms the fragment was not discarded.
- Full Fallback to Late-Z: On older hardware or
specific architectures, the presence of
discarddisables early fragment testing entirely. Every fragment generated by rasterization runs the fragment shader, and the depth test is performed entirely at the end of the pipeline.
Performance Implications
Relying on discard can dramatically reduce rendering
efficiency due to the loss of optimal Early-Z optimizations:
- Redundant Shading: If early depth testing is partially or fully disabled, the GPU executes fragment shaders on pixels that are eventually occluded or discarded.
- Coarse Z Degradation: Modern GPUs use hierarchical
or coarse depth-culling hardware (such as Hi-Z) to reject entire tiles
of fragments simultaneously. Using
discardoften breaks or limits the effectiveness of these coarse rejection algorithms for that draw call.
Overriding Behavior: Explicit Early Fragment Tests
OpenGL 4.2 introduced an explicit layout qualifier that allows
developers to force early fragment testing even when using
discard:
#version 420 core
layout(early_fragment_tests) in;
void main()
{
if (some_condition)
{
discard;
}
// Fragment shader logic
}When layout(early_fragment_tests) in; is declared:
- The depth and stencil tests are executed—and the depth buffer is updated—before the fragment shader executes.
- If the shader subsequently calls
discard, the fragment’s color outputs are abandoned, but the depth value remains written in the depth buffer.
This qualifier is generally used for custom algorithmic requirements, such as depth-only pre-passes, stencil routing, or specific occlusion masks, but it is unsuitable for standard cutout transparency because the discarded geometry will still cast invisible depth occlusions.
Best Practices to Mitigate Discard Overhead
- Use a Depth Pre-Pass: Render the scene's geometry
to a depth-only buffer first. In the subsequent color pass, depth writes
can be disabled and depth comparisons set to
GL_EQUALorGL_LEQUAL, restoring full early rejection benefits without visual artifacts. - Branch as Early as Possible: If
discardis required and no depth pre-pass is used, evaluate the discard condition as early in the fragment shader as possible to minimize wasted texture fetches and math for killed fragments. - Consider Alpha-to-Coverage: For foliage and
cutouts, multisampled alpha-to-coverage can frequently replace
discard, preserving standard hardware depth optimization paths.