use std::collections::HashMap;
use std::time::Duration;
use super::world::World;
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct SystemLabel(pub String);
impl SystemLabel {
pub fn new(label: impl Into<String>) -> Self {
Self(label.into())
}
}
impl<S: Into<String>> From<S> for SystemLabel {
fn from(s: S) -> Self {
Self(s.into())
}
}
impl std::fmt::Display for SystemLabel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum SystemStage {
PreUpdate = 0,
Update = 1,
PostUpdate = 2,
PreRender = 3,
Render = 4,
PostRender = 5,
Startup = 10,
Shutdown = 11,
}
impl SystemStage {
pub fn frame_stages() -> &'static [SystemStage] {
&[
SystemStage::PreUpdate,
SystemStage::Update,
SystemStage::PostUpdate,
SystemStage::PreRender,
SystemStage::Render,
SystemStage::PostRender,
]
}
pub fn name(&self) -> &'static str {
match self {
SystemStage::PreUpdate => "PreUpdate",
SystemStage::Update => "Update",
SystemStage::PostUpdate => "PostUpdate",
SystemStage::PreRender => "PreRender",
SystemStage::Render => "Render",
SystemStage::PostRender => "PostRender",
SystemStage::Startup => "Startup",
SystemStage::Shutdown => "Shutdown",
}
}
}
pub type System = Box<dyn FnMut(&mut World) + Send + Sync>;
struct SystemEntry {
label: Option<SystemLabel>,
after: Vec<SystemLabel>,
before: Vec<SystemLabel>,
system: System,
parallel_hint: bool,
has_run: bool,
run_count: u64,
total_time_ns: u64,
}
impl SystemEntry {
fn new(system: System) -> Self {
Self {
label: None,
after: Vec::new(),
before: Vec::new(),
system,
parallel_hint: false,
has_run: false,
run_count: 0,
total_time_ns: 0,
}
}
fn run(&mut self, world: &mut World) {
let start = std::time::Instant::now();
(self.system)(world);
let elapsed = start.elapsed().as_nanos() as u64;
self.run_count += 1;
self.has_run = true;
self.total_time_ns += elapsed;
}
fn average_time_us(&self) -> f64 {
if self.run_count == 0 { return 0.0; }
self.total_time_ns as f64 / self.run_count as f64 / 1000.0
}
}
impl std::fmt::Debug for SystemEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SystemEntry")
.field("label", &self.label)
.field("after", &self.after)
.field("run_count", &self.run_count)
.field("avg_us", &self.average_time_us())
.finish()
}
}
struct StageData {
entries: Vec<SystemEntry>,
sorted: bool,
}
impl StageData {
fn new() -> Self {
Self { entries: Vec::new(), sorted: false }
}
fn add(&mut self, entry: SystemEntry) {
self.sorted = false;
self.entries.push(entry);
}
fn sort(&mut self) {
if self.sorted { return; }
let n = self.entries.len();
if n <= 1 { self.sorted = true; return; }
let label_map: HashMap<SystemLabel, usize> = self.entries
.iter()
.enumerate()
.filter_map(|(i, e)| e.label.as_ref().map(|l| (l.clone(), i)))
.collect();
let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
let mut in_degree: Vec<usize> = vec![0; n];
for (i, entry) in self.entries.iter().enumerate() {
for after in &entry.after {
if let Some(&j) = label_map.get(after) {
adj[j].push(i);
in_degree[i] += 1;
}
}
for before in &entry.before {
if let Some(&j) = label_map.get(before) {
adj[i].push(j);
in_degree[j] += 1;
}
}
}
let mut queue: std::collections::VecDeque<usize> = (0..n)
.filter(|&i| in_degree[i] == 0)
.collect();
let mut order = Vec::with_capacity(n);
while let Some(i) = queue.pop_front() {
order.push(i);
for &j in &adj[i] {
in_degree[j] -= 1;
if in_degree[j] == 0 {
queue.push_back(j);
}
}
}
if order.len() != n {
eprintln!("ECS Schedule: cycle detected in system ordering, using insertion order");
self.sorted = true;
return;
}
let mut new_entries: Vec<Option<SystemEntry>> = self.entries.drain(..).map(Some).collect();
self.entries = order.into_iter().map(|i| new_entries[i].take().unwrap()).collect();
self.sorted = true;
}
fn run_all(&mut self, world: &mut World) {
self.sort();
for entry in &mut self.entries {
entry.run(world);
}
}
fn total_systems(&self) -> usize {
self.entries.len()
}
}
impl std::fmt::Debug for StageData {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StageData")
.field("systems", &self.entries.len())
.finish()
}
}
pub struct FixedTimestep {
pub hz: f64,
accumulator: f64,
pub max_steps_per_frame: usize,
pub steps_taken: u64,
system: System,
pub label: Option<SystemLabel>,
}
impl FixedTimestep {
pub fn new(hz: f64, system: impl FnMut(&mut World) + Send + Sync + 'static) -> Self {
Self {
hz,
accumulator: 0.0,
max_steps_per_frame: 10,
steps_taken: 0,
system: Box::new(system),
label: None,
}
}
pub fn with_label(mut self, label: impl Into<SystemLabel>) -> Self {
self.label = Some(label.into());
self
}
pub fn with_max_steps(mut self, max: usize) -> Self {
self.max_steps_per_frame = max;
self
}
pub fn tick(&mut self, dt: f64, world: &mut World) -> usize {
self.accumulator += dt;
let step = 1.0 / self.hz;
let mut steps = 0;
while self.accumulator >= step && steps < self.max_steps_per_frame {
(self.system)(world);
self.accumulator -= step;
steps += 1;
self.steps_taken += 1;
}
steps
}
pub fn interpolation_alpha(&self) -> f64 {
self.accumulator * self.hz
}
}
impl std::fmt::Debug for FixedTimestep {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FixedTimestep")
.field("hz", &self.hz)
.field("accumulator", &self.accumulator)
.field("steps_taken", &self.steps_taken)
.finish()
}
}
pub struct SystemSet {
pub label: SystemLabel,
pub stage: SystemStage,
pub enabled: bool,
systems: Vec<System>,
}
impl SystemSet {
pub fn new(label: impl Into<SystemLabel>, stage: SystemStage) -> Self {
Self {
label: label.into(),
stage,
enabled: true,
systems: Vec::new(),
}
}
pub fn add(mut self, system: impl FnMut(&mut World) + Send + Sync + 'static) -> Self {
self.systems.push(Box::new(system));
self
}
pub fn run_all(&mut self, world: &mut World) {
if !self.enabled { return; }
for sys in &mut self.systems {
sys(world);
}
}
pub fn len(&self) -> usize {
self.systems.len()
}
pub fn enable(&mut self) { self.enabled = true; }
pub fn disable(&mut self) { self.enabled = false; }
}
impl std::fmt::Debug for SystemSet {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SystemSet")
.field("label", &self.label)
.field("stage", &self.stage)
.field("enabled", &self.enabled)
.field("systems", &self.systems.len())
.finish()
}
}
pub struct Schedule {
stages: HashMap<SystemStage, StageData>,
fixed: Vec<FixedTimestep>,
sets: Vec<SystemSet>,
startup_done: bool,
frame_count: u64,
pub diagnostics_enabled: bool,
frame_times_ns: std::collections::VecDeque<u64>,
frame_times_limit: usize,
}
impl Schedule {
pub fn new() -> Self {
let mut stages = HashMap::new();
for &stage in SystemStage::frame_stages() {
stages.insert(stage, StageData::new());
}
stages.insert(SystemStage::Startup, StageData::new());
stages.insert(SystemStage::Shutdown, StageData::new());
Self {
stages,
fixed: Vec::new(),
sets: Vec::new(),
startup_done: false,
frame_count: 0,
diagnostics_enabled: false,
frame_times_ns: std::collections::VecDeque::new(),
frame_times_limit: 128,
}
}
pub fn add_system(
&mut self,
stage: SystemStage,
system: impl FnMut(&mut World) + Send + Sync + 'static,
) {
let entry = SystemEntry::new(Box::new(system));
self.stages
.entry(stage)
.or_insert_with(StageData::new)
.add(entry);
}
pub fn add_system_with_label(
&mut self,
stage: SystemStage,
label: impl Into<SystemLabel>,
system: impl FnMut(&mut World) + Send + Sync + 'static,
) {
let mut entry = SystemEntry::new(Box::new(system));
entry.label = Some(label.into());
self.stages
.entry(stage)
.or_insert_with(StageData::new)
.add(entry);
}
pub fn add_system_after(
&mut self,
stage: SystemStage,
after_label: impl Into<SystemLabel>,
system: impl FnMut(&mut World) + Send + Sync + 'static,
) {
let mut entry = SystemEntry::new(Box::new(system));
entry.after.push(after_label.into());
self.stages
.entry(stage)
.or_insert_with(StageData::new)
.add(entry);
}
pub fn add_system_before(
&mut self,
stage: SystemStage,
before_label: impl Into<SystemLabel>,
system: impl FnMut(&mut World) + Send + Sync + 'static,
) {
let mut entry = SystemEntry::new(Box::new(system));
entry.before.push(before_label.into());
self.stages
.entry(stage)
.or_insert_with(StageData::new)
.add(entry);
}
pub fn add_system_ordered(
&mut self,
stage: SystemStage,
label: Option<impl Into<SystemLabel>>,
after: Vec<SystemLabel>,
before: Vec<SystemLabel>,
system: impl FnMut(&mut World) + Send + Sync + 'static,
) {
let mut entry = SystemEntry::new(Box::new(system));
entry.label = label.map(|l| l.into());
entry.after = after;
entry.before = before;
self.stages
.entry(stage)
.or_insert_with(StageData::new)
.add(entry);
}
pub fn add_startup_system(
&mut self,
system: impl FnMut(&mut World) + Send + Sync + 'static,
) {
self.add_system(SystemStage::Startup, system);
}
pub fn add_shutdown_system(
&mut self,
system: impl FnMut(&mut World) + Send + Sync + 'static,
) {
self.add_system(SystemStage::Shutdown, system);
}
pub fn add_fixed_system(
&mut self,
hz: f64,
system: impl FnMut(&mut World) + Send + Sync + 'static,
) {
self.fixed.push(FixedTimestep::new(hz, system));
}
pub fn add_fixed_system_with_label(
&mut self,
hz: f64,
label: impl Into<SystemLabel>,
system: impl FnMut(&mut World) + Send + Sync + 'static,
) {
self.fixed.push(FixedTimestep::new(hz, system).with_label(label));
}
pub fn add_set(&mut self, set: SystemSet) {
self.sets.push(set);
}
pub fn mark_parallel(
&mut self,
stage: SystemStage,
label: &SystemLabel,
) {
if let Some(data) = self.stages.get_mut(&stage) {
for entry in &mut data.entries {
if entry.label.as_ref() == Some(label) {
entry.parallel_hint = true;
}
}
}
}
pub fn run(&mut self, world: &mut World) {
let frame_start = std::time::Instant::now();
if !self.startup_done {
self.run_stage(SystemStage::Startup, world);
self.startup_done = true;
}
for &stage in SystemStage::frame_stages() {
self.run_stage(stage, world);
}
world.advance_tick();
self.frame_count += 1;
if self.diagnostics_enabled {
let elapsed = frame_start.elapsed().as_nanos() as u64;
if self.frame_times_ns.len() >= self.frame_times_limit {
self.frame_times_ns.pop_front();
}
self.frame_times_ns.push_back(elapsed);
}
}
pub fn run_with_dt(&mut self, world: &mut World, dt: f64) {
let frame_start = std::time::Instant::now();
if !self.startup_done {
self.run_stage(SystemStage::Startup, world);
self.startup_done = true;
}
for fixed in &mut self.fixed {
fixed.tick(dt, world);
}
for &stage in SystemStage::frame_stages() {
self.run_stage(stage, world);
}
for set in &mut self.sets {
set.run_all(world);
}
world.advance_tick();
self.frame_count += 1;
if self.diagnostics_enabled {
let elapsed = frame_start.elapsed().as_nanos() as u64;
if self.frame_times_ns.len() >= self.frame_times_limit {
self.frame_times_ns.pop_front();
}
self.frame_times_ns.push_back(elapsed);
}
}
pub fn run_stage(&mut self, stage: SystemStage, world: &mut World) {
if let Some(data) = self.stages.get_mut(&stage) {
data.run_all(world);
}
}
pub fn shutdown(&mut self, world: &mut World) {
self.run_stage(SystemStage::Shutdown, world);
}
pub fn system_count(&self, stage: SystemStage) -> usize {
self.stages.get(&stage).map_or(0, |d| d.total_systems())
}
pub fn total_system_count(&self) -> usize {
self.stages.values().map(|d| d.total_systems()).sum()
}
pub fn frame_count(&self) -> u64 {
self.frame_count
}
pub fn average_frame_time_ms(&self) -> f64 {
if self.frame_times_ns.is_empty() { return 0.0; }
let sum: u64 = self.frame_times_ns.iter().sum();
sum as f64 / self.frame_times_ns.len() as f64 / 1_000_000.0
}
pub fn enable_diagnostics(&mut self) {
self.diagnostics_enabled = true;
}
pub fn is_started(&self) -> bool {
self.startup_done
}
pub fn set_enabled(&mut self, label: &SystemLabel, enabled: bool) {
for set in &mut self.sets {
if &set.label == label {
set.enabled = enabled;
}
}
}
pub fn remove_system(&mut self, stage: SystemStage, label: &SystemLabel) {
if let Some(data) = self.stages.get_mut(&stage) {
data.entries.retain(|e| e.label.as_ref() != Some(label));
data.sorted = false;
}
}
pub fn print_diagnostics(&self) {
println!("=== Schedule Diagnostics ===");
println!("Frames: {}", self.frame_count);
println!("Avg frame: {:.2} ms", self.average_frame_time_ms());
for &stage in SystemStage::frame_stages() {
if let Some(data) = self.stages.get(&stage) {
println!(
" Stage {:12}: {:3} systems",
stage.name(),
data.total_systems()
);
for entry in &data.entries {
let label = entry.label.as_ref()
.map(|l| l.0.as_str())
.unwrap_or("<unlabeled>");
println!(
" [{:20}] runs={:6} avg={:.2} µs",
label,
entry.run_count,
entry.average_time_us()
);
}
}
}
}
}
impl Default for Schedule {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for Schedule {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Schedule")
.field("frame_count", &self.frame_count)
.field("total_systems", &self.total_system_count())
.field("fixed_systems", &self.fixed.len())
.field("startup_done", &self.startup_done)
.finish()
}
}
pub struct ScheduleBuilder {
schedule: Schedule,
}
impl ScheduleBuilder {
pub fn new() -> Self {
Self { schedule: Schedule::new() }
}
pub fn system(mut self, stage: SystemStage, f: impl FnMut(&mut World) + Send + Sync + 'static) -> Self {
self.schedule.add_system(stage, f);
self
}
pub fn labeled_system(
mut self,
stage: SystemStage,
label: impl Into<SystemLabel>,
f: impl FnMut(&mut World) + Send + Sync + 'static,
) -> Self {
self.schedule.add_system_with_label(stage, label, f);
self
}
pub fn startup(mut self, f: impl FnMut(&mut World) + Send + Sync + 'static) -> Self {
self.schedule.add_startup_system(f);
self
}
pub fn fixed(mut self, hz: f64, f: impl FnMut(&mut World) + Send + Sync + 'static) -> Self {
self.schedule.add_fixed_system(hz, f);
self
}
pub fn with_diagnostics(mut self) -> Self {
self.schedule.enable_diagnostics();
self
}
pub fn build(self) -> Schedule {
self.schedule
}
}
impl Default for ScheduleBuilder {
fn default() -> Self {
Self::new()
}
}
pub trait RunCriteria: Send + Sync + 'static {
fn should_run(&mut self, world: &World) -> bool;
}
pub struct Always;
impl RunCriteria for Always {
fn should_run(&mut self, _: &World) -> bool { true }
}
pub struct Never;
impl RunCriteria for Never {
fn should_run(&mut self, _: &World) -> bool { false }
}
pub struct RunOnce {
ran: bool,
}
impl RunOnce {
pub fn new() -> Self { Self { ran: false } }
}
impl Default for RunOnce {
fn default() -> Self { Self::new() }
}
impl RunCriteria for RunOnce {
fn should_run(&mut self, _: &World) -> bool {
if self.ran { return false; }
self.ran = true;
true
}
}
pub struct EveryNFrames {
n: u64,
current: u64,
}
impl EveryNFrames {
pub fn new(n: u64) -> Self { Self { n, current: 0 } }
}
impl RunCriteria for EveryNFrames {
fn should_run(&mut self, _: &World) -> bool {
self.current += 1;
if self.current >= self.n {
self.current = 0;
true
} else {
false
}
}
}
pub struct WhenResource<T: super::world::Resource> {
_marker: std::marker::PhantomData<T>,
}
impl<T: super::world::Resource> WhenResource<T> {
pub fn new() -> Self { Self { _marker: std::marker::PhantomData } }
}
impl<T: super::world::Resource> Default for WhenResource<T> {
fn default() -> Self { Self::new() }
}
impl<T: super::world::Resource> RunCriteria for WhenResource<T> {
fn should_run(&mut self, world: &World) -> bool {
world.has_resource::<T>()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, Clone, PartialEq)]
struct Counter(i32);
#[test]
fn test_basic_schedule_run() {
let mut world = World::new();
world.insert_resource(Counter(0));
let mut schedule = Schedule::new();
schedule.add_system(SystemStage::Update, |w: &mut World| {
w.resource_mut::<Counter>().0 += 1;
});
schedule.run(&mut world);
assert_eq!(world.resource::<Counter>().0, 1);
schedule.run(&mut world);
assert_eq!(world.resource::<Counter>().0, 2);
}
#[test]
fn test_startup_system_runs_once() {
let mut world = World::new();
world.insert_resource(Counter(0));
let mut schedule = Schedule::new();
schedule.add_startup_system(|w: &mut World| {
w.resource_mut::<Counter>().0 += 100;
});
schedule.add_system(SystemStage::Update, |w: &mut World| {
w.resource_mut::<Counter>().0 += 1;
});
schedule.run(&mut world);
assert_eq!(world.resource::<Counter>().0, 101);
schedule.run(&mut world);
assert_eq!(world.resource::<Counter>().0, 102); }
#[test]
fn test_stage_ordering() {
let mut world = World::new();
let order = std::sync::Arc::new(std::sync::Mutex::new(Vec::<&'static str>::new()));
let mut schedule = Schedule::new();
let o1 = order.clone();
schedule.add_system(SystemStage::PostUpdate, move |_| { o1.lock().unwrap().push("PostUpdate"); });
let o2 = order.clone();
schedule.add_system(SystemStage::PreUpdate, move |_| { o2.lock().unwrap().push("PreUpdate"); });
let o3 = order.clone();
schedule.add_system(SystemStage::Update, move |_| { o3.lock().unwrap().push("Update"); });
schedule.run(&mut world);
let result = order.lock().unwrap().clone();
assert_eq!(result, vec!["PreUpdate", "Update", "PostUpdate"]);
}
#[test]
fn test_system_label_ordering() {
let mut world = World::new();
let order = std::sync::Arc::new(std::sync::Mutex::new(Vec::<i32>::new()));
let mut schedule = Schedule::new();
let o1 = order.clone();
schedule.add_system_with_label(SystemStage::Update, "second", move |_| {
o1.lock().unwrap().push(2);
});
let o2 = order.clone();
let mut e = SystemEntry::new(Box::new(move |_: &mut World| {
o2.lock().unwrap().push(1);
}));
e.label = Some("first".into());
e.before = vec!["second".into()];
schedule.stages.get_mut(&SystemStage::Update).unwrap().add(e);
schedule.run(&mut world);
let result = order.lock().unwrap().clone();
assert_eq!(result, vec![1, 2]);
}
#[test]
fn test_fixed_timestep() {
let mut world = World::new();
world.insert_resource(Counter(0));
let mut ts = FixedTimestep::new(10.0, |w: &mut World| {
w.resource_mut::<Counter>().0 += 1;
});
ts.tick(0.25, &mut world);
assert_eq!(world.resource::<Counter>().0, 2);
ts.tick(0.1, &mut world);
assert_eq!(world.resource::<Counter>().0, 3);
}
#[test]
fn test_schedule_builder() {
let mut world = World::new();
world.insert_resource(Counter(0));
let mut schedule = ScheduleBuilder::new()
.startup(|w: &mut World| { w.resource_mut::<Counter>().0 += 10; })
.system(SystemStage::Update, |w: &mut World| { w.resource_mut::<Counter>().0 += 1; })
.build();
schedule.run(&mut world);
assert_eq!(world.resource::<Counter>().0, 11);
}
#[test]
fn test_system_count() {
let mut schedule = Schedule::new();
schedule.add_system(SystemStage::Update, |_: &mut World| {});
schedule.add_system(SystemStage::Update, |_: &mut World| {});
schedule.add_system(SystemStage::PreUpdate, |_: &mut World| {});
assert_eq!(schedule.system_count(SystemStage::Update), 2);
assert_eq!(schedule.system_count(SystemStage::PreUpdate), 1);
assert_eq!(schedule.total_system_count(), 3);
}
#[test]
fn test_remove_system() {
let mut world = World::new();
world.insert_resource(Counter(0));
let mut schedule = Schedule::new();
schedule.add_system_with_label(SystemStage::Update, "counter", |w: &mut World| {
w.resource_mut::<Counter>().0 += 1;
});
schedule.run(&mut world);
assert_eq!(world.resource::<Counter>().0, 1);
schedule.remove_system(SystemStage::Update, &"counter".into());
schedule.run(&mut world);
assert_eq!(world.resource::<Counter>().0, 1); }
#[test]
fn test_system_set() {
let mut world = World::new();
world.insert_resource(Counter(0));
let o = std::sync::Arc::new(std::sync::Mutex::new(Vec::<i32>::new()));
let o1 = o.clone();
let o2 = o.clone();
let set = SystemSet::new("my_set", SystemStage::Update)
.add(move |_: &mut World| { o1.lock().unwrap().push(1); })
.add(move |_: &mut World| { o2.lock().unwrap().push(2); });
let mut schedule = Schedule::new();
schedule.add_set(set);
schedule.run_with_dt(&mut world, 0.016);
let result = o.lock().unwrap().clone();
assert_eq!(result, vec![1, 2]);
}
#[test]
fn test_run_once_criterion() {
let mut world = World::new();
let mut criterion = RunOnce::new();
assert!(criterion.should_run(&world));
assert!(!criterion.should_run(&world));
assert!(!criterion.should_run(&world));
}
#[test]
fn test_every_n_frames() {
let world = World::new();
let mut criterion = EveryNFrames::new(3);
assert!(!criterion.should_run(&world)); assert!(!criterion.should_run(&world)); assert!(criterion.should_run(&world)); assert!(!criterion.should_run(&world)); assert!(!criterion.should_run(&world)); assert!(criterion.should_run(&world)); }
#[test]
fn test_world_tick_advances() {
let mut world = World::new();
let mut schedule = Schedule::new();
assert_eq!(world.tick(), 0);
schedule.run(&mut world);
assert_eq!(world.tick(), 1);
schedule.run(&mut world);
assert_eq!(world.tick(), 2);
}
}