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