diff --git a/Cargo.lock b/Cargo.lock index 34bdd90..e2e17a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1453,6 +1453,7 @@ dependencies = [ "lyra-resource", "quote", "syn 2.0.48", + "thiserror", "tracing", "tracing-appender", "tracing-log 0.1.4", diff --git a/examples/testbed/src/free_fly_camera.rs b/examples/testbed/src/free_fly_camera.rs index d1c0ddc..26991b2 100644 --- a/examples/testbed/src/free_fly_camera.rs +++ b/examples/testbed/src/free_fly_camera.rs @@ -4,7 +4,7 @@ use lyra_engine::{ game::Game, input::{InputButtons, KeyCode, MouseMotion}, math::{Quat, Vec3, EulerRot}, - plugin::Plugin, ecs::{system::System, world::World, Access, Component}, DeltaTime, EventQueue, scene::CameraComponent, + plugin::Plugin, ecs::{system::{System, IntoSystem}, world::World, Access, Component}, DeltaTime, EventQueue, scene::CameraComponent, }; #[derive(Clone, Component)] @@ -159,6 +159,14 @@ impl System for FreeFlyCameraPlugin { } } +impl IntoSystem<()> for FreeFlyCameraPlugin { + type System = Self; + + fn into_system(self) -> Self::System { + self + } +} + impl Plugin for FreeFlyCameraPlugin { fn setup(&self, game: &mut Game) { game.with_system("free_fly_camera_system", FreeFlyCameraPlugin, &[]); diff --git a/examples/testbed/src/main.rs b/examples/testbed/src/main.rs index f621764..6db0d68 100644 --- a/examples/testbed/src/main.rs +++ b/examples/testbed/src/main.rs @@ -13,6 +13,7 @@ struct FixedTimestep { accumulator: f32, } +#[allow(dead_code)] impl FixedTimestep { pub fn new(max_tps: u32) -> Self { Self { @@ -185,6 +186,7 @@ async fn main() { Ok(()) }; + #[allow(unused_variables)] let fps_system = |world: &mut World| -> anyhow::Result<()> { let mut counter = world.get_resource_mut::(); @@ -245,6 +247,7 @@ async fn main() { .finish() ); + #[allow(unused_variables)] let test_system = |world: &mut World| -> anyhow::Result<()> { let handler = world.get_resource::(); diff --git a/lyra-ecs/src/archetype.rs b/lyra-ecs/src/archetype.rs index 0ad4920..52e1760 100644 --- a/lyra-ecs/src/archetype.rs +++ b/lyra-ecs/src/archetype.rs @@ -58,7 +58,7 @@ impl ComponentColumn { /// # Safety /// /// This column must have space to fit the component, if it does not have room it will panic. - pub unsafe fn set_at(&mut self, entity_index: usize, comp_src: NonNull, is_dynamic: bool, tick: Tick) { + pub unsafe fn set_at(&mut self, entity_index: usize, comp_src: NonNull, tick: Tick) { assert!(entity_index < self.capacity); let mut data = self.data.borrow_mut(); @@ -66,16 +66,6 @@ impl ComponentColumn { let dest = NonNull::new_unchecked(data.as_ptr().add(entity_index * self.info.layout.size)); ptr::copy_nonoverlapping(comp_src.as_ptr(), dest.as_ptr(), self.info.layout.size); - - // I thought there was a point for why I added the dealloc, but now the it - // only seems to cause invalid pointers crashes. - // - // if is_dynamic { - // unsafe { - // let layout = self.info.layout.into_layout().unwrap(); - // std::alloc::dealloc(comp_src.as_ptr(), layout); - // } - // } // check if a component spot is being set twice and that the entity's tick is // already stored @@ -258,11 +248,10 @@ impl Archetype { let entity_index = self.entities.len(); self.entities.insert(entity, ArchetypeEntityId(entity_index as u64)); - let is_dynamic = bundle.is_dynamic(); bundle.take(|data, type_id, _size| { let col = self.get_column_mut(type_id).unwrap(); - unsafe { col.set_at(entity_index, data, is_dynamic, *tick); } + unsafe { col.set_at(entity_index, data, *tick); } col.len += 1; }); diff --git a/lyra-ecs/src/system/batched.rs b/lyra-ecs/src/system/batched.rs index 295270a..a9ed48e 100644 --- a/lyra-ecs/src/system/batched.rs +++ b/lyra-ecs/src/system/batched.rs @@ -2,7 +2,7 @@ use lyra_ecs::world::World; use crate::Access; -use super::{System, Criteria}; +use super::{System, Criteria, IntoSystem}; /// A system that executes a batch of systems in order that they were given. /// You can optionally add criteria that must pass before the systems are @@ -79,4 +79,12 @@ impl System for BatchedSystem { Ok(()) } +} + +impl IntoSystem<()> for BatchedSystem { + type System = Self; + + fn into_system(self) -> Self::System { + self + } } \ No newline at end of file diff --git a/lyra-ecs/src/world.rs b/lyra-ecs/src/world.rs index ef7caa0..6d2cb98 100644 --- a/lyra-ecs/src/world.rs +++ b/lyra-ecs/src/world.rs @@ -166,20 +166,19 @@ impl World { if let Some(arch) = self.archetypes.values_mut().find(|a| a.is_archetype_for(&col_types)) { let res_index = arch.reserve_one(entity); - let is_dynamic = bundle.is_dynamic(); for (col_type, (col_ptr, col_info)) in orig_col.into_iter().zip(col_ptrs.into_iter()) { unsafe { let ptr = NonNull::new_unchecked(col_ptr.as_ptr() .add(res_index.0 as usize * col_info.layout.size)); let col = arch.get_column_mut(col_type).unwrap(); - col.set_at(res_index.0 as _, ptr, is_dynamic, tick); + col.set_at(res_index.0 as _, ptr, tick); } } bundle.take(|data, type_id, _size| { let col = arch.get_column_mut(type_id).unwrap(); - unsafe { col.set_at(res_index.0 as _, data, is_dynamic, tick); } + unsafe { col.set_at(res_index.0 as _, data, tick); } col.len += 1; }); diff --git a/lyra-game/Cargo.toml b/lyra-game/Cargo.toml index 5523264..87d853f 100644 --- a/lyra-game/Cargo.toml +++ b/lyra-game/Cargo.toml @@ -27,3 +27,4 @@ syn = "2.0.26" quote = "1.0.29" uuid = { version = "1.5.0", features = ["v4", "fast-rng"] } itertools = "0.11.0" +thiserror = "1.0.56" diff --git a/lyra-game/src/delta_time.rs b/lyra-game/src/delta_time.rs index dcf3314..49bd71a 100644 --- a/lyra-game/src/delta_time.rs +++ b/lyra-game/src/delta_time.rs @@ -1,5 +1,5 @@ use instant::Instant; -use lyra_ecs::{Component, world::World, system::IntoSystem}; +use lyra_ecs::{Component, world::World}; use crate::plugin::Plugin; @@ -34,6 +34,6 @@ pub struct DeltaTimePlugin; impl Plugin for DeltaTimePlugin { fn setup(&self, game: &mut crate::game::Game) { game.world().add_resource(DeltaTime(0.0, None)); - game.with_system("delta_time", delta_time_system.into_system(), &[]); + game.with_system("delta_time", delta_time_system, &[]); } } \ No newline at end of file diff --git a/lyra-game/src/game.rs b/lyra-game/src/game.rs index f214024..5330ffb 100755 --- a/lyra-game/src/game.rs +++ b/lyra-game/src/game.rs @@ -2,7 +2,7 @@ use std::{sync::Arc, collections::VecDeque, ptr::NonNull}; use async_std::task::block_on; -use lyra_ecs::{world::World, system::{GraphExecutor, System}}; +use lyra_ecs::{world::World, system::{System, IntoSystem}}; use tracing::{info, error, Level}; use tracing_appender::non_blocking; use tracing_subscriber::{ @@ -13,7 +13,15 @@ use tracing_subscriber::{ use winit::{window::{WindowBuilder, Window}, event::{Event, WindowEvent, KeyboardInput, ElementState, VirtualKeyCode, DeviceEvent}, event_loop::{EventLoop, ControlFlow}}; -use crate::{render::{renderer::{Renderer, BasicRenderer}, window::WindowOptions}, input::InputEvent, plugin::Plugin, change_tracker::Ct, EventQueue}; +use crate::{render::{renderer::{Renderer, BasicRenderer}, window::WindowOptions}, input::InputEvent, plugin::Plugin, change_tracker::Ct, EventQueue, StagedExecutor, Stage}; + +#[derive(Clone, Copy, Hash, Debug)] +pub enum GameStages { + Core, + User, +} + +impl Stage for GameStages {} pub struct Controls<'a> { pub world: &'a mut World, @@ -36,20 +44,18 @@ struct GameLoop { renderer: Box, world: World, - /// higher priority systems - engine_sys_dispatcher: GraphExecutor, - user_sys_dispatcher: GraphExecutor, + + staged_exec: StagedExecutor, } impl GameLoop { - pub async fn new(window: Arc, world: World, user_systems: GraphExecutor) -> GameLoop { + pub async fn new(window: Arc, world: World, staged_exec: StagedExecutor) -> GameLoop { Self { window: Arc::clone(&window), renderer: Box::new(BasicRenderer::create_with_window(window).await), world, - engine_sys_dispatcher: GraphExecutor::new(), - user_sys_dispatcher: user_systems, + staged_exec, } } @@ -68,12 +74,9 @@ impl GameLoop { async fn update(&mut self) { let world_ptr = NonNull::from(&self.world); - if let Err(e) = self.engine_sys_dispatcher.execute(world_ptr, true) { - error!("Error when executing engine ecs systems: '{}'", e); - } - if let Err(e) = self.user_sys_dispatcher.execute(world_ptr, true) { - error!("Error when executing user ecs systems: '{}'", e); + if let Err(e) = self.staged_exec.execute(world_ptr, true) { + error!("Error when executing staged systems: '{}'", e); } } @@ -206,16 +209,21 @@ impl GameLoop { pub struct Game { world: Option, plugins: VecDeque>, - system_dispatcher: Option, + system_exec: Option, startup_systems: VecDeque>, + } impl Default for Game { fn default() -> Self { + let mut staged = StagedExecutor::new(); + staged.add_stage(GameStages::Core); + staged.add_stage_after(GameStages::Core, GameStages::User); + Self { world: Some(World::new()), plugins: VecDeque::new(), - system_dispatcher: Some(GraphExecutor::new()), + system_exec: Some(staged), startup_systems: VecDeque::new(), } } @@ -233,16 +241,51 @@ impl Game { } /// Add a system to the ecs world - pub fn with_system(&mut self, name: &str, system: S, depends: &[&str]) -> &mut Self + pub fn with_system(&mut self, name: &str, system: S, depends: &[&str]) -> &mut Self where - S: System + 'static + S: IntoSystem, + >::System: 'static { - let system_dispatcher = self.system_dispatcher.as_mut().unwrap(); - system_dispatcher.insert_system(name, system, depends); + let system_dispatcher = self.system_exec.as_mut().unwrap(); + system_dispatcher.add_system_to_stage(GameStages::User, name, system.into_system(), depends); self } + /// Add a stage. + /// + /// This stage could run at any moment if nothing is dependent on it. + pub fn add_stage(&mut self, stage: T) -> &mut Self { + let system_dispatcher = self.system_exec.as_mut().unwrap(); + system_dispatcher.add_stage(stage); + + self + } + + /// Add a stage that executes after another one. + /// + /// Parameters: + /// * `before` - The stage that will run before `after`. + /// * `after` - The stage that will run after `before`. + pub fn add_stage_after(&mut self, before: T, after: U) -> &mut Self { + let system_dispatcher = self.system_exec.as_mut().unwrap(); + system_dispatcher.add_stage_after(before, after); + + self + } + + /// Add a system to an already existing stage. + /// + /// # Panics + /// Panics if the stage was not already added to the executor + pub fn add_system_to_stage + 'static>(&mut self, stage: ST, + name: &str, system: I, depends: &[&str]) -> &mut Self { + let system_dispatcher = self.system_exec.as_mut().unwrap(); + system_dispatcher.add_system_to_stage(stage, name, system.into_system(), depends); + + self + } + /// Add a startup system that will be ran right after plugins are setup. /// They will only be ran once pub fn with_startup_system(&mut self, system: S) -> &mut Self @@ -304,7 +347,7 @@ impl Game { let event_loop = EventLoop::new(); let window = Arc::new(WindowBuilder::new().build(&event_loop).unwrap()); - let system_dispatcher = self.system_dispatcher.take().unwrap(); + let system_dispatcher = self.system_exec.take().unwrap(); let mut g_loop = GameLoop::new(Arc::clone(&window), world, system_dispatcher).await; g_loop.on_init().await; diff --git a/lyra-game/src/input/action.rs b/lyra-game/src/input/action.rs index 91dbbf2..c9e8f9f 100644 --- a/lyra-game/src/input/action.rs +++ b/lyra-game/src/input/action.rs @@ -1,6 +1,6 @@ use std::{collections::HashMap, ops::Deref}; -use lyra_ecs::{world::World, system::IntoSystem}; +use lyra_ecs::world::World; use crate::plugin::Plugin; @@ -392,6 +392,6 @@ pub struct InputActionPlugin; impl Plugin for InputActionPlugin { fn setup(&self, game: &mut crate::game::Game) { - game.with_system("input_actions", actions_system.into_system(), &[]); + game.with_system("input_actions", actions_system, &[]); } } \ No newline at end of file diff --git a/lyra-game/src/input/system.rs b/lyra-game/src/input/system.rs index 087f8f4..bdda83a 100755 --- a/lyra-game/src/input/system.rs +++ b/lyra-game/src/input/system.rs @@ -1,7 +1,7 @@ use std::ptr::NonNull; use glam::Vec2; -use lyra_ecs::world::World; +use lyra_ecs::{world::World, system::IntoSystem}; use winit::event::MouseScrollDelta; use crate::{EventQueue, plugin::Plugin}; @@ -122,6 +122,14 @@ impl crate::ecs::system::System for InputSystem { } } +impl IntoSystem<()> for InputSystem { + type System = Self; + + fn into_system(self) -> Self::System { + self + } +} + /// Plugin that runs InputSystem #[derive(Default)] pub struct InputPlugin; diff --git a/lyra-game/src/lib.rs b/lyra-game/src/lib.rs index 21ecbcc..1445f40 100644 --- a/lyra-game/src/lib.rs +++ b/lyra-game/src/lib.rs @@ -16,6 +16,9 @@ pub mod change_tracker; pub mod events; pub use events::*; +pub mod stage; +pub use stage::*; + pub mod delta_time; pub use delta_time::*; diff --git a/lyra-game/src/render/window.rs b/lyra-game/src/render/window.rs index 4b591d2..a974544 100644 --- a/lyra-game/src/render/window.rs +++ b/lyra-game/src/render/window.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use glam::{Vec2, IVec2}; -use lyra_ecs::{world::World, system::IntoSystem}; +use lyra_ecs::world::World; use tracing::{warn, error}; use winit::{window::{Window, Fullscreen}, dpi::{LogicalPosition, LogicalSize, PhysicalPosition}, error::ExternalError}; @@ -374,6 +374,6 @@ impl Plugin for WindowPlugin { let window_options = WindowOptions::default(); game.world().add_resource(Ct::new(window_options)); - game.with_system("window_updater", window_updater_system.into_system(), &[]); + game.with_system("window_updater", window_updater_system, &[]); } } diff --git a/lyra-game/src/stage.rs b/lyra-game/src/stage.rs new file mode 100644 index 0000000..e06e8aa --- /dev/null +++ b/lyra-game/src/stage.rs @@ -0,0 +1,156 @@ +use std::{hash::{Hash, DefaultHasher, Hasher}, collections::{HashMap, HashSet, VecDeque}, ptr::NonNull, fmt::Debug}; + +use lyra_ecs::{system::{GraphExecutor, GraphExecutorError, System}, World}; + +#[derive(thiserror::Error, Debug)] +pub enum StagedExecutorError { + #[error("could not find the stage that {0} depends on")] + MissingStage(String, u64), + #[error("[stage={0}] could not find a system's dependency named `{1}`")] + MissingSystem(String, String), + #[error("[stage={0}] system `{1}` returned with an error: `{2}`")] + SystemError(String, String, anyhow::Error) +} + +impl StagedExecutorError { + pub fn from_graph_error(stage: String, value: GraphExecutorError) -> Self { + match value { + GraphExecutorError::MissingSystem(s) => Self::MissingSystem(stage, s), + GraphExecutorError::SystemError(s, e) => Self::SystemError(stage, s, e), + } + } +} + +/// A Stage can be used to group the execution of systems together. +pub trait Stage: Hash + Debug { + fn hash_stage(&self) -> u64 { + let mut s = DefaultHasher::new(); + Hash::hash(self, &mut s); + s.finish() + } +} + +struct StageStorage { + hash: u64, + name: String, + exec: GraphExecutor, + depend: Option, +} + +/// A system executor that executes systems in stages. +/// +/// Stages can depend on other stages to ensure that a group of systems run before another group. +#[derive(Default)] +pub struct StagedExecutor { + stages: HashMap, +} + +impl StagedExecutor { + pub fn new() -> Self { + Self::default() + } + + /// Add a stage that executes after another one. + /// + /// Parameters: + /// * `before` - The stage that will run before `after`. + /// * `after` - The stage that will run after `before`. + pub fn add_stage_after(&mut self, before: T, after: U) + where + T: Stage, + U: Stage, + { + let name = format!("{:?}", after); + + let strg = StageStorage { + hash: after.hash_stage(), + name, + exec: GraphExecutor::default(), + depend: Some(before.hash_stage()), + }; + self.stages.insert(after.hash_stage(), strg); + } + + /// Add a stage. + /// + /// This stage could run at any moment if nothing is dependent on it. + pub fn add_stage(&mut self, stage: T) { + let name = format!("{:?}", stage); + + let strg = StageStorage { + hash: stage.hash_stage(), + name, + exec: GraphExecutor::default(), + depend: None + }; + self.stages.insert(stage.hash_stage(), strg); + } + + /// Add a system to an already existing stage. + /// + /// # Panics + /// Panics if the stage was not already added to the executor + pub fn add_system_to_stage(&mut self, stage: T, name: &str, system: S, depends: &[&str]) + where + T: Stage, + S: System + 'static + { + let hash = stage.hash_stage(); + + let stage = self.stages.get_mut(&hash) + .expect("Unable to find the stage to add the system into! \ + Did you add the stage first?"); + let exec = &mut stage.exec; + + exec.insert_system(name, system, depends); + } + + /// Execute the staged systems in order. + /// + /// If `stop_on_error` is false but errors are encountered, those errors will be returned in a Vec. + pub fn execute(&mut self, world: NonNull, stop_on_error: bool) -> Result, StagedExecutorError> { + let mut stack = VecDeque::new(); + let mut visited = HashSet::new(); + + for (_, node) in self.stages.iter() { + self.topological_sort(&mut stack, &mut visited, node)?; + } + + let mut errors = vec![]; + while let Some(node) = stack.pop_front() { + let stage = self.stages.get_mut(&node).unwrap(); + + if let Err(e) = stage.exec.execute(world, stop_on_error) { + let e = StagedExecutorError::from_graph_error(stage.name.clone(), e); + + if stop_on_error { + return Err(e); + } + + errors.push(e); + unimplemented!("Cannot resume staged execution from error"); // TODO: resume staged execution from error + } + } + + Ok(errors) + } + + fn topological_sort<'a>(&'a self, stack: &mut VecDeque, visited: &mut HashSet, node: &'a StageStorage) -> Result<(), StagedExecutorError> { + if !visited.contains(&node.hash) { + visited.insert(node.hash); + + if let Some(depend) = node.depend { + let node = self.stages.get(&depend) + .ok_or_else(|| StagedExecutorError::MissingStage(node.name.clone(), depend))?; + + if !visited.contains(&node.hash) { + self.topological_sort(stack, visited, node)?; + } + } + + stack.push_back(node.hash); + } + + Ok(()) + } +} \ No newline at end of file