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}