docs: update tutorial to wgpu 29 / winit 0.30 APIs and pollster async pattern
This commit is contained in:
@@ -27,22 +27,18 @@ 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
|
or `kqueue` but for windows, input, and display lifecycle events instead of file
|
||||||
descriptors.
|
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,17 +56,16 @@ 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
|
||||||
@@ -79,6 +74,7 @@ simple_logger = "5"
|
|||||||
### 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;
|
||||||
@@ -86,26 +82,19 @@ use winit::event::WindowEvent;
|
|||||||
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 {
|
window: None,
|
||||||
handle,
|
state: None,
|
||||||
window: 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>,
|
||||||
}
|
}
|
||||||
@@ -125,11 +114,10 @@ impl ApplicationHandler<()> for App {
|
|||||||
self.window = Some(window.clone());
|
self.window = Some(window.clone());
|
||||||
|
|
||||||
self.state = Some(
|
self.state = Some(
|
||||||
self.handle
|
block_on(async {
|
||||||
.block_on(async {
|
State::new(window.clone()).await.expect("Failed to create wgpu State")
|
||||||
State::new(window.clone()).await.expect("Failed to create wgpu State")
|
})
|
||||||
})
|
.expect("Failed to create wgpu State"),
|
||||||
.expect("Failed to create wgpu State"),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,13 +147,9 @@ 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 D-Bus/Wayland/Vulkan entrypoints. 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: `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.
|
|
||||||
|
|
||||||
> **WHY: `ControlFlow::Poll` for the render loop**
|
> **WHY: `ControlFlow::Poll` for the render loop**
|
||||||
>
|
>
|
||||||
@@ -307,7 +291,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
|
||||||
@@ -375,12 +359,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,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -392,7 +376,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 {
|
||||||
@@ -415,7 +399,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(),
|
||||||
@@ -751,7 +735,7 @@ the master `State::new()` block (S3, Step 8):
|
|||||||
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
|
||||||
@@ -789,7 +773,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
|
||||||
@@ -846,10 +830,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.
|
||||||
@@ -903,8 +888,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.
|
||||||
|
|
||||||
@@ -997,12 +982,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,
|
||||||
},
|
},
|
||||||
})],
|
})],
|
||||||
@@ -1076,7 +1056,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,
|
||||||
},
|
},
|
||||||
})],
|
})],
|
||||||
@@ -1094,7 +1074,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
|
||||||
@@ -1209,7 +1189,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,
|
||||||
@@ -1300,7 +1280,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.
|
||||||
|
|||||||
Reference in New Issue
Block a user