Skip to content

Instantly share code, notes, and snippets.

@greggman
Last active March 24, 2025 05:00
Show Gist options
  • Save greggman/a971bb142a6cfc7487a5cc6ea18edd06 to your computer and use it in GitHub Desktop.
Save greggman/a971bb142a6cfc7487a5cc6ea18edd06 to your computer and use it in GitHub Desktop.
WebGPU array of struct using webgpu-utils
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;
}
<canvas></canvas>
<div id="fail" style="display: none">
<div class="content"></div>
</div>
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();
{"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