# The pipeline: shaders, buffers, and the first triangle The [software rasteriser](software-rasteriser) did three jobs per triangle: move the vertices to the screen, decide which pixels are covered, and choose each pixel's colour. On the graphics card the middle job is fixed hardware. The first and last are programs you write, the **vertex shader** and the **fragment shader**, and a **pipeline** binds them to a description of the data they are fed. This chapter draws one triangle, then the same data through an index buffer, and ends with what the card says when something is wrong. ```{index} single: shader single: vertex shader single: fragment shader single: WGSL ``` ## The shader Both programs live in one WGSL source: ```wgsl struct VOut { @builtin(position) pos: vec4, @location(0) col: vec3, }; @vertex fn vs(@location(0) p: vec2, @location(1) c: vec3) -> VOut { var o: VOut; o.pos = vec4(p, 0.0, 1.0); o.col = c; return o; } @fragment fn fs(i: VOut) -> @location(0) vec4 { return vec4(i.col, 1.0); } ``` `vs` runs once per vertex. Its inputs carry `@location` numbers, which is how the vertex data below finds them, and its output must include a `@builtin(position)`: the vertex in [clip space](foundations), `x, y, z, w`. With `z = 0` and `w = 1` the divide changes nothing, so `x` and `y` here are already normalised device coordinates, -1 to +1 with y up. `fs` runs once per covered pixel. What it receives is `vs`'s output **interpolated** across the triangle with barycentric weights, exactly as the rasteriser chapter computed them by hand, and perspective-correct without being asked. Its `@location(0)` output is the colour written to the target. WGSL is strictly typed: `vec2` is two 32-bit floats, `let` names a value, `var` a variable. There is no double precision; everything the card computes here is `f32`. `$gpu->shader($source)` compiles it, and a mistake in it is a Perl exception carrying the compiler's message, shown below. ```{index} single: vertex buffer single: pack; vertex data ``` ## Vertex data Three vertices, each a position and a colour, packed as 32-bit floats: ```perl # x y r g b my @tri = ( -0.8, -0.8, 1, 0, 0, 0.8, -0.8, 0, 1, 0, 0.0, 0.8, 0, 0, 1 ); my $verts = $gpu->vertex_buffer(pack 'f*', @tri); print $verts->size; # 60 ``` Fifteen floats of four bytes each. The buffer is bytes on the card and knows nothing of their meaning; the pipeline supplies that. ```{index} single: render pipeline single: vertex layout single: stride ``` ## The pipeline ```perl my $pipe = $gpu->render_pipeline($gpu->shader($WGSL), vertex => 'vs', fragment => 'fs', vertex_buffers => [ { attributes => ['float32x2', 'float32x3'] } ], ); ``` `vertex_buffers` describes each buffer as a list of attribute formats. The short form above lays them end to end: `float32x2` at `@location(0)`, offset 0; `float32x3` at `@location(1)`, offset 8; and a **stride** of 20 bytes from one vertex to the next. Written out in full, which is necessary only when the layout is not that simple: ```perl vertex_buffers => [ { stride => 20, attributes => [ { format => 'float32x2', location => 0, offset => 0 }, { format => 'float32x3', location => 1, offset => 8 } ], } ], ``` The format names are WebGPU's own: `float32` to `float32x4`, the `uint32` and `sint32` families, `unorm8x4`, `float16x2`. A name that is not one of them is refused with the whole list. The pipeline also fixes the **format** of the target it draws into, `rgba8unorm` unless told otherwise. For a window that must be the surface's format: ```perl my $pipe = $gpu->render_pipeline($shader, format => $surf->format, ...); ``` Compiling a pipeline is the expensive step. Build it once, before the frame loop, and use it every frame. ```{index} single: draw call ``` ## The draw ```perl my ($W, $H) = (256, 256); my $img = $gpu->texture(width => $W, height => $H); $gpu->render_pass($img, clear => [0.1, 0.1, 0.1, 1]) ->pipeline($pipe)->vertices($verts)->draw(3)->end; ``` Every recording call returns the pass, so a frame reads as one sentence: this pipeline, these vertices, three of them, done. The order matters in one place only: `pipeline` comes first, because what follows is checked against it. Read back, the pixels say what was drawn: ```text centre 63 65 127 255 corner 26 26 26 255 near bottom-left vertex 246 3 6 255 near bottom-right vertex 3 246 6 255 near top vertex 2 4 249 255 ``` The corner is the clear colour, 0.1 as a byte. Each vertex's neighbourhood is almost pure red, green or blue, and the centre is a mix of all three, which is the interpolation at work. Written out with `save_png` from the [previous chapter](gpu-device-and-window), it is the familiar colour-blended triangle, point upwards: NDC's y points up and the texture's top row comes first, the same flip [foundations](foundations) made in its screen mapping. ### The same pixels as the software rasteriser The rasteriser chapter filled a triangle with corners at pixel positions `(1,1)`, `(17,2)` and `(6,8)` on a 20 by 10 canvas and printed 54 pixels. Converted to NDC and drawn by the card into a 20 by 10 texture, printing `#` for every pixel that was written: ```perl my @ndc = map { (2 * $_->[0] / 20 - 1, 1 - 2 * $_->[1] / 10) } [1,1], [17,2], [6,8]; ``` ```text .................... .########........... ..##############.... ...###########...... ...#########........ ....#######......... .....####........... ......#............. .................... .................... ``` 54 pixels, identical to the figure the edge function produced, and identical again with the vertex order reversed. The card is running the same inside test at the same pixel centres. ```{index} single: index buffer single: index buffer; 16-bit indices ``` ## Index buffers A quad is two triangles sharing an edge. Listing six vertices repeats two of them; an **index buffer** lists each vertex once and says which three make each triangle: ```perl my $corners = $gpu->vertex_buffer(pack 'f*', -0.5, -0.5, 1, 0.8, 0.2, 0.5, -0.5, 1, 0.8, 0.2, 0.5, 0.5, 1, 0.8, 0.2, -0.5, 0.5, 1, 0.8, 0.2); my $quad = $gpu->index_buffer(pack 'L*', 0, 1, 2, 0, 2, 3); my $sq = $gpu->texture(width => 64, height => 64); $gpu->render_pass($sq, clear => [0, 0, 0, 1]) ->pipeline($pipe)->vertices($corners)->indices($quad)->draw_indexed(6)->end; ``` Same pipeline, same vertex layout; only the drawing call changed. The quad covers 1024 pixels of the 64 by 64 target, which is 32 by 32: half the width and half the height, exactly. `draw_indexed` counts indices, not vertices. This is the [meshes](meshes) chapter's indexed triangle mesh, a vertex array and an index array, going to the card as one buffer each. Indices are 32-bit by default. Sixteen-bit indices halve the buffer and must be declared, because the bytes alone cannot say which they are: ```perl my $q16 = $gpu->index_buffer(pack 'S*', 0, 1, 2, 0, 2, 3); # 12 bytes $gpu->render_pass($sq, clear => [0, 0, 0, 1]) ->pipeline($pipe)->vertices($corners)->indices($q16, 'uint16')->draw_indexed(6)->end; ``` The same 12 bytes without the `'uint16'` read as three 32-bit indices, and the draw is refused at `end`: `Index 6 extends beyond limit 3. Did you bind the correct index buffer?` ```{index} single: WebGPU; error messages single: shader; compilation errors single: validation error ``` ## When something is wrong Every failure is a Perl exception carrying wgpu's own text, so `eval` traps it and `$@` explains it. A call that succeeds returns its object, which is what makes the chaining above work and `... ->end or die` behave. The exceptions arrive at three places, and knowing which saves time. **At `shader`: the WGSL does not compile.** The message points into the source by line and column: ```text shader compilation failed: Validation Error Caused by: In Device::create_shader_module Shader '' parsing error: expected `;`, found "return" ┌─ wgsl:11:5 │ 11 │ return o; │ ^^^^^^ expected `;` ``` The missing semicolon was at the end of `o.col = c` on line 10; the parser noticed on line 11. Line numbers count from the first line of the WGSL string, not of the Perl file. A type error names the entry point, here `fs` returning a `vec3` where it declared a `vec4`: ```text Shader validation error: Entry point fs at Fragment is invalid ``` **At `render_pipeline`: the shader does not fit the description.** An entry point name that is not in the shader, or a shader input no buffer supplies: ```text Unable to find entry point 'vertex_main' ``` ```text Location[1] Float32x3 interpolated as Some(Perspective) with sampling Some(Center) is not provided by the previous stage outputs ``` The second is what a `vertex_buffers` list with only `float32x2` in it produces for this shader: nothing feeds `@location(1)`. **At `end`: the recording was inconsistent.** Mistakes made while recording are collected and reported when the pass is submitted, so the exception comes from `end` even though the call at fault was earlier. The message names the command: | mistake | the message ends with | |---|---| | `draw` with no `vertices` call | `requires vertex buffer 0 to be set` | | `draw(6)` on a three-vertex buffer | `Vertex 6 extends beyond limit 3` | | pipeline `format` differs from the target | `the RenderPass uses textures with formats [Some(Bgra8UnormSrgb)] but the RenderPipeline ... uses attachments with formats [Some(Rgba8Unorm)]` | | a shader `var` never bound | `expects a BindGroup to be set at index 0` | The format row is the one every windowed program hits once: a pipeline built with the default `rgba8unorm`, drawn into a frame whose surface is `bgra8unorm-srgb`. ```{index} single: vertex layout; silent mismatch ``` ### The one that says nothing **Symptom: the triangle is the wrong shape, or its colours are wrong, and nothing raised an exception.** Cause: a vertex format with fewer components than the shader's input. WebGPU allows it deliberately and fills in the missing components, so the layout `['float32x2', 'float32x2']` for this shader's `vec2` and `vec3` inputs is accepted. Its computed stride is 16 bytes instead of 20, so every vertex after the first is read from the wrong place. On a 64 by 64 target the correct layout covered 1352 pixels with a blue-grey centre, `63 67 125`; the short one covered 1146 pixels with a green centre, `22 62 0`. The layout and the `pack` template describe the same bytes twice. When a picture is nearly right, compare the two before suspecting anything else. ## Next The triangle's corners were already in clip space. The [next chapter](gpu-3d-scene) feeds the card model coordinates and the matrices from [foundations](foundations), and brings back the depth buffer, culling and lighting of the software arc.