Implement staged system execution, make it easier to add systems, remove some compiler warnings

This commit is contained in:
SeanOMik 2024-01-06 15:40:13 -05:00
parent 0a97cf7617
commit 9307265a5a
Signed by: SeanOMik
GPG Key ID: FEC9E2FC15235964
14 changed files with 264 additions and 45 deletions

1
Cargo.lock generated
View File

@ -1453,6 +1453,7 @@ dependencies = [
"lyra-resource",
"quote",
"syn 2.0.48",
"thiserror",
"tracing",
"tracing-appender",
"tracing-log 0.1.4",

View File

@ -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, &[]);

View File

@ -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::<fps_counter::FPSCounter>();
@ -245,6 +247,7 @@ async fn main() {
.finish()
);
#[allow(unused_variables)]
let test_system = |world: &mut World| -> anyhow::Result<()> {
let handler = world.get_resource::<ActionHandler>();

View File

@ -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<u8>, is_dynamic: bool, tick: Tick) {
pub unsafe fn set_at(&mut self, entity_index: usize, comp_src: NonNull<u8>, 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;
});

View File

@ -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
}
}

View File

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

View File

@ -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"

View File

@ -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, &[]);
}
}

View File

@ -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<dyn Renderer>,
world: World,
/// higher priority systems
engine_sys_dispatcher: GraphExecutor,
user_sys_dispatcher: GraphExecutor,
staged_exec: StagedExecutor,
}
impl GameLoop {
pub async fn new(window: Arc<Window>, world: World, user_systems: GraphExecutor) -> GameLoop {
pub async fn new(window: Arc<Window>, 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<World>,
plugins: VecDeque<Box<dyn Plugin>>,
system_dispatcher: Option<GraphExecutor>,
system_exec: Option<StagedExecutor>,
startup_systems: VecDeque<Box<dyn System>>,
}
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<S>(&mut self, name: &str, system: S, depends: &[&str]) -> &mut Self
pub fn with_system<S, A>(&mut self, name: &str, system: S, depends: &[&str]) -> &mut Self
where
S: System + 'static
S: IntoSystem<A>,
<S as IntoSystem<A>>::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<T: 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<T: Stage, U: Stage>(&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<ST: Stage, I: IntoSystem<()> + '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<S>(&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;

View File

@ -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, &[]);
}
}

View File

@ -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;

View File

@ -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::*;

View File

@ -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, &[]);
}
}

156
lyra-game/src/stage.rs Normal file
View File

@ -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<u64>,
}
/// 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<u64, StageStorage>,
}
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<T, U>(&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<T: 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<T, S>(&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<World>, stop_on_error: bool) -> Result<Vec<StagedExecutorError>, 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<u64>, visited: &mut HashSet<u64>, 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(())
}
}