use super::*;
use crate::world::World;
use std::collections::HashSet;
pub struct SystemBatch {
pub(crate) systems: Vec<Box<dyn System>>,
pub(crate) metas: Vec<crate::system::config::SystemMeta>,
pub access_info: AccessInfo,
}
impl Default for SystemBatch {
fn default() -> Self {
Self::new()
}
}
impl SystemBatch {
pub fn new() -> Self {
Self {
systems: Vec::new(),
metas: Vec::new(),
access_info: AccessInfo::new(),
}
}
pub(crate) fn add_system_with_meta(
&mut self,
system: Box<dyn System>,
config_info: AccessInfo,
meta: crate::system::config::SystemMeta,
) {
let mut sys_info = system.access_info();
sys_info.component_reads.extend(config_info.component_reads);
sys_info
.component_writes
.extend(config_info.component_writes);
sys_info.resource_reads.extend(config_info.resource_reads);
sys_info.resource_writes.extend(config_info.resource_writes);
sys_info.is_exclusive = sys_info.is_exclusive || config_info.is_exclusive;
self.access_info
.component_reads
.extend(sys_info.component_reads);
self.access_info
.component_writes
.extend(sys_info.component_writes);
self.access_info
.resource_reads
.extend(sys_info.resource_reads);
self.access_info
.resource_writes
.extend(sys_info.resource_writes);
self.access_info.is_exclusive = self.access_info.is_exclusive || sys_info.is_exclusive;
self.systems.push(system);
self.metas.push(meta);
}
pub fn is_compatible(&self, system: &dyn System, config_info: &AccessInfo) -> bool {
let mut sys_info = system.access_info();
sys_info
.component_reads
.extend(config_info.component_reads.iter().cloned());
sys_info
.component_writes
.extend(config_info.component_writes.iter().cloned());
sys_info
.resource_reads
.extend(config_info.resource_reads.iter().cloned());
sys_info
.resource_writes
.extend(config_info.resource_writes.iter().cloned());
sys_info.is_exclusive = sys_info.is_exclusive || config_info.is_exclusive;
self.access_info.is_compatible_with(&sys_info)
}
}
pub struct SetConfig {
pub name: &'static str,
pub before: Vec<&'static str>,
pub after: Vec<&'static str>,
pub phase: Option<Phase>,
}
impl SetConfig {
pub fn new<S: SystemSet>() -> Self {
Self {
name: S::set_name(),
before: Vec::new(),
after: Vec::new(),
phase: None,
}
}
pub fn before<S: SystemSet>(mut self) -> Self {
self.before.push(S::set_name());
self
}
pub fn after<S: SystemSet>(mut self) -> Self {
self.after.push(S::set_name());
self
}
pub fn in_phase(mut self, phase: Phase) -> Self {
self.phase = Some(phase);
self
}
}
pub struct Schedule {
unbuilt_configs: Vec<SystemConfig>,
set_configs: std::collections::HashMap<&'static str, SetConfig>,
pub(crate) phase_batches: Vec<(Phase, Vec<SystemBatch>)>,
pub(crate) legacy_batches: Vec<SystemBatch>,
pub(crate) uses_phases: bool,
last_run_tick: u32,
}
impl Schedule {
pub fn new() -> Self {
Self {
unbuilt_configs: Vec::new(),
set_configs: std::collections::HashMap::new(),
phase_batches: Vec::new(),
legacy_batches: Vec::new(),
uses_phases: false,
last_run_tick: 0,
}
}
pub fn configure_set(&mut self, config: SetConfig) {
self.set_configs.insert(config.name, config);
self.invalidate();
}
pub fn add_di_system<Params, S: IntoSystemConfig<Params>>(&mut self, system: S) {
self.unbuilt_configs.push(system.into_config());
self.invalidate();
}
pub fn add_system<S: System + 'static>(&mut self, system: S) {
self.unbuilt_configs
.push(SystemConfig::new(Box::new(system)));
self.invalidate();
}
pub fn add_systems<T, Configs: IntoSystemConfigs<T>>(&mut self, configs: Configs) {
configs.into_configs(self);
}
pub fn add_system_boxed(&mut self, system: Box<dyn System>) {
self.unbuilt_configs.push(SystemConfig::new(system));
self.invalidate();
}
fn invalidate(&mut self) {
let phase_batches = std::mem::take(&mut self.phase_batches);
let legacy_batches = std::mem::take(&mut self.legacy_batches);
let mut recovered = 0usize;
for batch in phase_batches
.into_iter()
.flat_map(|(_, batches)| batches)
.chain(legacy_batches)
{
debug_assert_eq!(batch.systems.len(), batch.metas.len());
for (system, meta) in batch.systems.into_iter().zip(batch.metas) {
self.unbuilt_configs
.push(SystemConfig::from_parts(system, meta));
recovered += 1;
}
}
if recovered > 0 {
tracing::debug!(
recovered_systems = recovered,
"Schedule modified after it was built: {recovered} already-compiled system(s) returned to the pending list and will be rebuilt on the next run.",
);
}
}
fn is_built(&self) -> bool {
!self.phase_batches.is_empty() || !self.legacy_batches.is_empty()
}
pub fn validate(&mut self) {
self.build();
}
fn build_batches_for(configs: Vec<SystemConfig>) -> Vec<SystemBatch> {
let count = configs.len();
if count == 0 {
return Vec::new();
}
let mut edge_set: HashSet<(usize, usize)> = HashSet::new();
let mut adj = vec![Vec::new(); count];
let mut in_degree = vec![0usize; count];
let add_edge = |from: usize,
to: usize,
edge_set: &mut HashSet<(usize, usize)>,
adj: &mut Vec<Vec<usize>>,
in_degree: &mut Vec<usize>| {
if edge_set.insert((from, to)) {
adj[from].push(to);
in_degree[to] += 1;
}
};
for i in 0..count {
for before_label in &configs[i].before {
let mut found = false;
for (j, config_j) in configs.iter().enumerate() {
if i != j && config_j.labels.contains(before_label) {
add_edge(i, j, &mut edge_set, &mut adj, &mut in_degree);
found = true;
}
}
if !found {
crate::gizmo_log!(
Warning,
"[Schedule] Sistem {}'in before('{}') label'ı eşleşmiyor!",
i,
before_label
);
}
}
for after_label in &configs[i].after {
let mut found = false;
for (j, config_j) in configs.iter().enumerate() {
if i != j && config_j.labels.contains(after_label) {
add_edge(j, i, &mut edge_set, &mut adj, &mut in_degree);
found = true;
}
}
if !found {
crate::gizmo_log!(
Warning,
"[Schedule] Sistem {}'in after('{}') label'ı eşleşmiyor!",
i,
after_label
);
}
}
}
let mut queue = std::collections::VecDeque::new();
for (i, deg) in in_degree.iter().enumerate() {
if *deg == 0 {
queue.push_back(i);
}
}
let mut sorted_indices = Vec::with_capacity(count);
while let Some(node) = queue.pop_front() {
sorted_indices.push(node);
for &neighbor in &adj[node] {
in_degree[neighbor] -= 1;
if in_degree[neighbor] == 0 {
queue.push_back(neighbor);
}
}
}
if sorted_indices.len() != count {
tracing::error!(
system_count = count,
sorted = sorted_indices.len(),
"[Schedule] cyclic system dependency detected — topological sort incomplete"
);
panic!(
"Cyclic dependency detected! {} sistemin {} tanesi sıralanabildi.",
count,
sorted_indices.len()
);
}
let mut predecessors = vec![Vec::<usize>::new(); count];
for (from, neighbors) in adj.iter().enumerate() {
for &to in neighbors {
predecessors[to].push(from);
}
}
let mut dummy_configs: Vec<Option<SystemConfig>> = configs.into_iter().map(Some).collect();
let mut batches: Vec<SystemBatch> = Vec::new();
let mut system_batch = vec![0usize; count];
for &idx in &sorted_indices {
let config = dummy_configs[idx].take().unwrap();
let meta = config.pristine_meta.clone().unwrap_or_else(|| config.snapshot_meta());
let earliest = predecessors[idx]
.iter()
.map(|&pred| system_batch[pred] + 1)
.max()
.unwrap_or(0);
let placed = (earliest..batches.len())
.rev()
.find(|&bidx| batches[bidx].is_compatible(&*config.system, &config.added_info));
let batch_idx = if let Some(bidx) = placed {
batches[bidx].add_system_with_meta(config.system, config.added_info, meta);
bidx
} else {
let new_idx = batches.len();
let mut new_batch = SystemBatch::new();
new_batch.add_system_with_meta(config.system, config.added_info, meta);
batches.push(new_batch);
new_idx
};
system_batch[idx] = batch_idx;
}
batches
}
pub fn build(&mut self) {
if self.is_built() {
return;
}
let mut configs = std::mem::take(&mut self.unbuilt_configs);
if configs.is_empty() {
return;
}
let system_count = configs.len();
for config in &mut configs {
config.pristine_meta = Some(config.snapshot_meta());
}
for config in &mut configs {
for set_name in &config.in_sets {
if let Some(set_cfg) = self.set_configs.get(set_name) {
config.before.extend(set_cfg.before.iter().copied());
config.after.extend(set_cfg.after.iter().copied());
if let Some(phase) = set_cfg.phase {
config.phase = phase;
}
}
}
}
let has_explicit_phase = configs.iter().any(|c| c.phase != Phase::Update);
self.uses_phases = has_explicit_phase;
if has_explicit_phase {
let mut phase_groups: std::collections::BTreeMap<Phase, Vec<SystemConfig>> =
std::collections::BTreeMap::new();
for config in configs {
phase_groups.entry(config.phase).or_default().push(config);
}
for (phase, group) in phase_groups {
let batches = Self::build_batches_for(group);
if !batches.is_empty() {
self.phase_batches.push((phase, batches));
}
}
} else {
self.legacy_batches = Self::build_batches_for(configs);
}
let batch_count: usize = if self.uses_phases {
self.phase_batches.iter().map(|(_, b)| b.len()).sum()
} else {
self.legacy_batches.len()
};
tracing::debug!(
system_count,
batch_count,
uses_phases = self.uses_phases,
"[Schedule] built system DAG into parallel batches"
);
}
fn run_batches(batches: &mut [SystemBatch], world: &mut World, dt: f32) {
#[cfg(not(target_arch = "wasm32"))]
use rayon::prelude::*;
#[cfg(target_arch = "wasm32")]
use crate::parallel_compat::*;
for batch in batches.iter_mut() {
batch.systems.par_iter_mut().for_each(|system| {
system.run(world, dt);
});
let queue_clone = world
.get_resource::<crate::commands::CommandQueue>()
.filter(|q| !q.is_empty())
.map(|q| (*q).clone());
if let Some(queue) = queue_clone {
queue.apply(world);
}
}
}
#[tracing::instrument(skip_all, name = "ecs_update")]
pub fn run(&mut self, world: &mut World, dt: f32) {
if !self.is_built() && !self.unbuilt_configs.is_empty() {
self.build();
}
world.begin_change_frame(self.last_run_tick);
if self.uses_phases {
for (_phase, batches) in &mut self.phase_batches {
let _span = tracing::info_span!("phase", name = _phase.name()).entered();
Self::run_batches(batches, world, dt);
}
} else {
Self::run_batches(&mut self.legacy_batches, world, dt);
}
self.last_run_tick = world.tick;
if let Some(mut profiler) = world.get_resource_mut::<crate::profiler::FrameProfiler>() {
profiler.end_frame();
}
}
#[cfg(test)]
pub(crate) fn total_batch_count(&self) -> usize {
if self.uses_phases {
self.phase_batches.iter().map(|(_, b)| b.len()).sum()
} else {
self.legacy_batches.len()
}
}
}
impl Default for Schedule {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod modify_after_build {
use super::*;
use crate::world::World;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
#[test]
fn a_system_added_after_the_first_run_does_not_drop_the_earlier_ones() {
let first = Arc::new(AtomicU32::new(0));
let second = Arc::new(AtomicU32::new(0));
let (f, s) = (first.clone(), second.clone());
let mut schedule = Schedule::new();
schedule.add_system(move |_w: &World, _dt: f32| {
f.fetch_add(1, Ordering::Relaxed);
});
let mut world = World::new();
schedule.run(&mut world, 0.016); assert_eq!(first.load(Ordering::Relaxed), 1, "the first system must run once");
schedule.add_system(move |_w: &World, _dt: f32| {
s.fetch_add(1, Ordering::Relaxed);
});
schedule.run(&mut world, 0.016);
assert_eq!(
second.load(Ordering::Relaxed),
1,
"the newly added system must run"
);
assert_eq!(
first.load(Ordering::Relaxed),
2,
"the system registered BEFORE the first run must survive the rebuild and run again; \
1 here means `invalidate()` dropped it, which is the bug this test was written for"
);
}
#[test]
fn ordering_constraints_survive_a_rebuild() {
let log: Arc<std::sync::Mutex<Vec<&'static str>>> =
Arc::new(std::sync::Mutex::new(Vec::new()));
let (l1, l2) = (log.clone(), log.clone());
let mut schedule = Schedule::new();
schedule.add_di_system(
(move || { l1.lock().unwrap().push("late"); })
.label("late")
.after("early"),
);
schedule.add_di_system(
(move || { l2.lock().unwrap().push("early"); })
.label("early"),
);
let mut world = World::new();
schedule.run(&mut world, 0.016);
assert_eq!(*log.lock().unwrap(), vec!["early", "late"], "before the rebuild");
log.lock().unwrap().clear();
schedule.add_system(|_w: &World, _dt: f32| {});
schedule.run(&mut world, 0.016);
assert_eq!(
*log.lock().unwrap(),
vec!["early", "late"],
"after the rebuild the `after(\"early\")` edge must still hold; an empty or lost \
metadata round-trip would let these run in either order"
);
}
#[test]
fn configuring_a_set_after_the_first_run_keeps_the_systems() {
let ran = Arc::new(AtomicU32::new(0));
let r = ran.clone();
let mut schedule = Schedule::new();
schedule.add_system(move |_w: &World, _dt: f32| {
r.fetch_add(1, Ordering::Relaxed);
});
let mut world = World::new();
schedule.run(&mut world, 0.016);
assert_eq!(ran.load(Ordering::Relaxed), 1);
schedule.configure_set(SetConfig {
name: "a_set_nothing_belongs_to",
before: Vec::new(),
after: Vec::new(),
phase: None,
});
schedule.run(&mut world, 0.016);
assert_eq!(
ran.load(Ordering::Relaxed),
2,
"configuring a set must not empty the schedule; 1 here means the rebuild dropped \
every system it had already compiled"
);
}
}