Skip to content

Render Pipeline

A render pipeline describes how the GPU turns vertices into pixels: which shaders run, how primitives are assembled, and which texture format receives the color. It is created before drawing. A draw call only binds it and calls draw().

The triangle example already runs one pipeline. This chapter separates the steps.

WGSL source is compiled into a GPUShaderModule. createShaderModule() does not throw on compilation errors. Read getCompilationInfo():

const shaderModule = device.createShaderModule({
label: "Triangle shader",
code: wgslSource,
});
const info = await shaderModule.getCompilationInfo();
const errors = info.messages.filter((message) => message.type === "error");
if (errors.length > 0) {
throw new Error(errors.map((message) => message.message).join("\n"));
}

The vertex and fragment entry points may share one module or live in two modules.

const pipeline = device.createRenderPipeline({
label: "Triangle pipeline",
layout: "auto",
vertex: {
module: shaderModule,
entryPoint: "vs_main",
},
fragment: {
module: shaderModule,
entryPoint: "fs_main",
targets: [{ format: canvasFormat }],
},
primitive: {
topology: "triangle-list",
},
});
  • layout: "auto" builds a pipeline layout from the @group and @binding attributes in the shader. It is the short path when the shader has no external resources. An explicit layout is shown with bind groups.
  • vertex.entryPoint must name an @vertex function.
  • fragment.targets[0].format must match the color attachment. For a canvas, use the result of navigator.gpu.getPreferredCanvasFormat(), which is "bgra8unorm" or "rgba8unorm".
  • primitive.topology controls how vertices become primitives. The default is "triangle-list": every three vertices form one triangle.

Common topologies:

topologyMeaning
"triangle-list"Independent triangle for every 3 vertices
"triangle-strip"Adjacent triangles share an edge
"line-list"Independent line segment for every 2 vertices
"line-strip"A polyline through successive vertices
"point-list"One point per vertex

frontFace defaults to "ccw" and cullMode defaults to "none".

The pipeline does not draw by itself. Each frame records commands:

  1. Call context.getCurrentTexture(), then createView(). Do not reuse that view on the next frame.
  2. beginRenderPass() names the color attachment. loadOp: "clear" fills it with clearValue. storeOp: "store" keeps the result.
  3. Call setPipeline(), then setVertexBuffer() and setBindGroup() when needed, then draw(vertexCount).
  4. Call pass.end(), encoder.finish(), and device.queue.submit().
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({
colorAttachments: [
{
view: context.getCurrentTexture().createView(),
clearValue: { r: 0.3, g: 0.3, b: 0.3, a: 1 },
loadOp: "clear",
storeOp: "store",
},
],
});
pass.setPipeline(pipeline);
pass.draw(3);
pass.end();
device.queue.submit([encoder.finish()]);

The argument to draw(3) is a vertex count, not a triangle count. Under "triangle-list", three vertices make one triangle. The positions can still live in a WGSL array, as they do in the triangle example. The next chapter reads them from a buffer instead.

The same triangle is filled with "triangle-list" and outlined with "line-strip". The outline repeats the first vertex, so it draws four vertices.

triangle-list

line-strip

Move the coordinates into a buffer so mesh data can be uploaded from JavaScript. The pipeline fields are listed under GPURenderPipeline.