Create graph system executor
This commit is contained in:
parent
9c6c32199d
commit
068eeecd4c
|
@ -37,6 +37,7 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"anyhow",
|
||||
"rand",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -45,6 +46,24 @@ version = "0.2.17"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.70"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
|
@ -75,6 +94,43 @@ dependencies = [
|
|||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
|
|
|
@ -7,6 +7,7 @@ edition = "2021"
|
|||
|
||||
[dependencies]
|
||||
anyhow = "1.0.75"
|
||||
thiserror = "1.0.50"
|
||||
|
||||
[dev-dependencies]
|
||||
rand = "0.8.5" # used for tests
|
||||
|
|
|
@ -0,0 +1,153 @@
|
|||
use std::{collections::{HashMap, VecDeque, HashSet}, ptr::NonNull};
|
||||
|
||||
use crate::{System, world::World};
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum GraphExecutorError {
|
||||
#[error("could not find a system's dependency named `{0}`")]
|
||||
MissingSystem(String),
|
||||
#[error("system `{0}` returned with an error: `{1}`")]
|
||||
SystemError(String, anyhow::Error)
|
||||
}
|
||||
|
||||
/// A single system in the graph.
|
||||
///
|
||||
/// This is a helper struct for the [`GraphExecutor`] that stores the name
|
||||
/// and dependencies of a system.
|
||||
pub struct GraphSystem {
|
||||
/// The name of this system
|
||||
name: String,
|
||||
/// The actual system
|
||||
system: Box<dyn System>,
|
||||
/// The dependencies of this system
|
||||
depends: Vec<String>,
|
||||
}
|
||||
|
||||
/// A system executor that represents the systems in a graph to handle dependencies.
|
||||
///
|
||||
/// The graph uses an adjacency list.
|
||||
pub struct GraphExecutor {
|
||||
systems: HashMap<String, GraphSystem>,
|
||||
}
|
||||
|
||||
impl GraphExecutor {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
systems: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Inserts a system into the Graph.
|
||||
///
|
||||
/// This does not validate that the dependencies of this system are existing. This is done on
|
||||
/// execution. It makes it easier when adding systems so you don't have to make sure you
|
||||
/// insert everything in the correct order.
|
||||
pub fn insert_system<S>(&mut self, name: &str, system: S, depends: &[&str])
|
||||
where
|
||||
S: System + 'static
|
||||
{
|
||||
let system = GraphSystem {
|
||||
name: name.to_string(),
|
||||
system: Box::new(system),
|
||||
depends: depends.iter().map(|d| d.to_string()).collect(),
|
||||
};
|
||||
self.systems.insert(name.to_string(), system);
|
||||
}
|
||||
|
||||
/// Executes the systems in the graph
|
||||
pub fn execute(&mut self, world: &World, stop_on_error: bool) -> Result<Vec<GraphExecutorError>, GraphExecutorError> {
|
||||
let world = NonNull::from(world);
|
||||
let mut stack = VecDeque::new();
|
||||
let mut visited = HashSet::new();
|
||||
|
||||
for (_name, node) in self.systems.iter() {
|
||||
self.topological_sort(&mut stack, &mut visited, node)?;
|
||||
}
|
||||
|
||||
let mut possible_errors = Vec::new();
|
||||
|
||||
while let Some(node) = stack.pop_front() {
|
||||
let system = self.systems.get_mut(node.as_str()).unwrap();
|
||||
|
||||
if let Err(e) = system.system.execute(world)
|
||||
.map_err(|e| GraphExecutorError::SystemError(node, e)) {
|
||||
if stop_on_error {
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
possible_errors.push(e);
|
||||
unimplemented!("Cannot resume topological execution from error"); // TODO: resume topological execution from error
|
||||
}
|
||||
}
|
||||
|
||||
Ok(possible_errors)
|
||||
}
|
||||
|
||||
fn topological_sort<'a, 'b>(&'a self, stack: &'b mut VecDeque<String>, visited: &mut HashSet<&'a str>, node: &'a GraphSystem) -> Result<(), GraphExecutorError> {
|
||||
|
||||
if !visited.contains(node.name.as_str()) {
|
||||
visited.insert(&node.name);
|
||||
|
||||
for depend in node.depends.iter() {
|
||||
let node = self.systems.get(depend)
|
||||
.ok_or_else(|| GraphExecutorError::MissingSystem(depend.clone()))?;
|
||||
|
||||
if !visited.contains(depend.as_str()) {
|
||||
self.topological_sort(stack, visited, node)?;
|
||||
}
|
||||
}
|
||||
|
||||
stack.push_back(node.name.clone());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{View, QueryBorrow, tests::Vec2, IntoSystem, world::World, Resource};
|
||||
|
||||
use super::GraphExecutor;
|
||||
|
||||
#[test]
|
||||
pub fn execution() {
|
||||
let mut world = World::new();
|
||||
let order_list = Vec::<String>::new();
|
||||
world.add_resource(order_list);
|
||||
|
||||
let mut exec = GraphExecutor::new();
|
||||
|
||||
/// TODO:
|
||||
/// * Implement FnArg for World
|
||||
/// * Implement ResourceMut
|
||||
|
||||
let a_system = |view: View<(QueryBorrow<Vec2>, Resource<Vec<String>>)>| -> anyhow::Result<()> {
|
||||
println!("System 'a' ran!");
|
||||
|
||||
let order = view.into_iter().next();
|
||||
|
||||
Ok(())
|
||||
};
|
||||
|
||||
let b_system = |view: View<QueryBorrow<Vec2>>| -> anyhow::Result<()> {
|
||||
println!("System 'b' ran!");
|
||||
|
||||
Ok(())
|
||||
};
|
||||
|
||||
let c_system = |view: View<QueryBorrow<Vec2>>| -> anyhow::Result<()> {
|
||||
println!("System 'c' ran!");
|
||||
|
||||
Ok(())
|
||||
};
|
||||
|
||||
exec.insert_system("c", c_system.into_system(), &[]);
|
||||
exec.insert_system("a", a_system.into_system(), &[]);
|
||||
exec.insert_system("b", b_system.into_system(), &["a"]);
|
||||
|
||||
|
||||
exec.execute(&world, true).unwrap();
|
||||
println!("Executed systems");
|
||||
}
|
||||
}
|
|
@ -2,6 +2,8 @@ use std::{ptr::NonNull, marker::PhantomData};
|
|||
|
||||
use crate::{world::World, View, Query, Access};
|
||||
|
||||
pub mod graph;
|
||||
|
||||
/// A system that does not mutate the world
|
||||
pub trait System {
|
||||
fn world_access(&self) -> Access;
|
||||
|
@ -127,7 +129,7 @@ impl<Q: Query> FnArg for FnArgStorage<Q> {
|
|||
mod tests {
|
||||
use std::{ptr::NonNull, sync::atomic::{AtomicU8, Ordering}, rc::Rc, ops::Add};
|
||||
|
||||
use crate::{tests::Vec2, View, QueryBorrow, world::World};
|
||||
use crate::{tests::{Vec2, Vec3}, View, QueryBorrow, world::World};
|
||||
use super::{System, IntoSystem};
|
||||
|
||||
#[test]
|
||||
|
@ -153,4 +155,28 @@ mod tests {
|
|||
|
||||
assert_eq!(count, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_system() {
|
||||
let mut world = World::new();
|
||||
world.spawn((Vec2::rand(), Vec3::rand()));
|
||||
world.spawn((Vec2::rand(), Vec3::rand()));
|
||||
world.spawn((Vec2::rand(),));
|
||||
world.spawn((Vec2::rand(),));
|
||||
|
||||
let mut count = 0;
|
||||
|
||||
let test_system = |view: View<(QueryBorrow<Vec2>, QueryBorrow<Vec3>)>| -> anyhow::Result<()> {
|
||||
for (v2, v3) in view.into_iter() {
|
||||
println!("Got v2 at '{:?}' and v3 at: '{:?}'", v2, v3);
|
||||
count += 1;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
};
|
||||
|
||||
test_system.into_system().execute(NonNull::from(&world)).unwrap();
|
||||
|
||||
assert_eq!(count, 2);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue