docs: restructure rainbow triangle guide to eliminate code duplication and assembly friction

This commit is contained in:
2026-05-31 22:05:57 -05:00
parent 97922d3616
commit b557ea8a1e

View File

@@ -182,6 +182,34 @@ the event loop must still drain. `exiting()` ensures we have one last clean
opportunity to flush the queue and release GPU resources before the process
exits.
### The `State` Impl Skeleton
The `State` struct (defined fully in S3) carries all GPU resources. Its
implementation has three public methods. Here is the complete signature
overview so you can see the full shape before diving into the init chain:
```rust
impl State {
// S3: async constructor — builds the full GPU init chain (8 steps)
async fn new(window: Arc<Window>) -> Result<Self, String> {
// ... (see S3)
}
// S7: synchronous render loop — records commands and submits to GPU
fn render(&mut self) {
// ... (see S7)
}
// S8: reconfigure swapchain when window dimensions change
fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) {
// ... (see S8)
}
}
```
The next sections fill in each method. S3 covers `State::new()` in full.
S7 covers `render()`. S8 covers `resize()`.
## S3: Connecting to the GPU — The Init Chain
New concept: **5-layer GPU connection.** Each layer adds a capability:
@@ -503,7 +531,10 @@ runtime.
### The Complete Shader
Create `shader.wgsl` in your project root (at the same level as `main.rs`):
Create the file `src/shader.wgsl` in the same directory as `src/main.rs`, at the
crate source root. The `include_str!` macro in the master `State::new()` block
(S3, Step 6) expects the path `shader.wgsl` relative to the source file, so it
must live alongside `main.rs`:
```wgsl
struct VertexOutput {
@@ -612,18 +643,10 @@ RGB color to RGBA by setting alpha = 1.0 (fully opaque). The
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
### How the Shader Is Loaded (S3, Step 6)
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()),
}
);
```
The Rust side loads the shader file at compile time and feeds the source to wgpu.
This code lives in the master `State::new()` block (S3, Step 6):
- **`ShaderModuleDescriptor`** — has two fields: `label` (debug string, shown
in graphics debuggers and validation messages) and `source` (the shader
@@ -654,30 +677,21 @@ strides the attribute begins.
> 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
### The Vertex Struct (S3, Step 7)
```rust
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct Vertex {
position: [f32; 3],
color: [f32; 3],
}
```
The `Vertex` struct is defined alongside the `VERTICES` constant at the top of
`main.rs`, before `impl State`. This is the same struct used in the master
initialization block (S3). The key annotations are:
> **WHY: `#[repr(C)]` + bytemuck for GPU data layout**
>
> `#[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 an unsafe block.
### Vertex Data
### Vertex Data (S3, Step 7)
```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
];
```
The `VERTICES` constant is defined at the top of `main.rs` alongside the
`Vertex` struct. It is the same data used in the master initialization block
(S3, Step 7).
- **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
@@ -690,18 +704,10 @@ const VERTICES: &[Vertex] = &[
determines which face is "front" and which is "back" — critical for
[culling](concepts/GLOSSARY.md) and correct normal computation.
### Buffer Upload
### Buffer Upload (S3, Step 7)
```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,
}
);
```
This code lives in the master `State::new()` block (S3, Step 7). The
`create_buffer_init` method is the combined allocate-and-upload call.
- **`use wgpu::util::DeviceExt`** — imports the extension trait that adds
`create_buffer_init` to `Device`. Without this import, the method is not
@@ -731,29 +737,11 @@ 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
### Vertex Buffer Layout (S3, Step 8)
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,
},
],
};
```
in the vertex buffer into per-vertex attributes. This layout is defined inside
the master `State::new()` block (S3, Step 8):
- **`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
@@ -775,47 +763,11 @@ let vertex_buffer_layout = wgpu::VertexBufferLayout {
receive the position values as the color input, rendering a triangle with
gradient colors derived from position data.
### The Complete Render Pipeline Descriptor
### The Complete Render Pipeline Descriptor (S3, Step 8)
```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,
});
```
The full `device.create_render_pipeline()` call with all descriptor fields
lives in the master `State::new()` block (S3, Step 8). Below is the
field-by-field walkthrough explaining every parameter.
### Field-by-Field Walkthrough
@@ -1320,13 +1272,15 @@ The full source is the codeblocks in sections S2S8, assembled in order into
```
src/
├── main.rs # Sections S2, S3, S5 (structs), S7 (render), S8 (resize)
├── main.rs # Sections S2 (event loop), S3 (State + init chain), S7 (render), S8 (resize)
├── shader.wgsl # Section S4 (the complete WGSL shader)
```
- `main.rs` combines the winit event loop (S2), the init chain and `State`
struct (S3), the `Vertex` type and `VERTICES` constant (S5), the `render`
method (S7), and the `resize` method (S8).
- `main.rs` combines the winit event loop (S2), the complete `State` struct
definition and full `State::new()` implementation covering all 8 init steps
(S3), the `render` method (S7), and the `resize` method (S8). Sections S5
and S6 explain the vertex data and pipeline concepts but their code lives
inside the master `State::new()` block in S3.
- `shader.wgsl` is the single file from S4: vertex shader, fragment shader,
and the `VertexOutput` struct.