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**
|
||||
>
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user