Skip to main content

pebble/rendering/
graphics_plugin.rs

1use crate::{
2    ecs::system::OnceExt,
3    prelude::{
4        Backend, Commands, GPUSurfaceHandle, Plugin, PresentableWindow, Res, ResMut, SystemStage,
5        WindowResource,
6    },
7    rendering::{async_init::PendingBackend, sync::init_channel},
8};
9
10/// Plugin that initialises the GPU backend asynchronously and handles window
11/// resize events.
12///
13/// It calls [`Backend::init`] with the window handle and a one-shot sender
14/// once (see [`setup_gpu_async`]), then polls the receiver every
15/// [`PreRender`](SystemStage::PreRender) tick until the backend arrives. Once
16/// available it also forwards window size changes to [`Backend::resize`].
17pub struct GraphicsPlugin<B, W> {
18    _marker: std::marker::PhantomData<(B, W)>,
19}
20
21impl<B: Backend, W: PresentableWindow> GraphicsPlugin<B, W>
22where
23    W::Handle: GPUSurfaceHandle,
24{
25    pub fn new() -> Self {
26        Self {
27            _marker: std::marker::PhantomData,
28        }
29    }
30}
31
32impl<B: Backend, W: PresentableWindow> Plugin for GraphicsPlugin<B, W>
33where
34    W::Handle: GPUSurfaceHandle,
35{
36    fn build(&self, app: &mut crate::prelude::App) {
37        // B arrives asynchronously (see poll_backend_ready) — mark it so a
38        // system elsewhere with a hard `Res<B>` requirement waits quietly
39        // instead of App treating it as a missing/misconfigured resource.
40        app.provides::<B>();
41        app.add_system(SystemStage::PreUpdate, setup_gpu_async::<B, W>.once())
42            .add_system(SystemStage::PreRender, poll_backend_ready::<B>)
43            .add_system(SystemStage::PreRender, handle_resize_async::<B, W>);
44    }
45}
46
47struct LastWindowSize(u32, u32);
48
49/// Kicks off backend initialisation and stores the pending receiver.
50/// `.once()`-registered — `WindowResource<W>` already exists by the time this
51/// first runs (inserted synchronously by `WindowPlugin::build`), so this
52/// always succeeds on its first invocation and is never invoked again.
53fn setup_gpu_async<B: Backend, W>(mut commands: Commands, window: Res<WindowResource<W>>) -> Option<()>
54where
55    W: PresentableWindow,
56    W::Handle: GPUSurfaceHandle,
57{
58    let (w, h) = W::size(&window.handle);
59    let (sender, receiver) = init_channel::<B>();
60    B::init(window.handle.clone(), w, h, sender);
61    commands.insert_resource(PendingBackend::<B> {
62        receiver: std::sync::Mutex::new(receiver),
63    });
64    Some(())
65}
66
67/// PreRender system: poll the one-shot channel; promote the backend to a
68/// resource and remove the pending marker once it arrives.
69fn poll_backend_ready<B: Backend>(mut commands: Commands, pending: Option<Res<PendingBackend<B>>>) {
70    if let Some(p) = pending {
71        let mut guard = match p.receiver.lock() {
72            Ok(g) => g,
73            Err(poisoned) => poisoned.into_inner(),
74        };
75
76        if let Ok(backend) = guard.try_recv() {
77            commands.insert_resource(backend);
78            commands.remove_resource::<PendingBackend<B>>();
79        }
80    }
81}
82
83/// PreRender system: forward the current window size to the backend so it can
84/// recreate the swapchain when the window is resized.
85///
86/// `Backend::resize` reconfigures the surface, which is expensive (it drains
87/// the GPU queue and recreates the swapchain), so this only calls it when the
88/// size has actually changed rather than unconditionally every frame.
89fn handle_resize_async<B: Backend, W: PresentableWindow>(
90    mut commands: Commands,
91    backend: Option<ResMut<B>>,
92    window: Res<WindowResource<W>>,
93    last_size: Option<Res<LastWindowSize>>,
94) where
95    W::Handle: GPUSurfaceHandle,
96{
97    let Some(mut backend) = backend else { return };
98    let (w, h) = W::size(&window.handle);
99    if w == 0 || h == 0 {
100        return;
101    }
102
103    if let Some(last_size) = &last_size {
104        if last_size.0 == w && last_size.1 == h {
105            return;
106        }
107    }
108
109    backend.resize(w, h);
110    commands.insert_resource(LastWindowSize(w, h));
111}