WebGPU Command Encoder
Record GPU work into a GPUCommandEncoder, encode render or compute passes and
copy commands, finish into a GPUCommandBuffer, and submit it to device.queue.
Quick Reference
| Step | Call | Returns |
|---|---|---|
| Create encoder | device.createCommandEncoder({ label }) |
GPUCommandEncoder |
| Start render pass | encoder.beginRenderPass(descriptor) |
GPURenderPassEncoder |
| Start compute pass | encoder.beginComputePass({ label, timestampWrites }) |
GPUComputePassEncoder |
| End any pass | passEncoder.end() |
undefined |
| Copy buffer | encoder.copyBufferToBuffer(...) |
undefined |
| Resolve queries | encoder.resolveQuerySet(...) |
undefined |
| Finish recording | encoder.finish({ label }) |
GPUCommandBuffer |
| Submit work | device.queue.submit([commandBuffer]) |
undefined |
| Await completion | device.queue.onSubmittedWorkDone() |
Promise<undefined> |
| Pass type | Begin method | Encoder type | Records |
|---|---|---|---|
| Render | beginRenderPass |
GPURenderPassEncoder |
draws into color/depth attachments |
| Compute | beginComputePass |
GPUComputePassEncoder |
dispatchWorkgroups calls |
| (none) | copy/resolve methods | GPUCommandEncoder directly |
data moves, query resolves |
Runtime model order: create encoder per frame, encode passes and copies, call
finish(), then queue.submit(). A GPUCommandBuffer is single-use.
Version: WebGPU 1.0-stable. Chrome 113+, Safari 26+, Firefox 141+.
Decision Tree
Need to do GPU work?
├─ Draw geometry into a texture (color/depth attachments)?
│ -> encoder.beginRenderPass(descriptor) -> draw* -> pass.end()
├─ Run a compute shader (dispatchWorkgroups)?
│ -> encoder.beginComputePass({...}) -> dispatchWorkgroups -> pass.end()
├─ Move data between resources, no shader?
│ ├─ buffer -> buffer -> encoder.copyBufferToBuffer(...)
│ ├─ buffer -> texture -> encoder.copyBufferToTexture(...)
│ ├─ texture -> buffer -> encoder.copyTextureToBuffer(...)
│ └─ texture -> texture -> encoder.copyTextureToTexture(...)
└─ Read GPU timing or occlusion counts?
-> createQuerySet -> timestampWrites on a pass -> resolveQuerySet -> copy to MAP_READ buffer
Copy methods are recorded directly on the GPUCommandEncoder, NEVER inside an
open pass. A pass MUST be ended with pass.end() before any encoder-level call.
Core Patterns
Pattern 1: One encoder per frame, never reuse a command buffer
ALWAYS create a fresh GPUCommandEncoder and a fresh GPUCommandBuffer for each
frame. NEVER call queue.submit twice with the same command buffer.
function frame() {
const encoder = device.createCommandEncoder({ label: "frame-encoder" });
// ... encode passes ...
const commandBuffer = encoder.finish();
device.queue.submit([commandBuffer]); // commandBuffer is now consumed
requestAnimationFrame(frame);
}
Pattern 2: Always end a pass before finishing the encoder
ALWAYS call passEncoder.end() before encoder.finish(). An encoder with an
open pass throws on finish().
const pass = encoder.beginRenderPass(renderPassDescriptor);
pass.setPipeline(pipeline);
pass.draw(3);
pass.end(); // mandatory before finish()
const cb = encoder.finish();
Pattern 3: Copy commands sit outside passes
ALWAYS record copyBufferToBuffer and the other copy methods directly on the
encoder, NEVER between beginRenderPass and pass.end().
const pass = encoder.beginComputePass();
pass.setPipeline(computePipeline);
pass.setBindGroup(0, bindGroup);
pass.dispatchWorkgroups(64);
pass.end(); // pass closed first
encoder.copyBufferToBuffer(storageBuffer, readbackBuffer); // then the copy
Pattern 4: Await GPU completion with onSubmittedWorkDone
ALWAYS use queue.onSubmittedWorkDone() to know when submitted work finished.
NEVER await it inside the render loop because it forces a CPU-GPU stall.
device.queue.submit([encoder.finish()]);
await device.queue.onSubmittedWorkDone(); // safe at load time, NOT per frame
await readbackBuffer.mapAsync(GPUMapMode.READ);
Pattern 5: Buffer copies obey the 4-byte alignment rule
ALWAYS keep sourceOffset, destinationOffset, and size as multiples of 4 in
copyBufferToBuffer. The source needs COPY_SRC, the destination needs COPY_DST.
For buffer-texture copies bytesPerRow MUST be a multiple of 256 (see
webgpu-core-memory-model).
encoder.copyBufferToBuffer(src, 0, dst, 0, 1024); // all multiples of 4
Pattern 6: Timestamp queries are a gated optional feature
ALWAYS request the timestamp-query feature before using timestampWrites or a
"timestamp" query set. NEVER assume the feature exists.
const device = await adapter.requestDevice({
requiredFeatures: adapter.features.has("timestamp-query")
? ["timestamp-query"]
: [],
});
Common Anti-Patterns
Calling
encoder.finish()with an open pass. ForgettingpassEncoder.end()leaves the pass open;finish()then throws. Fix: callpass.end()on every pass beforefinish().Reusing a
GPUCommandBufferafter submission. A command buffer is consumed byqueue.submit; submitting it again is a validation error. Fix: create a fresh encoder and buffer for every frame and every submit.Using
timestampWriteswithout thetimestamp-queryfeature. Creating a"timestamp"query set or passingtimestampWriteswithout the feature fails validation. Fix: feature-detect on the adapter and gate the code.
See references/anti-patterns.md for the full list with WHY explanations.
Critical Warnings
- NEVER call
encoder.finish()while any pass started on that encoder is still open. - NEVER call
queue.submitwith a command buffer that was already submitted. - NEVER record any command into an encoder after
finish()has been called on it. - NEVER record copy or resolve commands inside an open render or compute pass.
- NEVER
awaitonSubmittedWorkDone()ormapAsync()inside the per-frame render loop. - NEVER create a
"timestamp"query set without thetimestamp-querydevice feature. - NEVER pass a
destinationOffsettoresolveQuerySetthat is not a multiple of 256.
Reference Files
references/methods.md: full signatures forcreateCommandEncoder,GPURenderPassDescriptor,GPUComputePassDescriptor, all copy methods,finish,queue.submit,onSubmittedWorkDone,createQuerySet,resolveQuerySet, and debug group methods.references/examples.md: verified render pass, compute pass, buffer-to-buffer copy, and timestamp query examples.references/anti-patterns.md: mistakes with WHY-it-fails explanations.
Related skills: webgpu-core-memory-model (alignment, bytesPerRow 256 rule),
webgpu-impl-render-targets (color and depth attachment detail),
webgpu-syntax-compute-pipeline (compute pipeline and dispatchWorkgroups),
webgpu-impl-performance (timestamp profiling, render bundles, submit batching).