@@ -23,26 +23,22 @@ 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 the display server (X11 or Wayland on Linux). Think of it like `epoll`
or `kqueue` but for windows, input, and display lifecycle events instead of file
descriptors.
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.
The entire program runs on the tokio async runtime — wgpu's [adapter](concepts/GLOSSARY.md#adapter)
queries and [device](concepts/GLOSSARY.md#device) creation are async, and the
runtime is the natural home for the main event loop.
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 `#[tokio::main] async fn`** — the entry point runs on the tokio
runtime, giving us access to tokio's task scheduler and I/O facilities.
- **`tokio::spawn_blocking`** — winit's `event_loop.run_app()` is synchronous
and owns the display server connection. Blocking the tokio runtime thread with
an indefinite sync call would starve other tasks. We offload the blocking event
loop to a dedicated thread, then await the join handle.
- **`Handle::block_on()` in `resumed()`** — wgpu initialization (adapter and
- **`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. This initial GPU
setup takes ~50ms of wall time.
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<Window>`** — 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
@@ -60,52 +56,58 @@ Add these to your `Cargo.toml`:
@@ -159,18 +171,28 @@ impl ApplicationHandler<()> for App {
}
```
> **WHY: `spawn_blocking` for winit**
> **WHY: `pollster::block_on` for async GPU init**
>
> The display server event loop must run to completion and cannot be interrupted. If we ran `run_app()` on the tokio runtime thread, no other async tasks could execute. By spawning it on a blocking thread, the tokio runtime remains free for GPU queries, driver I/O, and future background tasks.
> **WHY: `Handle::block_on` for async GPU init**
>
> wgpu's `request_adapter` and `request_device` query the driver over async D-Bus/Wayland/Vulkan entrypoints. These futures must be polled by a runtime executor. `block_on` attaches temporarily to the runtime thread via its handle, polls the future to completion (~50ms), then returns the result.
> 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: `ControlFlow` Poll vs Wait — the CPU usage tradeoff**
>
> - `ControlFlow::Poll` keeps the render loop running continuously. winit fires `RedrawRequested` as fast as possible, giving you a tight loop ideal for games and animation. The downside: it consumes 100% of one CPU core because the event loop never sleeps.
> - `ControlFlow::Wait` blocks the event loop until a new event arrives. This is dramatically more CPU-efficient since the loop sleeps between frames. However, it requires explicit `request_redraw()` calls to trigger rendering — without them, the window stays static.
> - For this tutorial, `ControlFlow::Poll` is used for simplicity: you don't need to reason about when to request redraws. A real application would almost always use `ControlFlow::Wait` with targeted `request_redraw()` calls to avoid wasting CPU cycles.
> - The `request_redraw()` call in the `RedrawRequested` handler works with both modes, but has different semantics: under `Poll` it keeps the loop going (which is already happening anyway), while under `Wait` it's the sole mechanism that wakes the event loop for the next frame.
**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
@@ -182,6 +204,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
implState{
// S3: async constructor — builds the full GPU init chain (8 steps)
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
constVERTICES: &[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
drivers from the manufacturer's website (NVIDIA, AMD, or Intel). Some
integrated GPUs may require a Windows 10+ build. If DX12 is unavailable,
install the LunarG Vulkan Runtime.
- **macOS:** Metal is required. Ensure your GPU supports Metal (all Macs
from 2012 onward). Update macOS to the latest version. If you're on
Apple Silicon, wgpu will automatically use the Metal backend.
- **Linux:** Install Vulkan drivers and tools:
```bash
# Ubuntu/Debian
sudo apt install vulkan-tools mesa-vulkan-drivers
# Arch
sudo pacman -S vulkan-tools vulkan-radeon # or vulkan-intel / nvidia-utils
vulkaninfo # verify installation
```
**Symptom:** Program crashes with a surface lost error during rendering.
## 5. "Adapter not compatible" (macOS / Metal)
**Cause:** Display server restarted or GPU context was reset. The [Surface](concepts/GLOSSARY.md#surface) is permanently invalidated.
**Symptom:** wgpu reports that no adapter is compatible with the surface.
**Fix:** In the tutorial, this means the window needs to be reopened. In production code, handle the `Lost` variant of `CurrentSurfaceTexture` by recreating the surface via `Instance::create_surface()`.
**Cause:** On macOS, wgpu uses Metal exclusively. If the GPU does not support
Metal (e.g., very old Mac hardware or a GPU below Metal 2.0), no adapter will
be found.
## 6. Wayland surface crashes or doesn't render
**Fix:** Verify your Mac supports Metal. Check `System Information > Graphics`
in macOS. If Metal is not supported, the GPU cannot be used with wgpu.
**Symptom:** Program crashes or shows blank window on Wayland.
## 6. "Surface lost"
**Cause:** winit's Wayland backend may have compatibility issues with certain compositors.
**Symptom:** Program crashes with a surface lost error during rendering, or the
window renders blanks after being minimized/restored.
**Fix:** Force X11 backend:
**Cause:** The display server or GPU context was reset. The
[Surface](concepts/GLOSSARY.md#surface) is permanently invalidated. Common
triggers include:
- Window minimized and restored (some display servers)
**Fix:** No fix needed — this is expected for a continuous redraw demo. For production, switch to `ControlFlow::Wait` and call `request_redraw()` only when state changes.
## 9. Shader compilation or pipeline creation error
## 10. Shader compilation or pipeline creation error
**Symptom:** Program panics during `create_render_pipeline` with a shader validation error.
## 10. Triangle shows one solid color instead of gradient
## 11. Triangle shows one solid color instead of gradient
**Symptom:** Triangle renders but is a single uniform color instead of smoothly blending.
@@ -123,7 +162,7 @@ Not this (which returns a solid color):
return vec4<f32>(1.0, 1.0, 1.0, 1.0); // wrong: solid white
```
## 11. WGSL shader compilation errors
## 12. WGSL shader compilation errors
**Symptom:** Program panics or logs errors during `device.create_shader_module()` or `create_render_pipeline()` with messages referencing WGSL validation failures.
- Confirm the function name in your `@vertex`/`@fragment` declaration matches the string you pass to `ProgrammableStage::entry_point`
- If the error message is unclear, try compiling the shader in isolation to isolate syntax vs. pipeline-binding issues
## 12. GPU debugging with RenderDoc
## 13. GPU debugging with RenderDoc
**Symptom:** Rendering issues that are difficult to diagnose (artifacts, wrong output, silent failures).
**Cause:** GPU debugging is hard with standard tools. Graphics pipeline state, shader execution, and buffer contents are not easily inspectable at runtime.
**Fix:** Use [RenderDoc](https://renderdoc.org/) — a standalone GPU debugging tool supporting frame capture, pipeline state inspection, and shader debugging. It works with Vulkan (Linux), DX12 (Windows), and OpenGL. Launch RenderDoc, attach to your wgpu process, and capture frames to inspect the full graphics pipeline step by step.
**Fix:** Use [RenderDoc](https://renderdoc.org/) — a standalone GPU debugging tool supporting frame capture, pipeline state inspection, and shader debugging. It works with Vulkan (Linux), DX12/DX11 (Windows), and Metal (macOS). Launch RenderDoc, attach to your wgpu process, and capture frames to inspect the full graphics pipeline step by step. On macOS, RenderDoc supports Metal 2.0+ devices.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.