A depth buffer is a texture with a depth format, attached as the pass's depthStencilAttachment (depthLoadOp, depthStoreOp, depthClearValue), and the pipeline's depthStencil state must name the same format. Besides depth16unorm and stencil8, the formats are:
| Format | Bits | Notes |
|---|---|---|
| depth24plus | 24 or more | The usual choice; not copyable |
| depth24plus-stencil8 | 24 + 8 | Depth with stencil |
| depth32float | 32 float | Best with reversed depth (Depth Compare and Bias) |
| depth32float-stencil8 | 32 + 8 | Needs the feature of that name |
For formats with depth, depthWriteEnabled and depthCompare are required: leaving them out failed with "depthWriteEnabled is undefined" and "depthCompare is CompareFunction::Undefined while it's actually used". A pipeline without depthStencil failed in a pass with a depth attachment ("Attachment state ... is not compatible"). Recreate the depth texture when the canvas resizes, and store it only if a later pass reads it.
<!doctype html>
<style>
body { margin: 0; background: #f7f4ee; font: 14px system-ui, sans-serif; }
.stage { position: relative; width: 100%; max-width: 600px; }
.stage canvas { display: block; width: 100%; }
.stage canvas + canvas { position: absolute; inset: 0; pointer-events: none; }
</style>
<div class="stage">
<canvas id="view" width="600" height="320"></canvas>
<canvas id="labels" width="600" height="320"></canvas>
</div>
<script>
const canvas = document.getElementById('view');
const ink = document.getElementById('labels').getContext('2d');
function showMessage(text) { // 2D fallback when WebGPU is missing
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#fbeaea'; ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#8a2b2b'; ctx.font = '18px system-ui, sans-serif'; ctx.textAlign = 'center';
ctx.fillText(text, canvas.width / 2, canvas.height / 2);
}
// Two covers crossing like an X, spinning slowly; instance 0 is blue, instance 1 orange.
const code = /* wgsl */ `
@group(0) @binding(0) var<uniform> time: f32;
struct Out { @builtin(position) pos: vec4f, @location(0) color: vec3f }
@vertex fn vs(@builtin(vertex_index) v: u32, @builtin(instance_index) i: u32) -> Out {
let q = vec2f(f32(v & 1), f32(v >> 1)) - 0.5;
let a = time + select(0.8, -0.8, i == 1u); // the two covers face different ways
var p = vec3f(q.x * 1.2 * cos(a), q.y * 1.4, q.x * 1.2 * sin(a));
let z = p.z + 2.5;
let shade = 0.65 + 0.35 * (q.y + 0.5);
let color = select(vec3f(0.08, 0.40, 0.75), vec3f(0.88, 0.50, 0.15), i == 1u) * shade;
return Out(vec4f(p.x * 1.7, p.y * 1.7, (z - 1) / 3 * z, z), color); // depth 0..1 after the divide
}
@fragment fn fs(in: Out) -> @location(0) vec4f { return vec4f(in.color, 1); }`;
async function main() {
const adapter = await navigator.gpu?.requestAdapter();
if (!adapter) return showMessage('WebGPU is not available in this browser');
const device = await adapter.requestDevice();
const context = canvas.getContext('webgpu');
const format = navigator.gpu.getPreferredCanvasFormat();
context.configure({ device, format });
const module = device.createShaderModule({ code });
const base = { layout: 'auto', primitive: { topology: 'triangle-strip' }, vertex: { module }, fragment: { module, targets: [{ format }] } };
// depthWriteEnabled and depthCompare are required for depth formats: see what happens without them.
device.pushErrorScope('validation');
let missing = '';
try { device.createRenderPipeline({ ...base, depthStencil: { format: 'depth24plus' } }); } catch (e) { missing = e.message; }
const missingError = await device.popErrorScope();
if (missingError) missing = missingError.message.split('\n')[0];
const flat = device.createRenderPipeline(base);
const tested = device.createRenderPipeline({ ...base,
depthStencil: { format: 'depth24plus', depthWriteEnabled: true, depthCompare: 'less' } });
const depthTexture = device.createTexture({ size: [600, 320], format: 'depth24plus', usage: GPUTextureUsage.RENDER_ATTACHMENT });
const time = device.createBuffer({ size: 4, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
const groupFor = (p) => device.createBindGroup({ layout: p.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer: time } }] });
const flatGroup = groupFor(flat), testedGroup = groupFor(tested);
ink.font = '12px ui-monospace, monospace'; ink.fillStyle = '#222';
ink.fillText('no depthStencil: last drawn wins', 20, 22);
ink.fillText("depth24plus, depthCompare: 'less'", 320, 22);
ink.fillStyle = '#8a2b2b'; ink.font = '11px ui-monospace, monospace';
ink.fillText(`without depthWriteEnabled/depthCompare: ${missing}`.slice(0, 90), 10, 312);
function frame(now) {
device.queue.writeBuffer(time, 0, new Float32Array([now / 1500]));
const encoder = device.createCommandEncoder();
const view = context.getCurrentTexture().createView();
const left = encoder.beginRenderPass({ colorAttachments: [{ view, clearValue: [0.97, 0.96, 0.93, 1], loadOp: 'clear', storeOp: 'store' }] });
left.setViewport(0, 30, 300, 270, 0, 1);
left.setPipeline(flat); left.setBindGroup(0, flatGroup); left.draw(4, 2);
left.end();
const right = encoder.beginRenderPass({ colorAttachments: [{ view, loadOp: 'load', storeOp: 'store' }],
depthStencilAttachment: { view: depthTexture.createView(), depthClearValue: 1, depthLoadOp: 'clear', depthStoreOp: 'discard' } });
right.setViewport(300, 30, 300, 270, 0, 1);
right.setPipeline(tested); right.setBindGroup(0, testedGroup); right.draw(4, 2);
right.end();
device.queue.submit([encoder.finish()]);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
}
main();
</script>