Skip to main content

pebble/
app.rs

1use crate::{
2    ecs::{
3        events::{AsyncEventChannel, Events, drain_async_events},
4        plugin::Plugin,
5        resources::Resources,
6        system::{IntoSystem, System},
7        system_set::IntoSystemSet,
8    },
9};
10use std::collections::{BTreeMap, BinaryHeap, HashMap};
11
12/// Determines when during a frame a system is executed.
13///
14/// [`Startup`](SystemStage::Startup) runs on every tick until a system on it
15/// returns `Some(())` (then never again). Systems returning `Option<()>` are
16/// automatically "once" — they retry each tick until they succeed.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
18pub enum SystemStage {
19    /// Run-once startup systems — any system added here that returns
20    /// `Option<()>` runs until it returns `Some(())`, then is permanently
21    /// retired.
22    Startup,
23    /// Upload CPU-side source assets to the GPU backend.
24    AssetSync,
25    /// Construct GPU resources and upload assets that depend on other
26    /// processed assets.
27    AssetSyncDeps,
28    /// Before the main update.
29    PreUpdate,
30    /// Main game-logic update.
31    Update,
32    /// After the main update.
33    PostUpdate,
34    /// Prepare rendering data and poll for the GPU backend.
35    PreRender,
36    /// Issue draw calls.
37    Render,
38    /// Cleanup or post-processing after rendering.
39    PostRender,
40}
41
42/// Fixed per-tick order for all stages.
43const ALL_STAGES: [SystemStage; 9] = [
44    SystemStage::Startup,
45    SystemStage::AssetSync,
46    SystemStage::AssetSyncDeps,
47    SystemStage::PreUpdate,
48    SystemStage::Update,
49    SystemStage::PostUpdate,
50    SystemStage::PreRender,
51    SystemStage::Render,
52    SystemStage::PostRender,
53];
54
55/// Callback used to drive the application's main loop.
56///
57/// Set with [`App::set_runner`]. The default runner calls [`App::update`] in
58/// an infinite loop.
59pub type AppRunner = Box<dyn FnOnce(App)>;
60
61/// The central application object.
62///
63/// `App` owns the ECS world, resources, plugins, and systems. The typical
64/// lifecycle is:
65///
66/// 1. Create with [`App::new`].
67/// 2. Register plugins with [`add_plugin`](App::add_plugin).
68/// 3. Call [`build`](App::build) to run all plugin registrations and sort systems.
69/// 4. Call [`run`](App::run) to hand control to the runner.
70pub struct App {
71    pub(crate) world: hecs::World,
72    pub(crate) resources: Resources,
73    plugins: Vec<Box<dyn Plugin>>,
74    systems: BTreeMap<SystemStage, Vec<Box<dyn System>>>,
75    runner: Option<AppRunner>,
76    /// One closure per event type registered via [`add_event`](App::add_event),
77    /// each calling that type's [`Events::update`] to age its buffers. Run
78    /// at the front of every [`update`](App::update) tick, before any user
79    /// system, so a reader anywhere in the tick sees a consistent view.
80    event_updaters: Vec<Box<dyn FnMut(&hecs::World, &Resources)>>,
81    /// Set via [`set_ready_gate`](App::set_ready_gate). While present,
82    /// [`update`](App::update) calls it instead of running any stage; once it
83    /// returns `true` it is dropped and never consulted again for the rest of
84    /// the app's lifetime. Lets a plugin that can't finish its setup
85    /// synchronously (e.g. an in-flight async GPU backend init on `wasm32`)
86    /// hold the whole tick loop idle — checked once per `update` call, not a
87    /// busy/blocking wait — until every other system can safely assume that
88    /// setup is done.
89    ready_gate: Option<Box<dyn FnMut(&mut hecs::World, &mut Resources) -> bool>>,
90}
91
92impl Default for App {
93    fn default() -> Self {
94        Self::new()
95    }
96}
97
98impl App {
99    /// Create a new `App` with an empty world and a default infinite-loop runner.
100    ///
101    /// Builds in [`TimePlugin`](crate::time::TimePlugin) — `Res<Time>` works
102    /// without registering anything yourself — along with
103    /// [`AudioPlugin`](crate::audio::AudioPlugin) (`Res<AudioOutput>`), and,
104    /// on every target except `wasm32`,
105    /// [`GamepadPlugin`](crate::gamepad::GamepadPlugin) (`Res<Gamepads>`).
106    pub fn new() -> Self {
107        let mut world = hecs::World::default();
108        let mut resources = Resources::new(&mut world);
109        resources.insert_resource(&mut world, ());
110
111        let mut app = Self {
112            world: world,
113            resources: resources,
114            plugins: Vec::new(),
115            systems: BTreeMap::new(),
116            runner: Some(Box::new(|mut app| {
117                loop {
118                    app.update();
119                }
120            })),
121            event_updaters: Vec::new(),
122            ready_gate: None,
123        };
124        app.add_plugin(crate::time::TimePlugin);
125        #[cfg(not(target_arch = "wasm32"))]
126        app.add_plugin(crate::gamepad::GamepadPlugin);
127        app.add_plugin(crate::audio::AudioPlugin);
128        app
129    }
130
131    /// Run every system in `stage` once, then flush the command buffer.
132    fn run_stage_once(&mut self, stage: SystemStage) {
133        if let Some(systems) = self.systems.get_mut(&stage) {
134            for system in systems.iter_mut() {
135                let _guard = crate::ecs::resources::set_current_system(system.name());
136                system.run(&self.world, &self.resources);
137            }
138        }
139        self.resources.get_command_buffer().run_on(&mut self.world);
140    }
141
142    /// Queue a plugin to be built during [`build`](App::build).
143    pub fn add_plugin(&mut self, plugin: impl Plugin) -> &mut Self {
144        self.plugins.push(Box::new(plugin));
145        self
146    }
147
148    /// Hold every stage idle until `gate` reports readiness.
149    ///
150    /// `gate` is called once per [`update`](App::update) tick — instead of
151    /// running any [`SystemStage`] — for as long as it returns `false`. As
152    /// soon as it returns `true`, it is dropped and `update` goes back to
153    /// running stages normally, forever after; there is no re-checking, so
154    /// `gate` should only report `true` once its readiness condition can
155    /// never become false again (e.g. inserting a resource that is never
156    /// removed).
157    ///
158    /// Only one gate can be active — a second call replaces the first — since
159    /// this is meant for a single startup precondition (e.g. an in-flight
160    /// async GPU backend init on `wasm32`, which can't be waited on by
161    /// blocking the only thread the browser gives it), not general
162    /// scheduling.
163    pub fn set_ready_gate<F>(&mut self, gate: F) -> &mut Self
164    where
165        F: FnMut(&mut hecs::World, &mut Resources) -> bool + 'static,
166    {
167        self.ready_gate = Some(Box::new(gate));
168        self
169    }
170
171    /// Insert a resource into the world immediately.
172    pub fn add_resource(&mut self, res: impl hecs::Component) -> &mut Self {
173        self.resources.insert_resource(&mut self.world, res);
174        self
175    }
176
177    /// Borrow resource `T`, panicking if it is absent.
178    pub fn get_resource<'a, T: hecs::Component>(&'a self) -> hecs::Ref<'a, T> {
179        self.resources.get_resource(&self.world)
180    }
181
182    /// Mutably borrow resource `T`, panicking if it is absent.
183    pub fn get_resource_mut<'a, T: hecs::Component>(&'a self) -> hecs::RefMut<'a, T> {
184        self.resources.get_resource_mut(&self.world)
185    }
186
187    /// Insert resource `T` only if it is not already present.
188    ///
189    /// Returns `true` if the resource was inserted.
190    pub fn try_insert_resource<T: hecs::Component>(&mut self, res: T) -> bool {
191        self.resources.try_insert(&mut self.world, res)
192    }
193
194    /// Register event type `T`, making [`EventWriter<T>`](crate::ecs::events::EventWriter)
195    /// and [`EventReader<T>`](crate::ecs::events::EventReader) usable as
196    /// system parameters.
197    pub fn add_event<T: hecs::Component>(&mut self) -> &mut Self {
198        self.try_insert_resource(Events::<T>::default());
199        self.event_updaters.push(Box::new(|world, resources| {
200            resources.get_resource_mut::<Events<T>>(world).update();
201        }));
202        self
203    }
204
205    /// Register event type `T` as in [`add_event`](Self::add_event), and
206    /// additionally make [`AsyncEventWriter<T>`](crate::ecs::events::AsyncEventWriter)
207    /// usable as a system parameter.
208    pub fn add_async_event<T: hecs::Component>(&mut self) -> &mut Self {
209        self.add_event::<T>();
210        self.try_insert_resource(AsyncEventChannel::<T>::new());
211        self.add_system(SystemStage::PreUpdate, drain_async_events::<T>);
212        self
213    }
214
215    /// Register a single system to run at `stage`.
216    pub fn add_system<Marker>(
217        &mut self,
218        stage: SystemStage,
219        system: impl IntoSystem<Marker> + 'static,
220    ) -> &mut Self {
221        self.systems
222            .entry(stage)
223            .or_default()
224            .push(Box::new(system.into_system()));
225        self
226    }
227
228    /// Register multiple systems to run at `stage`.
229    ///
230    /// Accepts a tuple of systems via [`IntoSystemSet`].
231    pub fn add_systems<Marker>(
232        &mut self,
233        stage: SystemStage,
234        systems: impl IntoSystemSet<Marker>,
235    ) -> &mut Self {
236        let entry = self.systems.entry(stage).or_default();
237        entry.extend(systems.into_system_set());
238        self
239    }
240
241    /// Topologically sort `systems` by each system's [`System::after_ids`]/[`System::before_ids`]
242    /// constraints, breaking ties by original registration order.
243    ///
244    /// Panics if the constraints form a cycle.
245    fn sort_stage(stage: SystemStage, systems: &mut Vec<Box<dyn System>>) {
246        let id_index: HashMap<std::any::TypeId, usize> = systems
247            .iter()
248            .enumerate()
249            .map(|(i, s)| (s.ordering_id(), i))
250            .collect();
251
252        let n = systems.len();
253        let mut adjacency: Vec<Vec<usize>> = vec![Vec::new(); n];
254        let mut in_degree = vec![0usize; n];
255
256        for (i, system) in systems.iter().enumerate() {
257            for id in system.after_ids() {
258                if let Some(&dep) = id_index.get(id) {
259                    adjacency[dep].push(i);
260                    in_degree[i] += 1;
261                }
262            }
263            for id in system.before_ids() {
264                if let Some(&dependent) = id_index.get(id) {
265                    adjacency[i].push(dependent);
266                    in_degree[dependent] += 1;
267                }
268            }
269        }
270
271        // Min-heap on original index so ties resolve to registration order.
272        let mut ready: BinaryHeap<std::cmp::Reverse<usize>> = in_degree
273            .iter()
274            .enumerate()
275            .filter(|(_, d)| **d == 0)
276            .map(|(i, _)| std::cmp::Reverse(i))
277            .collect();
278
279        let mut order = Vec::with_capacity(n);
280        while let Some(std::cmp::Reverse(u)) = ready.pop() {
281            order.push(u);
282            for &v in &adjacency[u] {
283                in_degree[v] -= 1;
284                if in_degree[v] == 0 {
285                    ready.push(std::cmp::Reverse(v));
286                }
287            }
288        }
289
290        if order.len() != n {
291            let stuck: Vec<&'static str> = (0..n)
292                .filter(|i| in_degree[*i] > 0)
293                .map(|i| systems[i].name())
294                .collect();
295            panic!(
296                "{stage:?}: system ordering constraints form a cycle among: {stuck:?}"
297            );
298        }
299
300        let mut taken: Vec<Option<Box<dyn System>>> = systems.drain(..).map(Some).collect();
301        for i in order {
302            systems.push(taken[i].take().unwrap());
303        }
304    }
305
306    /// Build all plugins and sort systems.
307    ///
308    /// Plugins may register additional plugins during their `build` call; this
309    /// repeats until no new plugins are added, up to a hard limit of 64 passes
310    /// to catch accidental infinite registration cycles.
311    pub fn build(&mut self) -> &mut Self {
312        let mut iterations = 0;
313        const MAX_PLUGIN_BUILD_ITERATIONS: u32 = 64;
314
315        while !self.plugins.is_empty() {
316            iterations += 1;
317            if iterations > MAX_PLUGIN_BUILD_ITERATIONS {
318                panic!(
319                    "App::build() exceeded {MAX_PLUGIN_BUILD_ITERATIONS} plugin-registration passes — \
320                 likely a cycle where plugins keep registering each other. Check for a plugin whose \
321                 build() unconditionally re-adds itself or another plugin that re-adds it."
322                );
323            }
324            let plugins: Vec<_> = self.plugins.drain(..).collect();
325            for plugin in plugins {
326                plugin.build(self);
327            }
328        }
329
330        for (stage, systems) in self.systems.iter_mut() {
331            Self::sort_stage(*stage, systems);
332        }
333
334        self
335    }
336
337    /// Run every stage once per tick, in [`ALL_STAGES`] order — unless a
338    /// [`ready_gate`](App::set_ready_gate) is still pending, in which case
339    /// this tick only checks the gate and returns.
340    pub fn update(&mut self) {
341        if let Some(gate) = &mut self.ready_gate {
342            if !gate(&mut self.world, &mut self.resources) {
343                return;
344            }
345            self.ready_gate = None;
346        }
347
348        for updater in self.event_updaters.iter_mut() {
349            updater(&self.world, &self.resources);
350        }
351        for stage in ALL_STAGES {
352            self.run_stage_once(stage);
353        }
354    }
355
356    /// Replace the default runner with a custom one.
357    ///
358    /// The runner receives ownership of the `App` and is responsible for
359    /// calling [`update`](App::update) at the appropriate cadence (e.g. driven
360    /// by a window event loop).
361    pub fn set_runner<F>(&mut self, runner: F) -> &mut Self
362    where
363        F: FnOnce(App) + 'static,
364    {
365        self.runner = Some(Box::new(runner));
366        self
367    }
368
369    /// Consume the app and hand it to the configured runner.
370    ///
371    /// Panics if no runner has been set.
372    pub fn run(&mut self) {
373        let mut owned_app = std::mem::take(self);
374        let runner = owned_app.runner.take().expect("No runner found!");
375        runner(owned_app);
376    }
377}
378
379#[cfg(test)]
380mod tests {
381    use super::*;
382    use crate::ecs::system::{ResMut, SystemOrderingExt};
383
384    struct Order(Vec<&'static str>);
385
386    fn sys_a(mut o: ResMut<Order>) {
387        o.0.push("a");
388    }
389    fn sys_b(mut o: ResMut<Order>) {
390        o.0.push("b");
391    }
392    fn sys_c(mut o: ResMut<Order>) {
393        o.0.push("c");
394    }
395
396    #[test]
397    fn systems_run_in_declared_order() {
398        let mut app = App::new();
399        app.add_resource(Order(Vec::new()));
400
401        // Registered in a-b-c order, but both a and b declare they must run
402        // after c — the sort should move c first while leaving a before b
403        // (their relative registration order) intact.
404        app.add_system(SystemStage::Update, sys_a.after(sys_c));
405        app.add_system(SystemStage::Update, sys_b.after(sys_c));
406        app.add_system(SystemStage::Update, sys_c);
407
408        app.build();
409        app.update();
410
411        let order = app.get_resource::<Order>();
412        assert_eq!(order.0, vec!["c", "a", "b"]);
413    }
414
415    #[test]
416    #[should_panic(expected = "cycle")]
417    fn cyclic_ordering_constraints_panic() {
418        let mut app = App::new();
419        app.add_resource(Order(Vec::new()));
420
421        app.add_system(SystemStage::Update, sys_a.after(sys_b));
422        app.add_system(SystemStage::Update, sys_b.after(sys_a));
423
424        app.build();
425    }
426
427    #[test]
428    fn time_plugin_is_already_built_into_a_fresh_app() {
429        let mut app = App::new();
430        app.build();
431
432        // Doesn't panic — `Time` exists without anyone calling
433        // `add_plugin(TimePlugin)` themselves.
434        let _ = app.get_resource::<crate::time::Time>();
435    }
436
437    #[test]
438    fn registering_time_plugin_again_does_not_double_register_its_system() {
439        // Baseline: however many PreUpdate systems App::new()'s own
440        // automatic plugins (Time, Gamepad, ...) register on their own —
441        // not hardcoded, so this stays correct as more get added.
442        let mut baseline = App::new();
443        baseline.build();
444        let baseline_count = baseline.systems.get(&SystemStage::PreUpdate).map_or(0, Vec::len);
445
446        let mut app = App::new();
447        app.add_plugin(crate::time::TimePlugin); // redundant - App::new() already built it in
448        app.build();
449
450        // If TimePlugin weren't idempotent, this would be one more than baseline.
451        let tick_systems = app.systems.get(&SystemStage::PreUpdate).map_or(0, Vec::len);
452        assert_eq!(tick_systems, baseline_count);
453    }
454}