Skip to content

Instantly share code, notes, and snippets.

@greggman
Last active June 8, 2026 19:49
Show Gist options
  • Select an option

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

Select an option

Save greggman/ac2a856a7d8056ab4ebb3bb8bb614f57 to your computer and use it in GitHub Desktop.
WebGPU: Capture during rendering via toDataURL and toBlob

WebGPU: Capture during rendering via toDataURL and toBlob

view on jsgist

:root {
color-scheme: light dark;
}
.result {
display: inline-block;
border: 1px solid gray;
margin: 2px;
font-size: x-small;
}
hr {
margin-top: 1em;
}
canvas, img {
border: 1px solid gray;
width: 120px;
}
<canvas></canvas>
async function main() {
const adapter = await navigator.gpu?.requestAdapter();
const device = await adapter?.requestDevice();
if (!device) {
fail('need a browser that supports WebGPU');
return;
}
// Get a WebGPU context from the canvas and configure it
const canvas = document.querySelector('canvas');
const context = canvas.getContext('webgpu');
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
context.configure({
device,
format: presentationFormat,
});
const module = device.createShaderModule({
label: 'our hardcoded red triangle shaders',
code: /* wgsl */ `
struct VertexOut {
@builtin(position) pos: vec4f,
@location(0) @interpolate(flat, either) colorNdx: u32,
};
@vertex fn vs(
@builtin(vertex_index) vertexIndex : u32,
@builtin(instance_index) instanceIndex: u32,
) -> VertexOut {
let pos = array(
vec2f( 0.0, 0.5), // top center
vec2f(-0.5, -0.5), // bottom left
vec2f( 0.5, -0.5) // bottom right
);
let offsets = array(
vec2f( 0.0, 0.5), // top middle
vec2f(-0.5, -0.5), // left bottom
vec2f( 0.5, -0.5), // right bottom
);
return VertexOut(
vec4f(pos[vertexIndex] + offsets[instanceIndex], 0.0, 1.0),
instanceIndex,
);
}
@fragment fn fs(in: VertexOut) -> @location(0) vec4f {
let colors = array(
vec4f(1, 0, 0, 1), // red
vec4f(0, 1, 0, 1), // green
vec4f(0, 0, 1, 1), // blue
);
return colors[in.colorNdx];
}
`,
});
const pipeline = device.createRenderPipeline({
label: 'our hardcoded red triangle pipeline',
layout: 'auto',
vertex: {
module,
},
fragment: {
module,
targets: [{ format: presentationFormat }],
},
});
const renderPassDescriptor = {
label: 'our basic canvas renderPass',
colorAttachments: [
{
// view: <- to be filled out when we render
clearValue: [0.3, 0.3, 0.3, 1],
loadOp: 'clear',
storeOp: 'store',
},
],
};
function renderOneTriangle(ndx) {
// Get the current texture from the canvas context and
// set it as the texture to render to.
renderPassDescriptor.colorAttachments[0].view =
context.getCurrentTexture().createView();
renderPassDescriptor.colorAttachments[0].loadOp = ndx ? 'load' : 'clear';
const encoder = device.createCommandEncoder({ label: 'our encoder' });
const pass = encoder.beginRenderPass(renderPassDescriptor);
pass.setPipeline(pipeline);
pass.draw(3, 1, 0, ndx); // call our vertex shader 3 times
pass.end();
const commandBuffer = encoder.finish();
device.queue.submit([commandBuffer]);
}
function addImage(img, method, when) {
const div = document.createElement('div');
div.className = 'result';
div.append(img);
const msg = document.createElement('div');
msg.textContent = `${method}: ${when}`;
div.append(msg);
document.body.append(div);
}
function captureCanvasViaToDataURL(when) {
const img = new Image();
img.src = context.canvas.toDataURL();
addImage(img, 'toDataURL', when);
}
function captureCanvasViaToBlob(when) {
return new Promise(resolve => {
context.canvas.toBlob((blob) => {
const img = new Image();
img.src = URL.createObjectURL(blob);
addImage(img, 'toBlob', when);
resolve();
});
});
}
function render(captureMethod) {
const promises = [];
promises.push(captureMethod('before drawing'));
renderOneTriangle(0);
promises.push(captureMethod('after 1st draw'));
renderOneTriangle(1);
promises.push(captureMethod('after 2nd draw'));
renderOneTriangle(2);
promises.push(captureMethod('after 3rd draw'));
return Promise.all(promises);
}
function log(...args) {
document.body.appendChild(document.createElement('hr'));
const div = document.createElement('div');
div.textContent = args.join(' ');
document.body.appendChild(div);
}
log('start (expected: first result is black, but not broken)');
await render(captureCanvasViaToDataURL);
log('wait via setTimeout(0)');
await new Promise(resolve => setTimeout(resolve, 0));
await render(captureCanvasViaToBlob);
log('wait via setTimeout(100)');
await new Promise(resolve => setTimeout(resolve, 100));
await render(captureCanvasViaToDataURL);
log('wait via setTimeout(100)');
await new Promise(resolve => setTimeout(resolve, 100));
await render(captureCanvasViaToBlob);
log('wait via requestAnimationFrame()');
await new Promise(resolve => requestAnimationFrame(resolve));
await render(captureCanvasViaToDataURL);
log('wait via requestAnimationFrame()');
await new Promise(resolve => requestAnimationFrame(resolve));
await render(captureCanvasViaToBlob);
}
function fail(msg) {
// eslint-disable-next-line no-alert
alert(msg);
}
main();
{"name":"WebGPU: Capture during rendering via toDataURL and toBlob","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