Create graph system executor

This commit is contained in:
SeanOMik 2023-12-05 23:17:19 -05:00
parent 9c6c32199d
commit 068eeecd4c
Signed by: SeanOMik
GPG Key ID: 568F326C7EB33ACB
4 changed files with 237 additions and 1 deletions

56
lyra-ecs/Cargo.lock generated
View File

@ -37,6 +37,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"rand", "rand",
"thiserror",
] ]
[[package]] [[package]]
@ -45,6 +46,24 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 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]] [[package]]
name = "rand" name = "rand"
version = "0.8.5" version = "0.8.5"
@ -75,6 +94,43 @@ dependencies = [
"getrandom", "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]] [[package]]
name = "wasi" name = "wasi"
version = "0.11.0+wasi-snapshot-preview1" version = "0.11.0+wasi-snapshot-preview1"

View File

@ -7,6 +7,7 @@ edition = "2021"
[dependencies] [dependencies]
anyhow = "1.0.75" anyhow = "1.0.75"
thiserror = "1.0.50"
[dev-dependencies] [dev-dependencies]
rand = "0.8.5" # used for tests rand = "0.8.5" # used for tests

View File

@ -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");
}
}

View File

@ -2,6 +2,8 @@ use std::{ptr::NonNull, marker::PhantomData};
use crate::{world::World, View, Query, Access}; use crate::{world::World, View, Query, Access};
pub mod graph;
/// A system that does not mutate the world /// A system that does not mutate the world
pub trait System { pub trait System {
fn world_access(&self) -> Access; fn world_access(&self) -> Access;
@ -127,7 +129,7 @@ impl<Q: Query> FnArg for FnArgStorage<Q> {
mod tests { mod tests {
use std::{ptr::NonNull, sync::atomic::{AtomicU8, Ordering}, rc::Rc, ops::Add}; 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}; use super::{System, IntoSystem};
#[test] #[test]
@ -153,4 +155,28 @@ mod tests {
assert_eq!(count, 4); 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);
}
} }