Skip to content

Instantly share code, notes, and snippets.

@greggman
Last active February 24, 2026 20:55
Show Gist options
  • Select an option

  • Save greggman/54d9a080ec0fd870bdf56387775a018b to your computer and use it in GitHub Desktop.

Select an option

Save greggman/54d9a080ec0fd870bdf56387775a018b to your computer and use it in GitHub Desktop.
WebGPU: Check depth write values when multisampled
/*bug-in-github-api-content-can-not-be-empty*/
/*bug-in-github-api-content-can-not-be-empty*/
const adapter = await navigator.gpu?.requestAdapter({
compatibilityMode: true,
});
const device = await adapter?.requestDevice();
device.addEventListener('uncapturederror', e => console.error(e.error.message));
const texture0 = device.createTexture({
size: [2, 2],
sampleCount: 4,
format: 'r32float',
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT,
});
const texture1 = device.createTexture({
size: [texture0.width, texture0.height],
sampleCount: 4,
format: 'r32float',
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT,
});
const depthTexture = device.createTexture({
size: [texture0.width, texture0.height],
sampleCount: 4,
format: 'depth32float',
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT,
});
function render(sampling, useFragDepth) {
{
const module = device.createShaderModule({code: `
struct VOut {
@builtin(position) pos: vec4f,
@location(0) @interpolate(perspective, ${sampling}) i: f32,
};
@vertex fn vs(@builtin(vertex_index) vNdx: u32) -> VOut {
let pos = array(
vec3f(-1, 3, 1),
vec3f( 3, -1, 1),
vec3f(-1, -1, 0),
);
let p = pos[vNdx];
return VOut(vec4f(p, 1), p.z);
}
struct FOut {
@location(0) o0: vec4f,
@location(1) o1: vec4f,
${useFragDepth ? '@builtin(frag_depth) fragDepth: f32,' : ''}
}
@fragment fn fs(i: VOut) -> FOut {
return FOut(
vec4f(i.pos.z),
vec4f(i.i),
${useFragDepth ? 'i.i + 0.1,' : ''}
);
}
`,
});
const pipeline = device.createRenderPipeline({
layout: 'auto',
vertex: { module },
fragment: {
module,
targets: [
{ format: 'r32float' },
{ format: 'r32float' },
],
},
depthStencil: {
depthWriteEnabled: true,
depthCompare: 'always',
format: 'depth32float',
},
multisample: {
count: 4,
}
});
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({
colorAttachments: [
{
view: texture0.createView(),
clearValue: [0, 0, 0, 0],
loadOp: 'clear',
storeOp: 'store',
},
{
view: texture1.createView(),
clearValue: [0, 0, 0, 0],
loadOp: 'clear',
storeOp: 'store',
},
],
depthStencilAttachment: {
view: depthTexture.createView(),
depthClearValue: 1.0,
depthLoadOp: 'clear',
depthStoreOp: 'store',
},
});
pass.setPipeline(pipeline);
pass.draw(3);
pass.end();
device.queue.submit([encoder.finish()]);
}
}
async function copyMultisampleTexture(texture) {
const module = device.createShaderModule({code: `
@group(0) @binding(0) var tex: texture_multisampled_2d<f32>;
@group(0) @binding(1) var<storage, read_write> result: array<array<array<f32, ${texture.sampleCount}>, ${texture.width}>>;
@compute @workgroup_size(1) fn cs(@builtin(global_invocation_id) id: vec3u) {
result[id.y][id.x][id.z] = textureLoad(tex, id.xy, id.z).r;
}
`,
});
const pipeline = device.createComputePipeline({
layout: 'auto',
compute: { module },
});
const storageBuffer = device.createBuffer({
size: texture.width * texture.height * texture.sampleCount * 4,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
});
const resultBuffer = device.createBuffer({
size: storageBuffer.size,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
});
const bindGroup = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: texture.createView() },
{ binding: 1, resource: { buffer: storageBuffer }},
],
});
const encoder = device.createCommandEncoder();
const pass = encoder.beginComputePass();
pass.setPipeline(pipeline);
pass.setBindGroup(0, bindGroup);
pass.dispatchWorkgroups(depthTexture.width, depthTexture.height, depthTexture.sampleCount);
pass.end();
encoder.copyBufferToBuffer(storageBuffer, 0, resultBuffer, 0, resultBuffer.size);
device.queue.submit([encoder.finish()]);
await resultBuffer.mapAsync(GPUMapMode.READ);
const result = new Float32Array(resultBuffer.getMappedRange()).slice();
resultBuffer.unmap();
return result;
}
const range = (length, fn) => Array.from({ length }, fn);
async function showResults(labels, results, texture) {
const colHeadings = labels.join(' | ');
const colWidths = labels.map(v => v.length);
const headings = `║ ${range(texture.width, () => colHeadings).join(' ║ ')} ║`;
const rowSeparator = `------╫-${range(texture.width, (i) => colWidths.map(w => ''.padEnd(w, '-')).join('-+-')).join('-║-')}-║`;
for (let y = 0; y < texture.height; ++y) {
console.log(rowSeparator);
console.log(`y: ${y} ${headings}`);
console.log(rowSeparator);
for (let s = 0; s < texture.sampleCount; ++s) {
const line = [];
for (let x = 0; x < texture.width; ++x) {
const loc = [];
const offset = (y * texture.width + x) * texture.sampleCount + s;
results.forEach((result, i) => {
const v = result[offset];
loc.push(v.toFixed(4).padEnd(colWidths[i]));
});
line.push(loc.join(' | '))
}
console.log(`smp${s}: ║ ${line.join(' ║ ')} ║`);
}
}
console.log(rowSeparator);
console.log('');
}
for (const sampling of ['center', 'centroid', 'sample']) {
for (const useFragDepth of [false, true]) {
console.log(`==============[ ${sampling}, ${useFragDepth ? 'frag_depth' : ''} ]============`);
render(sampling, useFragDepth);
const results = await Promise.all([
await copyMultisampleTexture(texture0),
await copyMultisampleTexture(texture1),
await copyMultisampleTexture(depthTexture),
])
showResults(['position', 'interstage', 'depthtex'], results, texture0);
}
}
{"name":"WebGPU: Check depth write values when multisampled","settings":{},"filenames":["index.html","index.css","index.js"]}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment