GPU Computing · Graphics Pipeline

Multisample Coverage and What the Resolve Step Actually Does

How one shading result is shared across several coverage samples, why resolve averages coverage rather than colour, and where alpha-to-coverage and centroid sampling fit.

Supersampling is the straightforward fix for jagged edges: render at double resolution and average down. It is also expensive, because it multiplies both the geometry work and the shading work by four. Multisampling keeps the geometry multiplication and discards the shading multiplication, and the way it does that determines almost everything else about the technique — including the artifacts it cannot remove.

One colour per pixel, several coverage samples

A multisampled render target stores, per pixel, one colour value and a fixed number of coverage samples, typically two, four, eight or sixteen. Depth and stencil are stored per sample, not per pixel. When a triangle is rasterized, coverage is evaluated at every sample position, so the depth test is performed per sample and the stencil test is performed per sample.

The fragment shader, however, runs once per pixel — more precisely, once per set of covered samples that share a shading result. The shading position is a single point in the pixel, usually the pixel center, and the colour it produces is replicated to every covered sample that passed the depth and stencil tests. A pixel two-thirds covered by a triangle gets one shaded colour, held by two-thirds of its samples, and the remaining samples hold whatever was there before.

That asymmetry is the whole design. The expensive part of a frame is shading; the cheap parts are coverage evaluation and per-sample depth storage. Multisampling spends memory and bandwidth on the cheap parts and leaves the expensive part at its single-sample cost. The memory cost is real: at four samples, the depth buffer is four times its single-sample size, and the colour buffer grows by a fraction of that for the extra sample masks.

The shading position being fixed at the pixel center creates a problem. A pixel covered only at one corner has a center outside the triangle, so the shader would be evaluated at a point where the interpolated attributes are extrapolations of the triangle’s plane. The specification’s answer is centroid sampling: an interpolant qualified as centroid is evaluated at a point guaranteed to be inside the covered region, typically the centroid of the covered samples. For most geometry the difference is invisible; at the silhouette of a large triangle it can visibly shift the texture lookup, because the shading position moves by up to half a pixel depending on the triangle’s edge.

#version 450
// Alpha-to-coverage: the shader's alpha, quantised, becomes the fraction
// of samples that are kept. With 4 samples the alpha resolves to 5 levels.
layout(binding = 0) uniform sampler2D tex;
layout(location = 0) in vec2 uv;
layout(location = 0) out vec4 frag;

void main()
{
    vec4 texel = texture(tex, uv);
    // The colour is written normally. With alpha-to-coverage enabled, the
    // alpha value is converted into this fragment's sample mask afterwards.
    frag = texel;
}

Resolve: averaging coverage, not colour

A multisampled target cannot be displayed or sampled directly by another pass. It must be resolved into a single-sample target, and the resolve is the step where the samples’ colours are combined. If the implementation has done its job, each sample holds the shaded colour of the pixel that covered it, weighted by whether it was covered: a pixel covered by two samples of a red triangle over a blue background holds red in two samples and blue in two, and the resolve averages them to a hue halfway between.

The averaging is exact for coverage antialiasing, because the samples are binary. It is not exact for the interior of a triangle, where all samples hold the same shaded colour and the resolve reproduces it — no blur, no change. This is why a multisampled image looks sharp inside surfaces and smooth at edges, and why the technique is unsuitable for anything else: it antialiases geometric coverage only.

The resolve can be performed as an explicit operation that reads the multisampled attachment and writes a single-sample one, or implicitly by an API that presents the multisampled surface. The explicit form costs a full read and write of the target per resolve. The implicit form costs the same internally, unless the implementation can blend the resolve into the blend operation — a common optimisation for the final display pass, where the resolve is fused with whatever format conversion the display needs.

Per-sample shading is the option to skip the sharing. With it enabled, the shader runs once per covered sample rather than once per pixel, and each sample receives its own shading result. The technique is then supersampling by another name, with the shading cost multiplied by the sample count. It buys correctness for shaders whose output varies at a scale finer than a pixel: a shader with a high-frequency procedural pattern, a shader doing per-sample depth comparisons such as shadow mapping, or a shader that needs a different texture lookup per sample. The cost is the full multiplier that multisampling was designed to avoid, which is why it is used selectively on the passes that need it.

Coverage masks and sample-position patterns

The set of samples a pixel can be covered at is a mask, one bit per sample. Blending on a multisampled target operates on the covered samples only, and the coverage mask survives through to the resolve. This is what makes alpha-to-coverage work: the shader outputs an alpha value, and the implementation converts it into a coverage mask by comparing the alpha against thresholds associated with each sample position, so an alpha of 0.5 with four samples covers two of them.

The mapping from alpha to sample count is a quantisation, and its granularity is the sample count. With four samples, an alpha of 0.3 and an alpha of 0.4 both cover one sample, so a gradient of alpha becomes a banded gradient of coverage. The banding is only visible where the source is smooth, which is why alpha-to-coverage is used for foliage and foliage-like geometry and why it looks better at eight or sixteen samples.

Sample positions are implementation-defined within limits: the specification requires that the positions cover a reasonable spread of the pixel and that they be consistent across a draw. The arrangements in use include rotated grids and various fixed patterns chosen to reduce correlation artifacts. Two properties matter to shaders. First, the positions are not exposed to the shader — there is no portable way to read the sample positions — so per-sample logic must infer coverage from the depth value or from an interpolant. Second, the same pattern is used for depth, stencil and coverage, so the number of depth values per pixel is exactly the number of coverage samples.

Resolution at the silhouette has a limit imposed by the pattern. A triangle edge that passes between two sample positions is a discontinuity in coverage: the pixel is either partly covered or not, and the resolve produces a colour that is a step function of the edge’s position. With four samples the resolved coverage fraction can only take the values zero, one quarter, one half, three quarters and one, so the edge position it implies is quantised to a fraction of a pixel. That is the residual aliasing: it is not zero, and it manifests as a faint stair-stepping on near-horizontal edges, which is why the technique is sometimes combined with a post-process filter.

Depth and stencil at sample granularity

Storing depth per sample changes the behaviour of the depth test in ways that matter. When two triangles overlap partially within a pixel, each sample retains the depth of whichever triangle covered it. The result is a per-sample visibility decision, so a pixel can contain parts of two surfaces at different depths — which is exactly what gives multisampling its antialiased intersections between geometry. A pair of crossing planes in a multisampled render shows a clean intersection line, whereas the single-sample render shows a staircase.

The cost is bandwidth. Depth writes now write per-sample values, and a tile of the depth buffer is larger. Implementations compress depth heavily — a tile of samples with identical depth values compresses to a constant — so the effective bandwidth for large flat surfaces is close to the single-sample case, and the cost concentrates at silhouettes and intersections, which are exactly the regions with varied depth. The compression also interacts with early depth testing and hierarchical rejection: the hierarchical test still works per tile, but the per-sample depth test does not reject a whole pixel unless every sample fails.

Stencil behaves similarly, with one notable consequence: a stencil test that depends on per-sample coverage lets a shader use stencil as a per-sample mask, which is the basis of some deferred-shading optimisations and of the stencil-based decal techniques. The mask has as many bits as samples, and operations that write to the stencil write to the covered samples only.

Where multisampling stops helping

Multisampling does not antialias a texture, because texture sampling is done in the shader and the shader runs once per pixel. A high-frequency texture on a distant surface still shimmers, and the fix is mipmapping or anisotropic filtering, not sample count. It does not antialias shader-computed functions with high-frequency detail, for the same reason. And it does not antialias the interior of thin geometry: a triangle narrower than the sample spacing may cover no sample in some pixels and several in others, producing a dashed line rather than a smooth one; conservative rasterization for thin geometry addresses that by relaxing the coverage test.

The technique’s cost curve is unfavourable at high sample counts. Memory grows linearly with the count, the per-sample depth work grows with it, and the antialiasing quality improves only in proportion: doubling the sample count halves the quantisation step in resolved coverage. Eight samples therefore cost twice the depth footprint of four for one further halving of that step, which is why four is the common setting and eight or sixteen are quality options.

References

  1. The OpenGL Graphics System: A Specification, Version 4.6 (Core Profile) — Khronos Group
  2. Multisample Anti-Aliasing (OpenGL Wiki) — Khronos Group
  3. Vulkan Specification — Khronos Group
  4. Real-Time Rendering, Fourth Edition — Akenine-Moller, Haines, Hoffman, CRC Press 2018