unlit3dx/gpu.rs
1//! The GPU context and its asynchronous request.
2//!
3//! A scene is set up once the adapter and device exist, and the setup
4//! needs a mutable world — so the request is a future the world itself
5//! runs, driven by whoever drives the world, and the ready callback lands
6//! as a command between two event callbacks. Nothing outside this module
7//! needs to know any of that: [`request_gpu`] runs the request and calls
8//! back when the context is ready.
9
10use std::cell::RefCell;
11use std::rc::Rc;
12
13use unlit_ecs::{Command, Entity, EntityAccessError, World};
14
15use crate::executor::LocalTasks;
16use unlit_wgpu::capabilities::{DeviceCapabilities, DeviceTier};
17use unlit3d::frame::{FrameBehaviourOrder, OnFrame};
18use unlit3d::winit::builtin::{DisplayHandle, ExitRequest, Resumed, window};
19
20/// The frame-behaviour order the GPU request runs at: before the default
21/// band, so the GPU is there before any behaviour that states no order runs.
22pub(crate) const REQUEST_GPU_ORDER: i32 = -1;
23
24/// The GPU context a scene draws with.
25///
26/// Everything here is a wgpu handle, so the handles may cross into
27/// another thread's setup; the scene itself cannot cross threads and is
28/// set up where the request started. The native backends' handles are
29/// send-and-sync; the WebGPU backend's device holds an `Rc`, so on the
30/// web the handles only ever cross tasks, never threads.
31#[derive(Clone)]
32pub struct Gpu {
33 /// The instance presentation surfaces are created from.
34 pub instance: wgpu::Instance,
35 /// The adapter the device was requested from.
36 pub adapter: wgpu::Adapter,
37 /// The device every buffer and pipeline belongs to.
38 pub device: wgpu::Device,
39 /// The queue frames are submitted to.
40 pub queue: wgpu::Queue,
41 /// What the device can do beyond the WebGPU baseline, captured while
42 /// the adapter is still here to report it.
43 pub capabilities: DeviceCapabilities,
44}
45
46impl Gpu {
47 /// Request the adapter and device that can present to `surface`.
48 ///
49 /// The surface is a probe: it exists only so the adapter is required
50 /// to present to the window, and is dropped before the device is
51 /// requested.
52 pub async fn request(
53 instance: wgpu::Instance,
54 surface: wgpu::Surface<'static>,
55 ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
56 let adapter = request_adapter(&instance, Some(&surface)).await?;
57 // The surface only probes the adapter; the device request must not
58 // keep it alive.
59 drop(surface);
60 Self::from_adapter(instance, adapter).await
61 }
62
63 /// Request an adapter and device without a window to present to, for
64 /// headless renders.
65 pub async fn headless() -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
66 let instance =
67 wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle_from_env());
68 let adapter = request_adapter(&instance, None).await?;
69 Self::from_adapter(instance, adapter).await
70 }
71
72 /// Request a device from `adapter` and gather the context around it.
73 async fn from_adapter(
74 instance: wgpu::Instance,
75 adapter: wgpu::Adapter,
76 ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
77 log::debug!("adapter landed; requesting a device");
78 let tier = DeviceTier::from_env();
79 let capabilities = tier.capabilities_of(&adapter);
80 let (device, queue) = adapter
81 .request_device(&wgpu::DeviceDescriptor {
82 required_limits: tier.limits(&adapter.limits()),
83 ..Default::default()
84 })
85 .await?;
86 log::debug!("device landed");
87 Ok(Self {
88 instance,
89 adapter,
90 device,
91 queue,
92 capabilities,
93 })
94 }
95}
96
97/// Ask `instance` for a high-performance adapter that can present to
98/// `surface`, when one is given.
99async fn request_adapter(
100 instance: &wgpu::Instance,
101 surface: Option<&wgpu::Surface<'static>>,
102) -> Result<wgpu::Adapter, Box<dyn std::error::Error + Send + Sync>> {
103 log::debug!("requesting an adapter");
104 let adapter = instance
105 .request_adapter(&wgpu::RequestAdapterOptions {
106 power_preference: wgpu::PowerPreference::HighPerformance,
107 compatible_surface: surface,
108 ..Default::default()
109 })
110 .await?;
111 Ok(adapter)
112}
113
114/// Why a GPU request could not start the renderer.
115#[derive(Debug, thiserror::Error)]
116#[non_exhaustive]
117pub enum GpuRequestError {
118 /// The window surface could not be built from the display handle.
119 #[error("failed to build the presentation surface: {0}")]
120 Surface(#[from] wgpu::CreateSurfaceError),
121 /// The adapter or device request failed.
122 #[error("failed to request a GPU context: {0}")]
123 Request(#[source] Box<dyn std::error::Error + Send + Sync>),
124}
125
126/// The answer a GPU request settles on.
127enum GpuRequest {
128 /// The adapter and device are ready for the scene setup.
129 Ready(Gpu),
130 /// The request failed; the app is asked to exit instead.
131 Failed(GpuRequestError),
132}
133
134/// The stages a GPU request moves through, one per frame behaviour.
135#[derive(Clone, Copy, PartialEq, Eq)]
136enum RequestStage {
137 /// Waiting for a resumed window to build the request from.
138 WaitingForWindow,
139 /// The request future is running; its answer is read on a later frame.
140 Requesting,
141 /// The ready callback has been queued; nothing more to do.
142 Done,
143}
144
145/// Start the app's GPU request once the window exists, and run `ready`
146/// with the context once it lands.
147///
148/// The request runs as a task of the world it will set up, driven by the
149/// pump that opens each frame; this behaviour reads the answer on the frames
150/// that follow and runs the callback as a queued command with a mutable
151/// world, between two event callbacks. A failed request is logged and sets
152/// the app's [`ExitRequest`].
153pub fn request_gpu<F>(app: Entity, ready: F) -> OnFrame
154where
155 F: FnOnce(&mut World, &Gpu) + 'static,
156{
157 // The request future runs among the world's own tasks, so the answer
158 // slot needs no lock: the future and the behaviour that reads it run on
159 // the same thread, and the world's pump drives the future for us.
160 let answer: Rc<RefCell<Option<GpuRequest>>> = Rc::default();
161 let mut stage = RequestStage::WaitingForWindow;
162 let mut ready = Some(ready);
163 OnFrame::with_order(
164 FrameBehaviourOrder(REQUEST_GPU_ORDER),
165 move |world, _entity, _frame| {
166 if stage == RequestStage::Done {
167 return;
168 }
169 if stage == RequestStage::Requesting {
170 // The request future was driven by the pump that opened
171 // this frame, and a wake from the wgpu callback thread is
172 // queued for the next one.
173 // One poll takes the answer out for good, so it is moved
174 // into the command even if the callback panics half-way.
175 let Some(request) = answer.borrow_mut().take() else {
176 return;
177 };
178 stage = RequestStage::Done;
179 let Some(ready) = ready.take() else {
180 return;
181 };
182 world.root_scope().queue().push(RunGpuReady {
183 app,
184 request,
185 ready,
186 });
187 return;
188 }
189 let Some(window) = window(world) else {
190 return;
191 };
192 let mut resumed_rows = world.root_scope().query::<&Resumed>();
193
194 let resumed = resumed_rows.iter().next().is_some_and(|resumed| resumed.0);
195 if !resumed {
196 return;
197 }
198 let mut display_handle_rows = world.root_scope().query::<&DisplayHandle>();
199
200 let Some(display) = display_handle_rows
201 .iter()
202 .next()
203 .map(|display_handle| display_handle.0.clone())
204 else {
205 return;
206 };
207 stage = RequestStage::Requesting;
208 log::debug!("requesting a GPU context");
209 let slot = Rc::clone(&answer);
210 let spawned = LocalTasks::spawn(world, async move {
211 // The display handle is owned, not borrowed: the instance
212 // it builds has to outlive this callback.
213 let instance = wgpu::util::new_instance_with_webgpu_detection(
214 wgpu::InstanceDescriptor::new_with_display_handle_from_env(Box::new(display)),
215 )
216 .await;
217 let request = match instance.create_surface(window) {
218 Ok(probe) => match Gpu::request(instance, probe).await {
219 Ok(gpu) => GpuRequest::Ready(gpu),
220 Err(error) => GpuRequest::Failed(GpuRequestError::Request(error)),
221 },
222 Err(error) => GpuRequest::Failed(GpuRequestError::Surface(error)),
223 };
224 *slot.borrow_mut() = Some(request);
225 });
226 debug_assert!(spawned, "the frame's world carries its own tasks");
227 },
228 )
229}
230
231/// Run the GPU request's ready callback with a mutable world.
232struct RunGpuReady<F> {
233 /// The app entity the failure exit names.
234 app: Entity,
235 /// The request's answer.
236 request: GpuRequest,
237 /// The callback to run on success.
238 ready: F,
239}
240
241impl<F: FnOnce(&mut World, &Gpu) + 'static> Command for RunGpuReady<F> {
242 fn apply(self: Box<Self>, world: &mut World) -> Result<(), EntityAccessError> {
243 match self.request {
244 GpuRequest::Ready(gpu) => (self.ready)(world, &gpu),
245 GpuRequest::Failed(error) => {
246 log::error!("failed to start the renderer: {error}");
247 let _ = world
248 .root_scope()
249 .query_one::<&mut ExitRequest>(self.app)
250 .map(|mut request| {
251 let request = request.get();
252 request.0 = true
253 });
254 }
255 }
256 Ok(())
257 }
258}