use bevy_ecs::{
component::{ComponentId, Components},
schedule::{
ApplyDeferred, ConditionWithAccess, InternedScheduleLabel, NodeId, Schedule,
ScheduleBuildMetadata, Schedules,
},
system::SystemStateFlags,
};
use bevy_platform::collections::{hash_map::Entry, HashMap};
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct AppData {
pub schedules: Vec<ScheduleData>,
}
impl AppData {
pub fn from_schedules(
schedules: &Schedules,
world_components: &Components,
label_to_build_metadata: &HashMap<InternedScheduleLabel, ScheduleBuildMetadata>,
) -> Result<Self, ExtractAppDataError> {
Ok(Self {
schedules: schedules
.iter()
.map(|(_, schedule)| {
ScheduleData::from_schedule(
schedule,
world_components,
label_to_build_metadata.get(&schedule.label()),
)
})
.collect::<Result<_, ExtractAppDataError>>()?,
})
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ScheduleData {
pub name: String,
pub systems: Vec<SystemData>,
pub system_sets: Vec<SystemSetData>,
pub hierarchy: Vec<(SystemSetIndex, ScheduleIndex)>,
pub dependency: Vec<(ScheduleIndex, ScheduleIndex)>,
pub components: Vec<ComponentData>,
pub conflicts: Vec<SystemConflict>,
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
pub struct ComponentData {
pub name: String,
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
pub struct SystemData {
pub name: String,
pub apply_deferred: bool,
pub exclusive: bool,
pub deferred: bool,
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
pub struct SystemSetData {
pub name: String,
pub conditions: Vec<ConditionData>,
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
pub struct ConditionData {
pub name: String,
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum ScheduleIndex {
System(u32),
SystemSet(u32),
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
pub struct SystemConflict {
pub system_1: u32,
pub system_2: u32,
pub conflicting_access: AccessConflict,
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
pub enum AccessConflict {
World,
Components(Vec<u32>),
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Copy, PartialOrd, Ord)]
pub struct SystemSetIndex(pub u32);
impl ScheduleData {
pub fn from_schedule(
schedule: &Schedule,
world_components: &Components,
build_metadata: Option<&ScheduleBuildMetadata>,
) -> Result<Self, ExtractAppDataError> {
let graph = schedule.graph();
let mut system_key_to_index = HashMap::new();
let mut system_set_key_to_index = HashMap::new();
fn extract_condition_data(conditions: &[ConditionWithAccess]) -> Vec<ConditionData> {
conditions
.iter()
.map(|condition| ConditionData {
name: format!("{}", condition.condition.name()),
})
.collect()
}
let systems = schedule
.systems()
.map_err(|_| {
ExtractAppDataError::ScheduleNotInitialized(format!("{:?}", schedule.label()))
})?
.enumerate()
.map(|(index, (key, system))| {
system_key_to_index.insert(key, index);
let flags = system.flags();
SystemData {
name: format!("{}", system.name()),
apply_deferred: system.system_type()
== core::any::TypeId::of::<ApplyDeferred>(),
exclusive: flags.contains(SystemStateFlags::EXCLUSIVE),
deferred: flags.contains(SystemStateFlags::DEFERRED),
}
})
.collect();
let system_sets = graph
.system_sets
.iter()
.enumerate()
.map(|(index, (key, system_set, conditions))| {
system_set_key_to_index.insert(key, index);
SystemSetData {
name: format!("{:?}", system_set),
conditions: extract_condition_data(conditions),
}
})
.collect();
let node_id_to_schedule_index = |node_id: NodeId| match node_id {
NodeId::System(key) => ScheduleIndex::System(
*system_key_to_index
.get(&key)
.expect("the system this key refers to should have already been seen")
as _,
),
NodeId::Set(key) => ScheduleIndex::SystemSet(
*system_set_key_to_index
.get(&key)
.expect("the system set this key refers to should have already been seen")
as _,
),
};
let hierarchy = graph
.hierarchy()
.graph()
.all_edges()
.map(|(parent, child)| {
let parent = system_set_key_to_index
.get(
&parent
.as_set()
.expect("the parent of a system/set is always a set"),
)
.expect("the system set this key refers to should have already been seen");
let child = node_id_to_schedule_index(child);
(SystemSetIndex(*parent as _), child)
})
.collect();
let mut dependency = graph
.dependency()
.graph()
.all_edges()
.map(|(a, b)| (node_id_to_schedule_index(a), node_id_to_schedule_index(b)))
.collect::<Vec<_>>();
if let Some(build_metadata) = build_metadata {
dependency.extend(
build_metadata
.edges_added_by_build_passes
.iter()
.map(|(a, b)| {
(
node_id_to_schedule_index(NodeId::System(*a)),
node_id_to_schedule_index(NodeId::System(*b)),
)
}),
);
}
let mut component_id_to_index = HashMap::<ComponentId, usize>::new();
let mut components = vec![];
let conflicts = graph
.conflicting_systems()
.iter()
.map(|(system_1, system_2, conflicts)| {
let system_1 = system_key_to_index
.get(system_1)
.expect("the system this key refers to should have already been seen");
let system_2 = system_key_to_index
.get(system_2)
.expect("the system this key refers to should have already been seen");
SystemConflict {
system_1: *system_1 as _,
system_2: *system_2 as _,
conflicting_access: if conflicts.is_empty() {
AccessConflict::World
} else {
AccessConflict::Components(
conflicts
.iter()
.map(|id| match component_id_to_index.entry(*id) {
Entry::Occupied(entry) => *entry.get() as _,
Entry::Vacant(entry) => {
let component = world_components.get_info(*id).expect(
"the component has already been registered by the system",
);
components.push(ComponentData {
name: format!("{}", component.name()),
});
*entry.insert(components.len() - 1) as _
}
})
.collect(),
)
},
}
})
.collect();
Ok(Self {
name: format!("{:?}", schedule.label()),
components,
systems,
system_sets,
hierarchy,
dependency,
conflicts,
})
}
}
#[derive(Error, Debug)]
pub enum ExtractAppDataError {
#[error("executable schedule has not been created for label \"{0}\"")]
ScheduleNotInitialized(String),
}
#[cfg(test)]
pub mod tests {
use bevy_app::{App, Update};
use bevy_ecs::{
component::Component,
query::{With, Without},
schedule::{IntoScheduleConfigs, Schedules, SystemSet},
system::{Commands, Query},
};
use bevy_platform::collections::HashMap;
use crate::schedule_data::serde::{
AccessConflict, AppData, ComponentData, ExtractAppDataError, ScheduleData, ScheduleIndex,
SystemConflict, SystemData, SystemSetData, SystemSetIndex,
};
fn app_data_from_app(app: &mut App) -> Result<AppData, ExtractAppDataError> {
let schedules = app.world_mut().resource::<Schedules>();
let interned_labels = schedules
.iter()
.map(|(_, schedule)| schedule.label())
.collect::<Vec<_>>();
let mut label_to_build_metadata = HashMap::new();
for label in interned_labels {
let build_metadata = app
.world_mut()
.schedule_scope(label, |world, schedule| schedule.initialize(world))
.unwrap()
.unwrap();
label_to_build_metadata.insert(label, build_metadata);
}
let mut app_data = AppData::from_schedules(
app.world().resource::<Schedules>(),
app.world().components(),
&label_to_build_metadata,
)?;
remove_module_paths(&mut app_data);
sort_app_data(&mut app_data);
Ok(app_data)
}
pub fn remove_module_paths(app_data: &mut AppData) {
for schedule in app_data.schedules.iter_mut() {
for system in schedule.systems.iter_mut() {
system.name = system.name.rsplit_once(":").unwrap().1.to_string();
}
for set in schedule.system_sets.iter_mut() {
let name_modless = set
.name
.rsplit_once(":")
.map(|(_, suffix)| suffix)
.unwrap_or(set.name.as_str())
.to_string();
if set.name.starts_with("SystemTypeSet") {
set.name = format!("SystemTypeSet:{name_modless}");
} else {
set.name = name_modless;
}
}
for component in schedule.components.iter_mut() {
component.name = component.name.rsplit_once(":").unwrap().1.to_string();
}
}
}
pub fn sort_app_data(app_data: &mut AppData) {
app_data
.schedules
.sort_by_key(|schedule| schedule.name.clone());
app_data.schedules.iter_mut().for_each(sort_schedule);
fn sort_schedule(schedule: &mut ScheduleData) {
fn reorder_slice<T, K: Ord>(
slice: &mut [T],
key_fn: impl Fn(&T) -> K,
) -> HashMap<usize, usize> {
let mut mapping = (0..slice.len()).collect::<Vec<_>>();
mapping.sort_by_key(|index| key_fn(&slice[*index]));
slice.sort_by_key(key_fn);
mapping
.into_iter()
.enumerate()
.map(|(new, old)| (old, new))
.collect()
}
let system_old_index_to_new_index =
reorder_slice(&mut schedule.systems, |system| system.name.clone());
let system_set_old_index_to_new_index =
reorder_slice(&mut schedule.system_sets, |set| set.name.clone());
let component_old_index_to_new_index =
reorder_slice(&mut schedule.components, |component| component.name.clone());
let reindex_system = |index: &mut u32| {
*index = *system_old_index_to_new_index
.get(&(*index as usize))
.unwrap() as u32;
};
let reindex_system_set = |index: &mut u32| {
*index = *system_set_old_index_to_new_index
.get(&(*index as usize))
.unwrap() as u32;
};
let reindex_schedule_index = |index: &mut ScheduleIndex| match index {
ScheduleIndex::System(system) => reindex_system(system),
ScheduleIndex::SystemSet(set) => reindex_system_set(set),
};
let reindex_component = |index: &mut u32| {
*index = *component_old_index_to_new_index
.get(&(*index as usize))
.unwrap() as u32;
};
for set in schedule.system_sets.iter_mut() {
set.conditions
.sort_by_key(|condition| condition.name.clone());
}
for (parent, child) in schedule.hierarchy.iter_mut() {
reindex_system_set(&mut parent.0);
reindex_schedule_index(child);
}
schedule.hierarchy.sort();
for (parent, child) in schedule.dependency.iter_mut() {
reindex_schedule_index(parent);
reindex_schedule_index(child);
}
schedule.dependency.sort();
for conflict in schedule.conflicts.iter_mut() {
reindex_system(&mut conflict.system_1);
reindex_system(&mut conflict.system_2);
if conflict.system_1 > conflict.system_2 {
core::mem::swap(&mut conflict.system_1, &mut conflict.system_2);
}
match &mut conflict.conflicting_access {
AccessConflict::World => {}
AccessConflict::Components(components) => {
components.iter_mut().for_each(reindex_component);
components.sort();
}
};
}
schedule
.conflicts
.sort_by_key(|conflict| (conflict.system_1, conflict.system_2));
}
}
pub fn simple_system(name: &str) -> SystemData {
SystemData {
name: name.into(),
apply_deferred: false,
exclusive: false,
deferred: false,
}
}
pub fn simple_system_set(name: &str) -> SystemSetData {
SystemSetData {
name: name.into(),
conditions: vec![],
}
}
pub fn simple_component(name: &str) -> ComponentData {
ComponentData { name: name.into() }
}
pub fn conflict(
system_1: u32,
system_2: u32,
conflicting_access: AccessConflict,
) -> SystemConflict {
SystemConflict {
system_1,
system_2,
conflicting_access,
}
}
#[derive(SystemSet, Hash, PartialEq, Eq, Clone)]
struct MySet<const NUM: u32>;
impl<const NUM: u32> core::fmt::Debug for MySet<NUM> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "MySet<{NUM}>")
}
}
#[derive(Component)]
struct MyComponent<const NUM: u32>;
#[test]
fn linear() {
let mut app = App::empty();
fn a() {}
fn b() {}
fn c() {}
app.add_systems(Update, (a, b, c).chain());
let data = app_data_from_app(&mut app).unwrap();
assert_eq!(data.schedules.len(), 1);
let schedule = &data.schedules[0];
assert_eq!(schedule.name, "Update");
assert_eq!(
schedule.systems,
[simple_system("a"), simple_system("b"), simple_system("c"),]
);
assert_eq!(
schedule.system_sets,
[
simple_system_set("SystemTypeSet:a"),
simple_system_set("SystemTypeSet:b"),
simple_system_set("SystemTypeSet:c"),
]
);
assert_eq!(
schedule.hierarchy,
[
(SystemSetIndex(0), ScheduleIndex::System(0)),
(SystemSetIndex(1), ScheduleIndex::System(1)),
(SystemSetIndex(2), ScheduleIndex::System(2)),
]
);
assert_eq!(
schedule.dependency,
[
(ScheduleIndex::System(0), ScheduleIndex::System(1)),
(ScheduleIndex::System(1), ScheduleIndex::System(2)),
]
);
assert_eq!(schedule.components.len(), 0);
assert_eq!(schedule.conflicts.len(), 0);
}
#[test]
fn linear_with_system_sets() {
let mut app = App::empty();
app.configure_sets(Update, (MySet::<0>, MySet::<1>, MySet::<2>).chain());
let data = app_data_from_app(&mut app).unwrap();
assert_eq!(data.schedules.len(), 1);
let schedule = &data.schedules[0];
assert_eq!(schedule.name, "Update");
assert_eq!(schedule.systems, []);
assert_eq!(
schedule.system_sets,
[
simple_system_set("MySet<0>"),
simple_system_set("MySet<1>"),
simple_system_set("MySet<2>"),
]
);
assert_eq!(schedule.hierarchy, []);
assert_eq!(
schedule.dependency,
[
(ScheduleIndex::SystemSet(0), ScheduleIndex::SystemSet(1)),
(ScheduleIndex::SystemSet(1), ScheduleIndex::SystemSet(2)),
]
);
assert_eq!(schedule.components.len(), 0);
assert_eq!(schedule.conflicts.len(), 0);
}
#[test]
fn stack_of_system_sets() {
let mut app = App::empty();
fn a() {}
app.add_systems(Update, a.in_set(MySet::<0>))
.configure_sets(Update, MySet::<0>.in_set(MySet::<1>))
.configure_sets(Update, MySet::<1>.in_set(MySet::<2>));
let data = app_data_from_app(&mut app).unwrap();
assert_eq!(data.schedules.len(), 1);
let schedule = &data.schedules[0];
assert_eq!(schedule.name, "Update");
assert_eq!(schedule.systems, [simple_system("a")]);
assert_eq!(
schedule.system_sets,
[
simple_system_set("MySet<0>"),
simple_system_set("MySet<1>"),
simple_system_set("MySet<2>"),
simple_system_set("SystemTypeSet:a"),
]
);
assert_eq!(
schedule.hierarchy,
[
(SystemSetIndex(0), ScheduleIndex::System(0)),
(SystemSetIndex(1), ScheduleIndex::SystemSet(0)),
(SystemSetIndex(2), ScheduleIndex::SystemSet(1)),
(SystemSetIndex(3), ScheduleIndex::System(0)),
]
);
assert_eq!(schedule.dependency, []);
assert_eq!(schedule.components.len(), 0);
assert_eq!(schedule.conflicts.len(), 0);
}
#[test]
fn records_system_kind_flags() {
let mut app = App::empty();
fn a0(_commands: Commands) {}
fn a1(_commands: Commands) {}
fn b0() {}
fn b1() {}
fn c0() {}
fn c1() {}
app.add_systems(Update, (((a0, a1), (b0, b1)).chain(), (c0, c1).chain()));
let data = app_data_from_app(&mut app).unwrap();
assert_eq!(data.schedules.len(), 1);
let schedule = &data.schedules[0];
assert_eq!(schedule.name, "Update");
assert_eq!(
schedule.systems,
[
SystemData {
name: "a0".into(),
apply_deferred: false,
exclusive: false,
deferred: true,
},
SystemData {
name: "a1".into(),
apply_deferred: false,
exclusive: false,
deferred: true,
},
SystemData {
name: "apply_deferred".into(),
apply_deferred: true,
exclusive: true,
deferred: false,
},
simple_system("b0"),
simple_system("b1"),
simple_system("c0"),
simple_system("c1"),
]
);
assert_eq!(
schedule.system_sets,
[
simple_system_set("SystemTypeSet:a0"),
simple_system_set("SystemTypeSet:a1"),
simple_system_set("SystemTypeSet:b0"),
simple_system_set("SystemTypeSet:b1"),
simple_system_set("SystemTypeSet:c0"),
simple_system_set("SystemTypeSet:c1"),
]
);
assert_eq!(
schedule.hierarchy,
[
(SystemSetIndex(0), ScheduleIndex::System(0)),
(SystemSetIndex(1), ScheduleIndex::System(1)),
(SystemSetIndex(2), ScheduleIndex::System(3)),
(SystemSetIndex(3), ScheduleIndex::System(4)),
(SystemSetIndex(4), ScheduleIndex::System(5)),
(SystemSetIndex(5), ScheduleIndex::System(6)),
]
);
assert_eq!(
schedule.dependency,
[
(ScheduleIndex::System(0), ScheduleIndex::System(2)),
(ScheduleIndex::System(0), ScheduleIndex::System(3)),
(ScheduleIndex::System(0), ScheduleIndex::System(4)),
(ScheduleIndex::System(1), ScheduleIndex::System(2)),
(ScheduleIndex::System(1), ScheduleIndex::System(3)),
(ScheduleIndex::System(1), ScheduleIndex::System(4)),
(ScheduleIndex::System(2), ScheduleIndex::System(3)),
(ScheduleIndex::System(2), ScheduleIndex::System(4)),
(ScheduleIndex::System(5), ScheduleIndex::System(6)),
]
);
assert_eq!(schedule.components.len(), 0);
assert_eq!(schedule.conflicts.len(), 0);
}
#[test]
fn records_conflicts() {
let mut app = App::empty();
fn a0(_: Query<&MyComponent<0>>) {}
fn a1(_: Query<&MyComponent<0>>) {}
fn b0(_: Query<&MyComponent<1>>) {}
fn b1(_: Query<&mut MyComponent<1>>) {}
fn c0(
_: Query<(
&MyComponent<2>,
&mut MyComponent<3>,
&MyComponent<4>,
&MyComponent<5>,
)>,
) {
}
fn c1(
_: Query<(
&mut MyComponent<2>,
&MyComponent<3>,
&MyComponent<4>,
&MyComponent<6>,
)>,
) {
}
fn d0(_: Query<&mut MyComponent<7>, With<MyComponent<8>>>) {}
fn d1(_: Query<&mut MyComponent<7>, Without<MyComponent<8>>>) {}
fn e0(_: Query<&mut MyComponent<9>>) {}
fn e1(_: Query<&mut MyComponent<9>>) {}
app.add_systems(Update, (a0, a1, b0, b1, c0, c1, d0, d1, (e0, e1).chain()));
let data = app_data_from_app(&mut app).unwrap();
assert_eq!(data.schedules.len(), 1);
let schedule = &data.schedules[0];
assert_eq!(schedule.name, "Update");
assert_eq!(
schedule.systems,
[
simple_system("a0"),
simple_system("a1"),
simple_system("b0"),
simple_system("b1"),
simple_system("c0"),
simple_system("c1"),
simple_system("d0"),
simple_system("d1"),
simple_system("e0"),
simple_system("e1"),
]
);
assert_eq!(
schedule.system_sets,
[
simple_system_set("SystemTypeSet:a0"),
simple_system_set("SystemTypeSet:a1"),
simple_system_set("SystemTypeSet:b0"),
simple_system_set("SystemTypeSet:b1"),
simple_system_set("SystemTypeSet:c0"),
simple_system_set("SystemTypeSet:c1"),
simple_system_set("SystemTypeSet:d0"),
simple_system_set("SystemTypeSet:d1"),
simple_system_set("SystemTypeSet:e0"),
simple_system_set("SystemTypeSet:e1"),
]
);
assert_eq!(
schedule.hierarchy,
[
(SystemSetIndex(0), ScheduleIndex::System(0)),
(SystemSetIndex(1), ScheduleIndex::System(1)),
(SystemSetIndex(2), ScheduleIndex::System(2)),
(SystemSetIndex(3), ScheduleIndex::System(3)),
(SystemSetIndex(4), ScheduleIndex::System(4)),
(SystemSetIndex(5), ScheduleIndex::System(5)),
(SystemSetIndex(6), ScheduleIndex::System(6)),
(SystemSetIndex(7), ScheduleIndex::System(7)),
(SystemSetIndex(8), ScheduleIndex::System(8)),
(SystemSetIndex(9), ScheduleIndex::System(9)),
]
);
assert_eq!(
schedule.dependency,
[
(ScheduleIndex::System(8), ScheduleIndex::System(9)),
]
);
assert_eq!(
schedule.components,
[
simple_component("MyComponent<1>"),
simple_component("MyComponent<2>"),
simple_component("MyComponent<3>"),
]
);
assert_eq!(
schedule.conflicts,
[
conflict(2, 3, AccessConflict::Components(vec![0])),
conflict(4, 5, AccessConflict::Components(vec![1, 2]))
]
);
}
}