How Does gl_FragDepth Affect Early Depth Testing?
Writing directly to gl_FragDepth in a fragment shader
generally forces modern GPUs to disable early depth testing (Early-Z),
degrading rendering performance by executing shader workloads on pixels
that will ultimately be occluded. Because the GPU cannot predict what
value the shader will output, it must postpone depth testing until after
the fragment shader finishes executing. However, features like
conservative depth qualifiers and explicit early test layouts can
mitigate these performance penalties under specific conditions.
The Role of Early Fragment Tests
In standard graphics pipelines, the hardware attempts to perform depth and stencil tests before running the fragment shader. This mechanism, known as Early-Z, allows the GPU to immediately discard fragments that are occluded by previously rendered, closer geometry.
Skipping fragment shading for hidden pixels saves significant GPU cycles, reduces bandwidth consumption, and prevents unnecessary texture lookups or complex arithmetic operations. To run Early-Z safely, the rasterizer derives the fragment depth directly from the interpolated vertex positions (\(z / w\)) generated during primitive rasterization.
Why Writing to gl_FragDepth Disables Early-Z
When a fragment shader contains an assignment to
gl_FragDepth, it explicitly overrides the interpolated
hardware depth value. The rasterizer no longer knows the final depth
value of the fragment at the start of the pipeline.
Because the depth test depends on the final depth value, the pipeline must fall back to late depth testing:
- Primitive Rasterization: Fragments are generated with interpolated depths.
- Fragment Shader Execution: The shader runs
unconditionally for every rasterized fragment to compute both color and
the new
gl_FragDepth. - Late Depth Test: The GPU compares the newly written
gl_FragDepthagainst the depth buffer. - Framebuffer Write / Discard: If the test passes, the depth and color buffers update; if it fails, the fragment is discarded only after all shader computations have already finished.
This late execution causes high overdraw penalties, especially in scenes with complex geometry, overlapping transparent or opaque layers, and heavy fragment shaders.
Performance Repercussions
Disabling Early-Z impacts rendering pipelines in several ways:
- Increased Shading Cost: Shaders execute for pixels that end up completely hidden behind other objects.
- Disrupted Hierarchical-Z (Hi-Z): Coarse-level culling mechanisms that discard entire pixel tiles at lower resolutions often become partially or completely disabled.
- Bottlenecks in Deferred Rendering: Techniques relying on depth modification (such as raymarched impostors or custom depth biases) can bottleneck the geometry pass if applied broadly.
Mitigations and Modern GLSL Features
OpenGL and GLSL provide specific layout qualifiers to preserve or adapt early testing behavior when depth modification is required.
Conservative Depth
(layout(depth_*))
Introduced in OpenGL 4.2 (via
GL_ARB_conservative_depth), depth layout qualifiers allow
developers to provide guarantees to the GPU driver about how
gl_FragDepth will be modified relative to the default
interpolated depth:
layout(depth_any) out float gl_FragDepth;— The default state; depth can be anything, disabling Early-Z optimizations.layout(depth_greater) out float gl_FragDepth;— Guarantees the written depth will only be greater than or equal to the interpolated depth. The GPU can still safely cull fragments that fail the depth test using the original depth (assuming a standardGL_LESSdepth comparison).layout(depth_less) out float gl_FragDepth;— Guarantees the written depth will only be less than or equal to the interpolated depth.layout(depth_unchanged) out float gl_FragDepth;— Guaranteesgl_FragDepthmatches the interpolated depth, re-enabling full depth optimizations while allowing the shader to declare or conditionally reference the variable.
Explicit Early Fragment Tests
GLSL allows developers to force early depth tests explicitly using
the early_fragment_tests layout qualifier:
layout(early_fragment_tests) in;When this qualifier is active, the depth test occurs before shader
execution using the standard interpolated depth. If a shader attempts to
write to gl_FragDepth while
early_fragment_tests is declared, the written value is
ignored for the depth test, or the shader will trigger a compilation
error depending on driver constraints.
Best Practices
To maintain high frame rates and avoid disabling depth culling:
- Avoid modifying
gl_FragDepthunless dynamic, per-pixel depth calculation is strictly necessary (e.g., depth-correct impostors, screen-space decals, or voxel rendering). - Use polygon offset (
glPolygonOffset) or vertex shader depth adjustments instead of per-fragment depth modifications when resolving z-fighting. - Always specify conservative depth qualifiers
(
depth_greaterordepth_less) if modifying depth in GLSL 4.2 or higher to allow the hardware to retain coarse Hi-Z culling.