# Building a Rainbow Triangle ## S1: What We're Building We're creating a window containing a single triangle with smoothly blended colors: Red at the bottom-left corner, blue at the bottom-right corner, and green at the top vertex. The gradient between each pair of vertices is not computed by you — it is interpolated automatically by the GPU rasterizer in hardware. You provide three vertices, each carrying a position and a color. The rasterizer determines every pixel covered by the triangle and computes the color for that pixel by blending the three vertex colors proportionally to their distance. The result is a smooth rainbow gradient across a single primitive. We do not need a texture, a colormap, or a fragment shader with any branching — just three colored vertices and the default linear interpolation the [rasterizer](concepts/GLOSSARY.md#rasterizer) applies to every [interpolated value](concepts/GLOSSARY.md#interpolation). If you haven't read the [concept overview](concepts/graphics-pipeline.md), do so now. [Coordinate systems](concepts/coordinate-systems.md) explains how the GPU positions geometry. [Shader basics](concepts/shader-basics.md) covers the GPU programs that drive rendering. ## S2: The winit Application and Event Loop New concept: **event-driven windowing.** winit is the bridge between your Rust code and your platform's display server. Think of it like `epoll`, `kqueue`, or IOCP (Windows) but for windows, input, and display lifecycle events instead of file descriptors. GPU initialization (adapter and device queries) is async, while the winit event loop runs synchronously. We bridge these two execution models once at startup using `pollster::block_on`. ### Architecture Overview - **`main()` is synchronous** — the entry point initializes logging, creates the event loop, and calls `run_app()`. The entire program runs on a single thread. No async runtime is needed for the main loop. - **`pollster::block_on()` in `resumed()`** — wgpu initialization (adapter and device queries) is async, but winit's `resumed()` handler is synchronous. We bridge the two execution models exactly once at startup using `pollster`, a minimal single-threaded async executor. This initial GPU setup takes ~50ms of wall time. - **`Arc`** — shared reference count to the window, needed because both winit event handlers and wgpu [surface](concepts/GLOSSARY.md#surface) state must hold a reference to the same window object across the event loop boundary. - **`ControlFlow::Poll`** — continuous redraw mode. winit fires `RedrawRequested` as fast as the display server allows the window to be presented, giving us a tight render loop without a separate timer or explicit vsync setup. The display [present mode](concepts/GLOSSARY.md#present-mode) controls the actual vsync behavior. ### Dependencies Add these to your `Cargo.toml`: ```toml wgpu = "29" winit = "0.30" pollster = "0.4" bytemuck = { version = "1", features = ["derive"] } log = "0.4" ``` - `wgpu` — the GPU abstraction layer. Manages device lifecycles, shaders, buffers, pipelines, and command encoding. - `winit` — cross-platform window creation and event dispatch. Owns the display server connection. - `pollster` — minimal single-threaded async executor. Bridges wgpu's async GPU queries with synchronous winit callbacks. Polls futures to completion (~50ms) during initial GPU setup, then returns. - `bytemuck` — zero-copy casting between Rust structs and byte slices. Required for uploading vertex data to GPU buffers without manual serialization. - `log` / `simple_logger` — structured logging. wgpu and winit emit diagnostic messages via `log` when misconfigurations or driver issues are detected. ### Cross-Platform Window Creation winit abstracts all platform-specific windowing details. The same code works across every supported platform: - **Windows:** Native Win32/composition backend. - **macOS:** Cocoa backend. - **Linux:** X11 or Wayland, depending on your active session. No platform-specific conditionals or feature flags are needed — winit handles everything automatically. ### Complete Code ```rust use pollster::block_on; use std::sync::Arc; use winit::application::ApplicationHandler; use winit::dpi::LogicalSize; use winit::event::WindowEvent; use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop}; use winit::window::{Window, WindowId}; fn main() { simple_logger::init_with_level(log::Level::Debug).unwrap(); let event_loop = EventLoop::new().unwrap(); event_loop.run_app(&mut App { window: None, state: None, }) .unwrap(); } struct App { window: Option>, state: Option, } impl ApplicationHandler<()> for App { fn resumed(&mut self, event_loop_ctl: &ActiveEventLoop) { let window = Arc::new( event_loop_ctl .create_window( Window::default_attributes() .with_inner_size(LogicalSize::new(800.0, 600.0)) .with_title("Rainbow Triangle"), ) .unwrap(), ); event_loop_ctl.set_control_flow(ControlFlow::Poll); self.window = Some(window.clone()); // Graceful GPU initialization: if adapter/device creation fails, // log the error and close the window instead of panicking. match block_on(State::new(window.clone())) { Ok(state) => self.state = Some(state), Err(e) => { log::error!("Failed to initialize GPU: {}", e); window.close(); } } } fn window_event( &mut self, event_loop_ctl: &ActiveEventLoop, _window_id: WindowId, event: WindowEvent, ) { let Some(state) = self.state.as_mut() else { return }; let Some(window) = self.window.as_ref() else { return }; match event { WindowEvent::Resized(size) => state.resize(size), WindowEvent::CloseRequested { .. } => event_loop_ctl.exit(), WindowEvent::RedrawRequested => { state.render(); window.request_redraw(); } _ => {} } } fn exiting(&mut self, event_loop_ctl: &ActiveEventLoop) { event_loop_ctl.exit(); } } ``` > **WHY: `pollster::block_on` for async GPU init** > > wgpu's `request_adapter` and `request_device` query the driver over async > platform-specific entry points (Metal on macOS, DX12/Vulkan on Windows, > Vulkan on Linux). These futures must be polled by a runtime executor. We use > `pollster`, a minimal single-threaded async executor, to bridge wgpu's async > GPU initialization with winit's synchronous `resumed()` callback. > `pollster::block_on` polls the future to completion (~50ms) on the current > thread, then returns the result — no background runtime, no spawn overhead, > no cross-thread communication. > **WHY: `ControlFlow::Poll` for the render loop** > > winit supports `ControlFlow::Poll` (continuous redraw) and `ControlFlow::Wait` (idle until next event). A graphics application needs a steady render loop. `Poll` tells winit to keep firing `RedrawRequested` events. We re-queue ourselves inside the handler via `window.request_redraw()`, matching the wgpu swapchain presentation rhythm. **Why `request_redraw()`:** After presenting a frame to the display, we ask winit to schedule the next `RedrawRequested` frame. This creates an explicit render loop: render → present → request redraw → render → repeat. The rate is governed by the [swapchain](concepts/GLOSSARY.md#swapchain) [present mode](concepts/GLOSSARY.md#present-mode). **Why `exiting()`:** This is the final lifecycle signal before the process terminates. On some display servers, `CloseRequested` fires on the window but 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) -> Result { // ... (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) { // ... (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: ```text Instance │ ├──> Surface (winit window → GPU surface) │ ├──> Adapter (select GPU: integrated vs discrete) │ ├──> Device + Queue (GPU connection + command submission) │ └──> SurfaceConfiguration (swapchain: format, size, present mode) ``` 1. **[Instance](concepts/GLOSSARY.md#instance)** — opens a connection to the graphics driver. On Vulkan this loads the Vulkan loader and registers instance-level extensions. On WebGL this picks the browser GPU context. 2. **[Surface](concepts/GLOSSARY.md#surface)** — binds the instance to a specific window's swapchain. The surface is the wgpu representation of the window's display buffer. 3. **[Adapter](concepts/GLOSSARY.md#adapter)** — selects the physical GPU hardware. An adapter wraps the actual driver + silicon pair (e.g., Mesa RADV on AMD, NVIDIA driver on NVIDIA silicon). 4. **[Device](concepts/GLOSSARY.md#device) + [Queue](concepts/GLOSSARY.md#queue)** — the device owns all GPU resources (buffers, textures, shaders, pipelines). The queue is the submission channel: you encode work into command buffers and submit them to the queue. 5. **[SurfaceConfiguration](concepts/GLOSSARY.md#surface-configuration)** — allocates the swapchain [framebuffers](concepts/GLOSSARY.md#framebuffer) for this window at a specific resolution and pixel format. > **Cross-platform GPU adapter selection:** wgpu abstracts away the platform > graphics API. It automatically picks the best backend for your operating > system: > - **Windows:** DirectX 12 (primary), DirectX 11 (fallback), Vulkan > - **macOS:** Metal (primary and only supported backend) > - **Linux:** Vulkan (primary), OpenGL (fallback), DX12 (under Wine) > > `PowerPreference::HighPerformance` picks the discrete GPU on laptops > regardless of which backend wgpu selects. ### The State Struct ```rust struct State { surface: wgpu::Surface<'static>, device: wgpu::Device, queue: wgpu::Queue, config: wgpu::SurfaceConfiguration, window: Arc, pipeline: wgpu::RenderPipeline, vertex_buffer: wgpu::Buffer, } ``` - **`surface`** — connects to the window's display buffer. The `'static` lifetime is safe because `App` owns the window and lives for the entire lifetime of the process. The surface mediates all [swapchain](concepts/GLOSSARY.md#swapchain) operations. - **`device`** — owns all GPU resources. Every buffer, texture, shader module, and pipeline created in this guide is a child of the device. When the device is dropped, all its children are freed. - **`queue`** — the command submission channel. You encode a frame's worth of work into a [command buffer](concepts/GLOSSARY.md#command-buffer), then submit that buffer to the queue. The queue pushes work to the GPU hardware. - **`config`** — holds the surface's current width, height, pixel format, and [present mode](concepts/GLOSSARY.md#present-mode). When the window is resized, we reconfigure the surface with updated dimensions. - **`window`** — shared reference to the winit window. Stored as an `Arc` so the `resize()` method and the `CurrentSurfaceTexture::Outdated` recovery handler can access the window's current dimensions. When the surface becomes outdated (e.g., after a compositor restart or display hotplug), recovery requires reconfiguring the swapchain with the window's live size — and that requires holding a reference to the window itself. - **`pipeline`** — the compiled [render pipeline](concepts/GLOSSARY.md#pipeline-render). A render pipeline is an immutable configuration combining a shader, a vertex buffer layout, a primitive topology, and a color target setup. Switching pipelines mid-frame is expensive; most applications use a few pipelines and change them between draw calls. - **`vertex_buffer`** — GPU memory holding our vertex data. The GPU reads position and color data directly from this buffer during the vertex shader stage. ### Complete `State::new()` Implementation ```rust use wgpu::Surface; // --- Vertex type and data --- #[repr(C)] #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] struct Vertex { position: [f32; 3], color: [f32; 3], } 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 ]; impl State { async fn new(window: Arc) -> Result { // Step 1: Instance — connection to the graphics driver let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle()); // Step 2: Surface — binds our window to the GPU's swapchain let surface = instance .create_surface(window) .map_err(|e| format!("Failed to create surface: {:?}", e))?; // Step 3: Adapter — selects the physical GPU let adapter = instance .request_adapter(&wgpu::RequestAdapterOptions { power_preference: wgpu::PowerPreference::HighPerformance, force_fallback_adapter: false, compatible_surface: None, }) .await .ok_or("No GPU adapter found. Ensure your graphics drivers (Vulkan/Metal/DirectX) are installed.")?; // Step 4: Device + Queue — resource owner + command submission let (device, queue) = adapter .request_device(&wgpu::DeviceDescriptor::default(), None) .await .map_err(|e| format!("Failed to request device: {:?}", e))?; // Step 5: SurfaceConfiguration — allocates swapchain framebuffers let size = window.inner_size(); let surface_caps = surface.get_capabilities(&adapter); let format = surface_caps.formats.iter() .find(|f| f.is_srgb()) .copied() .unwrap_or(surface_caps.formats[0]); let config = wgpu::SurfaceConfiguration { usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, format, width: size.width.max(1), height: size.height.max(1), present_mode: wgpu::PresentMode::Mailbox, desired_maximum_frame_latency: 2, alpha_mode: surface_caps.alpha_modes[0], view_formats: vec![format.add_srgb_suffix()], }; surface.configure(&device, &config); // Step 6: Compile the shader module let shader_module = device.create_shader_module( wgpu::ShaderModuleDescriptor { label: Some("Rainbow Triangle Shader"), source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()), } ); // Step 7: Upload vertex data to GPU memory 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, } ); // Step 8: Create the render pipeline let vertex_buffer_layout = wgpu::VertexBufferLayout { array_stride: std::mem::size_of::() as u64, step_mode: wgpu::VertexStepMode::Vertex, attributes: &[ wgpu::VertexAttribute { offset: 0, format: wgpu::VertexFormat::Float32x3, shader_location: 0, }, wgpu::VertexAttribute { offset: std::mem::size_of::<[f32; 3]>() as u64, format: wgpu::VertexFormat::Float32x3, shader_location: 1, }, ], }; 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: &[Some(&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: Some(wgpu::BlendState::REPLACE), write_mask: wgpu::ColorWrites::ALL, })], compilation_options: Default::default(), }), multiview_mask: None, cache: None, }); Ok(Self { surface, device, queue, config, window: Arc::clone(&window), pipeline, vertex_buffer, }) } } ``` ### Init Steps Explained **Step 1 — Instance:** `Instance::new()` opens a connection to the graphics driver on the current platform. wgpu automatically selects the best backend: Metal on macOS, DirectX 12 (or DX11/Vulkan) on Windows, and Vulkan (or OpenGL) on Linux. The instance is the foundational wgpu object — every other wgpu operation requires it. **Step 2 — Surface:** `instance.create_surface(window)` binds the wgpu instance to the winit `Window`. This tells the GPU: "the pixels of *this* window will be the output of my rendering." The surface must match the window platform type exactly (Windows, macOS, X11, Wayland, etc.), and wgpu handles this mapping automatically through winit. **Step 3 — Adapter:** `request_adapter()` queries available GPUs and returns the best match for the given options. With `PowerPreference::HighPerformance`, wgpu prefers a discrete GPU over an integrated one on hybrid systems (e.g., NVIDIA + Intel Optimus on Windows, AMD Radeon + Intel Iris on macOS/Linux). This preference is cross-platform: it picks the high-performance GPU regardless of backend (Metal, DX12, or Vulkan). The `compatible_surface: None` path works because our `Instance` was created without a display handle; adapter selection remains correct across all backends. **Step 4 — Device + Queue:** `request_device()` allocates the logical GPU resource manager and its submission queue. The device tracks all GPU memory and validates API calls. The queue is the submission endpoint — every rendered frame becomes a [command buffer](concepts/GLOSSARY.md#command-buffer) that is submitted to this queue. This maps to platform-native concepts: `ID3D12Device` + `ID3D12CommandQueue` on DX12, `MTLDevice` + `MTLCommandQueue` on Metal, and `VkDevice` + `VkQueue` on Vulkan. > **Key insight — Validation layers catch GPU errors at runtime:** wgpu ships > with built-in validation layers that inspect your API calls for common > mistakes: incorrect buffer bindings, mismatched pipeline state, out-of-bounds > buffer slices, and resource lifecycle violations. These layers run > automatically during development and surface errors as log messages or > panics, saving hours of debugging silent GPU corruption. The tradeoff: > validation adds measurable overhead to every frame. In release builds, > disable validation by omitting `InstanceFlags::VALIDATION` when creating the > `Instance`, or set the `WGPU_VALIDATION=0` environment variable. **Step 5 — SurfaceConfiguration:** This allocates the [swapchain](concepts/GLOSSARY.md#swapchain) [framebuffers](concepts/GLOSSARY.md#framebuffer). We negotiate the pixel format with the driver (preferring an [sRGB](concepts/GLOSSARY.md) format for correct color display), pick the window dimensions (clamped to at least 1x1 to allow minimize-and-restore on some platforms), and select the [present mode](concepts/GLOSSARY.md#present-mode). `PresentMode::Mailbox` is a triple-buffered present mode that provides consistent 60fps without tearing on most platforms. `desired_maximum_frame_latency: 2` tells the swapchain to keep two frames of 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 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 { @builtin(position) clip_position: vec4, @location(0) vertex_color: vec3, }; @vertex fn vs_main( @location(0) position: vec3, @location(1) color: vec3, ) -> VertexOutput { var out: VertexOutput; out.clip_position = vec4(position, 1.0); out.vertex_color = color; return out; } @fragment fn fs_main(input: VertexOutput) -> @location(0) vec4 { return vec4(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`** — `@builtin(position)` is a reserved GPU output slot. Every vertex shader must produce a `vec4` 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`** — `@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. > **WHY: `@location` must match between Rust and WGSL** > > `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`** — 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`** — 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(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 with barycentric weights. > **Key insight — TWO `@builtin(position)` builtins, zero connection:** > Vertex `@builtin(position)` and fragment `@builtin(position)` are two > completely separate builtins that happen to share the same name. The vertex > shader outputs clip-space coordinates into `@builtin(position)` for the > rasterizer to perform perspective division and viewport transform. The > fragment shader receives an entirely different `@builtin(position)` injected > by the fragment stage, providing framebuffer pixel coordinates: `x`/`y` are > the pixel center within the viewport, `z` is the depth value (typically > [0, 1]), and `w` is the interpolated reciprocal of the vertex clip-space > w-coordinate (1/w). The vertex shader's position output is NOT passed to the > fragment shader's position input. They are independent builtins from > different pipeline stages. If you need to pass data from vertex to fragment > with interpolation, use `@location(N)` on regular struct fields — which is > exactly what `vertex_color` does in our shader. **`-> @location(0) vec4`** — 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-render) descriptor. The return type is `vec4` — RGBA with linear-space components. **`return vec4(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. ### How the Shader Is Loaded (S3, Step 6) 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 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 (S3, Step 7) 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 (S3, Step 7) 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 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) and correct normal computation. ### Buffer Upload (S3, Step 7) 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 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-render) 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 (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. This layout is defined inside the master `State::new()` block (S3, Step 8): - **`array_stride: 24`** — `size_of::()` = 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 (`Float32x3`) 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 (S3, Step 8) 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 **`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: &[Some(&vertex_buffer_layout)]`** — array of optional vertex buffer layouts. Each layout is wrapped in `Some` to indicate it is present. 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: Some(wgpu::BlendState::REPLACE)`** — explicitly replaces every fragment color with the new output. `None` would default to this behavior, but we make it explicit for clarity. With a custom blend state, new and existing colors can be 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. ## S7: The Render Loop — Recording and Submitting Commands New concept: **command buffers are scripts, not function calls.** You cannot call GPU operations directly from CPU code. Instead, you record commands into a [command buffer](concepts/GLOSSARY.md#command-buffer) — a script that the GPU queue executes asynchronously. Think of it like building an assembly listing: each recording method appends an instruction. When the script is complete, you submit it atomically to the [queue](concepts/GLOSSARY.md#queue). The GPU executes all instructions in parallel, in whatever order it determines is optimal. There is no `.await` on a draw call. The CPU returns immediately after submission and continues the next frame while the GPU works in the background. > **Key insight #4 — Command buffers are scripts, not function calls:** > `create_command_encoder()` opens a recording session. `begin_render_pass()` > starts a scoped drawing block. `render_pass.draw()` appends a draw command. > `encoder.finish()` seals the script. `queue.submit()` dispatches it. The GPU > executes it later, in parallel. There is no `.await` on a draw call. ### Render Loop Cycle ``` [RedrawRequested event] │ ▼ get_current_texture() → [Success?] → Yes → record commands │ │ └── No (Timeout/Occluded) → skip frame │ ▼ device.poll() → encoder.begin_render_pass() → draw() → submit() → present() ``` ### The `render(&mut self)` Method Signature ```rust fn render(&mut self) { // ... } ``` This is a **fully synchronous** method. It runs on the winit event loop thread (triggered by `RedrawRequested`), has no `async` keyword, no `.await`, and requires no async runtime. All wgpu recording and submission operations are synchronous and fast — they only encode instructions and push them to the queue; they do not wait for GPU completion. ### Acquiring a Back Buffer from the Swapchain ```rust let frame = self.surface.get_current_texture(); ``` `get_current_texture()` is how you acquire a back buffer from the [swapchain](concepts/GLOSSARY.md#swapchain). This is the framebuffer you render into for this frame. In a triple-buffered swapchain (`PresentMode::Mailbox`), there are up to two spare back buffers waiting for you. `get_current_texture()` hands you the next available one. In wgpu 29, this method returns `CurrentSurfaceTexture`, a standalone enum with 7 variants describing the state of the swapchain's next back buffer: > **Key insight #5 — 7 surface texture variants you must handle:** > `CurrentSurfaceTexture::Success(frame)` — render normally. > `CurrentSurfaceTexture::Suboptimal(frame)` — render (buffer available but > not ideal, e.g., format mismatch). `CurrentSurfaceTexture::Timeout` — skip > frame (GPU late). `CurrentSurfaceTexture::Occluded` — skip frame (window > fully covered). `CurrentSurfaceTexture::Outdated` — surface changed, > reconfigure. `CurrentSurfaceTexture::Lost` — surface destroyed, cannot > recover without re-init. > `CurrentSurfaceTexture::Validation { source, description }` — API > validation caught an error, skip frame and log. WHY `match` on the enum: `get_current_texture()` returns a `CurrentSurfaceTexture` enum, not a `Result`. You match on the variant directly. `Success` and `Suboptimal` both carry a `SurfaceTexture` you can render into — the only difference is that `Suboptimal` signals the buffer may not be ideal (e.g., a format downgrade). The Rust compiler enforces exhaustive matching across all 7 variants. ### The Complete `render` Implementation ```rust fn render(&mut self) { let frame = match self.surface.get_current_texture() { wgpu::CurrentSurfaceTexture::Success(frame) | wgpu::CurrentSurfaceTexture::Suboptimal(frame) => frame, wgpu::CurrentSurfaceTexture::Timeout => { log::warn!("Surface timeout — skipping frame"); return; } wgpu::CurrentSurfaceTexture::Occluded => { log::warn!("Surface occluded — skipping frame"); return; } wgpu::CurrentSurfaceTexture::Outdated => { log::warn!("Surface outdated — resizing"); let size = self.window.inner_size(); self.resize(size); return; } wgpu::CurrentSurfaceTexture::Lost => { log::error!("Surface lost — GPU resources invalidated; full re-init required"); // Production recovery: signal App to drop `self.state`, // then recreate on the next RedrawRequested or in a // dedicated recovery callback. See callout below. return; } wgpu::CurrentSurfaceTexture::Validation { source, description } => { log::error!("Surface validation error: {:?} — {}", source, description); return; } }; // Drive GPU work: shader compilation, memory allocation, fence signaling if let Err(e) = self.device.poll(wgpu::PollType::Wait { submission_index: None, timeout: None }) { log::error!("Device poll failed: {e}"); return; } let texture_view = frame.texture.create_view(&Default::default()); let mut encoder = self.device.create_command_encoder( &wgpu::CommandEncoderDescriptor { label: Some("Main Command Encoder"), }, ); { let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("Main Render Pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { view: &texture_view, depth_slice: None, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), store: wgpu::StoreOp::Store, }, })], depth_stencil_attachment: None, timestamp_writes: None, occlusion_query_set: None, multiview_mask: None, }); render_pass.set_pipeline(&self.pipeline); render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..)); render_pass.draw(0..3, 0..1); } // render_pass drops here — render pass ends automatically self.queue.submit(std::iter::once(encoder.finish())); frame.present(); } ``` ### Step by Step **`surface.get_current_texture()`** — Acquires the next available back buffer from the [swapchain](concepts/GLOSSARY.md#swapchain). The swapchain cycles through 2–3 pre-allocated back buffers. This call returns immediately if a buffer is available; it does not block on the GPU. > **Surface Lost recovery pattern:** `Lost` means the compositor destroyed the > surface (display server restart, GPU reset, hotplug, etc.). Every GPU > resource tied to that surface — the `Surface`, `Device`, `Queue`, pipeline, > buffers — is irrecoverably invalidated. You cannot reuse any of them. The > production pattern is to set `self.state = None` in `App`, then on the next > `RedrawRequested` (or in a dedicated recovery callback), re-run the full > `State::new()` initialization chain from S3. This recreates the adapter, > device, surface, and all child resources. Without this, continued renders > against a lost surface will either panic or silently produce corrupted > output. **`device.poll(wgpu::PollType::Wait { submission_index: None, timeout: None })`** — **Synchronous** call that drives in-flight GPU work to completion: shader compilation fences, memory allocation, and queue signaling. Without this, resources accumulate because the device does not reclaim finished work. Called once per frame. Returns `Result<(), MaintainError>` — if the device is lost, you recover by re-creating the device. WHY this is synchronous: `poll()` does not spawn a task or use `.await`. It runs a small internal loop checking platform-specific synchronization primitives (fences, events, etc.) until all in-flight work is done, then returns. On a busy GPU this can take a few milliseconds per frame — that is normal. **`texture.create_view(&Default::default())`** — A [texture view](concepts/GLOSSARY.md#texture-view) is how wgpu references a texture's memory inside a render pass. The GPU does not accept raw texture handles in render pass attachments — it requires a view that describes the mip level range, aspect, and dimension format. `Default::default()` creates a full-view covering all mip levels and all aspects. **`device.create_command_encoder(&desc)`** — Opens a recording session. The [command encoder](concepts/GLOSSARY.md#command-buffer) is where you append instructions. Think of it as building a function body: you add statements, then `finish()` closes the function and returns the compiled buffer. **`encoder.begin_render_pass(&desc)`** — Starts a scoped drawing block. The [render pass](concepts/GLOSSARY.md#render-pass) descriptor defines the target attachments (color, depth, stencil). The returned `RenderPass` is a scoped guard — when it drops, the render pass ends automatically. ### Render Pass Color Attachment ```rust color_attachments: &[Some(wgpu::RenderPassColorAttachment { view: &texture_view, depth_slice: None, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), store: wgpu::StoreOp::Store, }, })], ``` **`RenderPassColorAttachment` has exactly 4 fields:** - **`view: &texture_view`** — the framebuffer we draw into. Must match the color target format in the [render pipeline](concepts/GLOSSARY.md#pipeline-render). - **`depth_slice: None`** — only used for 3D texture slices. Not applicable to 2D rendering. - **`resolve_target: None`** — only used for MSAA resolve. When multisampling is active, the render pass writes to a multisampled buffer and resolves into this target. We have no MSAA, so `None`. - **`ops`** — [operations](concepts/GLOSSARY.md#operations) controlling load and store behavior. Two sub-fields: - **`load: LoadOp::Clear(color)`** — before drawing, fill the entire framebuffer with this color. **This IS your background color.** Black. `LoadOp::Load` keeps existing pixels (used in UI compositing where you draw on top of previous content). - **`store: StoreOp::Store`** — after drawing, keep what was written. The GPU writes the result back to the texture so the swapchain can present it. `StoreOp::Discard` throws away the result — used for offscreen renders where only the depth/stencil result matters. **`depth_stencil_attachment: None`** — No depth or stencil buffer. When you have a depth texture, it goes here. **`timestamp_writes: None`** — GPU hardware timestamps for profiling. Not used in production rendering; requires a query set. **`occlusion_query_set: None`** — hardware occlusion queries (count fragments that pass the depth test). Useful for visibility-based culling. **`multiview_mask: None`** — multiview rendering mask for VR / multi-viewport. ### Binding State and Drawing **`render_pass.set_pipeline(&self.pipeline)`** — Tells the GPU which [render pipeline](concepts/GLOSSARY.md#pipeline-render) to use for subsequent draw calls. The pipeline encapsulates the shader programs, vertex format, primitive topology, and output configuration. Must be set before any draw call in a render pass. Switching pipelines mid-pass is expensive and should be minimized. WHY this is necessary: the GPU hardware does not store pipeline state between frames. Every render pass starts with no pipeline bound. You must set it every frame. **`render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..))`** — Binds the [vertex buffer](concepts/GLOSSARY.md#vertex-buffer) to slot 0. `buffer.slice(..)` creates a [buffer slice](concepts/GLOSSARY.md#buffer-slice) covering the full buffer (equivalent to `buffer.slice(0..)`). Slot 0 corresponds to the first layout in the pipeline's vertex buffer layouts array. If you had multiple vertex buffers (e.g., separate position and instance buffers), you'd bind them to slots 0, 1, etc. **`render_pass.draw(0..3, 0..1)`** — The draw command. Two `Range` arguments: - First range `0..3` — vertex range. Draw vertices 0, 1, 2 (three vertices forming one triangle). - Second range `0..1` — instance range. Draw instance 0 (one instance). WHY two ranges: the vertex range controls which vertices from the buffer are read. The instance range controls instanced rendering — the same geometry drawn multiple times with different instance-data attributes. For a single triangle, one draw call with `0..1` instances is correct. **Render pass scope drop** — When the `render_pass` variable goes out of scope (the closing `}` in the block), the drop implementation ends the render pass and performs validation. If you forgot to set the pipeline or bind a required buffer, wgpu reports the error at drop time, not at draw time. **`encoder.finish()`** — Seals the command encoder. Returns the finished [command buffer](concepts/GLOSSARY.md#command-buffer) ready for submission. After `finish()`, the encoder cannot be used again. **`queue.submit(iter)`** — Dispatches one or more command buffers to the GPU. Takes an iterator of command buffers. We submit exactly one: the frame's command buffer. This is a fire-and-forget call — it queues the work and returns immediately. The GPU executes it asynchronously, in parallel with your next frame's CPU work. **`surface_texture.present()`** — Queues the rendered back buffer for display. This tells the swapchain: "this buffer is done, show it on screen." **If you forget this, you render to a buffer nobody sees.** The swapchain cycles the buffer from "render target" to "front buffer" on the next vsync. ### Why the Match Arms Differ - **`CurrentSurfaceTexture::Success(frame)` / `Suboptimal(frame)`** — the swapchain delivered a `SurfaceTexture` you can render into. `Success` means the buffer is ideal. `Suboptimal` means the buffer is available but may not be ideal (e.g., format mismatch, downgraded resolution). Both carry the same `SurfaceTexture`. Extract `frame.texture` to create a view, render, then call `frame.present()`. - **`CurrentSurfaceTexture::Timeout`** — the GPU exceeded the wait threshold for a back buffer. Skip the frame. The GPU will catch up. - **`CurrentSurfaceTexture::Occluded`** — the window is fully covered by another window. Skip the frame; there's no point rendering to an invisible surface. - **`CurrentSurfaceTexture::Outdated`** — the swapchain was created for a resolution that no longer matches the window. Reconfigure the surface using `self.window.inner_size()` to match the current dimensions. - **`CurrentSurfaceTexture::Lost`** — the GPU or display server has been reset. Without re-creating the device and surface, you cannot recover. In a real application, you'd trigger a full re-initialization. - **`CurrentSurfaceTexture::Validation { source, description }`** — the wgpu validation layer caught an API misuse. Log the diagnostic and skip the frame. ## S8: Handling Window Resize WHY `surface.configure()` on resize: The swapchain allocates back buffers at a fixed dimension. When the window size changes, the old back buffers no longer match the window's display surface. Presenting a mismatched-size buffer causes undefined behavior — the display server clips, stretches, or rejects it. `surface.configure()` allocates new back buffers matching the new dimensions and discards the old ones. WHY `width.max(1)`: On some display servers, minimizing a window briefly reports `0 × 0` size before restoring. A zero-dimension surface allocation panics. Clamping to 1 ensures the swapchain always has valid dimensions. WHY `std::mem::take(&mut self.config.view_formats)`: The `view_formats` field of `SurfaceConfiguration` is an owned `Vec`. When constructing the new configuration, you move the vector out of the old config rather than cloning it. `mem::take` replaces the field with `Vec::new()` (zero allocation) and returns the original vector. This avoids a heap allocation for what is typically a 1-element vec. ```rust fn resize(&mut self, size: winit::dpi::PhysicalSize) { if size.width > 0 && size.height > 0 { let config = wgpu::SurfaceConfiguration { usage: self.config.usage, format: self.config.format, width: size.width.max(1), height: size.height.max(1), present_mode: self.config.present_mode, desired_maximum_frame_latency: self.config.desired_maximum_frame_latency, alpha_mode: self.config.alpha_mode, view_formats: std::mem::take(&mut self.config.view_formats), }; self.surface.configure(&self.device, &config); self.config = config; } } ``` FIELD BY FIELD: **`usage` / `format` / `present_mode` / `alpha_mode`** — carried over from the old config unchanged. These properties are negotiated once at init time and do not change on resize. **`width` / `height`** — the new dimensions, clamped to at least 1. **`desired_maximum_frame_latency`** — swapchain back-pressure setting. Kept from the old config. This value controls how many frames the swapchain buffers between CPU submission and GPU presentation. A value of 2 (triple buffering) provides smooth frame pacing under variable CPU/GPU load. See S3 init step 5. **`view_formats`** — additional texture formats the surface can create views with. `std::mem::take()` moves the owned vector from the old config into the new config. After `take()`, the old config's `view_formats` is an empty `Vec`. This avoids a `clone()` of the vector. Since the old config is about to be overwritten by `self.config = config`, the emptied field is irrelevant. **`surface.configure(&self.device, &config)`** — takes a reference to the `Device` and the new `SurfaceConfiguration`. This is not async. It allocates the new swapchain buffers and replaces the old ones. Any in-flight renders using old buffers complete normally; the new buffers are available after this call returns. ### When `resize` Is Called In our `App::window_event` handler (S2), the `WindowEvent::Resized(size)` arm calls `state.resize(size)`. Since `State` owns an `Arc` (see S3), `resize()` has access to the window internally and needs only the new dimension. The resize fires once for every dimension change. On fast window resizing, you may receive dozens of resize events in succession. `surface.configure()` is fast enough to handle this — each call discards old buffers and allocates new ones. The GPU continues processing in-flight frames with the old buffer dimensions; there is no visual glitch because the swapchain handles the transition seamlessly. ## S9: Where All the Code Goes The full source is the codeblocks in sections S2–S8, assembled in order into `src/main.rs` and `src/shader.wgsl`. ### File Structure ``` src/ ├── 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 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. Refer to [concepts/GLOSSARY.md](concepts/GLOSSARY.md) for term definitions used throughout these sections. See [TROUBLESHOOTING.md](TROUBLESHOOTING.md) for common issues and their fixes. ## S10: Running It Run the project: ```bash cargo run ``` **Expected console output:** wgpu adapter info (GPU model, driver name), shader module compilation log, pipeline creation messages, and the `simple_logger` debug lines from surface status and device polling. **Expected visual:** A black background (from `LoadOp::Clear(wgpu::Color::BLACK)`) with a rainbow triangle spanning most of the window. Red at the bottom-left corner, blue at the bottom-right corner, green at the top vertex. Colors blend smoothly across the triangle surface via hardware interpolation. **Expected CPU usage:** 100% on one core due to `ControlFlow::Poll` driving a continuous redraw loop. This is normal for a demo that redraws every vsync. See [TROUBLESHOOTING.md](TROUBLESHOOTING.md) for common issues. ## S11: What You've Learned and What's Next ### Summary You have built a complete GPU-rendered application from scratch. Here is what each piece does: - **The 5-layer wgpu [init chain](concepts/GLOSSARY.md#instance):** [Instance](concepts/GLOSSARY.md#instance) → [Surface](concepts/GLOSSARY.md#surface) → [Adapter](concepts/GLOSSARY.md#adapter) → [Device](concepts/GLOSSARY.md#device) + [Queue](concepts/GLOSSARY.md#queue) → [SurfaceConfiguration](concepts/GLOSSARY.md#surface-configuration). Each layer adds a capability: driver connection, window binding, GPU selection, resource management, and swapchain allocation. - **The [render pipeline](concepts/GLOSSARY.md#pipeline-render):** Shaders, topology, and vertex layout compiled into a GPU configuration. Created once, reused every frame. Expensive to create, cheap to execute. - **The [command buffer](concepts/GLOSSARY.md#command-buffer) model:** Record instructions on the CPU, submit atomically to the queue, GPU executes asynchronously. No `.await` on a draw call. - **The [swapchain](concepts/GLOSSARY.md#swapchain) and [framebuffer](concepts/GLOSSARY.md#framebuffer):** Double-buffered rendering through [PresentMode::Mailbox](concepts/GLOSSARY.md#present-mode). Acquire a back buffer, render into it, present it to the display. - **GPU [interpolation](concepts/GLOSSARY.md#interpolation):** Vertex attributes automatically blended across triangle surfaces. You supply values at three points; the rasterizer computes every value in between. ### What's Next With the render loop and pipeline foundation in place, the next steps are: - **Textures and bind groups** — loading images onto the GPU and sampling them in fragment shaders - **Uniforms and 3D transforms** — projection, view, and model matrices for positioning geometry in 3D space - **Lighting and material models** — diffuse, specular, and PBR shading - **Depth buffering and z-fighting** — per-pixel depth testing for correct overlap ordering - **Compute shaders and GPU compute pipelines** — general-purpose GPU computation outside the graphics pipeline > **Prerequisite note — matrix math:** Every topic above ultimately depends on > matrix mathematics. Transforms (model, view, and projection matrices) move > geometry from local object space through world space, camera space, and > finally into clip space. In this tutorial, all vertex positions are hardcoded > NDC coordinates so we can focus on the rendering pipeline itself. Real > applications compute these coordinates via matrix multiplication: a > transformation matrix is uploaded to the GPU as a uniform, and the vertex > shader multiplies each vertex by that matrix before outputting > `clip_position`. If linear algebra is unfamiliar, study it before diving > into the next tutorials. Recommended resources: [Learn > OpenGL's linear algebra section](https://learnopengl.com/Getting-started/Coordinate-Systems) > for a graphics-oriented treatment, and > [3Blue1Brown's Essence of Linear Algebra](https://www.youtube.com/playlist?list=PLZHQObOWTQDPD3MizzM2xVFitgF8hE_ab) > for an intuitive visual foundation. Keep [concepts/GLOSSARY.md](concepts/GLOSSARY.md) handy as you move forward.