Skip to main content

pebble/rendering/
graphics_plugin.rs

1use crate::{
2    prelude::{Backend, Commands, GPUSurfaceHandle, Plugin, PresentableWindow, Res, ResMut, SystemStage, WindowResource},
3    rendering::sync::init_channel,
4};
5
6/// Plugin that initialises the GPU backend and handles window resize events.
7///
8/// **Native**: [`Backend::init`] is called synchronously during
9/// [`App::build`](crate::app::App::build) — before
10/// [`App::run`](crate::app::App::run) ever starts the update loop — and the
11/// finished backend is inserted as a resource right there. This relies on
12/// `Backend::init` blocking the calling thread until it has a value to send
13/// (native `WGPUBackend` does this via `pollster::block_on`).
14///
15/// **Web** (`wasm32`) can't block its single thread on an async device/adapter
16/// request, so instead this registers an [`App::set_ready_gate`] that polls
17/// the init channel once per tick — no busy loop, no blocking, just "not yet"
18/// until the backend arrives — during which every [`SystemStage`] sits idle.
19///
20/// Either way, by the time any stage actually runs, the backend is already a
21/// resource: every system, on every stage, from its very first tick onward,
22/// can take `B` as a plain `Res<B>`/`ResMut<B>` — no `Option<Res<B>>` guard or
23/// `.run_if::<ResourceExists<B>>()` needed anywhere for backend readiness.
24pub struct GraphicsPlugin<B, W> {
25    _marker: std::marker::PhantomData<(B, W)>,
26}
27
28impl<B: Backend, W: PresentableWindow> GraphicsPlugin<B, W>
29where
30    W::Handle: GPUSurfaceHandle,
31{
32    pub fn new() -> Self {
33        Self {
34            _marker: std::marker::PhantomData,
35        }
36    }
37}
38
39impl<B: Backend, W: PresentableWindow> Plugin for GraphicsPlugin<B, W>
40where
41    W::Handle: GPUSurfaceHandle,
42{
43    #[cfg(not(target_arch = "wasm32"))]
44    fn build(&self, app: &mut crate::prelude::App) {
45        let (handle, w, h) = {
46            let window = app.get_resource::<WindowResource<W>>();
47            let (w, h) = W::size(&window.handle);
48            (window.handle.clone(), w, h)
49        };
50
51        let (sender, receiver) = init_channel::<B>();
52        B::init(handle, w, h, sender);
53        match receiver.recv() {
54            Ok(backend) => {
55                app.add_resource(backend);
56            }
57            Err(_) => {
58                tracing::error!(
59                    "GPU backend init sender was dropped without ever sending a value — the app \
60                     has no usable backend; every backend-dependent system will panic on its first \
61                     Res<B>/ResMut<B> fetch"
62                );
63            }
64        }
65
66        app.add_system(SystemStage::PreRender, handle_resize_async::<B, W>);
67    }
68
69    /// No `Startup`/`PreRender` polling systems here — a system only runs
70    /// once its stage does, and every stage sits idle for as long as the
71    /// [`App::set_ready_gate`] below reports "not yet". Polling the init
72    /// channel from inside the gate itself is what keeps this waiting period
73    /// non-blocking without needing the ECS scheduler to be running at all.
74    #[cfg(target_arch = "wasm32")]
75    fn build(&self, app: &mut crate::prelude::App) {
76        let (handle, w, h) = {
77            let window = app.get_resource::<WindowResource<W>>();
78            let (w, h) = W::size(&window.handle);
79            (window.handle.clone(), w, h)
80        };
81
82        let (sender, receiver) = init_channel::<B>();
83        B::init(handle, w, h, sender);
84
85        let mut receiver = Some(receiver);
86        app.set_ready_gate(move |world, resources| {
87            let Some(recv) = receiver.as_mut() else { return false };
88            match recv.try_recv() {
89                Ok(backend) => {
90                    resources.insert_resource(world, backend);
91                    receiver = None;
92                    true
93                }
94                Err(oneshot::TryRecvError::Empty) => false,
95                Err(oneshot::TryRecvError::Disconnected) => {
96                    tracing::error!(
97                        "GPU backend init sender was dropped without ever sending a value — the \
98                         app has no usable backend and will stay idle forever"
99                    );
100                    receiver = None;
101                    false
102                }
103            }
104        });
105
106        app.add_system(SystemStage::PreRender, handle_resize_async::<B, W>);
107    }
108}
109
110struct LastWindowSize(u32, u32);
111
112/// PreRender system: forward the current window size to the backend so it can
113/// recreate the swapchain when the window is resized.
114///
115/// `Backend::resize` reconfigures the surface, which is expensive (it drains
116/// the GPU queue and recreates the swapchain), so this only calls it when the
117/// size has actually changed rather than unconditionally every frame.
118fn handle_resize_async<B: Backend, W: PresentableWindow>(
119    mut commands: Commands,
120    backend: Option<ResMut<B>>,
121    window: Res<WindowResource<W>>,
122    last_size: Option<Res<LastWindowSize>>,
123) where
124    W::Handle: GPUSurfaceHandle,
125{
126    let Some(mut backend) = backend else { return };
127    let (w, h) = W::size(&window.handle);
128    if w == 0 || h == 0 {
129        return;
130    }
131
132    if let Some(last_size) = &last_size {
133        if last_size.0 == w && last_size.1 == h {
134            return;
135        }
136    }
137
138    backend.resize(w, h);
139    commands.insert_resource(LastWindowSize(w, h));
140}