Skip to main content

concinnity_core/app/
mod.rs

1//! The headless driver: a world, the fixed virtual timestep it runs on, and the
2//! loop that steps the two together.
3//!
4//! Time here is virtual. Every tick publishes the same [`SimTiming`] budget and
5//! steps the world once, with no sleep and no wall clock, so a run is
6//! reproducible and goes as fast as the host can step it. Pacing a frame
7//! against a display, accumulating real elapsed time, and catching a signal are
8//! a windowed host's concerns; a host that has them drives the world itself.
9//!
10//! [`Driver`] is that seam: [`App`] implements it with nothing underneath, and
11//! a host with an operating system implements it over its own loop.
12//!
13//! [`SimTiming`]: crate::ecs::SimTiming
14
15#[cfg(debug_assertions)]
16mod alloc_guard;
17mod driver;
18mod fixed_timestep;
19
20#[cfg(test)]
21mod driver_tests;
22#[cfg(test)]
23mod headless_world_tests;
24#[cfg(test)]
25mod run_tests;
26
27use crate::ecs::{StepResult, SystemTable, World};
28use crate::result::CnResult;
29use fixed_timestep::FixedTimestep;
30
31pub use driver::Driver;
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34enum AppStatus {
35    Created,
36    Started,
37}
38
39/// A world and the headless loop that runs it.
40///
41/// [`run`](App::run) drives the world until a system stops it or its last
42/// system finishes; [`run_for`](App::run_for) is the bounded form, for a test
43/// or a tool that wants a known number of ticks. Both start the world if the
44/// caller has not.
45///
46/// In dev builds the loop holds itself to a steady state of no allocation per
47/// tick, which is asserted rather than assumed. See [`run`](App::run).
48pub struct App {
49    world: World,
50    // The systems the world is started with. A host that contributes none runs
51    // its world's content and nothing over it.
52    table: &'static SystemTable,
53    status: AppStatus,
54    sim: FixedTimestep,
55    #[cfg(debug_assertions)]
56    allocs: alloc_guard::AllocGuard,
57}
58
59impl core::fmt::Debug for App {
60    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
61        f.debug_struct("App")
62            .field("ticks", &self.sim.ticks())
63            .field("status", &self.status)
64            .finish_non_exhaustive()
65    }
66}
67
68impl App {
69    /// An app that runs `world` with no systems over it: what a caller gets
70    /// when the host contributes no system table.
71    pub fn from_world(world: World) -> Self {
72        Self::with_systems(world, &SystemTable::EMPTY)
73    }
74
75    /// An app that runs `world` under `table`, which is what gives it systems.
76    pub fn with_systems(world: World, table: &'static SystemTable) -> Self {
77        Self {
78            world,
79            table,
80            status: AppStatus::Created,
81            sim: FixedTimestep::default(),
82            #[cfg(debug_assertions)]
83            allocs: alloc_guard::AllocGuard::new(),
84        }
85    }
86
87    /// Borrow the world being run.
88    pub fn world(&self) -> &World {
89        &self.world
90    }
91
92    /// Ticks stepped so far.
93    pub fn ticks(&self) -> u64 {
94        self.sim.ticks()
95    }
96
97    /// Build the world's systems and run their `init`. Runs once: a second
98    /// call is [`InvalidState`](CnResult::InvalidState) rather than a second
99    /// `init` over the running world.
100    pub fn start(&mut self) -> Result<(), CnResult> {
101        if self.status != AppStatus::Created {
102            return Err(CnResult::InvalidState);
103        }
104        self.world.start(self.table)?;
105        self.status = AppStatus::Started;
106        Ok(())
107    }
108
109    /// Step until a system stops the world or the last one finishes, returning
110    /// which of the two ended the run.
111    ///
112    /// Under `debug_assertions` the loop asserts its own steady state: past a
113    /// warmup, a settled world's tick allocates nothing, and a stretch of ticks
114    /// that all allocate is a cost that would recur every frame for the life of
115    /// the app, so the run panics naming the tick that allocated least. The
116    /// counters behind that are process-wide, so the check stands down where it
117    /// cannot trust them: where no binary installed the tracking allocator, and
118    /// where another thread is allocating alongside the loop.
119    pub fn run(&mut self) -> Result<StepResult, CnResult> {
120        self.start_if_created()?;
121        loop {
122            let result = self.tick();
123            if result != StepResult::Continue {
124                return Ok(result);
125            }
126        }
127    }
128
129    /// Step at most `ticks` times, returning what the last tick reported:
130    /// `Continue` when the full count ran, `Stop` or `Done` when the world
131    /// ended the run early. The bounded form of [`run`](App::run).
132    pub fn run_for(&mut self, ticks: u64) -> Result<StepResult, CnResult> {
133        self.start_if_created()?;
134        let mut result = StepResult::Continue;
135        for _ in 0..ticks {
136            result = self.tick();
137            if result != StepResult::Continue {
138                break;
139            }
140        }
141        Ok(result)
142    }
143
144    // Start the world unless the caller already did, so a run is one call.
145    fn start_if_created(&mut self) -> Result<(), CnResult> {
146        if self.status == AppStatus::Created {
147            self.start()?;
148        }
149        Ok(())
150    }
151
152    // One tick: the frame's fixed timing budget, then the world's step. The
153    // allocation invariant brackets both, since publishing the budget is as
154    // much a per-tick cost as stepping is.
155    fn tick(&mut self) -> StepResult {
156        #[cfg(debug_assertions)]
157        self.allocs.begin_tick();
158        self.world.insert_resource(self.sim.advance());
159        let result = self.world.step();
160        #[cfg(debug_assertions)]
161        self.allocs.end_tick();
162        result
163    }
164}