Compare commits
6 Commits
fc2a04fe14
...
45b6819f1e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45b6819f1e | ||
|
|
0479cdf4e4 | ||
|
|
13dc88aafb | ||
|
|
5a7cb22bf2 | ||
|
|
b557ea8a1e | ||
|
|
97922d3616 |
@@ -23,26 +23,22 @@ programs that drive rendering.
|
|||||||
## S2: The winit Application and Event Loop
|
## S2: The winit Application and Event Loop
|
||||||
|
|
||||||
New concept: **event-driven windowing.** winit is the bridge between your Rust
|
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`
|
code and your platform's display server. Think of it like `epoll`, `kqueue`,
|
||||||
or `kqueue` but for windows, input, and display lifecycle events instead of file
|
or IOCP (Windows) but for windows, input, and display lifecycle events instead
|
||||||
descriptors.
|
of file descriptors.
|
||||||
|
|
||||||
The entire program runs on the tokio async runtime — wgpu's [adapter](concepts/GLOSSARY.md#adapter)
|
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`.
|
||||||
queries and [device](concepts/GLOSSARY.md#device) creation are async, and the
|
|
||||||
runtime is the natural home for the main event loop.
|
|
||||||
|
|
||||||
### Architecture Overview
|
### Architecture Overview
|
||||||
|
|
||||||
- **`main()` is `#[tokio::main] async fn`** — the entry point runs on the tokio
|
- **`main()` is synchronous** — the entry point initializes logging, creates
|
||||||
runtime, giving us access to tokio's task scheduler and I/O facilities.
|
the event loop, and calls `run_app()`. The entire program runs on a single
|
||||||
- **`tokio::spawn_blocking`** — winit's `event_loop.run_app()` is synchronous
|
thread. No async runtime is needed for the main loop.
|
||||||
and owns the display server connection. Blocking the tokio runtime thread with
|
- **`pollster::block_on()` in `resumed()`** — wgpu initialization (adapter and
|
||||||
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
|
|
||||||
device queries) is async, but winit's `resumed()` handler is synchronous. We
|
device queries) is async, but winit's `resumed()` handler is synchronous. We
|
||||||
bridge the two execution models exactly once at startup. This initial GPU
|
bridge the two execution models exactly once at startup using `pollster`,
|
||||||
setup takes ~50ms of wall time.
|
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
|
- **`Arc<Window>`** — shared reference count to the window, needed because both
|
||||||
winit event handlers and wgpu [surface](concepts/GLOSSARY.md#surface) state
|
winit event handlers and wgpu [surface](concepts/GLOSSARY.md#surface) state
|
||||||
must hold a reference to the same window object across the event loop
|
must hold a reference to the same window object across the event loop
|
||||||
@@ -60,52 +56,58 @@ Add these to your `Cargo.toml`:
|
|||||||
```toml
|
```toml
|
||||||
wgpu = "29"
|
wgpu = "29"
|
||||||
winit = "0.30"
|
winit = "0.30"
|
||||||
tokio = { version = "1", features = ["rt", "macros"] }
|
pollster = "0.4"
|
||||||
bytemuck = { version = "1", features = ["derive"] }
|
bytemuck = { version = "1", features = ["derive"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
simple_logger = "5"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
- `wgpu` — the GPU abstraction layer. Manages device lifecycles, shaders, buffers,
|
- `wgpu` — the GPU abstraction layer. Manages device lifecycles, shaders, buffers,
|
||||||
pipelines, and command encoding.
|
pipelines, and command encoding.
|
||||||
- `winit` — cross-platform window creation and event dispatch. Owns the display
|
- `winit` — cross-platform window creation and event dispatch. Owns the display
|
||||||
server connection.
|
server connection.
|
||||||
- `tokio` — async runtime for the main loop and all GPU queries.
|
- `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
|
- `bytemuck` — zero-copy casting between Rust structs and byte slices. Required
|
||||||
for uploading vertex data to GPU buffers without manual serialization.
|
for uploading vertex data to GPU buffers without manual serialization.
|
||||||
- `log` / `simple_logger` — structured logging. wgpu and winit emit diagnostic
|
- `log` / `simple_logger` — structured logging. wgpu and winit emit diagnostic
|
||||||
messages via `log` when misconfigurations or driver issues are detected.
|
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
|
### Complete Code
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
|
use pollster::block_on;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use winit::application::ApplicationHandler;
|
use winit::application::ApplicationHandler;
|
||||||
use winit::dpi::LogicalSize;
|
use winit::dpi::LogicalSize;
|
||||||
use winit::event::WindowEvent;
|
use winit::event::{WindowEvent, ElementState, KeyEvent};
|
||||||
|
use winit::event::keyboard::KeyCode;
|
||||||
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
|
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
|
||||||
use winit::window::{Window, WindowId};
|
use winit::window::{Window, WindowId};
|
||||||
|
|
||||||
#[tokio::main]
|
fn main() {
|
||||||
async fn main() {
|
|
||||||
simple_logger::init_with_level(log::Level::Debug).unwrap();
|
simple_logger::init_with_level(log::Level::Debug).unwrap();
|
||||||
|
|
||||||
let event_loop = EventLoop::new().unwrap();
|
let event_loop = EventLoop::new().unwrap();
|
||||||
let handle = tokio::Handle::current();
|
|
||||||
|
|
||||||
tokio::spawn_blocking(move || {
|
|
||||||
event_loop.run_app(&mut App {
|
event_loop.run_app(&mut App {
|
||||||
handle,
|
|
||||||
window: None,
|
window: None,
|
||||||
state: None,
|
state: None,
|
||||||
})
|
})
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
struct App {
|
struct App {
|
||||||
handle: tokio::Handle,
|
|
||||||
window: Option<Arc<Window>>,
|
window: Option<Arc<Window>>,
|
||||||
state: Option<State>,
|
state: Option<State>,
|
||||||
}
|
}
|
||||||
@@ -124,30 +126,40 @@ impl ApplicationHandler<()> for App {
|
|||||||
event_loop_ctl.set_control_flow(ControlFlow::Poll);
|
event_loop_ctl.set_control_flow(ControlFlow::Poll);
|
||||||
self.window = Some(window.clone());
|
self.window = Some(window.clone());
|
||||||
|
|
||||||
self.state = Some(
|
// Graceful GPU initialization: if adapter/device creation fails,
|
||||||
self.handle
|
// log the error and close the window instead of panicking.
|
||||||
.block_on(async {
|
match block_on(State::new(window.clone())) {
|
||||||
State::new(window.clone()).await.expect("Failed to create wgpu State")
|
Ok(state) => self.state = Some(state),
|
||||||
})
|
Err(e) => {
|
||||||
.expect("Failed to create wgpu State"),
|
log::error!("Failed to initialize GPU: {}", e);
|
||||||
);
|
window.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn window_event(
|
fn window_event(&mut self, event_loop: &ActiveEventLoop, _window_id: WindowId, event: WindowEvent) {
|
||||||
&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 {
|
match event {
|
||||||
WindowEvent::Resized(size) => state.resize(size),
|
WindowEvent::CloseRequested
|
||||||
WindowEvent::CloseRequested { .. } => event_loop_ctl.exit(),
|
| WindowEvent::KeyboardInput {
|
||||||
|
event: KeyEvent {
|
||||||
|
state: ElementState::Pressed,
|
||||||
|
logical_key: KeyCode::Escape,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
event_loop.exit();
|
||||||
|
}
|
||||||
|
WindowEvent::Resized(physical_size) => {
|
||||||
|
if let Some(state) = &mut self.state {
|
||||||
|
state.resize(*physical_size);
|
||||||
|
}
|
||||||
|
}
|
||||||
WindowEvent::RedrawRequested => {
|
WindowEvent::RedrawRequested => {
|
||||||
|
if let Some(state) = &mut self.state {
|
||||||
state.render();
|
state.render();
|
||||||
window.request_redraw();
|
}
|
||||||
|
self.window.as_ref().unwrap().request_redraw();
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -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.
|
> wgpu's `request_adapter` and `request_device` query the driver over async
|
||||||
|
> platform-specific entry points (Metal on macOS, DX12/Vulkan on Windows,
|
||||||
> **WHY: `Handle::block_on` for async GPU init**
|
> 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
|
||||||
> 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.
|
> 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**
|
> **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.
|
> 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
|
**Why `request_redraw()`:** After presenting a frame to the display, we ask
|
||||||
winit to schedule the next `RedrawRequested` frame. This creates an explicit
|
winit to schedule the next `RedrawRequested` frame. This creates an explicit
|
||||||
render loop: render → present → request redraw → render → repeat. The rate is
|
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
|
opportunity to flush the queue and release GPU resources before the process
|
||||||
exits.
|
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
|
## S3: Connecting to the GPU — The Init Chain
|
||||||
|
|
||||||
New concept: **5-layer GPU connection.** Each layer adds a capability:
|
New concept: **5-layer GPU connection.** Each layer adds a capability:
|
||||||
@@ -215,6 +265,16 @@ Instance
|
|||||||
allocates the swapchain [framebuffers](concepts/GLOSSARY.md#framebuffer) for
|
allocates the swapchain [framebuffers](concepts/GLOSSARY.md#framebuffer) for
|
||||||
this window at a specific resolution and pixel format.
|
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
|
### The State Struct
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
@@ -279,7 +339,7 @@ const VERTICES: &[Vertex] = &[
|
|||||||
impl State {
|
impl State {
|
||||||
async fn new(window: Arc<Window>) -> Result<Self, String> {
|
async fn new(window: Arc<Window>) -> Result<Self, String> {
|
||||||
// Step 1: Instance — connection to the graphics driver
|
// Step 1: Instance — connection to the graphics driver
|
||||||
let instance = wgpu::Instance::default();
|
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
|
||||||
|
|
||||||
// Step 2: Surface — binds our window to the GPU's swapchain
|
// Step 2: Surface — binds our window to the GPU's swapchain
|
||||||
let surface = instance
|
let surface = instance
|
||||||
@@ -294,7 +354,7 @@ impl State {
|
|||||||
compatible_surface: None,
|
compatible_surface: None,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.ok_or("No GPU adapter found. Ensure Vulkan drivers are installed.")?;
|
.ok_or("No GPU adapter found. Ensure your graphics drivers (Vulkan/Metal/DirectX) are installed.")?;
|
||||||
|
|
||||||
// Step 4: Device + Queue — resource owner + command submission
|
// Step 4: Device + Queue — resource owner + command submission
|
||||||
let (device, queue) = adapter
|
let (device, queue) = adapter
|
||||||
@@ -347,12 +407,12 @@ impl State {
|
|||||||
attributes: &[
|
attributes: &[
|
||||||
wgpu::VertexAttribute {
|
wgpu::VertexAttribute {
|
||||||
offset: 0,
|
offset: 0,
|
||||||
format: wgpu::VertexFormat::F32x3,
|
format: wgpu::VertexFormat::Float32x3,
|
||||||
shader_location: 0,
|
shader_location: 0,
|
||||||
},
|
},
|
||||||
wgpu::VertexAttribute {
|
wgpu::VertexAttribute {
|
||||||
offset: std::mem::size_of::<[f32; 3]>() as u64,
|
offset: std::mem::size_of::<[f32; 3]>() as u64,
|
||||||
format: wgpu::VertexFormat::F32x3,
|
format: wgpu::VertexFormat::Float32x3,
|
||||||
shader_location: 1,
|
shader_location: 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -364,7 +424,7 @@ impl State {
|
|||||||
vertex: wgpu::VertexState {
|
vertex: wgpu::VertexState {
|
||||||
module: &shader_module,
|
module: &shader_module,
|
||||||
entry_point: Some("vs_main"),
|
entry_point: Some("vs_main"),
|
||||||
buffers: &[vertex_buffer_layout],
|
buffers: &[Some(&vertex_buffer_layout)],
|
||||||
compilation_options: Default::default(),
|
compilation_options: Default::default(),
|
||||||
},
|
},
|
||||||
primitive: wgpu::PrimitiveState {
|
primitive: wgpu::PrimitiveState {
|
||||||
@@ -387,7 +447,7 @@ impl State {
|
|||||||
entry_point: Some("fs_main"),
|
entry_point: Some("fs_main"),
|
||||||
targets: &[Some(wgpu::ColorTargetState {
|
targets: &[Some(wgpu::ColorTargetState {
|
||||||
format: config.format,
|
format: config.format,
|
||||||
blend: None,
|
blend: Some(wgpu::BlendState::REPLACE),
|
||||||
write_mask: wgpu::ColorWrites::ALL,
|
write_mask: wgpu::ColorWrites::ALL,
|
||||||
})],
|
})],
|
||||||
compilation_options: Default::default(),
|
compilation_options: Default::default(),
|
||||||
@@ -411,32 +471,34 @@ impl State {
|
|||||||
|
|
||||||
### Init Steps Explained
|
### Init Steps Explained
|
||||||
|
|
||||||
**Step 1 — Instance:** `Instance::default()` opens a connection to the graphics
|
**Step 1 — Instance:** `Instance::new()` opens a connection to the graphics
|
||||||
driver on the current platform. On Linux with Vulkan, this loads `libvulkan.so`
|
driver on the current platform. wgpu automatically selects the best backend:
|
||||||
and creates a Vulkan `VkInstance`. On Windows, it loads `vulkan-1.dll`. The
|
Metal on macOS, DirectX 12 (or DX11/Vulkan) on Windows, and Vulkan (or OpenGL)
|
||||||
instance is the foundational wgpu object — every other wgpu operation requires
|
on Linux. The instance is the foundational wgpu object — every other wgpu
|
||||||
it.
|
operation requires it.
|
||||||
|
|
||||||
**Step 2 — Surface:** `instance.create_surface(window)` binds the wgpu instance
|
**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
|
to the winit `Window`. This tells the GPU: "the pixels of *this* window will be
|
||||||
the output of my rendering." In Vulkan terms, this is the first half of creating
|
the output of my rendering." The surface must match the window platform type
|
||||||
a `SwapchainKHR`. The surface must match the window platform type exactly (X11,
|
exactly (Windows, macOS, X11, Wayland, etc.), and wgpu handles this
|
||||||
Wayland, Windows, macOS, etc.).
|
mapping automatically through winit.
|
||||||
|
|
||||||
**Step 3 — Adapter:** `request_adapter()` queries available GPUs and returns the
|
**Step 3 — Adapter:** `request_adapter()` queries available GPUs and returns the
|
||||||
best match for the given options. With
|
best match for the given options. With
|
||||||
`PowerPreference::HighPerformance`, wgpu prefers a discrete GPU over an
|
`PowerPreference::HighPerformance`, wgpu prefers a discrete GPU over an
|
||||||
integrated one on hybrid systems (e.g., NVIDIA + Intel Optimus). The
|
integrated one on hybrid systems (e.g., NVIDIA + Intel Optimus on Windows,
|
||||||
`compatible_surface: None` path works because our `Instance` was created without
|
AMD Radeon + Intel Iris on macOS/Linux). This preference is cross-platform: it
|
||||||
a display handle; on Linux with Vulkan, the adapter selection remains correct
|
picks the high-performance GPU regardless of backend (Metal, DX12, or Vulkan).
|
||||||
because the surface itself was created through a compatible instance.
|
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
|
**Step 4 — Device + Queue:** `request_device()` allocates the logical GPU
|
||||||
resource manager and its submission queue. The device tracks all GPU memory and
|
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
|
validates API calls. The queue is the submission endpoint — every rendered frame
|
||||||
becomes a [command buffer](concepts/GLOSSARY.md#command-buffer) that is submitted
|
becomes a [command buffer](concepts/GLOSSARY.md#command-buffer) that is submitted
|
||||||
to this queue. On Vulkan, the device corresponds to `VkDevice` and the queue
|
to this queue. This maps to platform-native concepts: `ID3D12Device` + `ID3D12CommandQueue`
|
||||||
to a `VkQueue`.
|
on DX12, `MTLDevice` + `MTLCommandQueue` on Metal, and `VkDevice` + `VkQueue` on
|
||||||
|
Vulkan.
|
||||||
|
|
||||||
> **Key insight — Validation layers catch GPU errors at runtime:** wgpu ships
|
> **Key insight — Validation layers catch GPU errors at runtime:** wgpu ships
|
||||||
> with built-in validation layers that inspect your API calls for common
|
> with built-in validation layers that inspect your API calls for common
|
||||||
@@ -503,7 +565,10 @@ runtime.
|
|||||||
|
|
||||||
### The Complete Shader
|
### 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
|
```wgsl
|
||||||
struct VertexOutput {
|
struct VertexOutput {
|
||||||
@@ -612,18 +677,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
|
across the triangle; we just attach an alpha channel and return it. The output
|
||||||
merge stage writes this color directly to the framebuffer.
|
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:
|
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):
|
||||||
```rust
|
|
||||||
let shader_module = device.create_shader_module(
|
|
||||||
wgpu::ShaderModuleDescriptor {
|
|
||||||
label: Some("Rainbow Triangle Shader"),
|
|
||||||
source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
- **`ShaderModuleDescriptor`** — has two fields: `label` (debug string, shown
|
- **`ShaderModuleDescriptor`** — has two fields: `label` (debug string, shown
|
||||||
in graphics debuggers and validation messages) and `source` (the shader
|
in graphics debuggers and validation messages) and `source` (the shader
|
||||||
@@ -654,30 +711,21 @@ strides the attribute begins.
|
|||||||
> reports "method not found." This is a Rust trait-discovery issue, not a wgpu
|
> 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.
|
> API issue. Add `use wgpu::util::DeviceExt;` to bring the method into scope.
|
||||||
|
|
||||||
### The Vertex Struct
|
### The Vertex Struct (S3, Step 7)
|
||||||
|
|
||||||
```rust
|
The `Vertex` struct is defined alongside the `VERTICES` constant at the top of
|
||||||
#[repr(C)]
|
`main.rs`, before `impl State`. This is the same struct used in the master
|
||||||
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
|
initialization block (S3). The key annotations are:
|
||||||
struct Vertex {
|
|
||||||
position: [f32; 3],
|
|
||||||
color: [f32; 3],
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
> **WHY: `#[repr(C)]` + bytemuck for GPU data layout**
|
> **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.
|
> `#[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
|
The `VERTICES` constant is defined at the top of `main.rs` alongside the
|
||||||
const VERTICES: &[Vertex] = &[
|
`Vertex` struct. It is the same data used in the master initialization block
|
||||||
Vertex { position: [-0.5, -0.5, 0.0], color: [1.0, 0.0, 0.0] }, // red
|
(S3, Step 7).
|
||||||
Vertex { position: [ 0.5, -0.5, 0.0], color: [0.0, 0.0, 1.0] }, // blue
|
|
||||||
Vertex { position: [ 0.0, 0.5, 0.0], color: [0.0, 1.0, 0.0] }, // green
|
|
||||||
];
|
|
||||||
```
|
|
||||||
|
|
||||||
- **Positions are in NDC:** The [normalized device coordinates](concepts/GLOSSARY.md#ndc)
|
- **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
|
range from -1.0 (left/bottom) to +1.0 (right/top). Our triangle spans the
|
||||||
@@ -690,18 +738,10 @@ const VERTICES: &[Vertex] = &[
|
|||||||
determines which face is "front" and which is "back" — critical for
|
determines which face is "front" and which is "back" — critical for
|
||||||
[culling](concepts/GLOSSARY.md) and correct normal computation.
|
[culling](concepts/GLOSSARY.md) and correct normal computation.
|
||||||
|
|
||||||
### Buffer Upload
|
### Buffer Upload (S3, Step 7)
|
||||||
|
|
||||||
```rust
|
This code lives in the master `State::new()` block (S3, Step 7). The
|
||||||
use wgpu::util::DeviceExt;
|
`create_buffer_init` method is the combined allocate-and-upload call.
|
||||||
let vertex_buffer = device.create_buffer_init(
|
|
||||||
&wgpu::util::BufferInitDescriptor {
|
|
||||||
label: Some("Vertex Buffer"),
|
|
||||||
contents: bytemuck::cast_slice(VERTICES),
|
|
||||||
usage: wgpu::BufferUsages::VERTEX,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
- **`use wgpu::util::DeviceExt`** — imports the extension trait that adds
|
- **`use wgpu::util::DeviceExt`** — imports the extension trait that adds
|
||||||
`create_buffer_init` to `Device`. Without this import, the method is not
|
`create_buffer_init` to `Device`. Without this import, the method is not
|
||||||
@@ -731,29 +771,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
|
not at draw time. This validation-upfront model is what makes pipelines expensive
|
||||||
to create but cheap to execute.
|
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
|
Before the pipeline descriptor, you must tell wgpu how to parse the byte stream
|
||||||
in the vertex buffer into per-vertex attributes:
|
in the vertex buffer into per-vertex attributes. This layout is defined inside
|
||||||
|
the master `State::new()` block (S3, Step 8):
|
||||||
```rust
|
|
||||||
let vertex_buffer_layout = wgpu::VertexBufferLayout {
|
|
||||||
array_stride: std::mem::size_of::<Vertex>() as u64,
|
|
||||||
step_mode: wgpu::VertexStepMode::Vertex,
|
|
||||||
attributes: &[
|
|
||||||
wgpu::VertexAttribute {
|
|
||||||
offset: 0,
|
|
||||||
format: wgpu::VertexFormat::F32x3,
|
|
||||||
shader_location: 0,
|
|
||||||
},
|
|
||||||
wgpu::VertexAttribute {
|
|
||||||
offset: std::mem::size_of::<[f32; 3]>() as u64,
|
|
||||||
format: wgpu::VertexFormat::F32x3,
|
|
||||||
shader_location: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
- **`array_stride: 24`** — `size_of::<Vertex>()` = 24 bytes (6 × `f32` × 4 bytes).
|
- **`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
|
This is the byte distance from one vertex to the next in the buffer. The GPU
|
||||||
@@ -763,7 +785,7 @@ let vertex_buffer_layout = wgpu::VertexBufferLayout {
|
|||||||
the vertex shader processes. The other option is `Instance`, which advances
|
the vertex shader processes. The other option is `Instance`, which advances
|
||||||
per draw instance in instanced rendering. For a single triangle, `Vertex` is
|
per draw instance in instanced rendering. For a single triangle, `Vertex` is
|
||||||
correct: each of the three vertices has its own position and color.
|
correct: each of the three vertices has its own position and color.
|
||||||
- **First attribute — `shader_location: 0`**: reads 3 floats (`F32x3`) at byte
|
- **First attribute — `shader_location: 0`**: reads 3 floats (`Float32x3`) at byte
|
||||||
offset 0 of each vertex. These 3 floats map to the
|
offset 0 of each vertex. These 3 floats map to the
|
||||||
[shader location](concepts/GLOSSARY.md#shader-location) `@location(0)` in the
|
[shader location](concepts/GLOSSARY.md#shader-location) `@location(0)` in the
|
||||||
vertex shader — the `position` parameter. The GPU delivers `[x, y, z]` to
|
vertex shader — the `position` parameter. The GPU delivers `[x, y, z]` to
|
||||||
@@ -775,47 +797,11 @@ let vertex_buffer_layout = wgpu::VertexBufferLayout {
|
|||||||
receive the position values as the color input, rendering a triangle with
|
receive the position values as the color input, rendering a triangle with
|
||||||
gradient colors derived from position data.
|
gradient colors derived from position data.
|
||||||
|
|
||||||
### The Complete Render Pipeline Descriptor
|
### The Complete Render Pipeline Descriptor (S3, Step 8)
|
||||||
|
|
||||||
```rust
|
The full `device.create_render_pipeline()` call with all descriptor fields
|
||||||
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
lives in the master `State::new()` block (S3, Step 8). Below is the
|
||||||
label: Some("Triangle Pipeline"),
|
field-by-field walkthrough explaining every parameter.
|
||||||
layout: None,
|
|
||||||
vertex: wgpu::VertexState {
|
|
||||||
module: &shader_module,
|
|
||||||
entry_point: Some("vs_main"),
|
|
||||||
buffers: &[vertex_buffer_layout],
|
|
||||||
compilation_options: Default::default(),
|
|
||||||
},
|
|
||||||
primitive: wgpu::PrimitiveState {
|
|
||||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
|
||||||
strip_index_format: None,
|
|
||||||
front_face: wgpu::FrontFace::Ccw,
|
|
||||||
cull_mode: Some(wgpu::Face::Back),
|
|
||||||
unclipped_depth: false,
|
|
||||||
polygon_mode: wgpu::PolygonMode::Fill,
|
|
||||||
conservative: false,
|
|
||||||
},
|
|
||||||
depth_stencil: None,
|
|
||||||
multisample: wgpu::MultisampleState {
|
|
||||||
count: 1,
|
|
||||||
mask: !0,
|
|
||||||
alpha_to_coverage_enabled: false,
|
|
||||||
},
|
|
||||||
fragment: Some(wgpu::FragmentState {
|
|
||||||
module: &shader_module,
|
|
||||||
entry_point: Some("fs_main"),
|
|
||||||
targets: &[Some(wgpu::ColorTargetState {
|
|
||||||
format: config.format,
|
|
||||||
blend: None,
|
|
||||||
write_mask: wgpu::ColorWrites::ALL,
|
|
||||||
})],
|
|
||||||
compilation_options: Default::default(),
|
|
||||||
}),
|
|
||||||
multiview_mask: None,
|
|
||||||
cache: None,
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Field-by-Field Walkthrough
|
### Field-by-Field Walkthrough
|
||||||
|
|
||||||
@@ -837,7 +823,7 @@ must provide a `RenderPipelineLayout` created with `device.create_render_pipelin
|
|||||||
- **`entry_point: Some("vs_main")`** — selects which function in the module is
|
- **`entry_point: Some("vs_main")`** — selects which function in the module is
|
||||||
the vertex shader entry point. Must match the `@vertex fn vs_main(...)`
|
the vertex shader entry point. Must match the `@vertex fn vs_main(...)`
|
||||||
declaration exactly.
|
declaration exactly.
|
||||||
- **`buffers: &[vertex_buffer_layout]`** — array of vertex buffer layouts.
|
- **`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
|
Multiple layouts are used rarely (multi-mesh, GPU instancing with separate
|
||||||
instance buffers). For a single vertex buffer, one layout suffices.
|
instance buffers). For a single vertex buffer, one layout suffices.
|
||||||
- **`compilation_options: Default::default()`** — shader compilation backend
|
- **`compilation_options: Default::default()`** — shader compilation backend
|
||||||
@@ -894,10 +880,11 @@ draws at the same pixel. For a single triangle this is not a concern.
|
|||||||
`SurfaceConfiguration`. The pipeline writes in this format; the surface
|
`SurfaceConfiguration`. The pipeline writes in this format; the surface
|
||||||
reads in this format. A mismatch at render time produces an error. If
|
reads in this format. A mismatch at render time produces an error. If
|
||||||
you change the surface format, you must recreate the pipeline.
|
you change the surface format, you must recreate the pipeline.
|
||||||
- **`blend: None`** — disables blending. Without blending, every fragment
|
- **`blend: Some(wgpu::BlendState::REPLACE)`** — explicitly replaces every
|
||||||
color replaces the existing framebuffer pixel (`REPLACE` mode). With
|
fragment color with the new output. `None` would default to this behavior,
|
||||||
blending, new and existing colors are combined according to a blend
|
but we make it explicit for clarity. With a custom blend state, new and
|
||||||
equation (useful for transparency).
|
existing colors can be combined according to a blend equation
|
||||||
|
(useful for transparency).
|
||||||
- **`write_mask: ColorWrites::ALL`** — write all four RGBA channels.
|
- **`write_mask: ColorWrites::ALL`** — write all four RGBA channels.
|
||||||
You can mask out individual channels (e.g., write only R and G) if you
|
You can mask out individual channels (e.g., write only R and G) if you
|
||||||
need to preserve certain framebuffer channels across draw calls.
|
need to preserve certain framebuffer channels across draw calls.
|
||||||
@@ -951,8 +938,8 @@ fn render(&mut self) {
|
|||||||
```
|
```
|
||||||
|
|
||||||
This is a **fully synchronous** method. It runs on the winit event loop thread
|
This is a **fully synchronous** method. It runs on the winit event loop thread
|
||||||
(triggered by `RedrawRequested`), has no `async` keyword, no `.await`, and takes
|
(triggered by `RedrawRequested`), has no `async` keyword, no `.await`, and requires
|
||||||
no tokio handle. All wgpu recording and submission operations are synchronous
|
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
|
and fast — they only encode instructions and push them to the queue; they do not
|
||||||
wait for GPU completion.
|
wait for GPU completion.
|
||||||
|
|
||||||
@@ -1045,12 +1032,7 @@ fn render(&mut self) {
|
|||||||
depth_slice: None,
|
depth_slice: None,
|
||||||
resolve_target: None,
|
resolve_target: None,
|
||||||
ops: wgpu::Operations {
|
ops: wgpu::Operations {
|
||||||
load: wgpu::LoadOp::Clear(wgpu::Color {
|
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
|
||||||
r: 0.1,
|
|
||||||
g: 0.1,
|
|
||||||
b: 0.1,
|
|
||||||
a: 1.0,
|
|
||||||
}),
|
|
||||||
store: wgpu::StoreOp::Store,
|
store: wgpu::StoreOp::Store,
|
||||||
},
|
},
|
||||||
})],
|
})],
|
||||||
@@ -1096,9 +1078,9 @@ not reclaim finished work. Called once per frame. Returns
|
|||||||
re-creating the device.
|
re-creating the device.
|
||||||
|
|
||||||
WHY this is synchronous: `poll()` does not spawn a task or use `.await`. It
|
WHY this is synchronous: `poll()` does not spawn a task or use `.await`. It
|
||||||
runs a small internal loop checking Vulkan fence objects until all in-flight
|
runs a small internal loop checking platform-specific synchronization primitives
|
||||||
work is done, then returns. On a busy GPU this can take a few milliseconds per
|
(fences, events, etc.) until all in-flight work is done, then returns. On a
|
||||||
frame — that is normal.
|
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)
|
**`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
|
is how wgpu references a texture's memory inside a render pass. The GPU does
|
||||||
@@ -1124,7 +1106,7 @@ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
|||||||
depth_slice: None,
|
depth_slice: None,
|
||||||
resolve_target: None,
|
resolve_target: None,
|
||||||
ops: wgpu::Operations {
|
ops: wgpu::Operations {
|
||||||
load: wgpu::LoadOp::Clear(wgpu::Color { r: 0.1, g: 0.1, b: 0.1, a: 1.0 }),
|
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
|
||||||
store: wgpu::StoreOp::Store,
|
store: wgpu::StoreOp::Store,
|
||||||
},
|
},
|
||||||
})],
|
})],
|
||||||
@@ -1142,7 +1124,7 @@ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
|||||||
- **`ops`** — [operations](concepts/GLOSSARY.md#operations) controlling load
|
- **`ops`** — [operations](concepts/GLOSSARY.md#operations) controlling load
|
||||||
and store behavior. Two sub-fields:
|
and store behavior. Two sub-fields:
|
||||||
- **`load: LoadOp::Clear(color)`** — before drawing, fill the entire
|
- **`load: LoadOp::Clear(color)`** — before drawing, fill the entire
|
||||||
framebuffer with this color. **This IS your background color.** Dark gray.
|
framebuffer with this color. **This IS your background color.** Black.
|
||||||
`LoadOp::Load` keeps existing pixels (used in UI compositing where you
|
`LoadOp::Load` keeps existing pixels (used in UI compositing where you
|
||||||
draw on top of previous content).
|
draw on top of previous content).
|
||||||
- **`store: StoreOp::Store`** — after drawing, keep what was written. The
|
- **`store: StoreOp::Store`** — after drawing, keep what was written. The
|
||||||
@@ -1257,7 +1239,7 @@ and returns the original vector. This avoids a heap allocation for what is
|
|||||||
typically a 1-element vec.
|
typically a 1-element vec.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
fn resize(&mut self, size: wgpu::dpi::PhysicalSize<u32>) {
|
fn resize(&mut self, size: winit::dpi::PhysicalSize<u32>) {
|
||||||
if size.width > 0 && size.height > 0 {
|
if size.width > 0 && size.height > 0 {
|
||||||
let config = wgpu::SurfaceConfiguration {
|
let config = wgpu::SurfaceConfiguration {
|
||||||
usage: self.config.usage,
|
usage: self.config.usage,
|
||||||
@@ -1320,13 +1302,15 @@ The full source is the codeblocks in sections S2–S8, assembled in order into
|
|||||||
|
|
||||||
```
|
```
|
||||||
src/
|
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)
|
├── shader.wgsl # Section S4 (the complete WGSL shader)
|
||||||
```
|
```
|
||||||
|
|
||||||
- `main.rs` combines the winit event loop (S2), the init chain and `State`
|
- `main.rs` combines the winit event loop (S2), the complete `State` struct
|
||||||
struct (S3), the `Vertex` type and `VERTICES` constant (S5), the `render`
|
definition and full `State::new()` implementation covering all 8 init steps
|
||||||
method (S7), and the `resize` method (S8).
|
(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,
|
- `shader.wgsl` is the single file from S4: vertex shader, fragment shader,
|
||||||
and the `VertexOutput` struct.
|
and the `VertexOutput` struct.
|
||||||
|
|
||||||
@@ -1346,7 +1330,7 @@ cargo run
|
|||||||
module compilation log, pipeline creation messages, and the `simple_logger`
|
module compilation log, pipeline creation messages, and the `simple_logger`
|
||||||
debug lines from surface status and device polling.
|
debug lines from surface status and device polling.
|
||||||
|
|
||||||
**Expected visual:** A dark gray background (from `LoadOp::Clear`) with a
|
**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,
|
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
|
blue at the bottom-right corner, green at the top vertex. Colors blend smoothly
|
||||||
across the triangle surface via hardware interpolation.
|
across the triangle surface via hardware interpolation.
|
||||||
|
|||||||
@@ -22,7 +22,26 @@ graphics programming experience. Every sentence teaches GPU concepts — not Rus
|
|||||||
- **01 — Rainbow Triangle** — The minimal complete program: one triangle, smooth color interpolation
|
- **01 — Rainbow Triangle** — The minimal complete program: one triangle, smooth color interpolation
|
||||||
- More guides coming soon (textures, lighting, compute)
|
- More guides coming soon (textures, lighting, compute)
|
||||||
|
|
||||||
|
## Running the Project
|
||||||
|
|
||||||
|
From the project root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run
|
||||||
|
```
|
||||||
|
|
||||||
|
This works on all supported platforms. If you encounter build errors, see the
|
||||||
|
**Prerequisites** section above for platform-specific build tool requirements.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- Rust stable (edition 2024)
|
- Rust stable (edition 2024)
|
||||||
- Linux x86_64 with Vulkan drivers (`libvulkan1`)
|
- A system with a GPU supporting WebGPU-compatible APIs:
|
||||||
|
- **Windows:** Windows 10 or later with a DirectX 12-capable GPU
|
||||||
|
- **macOS:** macOS 10.13+ with a Metal-capable GPU
|
||||||
|
- **Linux:** Vulkan-capable GPU and drivers
|
||||||
|
|
||||||
|
> **Build tools:** Your platform may require additional build tools:
|
||||||
|
> - **Windows:** Visual Studio Build Tools (2019 or later)
|
||||||
|
> - **macOS:** Xcode Command Line Tools (`xcode-select --install`)
|
||||||
|
> - **Linux:** Vulkan ICD and development packages (e.g., `vulkan-validationlayers` on Ubuntu)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
```toml
|
```toml
|
||||||
wgpu = "29"
|
wgpu = "29"
|
||||||
winit = "0.30"
|
winit = "0.30"
|
||||||
tokio = { version = "1", features = ["rt", "macros"] }
|
pollster = "0.4"
|
||||||
bytemuck = { version = "1", features = ["derive"] }
|
bytemuck = { version = "1", features = ["derive"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
simple_logger = "5"
|
simple_logger = "5"
|
||||||
@@ -41,38 +41,77 @@ simple_logger = "5"
|
|||||||
view_formats: vec![format.add_srgb_suffix()],
|
view_formats: vec![format.add_srgb_suffix()],
|
||||||
```
|
```
|
||||||
|
|
||||||
## 4. Panic: "No adapter found"
|
## 4. "No adapter found"
|
||||||
|
|
||||||
**Symptom:** Program crashes immediately with `No GPU adapter found`.
|
**Symptom:** Program crashes or logs "No GPU adapter found" during startup.
|
||||||
|
|
||||||
**Cause:** No Vulkan driver installed. wgpu on Linux requires Vulkan.
|
**Cause:** wgpu cannot find a compatible graphics adapter. This can happen
|
||||||
|
across all platforms for different reasons.
|
||||||
|
|
||||||
**Fix:**
|
**Fix:**
|
||||||
```bash
|
|
||||||
sudo apt install libvulkan1 mesa-vulkan-drivers
|
|
||||||
vulkaninfo # verify installation
|
|
||||||
```
|
|
||||||
|
|
||||||
## 5. Panic: "Surface lost"
|
- **Windows:** Ensure your GPU supports DirectX 12 (WDDM 2.0+). Update GPU
|
||||||
|
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)
|
||||||
|
- Display configuration changed (hotplug, external monitor connected/disconnected)
|
||||||
|
- GPU driver reset
|
||||||
|
- Compositor restart (Linux/Wayland)
|
||||||
|
|
||||||
|
**Fix:** Handle the `CurrentSurfaceTexture::Lost` variant gracefully. In the
|
||||||
|
tutorial, this means the window may need to be reopened. In production code,
|
||||||
|
set `self.state = None`, then on the next redraw event, re-run the full
|
||||||
|
`State::new()` initialization chain to recreate all GPU resources.
|
||||||
|
|
||||||
|
## 7. Wayland surface crashes or doesn't render (Linux only)
|
||||||
|
|
||||||
|
**Symptom:** Program crashes or shows blank window on Wayland compositors
|
||||||
|
(Sway, GNOME Shell, KDE Plasma, etc.).
|
||||||
|
|
||||||
|
**Cause:** winit's Wayland backend may have compatibility issues with certain
|
||||||
|
compositors or Vulkan/Wayland interop layers.
|
||||||
|
|
||||||
|
**Fix:** Temporarily force the X11 backend:
|
||||||
```bash
|
```bash
|
||||||
WINIT_UNIX_BACKEND=x11 cargo run
|
WINIT_UNIX_BACKEND=x11 cargo run
|
||||||
```
|
```
|
||||||
|
|
||||||
## 7. Window won't close
|
If X11 works but Wayland does not, check your compositor version and winit
|
||||||
|
version. This is a Linux-specific issue and does not affect Windows or macOS.
|
||||||
|
|
||||||
|
## 8. Window won't close
|
||||||
|
|
||||||
**Symptom:** Clicking the X button or pressing Escape doesn't close the window.
|
**Symptom:** Clicking the X button or pressing Escape doesn't close the window.
|
||||||
|
|
||||||
@@ -85,7 +124,7 @@ fn exiting(&mut self, event_loop_ctl: &ActiveEventLoop) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 8. CPU at 100%
|
## 9. CPU at 100%
|
||||||
|
|
||||||
**Symptom:** One CPU core at 100% usage.
|
**Symptom:** One CPU core at 100% usage.
|
||||||
|
|
||||||
@@ -93,7 +132,7 @@ fn exiting(&mut self, event_loop_ctl: &ActiveEventLoop) {
|
|||||||
|
|
||||||
**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.
|
**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.
|
**Symptom:** Program panics during `create_render_pipeline` with a shader validation error.
|
||||||
|
|
||||||
@@ -104,7 +143,7 @@ fn exiting(&mut self, event_loop_ctl: &ActiveEventLoop) {
|
|||||||
- WGSL `@location(1)` matches Rust `shader_location: 1`
|
- WGSL `@location(1)` matches Rust `shader_location: 1`
|
||||||
- Vertex shader outputs `@builtin(position) clip_position: vec4<f32>`
|
- Vertex shader outputs `@builtin(position) clip_position: vec4<f32>`
|
||||||
|
|
||||||
## 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.
|
**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
|
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.
|
**Symptom:** Program panics or logs errors during `device.create_shader_module()` or `create_render_pipeline()` with messages referencing WGSL validation failures.
|
||||||
|
|
||||||
@@ -140,13 +179,13 @@ return vec4<f32>(1.0, 1.0, 1.0, 1.0); // wrong: solid white
|
|||||||
- Confirm the function name in your `@vertex`/`@fragment` declaration matches the string you pass to `ProgrammableStage::entry_point`
|
- 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
|
- 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).
|
**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.
|
**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.
|
||||||
|
|
||||||
## Additional Resources
|
## Additional Resources
|
||||||
|
|
||||||
|
|||||||
190
src/rainbow_traingle.rs
Normal file
190
src/rainbow_traingle.rs
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
use winit::{application::ApplicationHandler, dpi::LogicalSize, window::Window};
|
||||||
|
|
||||||
|
struct App {
|
||||||
|
window: Option<Arc<Window>>,
|
||||||
|
frame_state: Option<FrameState>,
|
||||||
|
handle: tokio::runtime::Handle,
|
||||||
|
}
|
||||||
|
struct FrameState {
|
||||||
|
surface: wgpu::Surface<'static>,
|
||||||
|
device: wgpu::Device,
|
||||||
|
queue: wgpu::Queue,
|
||||||
|
config: wgpu::SurfaceConfiguration,
|
||||||
|
window: Arc<Window>,
|
||||||
|
pipeline: wgpu::RenderPipeline,
|
||||||
|
vertex_buffer: wgpu::Buffer,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApplicationHandler for App {
|
||||||
|
fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
|
||||||
|
let window: Arc<Window> = Arc::new(
|
||||||
|
event_loop
|
||||||
|
.create_window(
|
||||||
|
Window::default_attributes().with_inner_size(LogicalSize::new(800.0, 800.0)),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
self.window = Some(window.clone());
|
||||||
|
|
||||||
|
self.frame_state = Some(
|
||||||
|
self.handle
|
||||||
|
.block_on(async {
|
||||||
|
FrameState::new(window.clone())
|
||||||
|
})
|
||||||
|
.expect("failed to create wgpu state"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 FrameState {
|
||||||
|
async fn new(window: Arc<Window>) -> Result<Self, String> {
|
||||||
|
// Step 1: Instance — connection to the graphics driver
|
||||||
|
let instance = wgpu::Instance::default();
|
||||||
|
|
||||||
|
// 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 Vulkan drivers 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::<Vertex>() 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: &[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,
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
surface,
|
||||||
|
device,
|
||||||
|
queue,
|
||||||
|
config,
|
||||||
|
window: Arc::clone(&window),
|
||||||
|
pipeline,
|
||||||
|
vertex_buffer,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user