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