Skip to main content

pebble/
app.rs

1use crate::{
2    assets::required::RequiredResources,
3    ecs::{
4        plugin::Plugin,
5        resources::Resources,
6        system::{IntoSystem, System},
7        system_set::IntoSystemSet,
8    },
9};
10use std::collections::BTreeMap;
11
12/// Determines when during a frame a system is executed.
13///
14/// There's no dedicated "run once at startup" stage — instead, any system on
15/// any stage can be made to run at most once with [`.once()`](crate::ecs::system::OnceExt::once),
16/// which turns "have I already done this" into the function's own return
17/// value (`Some(())` = done, retire; `None` = not ready, try again next
18/// tick) instead of a special stage with its own rules. A `.once()` system
19/// naturally waits as many ticks as it needs to (an async GPU backend, a
20/// `LazyResource` that isn't built yet) using the exact same requirement
21/// checks as every other system on its stage.
22///
23/// [`AssetSync`](SystemStage::AssetSync)/[`AssetSyncDeps`](SystemStage::AssetSyncDeps)
24/// are prioritized: they're re-run to convergence (repeated until a full
25/// pass produces no new resources) at the front of every tick and again
26/// after every other stage, so newly queued asset/resource work is drained
27/// before gameplay stages continue rather than waiting for the next tick's
28/// front pass. All other stages run once per [`App::update`] tick, in the
29/// order declared below.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
31pub enum SystemStage {
32    /// Before the main update.
33    PreUpdate,
34    /// Main game-logic update.
35    Update,
36    /// After the main update.
37    PostUpdate,
38    /// Prepare rendering data and poll for the GPU backend.
39    /// The backend resource becomes available here on the tick it finishes
40    /// initialising, making it visible to the asset sync stages.
41    PreRender,
42    /// Upload CPU-side source assets to the GPU backend.
43    AssetSync,
44    /// Construct lazy GPU resources and upload assets that depend on other
45    /// processed assets. Runs in a convergence loop so dependency chains
46    /// (e.g. LazyResource A → LazyResource B) resolve within a single tick.
47    AssetSyncDeps,
48    /// Issue draw calls.
49    Render,
50    /// Cleanup or post-processing after rendering.
51    PostRender,
52}
53
54impl SystemStage {
55    /// Returns `true` for stages that are prioritized and re-run until a
56    /// full pass produces no new resources, instead of running once in
57    /// their declared position in the tick order. See the type-level docs
58    /// on [`SystemStage`].
59    pub fn is_convergent(self) -> bool {
60        matches!(self, Self::AssetSync | Self::AssetSyncDeps)
61    }
62}
63
64/// Fixed per-tick order for every stage *except* the convergent ones
65/// (`AssetSync`, `AssetSyncDeps`), which are driven separately by
66/// [`App::reconverge`] — at the front of the tick and again after each of
67/// these — rather than appearing in this list.
68const TICK_STAGES: [SystemStage; 6] = [
69    SystemStage::PreUpdate,
70    SystemStage::Update,
71    SystemStage::PostUpdate,
72    SystemStage::PreRender,
73    SystemStage::Render,
74    SystemStage::PostRender,
75];
76
77/// Whether a system is safe to run right now, given its declared
78/// [`System::requires`]. See [`App::check_readiness`].
79enum Readiness {
80    /// No unmet requirement — go ahead and run it.
81    Ready,
82    /// Missing a resource that some plugin has declared (via
83    /// [`RequiredResources::provides`]) it eventually provides — wait
84    /// quietly, no error, and try again next pass/tick.
85    WaitingOnLazy,
86    /// Missing a resource nothing has ever declared it will provide —
87    /// almost certainly a genuine oversight, not a timing issue.
88    MissingUnprovided {
89        system: &'static str,
90        resource: &'static str,
91    },
92}
93
94/// Callback used to drive the application's main loop.
95///
96/// Set with [`App::set_runner`]. The default runner calls [`App::update`] in
97/// an infinite loop.
98pub type AppRunner = Box<dyn FnOnce(App)>;
99
100/// The central application object.
101///
102/// `App` owns the ECS world, resources, plugins, and systems. The typical
103/// lifecycle is:
104///
105/// 1. Create with [`App::new`].
106/// 2. Register plugins with [`add_plugin`](App::add_plugin).
107/// 3. Call [`build`](App::build) to run all plugin registrations, execute
108///    validate required resources, and settle `AssetSync`/`AssetSyncDeps`
109///    as far as they can go synchronously.
110/// 4. Call [`run`](App::run) to hand control to the runner.
111pub struct App {
112    pub(crate) world: hecs::World,
113    pub(crate) resources: Resources,
114    plugins: Vec<Box<dyn Plugin>>,
115    systems: BTreeMap<SystemStage, Vec<Box<dyn System>>>,
116    runner: Option<AppRunner>,
117    pub(crate) required: RequiredResources,
118}
119
120impl Default for App {
121    fn default() -> Self {
122        Self::new()
123    }
124}
125
126impl App {
127    /// Create a new `App` with an empty world and a default infinite-loop runner.
128    pub fn new() -> Self {
129        let mut world = hecs::World::default();
130        let mut resources = Resources::new(&mut world);
131        resources.insert_resource(&mut world, ());
132
133        Self {
134            world: world,
135            resources: resources,
136            plugins: Vec::new(),
137            systems: BTreeMap::new(),
138            runner: Some(Box::new(|mut app| {
139                loop {
140                    app.update();
141                }
142            })),
143            required: RequiredResources::new(),
144        }
145    }
146
147    /// Check `system` against `required` without running it. See
148    /// [`Readiness`]. Used by [`run_stage_once`](App::run_stage_once) for
149    /// every stage.
150    ///
151    /// A free function (rather than a `&self` method) so it only borrows
152    /// `world`/`resources`/`required` — the specific fields still available
153    /// while a caller holds a `&mut` borrow of `self.systems` to iterate the
154    /// very system being checked.
155    fn check_readiness(
156        world: &hecs::World,
157        resources: &Resources,
158        required: &RequiredResources,
159        system: &dyn System,
160    ) -> Readiness {
161        for req in system.requires() {
162            if (req.present)(world, resources) {
163                continue;
164            }
165            if required.is_provided(req.type_id) {
166                return Readiness::WaitingOnLazy;
167            }
168            return Readiness::MissingUnprovided {
169                system: system.name(),
170                resource: req.name,
171            };
172        }
173        Readiness::Ready
174    }
175
176    /// Panic with a message naming both the offending system and resource,
177    /// and pointing at the fix: either insert the resource before this
178    /// stage runs, or — if it legitimately does arrive later (an async
179    /// backend, a lazily-constructed resource) — register it with
180    /// `app.required.provides::<T>()` in the plugin that inserts it, so
181    /// consumers wait instead of erroring.
182    fn panic_missing_unprovided(stage: SystemStage, system: &'static str, resource: &'static str) -> ! {
183        panic!(
184            "{stage:?}: system `{system}` requires `{resource}`, which nothing has \
185             registered as provided.\n\n\
186             If `{resource}` genuinely arrives later (an async backend, a LazyResource, \
187             an Asset upload), call `app.required.provides::<{resource}>()` in whichever \
188             plugin inserts it, and this will wait instead of erroring. Otherwise, insert \
189             it via App::add_resource before this stage runs."
190        );
191    }
192
193    /// Run every system in `stage` once, flush the command buffer, and return
194    /// `true` if any resource was newly inserted during this pass.
195    ///
196    /// A system with an unmet hard [`Res`](crate::ecs::system::Res)/[`ResMut`](crate::ecs::system::ResMut)
197    /// requirement is skipped for this pass if the resource is registered as
198    /// [provided](RequiredResources::provides) somewhere (it'll get there —
199    /// just not yet), or panics immediately, naming the system and resource,
200    /// if nothing ever declared it would provide that resource at all.
201    ///
202    /// [`Commands::insert_resource`](crate::ecs::system::Commands::insert_resource)
203    /// bumps the generation counter at queue time, so both direct inserts and
204    /// deferred command-buffer inserts are detected here with no world
205    /// introspection needed after the flush.
206    fn run_stage_once(&mut self, stage: SystemStage) -> bool {
207        let gen_before = self.resources.generation();
208
209        if let Some(systems) = self.systems.get_mut(&stage) {
210            for system in systems.iter_mut() {
211                match Self::check_readiness(&self.world, &self.resources, &self.required, system.as_ref()) {
212                    Readiness::Ready => {}
213                    Readiness::WaitingOnLazy => continue,
214                    Readiness::MissingUnprovided { system, resource } => {
215                        Self::panic_missing_unprovided(stage, system, resource)
216                    }
217                }
218                let _guard = crate::ecs::resources::set_current_system(system.name());
219                system.run(&self.world, &self.resources);
220            }
221        }
222        self.resources.get_command_buffer().run_on(&mut self.world);
223
224        self.resources.generation() != gen_before
225    }
226
227    /// Run `AssetSync`, then `AssetSyncDeps`, repeating both until a full
228    /// pass produces no new resources, up to `max_passes`. Logs a warning if
229    /// the limit is reached — that usually means a [`LazyResource`](crate::assets::singleton_asset::LazyResource)
230    /// whose `construct()` or an [`Asset`](crate::assets::upload::Asset)
231    /// whose `upload()` always returns `None`.
232    ///
233    /// Called at the front of every tick and again after every stage in
234    /// [`update`](App::update) (and once during [`build`](App::build)), so
235    /// newly-queued asset/resource work is drained immediately instead of
236    /// waiting for the next tick's front pass.
237    fn reconverge(&mut self, max_passes: u32) {
238        for pass in 0..max_passes {
239            let gen_before = self.resources.generation();
240
241            self.run_stage_once(SystemStage::AssetSync);
242            self.run_stage_once(SystemStage::AssetSyncDeps);
243
244            if self.resources.generation() == gen_before {
245                return;
246            }
247            if pass == max_passes - 1 {
248                tracing::warn!(
249                    "AssetSync/AssetSyncDeps did not settle after {max_passes} passes — a \
250                     dependency may be permanently unsatisfiable. Check for a LazyResource \
251                     whose construct() or an Asset whose upload() always returns None."
252                );
253            }
254        }
255    }
256
257    /// Queue a plugin to be built during [`build`](App::build).
258    pub fn add_plugin(&mut self, plugin: impl Plugin) -> &mut Self {
259        self.plugins.push(Box::new(plugin));
260        self
261    }
262
263    /// Insert a resource into the world immediately.
264    pub fn add_resource(&mut self, res: impl hecs::Component) -> &mut Self {
265        self.resources.insert_resource(&mut self.world, res);
266        self
267    }
268
269    /// Borrow resource `T`, panicking if it is absent.
270    pub fn get_resource<'a, T: hecs::Component>(&'a self) -> hecs::Ref<'a, T> {
271        self.resources.get_resource(&self.world)
272    }
273
274    /// Mutably borrow resource `T`, panicking if it is absent.
275    pub fn get_resource_mut<'a, T: hecs::Component>(&'a self) -> hecs::RefMut<'a, T> {
276        self.resources.get_resource_mut(&self.world)
277    }
278
279    /// Insert resource `T` only if it is not already present.
280    ///
281    /// Returns `true` if the resource was inserted.
282    pub fn try_insert_resource<T: hecs::Component>(&mut self, res: T) -> bool {
283        self.resources.try_insert(&mut self.world, res)
284    }
285
286    /// Declare that resource type `T` is expected to be inserted later —
287    /// possibly asynchronously (a background thread's result, a hand-rolled
288    /// lazy resource) rather than up front. A system elsewhere with a hard
289    /// `Res<T>`/`ResMut<T>` requirement on `T` will then wait quietly for it
290    /// instead of `App` treating the absence as a configuration mistake and
291    /// panicking.
292    ///
293    /// [`GraphicsPlugin`](crate::rendering::graphics_plugin::GraphicsPlugin)
294    /// and [`LazyResourcePlugin`](crate::assets::singleton_asset::LazyResourcePlugin)
295    /// already call this for the backend and lazy resource types they
296    /// manage — reach for this directly only for your own resource types
297    /// that arrive outside of those.
298    pub fn provides<T: 'static>(&mut self) -> &mut Self {
299        self.required.provides::<T>();
300        self
301    }
302
303    /// Register a single system to run at `stage`.
304    pub fn add_system<Marker>(
305        &mut self,
306        stage: SystemStage,
307        system: impl IntoSystem<Marker> + 'static,
308    ) -> &mut Self {
309        self.systems
310            .entry(stage)
311            .or_default()
312            .push(Box::new(system.into_system()));
313        self
314    }
315
316    /// Register multiple systems to run at `stage`.
317    ///
318    /// Accepts a tuple of systems via [`IntoSystemSet`].
319    pub fn add_systems<Marker>(
320        &mut self,
321        stage: SystemStage,
322        systems: impl IntoSystemSet<Marker>,
323    ) -> &mut Self {
324        let entry = self.systems.entry(stage).or_default();
325        entry.extend(systems.into_system_set());
326        self
327    }
328
329    /// Build all plugins and validate required resources.
330    ///
331    /// Plugins may register additional plugins during their `build` call; this
332    /// repeats until no new plugins are added, up to a hard limit of 64 passes
333    /// to catch accidental infinite registration cycles.
334    pub fn build(&mut self) -> &mut Self {
335        let mut iterations = 0;
336        const MAX_PLUGIN_BUILD_ITERATIONS: u32 = 64;
337
338        while !self.plugins.is_empty() {
339            iterations += 1;
340            if iterations > MAX_PLUGIN_BUILD_ITERATIONS {
341                panic!(
342                    "App::build() exceeded {MAX_PLUGIN_BUILD_ITERATIONS} plugin-registration passes — \
343                 likely a cycle where plugins keep registering each other. Check for a plugin whose \
344                 build() unconditionally re-adds itself or another plugin that re-adds it."
345                );
346            }
347            let plugins: Vec<_> = self.plugins.drain(..).collect();
348            for plugin in plugins {
349                plugin.build(self);
350            }
351        }
352
353        self.required.validate();
354
355        // Resolve as much as possible synchronously (headless/CPU-only
356        // backends, tests) so resources are ready immediately after
357        // build(). Anything still pending (an async GPU backend, say)
358        // keeps getting retried every tick by update().
359        self.reconverge(64);
360
361        self
362    }
363
364    /// Run every stage once per tick, in [`TICK_STAGES`] order. Before every
365    /// tick, and again after every stage, [`reconverge`](App::reconverge)
366    /// drains `AssetSync`/`AssetSyncDeps` — so newly-queued asset or
367    /// resource work is handled immediately rather than waiting for the
368    /// next tick's front pass.
369    pub fn update(&mut self) {
370        self.reconverge(64);
371
372        for stage in TICK_STAGES {
373            self.run_stage_once(stage);
374            self.reconverge(64);
375        }
376    }
377
378    /// Replace the default runner with a custom one.
379    ///
380    /// The runner receives ownership of the `App` and is responsible for
381    /// calling [`update`](App::update) at the appropriate cadence (e.g. driven
382    /// by a window event loop).
383    pub fn set_runner<F>(&mut self, runner: F) -> &mut Self
384    where
385        F: FnOnce(App) + 'static,
386    {
387        self.runner = Some(Box::new(runner));
388        self
389    }
390
391    /// Consume the app and hand it to the configured runner.
392    ///
393    /// Panics if no runner has been set.
394    pub fn run(&mut self) {
395        let mut owned_app = std::mem::take(self);
396        let runner = owned_app.runner.take().expect("No runner found!");
397        runner(owned_app);
398    }
399}