Skip to main content

pebble/assets/
singleton_asset.rs

1//! Lazy resources — singleton GPU resources that are constructed once,
2//! on-demand, as soon as their backend and dependencies become available,
3//! but (unlike [`Asset`]) are never tracked by a [`Handle`] or stored in a
4//! pool, because there is only ever one of them.
5//!
6//! Use this for things like: a shared bind group layout, a camera's GPU
7//! buffer, a depth texture — anything that (a) needs a device/backend and
8//! possibly other resources to exist before it can be built, and (b) has
9//! exactly one instance for the whole app, accessed directly via `Res<T>`
10//! rather than through a handle.
11//!
12//! If you find yourself wanting more than one instance of something (e.g.
13//! multiple cameras, multiple independently-loaded textures), that's a
14//! sign you actually want [`Asset`] + [`Handle`], not [`LazyResource`].
15
16use crate::{
17    app::SystemStage,
18    assets::deps::Dependencies,
19    ecs::plugin::Plugin,
20    ecs::resources::Resources,
21    ecs::system::{Commands, Local, Res},
22};
23
24/// A resource that is constructed once, lazily, as soon as its device and
25/// dependencies are available — and never rebuilt or reconstructed after
26/// that (unless you explicitly remove it yourself).
27///
28/// Unlike [`Asset`], there is no `Source` and no `Handle` — a
29/// `LazyResource` has nothing authored to parse from; it's pure
30/// construction from a device plus whatever else it depends on.
31pub trait LazyResource<B>: 'static + Send + Sync + Sized {
32    /// Other resources this singleton needs before it can be built.
33    /// Use `()` if none are needed.
34    type Deps<'a>: Dependencies<'a>;
35
36    /// Attempt to construct this singleton. Return `None` if construction
37    /// can't succeed yet for a reason not already covered by `Deps`
38    /// readiness (e.g. a transient condition) — the plugin will retry
39    /// next tick.
40    fn construct<'a>(backend: &B, deps: &Self::Deps<'a>) -> Option<Self>;
41}
42
43/// Registers the system that lazily constructs `T` once `B` and `T::Deps`
44/// are available, and never again afterward.
45pub struct LazyResourcePlugin<B, T: LazyResource<B>> {
46    _marker: std::marker::PhantomData<(B, T)>,
47}
48
49impl<B, T: LazyResource<B>> LazyResourcePlugin<B, T> {
50    /// Create the plugin. See the type-level docs for what registering it does.
51    pub fn new() -> Self {
52        Self {
53            _marker: std::marker::PhantomData,
54        }
55    }
56}
57
58impl<B, T> Plugin for LazyResourcePlugin<B, T>
59where
60    B: 'static + Send + Sync,
61    T: LazyResource<B>,
62{
63    fn build(&self, app: &mut crate::prelude::App) {
64        // T is constructed lazily/asynchronously (see construct_resource
65        // below) — mark it so a system elsewhere with a hard `Res<T>`
66        // requirement waits quietly instead of App treating it as missing.
67        app.provides::<T>();
68        app.add_system(SystemStage::AssetSyncDeps, construct_resource::<B, T>);
69    }
70}
71
72/// Ticks a `LazyResource` can stay unconstructed before the plugin
73/// escalates from a quiet `trace!` to a `warn!`. See the identical
74/// `STUCK_AFTER_TICKS` in `assets/plugin.rs` — same reasoning, same value,
75/// kept as a separate constant since the two modules have no shared base to
76/// hang a common one off without adding coupling for a single `u32`.
77const STUCK_AFTER_TICKS: u32 = 300;
78
79fn construct_resource<B, T>(
80    mut commands: Commands,
81    backend: Option<Res<B>>,
82    existing: Option<Res<T>>,
83    mut waiting_ticks: Local<u32>,
84    world: &hecs::World,
85    resources: &Resources,
86) where
87    B: 'static + Send + Sync,
88    T: LazyResource<B>,
89{
90    // Already built — nothing to do, forever.
91    if existing.is_some() {
92        return;
93    }
94
95    let Some(backend) = backend else {
96        warn_if_stuck::<T>("waiting on backend to construct resource", &mut waiting_ticks);
97        return;
98    };
99
100    let Some(deps) = T::Deps::try_gather(world, resources) else {
101        warn_if_stuck::<T>("waiting on dependencies to construct resource", &mut waiting_ticks);
102        return;
103    };
104
105    match T::construct(&backend, &deps) {
106        Some(value) => {
107            commands.insert_resource(value);
108        }
109        None => {
110            warn_if_stuck::<T>("construct() returned None, will retry next tick", &mut waiting_ticks);
111        }
112    }
113}
114
115fn warn_if_stuck<T: 'static>(reason: &str, waiting_ticks: &mut u32) {
116    *waiting_ticks += 1;
117    if *waiting_ticks >= STUCK_AFTER_TICKS && waiting_ticks.is_multiple_of(STUCK_AFTER_TICKS) {
118        tracing::warn!(
119            "{}: still not constructed after {} ticks ({reason}) — if this is waiting on a \
120             resource that's never actually going to appear, it will wait forever.",
121            std::any::type_name::<T>(),
122            *waiting_ticks
123        );
124    } else {
125        tracing::trace!("{}: {reason}", std::any::type_name::<T>());
126    }
127}