ecs: prepare system ranges based on stage and thread locals
This commit is contained in:
parent
44c08f90aa
commit
f85ec04a48
@ -7,8 +7,12 @@ use crossbeam_channel::{Receiver, Sender};
|
|||||||
use fixedbitset::FixedBitSet;
|
use fixedbitset::FixedBitSet;
|
||||||
use hecs::{ArchetypesGeneration, World};
|
use hecs::{ArchetypesGeneration, World};
|
||||||
use rayon::ScopeFifo;
|
use rayon::ScopeFifo;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::{
|
||||||
|
ops::Range,
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct ParallelExecutor {
|
pub struct ParallelExecutor {
|
||||||
stages: Vec<ExecutorStage>,
|
stages: Vec<ExecutorStage>,
|
||||||
last_schedule_generation: usize,
|
last_schedule_generation: usize,
|
||||||
@ -33,47 +37,38 @@ impl ParallelExecutor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn prepare(&mut self, schedule: &mut Schedule, world: &World) {
|
pub fn run(&mut self, schedule: &mut Schedule, world: &mut World, resources: &mut Resources) {
|
||||||
let schedule_generation = schedule.generation();
|
let schedule_generation = schedule.generation();
|
||||||
let schedule_changed = schedule_generation != self.last_schedule_generation;
|
let schedule_changed = schedule.generation() != self.last_schedule_generation;
|
||||||
|
|
||||||
if schedule_changed {
|
if schedule_changed {
|
||||||
self.stages.clear();
|
self.stages.clear();
|
||||||
self.stages
|
self.stages
|
||||||
.resize_with(schedule.stage_order.len(), || ExecutorStage::default());
|
.resize_with(schedule.stage_order.len(), || ExecutorStage::default());
|
||||||
}
|
}
|
||||||
|
|
||||||
for (stage_index, stage_name) in schedule.stage_order.iter().enumerate() {
|
|
||||||
let executor_stage = &mut self.stages[stage_index];
|
|
||||||
if let Some(systems) = schedule.stages.get(stage_name) {
|
|
||||||
executor_stage.prepare(world, systems, schedule_changed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.last_schedule_generation = schedule_generation;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run(&mut self, schedule: &mut Schedule, world: &mut World, resources: &mut Resources) {
|
|
||||||
self.prepare(schedule, world);
|
|
||||||
for (stage_name, executor_stage) in schedule.stage_order.iter().zip(self.stages.iter_mut())
|
for (stage_name, executor_stage) in schedule.stage_order.iter().zip(self.stages.iter_mut())
|
||||||
{
|
{
|
||||||
if let Some(stage_systems) = schedule.stages.get_mut(stage_name) {
|
if let Some(stage_systems) = schedule.stages.get_mut(stage_name) {
|
||||||
executor_stage.run(world, resources, stage_systems);
|
executor_stage.run(world, resources, stage_systems, schedule_changed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.clear_trackers {
|
if self.clear_trackers {
|
||||||
world.clear_trackers();
|
world.clear_trackers();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.last_schedule_generation = schedule_generation;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ExecutorStage {
|
pub struct ExecutorStage {
|
||||||
/// each system's set of dependencies
|
/// each system's set of dependencies
|
||||||
system_dependencies: Vec<FixedBitSet>,
|
system_dependencies: Vec<FixedBitSet>,
|
||||||
/// each system's dependents (the systems that can't run until this system has run)
|
/// each system's dependents (the systems that can't run until this system has run)
|
||||||
system_dependents: Vec<Vec<usize>>,
|
system_dependents: Vec<Vec<usize>>,
|
||||||
|
/// stores the indices of thread local systems in this stage, which are used during stage.prepare()
|
||||||
|
thread_local_system_indices: Vec<usize>,
|
||||||
|
next_thread_local_index: usize,
|
||||||
/// the currently finished systems
|
/// the currently finished systems
|
||||||
finished_systems: FixedBitSet,
|
finished_systems: FixedBitSet,
|
||||||
running_systems: FixedBitSet,
|
running_systems: FixedBitSet,
|
||||||
@ -89,6 +84,8 @@ impl Default for ExecutorStage {
|
|||||||
Self {
|
Self {
|
||||||
system_dependents: Default::default(),
|
system_dependents: Default::default(),
|
||||||
system_dependencies: Default::default(),
|
system_dependencies: Default::default(),
|
||||||
|
thread_local_system_indices: Default::default(),
|
||||||
|
next_thread_local_index: 0,
|
||||||
finished_systems: Default::default(),
|
finished_systems: Default::default(),
|
||||||
running_systems: Default::default(),
|
running_systems: Default::default(),
|
||||||
sender,
|
sender,
|
||||||
@ -104,48 +101,54 @@ enum RunReadyResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum RunReadyType {
|
enum RunReadyType {
|
||||||
All,
|
Range(Range<usize>),
|
||||||
Dependents(usize),
|
Dependents(usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExecutorStage {
|
impl ExecutorStage {
|
||||||
// TODO: add a start_index parameter here
|
pub fn prepare_to_next_thread_local(
|
||||||
pub fn prepare(
|
|
||||||
&mut self,
|
&mut self,
|
||||||
world: &World,
|
world: &World,
|
||||||
systems: &Vec<Arc<Mutex<Box<dyn System>>>>,
|
systems: &[Arc<Mutex<Box<dyn System>>>],
|
||||||
schedule_changed: bool,
|
schedule_changed: bool,
|
||||||
) {
|
) {
|
||||||
// if the schedule has changed, clear executor state / fill it with new defaults
|
let (prepare_system_start_index, last_thread_local_index) =
|
||||||
if schedule_changed {
|
if self.next_thread_local_index == 0 {
|
||||||
self.system_dependencies.clear();
|
(0, None)
|
||||||
self.system_dependencies
|
} else {
|
||||||
.resize_with(systems.len(), || FixedBitSet::with_capacity(systems.len()));
|
// start right after the last thread local system
|
||||||
|
(
|
||||||
|
self.thread_local_system_indices[self.next_thread_local_index - 1] + 1,
|
||||||
|
Some(self.thread_local_system_indices[self.next_thread_local_index - 1]),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
self.system_dependents.clear();
|
let prepare_system_index_range = if let Some(index) = self
|
||||||
self.system_dependents.resize(systems.len(), Vec::new());
|
.thread_local_system_indices
|
||||||
|
.get(self.next_thread_local_index)
|
||||||
|
{
|
||||||
|
// if there is an upcoming thread local system, prepare up to (and including) it
|
||||||
|
prepare_system_start_index..(*index + 1)
|
||||||
|
} else {
|
||||||
|
// if there are no upcoming thread local systems, prepare everything right now
|
||||||
|
prepare_system_start_index..systems.len()
|
||||||
|
};
|
||||||
|
|
||||||
self.finished_systems.grow(systems.len());
|
|
||||||
self.running_systems.grow(systems.len());
|
|
||||||
}
|
|
||||||
|
|
||||||
let archetypes_generation = world.archetypes_generation();
|
|
||||||
let archetypes_generation_changed =
|
let archetypes_generation_changed =
|
||||||
self.last_archetypes_generation != archetypes_generation;
|
self.last_archetypes_generation != world.archetypes_generation();
|
||||||
|
|
||||||
if schedule_changed || archetypes_generation_changed {
|
if schedule_changed || archetypes_generation_changed {
|
||||||
// update each system's archetype access to latest world archetypes
|
// update each system's archetype access to latest world archetypes
|
||||||
for system in systems.iter() {
|
for system_index in prepare_system_index_range.clone() {
|
||||||
let mut system = system.lock().unwrap();
|
let mut system = systems[system_index].lock().unwrap();
|
||||||
system.update_archetype_access(world);
|
system.update_archetype_access(world);
|
||||||
}
|
}
|
||||||
|
|
||||||
// calculate dependencies between systems and build execution order
|
// calculate dependencies between systems and build execution order
|
||||||
let mut current_archetype_access = ArchetypeAccess::default();
|
let mut current_archetype_access = ArchetypeAccess::default();
|
||||||
let mut current_resource_access = TypeAccess::default();
|
let mut current_resource_access = TypeAccess::default();
|
||||||
let mut last_thread_local_index: Option<usize> = None;
|
for system_index in prepare_system_index_range.clone() {
|
||||||
for (system_index, system) in systems.iter().enumerate() {
|
let system = systems[system_index].lock().unwrap();
|
||||||
let system = system.lock().unwrap();
|
|
||||||
let archetype_access = system.archetype_access();
|
let archetype_access = system.archetype_access();
|
||||||
match system.thread_local_execution() {
|
match system.thread_local_execution() {
|
||||||
ThreadLocalExecution::NextFlush => {
|
ThreadLocalExecution::NextFlush => {
|
||||||
@ -154,15 +157,16 @@ impl ExecutorStage {
|
|||||||
if current_archetype_access.is_compatible(archetype_access) == false
|
if current_archetype_access.is_compatible(archetype_access) == false
|
||||||
|| current_resource_access.is_compatible(resource_access) == false
|
|| current_resource_access.is_compatible(resource_access) == false
|
||||||
{
|
{
|
||||||
for earlier_system_index in 0..system_index {
|
for earlier_system_index in
|
||||||
|
prepare_system_index_range.start..system_index
|
||||||
|
{
|
||||||
let earlier_system = systems[earlier_system_index].lock().unwrap();
|
let earlier_system = systems[earlier_system_index].lock().unwrap();
|
||||||
|
|
||||||
// ignore "immediate" thread local systems, we handle them separately
|
// due to how prepare ranges work, previous systems should all be "NextFlush"
|
||||||
if let ThreadLocalExecution::Immediate =
|
debug_assert_eq!(
|
||||||
earlier_system.thread_local_execution()
|
earlier_system.thread_local_execution(),
|
||||||
{
|
ThreadLocalExecution::NextFlush
|
||||||
continue;
|
);
|
||||||
}
|
|
||||||
|
|
||||||
// if earlier system is incompatible, make the current system dependent
|
// if earlier system is incompatible, make the current system dependent
|
||||||
if earlier_system
|
if earlier_system
|
||||||
@ -190,8 +194,7 @@ impl ExecutorStage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ThreadLocalExecution::Immediate => {
|
ThreadLocalExecution::Immediate => {
|
||||||
last_thread_local_index = Some(system_index);
|
for earlier_system_index in prepare_system_index_range.start..system_index {
|
||||||
for earlier_system_index in 0..system_index {
|
|
||||||
// treat all earlier systems as "incompatible" to ensure we run this thread local system exclusively
|
// treat all earlier systems as "incompatible" to ensure we run this thread local system exclusively
|
||||||
self.system_dependents[earlier_system_index].push(system_index);
|
self.system_dependents[earlier_system_index].push(system_index);
|
||||||
self.system_dependencies[system_index].insert(earlier_system_index);
|
self.system_dependencies[system_index].insert(earlier_system_index);
|
||||||
@ -201,7 +204,7 @@ impl ExecutorStage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.last_archetypes_generation = archetypes_generation;
|
self.next_thread_local_index += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_ready_systems<'run>(
|
fn run_ready_systems<'run>(
|
||||||
@ -216,8 +219,8 @@ impl ExecutorStage {
|
|||||||
let mut all;
|
let mut all;
|
||||||
let mut dependents;
|
let mut dependents;
|
||||||
let system_index_iter: &mut dyn Iterator<Item = usize> = match run_ready_type {
|
let system_index_iter: &mut dyn Iterator<Item = usize> = match run_ready_type {
|
||||||
RunReadyType::All => {
|
RunReadyType::Range(range) => {
|
||||||
all = 0..systems.len();
|
all = range;
|
||||||
&mut all
|
&mut all
|
||||||
}
|
}
|
||||||
RunReadyType::Dependents(system_index) => {
|
RunReadyType::Dependents(system_index) => {
|
||||||
@ -272,14 +275,49 @@ impl ExecutorStage {
|
|||||||
world: &mut World,
|
world: &mut World,
|
||||||
resources: &mut Resources,
|
resources: &mut Resources,
|
||||||
systems: &[Arc<Mutex<Box<dyn System>>>],
|
systems: &[Arc<Mutex<Box<dyn System>>>],
|
||||||
|
schedule_changed: bool,
|
||||||
) {
|
) {
|
||||||
|
// if the schedule has changed, clear executor state / fill it with new defaults
|
||||||
|
if schedule_changed {
|
||||||
|
self.system_dependencies.clear();
|
||||||
|
self.system_dependencies
|
||||||
|
.resize_with(systems.len(), || FixedBitSet::with_capacity(systems.len()));
|
||||||
|
self.thread_local_system_indices = Vec::new();
|
||||||
|
|
||||||
|
self.system_dependents.clear();
|
||||||
|
self.system_dependents.resize(systems.len(), Vec::new());
|
||||||
|
|
||||||
|
self.finished_systems.grow(systems.len());
|
||||||
|
self.running_systems.grow(systems.len());
|
||||||
|
|
||||||
|
for (system_index, system) in systems.iter().enumerate() {
|
||||||
|
let system = system.lock().unwrap();
|
||||||
|
if system.thread_local_execution() == ThreadLocalExecution::Immediate {
|
||||||
|
self.thread_local_system_indices.push(system_index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.next_thread_local_index = 0;
|
||||||
|
self.prepare_to_next_thread_local(world, systems, schedule_changed);
|
||||||
|
|
||||||
self.finished_systems.clear();
|
self.finished_systems.clear();
|
||||||
self.running_systems.clear();
|
self.running_systems.clear();
|
||||||
|
|
||||||
let mut run_ready_result = RunReadyResult::Ok;
|
let mut run_ready_result = RunReadyResult::Ok;
|
||||||
|
let run_ready_system_index_range = if let Some(index) = self
|
||||||
|
.thread_local_system_indices
|
||||||
|
.get(0)
|
||||||
|
{
|
||||||
|
// if there is an upcoming thread local system, run up to (and including) it
|
||||||
|
0..(*index + 1)
|
||||||
|
} else {
|
||||||
|
// if there are no upcoming thread local systems, run everything right now
|
||||||
|
0..systems.len()
|
||||||
|
};
|
||||||
rayon::scope_fifo(|scope| {
|
rayon::scope_fifo(|scope| {
|
||||||
run_ready_result =
|
run_ready_result =
|
||||||
self.run_ready_systems(systems, RunReadyType::All, scope, world, resources);
|
self.run_ready_systems(systems, RunReadyType::Range(run_ready_system_index_range), scope, world, resources);
|
||||||
});
|
});
|
||||||
loop {
|
loop {
|
||||||
// if all systems in the stage are finished, break out of the loop
|
// if all systems in the stage are finished, break out of the loop
|
||||||
@ -296,7 +334,7 @@ impl ExecutorStage {
|
|||||||
self.finished_systems.insert(thread_local_index);
|
self.finished_systems.insert(thread_local_index);
|
||||||
self.sender.send(thread_local_index).unwrap();
|
self.sender.send(thread_local_index).unwrap();
|
||||||
|
|
||||||
// TODO: if archetype generation has changed, call "prepare" on all systems after this one
|
self.prepare_to_next_thread_local(world, systems, schedule_changed);
|
||||||
|
|
||||||
run_ready_result = RunReadyResult::Ok;
|
run_ready_result = RunReadyResult::Ok;
|
||||||
} else {
|
} else {
|
||||||
@ -335,6 +373,8 @@ impl ExecutorStage {
|
|||||||
ThreadLocalExecution::Immediate => { /* already ran */ }
|
ThreadLocalExecution::Immediate => { /* already ran */ }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.last_archetypes_generation = world.archetypes_generation();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -345,9 +385,10 @@ mod tests {
|
|||||||
resource::{Res, ResMut, Resources},
|
resource::{Res, ResMut, Resources},
|
||||||
schedule::Schedule,
|
schedule::Schedule,
|
||||||
system::{IntoQuerySystem, IntoThreadLocalSystem, Query},
|
system::{IntoQuerySystem, IntoThreadLocalSystem, Query},
|
||||||
|
Commands,
|
||||||
};
|
};
|
||||||
use fixedbitset::FixedBitSet;
|
use fixedbitset::FixedBitSet;
|
||||||
use hecs::World;
|
use hecs::{Entity, World};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
@ -355,6 +396,63 @@ mod tests {
|
|||||||
count: Arc<Mutex<usize>>,
|
count: Arc<Mutex<usize>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cross_stage_archetype_change_prepare() {
|
||||||
|
let mut world = World::new();
|
||||||
|
let mut resources = Resources::default();
|
||||||
|
let mut schedule = Schedule::default();
|
||||||
|
schedule.add_stage("PreArchetypeChange");
|
||||||
|
schedule.add_stage("PostArchetypeChange");
|
||||||
|
|
||||||
|
fn insert(mut commands: Commands) {
|
||||||
|
commands.spawn((1u32,));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read(query: Query<&u32>, mut entities: Query<Entity>) {
|
||||||
|
for entity in &mut entities.iter() {
|
||||||
|
// query.get() does a "system permission check" that will fail if the entity is from a
|
||||||
|
// new archetype which hasnt been "prepared yet"
|
||||||
|
query.get::<u32>(entity).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(1, entities.iter().iter().count());
|
||||||
|
}
|
||||||
|
|
||||||
|
schedule.add_system_to_stage("PreArchetypeChange", insert.system());
|
||||||
|
schedule.add_system_to_stage("PostArchetypeChange", read.system());
|
||||||
|
|
||||||
|
let mut executor = ParallelExecutor::default();
|
||||||
|
executor.run(&mut schedule, &mut world, &mut resources);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn intra_stage_archetype_change_prepare() {
|
||||||
|
let mut world = World::new();
|
||||||
|
let mut resources = Resources::default();
|
||||||
|
let mut schedule = Schedule::default();
|
||||||
|
schedule.add_stage("update");
|
||||||
|
|
||||||
|
fn insert(world: &mut World, _resources: &mut Resources) {
|
||||||
|
world.spawn((1u32,));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read(query: Query<&u32>, mut entities: Query<Entity>) {
|
||||||
|
for entity in &mut entities.iter() {
|
||||||
|
// query.get() does a "system permission check" that will fail if the entity is from a
|
||||||
|
// new archetype which hasnt been "prepared yet"
|
||||||
|
query.get::<u32>(entity).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(1, entities.iter().iter().count());
|
||||||
|
}
|
||||||
|
|
||||||
|
schedule.add_system_to_stage("update", insert.thread_local_system());
|
||||||
|
schedule.add_system_to_stage("update", read.system());
|
||||||
|
|
||||||
|
let mut executor = ParallelExecutor::default();
|
||||||
|
executor.run(&mut schedule, &mut world, &mut resources);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn schedule() {
|
fn schedule() {
|
||||||
let mut world = World::new();
|
let mut world = World::new();
|
||||||
@ -475,7 +573,7 @@ mod tests {
|
|||||||
world: &mut World,
|
world: &mut World,
|
||||||
resources: &mut Resources,
|
resources: &mut Resources,
|
||||||
) {
|
) {
|
||||||
executor.prepare(schedule, world);
|
executor.run(schedule, world, resources);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
executor.stages[0].system_dependents,
|
executor.stages[0].system_dependents,
|
||||||
@ -536,8 +634,6 @@ mod tests {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
executor.run(schedule, world, resources);
|
|
||||||
|
|
||||||
let counter = resources.get::<Counter>().unwrap();
|
let counter = resources.get::<Counter>().unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
*counter.count.lock().unwrap(),
|
*counter.count.lock().unwrap(),
|
||||||
|
|||||||
@ -3,7 +3,7 @@ use fixedbitset::FixedBitSet;
|
|||||||
use hecs::{Access, Query, World};
|
use hecs::{Access, Query, World};
|
||||||
use std::{any::TypeId, borrow::Cow, collections::HashSet};
|
use std::{any::TypeId, borrow::Cow, collections::HashSet};
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
|
||||||
pub enum ThreadLocalExecution {
|
pub enum ThreadLocalExecution {
|
||||||
Immediate,
|
Immediate,
|
||||||
NextFlush,
|
NextFlush,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user