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/// Callback used to drive the application's main loop.
82///
83/// Set with [`App::set_runner`]. The default runner calls [`App::update`] in
84/// an infinite loop.
85pub type AppRunner = Box<dyn FnOnce(App)>;
86
87/// The central application object.
88///
89/// `App` owns the ECS world, resources, plugins, and systems. The typical
90/// lifecycle is:
91///
92/// 1. Create with [`App::new`].
93/// 2. Register plugins with [`add_plugin`](App::add_plugin).
94/// 3. Call [`build`](App::build) to run all plugin registrations, execute
95///    startup systems, and validate required resources.
96/// 4. Call [`run`](App::run) to hand control to the runner.
97pub struct App {
98    pub(crate) world: hecs::World,
99    pub(crate) resources: Resources,
100    plugins: Vec<Box<dyn Plugin>>,
101    systems: BTreeMap<SystemStage, Vec<Box<dyn System>>>,
102    runner: Option<AppRunner>,
103    pub(crate) required: RequiredResources,
104    /// Per-`Startup`-system "has it run yet" flags, indexed the same as
105    /// `systems[&SystemStage::Startup]`. Sized once in [`build`](App::build).
106    /// A system is only ever invoked once its [`System::requires`] are all
107    /// satisfied, and once invoked it is never invoked again.
108    startup_done: Vec<bool>,
109}
110
111impl Default for App {
112    fn default() -> Self {
113        Self::new()
114    }
115}
116
117impl App {
118    /// Create a new `App` with an empty world and a default infinite-loop runner.
119    pub fn new() -> Self {
120        let mut world = hecs::World::default();
121        let mut resources = Resources::new(&mut world);
122        resources.insert_resource(&mut world, ());
123
124        Self {
125            world: world,
126            resources: resources,
127            plugins: Vec::new(),
128            systems: BTreeMap::new(),
129            runner: Some(Box::new(|mut app| {
130                loop {
131                    app.update();
132                }
133            })),
134            required: RequiredResources::new(),
135            startup_done: Vec::new(),
136        }
137    }
138
139    /// Run every system in `stage` once, flush the command buffer, and return
140    /// `true` if any resource was newly inserted during this pass.
141    ///
142    /// [`Commands::insert_resource`](crate::ecs::system::Commands::insert_resource)
143    /// bumps the generation counter at queue time, so both direct inserts and
144    /// deferred command-buffer inserts are detected here with no world
145    /// introspection needed after the flush.
146    fn run_stage_once(&mut self, stage: SystemStage) -> bool {
147        let gen_before = self.resources.generation();
148
149        if let Some(systems) = self.systems.get_mut(&stage) {
150            for system in systems.iter_mut() {
151                let _guard = crate::ecs::resources::set_current_system(system.name());
152                system.run(&self.world, &self.resources);
153            }
154        }
155        self.resources.get_command_buffer().run_on(&mut self.world);
156
157        self.resources.generation() != gen_before
158    }
159
160    /// Run every not-yet-fired `Startup` system whose hard requirements are
161    /// currently satisfied, marking each one done so it never runs again.
162    /// A system whose requirements aren't met yet is left pending — silently,
163    /// no panic — for another attempt on a later pass/tick. Returns `true` if
164    /// any system ran (i.e. a resource may have changed).
165    fn run_startup_ready(&mut self) -> bool {
166        let Some(systems) = self.systems.get_mut(&SystemStage::Startup) else {
167            return false;
168        };
169        if self.startup_done.len() != systems.len() {
170            self.startup_done.resize(systems.len(), false);
171        }
172
173        let mut any_ran = false;
174        for (idx, system) in systems.iter_mut().enumerate() {
175            if self.startup_done[idx] {
176                continue;
177            }
178            let ready = system
179                .requires()
180                .iter()
181                .all(|req| (req.present)(&self.world, &self.resources));
182            if !ready {
183                continue;
184            }
185
186            let _guard = crate::ecs::resources::set_current_system(system.name());
187            system.run(&self.world, &self.resources);
188            self.startup_done[idx] = true;
189            any_ran = true;
190        }
191
192        if any_ran {
193            self.resources.get_command_buffer().run_on(&mut self.world);
194        }
195        any_ran
196    }
197
198    /// Panic before running `stage` if any of its systems declares a hard
199    /// [`Res`](crate::ecs::system::Res)/[`ResMut`](crate::ecs::system::ResMut)
200    /// requirement on a resource that isn't present yet.
201    ///
202    /// Only applied to non-convergent stages: convergent stages
203    /// ([`Startup`](SystemStage::Startup), [`AssetSync`](SystemStage::AssetSync),
204    /// [`AssetSyncDeps`](SystemStage::AssetSyncDeps)) are handled by
205    /// [`reconverge`](App::reconverge) instead, which skips systems that
206    /// aren't ready rather than treating it as an error.
207    fn validate_stage_resources(&self, stage: SystemStage) {
208        if stage.is_convergent() {
209            return;
210        }
211
212        let Some(systems) = self.systems.get(&stage) else {
213            return;
214        };
215
216        let mut missing: Vec<(&'static str, &'static str)> = Vec::new();
217        for system in systems {
218            for req in system.requires() {
219                if !(req.present)(&self.world, &self.resources) {
220                    missing.push((system.name(), req.name));
221                    tracing::error!(
222                        stage = ?stage,
223                        system = system.name(),
224                        resource = req.name,
225                        "system requires a resource that is not yet available"
226                    );
227                }
228            }
229        }
230
231        if !missing.is_empty() {
232            missing.sort_unstable();
233            missing.dedup();
234            panic!(
235                "{stage:?}: system(s) require resource(s) that are not yet available:\n{}\n\n\
236                 Insert these via App::add_resource, or a Startup/AssetSync system, before \
237                 this stage runs.",
238                missing
239                    .iter()
240                    .map(|(system, resource)| format!(" - system `{system}` requires `{resource}`"))
241                    .collect::<Vec<_>>()
242                    .join("\n")
243            );
244        }
245    }
246
247    /// Prioritize resource/asset construction: run any not-yet-fired
248    /// `Startup` systems that are now ready, then `AssetSync`, then
249    /// `AssetSyncDeps` — repeating the whole trio until a full pass produces
250    /// no new resources, up to `max_passes`. `Startup` runs first each pass
251    /// so a same-pass consumer (an `AssetSyncDeps` system, say) can see what
252    /// it just produced. Logs a warning if the limit is reached — that
253    /// usually means a [`LazyResource`](crate::assets::singleton_asset::LazyResource)
254    /// or [`Asset`](crate::assets::upload::Asset) dependency is permanently
255    /// unsatisfiable (or a `Startup` system's requirement is never met).
256    ///
257    /// Called at the front of every tick and again after every stage in
258    /// [`update`](App::update) (and once during [`build`](App::build)), so
259    /// newly-queued asset/resource work — and any `Startup` system it
260    /// unblocks — is drained immediately instead of waiting for next tick's
261    /// front pass.
262    fn reconverge(&mut self, max_passes: u32) {
263        for pass in 0..max_passes {
264            let gen_before = self.resources.generation();
265
266            self.run_startup_ready();
267            self.run_stage_once(SystemStage::AssetSync);
268            self.run_stage_once(SystemStage::AssetSyncDeps);
269
270            if self.resources.generation() == gen_before {
271                return;
272            }
273            if pass == max_passes - 1 {
274                tracing::warn!(
275                    "Startup/AssetSync/AssetSyncDeps did not settle after {max_passes} passes — \
276                     a dependency may be permanently unsatisfiable. Check for a Startup system \
277                     whose Res/ResMut requirement is never met, a LazyResource whose construct() \
278                     always returns None, or an Asset whose upload() always returns None."
279                );
280            }
281        }
282    }
283
284    /// Queue a plugin to be built during [`build`](App::build).
285    pub fn add_plugin(&mut self, plugin: impl Plugin) -> &mut Self {
286        self.plugins.push(Box::new(plugin));
287        self
288    }
289
290    /// Insert a resource into the world immediately.
291    pub fn add_resource(&mut self, res: impl hecs::Component) -> &mut Self {
292        self.resources.insert_resource(&mut self.world, res);
293        self
294    }
295
296    /// Borrow resource `T`, panicking if it is absent.
297    pub fn get_resource<'a, T: hecs::Component>(&'a self) -> hecs::Ref<'a, T> {
298        self.resources.get_resource(&self.world)
299    }
300
301    /// Mutably borrow resource `T`, panicking if it is absent.
302    pub fn get_resource_mut<'a, T: hecs::Component>(&'a self) -> hecs::RefMut<'a, T> {
303        self.resources.get_resource_mut(&self.world)
304    }
305
306    /// Insert resource `T` only if it is not already present.
307    ///
308    /// Returns `true` if the resource was inserted.
309    pub fn try_insert_resource<T: hecs::Component>(&mut self, res: T) -> bool {
310        self.resources.try_insert(&mut self.world, res)
311    }
312
313    /// Register a single system to run at `stage`.
314    pub fn add_system<Marker>(
315        &mut self,
316        stage: SystemStage,
317        system: impl IntoSystem<Marker> + 'static,
318    ) -> &mut Self {
319        self.systems
320            .entry(stage)
321            .or_default()
322            .push(Box::new(system.into_system()));
323        self
324    }
325
326    /// Register multiple systems to run at `stage`.
327    ///
328    /// Accepts a tuple of systems via [`IntoSystemSet`].
329    pub fn add_systems<Marker>(
330        &mut self,
331        stage: SystemStage,
332        systems: impl IntoSystemSet<Marker>,
333    ) -> &mut Self {
334        let entry = self.systems.entry(stage).or_default();
335        entry.extend(systems.into_system_set());
336        self
337    }
338
339    /// Build all plugins, run startup systems, and validate required resources.
340    ///
341    /// Plugins may register additional plugins during their `build` call; this
342    /// repeats until no new plugins are added, up to a hard limit of 64 passes
343    /// to catch accidental infinite registration cycles.
344    pub fn build(&mut self) -> &mut Self {
345        let mut iterations = 0;
346        const MAX_PLUGIN_BUILD_ITERATIONS: u32 = 64;
347
348        while !self.plugins.is_empty() {
349            iterations += 1;
350            if iterations > MAX_PLUGIN_BUILD_ITERATIONS {
351                panic!(
352                    "App::build() exceeded {MAX_PLUGIN_BUILD_ITERATIONS} plugin-registration passes — \
353                 likely a cycle where plugins keep registering each other. Check for a plugin whose \
354                 build() unconditionally re-adds itself or another plugin that re-adds it."
355                );
356            }
357            let plugins: Vec<_> = self.plugins.drain(..).collect();
358            for plugin in plugins {
359                plugin.build(self);
360            }
361        }
362
363        self.required.validate();
364
365        // Size the per-system done-tracking now that every plugin has
366        // registered its Startup systems.
367        let startup_len = self
368            .systems
369            .get(&SystemStage::Startup)
370            .map(Vec::len)
371            .unwrap_or(0);
372        self.startup_done = vec![false; startup_len];
373
374        // Resolve as much as possible synchronously (headless/CPU-only
375        // backends, tests) so resources are ready immediately after
376        // build(). Anything still pending — a Startup system waiting on an
377        // async GPU backend, say — keeps getting retried every tick by
378        // update(), prioritized ahead of PreUpdate/Update/etc.
379        self.reconverge(64);
380
381        self
382    }
383
384    /// Run every stage once per tick, in [`TICK_STAGES`] order. Before every
385    /// tick, and again after every stage, [`reconverge`](App::reconverge)
386    /// drains `Startup`/`AssetSync`/`AssetSyncDeps` — so newly-queued asset
387    /// or resource work (and any `Startup` system it unblocks) is handled
388    /// immediately rather than waiting for the next tick's front pass.
389    pub fn update(&mut self) {
390        self.reconverge(64);
391
392        for stage in TICK_STAGES {
393            self.validate_stage_resources(stage);
394            self.run_stage_once(stage);
395            self.reconverge(64);
396        }
397    }
398
399    /// Replace the default runner with a custom one.
400    ///
401    /// The runner receives ownership of the `App` and is responsible for
402    /// calling [`update`](App::update) at the appropriate cadence (e.g. driven
403    /// by a window event loop).
404    pub fn set_runner<F>(&mut self, runner: F) -> &mut Self
405    where
406        F: FnOnce(App) + 'static,
407    {
408        self.runner = Some(Box::new(runner));
409        self
410    }
411
412    /// Consume the app and hand it to the configured runner.
413    ///
414    /// Panics if no runner has been set.
415    pub fn run(&mut self) {
416        let mut owned_app = std::mem::take(self);
417        let runner = owned_app.runner.take().expect("No runner found!");
418        runner(owned_app);
419    }
420}