v154 · graphics · webgpu
WebGPU: WGSL fragment depth modifiers
Writing @builtin(frag_depth) from a fragment shader forces the GPU to give up early depth testing: it cannot know whether a fragment will survive until the shader has run, so it runs the shader on everything. Chrome 154 lets you promise which direction you will move the depth — less or greater — and the hardware can keep the optimisation.
concepts
-
What this machine can do
WebGPU needs an adapter, and an adapter needs a GPU the browser is willing to use. The probe reports each step separately, so "not supported" is replaced by which specific thing was missing — and where a device is available it compiles the shaders for real.
-
The three shader forms
Plain
@builtin(frag_depth)against thelessandgreatervariants, with the WGSL side by side and a compile attempt for each. The modifier is a promise, and breaking it is undefined behaviour rather than an error, which is worth understanding before you use it. -
Why early-Z matters
A model of the fragment pipeline you can drive: change the depth complexity and see how many fragment shader invocations early-Z saves. The gap is the reason the modifier exists, and it grows with overdraw.
why it shipped
A depth test normally happens before the fragment shader: if a fragment is behind something already drawn, there is no point shading it. That reordering — early-Z — is one of the largest wins in a rasteriser, and it depends on the hardware knowing the fragment's depth in advance.
A shader that writes frag_depth breaks that assumption, so drivers disable early-Z for the whole draw call. But most shaders that write depth only ever push a fragment further away — soft particles, impostors, parallax mapping. If the shader promises that, the hardware can still reject a fragment that was already behind the depth buffer, because moving it further can only keep it behind. The modifier is that promise, and it is the same mechanism as GLSL's depth_greater layout qualifier.
the API
// Chrome 154: the modifier tells the hardware which way the depth moves.
@fragment
fn main() -> @builtin(frag_depth) greater f32 {
return 0.9; // only ever pushes fragments further away
}
// Without it, early-Z is disabled for the whole draw call.
@fragment
fn main() -> @builtin(frag_depth) f32 {
return 0.9;
}
The promise is not checked at runtime. A shader declared greater that returns a nearer depth produces undefined results — which is the trade: you get the optimisation by taking responsibility for the invariant.