From 068eeecd4ce1cb3d1ee1e18830f9c44a7f45a8a5 Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Tue, 5 Dec 2023 23:17:19 -0500 Subject: [PATCH] Create graph system executor --- lyra-ecs/Cargo.lock | 56 +++++++++++++ lyra-ecs/Cargo.toml | 1 + lyra-ecs/src/system/graph.rs | 153 +++++++++++++++++++++++++++++++++++ lyra-ecs/src/system/mod.rs | 28 ++++++- 4 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 lyra-ecs/src/system/graph.rs diff --git a/lyra-ecs/Cargo.lock b/lyra-ecs/Cargo.lock index 587ab0a..36bfcc7 100644 --- a/lyra-ecs/Cargo.lock +++ b/lyra-ecs/Cargo.lock @@ -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" diff --git a/lyra-ecs/Cargo.toml b/lyra-ecs/Cargo.toml index d913889..bac5140 100644 --- a/lyra-ecs/Cargo.toml +++ b/lyra-ecs/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] anyhow = "1.0.75" +thiserror = "1.0.50" [dev-dependencies] rand = "0.8.5" # used for tests diff --git a/lyra-ecs/src/system/graph.rs b/lyra-ecs/src/system/graph.rs new file mode 100644 index 0000000..5c32e84 --- /dev/null +++ b/lyra-ecs/src/system/graph.rs @@ -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, + /// The dependencies of this system + depends: Vec, +} + +/// A system executor that represents the systems in a graph to handle dependencies. +/// +/// The graph uses an adjacency list. +pub struct GraphExecutor { + systems: HashMap, +} + +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(&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, 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, 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::::new(); + world.add_resource(order_list); + + let mut exec = GraphExecutor::new(); + + /// TODO: + /// * Implement FnArg for World + /// * Implement ResourceMut + + let a_system = |view: View<(QueryBorrow, Resource>)>| -> anyhow::Result<()> { + println!("System 'a' ran!"); + + let order = view.into_iter().next(); + + Ok(()) + }; + + let b_system = |view: View>| -> anyhow::Result<()> { + println!("System 'b' ran!"); + + Ok(()) + }; + + let c_system = |view: View>| -> 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"); + } +} \ No newline at end of file diff --git a/lyra-ecs/src/system/mod.rs b/lyra-ecs/src/system/mod.rs index b2e9be9..c636a53 100644 --- a/lyra-ecs/src/system/mod.rs +++ b/lyra-ecs/src/system/mod.rs @@ -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 FnArg for FnArgStorage { 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, QueryBorrow)>| -> 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); + } } \ No newline at end of file