Stencil Operations and Masks

The stencil buffer holds a small integer per pixel. stencilFront and stencilBack set a compare against the reference from pass.setStencilReference() and what to do on failOp, depthFailOp and passOp (keep, zero, replace, invert, increment-clamp, decrement-clamp, increment-wrap, decrement-wrap), filtered by stencilReadMask and stencilWriteMask. Here a first pipeline writes 1 into two round "shop windows" without touching color, and a second draws the covers only where the stencil equals 1:

Covers seen through stencil-masked windowsHTMLLive
<canvas id="gpu" width="480" height="120"></canvas>
<script type="module">
const code = /* wgsl */ `
const tints = array(vec3f(.08, .40, .75), vec3f(.18, .49, .20), vec3f(.94, .42, 0),
                    vec3f(.42, .11, .60), vec3f(.22, .28, .31), vec3f(.78, .16, .16));
struct Out { @builtin(position) pos: vec4f, @location(0) uv: vec2f,
             @location(1) @interpolate(flat) b: u32 }
@vertex fn vs(@builtin(vertex_index) v: u32, @builtin(instance_index) b: u32) -> Out {
  let uv = array(vec2f(0, 0), vec2f(1, 0), vec2f(0, 1), vec2f(1, 0), vec2f(1, 1),
                 vec2f(0, 1))[v];
  if (b == 6) { return Out(vec4f(uv * 2 - 1, 0, 1), uv, b); }   // instance 6: whole screen
  return Out(vec4f(-0.93 + f32(b) * 0.31 + uv.x * 0.27, uv.y * 1.7 - 0.85, 0, 1), uv, b);
}
@fragment fn window(in: Out) {                       // writes stencil only, no color
  let p = (in.uv - 0.5) * vec2f(480, 120);
  if (length(p - vec2f(-60, 0)) > 55 && length(p - vec2f(90, 0)) > 40) { discard; }
}
@fragment fn cover(in: Out) -> @location(0) vec4f { return vec4f(tints[in.b], 1); }`;
const device = await (await navigator.gpu.requestAdapter()).requestDevice();
const context = gpu.getContext('webgpu'), format = navigator.gpu.getPreferredCanvasFormat();
context.configure({ device, format });
const module = device.createShaderModule({ code });
const stencil = (compare, passOp) => ({ format: 'stencil8',
  stencilFront: { compare, passOp }, stencilBack: { compare, passOp } });
const mask = device.createRenderPipeline({ layout: 'auto', vertex: { module },
  fragment: { module, entryPoint: 'window', targets: [{ format, writeMask: 0 }] },
  depthStencil: stencil('always', 'replace') });         // stencil = reference where drawn
const covers = device.createRenderPipeline({ layout: 'auto', vertex: { module },
  fragment: { module, entryPoint: 'cover', targets: [{ format }] },
  depthStencil: stencil('equal', 'keep') });             // draw only where stencil == 1
const view = device.createTexture({ size: [480, 120], format: 'stencil8',
  usage: GPUTextureUsage.RENDER_ATTACHMENT }).createView();
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({ colorAttachments: [{ loadOp: 'clear', storeOp: 'store',
    view: context.getCurrentTexture().createView(), clearValue: [0.22, 0.2, 0.18, 1] }],
  depthStencilAttachment: { view, stencilClearValue: 0, stencilLoadOp: 'clear',
    stencilStoreOp: 'discard' } });
pass.setStencilReference(1);
pass.setPipeline(mask), pass.draw(6, 1, 0, 6);          // 1: punch two round windows
pass.setPipeline(covers), pass.draw(6, 6);              // 2: covers show through them
pass.end(), device.queue.submit([encoder.finish()]);
await device.queue.onSubmittedWorkDone();
window.__done = true;
</script>
Browser output of Listing 4.64
Browser output of 64

Stencil 672,850 also drives portals, mirrors and outlines.