docs: make guide cross-platform and improve error handling resilience

This commit is contained in:
2026-05-31 22:34:56 -05:00
parent 5a7cb22bf2
commit 13dc88aafb
3 changed files with 145 additions and 53 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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:**
- **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
sudo apt install libvulkan1 mesa-vulkan-drivers
# 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
```
## 5. Panic: "Surface lost"
## 5. "Adapter not compatible" (macOS / Metal)
**Symptom:** Program crashes with a surface lost error during rendering.
**Symptom:** wgpu reports that no adapter is compatible with the surface.
**Cause:** Display server restarted or GPU context was reset. The [Surface](concepts/GLOSSARY.md#surface) is permanently invalidated.
**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.
**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()`.
**Fix:** Verify your Mac supports Metal. Check `System Information > Graphics`
in macOS. If Metal is not supported, the GPU cannot be used with wgpu.
## 6. Wayland surface crashes or doesn't render
## 6. "Surface lost"
**Symptom:** Program crashes or shows blank window on Wayland.
**Symptom:** Program crashes with a surface lost error during rendering, or the
window renders blanks after being minimized/restored.
**Cause:** winit's Wayland backend may have compatibility issues with certain compositors.
**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:** Force X11 backend:
**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