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/// Stages are iterated in the order defined here — [`Startup`](SystemStage::Startup)
15/// runs once during [`App::build`], all others run every [`App::update`] tick.
16///
17/// Asset sync stages ([`AssetSync`](SystemStage::AssetSync),
18/// [`AssetSyncDeps`](SystemStage::AssetSyncDeps)) run **after** [`PreRender`](SystemStage::PreRender)
19/// so that the GPU backend — which is delivered in `PreRender` via a one-shot
20/// channel — is guaranteed to be present before asset upload is attempted.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
22pub enum SystemStage {
23    /// Runs once at startup, before the main loop begins.
24    Startup,
25    /// Runs before the main update.
26    PreUpdate,
27    /// Main game-logic update.
28    Update,
29    /// Runs after the main update.
30    PostUpdate,
31    /// Prepare rendering data and poll for the GPU backend.
32    /// The backend resource becomes available here on the tick it finishes
33    /// initialising, making it visible to the asset sync stages below.
34    PreRender,
35    /// Upload CPU-side source assets to the GPU backend.
36    /// Runs after [`PreRender`](SystemStage::PreRender) so the backend is
37    /// guaranteed to be present.
38    AssetSync,
39    /// Construct lazy GPU resources and upload assets that depend on other
40    /// processed assets. Runs in a convergence loop so dependency chains
41    /// (e.g. LazyResource A → LazyResource B) resolve within a single tick.
42    AssetSyncDeps,
43    /// Issue draw calls.
44    Render,
45    /// Cleanup or post-processing after rendering.
46    PostRender,
47}
48
49impl SystemStage {
50    /// Returns `true` for stages that are re-run until no new resources are
51    /// inserted — collapsing multi-tick dependency chains into one tick.
52    ///
53    /// `Startup` is included so a deferred or async resource producer (e.g. a
54    /// background thread pool result, or a [`LazyResource`](crate::assets::singleton_asset::LazyResource)-style
55    /// startup system using `Option<Res<T>>` to wait) gets another pass
56    /// within the same `build()` call rather than only running once.
57    pub fn is_convergent(self) -> bool {
58        matches!(self, Self::Startup | Self::AssetSync | Self::AssetSyncDeps)
59    }
60}
61
62/// Callback used to drive the application's main loop.
63///
64/// Set with [`App::set_runner`]. The default runner calls [`App::update`] in
65/// an infinite loop.
66pub type AppRunner = Box<dyn FnOnce(App)>;
67
68/// The central application object.
69///
70/// `App` owns the ECS world, resources, plugins, and systems. The typical
71/// lifecycle is:
72///
73/// 1. Create with [`App::new`].
74/// 2. Register plugins with [`add_plugin`](App::add_plugin).
75/// 3. Call [`build`](App::build) to run all plugin registrations, execute
76///    startup systems, and validate required resources.
77/// 4. Call [`run`](App::run) to hand control to the runner.
78pub struct App {
79    pub(crate) world: hecs::World,
80    pub(crate) resources: Resources,
81    plugins: Vec<Box<dyn Plugin>>,
82    systems: BTreeMap<SystemStage, Vec<Box<dyn System>>>,
83    runner: Option<AppRunner>,
84    pub(crate) required: RequiredResources,
85    /// Runtime stage order, cached once in [`build`](App::build) so
86    /// [`update`](App::update) never heap-allocates per tick.
87    update_stages: Vec<SystemStage>,
88}
89
90impl Default for App {
91    fn default() -> Self {
92        Self::new()
93    }
94}
95
96impl App {
97    /// Create a new `App` with an empty world and a default infinite-loop runner.
98    pub fn new() -> Self {
99        let mut world = hecs::World::default();
100        let mut resources = Resources::new(&mut world);
101        resources.insert_resource(&mut world, ());
102
103        Self {
104            world: world,
105            resources: resources,
106            plugins: Vec::new(),
107            systems: BTreeMap::new(),
108            runner: Some(Box::new(|mut app| {
109                loop {
110                    app.update();
111                }
112            })),
113            required: RequiredResources::new(),
114            update_stages: Vec::new(),
115        }
116    }
117
118    /// Run every system in `stage` once, flush the command buffer, and return
119    /// `true` if any resource was newly inserted during this pass.
120    ///
121    /// [`Commands::insert_resource`](crate::ecs::system::Commands::insert_resource)
122    /// bumps the generation counter at queue time, so both direct inserts and
123    /// deferred command-buffer inserts are detected here with no world
124    /// introspection needed after the flush.
125    fn run_stage_once(&mut self, stage: SystemStage) -> bool {
126        let gen_before = self.resources.generation();
127
128        if let Some(systems) = self.systems.get_mut(&stage) {
129            for system in systems.iter_mut() {
130                let _guard = crate::ecs::resources::set_current_system(system.name());
131                system.run(&self.world, &self.resources);
132            }
133        }
134        self.resources.get_command_buffer().run_on(&mut self.world);
135
136        self.resources.generation() != gen_before
137    }
138
139    /// Panic before running `stage` if any of its systems declares a hard
140    /// [`Res`](crate::ecs::system::Res)/[`ResMut`](crate::ecs::system::ResMut)
141    /// requirement on a resource that isn't present yet.
142    ///
143    /// Only applied to non-convergent stages: convergent stages
144    /// ([`Startup`](SystemStage::Startup), [`AssetSync`](SystemStage::AssetSync),
145    /// [`AssetSyncDeps`](SystemStage::AssetSyncDeps)) are re-run precisely
146    /// because a resource may not exist yet on an early pass — their systems
147    /// are expected to use `Option<Res<T>>`/`Option<ResMut<T>>` to wait for
148    /// it, so a hard requirement there is checked (and will panic normally)
149    /// only once actually fetched.
150    fn validate_stage_resources(&self, stage: SystemStage) {
151        if stage.is_convergent() {
152            return;
153        }
154
155        let Some(systems) = self.systems.get(&stage) else {
156            return;
157        };
158
159        let mut missing: Vec<(&'static str, &'static str)> = Vec::new();
160        for system in systems {
161            for req in system.requires() {
162                if !(req.present)(&self.world, &self.resources) {
163                    missing.push((system.name(), req.name));
164                    tracing::error!(
165                        stage = ?stage,
166                        system = system.name(),
167                        resource = req.name,
168                        "system requires a resource that is not yet available"
169                    );
170                }
171            }
172        }
173
174        if !missing.is_empty() {
175            missing.sort_unstable();
176            missing.dedup();
177            panic!(
178                "{stage:?}: system(s) require resource(s) that are not yet available:\n{}\n\n\
179                 Insert these via App::add_resource, or a Startup/AssetSync system, before \
180                 this stage runs.",
181                missing
182                    .iter()
183                    .map(|(system, resource)| format!(" - system `{system}` requires `{resource}`"))
184                    .collect::<Vec<_>>()
185                    .join("\n")
186            );
187        }
188    }
189
190    /// Re-run `stage` until a full pass produces no new resources, up to
191    /// `max_passes`. Logs a warning if the limit is reached — that usually
192    /// means a [`LazyResource`](crate::assets::singleton_asset::LazyResource)
193    /// or [`Asset`](crate::assets::upload::Asset) dependency is permanently
194    /// unsatisfiable.
195    fn run_stage_to_convergence(&mut self, stage: SystemStage, max_passes: u32) {
196        for pass in 0..max_passes {
197            if !self.run_stage_once(stage) {
198                return;
199            }
200            if pass == max_passes - 1 {
201                tracing::warn!(
202                    "{stage:?}: convergence did not settle after {max_passes} passes — \
203                     a dependency may be permanently unsatisfiable. Check for a \
204                     LazyResource whose construct() or an Asset whose upload() \
205                     always returns None."
206                );
207            }
208        }
209    }
210
211    /// Queue a plugin to be built during [`build`](App::build).
212    pub fn add_plugin(&mut self, plugin: impl Plugin) -> &mut Self {
213        self.plugins.push(Box::new(plugin));
214        self
215    }
216
217    /// Insert a resource into the world immediately.
218    pub fn add_resource(&mut self, res: impl hecs::Component) -> &mut Self {
219        self.resources.insert_resource(&mut self.world, res);
220        self
221    }
222
223    /// Borrow resource `T`, panicking if it is absent.
224    pub fn get_resource<'a, T: hecs::Component>(&'a self) -> hecs::Ref<'a, T> {
225        self.resources.get_resource(&self.world)
226    }
227
228    /// Mutably borrow resource `T`, panicking if it is absent.
229    pub fn get_resource_mut<'a, T: hecs::Component>(&'a self) -> hecs::RefMut<'a, T> {
230        self.resources.get_resource_mut(&self.world)
231    }
232
233    /// Insert resource `T` only if it is not already present.
234    ///
235    /// Returns `true` if the resource was inserted.
236    pub fn try_insert_resource<T: hecs::Component>(&mut self, res: T) -> bool {
237        self.resources.try_insert(&mut self.world, res)
238    }
239
240    /// Register a single system to run at `stage`.
241    pub fn add_system<Marker>(
242        &mut self,
243        stage: SystemStage,
244        system: impl IntoSystem<Marker> + 'static,
245    ) -> &mut Self {
246        self.systems
247            .entry(stage)
248            .or_default()
249            .push(Box::new(system.into_system()));
250        self
251    }
252
253    /// Register multiple systems to run at `stage`.
254    ///
255    /// Accepts a tuple of systems via [`IntoSystemSet`].
256    pub fn add_systems<Marker>(
257        &mut self,
258        stage: SystemStage,
259        systems: impl IntoSystemSet<Marker>,
260    ) -> &mut Self {
261        let entry = self.systems.entry(stage).or_default();
262        entry.extend(systems.into_system_set());
263        self
264    }
265
266    /// Build all plugins, run startup systems, and validate required resources.
267    ///
268    /// Plugins may register additional plugins during their `build` call; this
269    /// repeats until no new plugins are added, up to a hard limit of 64 passes
270    /// to catch accidental infinite registration cycles.
271    pub fn build(&mut self) -> &mut Self {
272        let mut iterations = 0;
273        const MAX_PLUGIN_BUILD_ITERATIONS: u32 = 64;
274
275        while !self.plugins.is_empty() {
276            iterations += 1;
277            if iterations > MAX_PLUGIN_BUILD_ITERATIONS {
278                panic!(
279                    "App::build() exceeded {MAX_PLUGIN_BUILD_ITERATIONS} plugin-registration passes — \
280                 likely a cycle where plugins keep registering each other. Check for a plugin whose \
281                 build() unconditionally re-adds itself or another plugin that re-adds it."
282                );
283            }
284            let plugins: Vec<_> = self.plugins.drain(..).collect();
285            for plugin in plugins {
286                plugin.build(self);
287            }
288        }
289
290        self.required.validate();
291
292        // Run startup systems to convergence (re-run until no new resources
293        // appear) so a deferred/async producer — a background thread pool
294        // result, a LazyResource-style construction — gets the extra passes
295        // it needs within build(), then remove them from the map so they are
296        // never re-run by update().
297        self.run_stage_to_convergence(SystemStage::Startup, 64);
298        self.systems.remove(&SystemStage::Startup);
299
300        // For synchronous backends (headless, tests, CPU-only assets), drain
301        // the asset pipeline to completion before the first frame. For windowed
302        // GPU apps the backend isn't available yet so these exit immediately.
303        self.run_stage_to_convergence(SystemStage::AssetSync, 64);
304        self.run_stage_to_convergence(SystemStage::AssetSyncDeps, 64);
305
306        // Cache the runtime stage order once — update() reads this slice every
307        // tick without allocating.
308        self.update_stages = self.systems.keys().copied().collect();
309
310        self
311    }
312
313    /// Run all non-startup systems in stage order, flushing the command buffer
314    /// after each stage. Convergent stages ([`AssetSync`](SystemStage::AssetSync),
315    /// [`AssetSyncDeps`](SystemStage::AssetSyncDeps)) are re-run until no new
316    /// resources are inserted, resolving dependency chains within a single tick.
317    pub fn update(&mut self) {
318        // Copy the stage list so the borrow on self.update_stages doesn't
319        // conflict with the &mut self needed by run_stage_once / run_stage_to_convergence.
320        // update_stages is a small, stable Vec (set once in build), so this clone
321        // is cheap and avoids unsafe splitting borrows.
322        let stages = self.update_stages.clone();
323        for stage in stages {
324            if stage.is_convergent() {
325                self.run_stage_to_convergence(stage, 64);
326            } else {
327                self.validate_stage_resources(stage);
328                self.run_stage_once(stage);
329            }
330        }
331    }
332
333    /// Replace the default runner with a custom one.
334    ///
335    /// The runner receives ownership of the `App` and is responsible for
336    /// calling [`update`](App::update) at the appropriate cadence (e.g. driven
337    /// by a window event loop).
338    pub fn set_runner<F>(&mut self, runner: F) -> &mut Self
339    where
340        F: FnOnce(App) + 'static,
341    {
342        self.runner = Some(Box::new(runner));
343        self
344    }
345
346    /// Consume the app and hand it to the configured runner.
347    ///
348    /// Panics if no runner has been set.
349    pub fn run(&mut self) {
350        let mut owned_app = std::mem::take(self);
351        let runner = owned_app.runner.take().expect("No runner found!");
352        runner(owned_app);
353    }
354}