pebble-engine 0.27.0

A modular, ECS-style graphics/app framework for Rust.
Documentation
use crate::{
    prelude::{Backend, Commands, GPUSurfaceHandle, Plugin, PresentableWindow, Res, ResMut, SystemStage, WindowResource},
    rendering::sync::init_channel,
};

/// Plugin that initialises the GPU backend and handles window resize events.
///
/// **Native**: [`Backend::init`] is called synchronously during
/// [`App::build`](crate::app::App::build) — before
/// [`App::run`](crate::app::App::run) ever starts the update loop — and the
/// finished backend is inserted as a resource right there. This relies on
/// `Backend::init` blocking the calling thread until it has a value to send
/// (native `WGPUBackend` does this via `pollster::block_on`).
///
/// **Web** (`wasm32`) can't block its single thread on an async device/adapter
/// request, so instead this registers an [`App::set_ready_gate`] that polls
/// the init channel once per tick — no busy loop, no blocking, just "not yet"
/// until the backend arrives — during which every [`SystemStage`] sits idle.
///
/// Either way, by the time any stage actually runs, the backend is already a
/// resource: every system, on every stage, from its very first tick onward,
/// can take `B` as a plain `Res<B>`/`ResMut<B>` — no `Option<Res<B>>` guard or
/// `.run_if::<ResourceExists<B>>()` needed anywhere for backend readiness.
pub struct GraphicsPlugin<B, W> {
    _marker: std::marker::PhantomData<(B, W)>,
}

impl<B: Backend, W: PresentableWindow> GraphicsPlugin<B, W>
where
    W::Handle: GPUSurfaceHandle,
{
    pub fn new() -> Self {
        Self {
            _marker: std::marker::PhantomData,
        }
    }
}

impl<B: Backend, W: PresentableWindow> Plugin for GraphicsPlugin<B, W>
where
    W::Handle: GPUSurfaceHandle,
{
    #[cfg(not(target_arch = "wasm32"))]
    fn build(&self, app: &mut crate::prelude::App) {
        let (handle, w, h) = {
            let window = app.get_resource::<WindowResource<W>>();
            let (w, h) = W::size(&window.handle);
            (window.handle.clone(), w, h)
        };

        let (sender, receiver) = init_channel::<B>();
        B::init(handle, w, h, sender);
        match receiver.recv() {
            Ok(backend) => {
                app.add_resource(backend);
            }
            Err(_) => {
                tracing::error!(
                    "GPU backend init sender was dropped without ever sending a value — the app \
                     has no usable backend; every backend-dependent system will panic on its first \
                     Res<B>/ResMut<B> fetch"
                );
            }
        }

        app.add_system(SystemStage::PreRender, handle_resize_async::<B, W>);
    }

    /// No `Startup`/`PreRender` polling systems here — a system only runs
    /// once its stage does, and every stage sits idle for as long as the
    /// [`App::set_ready_gate`] below reports "not yet". Polling the init
    /// channel from inside the gate itself is what keeps this waiting period
    /// non-blocking without needing the ECS scheduler to be running at all.
    #[cfg(target_arch = "wasm32")]
    fn build(&self, app: &mut crate::prelude::App) {
        let (handle, w, h) = {
            let window = app.get_resource::<WindowResource<W>>();
            let (w, h) = W::size(&window.handle);
            (window.handle.clone(), w, h)
        };

        let (sender, receiver) = init_channel::<B>();
        B::init(handle, w, h, sender);

        let mut receiver = Some(receiver);
        app.set_ready_gate(move |world, resources| {
            let Some(recv) = receiver.as_mut() else { return false };
            match recv.try_recv() {
                Ok(backend) => {
                    resources.insert_resource(world, backend);
                    receiver = None;
                    true
                }
                Err(oneshot::TryRecvError::Empty) => false,
                Err(oneshot::TryRecvError::Disconnected) => {
                    tracing::error!(
                        "GPU backend init sender was dropped without ever sending a value — the \
                         app has no usable backend and will stay idle forever"
                    );
                    receiver = None;
                    false
                }
            }
        });

        app.add_system(SystemStage::PreRender, handle_resize_async::<B, W>);
    }
}

struct LastWindowSize(u32, u32);

/// PreRender system: forward the current window size to the backend so it can
/// recreate the swapchain when the window is resized.
///
/// `Backend::resize` reconfigures the surface, which is expensive (it drains
/// the GPU queue and recreates the swapchain), so this only calls it when the
/// size has actually changed rather than unconditionally every frame.
fn handle_resize_async<B: Backend, W: PresentableWindow>(
    mut commands: Commands,
    backend: Option<ResMut<B>>,
    window: Res<WindowResource<W>>,
    last_size: Option<Res<LastWindowSize>>,
) where
    W::Handle: GPUSurfaceHandle,
{
    let Some(mut backend) = backend else { return };
    let (w, h) = W::size(&window.handle);
    if w == 0 || h == 0 {
        return;
    }

    if let Some(last_size) = &last_size {
        if last_size.0 == w && last_size.1 == h {
            return;
        }
    }

    backend.resize(w, h);
    commands.insert_resource(LastWindowSize(w, h));
}