How Early-Z Rejection Optimizes DirectX Rendering
Early-Z rejection is a hardware-level optimization technique in modern graphics pipelines that executes depth testing before the pixel (fragment) shader runs, rather than after. By determining fragment visibility prior to executing costly shading computations, DirectX-based engines can discard occluded pixels immediately, substantially reducing overdraw and freeing up GPU compute bandwidth. This article explores how early-Z functions within DirectX, the specific performance bottlenecks it solves, the conditions that disable it, and best practices developers use to maximize its effectiveness.
The Problem: Overdraw and Late-Z Bottlenecks
In the conventional rendering pipeline (historically known as Late-Z), the sequence of operations runs as follows:
- Geometry & Rasterization: Primitives are transformed, clipped, and rasterized into potential fragments.
- Pixel Shading: The pixel shader executes complex mathematical models, texture fetches, lighting evaluations, and material calculations.
- Output Merger (Depth/Stencil Test): The output merger compares the fragment's depth against the depth buffer. If it passes, the fragment is written to the render target; if it fails (because a closer object already occupies that space), the pixel is discarded.
When multiple overlapping objects occupy the same screen space—a phenomenon called overdraw—the pixel shader repeatedly shades fragments that will never be visible to the user. In scenes with heavy materials, multiple dynamic lights, and complex post-processing, this redundant computation creates severe memory bandwidth and ALU bottlenecks.
How Early-Z Rejection Works in DirectX
Early-Z moves the depth and stencil tests forward in the pipeline, executing them directly after rasterization and prior to the pixel shader.
When a triangle is rasterized, the GPU evaluates the interpolated depth (\(Z\)) value of each covered pixel against the current value in the depth buffer:
- Occluded Fragments: If an existing primitive is already closer to the camera at that pixel coordinate, the incoming fragment fails the depth test and is rejected immediately. The pixel shader never runs.
- Visible Fragments: If the fragment is closer, it passes the test, the depth buffer is updated, and the fragment proceeds to the pixel shader for standard execution.
DirectX 11 and DirectX 12 rely on the GPU's underlying hardware depth engines (such as HiZ or Hierarchical Z) to perform these tests at coarse and fine granularities. This ensures that entire blocks of invisible pixels are discarded with minimal hardware overhead.
Conditions That Invalidate Early-Z
Early-Z relies on knowing the fragment's final depth value and ensuring that the fragment's execution will not alter visibility dynamically. In DirectX, early-Z is automatically disabled or demoted back to late-Z under specific HLSL conditions:
- Modifying Depth in Shader (
SV_Depth): If a pixel shader writes to theSV_Depthsemantic, the hardware cannot determine the final depth value prior to shader execution. - Discard Operations (
clip()ordiscard): If a shader conditionally cancels a fragment viaclip()ordiscard, the GPU cannot safely update the depth buffer early, as a discarded pixel should not block objects behind it. While some architectures support "partial" early-Z (testing early but writing depth late), this can still stall rasterization and degrade throughput. - Unordered Access View (UAV) Writes: Writing to UAVs inside the pixel shader can introduce side effects that must occur regardless of visibility, potentially forcing the driver to defer depth testing.
To override driver hesitation when writing to depth or using clip
operations, HLSL provides the [earlydepthstencil]
attribute. Placing this attribute on the pixel shader forces early depth
testing before execution. However, if the shader modifies depth, the
compiler will flag an error, and using it with discard
means the depth buffer updates even if the pixel is discarded,
potentially leaving depth artifacts.
Maximizing Early-Z Efficiency in DirectX Pipelines
To leverage early-Z rejection effectively, rendering engines implement specific architectural strategies:
- Front-to-Back Sorting: Opaque geometry should be sorted and rendered from front to back (closest to furthest relative to the camera). This guarantees that the front-most surfaces populate the depth buffer first, maximizing rejection rates for subsequent objects.
- Depth Pre-Pass (Z-Prepass): For scenes with expensive pixel shaders, engines often execute a lightweight first pass that writes only to the depth buffer using a null or minimal pixel shader. During the subsequent main lighting pass, depth writes are disabled, depth tests are set to equal or less-equal, and early-Z guarantees zero overdraw for opaque meshes.
- Isolating Discard Operations: Alpha-tested surfaces
(such as foliage or wire fences) that require
clip()should be grouped and rendered separately from completely opaque geometry to prevent pipeline stalls on fully opaque objects.