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**
>
@@ -224,8 +246,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
@@ -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