Skip to main content

galeon_engine/
schedule.rs

1// SPDX-License-Identifier: AGPL-3.0-only OR Commercial
2
3use crate::function_system::{IntoSystem, System};
4use crate::world::World;
5
6struct SystemEntry {
7    stage: &'static str,
8    system: Box<dyn System>,
9}
10
11/// Stage-based system scheduler.
12///
13/// Systems are grouped into stages. Stages run in the order they were first
14/// registered. Within a stage, systems run in registration order.
15///
16/// Systems are parameterized functions that declare their data access via
17/// [`SystemParam`](crate::system_param::SystemParam).
18pub struct Schedule {
19    systems: Vec<SystemEntry>,
20    stage_order: Vec<&'static str>,
21}
22
23impl Schedule {
24    pub fn new() -> Self {
25        Self {
26            systems: Vec::new(),
27            stage_order: Vec::new(),
28        }
29    }
30
31    /// Add a system to a named stage.
32    ///
33    /// Accepts any function that implements [`IntoSystem`] — parameterized
34    /// functions like `fn(Res<T>, QueryMut<U>)`.
35    pub fn add_system<P>(
36        &mut self,
37        stage: &'static str,
38        name: &'static str,
39        func: impl IntoSystem<P>,
40    ) -> &mut Self {
41        if !self.stage_order.contains(&stage) {
42            self.stage_order.push(stage);
43        }
44        self.systems.push(SystemEntry {
45            stage,
46            system: func.into_system(name),
47        });
48        self
49    }
50
51    /// Run all systems in stage order.
52    ///
53    /// Execution order each tick:
54    /// 1. **Drain deadlines** — fires all overdue deadlines into Events `current`.
55    /// 2. **Advance event buffers** — `current` → `previous` (fired deadlines
56    ///    become readable), old `previous` cleared.
57    /// 3. **Run systems by stage** — `EventReader` sees fired deadlines + any
58    ///    events from the previous tick. Commands applied between stages.
59    pub fn run(&mut self, world: &mut World) {
60        // 1. Drain all overdue deadlines → writes to Events<T> current buffer.
61        world.drain_all_deadlines();
62
63        // 2. Capture deadline-fired render events from `current` BEFORE the
64        //    swap clears it. Each extractor tracks an offset into current so
65        //    it only reads events added since the last flush — no duplicates.
66        world.flush_render_events();
67
68        // 3. Advance all event buffers: current → previous, clear current.
69        //    Deadline + last-tick events move into `previous`, readable by
70        //    EventReader. Extractor offsets auto-reset when current shrinks.
71        world.update_events();
72
73        for stage_idx in 0..self.stage_order.len() {
74            let stage = self.stage_order[stage_idx];
75            for entry in &mut self.systems {
76                if entry.stage == stage {
77                    entry.system.run(world);
78                }
79            }
80            world.apply_commands();
81        }
82
83        // 4. Capture system-written render events from `current`.
84        world.flush_render_events();
85    }
86
87    /// Returns the number of registered systems.
88    pub fn system_count(&self) -> usize {
89        self.systems.len()
90    }
91
92    /// Returns the stage names in execution order.
93    pub fn stages(&self) -> &[&'static str] {
94        &self.stage_order
95    }
96}
97
98impl Default for Schedule {
99    fn default() -> Self {
100        Self::new()
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use crate::component::Component;
108    use crate::system_param::{QueryMut, Res, ResMut};
109
110    #[derive(Debug)]
111    struct Counter(u32);
112    impl Component for Counter {}
113
114    fn increment_system(mut counters: QueryMut<'_, Counter>) {
115        for (_, counter) in counters.iter_mut() {
116            counter.0 += 1;
117        }
118    }
119
120    fn double_system(mut counters: QueryMut<'_, Counter>) {
121        for (_, counter) in counters.iter_mut() {
122            counter.0 *= 2;
123        }
124    }
125
126    #[test]
127    fn schedule_runs_systems_in_stage_order() {
128        let mut world = World::new();
129        world.spawn((Counter(1),));
130
131        let mut schedule = Schedule::new();
132        schedule.add_system::<(QueryMut<'_, Counter>,)>("simulate", "increment", increment_system);
133        schedule.add_system::<(QueryMut<'_, Counter>,)>("post", "double", double_system);
134
135        schedule.run(&mut world);
136
137        // 1 + 1 = 2, then 2 * 2 = 4
138        let val: Vec<u32> = world.query::<&Counter>().map(|(_, c)| c.0).collect();
139        assert_eq!(val, vec![4]);
140    }
141
142    #[test]
143    fn schedule_systems_within_stage_run_in_order() {
144        let mut world = World::new();
145        world.spawn((Counter(1),));
146
147        let mut schedule = Schedule::new();
148        schedule.add_system::<(QueryMut<'_, Counter>,)>("simulate", "increment", increment_system);
149        schedule.add_system::<(QueryMut<'_, Counter>,)>("simulate", "double", double_system);
150
151        schedule.run(&mut world);
152
153        // 1 + 1 = 2, then 2 * 2 = 4
154        let val: Vec<u32> = world.query::<&Counter>().map(|(_, c)| c.0).collect();
155        assert_eq!(val, vec![4]);
156    }
157
158    #[test]
159    fn schedule_stage_order_matters() {
160        let mut world = World::new();
161        world.spawn((Counter(1),));
162
163        let mut schedule = Schedule::new();
164        schedule.add_system::<(QueryMut<'_, Counter>,)>("pre", "double", double_system);
165        schedule.add_system::<(QueryMut<'_, Counter>,)>("simulate", "increment", increment_system);
166
167        schedule.run(&mut world);
168
169        // 1 * 2 = 2, then 2 + 1 = 3
170        let val: Vec<u32> = world.query::<&Counter>().map(|(_, c)| c.0).collect();
171        assert_eq!(val, vec![3]);
172    }
173
174    #[test]
175    fn empty_schedule_is_safe() {
176        let mut world = World::new();
177        let mut schedule = Schedule::new();
178        schedule.run(&mut world); // no-op
179    }
180
181    // -- Parameterized system tests in the schedule --
182
183    fn param_increment(mut counters: QueryMut<'_, Counter>) {
184        for (_, c) in counters.iter_mut() {
185            c.0 += 1;
186        }
187    }
188
189    #[test]
190    fn schedule_accepts_parameterized_system() {
191        let mut world = World::new();
192        world.spawn((Counter(0),));
193
194        let mut schedule = Schedule::new();
195        schedule.add_system::<(QueryMut<'_, Counter>,)>(
196            "update",
197            "param_increment",
198            param_increment,
199        );
200
201        schedule.run(&mut world);
202
203        let val: Vec<u32> = world.query::<&Counter>().map(|(_, c)| c.0).collect();
204        assert_eq!(val, vec![1]);
205    }
206
207    struct Speed(f32);
208
209    fn apply_speed(speed: Res<'_, Speed>, mut counters: QueryMut<'_, Counter>) {
210        for (_, c) in counters.iter_mut() {
211            c.0 += speed.0 as u32;
212        }
213    }
214
215    #[test]
216    fn schedule_multi_param_systems_across_stages() {
217        let mut world = World::new();
218        world.insert_resource(Speed(10.0));
219        world.spawn((Counter(0),));
220
221        let mut schedule = Schedule::new();
222        schedule.add_system::<(QueryMut<'_, Counter>,)>("pre", "increment", increment_system);
223        schedule.add_system::<(Res<'_, Speed>, QueryMut<'_, Counter>)>(
224            "post",
225            "apply_speed",
226            apply_speed,
227        );
228
229        schedule.run(&mut world);
230
231        // 0 + 1 = 1, then 1 + 10 = 11
232        let val: Vec<u32> = world.query::<&Counter>().map(|(_, c)| c.0).collect();
233        assert_eq!(val, vec![11]);
234    }
235
236    fn increment_speed(mut speed: ResMut<'_, Speed>) {
237        speed.0 += 1.0;
238    }
239
240    #[test]
241    fn schedule_res_mut_persists_across_runs() {
242        let mut world = World::new();
243        world.insert_resource(Speed(0.0));
244
245        let mut schedule = Schedule::new();
246        schedule.add_system::<(ResMut<'_, Speed>,)>("update", "inc_speed", increment_speed);
247
248        schedule.run(&mut world);
249        schedule.run(&mut world);
250
251        assert!((world.resource::<Speed>().0 - 2.0).abs() < f32::EPSILON);
252    }
253
254    // -- Commands integration tests --
255
256    use crate::commands::Commands;
257
258    fn spawn_via_commands(mut cmds: Commands<'_>) {
259        cmds.spawn((Counter(100),));
260    }
261
262    #[test]
263    fn schedule_applies_commands_between_stages() {
264        let mut world = World::new();
265
266        // Stage "spawn" queues a deferred spawn.
267        // Stage "read" should see the spawned entity.
268        let mut schedule = Schedule::new();
269        schedule.add_system::<(Commands<'_>,)>("spawn", "spawner", spawn_via_commands);
270        schedule.add_system::<(QueryMut<'_, Counter>,)>("read", "increment", increment_system);
271
272        schedule.run(&mut world);
273
274        // Entity spawned by commands, then incremented: 100 + 1 = 101
275        let vals: Vec<u32> = world.query::<&Counter>().map(|(_, c)| c.0).collect();
276        assert_eq!(vals, vec![101]);
277    }
278
279    fn despawn_all_via_commands(
280        counters: crate::system_param::Query<'_, Counter>,
281        mut cmds: Commands<'_>,
282    ) {
283        for (entity, _) in counters.iter() {
284            cmds.despawn(entity);
285        }
286    }
287
288    #[test]
289    fn schedule_commands_despawn_visible_to_next_stage() {
290        let mut world = World::new();
291        world.spawn((Counter(1),));
292        world.spawn((Counter(2),));
293
294        let mut schedule = Schedule::new();
295        schedule.add_system::<(crate::system_param::Query<'_, Counter>, Commands<'_>)>(
296            "cleanup",
297            "despawn_all",
298            despawn_all_via_commands,
299        );
300        schedule.add_system::<(QueryMut<'_, Counter>,)>("post", "increment", increment_system);
301
302        schedule.run(&mut world);
303
304        // All entities despawned between stages — nothing to increment.
305        assert_eq!(world.entity_count(), 0);
306    }
307
308    // -------------------------------------------------------------------------
309    // Events integration test
310    // -------------------------------------------------------------------------
311
312    use crate::event::{EventReader, EventWriter};
313
314    #[derive(Debug, PartialEq)]
315    struct ScoreEvent {
316        points: u32,
317    }
318
319    fn produce_event(mut writer: EventWriter<'_, ScoreEvent>) {
320        writer.send(ScoreEvent { points: 10 });
321    }
322
323    fn consume_event(reader: EventReader<'_, ScoreEvent>, mut counters: QueryMut<'_, Counter>) {
324        let total: u32 = reader.read().map(|e| e.points).sum();
325        for (_, counter) in counters.iter_mut() {
326            counter.0 += total;
327        }
328    }
329
330    #[test]
331    fn schedule_event_writer_reader_cross_tick() {
332        let mut world = World::new();
333        world.add_event::<ScoreEvent>();
334        world.spawn((Counter(0),));
335
336        let mut schedule = Schedule::new();
337        // System A writes events in stage "produce".
338        schedule.add_system::<(EventWriter<'_, ScoreEvent>,)>("produce", "produce", produce_event);
339        // System B reads events in stage "consume".
340        schedule.add_system::<(EventReader<'_, ScoreEvent>, QueryMut<'_, Counter>)>(
341            "consume",
342            "consume",
343            consume_event,
344        );
345
346        // Run 1: system A sends the event (goes to current buffer).
347        // update_events runs at the start, but current is empty — nothing moves.
348        schedule.run(&mut world);
349
350        // Counter unchanged: no events were in previous during run 1.
351        let val: Vec<u32> = world.query::<&Counter>().map(|(_, c)| c.0).collect();
352        assert_eq!(val, vec![0]);
353
354        // Run 2: update_events moves run-1's current → previous.
355        // System B can now read the ScoreEvent(10) and adds 10 to the counter.
356        schedule.run(&mut world);
357
358        let val: Vec<u32> = world.query::<&Counter>().map(|(_, c)| c.0).collect();
359        assert_eq!(val, vec![10]);
360    }
361
362    #[test]
363    fn schedule_events_cleared_after_two_ticks() {
364        let mut world = World::new();
365        world.add_event::<ScoreEvent>();
366        world.spawn((Counter(0),));
367
368        let mut schedule = Schedule::new();
369        schedule.add_system::<(EventWriter<'_, ScoreEvent>,)>("produce", "produce", produce_event);
370        schedule.add_system::<(EventReader<'_, ScoreEvent>, QueryMut<'_, Counter>)>(
371            "consume",
372            "consume",
373            consume_event,
374        );
375
376        // Tick 1: event sent to current.
377        schedule.run(&mut world);
378        // Tick 2: event moves to previous, reader adds 10.
379        schedule.run(&mut world);
380        // Tick 3: previous cleared (run 3's update_events), new event sent.
381        //         Reader adds 10 again (from run 2's send).
382        schedule.run(&mut world);
383
384        // After tick 3 the counter has 10 (tick 2 read) + 10 (tick 3 read) = 20.
385        let val: Vec<u32> = world.query::<&Counter>().map(|(_, c)| c.0).collect();
386        assert_eq!(val, vec![20]);
387    }
388
389    // -------------------------------------------------------------------------
390    // Render event accumulation tests
391    // -------------------------------------------------------------------------
392
393    use crate::render_event::{RenderEvent, RenderEventRegistry};
394
395    #[derive(Debug)]
396    struct ImpactRenderEvent {
397        entity_index: u32,
398    }
399
400    impl RenderEvent for ImpactRenderEvent {
401        const KIND: u32 = 1;
402        fn entity(&self) -> u32 {
403            self.entity_index
404        }
405        fn position(&self) -> [f32; 3] {
406            [0.0; 3]
407        }
408    }
409
410    #[test]
411    fn schedule_flush_captures_system_written_render_events() {
412        let mut world = World::new();
413        world.add_event::<ImpactRenderEvent>();
414
415        let mut registry = RenderEventRegistry::new();
416        registry.register::<ImpactRenderEvent>();
417        world.insert_resource(registry);
418
419        fn emit_impact(mut writer: EventWriter<'_, ImpactRenderEvent>) {
420            writer.send(ImpactRenderEvent { entity_index: 42 });
421        }
422
423        let mut schedule = Schedule::new();
424        schedule.add_system::<(EventWriter<'_, ImpactRenderEvent>,)>("sim", "emit", emit_impact);
425
426        schedule.run(&mut world);
427
428        // Flush happened at end of schedule.run() — drain should have the event.
429        let events = world.resource::<RenderEventRegistry>().drain();
430        assert_eq!(events.len(), 1);
431        assert_eq!(events[0].entity, 42);
432    }
433
434    #[test]
435    fn schedule_flush_captures_deadline_fired_render_events() {
436        use crate::deadline::{Clock, Deadlines, TestClock, Timestamp};
437
438        let mut world = World::new();
439        world.add_event::<ImpactRenderEvent>();
440        world.add_deadline_type::<ImpactRenderEvent>();
441
442        // Install a clock so drain_all_deadlines actually fires.
443        world
444            .insert_resource(Box::new(TestClock::new(Timestamp::from_micros(0))) as Box<dyn Clock>);
445
446        let mut registry = RenderEventRegistry::new();
447        registry.register::<ImpactRenderEvent>();
448        world.insert_resource(registry);
449
450        // Schedule a deadline at time 0 — clock is at 0, so it fires immediately.
451        world
452            .resource_mut::<Deadlines<ImpactRenderEvent>>()
453            .schedule(
454                Timestamp::from_micros(0),
455                ImpactRenderEvent { entity_index: 99 },
456            );
457
458        let mut schedule = Schedule::new();
459        schedule.run(&mut world);
460
461        // Pre-swap flush captured the deadline event before update_events cleared current.
462        let events = world.resource::<RenderEventRegistry>().drain();
463        assert_eq!(events.len(), 1);
464        assert_eq!(events[0].entity, 99);
465    }
466
467    #[test]
468    fn multi_tick_render_events_accumulate_across_schedule_runs() {
469        let mut world = World::new();
470        world.add_event::<ImpactRenderEvent>();
471
472        let mut registry = RenderEventRegistry::new();
473        registry.register::<ImpactRenderEvent>();
474        world.insert_resource(registry);
475
476        fn emit_impact(mut writer: EventWriter<'_, ImpactRenderEvent>) {
477            writer.send(ImpactRenderEvent { entity_index: 1 });
478        }
479
480        let mut schedule = Schedule::new();
481        schedule.add_system::<(EventWriter<'_, ImpactRenderEvent>,)>("sim", "emit", emit_impact);
482
483        // Two ticks without draining — simulates multi-tick frame.
484        schedule.run(&mut world);
485        schedule.run(&mut world);
486
487        let events = world.resource::<RenderEventRegistry>().drain();
488        assert_eq!(events.len(), 2);
489    }
490}