Praxis · M3 · ticket #297 · PR #380 · prototype branch prototype/visibility-d

Visibility Buffer Explained

ADR 0019 chose a clustered forward main pass, option A, and set a gate: past 6 ms of GPU on a Reference scene, options B and D are prototyped on that scene before anything changes. town read 8.6 ms. B, a visibility pre-pass, added 1.9 to 3.5 ms. This guide is D: a visibility buffer, in which the depth passes record which triangle each sample shows and a compute pass shades from that record instead of rasterising the scene a third time. It explains what D was meant to save, how its IDs are written, how a compute shader rebuilds a triangle and its perspective-correct weights, how it gets texture gradients with no quad of neighbours, how it keeps MSAA, and what the measurements say. Two live demos show the weights with a corner behind the camera, and which neighbours each derivative form differences against.

Provenance. The prototype is two commits on prototype/visibility-d, 358067b and b846161, and stays unmerged. Its findings and measurements are PR #380's, recorded in docs/performance.md and in ADR 0019's amendment at 496e7e70, measured on an Apple M4 Pro under macOS 27.0.1 with wgpu 30.0.1 on Metal. The owner's decision of 2026-10-29 keeps option A. Where the guide brings in the published literature, it names the source.

Chapter 1 · background

What D was meant to save

Key idea. A forward lit pass pays for its shading in rasteriser work: every triangle's vertices again at depth Equal, and 2×2 quads of fragment invocations that small triangles fill mostly with helpers. A visibility buffer shades each pixel's triangles once, in compute, so D tested whether that waste is what breaches the gate.

Option A draws town's triangles twice: once into depth, once to shade at depth Equal. A visibility buffer draws them once, writing which triangle each sample shows, and shades from that record in a compute pass.

A fragment shader never runs alone. The GPU runs fragment invocations in 2×2 quads, so each can take a derivative by subtracting its neighbour's value: that is how textureSample picks a mip level. Where a triangle covers only some of a quad's pixels, the GPU fills the rest with helper invocations, which compute values only so their neighbours can difference against them, and whose outputs are discarded. A triangle about a pixel across can occupy a whole quad to shade one pixel, and a pixel on the edge between two triangles is shaded in two quads. That waste is called quad overdraw.

town is the scene where it should bite. Its frames draw a median of 5,552,214 triangles over 137,730 Instances, about 1.5 triangles per pixel of a 2560×1440 target (this page's arithmetic). On top of the quads, A's lit pass runs the vertex shader for every triangle of the frame's draw a second time, though many own no sample once the depth pre-pass has run.

D removes both. Its compute pass runs one invocation per pixel, so no helper runs, and nothing is rasterised a second time. The research note behind ADR 0019 describes it: a pre-pass writes per-sample triangle IDs, then a compute pass "rebuilds each pixel's triangle from the IDs and the vertex and index buffers, interpolates attributes with analytic derivatives, samples with textureSampleGrad, shades every Material in one shader". Burns and Hunt proposed it in 2013, a buffer that "only stores a triangle index and instance ID per sample, encoded in as few as four bytes" in place of a G-buffer, "often 20+ bytes per visibility sample". Their aim was memory bandwidth. D's aim on town was the gate.

D pays for what it removes. Each pixel must find its triangle's three corners, run the vertex shader's work for each, and compute its own weights and gradients: work the rasteriser does in fixed-function hardware for a forward pass. Whether that costs less than the quads and the second vertex pass is what the prototype measured.

The frame's passes with the visibility_buffer Render switch on

  1. culling phase one
  2. Froxel pass
  3. depth pre-pass, with IDs
  4. depth pyramid
  5. culling phase two
  6. phase two's depth pass, with IDs
  7. shadow mask pass (rays on)
  8. material pass
  9. tonemapping
Orange: the depth passes now write a triangle ID per sample beside depth, and with shadow rays on the Surface normals as before. With the switch on, phase two's depth pass is always a pass of its own. Blue: the material pass, a compute pass, replaces the lit pass and writes the HDR target. draw_calls reads 3 against A's 4. The switch is --visibility-buffer on praxis bench and praxis capture.
Chapter 2 · visibility_ids.wgsl · visibility_id.wgsl

Writing the IDs

Key idea. Each sample's ID is the triangle's primitive_index within its phase's draw, with a bit for phase two, split into two 16-bit halves because a multisampled 32-bit integer target is not guaranteed. Written in the depth passes, the last ID a sample keeps is the nearest triangle's, and a flat input makes it that triangle's own.

The ID is the fragment stage's primitive index, which WGSL defines as a "per-primitive index based on the number of primitives processed for the current instance". Praxis draws each culling phase's triangles in one indexed draw, so within a phase the index names a triangle of the frame's draw. Phase two's draw starts where phase one's ends, and its primitive_index starts again at 0, so phase two's IDs carry bit 31, PHASE_TWO_BIT. A pipeline override constant, PHASE_TWO, tells each depth pass's pipeline whether to set it.

// visibility_ids.wgsl at b846161: the ID a depth pass writes.
fn visibility_id(primitive: u32, material_id: u32) -> vec2<u32> {
    let id = select(primitive, primitive | PHASE_TWO_BIT, PHASE_TWO);
    return encode_visibility_id(select(id, 0u, material_id == NO_MATERIAL - 1u));
}

// visibility_id.wgsl: two 16-bit halves.
fn encode_visibility_id(id: u32) -> vec2<u32> {
    return vec2<u32>(id & 0xffffu, id >> 16u);
}
  • The format. The IDs go into a 4x MSAA Rg16Uint target beside depth. WebGPU's table of plain colour formats gives r32uint no multisampling; rg16uint has it wherever the device has WebGPU's core features and limits. Cleared to all bits set, the ID NO_TRIANGLE, they take 2560 × 1440 × 4 samples × 4 bytes = 58,982,400 bytes at 1440p.
  • The feature. primitive_index needs wgpu's PRIMITIVE_INDEX, which is outside Praxis's Capability floor. The prototype requests it where the adapter has it, as B's did.
  • The flat input. Each ID entry point reads the flat Material ID and compares it with NO_MATERIAL - 1, an ID no Material has, so the read has no effect but survives the compiler. On the M4 Pro, a fragment stage that reads no flat input can merge the fragments of index-sharing triangles, and one triangle's ID would land on its neighbour's samples (MSAA Fragment Merging Explained). With the read, each sample's ID names the triangle that covers it.
  • With shadow rays on, the same fragment stage writes two colour targets, the Surface normals the shadow mask pass reads and the ID.

Why in the depth passes, when B could not

B wrote its IDs in a pass of its own, drawing the frame again at depth Equal, which cost 2.08 ms. It had to: B's lit pass rasterised the named triangles again at depth Equal, so the IDs had to name the triangles that pass would show, under the same depth state. D draws nothing again. The depth passes test depth with Greater and write it, so a fragment writes its ID only when it is nearer than everything before it, and the last ID a sample keeps belongs to the nearest triangle. Nothing later must agree with that choice, so D's IDs need agree only with themselves.

That made them cheap. In frames with shadow rays on, depth_prepass went from 2,139 µs under A to 2,356 µs under D, about 0.22 ms. GPU memory grew by 58,982,432 bytes: the IDs plus the material pass's 32-byte uniform.

Chapter 3 · material_pass.wgsl · surface.wgsl

Rebuilding the triangle

Key idea. With the three corners' clip x, y and w as the rows of a matrix M, a pixel's perspective-correct weights are proportional to its device point times M's inverse. Each column of the inverse is a cross product of two rows, and normalising the weights to sum to 1 cancels the determinant, so no corner is ever divided by its w.

A forward pass gets its interpolated attributes from the rasteriser. A compute pass gets only an ID, and must find the triangle, run its corners through the vertex shader's work, and work out how much of each corner the pixel sees.

From an ID to three corners

The material pass clears bit 31, and if it was set adds phase two's first index divided by 3, to get the triangle's place in the frame's draw. That draw's packed indices give three vertices. For each, it calls vertex_output, the very function the lit pass's vertex shader calls, moved into a part both share, surface.wgsl: the packed index names a Meshlet record, which names the Instance, the Meshlet and the Material; the vertex comes from the vertex Arena; and out come the camera-relative position (ADR 0014), the clip position, normal, tangent and UV. Calling the same function is what lets D draw A's image: the corners are computed exactly as A's vertex shader computes them.

Weights without dividing by w

Rasterisers interpolate in screen space and then correct for perspective, dividing by each corner's w. A compute pass could do the same: project each corner, take 2D weights on the projected triangle, and correct each by 1/w. The prototype does not. Write each corner's clip coordinates (x, y, w) as a row of a 3×3 matrix M, and let p = (xndc, yndc, 1) be the pixel centre's device point.

point with weights λ on the triangle, in clip space: λᵀ M it projects to p exactly when λᵀ M = s p for some s > 0 so λᵀ = s p M⁻¹ ∝ p M⁻¹ column i of M⁻¹ = (row i+1 × row i+2) / det M normalise λ to sum to 1: s and det M cancel
// material_pass.wgsl at b846161.
let rows = mat3x3<f32>(first_corner.clip_position.xyw,
                       second_corner.clip_position.xyw,
                       third_corner.clip_position.xyw);
let edges = mat3x3<f32>(cross(rows[1], rows[2]), cross(rows[2], rows[0]), cross(rows[0], rows[1]));

fn barycentric_weights(edges: mat3x3<f32>, point: vec3<f32>) -> vec3<f32> {
    let unnormalised = point * edges;   // a row vector times the matrix: dot(point, each column)
    let sum = unnormalised.x + unnormalised.y + unnormalised.z;
    return unnormalised / select(sum, 1.0, sum == 0.0);
}

The only division is by the sum of the unnormalised weights. That sum is 0 only where the pixel's ray runs parallel to the triangle's plane, which a neighbour pixel evaluated for a derivative (chapter 4) can reach at a grazing angle; the prototype then leaves the weights unnormalised, and finite. A corner behind the camera has a negative w, and nothing divides by it, so it needs no clipping. The same identity gives the depth along the ray: the plane is met at w = det M / sum.

This is Olano and Greer's 1997 scan conversion in 2D homogeneous coordinates: "three edge functions = M-1 = inverse of 2D homogeneous vertex matrix", and each parameter's "interpolation function = parameter vector * M-1". Their algorithm "avoids costly clipping tests" and handles clipping "by the addition of clip edges, without the need to actually split the clipped triangle". They still add a near clip edge when interpolating, because values such as 1/w can overflow near the eye, and the clip edge lets them store 1/w in fixed point; the material pass works in f32, at pixels whose samples the triangle covers and their neighbours. It needs only the interpolation half. Which pixels a triangle covers is already settled: the depth passes' rasteriser, which does clip, wrote the IDs.

Presets
the triangle, top down corners divided by w probe pixel (drag it)

Live demo. Left: the scene from above, the camera at the bottom looking up, its 90° view between the grey lines; drag corners A, B and C, even below the camera's line, where w turns negative. Each corner keeps its own height. Right: the screen. Pixels whose homogeneous weights are all positive, with the point in front of the camera, are coloured by the weights (A red, B green, C blue), which is exactly perspective-correct interpolation. The dashed outline joins the corners divided by w: with every corner in front it is the triangle; with one behind, that corner lands mirrored, and the outline no longer bounds what is visible, which is why a rasteriser clips first. Drag the probe ring to read both sets of weights. In exact arithmetic they are the same function, since the 2D weights over w, normalised, equal p M-1 normalised; the divided route fails where a corner's w is 0, and the homogeneous route never divides by a corner's w.
Chapter 4 · shade_triangle

Derivatives without a quad

Key idea. A compute invocation has no quad to difference against, so the material pass evaluates the triangle on its own plane at neighbouring pixel centres, as helper invocations would. Differencing across the 2×2 quad from its top-left pixel matched A's image on the M4 Pro; differencing each pixel against its own neighbours did not.

Texture sampling picks a mip level from how fast the UV changes from one pixel to the next. In a fragment shader the hardware takes that from the quad. WGSL's dpdx returns "the same as either dpdxFine(e) or dpdxCoarse(e)": the fine form differences within each row and column of the quad, the coarse form "may result in fewer unique positions", and WGSL leaves the choice to the implementation. A compute shader has none of this, so it computes gradients itself and samples with textureSampleGrad, which "samples a texture using explicit gradients".

Where the gradients come from matters. Karis's Nanite talk, whose material pass still runs as pixel shaders, notes that its quads "span across triangles", and also depth discontinuities, UV seams and different objects: "Finite differences of discontinuities are nonsense and often huge which results in high mip levels being used". Nanite computes analytic derivatives of the attributes across the triangle and replaces every Sample with SampleGrad, at an overhead it measured as "less than 2%". The prototype stays on the triangle's own plane as well. It evaluates the same triangle, with the same weights function, at neighbouring pixel centres, even where those centres fall outside it, just as a helper invocation extrapolates the triangle. No difference crosses into another triangle.

Two forms, measured

The prototype measured two forms against A's image of town at 640×360, counting pixels that differ by more than one 8-bit step at the eight frames the Goldens test uses:

Frame59300500700900120015001800
Each pixel's own right and lower neighbours1481322,0541,8811,7971,7081,5651,369
The quad's top-left pixel to its right and lower neighbours, shared by the quad510107561604657590617

Pixels past one 8-bit step from A's image, of 230,400. The per-quad row is docs/performance.md's at 496e7e70; the per-pixel row is the prototype test's output, whose range, 132 to 2,054, the findings give.

The per-quad form cut the differences by a factor of 2 to 30, so the M4 Pro's dpdx and dpdy in the lit pass behave as one difference per quad. The prototype ships that form, to match A. It costs one more weight evaluation and one more normal-Texture sample per triangle than the per-pixel form, since the quad's top-left pixel is a fourth point: in round 4 the gate read 8,795.0 µs with it and 8,430.5 µs with the per-pixel form, about 365 µs. Neither form is wrong. The per-pixel one, whose gate was the reading below A's, is the one a D adopted for its own sake would ship, the findings say.

The same neighbours serve two purposes. The UV differences are the gradients every Texture is sampled with, anisotropic filtering included. The shading normal's differences, with the normal Texture sampled at the neighbours' UVs too, widen roughness for specular antialiasing (Specular Antialiasing Explained), as dpdx(normal) does in the lit pass.

Derivative form
points the chosen pixel differences level differs from per quad helper invocation

Live demo. A triangle receding from the camera over an 8×8 grid of pixels, its UV interpolated with the homogeneous weights of chapter 3 and sampled from a 256-texel Texture. Each covered pixel shows its mip level, the base-2 log of its longer UV gradient in texels, a simplified isotropic rule; the dashed pixels are the helper invocations a fragment shader would run. Click a pixel to see which points its derivatives use. Per quad is the prototype's shipped form, one difference for the whole quad from its top-left pixel. WGSL fine differences within the pixel's own row and column of the quad, as dpdxFine and dpdyFine are defined. Per pixel is the prototype's other form, each pixel's own right and lower neighbours, which can lie in the next quad. With the far corner's w at 1 there is no perspective, the UV is linear across the screen, and all three agree. The more the triangle recedes, the more the levels differ; whether a pixel then lands on another mip depends on how near a whole level it sits.
Chapter 5 · shade_pixels

MSAA from IDs

Key idea. The material pass shades each distinct triangle among a pixel's four samples once, at the pixel centre, and weights it by the samples it covers: the lit pass's own MSAA rate and resolve, so edges stay antialiased without shading every sample.

A 4x MSAA forward pass shades each triangle once per pixel, at the pixel centre, and writes the result into the samples that triangle covers; the resolve then averages the four (MSAA Fragment Merging Explained). The research note offered D two ways to keep MSAA: per-sample IDs and per-sample shading at edges, or else SMAA 1x. The prototype found a third, the one the forward pass itself uses. One invocation per pixel, in 8×8 workgroups, loads the four samples' IDs and walks the distinct ones:

// material_pass.wgsl at b846161, the end of shade_pixels.
var colour = vec4<f32>(0.0);
var unshaded = 0xfu;
while unshaded != 0u {
    let id = ids[firstTrailingBit(unshaded)];
    let sample_bits = select(vec4<u32>(0u), vec4<u32>(1u, 2u, 4u, 8u), ids == vec4<u32>(id));
    let matching = dot(sample_bits, vec4<u32>(1u));
    var shaded = cleared;
    if id != NO_TRIANGLE {
        shaded = shade_triangle(id, pixel_centre, vec2<f32>(size));
    }
    colour += shaded * f32(countOneBits(matching));
    unshaded &= ~matching;
}
textureStore(hdr_target, pixel, colour / 4.0);
  • One shade per triangle per pixel. A pixel inside one triangle shades once. A pixel on an edge shades once per triangle its samples show, up to four, so edges cost more, exactly as they do in the forward pass. Samples no triangle covers take the clear colour.
  • The same resolve. The lit pass writes each colour weighted by 1/(1 + luminance), so that a plain average of the samples is a luminance-weighted resolve, as Karis describes: "weight = 1/(1 + luma). For a weighted average, sum all samples and divide by the summed weights." The material pass calls the same lit_colour, which applies that weight, and weights the clear colour the same way, so its sum over 4 equals the hardware resolve.
  • The same lighting. lit_colour, shared with the lit pass's fs_main, reads the Froxel lights, the shadow mask, ambient and emissive, applies exposure and clamps to fp16's largest value.

Burns and Hunt resolve MSAA the same way. Their deferred pass creates "one shading sample record … for each unique visibility sample", weighted "in proportion to the number of times they appear within the pixel", which "results in one shading sample per pixel, per triangle".

How the match was checked

the_visibility_buffer_shades_the_image_the_lit_pass_shades renders a white wall lit by nine point lights with a red quad turned 0.5 rad over it, partly over the background, at 64×64. Every pixel must be within one 8-bit step of A's, with shadow rays on and off; dividing by 3.9 instead of 4, which brightens it about 2.6%, fails it. tests/visibility_buffer.rs captures town at the Goldens test's eight frames at 640×360 and allows at most 700 pixels past one step per frame.

Nine storage buffers

The material pass's compute stage reads nine storage buffers: lights, Froxel lists, Materials, Instances, vertices, Meshlets, Meshlet vertices, Meshlet records and packed indices. Nine is the Capability floor's max_storage_buffers_per_shader_stage. So the pass declares the frame and geometry buffers it reads in bind groups of its own, leaving the culling pass's layout as it was, and instead of binding the frame's draw, a tenth buffer, it has the GPU copy phase two's first index, 4 bytes, into its uniform each frame with copy_buffer_to_buffer. The uniform also holds the clear colour, which the CPU writes: 16 more uploaded bytes a frame.

Chapter 6 · PR #380

Reading the result

Key idea. Judge spans, not passes. D's gate stayed within about 2% of A's either way, its GPU frame grew because its CPU frame shrank, and the 8.6 ms is the triangles every option draws.

praxis bench on town at 2560×1440 with 4x MSAA, 1,800 measured frames, A then D in each of three rounds, and a fourth round adding F, D with per-pixel derivatives. Medians in µs:

RoundBuildGate, rays offGPU frameCPU framedepth_prepassmaterial_pass
1A8,667.315,378.716,439.02,139.1—
1D8,806.816,941.514,583.02,355.45,390.5
2A8,691.315,404.716,850.02,138.1—
2D8,770.216,961.714,372.52,355.85,412.8
3A8,631.615,328.016,350.52,140.0—
3D8,767.216,883.314,197.62,357.05,375.0
4A8,631.715,329.516,528.82,141.0—
4D8,795.016,945.014,360.02,357.45,419.5
4F8,430.516,634.814,470.62,337.45,064.0

Apple M4 Pro, macOS 27.0.1, wgpu 30.0.1 on Metal, release builds of the prototype branch, from docs/performance.md at 496e7e70. Pass times are from frames with shadow rays on.

The gate

The gate runs from the first culling dispatch to the end of the lit pass, or here the material pass, with shadow rays off (Frame Timing Explained). In the median rounds D read 8.77 ms against A's 8.67, 1.2% slower; F read 8.43 ms against 8.63 in its round, 2.3% faster. Neither is near 6 ms. The IDs cost about 0.22 ms, and shading takes roughly 5 ms in the lit pass or the material pass alike: rebuilding each pixel's triangles in compute costs on the M4 Pro about what rasterising them again did.

Why the pass times do not add up

On Metal, passes that do not write what each other binds overlap, so a pass's own timestamps include time spent waiting on the pass before. A's lit_pass reads 11,256 µs in rays-on frames; D's tonemapping_pass reads 12,424 against A's 4,029, though tonemapping did not change. Only spans, the gate and the GPU frame, count shared time once (Frame Timing Explained).

Why the GPU frame grew 1.5 ms

With shadow rays on, D's GPU frame read 16.9 ms against A's 15.4, while its gate moved about 1%. Ticket #378 found the mechanism: when the CPU frame is shorter than the GPU frame, frames queue behind each other, and Metal stops overlapping passes that it overlaps when the GPU is idle at a frame's start. D's CPU frame, 14.4 ms, is shorter than its GPU frame; A's, 16.4 ms, is longer than its own. D's CPU frame was 2.1 ms shorter because the material pass binds no TLAS, and each use of a bind group holding a TLAS cost wgpu-core 30.0.1 a walk over every Instance's BLAS (#352). Since PR #383, only the shadow mask pass binds the TLASes on main, so today's A loses the CPU gap and the overlap alike. The GPU-frame comparison holds for the code it measured, not for today's.

The image

At 2560×1440 the eight frames differ from A's by more than one step at 23, 29, 735, 8,060, 8,802, 8,947, 8,693 and 8,719 of 3,686,400 pixels, the largest channel difference 132. The differing pixels sit alone on the silhouettes of rooftop objects and other geometry under a pixel wide. The PR's images, A, D and their difference, are in the reading list.

The decision

Both options ADR 0019 named have now run on town, and neither moves the gate: B added 1.9 to 3.5 ms, and D read from 2.3% under A to 1.2% over it. The 8.6 ms is the frame's triangles: culling 1.3 ms, the depth pre-pass 2.1 and shading roughly 5, overlapping. The owner kept A and the provisional 6 ms, the prototype stays on its branch, PRIMITIVE_INDEX stays out of the Capability floor, and ADR 0019 records the reopen clause as spent on town until LOD (#360) reduces its triangles. Fewer triangles is the lever every option shares.

  • What a D for its own sake would still need. PRIMITIVE_INDEX in the Capability floor; 59 MB of IDs at 1440p; a storage-buffer limit above the floor's 9 for any further inputs; and a forward pass beside it for transparency.
  • What it showed regardless. Shading from IDs in compute can reproduce a forward MSAA image within a step at nearly every pixel, and on this GPU a forward pass's quad derivatives are per quad.
Review

Check yourself

B wrote its IDs in a pass of its own at depth Equal. Why can D write them in the depth passes?

B's lit pass drew the named triangles again at depth Equal, so its IDs had to name what that pass would show, under its depth state. D draws nothing again. In the depth passes, a fragment writes its ID only when it passes the Greater test, so each sample keeps the nearest triangle's ID, and the flat Material ID read keeps the M4 Pro from merging fragments. The IDs need agree only with themselves, and cost about 0.22 ms instead of B's 2.08.

The material pass never divides a corner by its w. What does it divide by, and when is that zero?

By the sum of the unnormalised weights, p · (row i+1 × row i+2) over the three corners. Normalising to sum 1 cancels the unknown scale and det M. The sum is zero only where the pixel's ray runs parallel to the triangle's plane, which a neighbour evaluated for a derivative can reach at a grazing angle; the weights are then left unnormalised. A corner behind the camera, with negative w, needs no clipping.

D's gate moved about 1%, but its GPU frame with rays on grew 1.5 ms. Which figure judges D, and why does the GPU frame comparison not hold for today's main?

The gate, a span that counts overlapping passes once. The GPU frame grew because D's CPU frame, 2.1 ms shorter since the material pass binds no TLAS, fell below its GPU frame: frames queued, and Metal stopped overlapping passes it overlaps when the GPU is idle at a frame's start (#378). Since PR #383 only the shadow mask pass binds the TLASes, so A on main is in the same position.

Reference

Reading list

Grouped by source, each with what it is good for here. Links into the prototype are pinned to b846161, and the findings to main at 496e7e70.

This ticket

In the repo

The code, at b846161

Papers and specifications

Books

  • Akenine-Möller, Haines, Hoffman et al., Real-Time Rendering, 4th ed., §20.5 Deferred Texturing, pp. 905–908, and §23.1 Rasterization, pp. 993–1002 the visibility buffer among deferred techniques, and rasterisation with 2D homogeneous coordinates and quads. Sections and pages from the book's contents, bibliography and index.
  • Pharr, Jakob and Humphreys, Physically Based Rendering, 4th ed., §10.1.1 Finding the Texture Sampling Rate texture gradients from screen-space partial derivatives, the problem chapter 4 solves
  • Lengyel, Foundations of Game Engine Development, Volume 2: Rendering projections, homogeneous coordinates and rasterisation
  • Gregory, Game Engine Architecture where a renderer's passes sit in an engine
Reference

Glossary

Instance, Meshlet, Material ID, Surface normals, Shadow mask, Render switches, Reference scene and Capability floor are CONTEXT.md's definitions. The rest are this guide's own.

Visibility buffer
A triangle ID per sample, written while depth is resolved, from which a later pass rebuilds and shades the surface each pixel shows.
Visibility ID
In the prototype, a triangle's primitive_index in its phase's draw, with bit 31 for phase two, stored as two 16-bit halves.
Material pass
The prototype's compute pass that shades each pixel from its samples' IDs, in place of the lit pass.
Primitive index
A fragment-stage built-in value: the index of the fragment's triangle among those processed for the current instance.
Quad
A 2×2 group of fragment invocations that run together so each can take derivatives against its neighbours.
Helper invocation
A fragment invocation for a pixel of a quad the triangle does not cover, run only so its neighbours can take derivatives.
Coarse and fine derivatives
A coarse derivative may share one difference across a quad; a fine one differences within the invocation's own row or column of it.
Homogeneous barycentrics
Perspective-correct weights from a pixel's device point times the inverse of the corners' (x, y, w) matrix, normalised, with no divide by any corner's w.
Instance
One Renderable's data on the GPU: its World transform, its Mesh, and the Material ID that overrides its Mesh's Materials, if any.
Meshlet
A small group of one Primitive's triangles, bounded in vertex and triangle count, that the GPU draws as a unit and can cull as one.
Material ID
The number that identifies a Material to the GPU.
Capability floor
The GPU features and limits the engine requires to start. An adapter below it is refused with every gap named; nothing in it has a fallback.
Built from ticket #297, PR #380 at 496e7e70, the prototype at b846161, docs/performance.md, ADR 0019, CONTEXT.md and the sources in the reading list. Every measurement is the PR's own, on one Apple M4 Pro. Both demos are this page's illustrative models; the second's mip rule is simplified.