Last active
February 24, 2026 20:55
-
-
Save greggman/54d9a080ec0fd870bdf56387775a018b to your computer and use it in GitHub Desktop.
WebGPU: Check depth write values when multisampled
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /*bug-in-github-api-content-can-not-be-empty*/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /*bug-in-github-api-content-can-not-be-empty*/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| {"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