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 system.run(&self.world, &self.resources);
131 }
132 }
133 self.resources.get_command_buffer().run_on(&mut self.world);
134
135 self.resources.generation() != gen_before
136 }
137
138 /// Panic before running `stage` if any of its systems declares a hard
139 /// [`Res`](crate::ecs::system::Res)/[`ResMut`](crate::ecs::system::ResMut)
140 /// requirement on a resource that isn't present yet.
141 ///
142 /// Only applied to non-convergent stages: convergent stages
143 /// ([`Startup`](SystemStage::Startup), [`AssetSync`](SystemStage::AssetSync),
144 /// [`AssetSyncDeps`](SystemStage::AssetSyncDeps)) are re-run precisely
145 /// because a resource may not exist yet on an early pass — their systems
146 /// are expected to use `Option<Res<T>>`/`Option<ResMut<T>>` to wait for
147 /// it, so a hard requirement there is checked (and will panic normally)
148 /// only once actually fetched.
149 fn validate_stage_resources(&self, stage: SystemStage) {
150 if stage.is_convergent() {
151 return;
152 }
153
154 let Some(systems) = self.systems.get(&stage) else {
155 return;
156 };
157
158 let mut missing: Vec<&'static str> = Vec::new();
159 for system in systems {
160 for req in system.requires() {
161 if !(req.present)(&self.world, &self.resources) {
162 missing.push(req.name);
163 }
164 }
165 }
166
167 if !missing.is_empty() {
168 missing.sort_unstable();
169 missing.dedup();
170 panic!(
171 "{stage:?}: system(s) require resource(s) that are not yet available:\n{}\n\n\
172 Insert these via App::add_resource, or a Startup/AssetSync system, before \
173 this stage runs.",
174 missing
175 .iter()
176 .map(|m| format!(" - {m}"))
177 .collect::<Vec<_>>()
178 .join("\n")
179 );
180 }
181 }
182
183 /// Re-run `stage` until a full pass produces no new resources, up to
184 /// `max_passes`. Logs a warning if the limit is reached — that usually
185 /// means a [`LazyResource`](crate::assets::singleton_asset::LazyResource)
186 /// or [`Asset`](crate::assets::upload::Asset) dependency is permanently
187 /// unsatisfiable.
188 fn run_stage_to_convergence(&mut self, stage: SystemStage, max_passes: u32) {
189 for pass in 0..max_passes {
190 if !self.run_stage_once(stage) {
191 return;
192 }
193 if pass == max_passes - 1 {
194 tracing::warn!(
195 "{stage:?}: convergence did not settle after {max_passes} passes — \
196 a dependency may be permanently unsatisfiable. Check for a \
197 LazyResource whose construct() or an Asset whose upload() \
198 always returns None."
199 );
200 }
201 }
202 }
203
204 /// Queue a plugin to be built during [`build`](App::build).
205 pub fn add_plugin(&mut self, plugin: impl Plugin) -> &mut Self {
206 self.plugins.push(Box::new(plugin));
207 self
208 }
209
210 /// Insert a resource into the world immediately.
211 pub fn add_resource(&mut self, res: impl hecs::Component) -> &mut Self {
212 self.resources.insert_resource(&mut self.world, res);
213 self
214 }
215
216 /// Borrow resource `T`, panicking if it is absent.
217 pub fn get_resource<'a, T: hecs::Component>(&'a self) -> hecs::Ref<'a, T> {
218 self.resources.get_resource(&self.world)
219 }
220
221 /// Mutably borrow resource `T`, panicking if it is absent.
222 pub fn get_resource_mut<'a, T: hecs::Component>(&'a self) -> hecs::RefMut<'a, T> {
223 self.resources.get_resource_mut(&self.world)
224 }
225
226 /// Insert resource `T` only if it is not already present.
227 ///
228 /// Returns `true` if the resource was inserted.
229 pub fn try_insert_resource<T: hecs::Component>(&mut self, res: T) -> bool {
230 self.resources.try_insert(&mut self.world, res)
231 }
232
233 /// Register a single system to run at `stage`.
234 pub fn add_system<Marker>(
235 &mut self,
236 stage: SystemStage,
237 system: impl IntoSystem<Marker> + 'static,
238 ) -> &mut Self {
239 self.systems
240 .entry(stage)
241 .or_default()
242 .push(Box::new(system.into_system()));
243 self
244 }
245
246 /// Register multiple systems to run at `stage`.
247 ///
248 /// Accepts a tuple of systems via [`IntoSystemSet`].
249 pub fn add_systems<Marker>(
250 &mut self,
251 stage: SystemStage,
252 systems: impl IntoSystemSet<Marker>,
253 ) -> &mut Self {
254 let entry = self.systems.entry(stage).or_default();
255 entry.extend(systems.into_system_set());
256 self
257 }
258
259 /// Build all plugins, run startup systems, and validate required resources.
260 ///
261 /// Plugins may register additional plugins during their `build` call; this
262 /// repeats until no new plugins are added, up to a hard limit of 64 passes
263 /// to catch accidental infinite registration cycles.
264 pub fn build(&mut self) -> &mut Self {
265 let mut iterations = 0;
266 const MAX_PLUGIN_BUILD_ITERATIONS: u32 = 64;
267
268 while !self.plugins.is_empty() {
269 iterations += 1;
270 if iterations > MAX_PLUGIN_BUILD_ITERATIONS {
271 panic!(
272 "App::build() exceeded {MAX_PLUGIN_BUILD_ITERATIONS} plugin-registration passes — \
273 likely a cycle where plugins keep registering each other. Check for a plugin whose \
274 build() unconditionally re-adds itself or another plugin that re-adds it."
275 );
276 }
277 let plugins: Vec<_> = self.plugins.drain(..).collect();
278 for plugin in plugins {
279 plugin.build(self);
280 }
281 }
282
283 self.required.validate();
284
285 // Run startup systems to convergence (re-run until no new resources
286 // appear) so a deferred/async producer — a background thread pool
287 // result, a LazyResource-style construction — gets the extra passes
288 // it needs within build(), then remove them from the map so they are
289 // never re-run by update().
290 self.run_stage_to_convergence(SystemStage::Startup, 64);
291 self.systems.remove(&SystemStage::Startup);
292
293 // For synchronous backends (headless, tests, CPU-only assets), drain
294 // the asset pipeline to completion before the first frame. For windowed
295 // GPU apps the backend isn't available yet so these exit immediately.
296 self.run_stage_to_convergence(SystemStage::AssetSync, 64);
297 self.run_stage_to_convergence(SystemStage::AssetSyncDeps, 64);
298
299 // Cache the runtime stage order once — update() reads this slice every
300 // tick without allocating.
301 self.update_stages = self.systems.keys().copied().collect();
302
303 self
304 }
305
306 /// Run all non-startup systems in stage order, flushing the command buffer
307 /// after each stage. Convergent stages ([`AssetSync`](SystemStage::AssetSync),
308 /// [`AssetSyncDeps`](SystemStage::AssetSyncDeps)) are re-run until no new
309 /// resources are inserted, resolving dependency chains within a single tick.
310 pub fn update(&mut self) {
311 // Copy the stage list so the borrow on self.update_stages doesn't
312 // conflict with the &mut self needed by run_stage_once / run_stage_to_convergence.
313 // update_stages is a small, stable Vec (set once in build), so this clone
314 // is cheap and avoids unsafe splitting borrows.
315 let stages = self.update_stages.clone();
316 for stage in stages {
317 if stage.is_convergent() {
318 self.run_stage_to_convergence(stage, 64);
319 } else {
320 self.validate_stage_resources(stage);
321 self.run_stage_once(stage);
322 }
323 }
324 }
325
326 /// Replace the default runner with a custom one.
327 ///
328 /// The runner receives ownership of the `App` and is responsible for
329 /// calling [`update`](App::update) at the appropriate cadence (e.g. driven
330 /// by a window event loop).
331 pub fn set_runner<F>(&mut self, runner: F) -> &mut Self
332 where
333 F: FnOnce(App) + 'static,
334 {
335 self.runner = Some(Box::new(runner));
336 self
337 }
338
339 /// Consume the app and hand it to the configured runner.
340 ///
341 /// Panics if no runner has been set.
342 pub fn run(&mut self) {
343 let mut owned_app = std::mem::take(self);
344 let runner = owned_app.runner.take().expect("No runner found!");
345 runner(owned_app);
346 }
347}