docs: restructure rainbow triangle guide to eliminate code duplication and assembly friction
This commit is contained in:
@@ -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 S2–S8, 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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user