Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45b6819f1e | ||
|
|
0479cdf4e4 | ||
|
|
13dc88aafb | ||
|
|
5a7cb22bf2 | ||
|
|
b557ea8a1e | ||
|
|
97922d3616 |
+188
-204
@@ -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`:
|
||||
```toml
|
||||
wgpu = "29"
|
||||
winit = "0.30"
|
||||
tokio = { version = "1", features = ["rt", "macros"] }
|
||||
pollster = "0.4"
|
||||
bytemuck = { version = "1", features = ["derive"] }
|
||||
log = "0.4"
|
||||
simple_logger = "5"
|
||||
```
|
||||
|
||||
- `wgpu` — the GPU abstraction layer. Manages device lifecycles, shaders, buffers,
|
||||
pipelines, and command encoding.
|
||||
- `winit` — cross-platform window creation and event dispatch. Owns the display
|
||||
server connection.
|
||||
- `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
|
||||
for uploading vertex data to GPU buffers without manual serialization.
|
||||
- `log` / `simple_logger` — structured logging. wgpu and winit emit diagnostic
|
||||
messages via `log` when misconfigurations or driver issues are detected.
|
||||
|
||||
### Cross-Platform Window Creation
|
||||
|
||||
winit abstracts all platform-specific windowing details. The same code works
|
||||
across every supported platform:
|
||||
|
||||
- **Windows:** Native Win32/composition backend.
|
||||
- **macOS:** Cocoa backend.
|
||||
- **Linux:** X11 or Wayland, depending on your active session.
|
||||
|
||||
No platform-specific conditionals or feature flags are needed — winit handles
|
||||
everything automatically.
|
||||
|
||||
### Complete Code
|
||||
|
||||
```rust
|
||||
use pollster::block_on;
|
||||
use std::sync::Arc;
|
||||
use winit::application::ApplicationHandler;
|
||||
use winit::dpi::LogicalSize;
|
||||
use winit::event::WindowEvent;
|
||||
use winit::event::{WindowEvent, ElementState, KeyEvent};
|
||||
use winit::event::keyboard::KeyCode;
|
||||
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
|
||||
use winit::window::{Window, WindowId};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
fn main() {
|
||||
simple_logger::init_with_level(log::Level::Debug).unwrap();
|
||||
|
||||
let event_loop = EventLoop::new().unwrap();
|
||||
let handle = tokio::Handle::current();
|
||||
|
||||
tokio::spawn_blocking(move || {
|
||||
event_loop.run_app(&mut App {
|
||||
handle,
|
||||
window: None,
|
||||
state: None,
|
||||
})
|
||||
event_loop.run_app(&mut App {
|
||||
window: None,
|
||||
state: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
struct App {
|
||||
handle: tokio::Handle,
|
||||
window: Option<Arc<Window>>,
|
||||
state: Option<State>,
|
||||
}
|
||||
@@ -124,30 +126,40 @@ impl ApplicationHandler<()> for App {
|
||||
event_loop_ctl.set_control_flow(ControlFlow::Poll);
|
||||
self.window = Some(window.clone());
|
||||
|
||||
self.state = Some(
|
||||
self.handle
|
||||
.block_on(async {
|
||||
State::new(window.clone()).await.expect("Failed to create wgpu State")
|
||||
})
|
||||
.expect("Failed to create wgpu State"),
|
||||
);
|
||||
// Graceful GPU initialization: if adapter/device creation fails,
|
||||
// log the error and close the window instead of panicking.
|
||||
match block_on(State::new(window.clone())) {
|
||||
Ok(state) => self.state = Some(state),
|
||||
Err(e) => {
|
||||
log::error!("Failed to initialize GPU: {}", e);
|
||||
window.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn window_event(
|
||||
&mut self,
|
||||
event_loop_ctl: &ActiveEventLoop,
|
||||
_window_id: WindowId,
|
||||
event: WindowEvent,
|
||||
) {
|
||||
let Some(state) = self.state.as_mut() else { return };
|
||||
let Some(window) = self.window.as_ref() else { return };
|
||||
|
||||
fn window_event(&mut self, event_loop: &ActiveEventLoop, _window_id: WindowId, event: WindowEvent) {
|
||||
match event {
|
||||
WindowEvent::Resized(size) => state.resize(size),
|
||||
WindowEvent::CloseRequested { .. } => event_loop_ctl.exit(),
|
||||
WindowEvent::CloseRequested
|
||||
| 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 => {
|
||||
state.render();
|
||||
window.request_redraw();
|
||||
if let Some(state) = &mut self.state {
|
||||
state.render();
|
||||
}
|
||||
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.
|
||||
|
||||
> **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
|
||||
impl State {
|
||||
// S3: async constructor — builds the full GPU init chain (8 steps)
|
||||
async fn new(window: Arc<Window>) -> Result<Self, String> {
|
||||
// ... (see S3)
|
||||
}
|
||||
|
||||
// S7: synchronous render loop — records commands and submits to GPU
|
||||
fn render(&mut self) {
|
||||
// ... (see S7)
|
||||
}
|
||||
|
||||
// S8: reconfigure swapchain when window dimensions change
|
||||
fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) {
|
||||
// ... (see S8)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The next sections fill in each method. S3 covers `State::new()` in full.
|
||||
S7 covers `render()`. S8 covers `resize()`.
|
||||
|
||||
## S3: Connecting to the GPU — The Init Chain
|
||||
|
||||
New concept: **5-layer GPU connection.** Each layer adds a capability:
|
||||
@@ -212,8 +262,18 @@ Instance
|
||||
queue is the submission channel: you encode work into command buffers and
|
||||
submit them to the queue.
|
||||
5. **[SurfaceConfiguration](concepts/GLOSSARY.md#surface-configuration)** —
|
||||
allocates the swapchain [framebuffers](concepts/GLOSSARY.md#framebuffer) for
|
||||
this window at a specific resolution and pixel format.
|
||||
allocates the swapchain [framebuffers](concepts/GLOSSARY.md#framebuffer) for
|
||||
this window at a specific resolution and pixel format.
|
||||
|
||||
> **Cross-platform GPU adapter selection:** wgpu abstracts away the platform
|
||||
> graphics API. It automatically picks the best backend for your operating
|
||||
> system:
|
||||
> - **Windows:** DirectX 12 (primary), DirectX 11 (fallback), Vulkan
|
||||
> - **macOS:** Metal (primary and only supported backend)
|
||||
> - **Linux:** Vulkan (primary), OpenGL (fallback), DX12 (under Wine)
|
||||
>
|
||||
> `PowerPreference::HighPerformance` picks the discrete GPU on laptops
|
||||
> regardless of which backend wgpu selects.
|
||||
|
||||
### The State Struct
|
||||
|
||||
@@ -279,7 +339,7 @@ const VERTICES: &[Vertex] = &[
|
||||
impl State {
|
||||
async fn new(window: Arc<Window>) -> Result<Self, String> {
|
||||
// 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
|
||||
let surface = instance
|
||||
@@ -294,7 +354,7 @@ impl State {
|
||||
compatible_surface: None,
|
||||
})
|
||||
.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
|
||||
let (device, queue) = adapter
|
||||
@@ -347,12 +407,12 @@ impl State {
|
||||
attributes: &[
|
||||
wgpu::VertexAttribute {
|
||||
offset: 0,
|
||||
format: wgpu::VertexFormat::F32x3,
|
||||
format: wgpu::VertexFormat::Float32x3,
|
||||
shader_location: 0,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: std::mem::size_of::<[f32; 3]>() as u64,
|
||||
format: wgpu::VertexFormat::F32x3,
|
||||
format: wgpu::VertexFormat::Float32x3,
|
||||
shader_location: 1,
|
||||
},
|
||||
],
|
||||
@@ -364,7 +424,7 @@ impl State {
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader_module,
|
||||
entry_point: Some("vs_main"),
|
||||
buffers: &[vertex_buffer_layout],
|
||||
buffers: &[Some(&vertex_buffer_layout)],
|
||||
compilation_options: Default::default(),
|
||||
},
|
||||
primitive: wgpu::PrimitiveState {
|
||||
@@ -387,7 +447,7 @@ impl State {
|
||||
entry_point: Some("fs_main"),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: config.format,
|
||||
blend: None,
|
||||
blend: Some(wgpu::BlendState::REPLACE),
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
compilation_options: Default::default(),
|
||||
@@ -411,32 +471,34 @@ impl State {
|
||||
|
||||
### Init Steps Explained
|
||||
|
||||
**Step 1 — Instance:** `Instance::default()` opens a connection to the graphics
|
||||
driver on the current platform. On Linux with Vulkan, this loads `libvulkan.so`
|
||||
and creates a Vulkan `VkInstance`. On Windows, it loads `vulkan-1.dll`. The
|
||||
instance is the foundational wgpu object — every other wgpu operation requires
|
||||
it.
|
||||
**Step 1 — Instance:** `Instance::new()` opens a connection to the graphics
|
||||
driver on the current platform. wgpu automatically selects the best backend:
|
||||
Metal on macOS, DirectX 12 (or DX11/Vulkan) on Windows, and Vulkan (or OpenGL)
|
||||
on Linux. The instance is the foundational wgpu object — every other wgpu
|
||||
operation requires it.
|
||||
|
||||
**Step 2 — Surface:** `instance.create_surface(window)` binds the wgpu instance
|
||||
to the winit `Window`. This tells the GPU: "the pixels of *this* window will be
|
||||
the output of my rendering." In Vulkan terms, this is the first half of creating
|
||||
a `SwapchainKHR`. The surface must match the window platform type exactly (X11,
|
||||
Wayland, Windows, macOS, etc.).
|
||||
the output of my rendering." The surface must match the window platform type
|
||||
exactly (Windows, macOS, X11, Wayland, etc.), and wgpu handles this
|
||||
mapping automatically through winit.
|
||||
|
||||
**Step 3 — Adapter:** `request_adapter()` queries available GPUs and returns the
|
||||
best match for the given options. With
|
||||
`PowerPreference::HighPerformance`, wgpu prefers a discrete GPU over an
|
||||
integrated one on hybrid systems (e.g., NVIDIA + Intel Optimus). The
|
||||
`compatible_surface: None` path works because our `Instance` was created without
|
||||
a display handle; on Linux with Vulkan, the adapter selection remains correct
|
||||
because the surface itself was created through a compatible instance.
|
||||
integrated one on hybrid systems (e.g., NVIDIA + Intel Optimus on Windows,
|
||||
AMD Radeon + Intel Iris on macOS/Linux). This preference is cross-platform: it
|
||||
picks the high-performance GPU regardless of backend (Metal, DX12, or Vulkan).
|
||||
The `compatible_surface: None` path works because our `Instance` was created without
|
||||
a display handle; adapter selection remains correct across all backends.
|
||||
|
||||
**Step 4 — Device + Queue:** `request_device()` allocates the logical GPU
|
||||
resource manager and its submission queue. The device tracks all GPU memory and
|
||||
validates API calls. The queue is the submission endpoint — every rendered frame
|
||||
becomes a [command buffer](concepts/GLOSSARY.md#command-buffer) that is submitted
|
||||
to this queue. On Vulkan, the device corresponds to `VkDevice` and the queue
|
||||
to a `VkQueue`.
|
||||
to this queue. This maps to platform-native concepts: `ID3D12Device` + `ID3D12CommandQueue`
|
||||
on DX12, `MTLDevice` + `MTLCommandQueue` on Metal, and `VkDevice` + `VkQueue` on
|
||||
Vulkan.
|
||||
|
||||
> **Key insight — Validation layers catch GPU errors at runtime:** wgpu ships
|
||||
> with built-in validation layers that inspect your API calls for common
|
||||
@@ -503,7 +565,10 @@ runtime.
|
||||
|
||||
### The Complete Shader
|
||||
|
||||
Create `shader.wgsl` in your project root (at the same level as `main.rs`):
|
||||
Create the file `src/shader.wgsl` — in the same directory as `src/main.rs`, at the
|
||||
crate source root. The `include_str!` macro in the master `State::new()` block
|
||||
(S3, Step 6) expects the path `shader.wgsl` relative to the source file, so it
|
||||
must live alongside `main.rs`:
|
||||
|
||||
```wgsl
|
||||
struct VertexOutput {
|
||||
@@ -612,18 +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
|
||||
merge stage writes this color directly to the framebuffer.
|
||||
|
||||
### Rust Shader Module Creation
|
||||
### How the Shader Is Loaded (S3, Step 6)
|
||||
|
||||
The Rust side loads the shader file at compile time and feeds the source to wgpu:
|
||||
|
||||
```rust
|
||||
let shader_module = device.create_shader_module(
|
||||
wgpu::ShaderModuleDescriptor {
|
||||
label: Some("Rainbow Triangle Shader"),
|
||||
source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()),
|
||||
}
|
||||
);
|
||||
```
|
||||
The Rust side loads the shader file at compile time and feeds the source to wgpu.
|
||||
This code lives in the master `State::new()` block (S3, Step 6):
|
||||
|
||||
- **`ShaderModuleDescriptor`** — has two fields: `label` (debug string, shown
|
||||
in graphics debuggers and validation messages) and `source` (the shader
|
||||
@@ -654,30 +711,21 @@ strides the attribute begins.
|
||||
> reports "method not found." This is a Rust trait-discovery issue, not a wgpu
|
||||
> API issue. Add `use wgpu::util::DeviceExt;` to bring the method into scope.
|
||||
|
||||
### The Vertex Struct
|
||||
### The Vertex Struct (S3, Step 7)
|
||||
|
||||
```rust
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
struct Vertex {
|
||||
position: [f32; 3],
|
||||
color: [f32; 3],
|
||||
}
|
||||
```
|
||||
The `Vertex` struct is defined alongside the `VERTICES` constant at the top of
|
||||
`main.rs`, before `impl State`. This is the same struct used in the master
|
||||
initialization block (S3). The key annotations are:
|
||||
|
||||
> **WHY: `#[repr(C)]` + bytemuck for GPU data layout**
|
||||
>
|
||||
> `#[repr(C)]` forces the Rust compiler to lay out the struct fields in declaration order with no padding reordering. Without this, Rust is free to reorder fields for optimal alignment, which would break the byte layout the shader expects. `bytemuck::Pod` ("Plain Old Data") guarantees the struct has no padding holes, no destructors, and a trivial memory representation. wgpu requires all vertex types to be Pod so they can be safely transmuted to bytes. `bytemuck::Zeroable` guarantees that initializing the struct's memory to all-zero bytes produces a valid instance. Required because `Pod` alone does not guarantee zero is a valid discriminant for enums or optional types. Combined with Pod, it enables `bytemuck::cast_slice` to convert between `&[Vertex]` and `&[u8]` without an unsafe block.
|
||||
|
||||
### Vertex Data
|
||||
### Vertex Data (S3, Step 7)
|
||||
|
||||
```rust
|
||||
const VERTICES: &[Vertex] = &[
|
||||
Vertex { position: [-0.5, -0.5, 0.0], color: [1.0, 0.0, 0.0] }, // red
|
||||
Vertex { position: [ 0.5, -0.5, 0.0], color: [0.0, 0.0, 1.0] }, // blue
|
||||
Vertex { position: [ 0.0, 0.5, 0.0], color: [0.0, 1.0, 0.0] }, // green
|
||||
];
|
||||
```
|
||||
The `VERTICES` constant is defined at the top of `main.rs` alongside the
|
||||
`Vertex` struct. It is the same data used in the master initialization block
|
||||
(S3, Step 7).
|
||||
|
||||
- **Positions are in NDC:** The [normalized device coordinates](concepts/GLOSSARY.md#ndc)
|
||||
range from -1.0 (left/bottom) to +1.0 (right/top). Our triangle spans the
|
||||
@@ -690,18 +738,10 @@ const VERTICES: &[Vertex] = &[
|
||||
determines which face is "front" and which is "back" — critical for
|
||||
[culling](concepts/GLOSSARY.md) and correct normal computation.
|
||||
|
||||
### Buffer Upload
|
||||
### Buffer Upload (S3, Step 7)
|
||||
|
||||
```rust
|
||||
use wgpu::util::DeviceExt;
|
||||
let vertex_buffer = device.create_buffer_init(
|
||||
&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("Vertex Buffer"),
|
||||
contents: bytemuck::cast_slice(VERTICES),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
}
|
||||
);
|
||||
```
|
||||
This code lives in the master `State::new()` block (S3, Step 7). The
|
||||
`create_buffer_init` method is the combined allocate-and-upload call.
|
||||
|
||||
- **`use wgpu::util::DeviceExt`** — imports the extension trait that adds
|
||||
`create_buffer_init` to `Device`. Without this import, the method is not
|
||||
@@ -731,29 +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
|
||||
to create but cheap to execute.
|
||||
|
||||
### Vertex Buffer Layout
|
||||
### Vertex Buffer Layout (S3, Step 8)
|
||||
|
||||
Before the pipeline descriptor, you must tell wgpu how to parse the byte stream
|
||||
in the vertex buffer into per-vertex attributes:
|
||||
|
||||
```rust
|
||||
let vertex_buffer_layout = wgpu::VertexBufferLayout {
|
||||
array_stride: std::mem::size_of::<Vertex>() as u64,
|
||||
step_mode: wgpu::VertexStepMode::Vertex,
|
||||
attributes: &[
|
||||
wgpu::VertexAttribute {
|
||||
offset: 0,
|
||||
format: wgpu::VertexFormat::F32x3,
|
||||
shader_location: 0,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: std::mem::size_of::<[f32; 3]>() as u64,
|
||||
format: wgpu::VertexFormat::F32x3,
|
||||
shader_location: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
in the vertex buffer into per-vertex attributes. This layout is defined inside
|
||||
the master `State::new()` block (S3, Step 8):
|
||||
|
||||
- **`array_stride: 24`** — `size_of::<Vertex>()` = 24 bytes (6 × `f32` × 4 bytes).
|
||||
This is the byte distance from one vertex to the next in the buffer. The GPU
|
||||
@@ -763,7 +785,7 @@ let vertex_buffer_layout = wgpu::VertexBufferLayout {
|
||||
the vertex shader processes. The other option is `Instance`, which advances
|
||||
per draw instance in instanced rendering. For a single triangle, `Vertex` is
|
||||
correct: each of the three vertices has its own position and color.
|
||||
- **First attribute — `shader_location: 0`**: reads 3 floats (`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
|
||||
[shader location](concepts/GLOSSARY.md#shader-location) `@location(0)` in the
|
||||
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
|
||||
gradient colors derived from position data.
|
||||
|
||||
### The Complete Render Pipeline Descriptor
|
||||
### The Complete Render Pipeline Descriptor (S3, Step 8)
|
||||
|
||||
```rust
|
||||
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("Triangle Pipeline"),
|
||||
layout: None,
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader_module,
|
||||
entry_point: Some("vs_main"),
|
||||
buffers: &[vertex_buffer_layout],
|
||||
compilation_options: Default::default(),
|
||||
},
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
strip_index_format: None,
|
||||
front_face: wgpu::FrontFace::Ccw,
|
||||
cull_mode: Some(wgpu::Face::Back),
|
||||
unclipped_depth: false,
|
||||
polygon_mode: wgpu::PolygonMode::Fill,
|
||||
conservative: false,
|
||||
},
|
||||
depth_stencil: None,
|
||||
multisample: wgpu::MultisampleState {
|
||||
count: 1,
|
||||
mask: !0,
|
||||
alpha_to_coverage_enabled: false,
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader_module,
|
||||
entry_point: Some("fs_main"),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: config.format,
|
||||
blend: None,
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
compilation_options: Default::default(),
|
||||
}),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
});
|
||||
```
|
||||
The full `device.create_render_pipeline()` call with all descriptor fields
|
||||
lives in the master `State::new()` block (S3, Step 8). Below is the
|
||||
field-by-field walkthrough explaining every parameter.
|
||||
|
||||
### Field-by-Field Walkthrough
|
||||
|
||||
@@ -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
|
||||
the vertex shader entry point. Must match the `@vertex fn vs_main(...)`
|
||||
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
|
||||
instance buffers). For a single vertex buffer, one layout suffices.
|
||||
- **`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
|
||||
reads in this format. A mismatch at render time produces an error. If
|
||||
you change the surface format, you must recreate the pipeline.
|
||||
- **`blend: None`** — disables blending. Without blending, every fragment
|
||||
color replaces the existing framebuffer pixel (`REPLACE` mode). With
|
||||
blending, new and existing colors are combined according to a blend
|
||||
equation (useful for transparency).
|
||||
- **`blend: Some(wgpu::BlendState::REPLACE)`** — explicitly replaces every
|
||||
fragment color with the new output. `None` would default to this behavior,
|
||||
but we make it explicit for clarity. With a custom blend state, new and
|
||||
existing colors can be combined according to a blend equation
|
||||
(useful for transparency).
|
||||
- **`write_mask: ColorWrites::ALL`** — write all four RGBA channels.
|
||||
You can mask out individual channels (e.g., write only R and G) if you
|
||||
need to preserve certain framebuffer channels across draw calls.
|
||||
@@ -951,8 +938,8 @@ fn render(&mut self) {
|
||||
```
|
||||
|
||||
This is a **fully synchronous** method. It runs on the winit event loop thread
|
||||
(triggered by `RedrawRequested`), has no `async` keyword, no `.await`, and takes
|
||||
no tokio handle. All wgpu recording and submission operations are synchronous
|
||||
(triggered by `RedrawRequested`), has no `async` keyword, no `.await`, and requires
|
||||
no async runtime. All wgpu recording and submission operations are synchronous
|
||||
and fast — they only encode instructions and push them to the queue; they do not
|
||||
wait for GPU completion.
|
||||
|
||||
@@ -1045,12 +1032,7 @@ fn render(&mut self) {
|
||||
depth_slice: None,
|
||||
resolve_target: None,
|
||||
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,
|
||||
},
|
||||
})],
|
||||
@@ -1096,9 +1078,9 @@ not reclaim finished work. Called once per frame. Returns
|
||||
re-creating the device.
|
||||
|
||||
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
|
||||
work is done, then returns. On a busy GPU this can take a few milliseconds per
|
||||
frame — that is normal.
|
||||
runs a small internal loop checking platform-specific synchronization primitives
|
||||
(fences, events, etc.) until all in-flight work is done, then returns. On a
|
||||
busy GPU this can take a few milliseconds per frame — that is normal.
|
||||
|
||||
**`texture.create_view(&Default::default())`** — A [texture view](concepts/GLOSSARY.md#texture-view)
|
||||
is how wgpu references a texture's memory inside a render pass. The GPU does
|
||||
@@ -1124,7 +1106,7 @@ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
depth_slice: None,
|
||||
resolve_target: None,
|
||||
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,
|
||||
},
|
||||
})],
|
||||
@@ -1142,7 +1124,7 @@ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
- **`ops`** — [operations](concepts/GLOSSARY.md#operations) controlling load
|
||||
and store behavior. Two sub-fields:
|
||||
- **`load: LoadOp::Clear(color)`** — before drawing, fill the entire
|
||||
framebuffer with this color. **This IS your background color.** Dark gray.
|
||||
framebuffer with this color. **This IS your background color.** Black.
|
||||
`LoadOp::Load` keeps existing pixels (used in UI compositing where you
|
||||
draw on top of previous content).
|
||||
- **`store: StoreOp::Store`** — after drawing, keep what was written. The
|
||||
@@ -1257,7 +1239,7 @@ and returns the original vector. This avoids a heap allocation for what is
|
||||
typically a 1-element vec.
|
||||
|
||||
```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 {
|
||||
let config = wgpu::SurfaceConfiguration {
|
||||
usage: self.config.usage,
|
||||
@@ -1320,13 +1302,15 @@ The full source is the codeblocks in sections S2–S8, assembled in order into
|
||||
|
||||
```
|
||||
src/
|
||||
├── main.rs # Sections S2, S3, S5 (structs), S7 (render), S8 (resize)
|
||||
├── main.rs # Sections S2 (event loop), S3 (State + init chain), S7 (render), S8 (resize)
|
||||
├── shader.wgsl # Section S4 (the complete WGSL shader)
|
||||
```
|
||||
|
||||
- `main.rs` combines the winit event loop (S2), the init chain and `State`
|
||||
struct (S3), the `Vertex` type and `VERTICES` constant (S5), the `render`
|
||||
method (S7), and the `resize` method (S8).
|
||||
- `main.rs` combines the winit event loop (S2), the complete `State` struct
|
||||
definition and full `State::new()` implementation covering all 8 init steps
|
||||
(S3), the `render` method (S7), and the `resize` method (S8). Sections S5
|
||||
and S6 explain the vertex data and pipeline concepts but their code lives
|
||||
inside the master `State::new()` block in S3.
|
||||
- `shader.wgsl` is the single file from S4: vertex shader, fragment shader,
|
||||
and the `VertexOutput` struct.
|
||||
|
||||
@@ -1346,7 +1330,7 @@ cargo run
|
||||
module compilation log, pipeline creation messages, and the `simple_logger`
|
||||
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,
|
||||
blue at the bottom-right corner, green at the top vertex. Colors blend smoothly
|
||||
across the triangle surface via hardware interpolation.
|
||||
|
||||
+20
-1
@@ -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
|
||||
- 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
|
||||
|
||||
- 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)
|
||||
|
||||
+62
-23
@@ -10,7 +10,7 @@
|
||||
```toml
|
||||
wgpu = "29"
|
||||
winit = "0.30"
|
||||
tokio = { version = "1", features = ["rt", "macros"] }
|
||||
pollster = "0.4"
|
||||
bytemuck = { version = "1", features = ["derive"] }
|
||||
log = "0.4"
|
||||
simple_logger = "5"
|
||||
@@ -41,38 +41,77 @@ simple_logger = "5"
|
||||
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:**
|
||||
```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
|
||||
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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -104,7 +143,7 @@ fn exiting(&mut self, event_loop_ctl: &ActiveEventLoop) {
|
||||
- WGSL `@location(1)` matches Rust `shader_location: 1`
|
||||
- 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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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`
|
||||
- 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.
|
||||
|
||||
## Additional Resources
|
||||
|
||||
|
||||
@@ -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