docs: append sections S4-S6 (shaders, vertex data, render pipeline)
This commit is contained in:
@@ -438,3 +438,450 @@ back pressure, smoothing out frame time spikes.
|
||||
|
||||
Steps 6 through 8 — shader module compilation, vertex buffer upload, and render
|
||||
pipeline assembly — will be explored in detail in the next sections.
|
||||
|
||||
## S4: Writing the Shaders
|
||||
|
||||
New concept: **shaders are GPU programs.** A [shader](concepts/GLOSSARY.md#shader)
|
||||
is a function or set of functions that runs on the GPU, compiled once at pipeline
|
||||
creation time, then executed thousands of times in parallel. Each invocation
|
||||
operates on different data but follows the identical instruction sequence. There
|
||||
is no heap allocation, no recursion, no I/O, and no shared mutable state. The
|
||||
GPU runs every invocation of a shader in lockstep: if one thread takes a
|
||||
different branch, the entire wavefront serializes both paths and discards the
|
||||
dead result. This is why you write shaders differently from CPU code — you
|
||||
optimize for parallelism and branchless arithmetic.
|
||||
|
||||
A [shader module](concepts/GLOSSARY.md#shader) can contain multiple entry points.
|
||||
For rendering, the two mandatory entry points are the [vertex shader](concepts/GLOSSARY.md#vertex-shader)
|
||||
and the [fragment shader](concepts/GLOSSARY.md#fragment-shader). The vertex
|
||||
shader runs once per [vertex](concepts/GLOSSARY.md#vertex). The fragment shader
|
||||
runs once per [fragment](concepts/GLOSSARY.md#fragment) — that is, once per pixel
|
||||
covered by the rasterized [primitive](concepts/GLOSSARY.md#primitive).
|
||||
|
||||
> **Key insight #1 — Interpolation is free hardware:** The vertex shader outputs
|
||||
> per-vertex colors at `@location(0)`. The [rasterizer](concepts/GLOSSARY.md#rasterizer)
|
||||
> automatically interpolates them across the triangle surface using
|
||||
> [barycentric coordinates](concepts/GLOSSARY.md#barycentric-coordinates). The
|
||||
> fragment shader just returns whatever it receives. The rainbow gradient is not
|
||||
> programmed — it is a consequence of the pipeline architecture. You supply
|
||||
> colors at three points; the hardware computes every color in between at zero
|
||||
> shader cost.
|
||||
|
||||
**Why WGSL:** WebGPU Shading Language ([WGSL](concepts/GLOSSARY.md#wgsl)) is
|
||||
the single source format. wgpu compiles it to the platform-native intermediate
|
||||
at runtime: SPIR-V for Vulkan, MSL for Metal, DXIL for DirectX. You write one
|
||||
shader file and wgpu produces the right binary for every backend.
|
||||
|
||||
**Why `include_str!("shader.wgsl")`:** This Rust macro embeds the file contents
|
||||
at compile time. The shader source becomes a string literal inside your binary.
|
||||
At runtime there is zero file I/O. No paths to resolve, no loading failures,
|
||||
no async reads. If the file is missing or malformed, the build fails, not the
|
||||
runtime.
|
||||
|
||||
### The Complete Shader
|
||||
|
||||
Create `shader.wgsl` in your project root (at the same level as `main.rs`):
|
||||
|
||||
```wgsl
|
||||
struct VertexOutput {
|
||||
@builtin(position) clip_position: vec4<f32>,
|
||||
@location(0) vertex_color: vec3<f32>,
|
||||
};
|
||||
|
||||
@vertex
|
||||
fn vs_main(
|
||||
@location(0) position: vec3<f32>,
|
||||
@location(1) color: vec3<f32>,
|
||||
) -> VertexOutput {
|
||||
var out: VertexOutput;
|
||||
out.clip_position = vec4<f32>(position, 1.0);
|
||||
out.vertex_color = color;
|
||||
return out;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
|
||||
return vec4<f32>(input.vertex_color, 1.0);
|
||||
}
|
||||
```
|
||||
|
||||
### Line-by-Line Walkthrough
|
||||
|
||||
**`struct VertexOutput { ... }`** — Defines the data flowing between the vertex
|
||||
shader and the fragment shader. This struct is not a Rust type and not a buffer
|
||||
layout — it is the output contract of the vertex shader that the rasterizer
|
||||
carries through to the fragment shader.
|
||||
|
||||
**`@builtin(position) clip_position: vec4<f32>`** — `@builtin(position)` is a
|
||||
reserved GPU output slot. Every vertex shader must produce a `vec4<f32>` at
|
||||
this slot. This value is the vertex position in [clip space](concepts/GLOSSARY.md#clip-space).
|
||||
The GPU uses it for perspective division (dividing x, y, z by w to produce
|
||||
[ndc](concepts/GLOSSARY.md#ndc)) and clipping. In our triangle, the w
|
||||
component is 1.0, so perspective division is the identity operation — our
|
||||
positions are already in the right space.
|
||||
|
||||
**`@location(0) vertex_color: vec3<f32>`** — `@location(0)` marks this field
|
||||
for interpolation. Any `@location(n)` output from the vertex shader that is
|
||||
not a builtin is automatically interpolated by the rasterizer using barycentric
|
||||
weights. At each vertex, the value is exact. Inside the triangle, it is the
|
||||
weighted blend of all three vertex values. The fragment shader receives a
|
||||
different `vertex_color` for every pixel, without any manual interpolation code.
|
||||
|
||||
> **Key insight #2 — THE LOCATIONS MUST MATCH:** `shader_location: 0` in
|
||||
> Rust's `VertexAttribute` MUST equal `@location(0)` in WGSL's parameter
|
||||
> annotation. If they differ, the shader reads from the wrong memory offset
|
||||
> and produces garbage. This is not a type error or a runtime panic — it is
|
||||
> silent data corruption. The GPU reads whatever bytes live at the mismatched
|
||||
> offset and interprets them as floats.
|
||||
|
||||
**`@vertex fn vs_main(...)`** — `@vertex` declares this function as the vertex
|
||||
shader entry point. The function is invoked once per vertex in the draw call.
|
||||
For our triangle with three vertices, `vs_main` runs exactly three times.
|
||||
|
||||
**`@location(0) position: vec3<f32>`** — This input parameter receives data
|
||||
from the vertex buffer mapped by `shader_location: 0`. In our Rust
|
||||
`VertexBufferLayout`, the first `VertexAttribute` reads 3 floats at offset 0
|
||||
and delivers them to the shader at location 0. This is the raw NDC position.
|
||||
|
||||
**`@location(1) color: vec3<f32>`** — The second vertex buffer attribute
|
||||
mapped to location 1. Reads 3 floats at the offset after the position
|
||||
(12 bytes into each vertex) — the per-vertex RGB color.
|
||||
|
||||
**`var out: VertexOutput;`** — Local variable declaration. WGSL requires
|
||||
explicit variable bindings. `var` creates a mutable local.
|
||||
|
||||
**`out.clip_position = vec4<f32>(position, 1.0);`** — Converts the `vec3`
|
||||
input into [homogeneous coordinates](concepts/GLOSSARY.md#homogeneous-coordinates)
|
||||
by appending w = 1.0. This promotes the position from 3D to clip space. With
|
||||
w = 1.0, perspective division (x/w, y/w, z/w) leaves the coordinates unchanged.
|
||||
If we were using perspective projection, the vertex shader would compute a
|
||||
nontrivial w value from the depth.
|
||||
|
||||
**`out.vertex_color = color;`** — Passes the input color through to the output.
|
||||
The rasterizer picks this field up, interpolates it across the triangle surface,
|
||||
and delivers the interpolated value to every fragment.
|
||||
|
||||
**`@fragment fn fs_main(input: VertexOutput)`** — `@fragment` declares the
|
||||
fragment shader entry point. `input` is the rasterizer's interpolated output
|
||||
from the vertex shader. Every `@location(n)` field in `VertexOutput` is now
|
||||
pre-blended. The `@builtin(position)` field is not interpolated — it is the
|
||||
original vertex position.
|
||||
|
||||
**`-> @location(0) vec4<f32>`** — The fragment shader must output at least one
|
||||
color value at `@location(0)`. This number must match the corresponding color
|
||||
target in the [render pipeline](concepts/GLOSSARY.md#pipeline) descriptor. The
|
||||
return type is `vec4<f32>` — RGBA with linear-space components.
|
||||
|
||||
**`return vec4<f32>(input.vertex_color, 1.0);`** — Promotes the interpolated
|
||||
RGB color to RGBA by setting alpha = 1.0 (fully opaque). The
|
||||
[rasterizer](concepts/GLOSSARY.md#rasterizer) interpolated `input.vertex_color`
|
||||
across the triangle; we just attach an alpha channel and return it. The output
|
||||
merge stage writes this color directly to the framebuffer.
|
||||
|
||||
### Rust Shader Module Creation
|
||||
|
||||
The Rust side loads the shader file at compile time and feeds the source to wgpu:
|
||||
|
||||
```rust
|
||||
let shader_module = device.create_shader_module(
|
||||
wgpu::ShaderModuleDescriptor {
|
||||
label: Some("Rainbow Triangle Shader"),
|
||||
source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()),
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
- **`ShaderModuleDescriptor`** — has two fields: `label` (debug string, shown
|
||||
in graphics debuggers and validation messages) and `source` (the shader
|
||||
text).
|
||||
- **`ShaderSource::Wgsl(...)`** — wraps the WGSL string. wgpu also accepts
|
||||
SPIR-V binary source via `ShaderSource::SpirV`, but WGSL is the native
|
||||
path.
|
||||
- **`device.create_shader_module()`** — takes the descriptor and parses +
|
||||
validates the shader. On Vulkan, wgpu translates WGSL to SPIR-V internally.
|
||||
If the shader has syntax errors, type mismatches, or unresolved entry points,
|
||||
this call returns an error.
|
||||
- **`&shader_module`** — the resulting handle is passed by reference into the
|
||||
render pipeline descriptor. The module remains valid for the lifetime of the
|
||||
pipeline.
|
||||
|
||||
## S5: Uploading Vertex Data to the GPU
|
||||
|
||||
New concept: **GPU memory isolation.** The GPU cannot read Rust heap or stack
|
||||
memory directly. Vertex data must be laid out as a flat byte array and uploaded
|
||||
into a dedicated GPU [[buffer slice]](concepts/GLOSSARY.md#buffer-slice). The
|
||||
pipeline configuration then describes how to interpret those bytes: how many
|
||||
bytes per vertex, what format each attribute has, and where in the vertex
|
||||
strides the attribute begins.
|
||||
|
||||
> **Key insight #3 — `create_buffer_init` is an extension trait:** The method
|
||||
> lives in `wgpu::util::DeviceExt`, not on `Device` directly. If you call
|
||||
> `device.create_buffer_init(...)` without importing the trait, the compiler
|
||||
> reports "method not found." This is a Rust trait-discovery issue, not a wgpu
|
||||
> API issue. Add `use wgpu::util::DeviceExt;` to bring the method into scope.
|
||||
|
||||
### The Vertex Struct
|
||||
|
||||
```rust
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
struct Vertex {
|
||||
position: [f32; 3],
|
||||
color: [f32; 3],
|
||||
}
|
||||
```
|
||||
|
||||
- **`#[repr(C)]`** — Forces the Rust compiler to lay out the struct fields in
|
||||
declaration order with no padding reordering. Without this, Rust is free to
|
||||
reorder fields for optimal alignment, which would break the byte layout the
|
||||
shader expects.
|
||||
- **`bytemuck::Pod`** — "Plain Old Data." Guarantees the struct has no padding
|
||||
holes, no destructors, and a trivial memory representation. wgpu requires
|
||||
all vertex types to be Pod so they can be safely transmuted to bytes.
|
||||
- **`bytemuck::Zeroable`** — Guarantees that initializing the struct's memory
|
||||
to all-zero bytes produces a valid instance. Required because `Pod` alone
|
||||
does not guarantee zero is a valid discriminant for enums or optional types.
|
||||
Combined with Pod, it enables `bytemuck::cast_slice` to convert between
|
||||
`&[Vertex]` and `&[u8]` without a `unsafe` block.
|
||||
|
||||
### Vertex Data
|
||||
|
||||
```rust
|
||||
const VERTICES: &[Vertex] = &[
|
||||
Vertex { position: [-0.5, -0.5, 0.0], color: [1.0, 0.0, 0.0] }, // red
|
||||
Vertex { position: [ 0.5, -0.5, 0.0], color: [0.0, 0.0, 1.0] }, // blue
|
||||
Vertex { position: [ 0.0, 0.5, 0.0], color: [0.0, 1.0, 0.0] }, // green
|
||||
];
|
||||
```
|
||||
|
||||
- **Positions are in NDC:** The [normalized device coordinates](concepts/GLOSSARY.md#ndc)
|
||||
range from -1.0 (left/bottom) to +1.0 (right/top). Our triangle spans the
|
||||
bottom half of the screen: the bottom-left corner at (-0.5, -0.5), the
|
||||
bottom-right at (0.5, -0.5), and the top center at (0.0, 0.5). This
|
||||
produces an upright, centered triangle.
|
||||
- **CCW winding order:** The vertices are listed counter-clockwise:
|
||||
red → blue → green. In a standard right-handed coordinate system, connecting
|
||||
vertices in this sequence traces the triangle counter-clockwise. This
|
||||
determines which face is "front" and which is "back" — critical for
|
||||
[culling](concepts/GLOSSARY.md#rasterizer) and correct normal computation.
|
||||
|
||||
### Buffer Upload
|
||||
|
||||
```rust
|
||||
use wgpu::util::DeviceExt;
|
||||
let vertex_buffer = device.create_buffer_init(
|
||||
&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("Vertex Buffer"),
|
||||
contents: bytemuck::cast_slice(VERTICES),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
- **`use wgpu::util::DeviceExt`** — imports the extension trait that adds
|
||||
`create_buffer_init` to `Device`. Without this import, the method is not
|
||||
visible.
|
||||
- **`device.create_buffer_init(...)`** — combined allocate-and-upload. It
|
||||
creates a GPU buffer, allocates system memory, copies the `contents` slice
|
||||
into staging storage, and issues a synchronous copy to GPU memory. This is a
|
||||
convenience wrapper around `create_buffer` + `queue.write_buffer`.
|
||||
- **`bytemuck::cast_slice(VERTICES)`** — converts `&[Vertex; 3]` to `&[u8]`
|
||||
by reinterpreting the same memory at a byte level. The GPU receives 72 bytes:
|
||||
three vertices × 24 bytes per vertex (6 × `f32` = 6 × 4 bytes). No copy, no
|
||||
serialization — just a pointer reinterpretation.
|
||||
- **`BufferUsages::VERTEX`** — declares this buffer will be bound as a vertex
|
||||
buffer in the pipeline. wgpu's validation layer will reject any attempt to
|
||||
use this buffer for staging, uniform, or storage access. Usage bits
|
||||
are chosen at creation and cannot be changed.
|
||||
|
||||
## S6: Compiling the Render Pipeline
|
||||
|
||||
New concept: **the render pipeline is a compiled GPU configuration.** A
|
||||
[render pipeline](concepts/GLOSSARY.md#pipeline) bundles every decision the GPU
|
||||
needs to execute a draw: which shaders to run, how to interpret vertex buffer
|
||||
bytes, what [topology](concepts/GLOSSARY.md#topology) to use, whether to cull
|
||||
back faces, what blend mode to apply, and where to write the output. Pipeline
|
||||
creation is not a simple struct allocation — it compiles these choices into a
|
||||
GPU-executable configuration. Errors in any field are caught at creation time,
|
||||
not at draw time. This validation-upfront model is what makes pipelines expensive
|
||||
to create but cheap to execute.
|
||||
|
||||
### Vertex Buffer Layout
|
||||
|
||||
Before the pipeline descriptor, you must tell wgpu how to parse the byte stream
|
||||
in the vertex buffer into per-vertex attributes:
|
||||
|
||||
```rust
|
||||
let vertex_buffer_layout = wgpu::VertexBufferLayout {
|
||||
array_stride: std::mem::size_of::<Vertex>() as u64,
|
||||
step_mode: wgpu::VertexStepMode::Vertex,
|
||||
attributes: &[
|
||||
wgpu::VertexAttribute {
|
||||
offset: 0,
|
||||
format: wgpu::VertexFormat::F32x3,
|
||||
shader_location: 0,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: std::mem::size_of::<[f32; 3]>() as u64,
|
||||
format: wgpu::VertexFormat::F32x3,
|
||||
shader_location: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
- **`array_stride: 24`** — `size_of::<Vertex>()` = 24 bytes (6 × `f32` × 4 bytes).
|
||||
This is the byte distance from one vertex to the next in the buffer. The GPU
|
||||
uses this to step through the buffer: vertex 0 starts at byte 0, vertex 1
|
||||
at byte 24, vertex 2 at byte 48.
|
||||
- **`step_mode: Vertex`** — advance the buffer by one stride for every vertex
|
||||
the vertex shader processes. The other option is `Instance`, which advances
|
||||
per draw instance in instanced rendering. For a single triangle, `Vertex` is
|
||||
correct: each of the three vertices has its own position and color.
|
||||
- **First attribute — `shader_location: 0`**: reads 3 floats (`F32x3`) at byte
|
||||
offset 0 of each vertex. These 3 floats map to the
|
||||
[shader location](concepts/GLOSSARY.md#shader-location) `@location(0)` in the
|
||||
vertex shader — the `position` parameter. The GPU delivers `[x, y, z]` to
|
||||
that function argument.
|
||||
- **Second attribute — `shader_location: 1`**: reads 3 floats at offset 12
|
||||
(`size_of::<[f32; 3]>()` = 3 × 4 = 12). Skips past the position array to
|
||||
the color array inside each vertex. Maps to `@location(1)` in the shader —
|
||||
the `color` parameter. If the offset were 0 instead of 12, the shader would
|
||||
receive the position values as the color input, rendering a triangle with
|
||||
gradient colors derived from position data.
|
||||
|
||||
### The Complete Render Pipeline Descriptor
|
||||
|
||||
```rust
|
||||
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("Triangle Pipeline"),
|
||||
layout: None,
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader_module,
|
||||
entry_point: Some("vs_main"),
|
||||
buffers: &[vertex_buffer_layout],
|
||||
compilation_options: Default::default(),
|
||||
},
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
strip_index_format: None,
|
||||
front_face: wgpu::FrontFace::Ccw,
|
||||
cull_mode: Some(wgpu::Face::Back),
|
||||
unclipped_depth: false,
|
||||
polygon_mode: wgpu::PolygonMode::Fill,
|
||||
conservative: false,
|
||||
},
|
||||
depth_stencil: None,
|
||||
multisample: wgpu::MultisampleState {
|
||||
count: 1,
|
||||
mask: !0,
|
||||
alpha_to_coverage_enabled: false,
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader_module,
|
||||
entry_point: Some("fs_main"),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: config.format,
|
||||
blend: None,
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
compilation_options: Default::default(),
|
||||
}),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
});
|
||||
```
|
||||
|
||||
### Field-by-Field Walkthrough
|
||||
|
||||
**`RenderPipelineDescriptor` has 9 fields.** Every field must be present. The
|
||||
structure does not use `..Default::default()` at the descriptor level — each
|
||||
field is filled explicitly.
|
||||
|
||||
**`label: Some("Triangle Pipeline")`** — Debug string. Shown in GPU profilers
|
||||
(RenderDoc, Nvidia Nsight) and wgpu validation error messages. Omitting it
|
||||
produces anonymous pipelines that are impossible to trace during debugging.
|
||||
|
||||
**`layout: None`** — Derives the pipeline layout from the shader module
|
||||
automatically. When no push constants or bind groups are used, `None` tells wgpu
|
||||
to infer the layout. If you later add `@group(n)` bindings to your shader, you
|
||||
must provide a `RenderPipelineLayout` created with `device.create_render_pipeline_layout()`.
|
||||
|
||||
**`vertex` — [`VertexState`](concepts/GLOSSARY.md#vertex-shader) (4 fields):**
|
||||
- **`module: &shader_module`** — references the compiled shader module from S4.
|
||||
- **`entry_point: Some("vs_main")`** — selects which function in the module is
|
||||
the vertex shader entry point. Must match the `@vertex fn vs_main(...)`
|
||||
declaration exactly.
|
||||
- **`buffers: &[vertex_buffer_layout]`** — array of vertex buffer layouts.
|
||||
Multiple layouts are used rarely (multi-mesh, GPU instancing with separate
|
||||
instance buffers). For a single vertex buffer, one layout suffices.
|
||||
- **`compilation_options: Default::default()`** — shader compilation backend
|
||||
hints. Default uses the backend's standard flags for optimization and SPIR-V
|
||||
version.
|
||||
|
||||
**`primitive` — [`PrimitiveState`](concepts/GLOSSARY.md#primitive) (7 fields):**
|
||||
- **`topology: TriangleList`** — every 3 consecutive vertices form one
|
||||
triangle. For 3 vertices, this produces exactly 1 triangle. If we had 6
|
||||
vertices, it would produce 2 independent triangles.
|
||||
- **`strip_index_format: None`** — only set for `TriangleStrip` or `LineStrip`
|
||||
topologies when using restart indices. Not applicable to `TriangleList`.
|
||||
- **`front_face: Ccw`** — counter-clockwise winding defines the front face of
|
||||
a triangle. Combined with `cull_mode`, this determines which triangles are
|
||||
visible. Because our vertices are listed CCW in S5, triangles drawn in that
|
||||
order face toward the viewer.
|
||||
- **`cull_mode: Some(wgpu::Face::Back)`** — discard triangles whose winding
|
||||
indicates a back face. For a single triangle viewed from the front, this is
|
||||
harmless but establishes correct culling for 3D geometry where back faces
|
||||
are guaranteed not to be visible.
|
||||
- **`unclipped_depth: false`** — depth values outside [0.0, 1.0] are clipped
|
||||
(the standard behavior). `true` allows depth values beyond the normal range
|
||||
to pass through — used for specific depth-testing tricks.
|
||||
- **`polygon_mode: Fill`** — render the full interior of the triangle. Other
|
||||
options are `Line` (wireframe edges) and `Point` (vertex points only).
|
||||
- **`conservative: false`** — the rasterizer fragments only pixels provably
|
||||
inside the triangle. `true` fragments every pixel that *might* intersect the
|
||||
triangle — used for conservative rasterization (shadow volumes, occlusion
|
||||
queries).
|
||||
|
||||
**`depth_stencil: None`** — No depth buffer or stencil buffer. Without depth
|
||||
testing, triangles are drawn in submission order: later draws overwrite earlier
|
||||
draws at the same pixel. For a single triangle this is not a concern.
|
||||
|
||||
**`multisample` — [`MultisampleState`](concepts/GLOSSARY.md#fragment) (3 fields):**
|
||||
- **`count: 1`** — no multisampling. Each pixel produces one fragment. Higher
|
||||
values (2, 4, 8) activate MSAA, sampling multiple points per pixel and
|
||||
reducing aliasing at the cost of framebuffer bandwidth.
|
||||
- **`mask: !0`** — all sample bits are enabled. This mask allows you to
|
||||
selectively disable individual MSAA samples (advanced use case).
|
||||
- **`alpha_to_coverage_enabled: false`** — do not use the alpha channel of the
|
||||
fragment color as a coverage mask. Enabled for transparent edge antialiasing
|
||||
(e.g., font rendering).
|
||||
|
||||
**`fragment` — [`FragmentState`](concepts/GLOSSARY.md#fragment-shader) (4 fields):**
|
||||
- **`module: &shader_module`** — same shader module as the vertex shader.
|
||||
- **`entry_point: Some("fs_main")`** — selects the fragment shader entry point.
|
||||
Must match `@fragment fn fs_main(...)` in the WGSL.
|
||||
- **`targets`** — array of color target states, one per render pass output
|
||||
attachment. `&[Some(...)]` means one color target present. `None` at this
|
||||
index would mean a render pass with no color output (e.g., depth-only pass).
|
||||
- **`ColorTargetState` has exactly 3 fields** (no `view_formats` field):
|
||||
- **`format: config.format`** — MUST match the surface format from
|
||||
`SurfaceConfiguration`. The pipeline writes in this format; the surface
|
||||
reads in this format. A mismatch at render time produces an error. If
|
||||
you change the surface format, you must recreate the pipeline.
|
||||
- **`blend: None`** — disables blending. Without blending, every fragment
|
||||
color replaces the existing framebuffer pixel (`REPLACE` mode). With
|
||||
blending, new and existing colors are combined according to a blend
|
||||
equation (useful for transparency).
|
||||
- **`write_mask: ColorWrites::ALL`** — write all four RGBA channels.
|
||||
You can mask out individual channels (e.g., write only R and G) if you
|
||||
need to preserve certain framebuffer channels across draw calls.
|
||||
- **`compilation_options: Default::default()`** — fragment shader compilation
|
||||
flags, same as the vertex compilation options above.
|
||||
|
||||
**`multiview_mask: None`** — no multiview rendering. Multiview is for
|
||||
stereoscopic (VR) or multi-viewport single-pass rendering. Not used here.
|
||||
|
||||
**`cache: None`** — no pipeline cache. A pipeline cache stores compiled shader
|
||||
binaries to speed up subsequent pipeline creation. Useful when creating many
|
||||
pipelines dynamically; for a single pipeline, caching has no practical benefit.
|
||||
|
||||
Reference in New Issue
Block a user