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:

  1. Depth Buffer Corruption: If an early depth test updates the depth buffer before the fragment shader runs, and the shader subsequently encounters a discard statement, 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.
  2. 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 discard disables 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 discard often 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

  1. 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_EQUAL or GL_LEQUAL, restoring full early rejection benefits without visual artifacts.
  2. Branch as Early as Possible: If discard is 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.
  3. Consider Alpha-to-Coverage: For foliage and cutouts, multisampled alpha-to-coverage can frequently replace discard, preserving standard hardware depth optimization paths.