floem_renderer/gpu_resources.rs
1//! Asynchronous GPU resource acquisition for rendering with wgpu.
2//!
3//! To support WebGPU on WASM, the GPU resources need to be acquired asynchronously because
4//! the wgpu library provides only asynchronous methods for requesting adapters and devices.
5//! In WASM, blocking the main thread is not an option, as the JavaScript
6//! execution model does not support thread blocking. Consequently, we must use asynchronous
7//! execution (via `wasm_bindgen_futures`) to handle these operations.
8//!
9//! Based on a [code snippet by Luke Petherbridge](https://github.com/rust-windowing/winit/issues/3560#issuecomment-2085754164).
10
11use std::{future::Future, sync::Arc};
12
13#[cfg(feature = "crossbeam")]
14use crossbeam::channel::{bounded as sync_channel, Receiver};
15#[cfg(not(feature = "crossbeam"))]
16use std::sync::mpsc::{sync_channel, Receiver};
17use wgpu::{Backends, InstanceFlags};
18
19use winit::window::{Window, WindowId};
20
21/// The acquired GPU resources needed for rendering with wgpu.
22#[derive(Debug, Clone)]
23pub struct GpuResources {
24 /// The wgpu instance
25 pub instance: wgpu::Instance,
26
27 /// The adapter that represents the GPU or a rendering backend. It provides information about
28 /// the capabilities of the hardware and is used to request a logical device (`wgpu::Device`).
29 pub adapter: wgpu::Adapter,
30
31 /// The logical device that serves as an interface to the GPU. It is responsible for creating
32 /// resources such as buffers, textures, and pipelines, and manages the execution of commands.
33 /// The `device` provides a connection to the physical hardware represented by the `adapter`.
34 pub device: wgpu::Device,
35
36 /// The command queue that manages the submission of command buffers to the GPU for execution.
37 /// It is used to send rendering and computation commands to the device. The `queue` ensures
38 /// that commands are executed in the correct order and manages synchronization.
39 pub queue: wgpu::Queue,
40}
41
42impl GpuResources {
43 /// Request GPU resources
44 ///
45 /// # Parameters
46 /// - `on_result`: Function to notify upon completion or error.
47 /// - `window`: The window to associate with the created surface.
48 pub fn request<F: Fn(WindowId) + 'static>(
49 on_result: F,
50 required_features: wgpu::Features,
51 window: Arc<dyn Window>,
52 ) -> Receiver<Result<(Self, wgpu::Surface<'static>), GpuResourceError>> {
53 let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
54 backends: Backends::from_env().unwrap_or(Backends::all()),
55 flags: InstanceFlags::from_env_or_default(),
56 ..Default::default()
57 });
58 // Channel passing to do async out-of-band within the winit event_loop since wasm can't
59 // execute futures with a return value
60 let (tx, rx) = sync_channel(1);
61
62 spawn({
63 async move {
64 let surface = match instance.create_surface(Arc::clone(&window)) {
65 Ok(surface) => surface,
66 Err(err) => {
67 tx.send(Err(GpuResourceError::SurfaceCreationError(err)))
68 .unwrap();
69 on_result(window.id());
70 return;
71 }
72 };
73
74 let Ok(adapter) = instance
75 .request_adapter(&wgpu::RequestAdapterOptions {
76 power_preference: wgpu::PowerPreference::default(),
77 compatible_surface: Some(&surface),
78 force_fallback_adapter: false,
79 })
80 .await
81 else {
82 tx.send(Err(GpuResourceError::AdapterNotFoundError))
83 .unwrap();
84 on_result(window.id());
85 return;
86 };
87
88 tx.send(
89 adapter
90 .request_device(&wgpu::DeviceDescriptor {
91 label: None,
92 required_features,
93 ..Default::default()
94 })
95 .await
96 .map_err(GpuResourceError::DeviceRequestError)
97 .map(|(device, queue)| Self {
98 adapter,
99 device,
100 queue,
101 instance,
102 })
103 .map(|res| (res, surface)),
104 )
105 .unwrap();
106 on_result(window.id());
107 }
108 });
109 rx
110 }
111}
112
113/// Possible errors during GPU resource setup.
114#[derive(Debug)]
115pub enum GpuResourceError {
116 SurfaceCreationError(wgpu::CreateSurfaceError),
117 AdapterNotFoundError,
118 DeviceRequestError(wgpu::RequestDeviceError),
119}
120
121impl std::fmt::Display for GpuResourceError {
122 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123 match self {
124 GpuResourceError::SurfaceCreationError(err) => {
125 write!(f, "Surface creation error: {err}")
126 }
127 GpuResourceError::AdapterNotFoundError => {
128 write!(f, "Failed to find a suitable GPU adapter")
129 }
130 GpuResourceError::DeviceRequestError(err) => write!(f, "Device request error: {err}"),
131 }
132 }
133}
134
135/// Spawns a future for execution, adapting to the target environment.
136///
137/// On WASM (`wasm32`), it uses `wasm_bindgen_futures::spawn_local` to avoid blocking
138/// the main thread. On other targets, it uses `pollster::block_on` to synchronously
139/// wait for the future to complete.
140pub fn spawn<F>(future: F)
141where
142 F: Future<Output = ()> + 'static,
143{
144 #[cfg(target_arch = "wasm32")]
145 wasm_bindgen_futures::spawn_local(future);
146 #[cfg(not(target_arch = "wasm32"))]
147 futures::executor::block_on(future)
148}