docs: make guide cross-platform and improve error handling resilience
This commit is contained in:
@@ -23,9 +23,9 @@ 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.
|
||||
|
||||
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`.
|
||||
|
||||
@@ -71,6 +71,18 @@ log = "0.4"
|
||||
- `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
|
||||
@@ -113,12 +125,15 @@ impl ApplicationHandler<()> for App {
|
||||
event_loop_ctl.set_control_flow(ControlFlow::Poll);
|
||||
self.window = Some(window.clone());
|
||||
|
||||
self.state = Some(
|
||||
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(
|
||||
@@ -149,7 +164,14 @@ impl ApplicationHandler<()> for App {
|
||||
|
||||
> **WHY: `pollster::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. 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.
|
||||
> 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**
|
||||
>
|
||||
@@ -227,6 +249,16 @@ Instance
|
||||
allocates the swapchain [framebuffers](concepts/GLOSSARY.md#framebuffer) for
|
||||
this window at a specific resolution and pixel format.
|
||||
|
||||
> **Cross-platform GPU adapter selection:** wgpu abstracts away the platform
|
||||
> graphics API. It automatically picks the best backend for your operating
|
||||
> system:
|
||||
> - **Windows:** DirectX 12 (primary), DirectX 11 (fallback), Vulkan
|
||||
> - **macOS:** Metal (primary and only supported backend)
|
||||
> - **Linux:** Vulkan (primary), OpenGL (fallback), DX12 (under Wine)
|
||||
>
|
||||
> `PowerPreference::HighPerformance` picks the discrete GPU on laptops
|
||||
> regardless of which backend wgpu selects.
|
||||
|
||||
### The State Struct
|
||||
|
||||
```rust
|
||||
@@ -306,7 +338,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
|
||||
@@ -423,32 +455,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
|
||||
@@ -1028,9 +1062,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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user