GPU Computing · Graphics Pipeline

Quad Shading, Helper Invocations, and Where Derivatives Come From

Why fragment shaders execute in 2x2 quads, what a derivative instruction computes when neighbouring pixels are not covered, and how helper invocations keep implicit gradients defined.

A fragment shader has no way to ask “what is the value of this attribute at the neighbouring pixel”. The shading language has no neighbour operator, no portable pixel-relative addressing, and no loop over the screen. Yet texture sampling with automatic level of detail needs exactly that information: the mip level is selected from the rate at which texture coordinates change across the surface, and the rate is a screen-space derivative. So derivatives exist, and their existence forces a specific execution model onto every fragment shader, whether or not it uses them.

Why a fragment shader cannot run alone

The unit that executes fragment work is a SIMD machine, typically 8, 16 or 32 lanes wide. Filling those lanes with fragments from arbitrary parts of the screen would be simple but would make derivatives impossible to compute, because no lane would reliably hold a neighbour. The alternative — grouping lanes by screen position — solves the derivative problem and constrains everything else.

The chosen grouping is the 2×2 quad. Fragments are dispatched in blocks of four, covering the sample positions at (x, y), (x+1, y), (x, y+1) and (x+1, y+1) for even x and even y. A quad is the smallest rectangle that admits a forward difference in both screen directions, so a derivative instruction can be implemented as a subtraction between lanes rather than as a memory operation.

Quads are a scheduling property, not a visible primitive. Nothing in the shading language exposes which lane holds which corner, and the mapping of lanes to corners is implementation-defined. What is guaranteed is the arithmetic: for a given quad, the derivative of a value is computed from the differences between the lane holding (x, y) and the lanes holding (x+1, y) and (x, y+1).

How a coarse derivative is computed

The core instruction is a pair of differences. Given a value f held by every lane, the derivative in x is f(x+1, y) − f(x, y) and the derivative in y is f(x, y+1) − f(x, y). Implementations execute this with a lane shuffle or with a dedicated cross-lane datapath, and the result is broadcast to all four lanes of the quad.

Two details follow. First, the derivative is piecewise constant over the quad: all four lanes receive the same value, not a per-lane value. A shader that computes a derivative and uses it in a per-lane expression is using the same derivative at all four sample points, which is why the standard 2×2 filter kernel for texture filtering is a box at the quad’s granularity rather than a smooth estimate. Second, the two samples being differenced are adjacent sample positions one pixel apart, so the result is the change over a one-pixel step — the same distance in which the texture coordinate’s rate is measured after the rasterizer has mapped it to screen space.

The fine derivative variants in current shading languages return a value computed from a smaller neighbourhood: in practice a pair of differences taken across the quad’s halves, giving four distinct values instead of one, at higher cost. The coarse variant is what the automatic level-of-detail computation uses.

#version 450
layout(location = 0) in vec2 uv;
layout(location = 0) out vec4 color;

// Coarse derivatives: one value per quad, computed from lane pairs.
// dFdxFine instead splits the quad in halves and reports per-lane values.
void main()
{
    vec2 dx = dFdx(uv);
    vec2 dy = dFdy(uv);

    // Approximate texel footprint: the larger axis extent of the
    // parallelogram spanned by the two screen-space derivatives.
    float footprint = max(length(dx), length(dy));

    // A footprint of one texel is mip 0; each doubling adds a level.
    float lod = max(log2(footprint), 0.0);

    color = vec4(vec3(lod / 8.0), 1.0);
}

The cost accounting matters when a shader needs derivatives for something other than texture sampling. Because the derivative requires all four lanes of the quad to have reached the same point in the program, an implementation cannot keep lanes in lockstep across divergent control flow and still produce derivatives. The consequence is that shaders with non-uniform branching are either executed without divergence suppression — which requires the hardware to reconverge at the point where a derivative is needed — or are compiled so that the derivative’s operands are computed in uniform control flow.

Helper invocations

A quad at the silhouette of a triangle has only one or two covered sample points. The other lanes have no fragment: the sample point is outside the triangle, and by the coverage rules of rasterization rules and the top-left fill convention that position belongs to no fragment at all. But the covered lane’s derivative needs a neighbour, and the neighbour does not exist.

The specification’s answer is to run the shader anyway for those lanes, calling them helper invocations. A helper invocation executes every instruction the real invocation does, with inputs that are extrapolated from the triangle’s plane equations beyond its edge, and its results are discarded. It exists so that the real invocation’s derivative has a plausible neighbour.

The extrapolation is well-defined because the rasterizer already holds the attribute plane equations. A sample position outside the triangle still has a value under those planes; the helper simply evaluates them there. This is why helper invocations produce sane derivatives even at a silhouette: the neighbour’s value is the linear continuation of the triangle’s surface, which is exactly the rate of change the derivative is looking for.

Helper invocations are not free. A quad with one covered lane still executes four lanes’ worth of shader work, so a triangle whose projected area is a handful of quads can pay four times the fragment cost per covered sample. A quad with no covered lane need not be executed at all, but a partially covered quad runs all four lanes, because dropping the uncovered ones would remove the neighbours the derivative is computed from. The cost shows up in scenes with many small triangles and in the first frame of a render where geometry is distant, which is one reason very small triangles are a performance pathology independent of their fill rate.

The behaviour of side effects inside helper invocations is easily got wrong. A helper invocation computes values that nothing reads, but the shading language does not give its memory operations the same guarantee: a store or an atomic issued by a lane whose sample is not covered has no specified effect on the resource, and its results are undefined. A shader that needs a side effect to happen exactly once per covered sample therefore has to guard the side effect on gl_HelperInvocation, the built-in that reports whether the current invocation is a helper, rather than assume that uncovered lanes never run.

Discard and early termination

The discard operation, which removes a fragment from the framebuffer, interacts with quad execution in a way that constrains optimisation. A discarded fragment still ran, and if its quad-mates later need a derivative, the discarded lane’s value is needed. Implementations therefore cannot simply skip the remainder of a quad when one lane discards; the other lanes continue. Some implementations defer the discard’s effect until the shader finishes, running the whole shader and suppressing the write, which preserves quad behaviour at the cost of not saving the work.

The expensive part of discard is not the discard itself but what it does to depth testing. A shader that may discard cannot have its depth test performed before the shader runs, because the shader might remove the fragment and the test’s write would be wrong. Implementations that support early depth testing use a shader compilation mode that proves the fragment’s depth is not modified and the shader does not discard; only then can the depth test be hoisted. Introducing a single discard into a shader that previously allowed early depth testing can therefore cost a large fraction of the pipeline’s depth-test bandwidth, because every fragment now shades before it is tested. Depth pre-passes and early depth testing and the order of per-fragment operations are the standard response.

What the model does not guarantee

The quad model does not guarantee that neighbouring quads are scheduled together, that the same shader invocation order is used between runs, or that derivatives are stable for fragments of different primitives covering the same location. Two triangles sharing an edge, each producing a partial quad at the seam, compute derivatives from their own extrapolations; the two extrapolations agree on the edge only if the two triangles’ surfaces agree there, which for a flat-shaded seam they do not.

The model also does not guarantee that derivatives are the true mathematical gradients of the interpolated function. They are finite differences at a fixed step, so a function with detail finer than one pixel shows only the average rate, which is the aliasing the mip selection is designed to avoid. A shader that tries to reconstruct subpixel detail from derivatives is reconstructing it from a one-pixel box filter and cannot do better than that filter allows.

References

  1. The OpenGL Graphics System: A Specification, Version 4.6 (Core Profile) — Khronos Group
  2. SPIR-V Specification — Khronos Group
  3. Vulkan Specification — Khronos Group
  4. A Trip Through the Graphics Pipeline — Fabian Giesen