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, 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 pub fn new() -> Self {
51 Self {
52 _marker: std::marker::PhantomData,
53 }
54 }
55}
56
57impl<B, T> Plugin for LazyResourcePlugin<B, T>
58where
59 B: 'static + Send + Sync,
60 T: LazyResource<B>,
61{
62 fn build(&self, app: &mut crate::prelude::App) {
63 // T is constructed lazily/asynchronously (see construct_resource
64 // below) — mark it so a system elsewhere with a hard `Res<T>`
65 // requirement waits quietly instead of App treating it as missing.
66 app.provides::<T>();
67 app.add_system(SystemStage::AssetSyncDeps, construct_resource::<B, T>);
68 }
69}
70
71fn construct_resource<B, T>(
72 mut commands: Commands,
73 backend: Option<Res<B>>,
74 existing: Option<Res<T>>,
75 world: &hecs::World,
76 resources: &Resources,
77) where
78 B: 'static + Send + Sync,
79 T: LazyResource<B>,
80{
81 // Already built — nothing to do, forever.
82 if existing.is_some() {
83 return;
84 }
85
86 let Some(backend) = backend else {
87 tracing::trace!(
88 "{}: waiting on backend to construct resource",
89 std::any::type_name::<T>()
90 );
91 return;
92 };
93
94 let Some(deps) = T::Deps::try_gather(world, resources) else {
95 tracing::trace!(
96 "{}: waiting on dependencies to construct resource",
97 std::any::type_name::<T>()
98 );
99 return;
100 };
101
102 match T::construct(&backend, &deps) {
103 Some(value) => {
104 commands.insert_resource(value);
105 }
106 None => {
107 tracing::trace!(
108 "{}: construct() returned None, will retry next tick",
109 std::any::type_name::<T>()
110 );
111 }
112 }
113}