Last active
March 24, 2025 05:00
-
-
Save greggman/a971bb142a6cfc7487a5cc6ea18edd06 to your computer and use it in GitHub Desktop.
WebGPU array of struct using webgpu-utils
This file contains 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
html, body { margin: 0; height: 100% } | |
canvas { width: 100%; height: 100%; display: block; } | |
#fail { | |
position: fixed; | |
left: 0; | |
top: 0; | |
width: 100%; | |
height: 100%; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
background: red; | |
color: white; | |
font-weight: bold; | |
font-family: monospace; | |
font-size: 16pt; | |
text-align: center; | |
} |
This file contains 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
<canvas></canvas> | |
<div id="fail" style="display: none"> | |
<div class="content"></div> | |
</div> | |
This file contains 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
import { | |
getSizeAndAlignmentOfUnsizedArrayElement, | |
makeShaderDataDefinitions, | |
makeStructuredView, | |
} from 'https://greggman.github.io/webgpu-utils/dist/1.x/webgpu-utils.module.js'; | |
async function main() { | |
const adapter = await navigator.gpu?.requestAdapter(); | |
const device = await adapter?.requestDevice(); | |
if (!device) { | |
fail('need webgpu'); | |
return; | |
} | |
device.addEventListener('uncapturederror', e => console.error(e.error.message)); | |
const canvas = document.querySelector('canvas'); | |
const context = canvas.getContext('webgpu'); | |
const presentationFormat = navigator.gpu.getPreferredCanvasFormat(adapter); | |
context.configure({ | |
format: presentationFormat, | |
device, | |
}); | |
const numThings = 100; | |
const shaderSrc = ` | |
struct MyThing { | |
a: f32, | |
b: f32, | |
c: f32, | |
foo: u32, // (you must pick a representation for bool) | |
bar: u32, | |
// uniform array elements must be aligned to 16 bytes (storage buffers don't have this restriction) | |
pad0: u32, | |
pad1: u32, | |
pad2: u32, | |
}; | |
// uniform arrays must be sized at compile time (storage buffers don't have this restriction) | |
@group(0) @binding(0) var<uniform> things: array<MyThing, ${numThings}>; | |
struct MyVSOutput { | |
@builtin(position) position: vec4f, | |
@location(0) color: vec4f, | |
}; | |
@vertex fn myVSMain(@builtin(instance_index) i: u32, @builtin(vertex_index) vNdx: u32) -> MyVSOutput { | |
let pos = array( | |
vec2f(-1, -1), | |
vec2f( 1, -1), | |
vec2f(-1, 1), | |
vec2f(-1, 1), | |
vec2f( 1, -1), | |
vec2f( 1, 1), | |
); | |
let colors = array( | |
vec3f(1, 0, 0), | |
vec3f(1, 1, 0), | |
vec3f(0, 1, 0), | |
vec3f(0, 1, 1), | |
vec3f(0, 0, 1), | |
vec3f(1, 0, 1), | |
); | |
var vsOut: MyVSOutput; | |
let thing = things[i]; | |
vsOut.position = vec4(pos[vNdx] * thing.c + vec2f(thing.a, thing.b), 0, 1); | |
let brightness = select(0.5, 1.0, (thing.foo & 1) != 0); | |
let color = colors[thing.bar]; | |
vsOut.color = vec4f(color * brightness, 1); | |
return vsOut; | |
} | |
@fragment fn myFSMain(v: MyVSOutput) -> @location(0) vec4f { | |
return vec4f(v.color); | |
} | |
`; | |
const module = device.createShaderModule({code: shaderSrc}); | |
const defs = makeShaderDataDefinitions(shaderSrc); | |
// If we used an unsized strorage buffer in the shader then we'd need to tell webgpu-utils | |
// the size of the buffer so it knows how many element views to make | |
//const {size} = getSizeAndAlignmentOfUnsizedArrayElement(defs.uniforms.things); | |
//const thingsView = makeStructuredView(defs.uniforms.things, new ArrayBuffer(size * numThings)); | |
// We're using a buffer with a fixed size so we don't need the 2 lines above. | |
const thingsView = makeStructuredView(defs.uniforms.things); | |
const r = (min, max) => Math.random() * (max - min) + min; | |
const things = []; | |
for (let i = 0; i < 100; ++i) { | |
things.push({ | |
a: r(-1, 1), | |
b: r(-1, 1), | |
c: r(0.02, 0.1), | |
foo: r(0, 1) < 0.5 ? 0 : 1, | |
bar: r(0, 6) | 0, | |
}); | |
} | |
thingsView.set(things); | |
const thingBuffer = device.createBuffer({ | |
size: thingsView.arrayBuffer.byteLength, | |
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, | |
}); | |
// copy the thing data into the thing buffer | |
device.queue.writeBuffer(thingBuffer, 0, thingsView.arrayBuffer); | |
const pipeline = device.createRenderPipeline({ | |
layout: 'auto', | |
vertex: { module }, | |
fragment: { module, targets: [{ format: presentationFormat }] }, | |
}); | |
const bindGroup = device.createBindGroup({ | |
layout: pipeline.getBindGroupLayout(0), | |
entries: [ | |
{ binding: 0, resource: { buffer: thingBuffer } }, | |
], | |
}); | |
const renderPassDescriptor = { | |
colorAttachments: [ | |
{ | |
view: undefined, // Assigned later | |
clearValue: [0.2, 0.2, 0.2, 1], | |
loadOp: 'clear', | |
storeOp: 'store', | |
}, | |
], | |
}; | |
function render() { | |
const canvasTexture = context.getCurrentTexture(); | |
renderPassDescriptor.colorAttachments[0].view = canvasTexture.createView(); | |
const commandEncoder = device.createCommandEncoder(); | |
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); | |
passEncoder.setPipeline(pipeline); | |
passEncoder.setBindGroup(0, bindGroup); | |
passEncoder.draw(6, numThings); | |
passEncoder.end(); | |
device.queue.submit([commandEncoder.finish()]); | |
} | |
const observer = new ResizeObserver(entries => { | |
for (const entry of entries) { | |
const canvas = entry.target; | |
const width = entry.contentBoxSize[0].inlineSize; | |
const height = entry.contentBoxSize[0].blockSize; | |
canvas.width = Math.max(1, Math.min(width, device.limits.maxTextureDimension2D)); | |
canvas.height = Math.max(1, Math.min(height, device.limits.maxTextureDimension2D)); | |
render(); | |
} | |
}); | |
observer.observe(canvas); | |
} | |
function fail(msg) { | |
const elem = document.querySelector('#fail'); | |
const contentElem = elem.querySelector('.content'); | |
elem.style.display = ''; | |
contentElem.textContent = msg; | |
} | |
main(); | |
This file contains 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 array of struct using webgpu-utils","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