Skip to main content

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}