encoder.beginRenderPass() returns a GPURenderPassEncoder and locks the command encoder until pass.end(). Inside, you set a pipeline, bind groups, vertex and index buffers, the viewport (setViewport(x, y, w, h, minDepth, maxDepth)), a scissor rectangle and, per draw, blend constants and stencil references. Opening a second pass before end() failed ("Recording in [CommandEncoder] which is locked while [RenderPassEncoder] is open"), as did any command after end() and a scissor rectangle larger than the target. A 16 x 16 viewport on an 8 x 8 target was accepted, since viewports may extend past the attachment. State set in one pass does not carry into the next: each pass starts with no pipeline and a full-target viewport. Keep passes few: each loads and stores its attachments.
<!doctype html>
<style>
body { margin: 0; background: #f7f4ee; font: 14px system-ui, sans-serif; }
.stage { position: relative; width: 100%; max-width: 600px; }
.stage canvas { display: block; width: 100%; }
.stage canvas + canvas { position: absolute; inset: 0; pointer-events: none; }
</style>
<div class="stage">
<canvas id="view" width="600" height="340"></canvas>
<canvas id="labels" width="600" height="340"></canvas>
</div>
<script>
const canvas = document.getElementById('view');
const ink = document.getElementById('labels').getContext('2d');
function showMessage(text) { // 2D fallback when WebGPU is missing
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#fbeaea'; ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#8a2b2b'; ctx.font = '18px system-ui, sans-serif'; ctx.textAlign = 'center';
ctx.fillText(text, canvas.width / 2, canvas.height / 2);
}
const code = /* wgsl */ `
struct Out { @builtin(position) pos: vec4f, @location(0) uv: vec2f, @location(1) @interpolate(flat) i: u32 }
@vertex fn vs(@builtin(vertex_index) v: u32, @builtin(instance_index) i: u32) -> Out {
let q = vec2f(f32(v & 1), f32(v >> 1));
return Out(vec4f(-0.9 + f32(i) * 0.3 + q.x * 0.24, -0.6 + q.y * (1.0 + f32(i % 3) * 0.15), 0, 1), q, i);
}
@fragment fn fs(in: Out) -> @location(0) vec4f {
let tints = array(vec3f(.12, .37, .55), vec3f(.36, .25, .60), vec3f(.88, .60, .06),
vec3f(.25, .49, .23), vec3f(.71, .27, .18), vec3f(.16, .62, .56));
let stripes = step(0.5, fract(in.uv.y * 12)) * step(0.1, in.uv.x) * step(in.uv.x, 0.9) * 0.15; // fine detail
return vec4f(tints[in.i] * (1 - stripes), 1);
}`;
async function main() {
const adapter = await navigator.gpu?.requestAdapter();
if (!adapter) return showMessage('WebGPU is not available in this browser');
const device = await adapter.requestDevice();
const context = canvas.getContext('webgpu');
const format = navigator.gpu.getPreferredCanvasFormat();
context.configure({ device, format });
const module = device.createShaderModule({ code });
const pipeline = device.createRenderPipeline({ layout: 'auto', primitive: { topology: 'triangle-strip' },
vertex: { module }, fragment: { module, targets: [{ format }] } });
// A mistake first, on a scratch texture: opening a second pass while the first is still open.
const scratch = device.createTexture({ size: [8, 8], format, usage: GPUTextureUsage.RENDER_ATTACHMENT }).createView();
device.pushErrorScope('validation');
const bad = device.createCommandEncoder();
bad.beginRenderPass({ colorAttachments: [{ view: scratch, loadOp: 'clear', storeOp: 'store' }] });
bad.beginRenderPass({ colorAttachments: [{ view: scratch, loadOp: 'clear', storeOp: 'store' }] });
bad.finish();
const locked = await device.popErrorScope();
const view = context.getCurrentTexture().createView(); // valid until this task ends
const encoder = device.createCommandEncoder();
// Pass 1: clear, then draw the whole shelf with the default full-target viewport.
const first = encoder.beginRenderPass({ colorAttachments: [{ view, loadOp: 'clear', storeOp: 'store',
clearValue: [0.97, 0.96, 0.93, 1] }] });
first.setPipeline(pipeline);
first.draw(4, 6);
first.end(); // unlocks the encoder
// Pass 2: keep pass 1's pixels (loadOp 'load'). State does not carry over: set the pipeline again.
const second = encoder.beginRenderPass({ colorAttachments: [{ view, loadOp: 'load', storeOp: 'store' }] });
second.setPipeline(pipeline);
second.setViewport(-500, -380, 1600, 1000, 0, 1); // an oversized viewport: a 2.7x zoom
second.setScissorRect(390, 150, 190, 150); // ...but only this rectangle is written
second.draw(4, 6);
second.end();
device.queue.submit([encoder.finish()]);
ink.strokeStyle = '#222'; ink.lineWidth = 2; ink.strokeRect(390, 150, 190, 150);
ink.font = '12px ui-monospace, monospace'; ink.fillStyle = '#222';
ink.fillText("pass 1: loadOp 'clear', draw(4, 6)", 12, 20);
ink.fillText("pass 2: loadOp 'load', setViewport + setScissorRect(390, 150, 190, 150)", 12, 38);
ink.fillStyle = '#8a2b2b'; ink.font = '11px ui-monospace, monospace';
ink.fillText('two passes open at once:', 12, 316);
ink.fillText(locked ? locked.message.split('\n')[0].slice(0, 86) : 'accepted', 12, 332);
}
main();
</script>