From 61efc358ce357da1df714b07b830a3f56529981e Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Sun, 24 Mar 2024 22:40:38 -0400 Subject: [PATCH 01/11] scene: make scenes own its own world, no references --- Cargo.lock | 7 ----- lyra-scene/src/lib.rs | 65 +++++++------------------------------------ 2 files changed, 10 insertions(+), 62 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 90b0d29..642fd0c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -984,12 +984,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fps_counter" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3aaba7ff514ee9d802b562927f80b1e94e93d8e74c31b134c9c3762dabf1a36b" - [[package]] name = "fsevent-sys" version = "4.1.0" @@ -3104,7 +3098,6 @@ version = "0.1.0" dependencies = [ "anyhow", "async-std", - "fps_counter", "lyra-engine", "tracing", ] diff --git a/lyra-scene/src/lib.rs b/lyra-scene/src/lib.rs index 345b2a1..d5dc820 100644 --- a/lyra-scene/src/lib.rs +++ b/lyra-scene/src/lib.rs @@ -1,4 +1,4 @@ -use std::{collections::VecDeque, ops::{Deref, DerefMut}}; +use std::collections::VecDeque; use lyra_ecs::{query::Entities, relation::ChildOf, Bundle, Component, Entity, World}; @@ -21,77 +21,31 @@ pub struct SceneNodeFlag; #[derive(Component)] pub struct SceneNodeRoot; -enum MutCow<'a, T> { - Mut(&'a mut T), - Owned(T), -} - -impl<'a, T> Deref for MutCow<'a, T> { - type Target = T; - - fn deref(&self) -> &Self::Target { - match self { - MutCow::Mut(t) => t, - MutCow::Owned(t) => t, - } - } -} - -impl<'a, T> DerefMut for MutCow<'a, T> { - fn deref_mut(&mut self) -> &mut Self::Target { - match self { - MutCow::Mut(t) => t, - MutCow::Owned(t) => t, - } - } -} - /// A SceneGraph is a Graph of nodes that represents the hierarchy of a scene. /// /// This SceneGraph is special in the sense that it is literally just an ECS world with methods /// implemented for it that make it easier to use for a SceneGraph. //#[derive(Default)] -pub struct SceneGraph<'a> { - pub(crate) world: MutCow<'a, World>, +pub struct SceneGraph { + pub(crate) world: World, root_node: SceneNode, } -impl<'a> SceneGraph<'a> { +impl SceneGraph { /// Create a new SceneGraph with its own ECS World. pub fn new() -> Self { - let mut world = World::new(); + let world = World::new(); - let e = world.spawn((Transform::from_translation(Vec3::new(0.0, 0.0, 0.0)), SceneNodeRoot)); - let root = SceneNode::new(None, e); - - Self { - world: MutCow::Owned(world), - root_node: root - } - } - - /// Retrieve a SceneGraph from an ECS World. - /// - /// Returns `None` if the `root_entity` was not created from a `SceneGraph` that was later - /// inserted into another world with [`SceneGraph::into_world`]. - pub fn from_world(world: &'a mut World, root_entity: Entity) -> Option { - if world.view_one::<(&SceneNodeRoot, &Transform)>(root_entity).get().is_none() { - None - } else { - Some(Self { - world: MutCow::Mut(world), - root_node: SceneNode::new(None, root_entity), - }) - } + Self::from_world(world) } /// Create a new SceneGraph inside an existing ECS World. - pub fn new_from_world(world: &'a mut World) -> Self { + pub fn from_world(mut world: World) -> Self { let root_en = world.spawn((Transform::from_translation(Vec3::new(0.0, 0.0, 0.0)), SceneNodeRoot)); let root = SceneNode::new(None, root_en); Self { - world: MutCow::Mut(world), + world, root_node: root, } } @@ -185,7 +139,8 @@ impl<'a> SceneGraph<'a> { where F: FnMut(&SceneNode, Transform), { - let v = self.world.view::<(Entities, &Transform)>() + let v = self.world + .view::<(Entities, &Transform)>() .relates_to::(start.entity()); for ((e, _), _rel) in v.iter() { From 46cdcfdd3b9d809c75827c8a6bcdaa062b4fa6d6 Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Sat, 30 Mar 2024 22:12:00 -0400 Subject: [PATCH 02/11] ecs: make resources Send + Sync, rewrite Commands, CommandsQueue so that they are Send + Sync --- lyra-ecs/Cargo.toml | 1 + lyra-ecs/src/command.rs | 149 ++++++++++++++++++++++----- lyra-ecs/src/lib.rs | 3 + lyra-ecs/src/query/resource.rs | 12 ++- lyra-ecs/src/relation/relate_pair.rs | 6 +- lyra-ecs/src/resource.rs | 45 +++++--- lyra-ecs/src/system/graph.rs | 17 +-- lyra-ecs/src/world.rs | 36 ++++--- 8 files changed, 193 insertions(+), 76 deletions(-) diff --git a/lyra-ecs/Cargo.toml b/lyra-ecs/Cargo.toml index 47643c8..8068984 100644 --- a/lyra-ecs/Cargo.toml +++ b/lyra-ecs/Cargo.toml @@ -14,6 +14,7 @@ lyra-math = { path = "../lyra-math", optional = true } anyhow = "1.0.75" thiserror = "1.0.50" paste = "1.0.14" +atomic_refcell = "0.1.13" [dev-dependencies] rand = "0.8.5" # used for tests diff --git a/lyra-ecs/src/command.rs b/lyra-ecs/src/command.rs index b7f6f91..0a3b320 100644 --- a/lyra-ecs/src/command.rs +++ b/lyra-ecs/src/command.rs @@ -1,4 +1,4 @@ -use std::{any::Any, cell::RefMut, collections::VecDeque, ptr::{self, NonNull}}; +use std::{any::Any, cell::RefMut, mem::{self, MaybeUninit}, ptr::{self, NonNull}}; use crate::{system::FnArgFetcher, Access, Bundle, Entities, Entity, World}; @@ -23,24 +23,73 @@ where } } -type RunCommand = unsafe fn(cmd: Box, world: &mut World); +type RunCommand = unsafe fn(cmd: *mut (), world: Option<&mut World>) -> usize; + +#[repr(C, packed)] +struct PackedCommand { + run: RunCommand, + cmd: T, +} /// Stores a queue of commands that will get executed after the system is ran. /// /// This struct can be inserted as a resource into the world, and the commands will be /// executed by the [`GraphExecutor`](crate::system::GraphExecutor) after the system is executed. #[derive(Default)] -pub struct CommandQueue(VecDeque<(RunCommand, Box)>); +pub struct CommandQueue { + data: Vec>, +} + +impl CommandQueue { + /// Execute the commands in the queue. + /// + /// If `world` is `None`, the commands will just be dropped and the memory freed. + fn execute(&mut self, mut world: Option<&mut World>) { + let range = self.data.as_mut_ptr_range(); + let mut current = range.start; + let end = range.end; + + while current < end { + // Retrieve the runner for the command. + // Safety: current pointer will either be the start of the buffer, or at the start of a new PackedCommand + let run_fn = unsafe { current.cast::().read_unaligned() }; + + // Retrieves the pointer to the command which is just after RunCommand due to PackedCommand. + // Safety: PackedCommand is repr C and packed, so it will be right after the RunCommand. + current = unsafe { current.add(mem::size_of::()) }; + + // Now run the command, providing the type erased pointer to the command. + let read_size = unsafe { run_fn(current.cast(), world.as_deref_mut()) }; + + // The pointer is added to so that it is just after the command that was ran. + // Safety: the RunCommand returns the size of the command + current = unsafe { current.add(read_size) }; + } + + // Safety: all of the commands were just read from the pointers. + unsafe { self.data.set_len(0) }; + } +} + +impl Drop for CommandQueue { + fn drop(&mut self) { + if !self.data.is_empty() { + println!("CommandQueue has commands but is being dropped"); + } + + self.execute(None); + } +} /// Used in a system to queue up commands that will run right after this system. /// /// This can be used to delay the mutation of the world until after the system is ran. These -/// must be used if you're mutating the world inside a [`ViewState`](crate::query::ViewState). +/// must be used if you're mutating the world inside a [`View`](crate::query::View). /// /// ```nobuild /// fn particle_spawner_system( /// commands: Commands, -/// view: ViewState<(&Campfire, &Transform)> +/// view: View<(&Campfire, &Transform)> /// ) -> anyhow::Result<()> { /// for (campfire, pos) in view.iter() { /// // If you do not use commands to spawn this, the next iteration @@ -66,17 +115,44 @@ impl<'a, 'b> Commands<'a, 'b> { /// Add a command to the end of the command queue pub fn add(&mut self, cmd: C) { - let cmd = Box::new(cmd); + let run_fn = |cmd_ptr: *mut (), world: Option<&mut World>| { + // Safety: the pointer is a type-erased pointer to the command. The pointer is read + // then dropped out of scope, this closure will not be ran again so no use-after-free + // will occur. + let cmd: C = unsafe { ptr::read_unaligned(cmd_ptr.cast::()) }; + match world { + Some(world) => cmd.run(world), + None => {} // cmd just gets dropped + } - let run_fn = |cmd: Box, world: &mut World| { - let cmd = cmd.as_any_boxed() - .downcast::() - .unwrap(); - - cmd.run(world); + // the size of the command must be returned to increment the pointer when applying + // the command queue. + mem::size_of::() }; - self.queue.0.push_back((run_fn, cmd)); + let data = &mut self.queue.data; + + // Reserve enough bytes from the vec to store the packed command and its run fn. + let old_len = data.len(); + data.reserve(mem::size_of::>()); + + // Get a pointer to the end of the packed data. Safe since we just reserved enough memory + // to store this command. + let end_ptr = unsafe { data.as_mut_ptr().add(old_len) }; + + unsafe { + // write the command and its runner into the buffer + end_ptr.cast::>() + // written unaligned to keep everything packed + .write_unaligned(PackedCommand { + run: run_fn, + cmd, + }); + + // we wrote to the vec's buffer without using its api, so we need manually + // set the length of the vec. + data.set_len(old_len + mem::size_of::>()); + } } /// Spawn an entity into the World. See [`World::spawn`] @@ -91,14 +167,8 @@ impl<'a, 'b> Commands<'a, 'b> { } /// Execute all commands in the queue, in order of insertion - pub fn execute(&mut self, world: &mut World) -> anyhow::Result<()> { - while let Some((cmd_fn, cmd_ptr)) = self.queue.0.pop_front() { - unsafe { - cmd_fn(cmd_ptr, world); - } - } - - Ok(()) + pub fn execute(&mut self, world: &mut World) { + self.queue.execute(Some(world)); } } @@ -125,21 +195,26 @@ impl FnArgFetcher for Commands<'_, '_> { let mut cmds = Commands::new(&mut state, world); // safety: Commands has a mut borrow only to entities in the world let world = unsafe { world_ptr.as_mut() }; - cmds.execute(world).unwrap() + cmds.execute(world); } } +/// A system for executing deferred commands that are stored in a [`World`] as a Resource. +/// +/// Commands are usually added inside a system from a [`Commands`] object created just for it +/// as an fn argument. However, there may be cases that commands cannot be added that way, so +/// they can also be added as a resource and executed later in this system. pub fn execute_deferred_commands(world: &mut World, mut commands: RefMut) -> anyhow::Result<()> { - commands.execute(world)?; + commands.execute(world); Ok(()) } #[cfg(test)] mod tests { - use std::{cell::Ref, ptr::NonNull}; + use std::{cell::Ref, ptr::NonNull, sync::{atomic::{AtomicU32, Ordering}, Arc}}; - use crate::{system::{GraphExecutor, IntoSystem}, tests::Vec2, Commands, DynTypeId, World}; + use crate::{system::{GraphExecutor, IntoSystem}, tests::Vec2, CommandQueue, Commands, DynTypeId, World}; #[test] fn deferred_commands() { @@ -170,4 +245,28 @@ mod tests { let vec2: Ref = unsafe { col.get(3) }; assert_eq!(vec2.clone(), spawned_vec); } + + /// A test that ensures a command in a command queue will only ever run once. + #[test] + fn commands_only_one_exec() { + let mut world = World::new(); + + let counter = Arc::new(AtomicU32::new(0)); + + let mut queue = CommandQueue::default(); + let mut commands = Commands::new(&mut queue, &mut world); + + let counter_cl = counter.clone(); + commands.add(move |_world: &mut World| { + counter_cl.fetch_add(1, Ordering::AcqRel); + }); + + queue.execute(Some(&mut world)); + assert_eq!(1, counter.load(Ordering::Acquire)); + + queue.execute(Some(&mut world)); + // If its not one, the command somehow was executed. + // I would be surprised it wouldn't cause some segfault but still increment the counter + assert_eq!(1, counter.load(Ordering::Acquire)); + } } \ No newline at end of file diff --git a/lyra-ecs/src/lib.rs b/lyra-ecs/src/lib.rs index 5f1496f..7244349 100644 --- a/lyra-ecs/src/lib.rs +++ b/lyra-ecs/src/lib.rs @@ -51,6 +51,9 @@ pub mod math; pub use lyra_ecs_derive::*; +pub use atomic_refcell::AtomicRef; +pub use atomic_refcell::AtomicRefMut; + #[cfg(test)] mod tests; diff --git a/lyra-ecs/src/query/resource.rs b/lyra-ecs/src/query/resource.rs index 328f2b2..4920e70 100644 --- a/lyra-ecs/src/query/resource.rs +++ b/lyra-ecs/src/query/resource.rs @@ -1,4 +1,6 @@ -use std::{marker::PhantomData, cell::{Ref, RefMut}}; +use std::marker::PhantomData; + +use atomic_refcell::{AtomicRef, AtomicRefMut}; use crate::{World, resource::ResourceObject}; @@ -9,7 +11,7 @@ pub struct FetchResource<'a, T> { _phantom: PhantomData, } -impl<'a, T: 'a + 'static> Fetch<'a> for FetchResource<'a, T> { +impl<'a, T: ResourceObject + 'a> Fetch<'a> for FetchResource<'a, T> { type Item = Res<'a, T>; fn dangling() -> Self { @@ -79,7 +81,7 @@ impl AsQuery for QueryResource { } /// A struct used for querying resources from the World. -pub struct Res<'a, T>(pub(crate) Ref<'a, T>); +pub struct Res<'a, T>(pub(crate) AtomicRef<'a, T>); impl<'a, T: ResourceObject> std::ops::Deref for Res<'a, T> { type Target = T; @@ -98,7 +100,7 @@ pub struct FetchResourceMut<'a, T> { _phantom: PhantomData, } -impl<'a, T: 'a + 'static> Fetch<'a> for FetchResourceMut<'a, T> { +impl<'a, T: ResourceObject + 'a> Fetch<'a> for FetchResourceMut<'a, T> { type Item = ResMut<'a, T>; fn dangling() -> Self { @@ -167,7 +169,7 @@ impl AsQuery for QueryResourceMut { } /// A struct used for querying resources from the World. -pub struct ResMut<'a, T>(pub(crate) RefMut<'a, T>); +pub struct ResMut<'a, T>(pub(crate) AtomicRefMut<'a, T>); impl<'a, T: ResourceObject> std::ops::Deref for ResMut<'a, T> { type Target = T; diff --git a/lyra-ecs/src/relation/relate_pair.rs b/lyra-ecs/src/relation/relate_pair.rs index e19ddbb..d66645b 100644 --- a/lyra-ecs/src/relation/relate_pair.rs +++ b/lyra-ecs/src/relation/relate_pair.rs @@ -83,8 +83,10 @@ where /// A query that fetches the origin, and target of a relation of type `R`. /// /// It provides it as a tuple in the following format: `(origin, relation, target)`. -/// Similar to [`RelatesTo`](super::RelatesTo), you can use [`ViewState::relate_pair`] to get a view that fetches the -/// pair, or unlike [`RelatesTo`](super::RelatesTo), you can do the common procedure of using [`World::view`]. +/// Similar to [`RelatesTo`](super::RelatesTo), you can use +/// [`ViewState::relate_pair`](crate::relation::ViewState::relate_pair) to get a view that +/// fetches the pair, or unlike [`RelatesTo`](super::RelatesTo), you can do the common +/// procedure of using [`World::view`]. pub struct RelatePair { _marker: PhantomData, } diff --git a/lyra-ecs/src/resource.rs b/lyra-ecs/src/resource.rs index ae48fc6..3fc6508 100644 --- a/lyra-ecs/src/resource.rs +++ b/lyra-ecs/src/resource.rs @@ -1,26 +1,40 @@ -use std::{any::{TypeId, Any}, cell::{RefCell, Ref, RefMut}}; +use std::any::{TypeId, Any}; + +use atomic_refcell::{AtomicRef, AtomicRefCell, AtomicRefMut}; /// Shorthand for `Send + Sync + 'static`, so it never needs to be implemented manually. -pub trait ResourceObject: 'static {} -impl ResourceObject for T {} +pub trait ResourceObject: Send + Sync + Any { + fn as_any(&self) -> &dyn Any; + fn as_any_mut(&mut self) -> &mut dyn Any; +} + +impl ResourceObject for T { + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } +} /// A type erased storage for a Resource. pub struct ResourceData { - pub(crate) data: Box>, + pub(crate) data: Box>, type_id: TypeId, } impl ResourceData { - pub fn new(data: T) -> Self { + pub fn new(data: T) -> Self { Self { - data: Box::new(RefCell::new(data)), + data: Box::new(AtomicRefCell::new(data)), type_id: TypeId::of::(), } } /// Returns a boolean indicating whether or not `T`` is of the same type of the Resource - pub fn is(&self) -> bool { + pub fn is(&self) -> bool { self.type_id == TypeId::of::() } @@ -30,8 +44,8 @@ impl ResourceData { /// /// * If the data is already borrowed mutably, this will panic. /// * If the type of `T` is not the same as the resource type. - pub fn get(&self) -> Ref { - Ref::map(self.data.borrow(), |a| a.downcast_ref().unwrap()) + pub fn get(&self) -> AtomicRef { + AtomicRef::map(self.data.borrow(), |a| a.as_any().downcast_ref().unwrap()) } /// Mutably borrow the data inside of the resource. @@ -40,8 +54,8 @@ impl ResourceData { /// /// * If the data is already borrowed mutably, this will panic. /// * If the type of `T` is not the same as the resource type. - pub fn get_mut(&self) -> RefMut { - RefMut::map(self.data.borrow_mut(), |a| a.downcast_mut().unwrap()) + pub fn get_mut(&self) -> AtomicRefMut { + AtomicRefMut::map(self.data.borrow_mut(), |a| a.as_any_mut().downcast_mut().unwrap()) } /// Borrow the data inside of the resource. @@ -49,10 +63,9 @@ impl ResourceData { /// # Panics /// /// * If the type of `T` is not the same as the resource type. - pub fn try_get(&self) -> Option> { - + pub fn try_get(&self) -> Option> { self.data.try_borrow() - .map(|r| Ref::map(r, |a| a.downcast_ref().unwrap())) + .map(|r| AtomicRef::map(r, |a| a.as_any().downcast_ref().unwrap())) .ok() } @@ -61,9 +74,9 @@ impl ResourceData { /// # Panics /// /// * If the type of `T` is not the same as the resource type. - pub fn try_get_mut(&self) -> Option> { + pub fn try_get_mut(&self) -> Option> { self.data.try_borrow_mut() - .map(|r| RefMut::map(r, |a| a.downcast_mut().unwrap())) + .map(|r| AtomicRefMut::map(r, |a| a.as_any_mut().downcast_mut().unwrap())) .ok() } } \ No newline at end of file diff --git a/lyra-ecs/src/system/graph.rs b/lyra-ecs/src/system/graph.rs index 73fbc17..d6851f8 100644 --- a/lyra-ecs/src/system/graph.rs +++ b/lyra-ecs/src/system/graph.rs @@ -58,7 +58,8 @@ impl GraphExecutor { } /// Executes the systems in the graph - pub fn execute(&mut self, mut world_ptr: NonNull, stop_on_error: bool) -> Result, GraphExecutorError> { + pub fn execute(&mut self, mut world_ptr: NonNull, stop_on_error: bool) + -> Result, GraphExecutorError> { let mut stack = VecDeque::new(); let mut visited = HashSet::new(); @@ -99,23 +100,15 @@ impl GraphExecutor { let mut commands = Commands::new(&mut queue, world); let world = unsafe { world_ptr.as_mut() }; - if let Err(e) = commands.execute(world) - .map_err(|e| GraphExecutorError::Command(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 - } + commands.execute(world); } } Ok(possible_errors) } - fn topological_sort<'a>(&'a self, stack: &mut VecDeque, visited: &mut HashSet<&'a str>, node: &'a GraphSystem) -> Result<(), GraphExecutorError> { + fn topological_sort<'a>(&'a self, stack: &mut VecDeque, + visited: &mut HashSet<&'a str>, node: &'a GraphSystem) -> Result<(), GraphExecutorError> { if !visited.contains(node.name.as_str()) { visited.insert(&node.name); diff --git a/lyra-ecs/src/world.rs b/lyra-ecs/src/world.rs index 31abb31..eb4593f 100644 --- a/lyra-ecs/src/world.rs +++ b/lyra-ecs/src/world.rs @@ -1,4 +1,6 @@ -use std::{any::TypeId, cell::{Ref, RefMut}, collections::HashMap, ptr::NonNull}; +use std::{any::TypeId, collections::HashMap, ptr::NonNull}; + +use atomic_refcell::{AtomicRef, AtomicRefMut}; use crate::{archetype::{Archetype, ArchetypeId}, bundle::Bundle, query::{dynamic::DynamicView, AsQuery, Query, ViewState, ViewIter, ViewOne}, resource::ResourceData, ComponentInfo, DynTypeId, Entities, Entity, ResourceObject, Tick, TickTracker}; @@ -271,17 +273,17 @@ impl World { //pub fn view_one(&self, entity: EntityId) -> - pub fn add_resource(&mut self, data: T) { + pub fn add_resource(&mut self, data: T) { self.resources.insert(TypeId::of::(), ResourceData::new(data)); } - pub fn add_resource_default(&mut self) { + pub fn add_resource_default(&mut self) { self.resources.insert(TypeId::of::(), ResourceData::new(T::default())); } /// Get a resource from the world, or insert it into the world with the provided /// `fn` and return it. - pub fn get_resource_or_else(&mut self, f: F) -> RefMut + pub fn get_resource_or_else(&mut self, f: F) -> AtomicRefMut where F: Fn() -> T + 'static { @@ -291,7 +293,7 @@ impl World { } /// Get a resource from the world, or insert it into the world as its default. - pub fn get_resource_or_default(&mut self) -> RefMut + pub fn get_resource_or_default(&mut self) -> AtomicRefMut { self.resources.entry(TypeId::of::()) .or_insert_with(|| ResourceData::new(T::default())) @@ -302,21 +304,21 @@ impl World { /// /// Will panic if the resource is not in the world. See [`World::try_get_resource`] for /// a function that returns an option. - pub fn get_resource(&self) -> Ref { + pub fn get_resource(&self) -> AtomicRef { self.resources.get(&TypeId::of::()) .expect(&format!("World is missing resource of type '{}'", std::any::type_name::())) .get() } /// Returns boolean indicating if the World contains a resource of type `T`. - pub fn has_resource(&self) -> bool { + pub fn has_resource(&self) -> bool { self.resources.contains_key(&TypeId::of::()) } /// Attempts to get a resource from the World. /// /// Returns `None` if the resource was not found. - pub fn try_get_resource(&self) -> Option> { + pub fn try_get_resource(&self) -> Option> { self.resources.get(&TypeId::of::()) .and_then(|r| r.try_get()) } @@ -325,7 +327,7 @@ impl World { /// /// Will panic if the resource is not in the world. See [`World::try_get_resource_mut`] for /// a function that returns an option. - pub fn get_resource_mut(&self) -> RefMut { + pub fn get_resource_mut(&self) -> AtomicRefMut { self.resources.get(&TypeId::of::()) .expect(&format!("World is missing resource of type '{}'", std::any::type_name::())) .get_mut() @@ -334,7 +336,7 @@ impl World { /// Attempts to get a mutable borrow of a resource from the World. /// /// Returns `None` if the resource was not found. - pub fn try_get_resource_mut(&self) -> Option> { + pub fn try_get_resource_mut(&self) -> Option> { self.resources.get(&TypeId::of::()) .and_then(|r| r.try_get_mut()) } @@ -364,6 +366,10 @@ impl World { } } +// TODO: Ensure that all non-send resources are only accessible on the main thread. +/* unsafe impl Send for World {} +unsafe impl Sync for World {} */ + #[cfg(test)] mod tests { use crate::{query::TickOf, tests::{Vec2, Vec3}, Entity}; @@ -440,16 +446,14 @@ mod tests { #[test] fn resource_multi_borrow() { let mut world = World::new(); - { - let counter = SimpleCounter(4582); - world.add_resource(counter); - } + let counter = SimpleCounter(4582); + world.add_resource(counter); // test multiple borrows at the same time let counter = world.get_resource::(); assert_eq!(counter.0, 4582); let counter2 = world.get_resource::(); - assert_eq!(counter2.0, 4582); + assert_eq!(counter.0, 4582); assert_eq!(counter2.0, 4582); } @@ -461,7 +465,7 @@ mod tests { world.add_resource(counter); } - // test multiple borrows at the same time + // test that its only possible to get a single mutable borrow let counter = world.get_resource_mut::(); assert_eq!(counter.0, 4582); assert!(world.try_get_resource_mut::().is_none()); From e00d0d71d1af4d5d8062eb287f2b17de55bf7734 Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Sat, 30 Mar 2024 22:17:12 -0400 Subject: [PATCH 03/11] examples: move assets outside of testbed for other examples --- examples/{testbed => }/assets/AntiqueCamera.glb | Bin examples/{testbed => }/assets/crate/Image.png | Bin examples/{testbed => }/assets/crate/container2.png | Bin examples/{testbed => }/assets/crate/crate.bin | Bin examples/{testbed => }/assets/crate/crate.gltf | 0 examples/{testbed => }/assets/cube-embedded.gltf | 0 examples/{testbed => }/assets/cube-texture-bin.glb | Bin .../{testbed => }/assets/cube-texture-embedded.gltf | 0 examples/{testbed => }/assets/happy-tree.png | Bin .../assets/lumberyard-bistro/.gitignore | 0 .../assets/pos-testing/child-node-cubes.glb | Bin .../assets/pos-testing/separate-nodes-two-cubes.glb | Bin examples/{testbed => }/assets/sponza/.gitignore | 0 examples/{testbed => }/assets/sponza/README.md | 0 .../assets/texture-sep/texture-sep.bin | Bin .../assets/texture-sep/texture-sep.gltf | 0 .../{testbed => }/assets/texture-sep/uvgrid.png | Bin .../{testbed => }/assets/texture-sep/uvgrid.png.bk | Bin examples/testbed/src/main.rs | 4 ++-- 19 files changed, 2 insertions(+), 2 deletions(-) rename examples/{testbed => }/assets/AntiqueCamera.glb (100%) rename examples/{testbed => }/assets/crate/Image.png (100%) rename examples/{testbed => }/assets/crate/container2.png (100%) rename examples/{testbed => }/assets/crate/crate.bin (100%) rename examples/{testbed => }/assets/crate/crate.gltf (100%) rename examples/{testbed => }/assets/cube-embedded.gltf (100%) rename examples/{testbed => }/assets/cube-texture-bin.glb (100%) rename examples/{testbed => }/assets/cube-texture-embedded.gltf (100%) rename examples/{testbed => }/assets/happy-tree.png (100%) rename examples/{testbed => }/assets/lumberyard-bistro/.gitignore (100%) rename examples/{testbed => }/assets/pos-testing/child-node-cubes.glb (100%) rename examples/{testbed => }/assets/pos-testing/separate-nodes-two-cubes.glb (100%) rename examples/{testbed => }/assets/sponza/.gitignore (100%) rename examples/{testbed => }/assets/sponza/README.md (100%) rename examples/{testbed => }/assets/texture-sep/texture-sep.bin (100%) rename examples/{testbed => }/assets/texture-sep/texture-sep.gltf (100%) rename examples/{testbed => }/assets/texture-sep/uvgrid.png (100%) rename examples/{testbed => }/assets/texture-sep/uvgrid.png.bk (100%) diff --git a/examples/testbed/assets/AntiqueCamera.glb b/examples/assets/AntiqueCamera.glb similarity index 100% rename from examples/testbed/assets/AntiqueCamera.glb rename to examples/assets/AntiqueCamera.glb diff --git a/examples/testbed/assets/crate/Image.png b/examples/assets/crate/Image.png similarity index 100% rename from examples/testbed/assets/crate/Image.png rename to examples/assets/crate/Image.png diff --git a/examples/testbed/assets/crate/container2.png b/examples/assets/crate/container2.png similarity index 100% rename from examples/testbed/assets/crate/container2.png rename to examples/assets/crate/container2.png diff --git a/examples/testbed/assets/crate/crate.bin b/examples/assets/crate/crate.bin similarity index 100% rename from examples/testbed/assets/crate/crate.bin rename to examples/assets/crate/crate.bin diff --git a/examples/testbed/assets/crate/crate.gltf b/examples/assets/crate/crate.gltf similarity index 100% rename from examples/testbed/assets/crate/crate.gltf rename to examples/assets/crate/crate.gltf diff --git a/examples/testbed/assets/cube-embedded.gltf b/examples/assets/cube-embedded.gltf similarity index 100% rename from examples/testbed/assets/cube-embedded.gltf rename to examples/assets/cube-embedded.gltf diff --git a/examples/testbed/assets/cube-texture-bin.glb b/examples/assets/cube-texture-bin.glb similarity index 100% rename from examples/testbed/assets/cube-texture-bin.glb rename to examples/assets/cube-texture-bin.glb diff --git a/examples/testbed/assets/cube-texture-embedded.gltf b/examples/assets/cube-texture-embedded.gltf similarity index 100% rename from examples/testbed/assets/cube-texture-embedded.gltf rename to examples/assets/cube-texture-embedded.gltf diff --git a/examples/testbed/assets/happy-tree.png b/examples/assets/happy-tree.png similarity index 100% rename from examples/testbed/assets/happy-tree.png rename to examples/assets/happy-tree.png diff --git a/examples/testbed/assets/lumberyard-bistro/.gitignore b/examples/assets/lumberyard-bistro/.gitignore similarity index 100% rename from examples/testbed/assets/lumberyard-bistro/.gitignore rename to examples/assets/lumberyard-bistro/.gitignore diff --git a/examples/testbed/assets/pos-testing/child-node-cubes.glb b/examples/assets/pos-testing/child-node-cubes.glb similarity index 100% rename from examples/testbed/assets/pos-testing/child-node-cubes.glb rename to examples/assets/pos-testing/child-node-cubes.glb diff --git a/examples/testbed/assets/pos-testing/separate-nodes-two-cubes.glb b/examples/assets/pos-testing/separate-nodes-two-cubes.glb similarity index 100% rename from examples/testbed/assets/pos-testing/separate-nodes-two-cubes.glb rename to examples/assets/pos-testing/separate-nodes-two-cubes.glb diff --git a/examples/testbed/assets/sponza/.gitignore b/examples/assets/sponza/.gitignore similarity index 100% rename from examples/testbed/assets/sponza/.gitignore rename to examples/assets/sponza/.gitignore diff --git a/examples/testbed/assets/sponza/README.md b/examples/assets/sponza/README.md similarity index 100% rename from examples/testbed/assets/sponza/README.md rename to examples/assets/sponza/README.md diff --git a/examples/testbed/assets/texture-sep/texture-sep.bin b/examples/assets/texture-sep/texture-sep.bin similarity index 100% rename from examples/testbed/assets/texture-sep/texture-sep.bin rename to examples/assets/texture-sep/texture-sep.bin diff --git a/examples/testbed/assets/texture-sep/texture-sep.gltf b/examples/assets/texture-sep/texture-sep.gltf similarity index 100% rename from examples/testbed/assets/texture-sep/texture-sep.gltf rename to examples/assets/texture-sep/texture-sep.gltf diff --git a/examples/testbed/assets/texture-sep/uvgrid.png b/examples/assets/texture-sep/uvgrid.png similarity index 100% rename from examples/testbed/assets/texture-sep/uvgrid.png rename to examples/assets/texture-sep/uvgrid.png diff --git a/examples/testbed/assets/texture-sep/uvgrid.png.bk b/examples/assets/texture-sep/uvgrid.png.bk similarity index 100% rename from examples/testbed/assets/texture-sep/uvgrid.png.bk rename to examples/assets/texture-sep/uvgrid.png.bk diff --git a/examples/testbed/src/main.rs b/examples/testbed/src/main.rs index be558ba..372ff32 100644 --- a/examples/testbed/src/main.rs +++ b/examples/testbed/src/main.rs @@ -73,7 +73,7 @@ async fn main() { //let diffuse_texture = resman.request::("assets/happy-tree.png").unwrap(); //let antique_camera_model = resman.request::("assets/AntiqueCamera.glb").unwrap(); //let cube_model = resman.request::("assets/cube-texture-bin.glb").unwrap(); - let cube_gltf = resman.request::("assets/texture-sep/texture-sep.gltf").unwrap(); + let cube_gltf = resman.request::("../assets/texture-sep/texture-sep.gltf").unwrap(); /*let crate_gltf = resman.request::("assets/crate/crate.gltf").unwrap(); let separate_gltf = resman.request::("assets/pos-testing/child-node-cubes.glb").unwrap(); */ @@ -88,7 +88,7 @@ async fn main() { let separate_scene = &separate_gltf.data_ref() .unwrap().scenes[0]; */ - let sponza_model = resman.request::("assets/sponza/Sponza.gltf").unwrap(); + let sponza_model = resman.request::("../assets/sponza/Sponza.gltf").unwrap(); drop(resman); sponza_model.wait_recurse_dependencies_load(); From e5018c8258ca2cc94c4c914a25f76a5b50adcf52 Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Sat, 30 Mar 2024 22:42:41 -0400 Subject: [PATCH 04/11] reflect: fix type registry from changes with ecs resources --- .envrc | 1 + lyra-reflect/src/lib.rs | 28 ++++++++++++++-------------- lyra-reflect/src/registry.rs | 14 +++++++------- lyra-reflect/src/resource.rs | 20 ++++++++++++-------- 4 files changed, 34 insertions(+), 29 deletions(-) create mode 100644 .envrc diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..17d6464 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use_nix \ No newline at end of file diff --git a/lyra-reflect/src/lib.rs b/lyra-reflect/src/lib.rs index 8823aa7..d1bf4f7 100644 --- a/lyra-reflect/src/lib.rs +++ b/lyra-reflect/src/lib.rs @@ -2,7 +2,7 @@ use std::{any::TypeId, any::Any, cell::{Ref, RefMut}}; -use lyra_ecs::World; +use lyra_ecs::{AtomicRef, AtomicRefMut, World}; extern crate self as lyra_reflect; @@ -250,46 +250,46 @@ pub trait FromType { pub trait ReflectWorldExt { /// Retrieves the type registry from the world. - fn get_type_registry(&self) -> Ref; + fn get_type_registry(&self) -> AtomicRef; /// Retrieves the type registry mutably from the world. - fn get_type_registry_mut(&self) -> RefMut; + fn get_type_registry_mut(&self) -> AtomicRefMut; /// Get a registered type from the type registry. Returns `None` if the type is not registered - fn get_type(&self, type_id: TypeId) -> Option>; + fn get_type(&self, type_id: TypeId) -> Option>; /// Get a mutable registered type from the type registry in the world. returns `None` if the type is not registered. - fn get_type_mut(&self, type_id: TypeId) -> Option>; + fn get_type_mut(&self, type_id: TypeId) -> Option>; /// Get a registered type, or register a new type and return it. - fn get_type_or_default(&self, type_id: TypeId) -> RefMut; + fn get_type_or_default(&self, type_id: TypeId) -> AtomicRefMut; } impl ReflectWorldExt for World { - fn get_type_registry(&self) -> Ref { + fn get_type_registry(&self) -> AtomicRef { self.get_resource::() } - fn get_type_registry_mut(&self) -> RefMut { + fn get_type_registry_mut(&self) -> AtomicRefMut { self.get_resource_mut::() } - fn get_type(&self, type_id: TypeId) -> Option> { + fn get_type(&self, type_id: TypeId) -> Option> { let r = self.get_resource::(); if r.has_type(type_id) { - Some(Ref::map(r, |tr| tr.get_type(type_id).unwrap())) + Some(AtomicRef::map(r, |tr| tr.get_type(type_id).unwrap())) } else { None } } - fn get_type_mut(&self, type_id: TypeId) -> Option> { + fn get_type_mut(&self, type_id: TypeId) -> Option> { let r = self.get_resource_mut::(); if r.has_type(type_id) { - Some(RefMut::map(r, |tr| tr.get_type_mut(type_id).unwrap())) + Some(AtomicRefMut::map(r, |tr| tr.get_type_mut(type_id).unwrap())) } else { None } } - fn get_type_or_default(&self, type_id: TypeId) -> RefMut { + fn get_type_or_default(&self, type_id: TypeId) -> AtomicRefMut { let r = self.get_resource_mut::(); - RefMut::map(r, |tr| tr.get_type_or_default(type_id)) + AtomicRefMut::map(r, |tr| tr.get_type_or_default(type_id)) } } \ No newline at end of file diff --git a/lyra-reflect/src/registry.rs b/lyra-reflect/src/registry.rs index 46651f4..135dd79 100644 --- a/lyra-reflect/src/registry.rs +++ b/lyra-reflect/src/registry.rs @@ -1,4 +1,4 @@ -use std::{any::{TypeId, type_name}, collections::HashMap, ops::Deref}; +use std::{any::{type_name, Any, TypeId}, collections::HashMap, ops::Deref}; /// Storage for #[derive(Default, Clone)] @@ -44,19 +44,19 @@ impl TypeRegistry { } } -pub trait TypeData: std::any::Any { - fn as_any(&self) -> &dyn std::any::Any; - fn as_any_mut(&mut self) -> &mut dyn std::any::Any; +pub trait TypeData: Send + Sync + Any { + fn as_any(&self) -> &dyn Any; + fn as_any_mut(&mut self) -> &mut dyn Any; fn boxed_clone(&self) -> Box; } -impl TypeData for T { - fn as_any(&self) -> &dyn std::any::Any { +impl TypeData for T { + fn as_any(&self) -> &dyn Any { self } - fn as_any_mut(&mut self) -> &mut dyn std::any::Any { + fn as_any_mut(&mut self) -> &mut dyn Any { self } diff --git a/lyra-reflect/src/resource.rs b/lyra-reflect/src/resource.rs index ab7445c..f7caca1 100644 --- a/lyra-reflect/src/resource.rs +++ b/lyra-reflect/src/resource.rs @@ -1,6 +1,6 @@ -use std::{any::{Any, TypeId}, cell::{Ref, RefMut}, ptr::NonNull}; +use std::{any::{Any, TypeId}, mem, ptr::NonNull}; -use lyra_ecs::{World, ResourceObject}; +use lyra_ecs::{AtomicRef, AtomicRefMut, ResourceObject, World}; use crate::{Reflect, FromType}; @@ -8,20 +8,20 @@ use crate::{Reflect, FromType}; pub struct ReflectedResource { pub type_id: TypeId, - fn_reflect: for<'a> fn (world: &'a World) -> Option>, - fn_reflect_mut: for<'a> fn (world: &'a mut World) -> Option>, + fn_reflect: for<'a> fn (world: &'a World) -> Option>, + fn_reflect_mut: for<'a> fn (world: &'a mut World) -> Option>, fn_reflect_ptr: fn (world: &mut World) -> Option>, fn_refl_insert: fn (world: &mut World, this: Box), } impl ReflectedResource { /// Retrieves the reflected resource from the world. - pub fn reflect<'a>(&self, world: &'a World) -> Option> { + pub fn reflect<'a>(&self, world: &'a World) -> Option> { (self.fn_reflect)(world) } /// Retrieves a mutable reflected resource from the world. - pub fn reflect_mut<'a>(&self, world: &'a mut World) -> Option> { + pub fn reflect_mut<'a>(&self, world: &'a mut World) -> Option> { (self.fn_reflect_mut)(world) } @@ -41,11 +41,15 @@ impl FromType for ReflectedResource { type_id: TypeId::of::(), fn_reflect: |world: &World| { world.try_get_resource::() - .map(|r| r as Ref) + .map(|r| { + AtomicRef::map(r, |r| r as &dyn Reflect) + }) }, fn_reflect_mut: |world: &mut World| { world.try_get_resource_mut::() - .map(|r| r as RefMut) + .map(|r| { + AtomicRefMut::map(r, |r| r as &mut dyn Reflect) + }) }, fn_reflect_ptr: |world: &mut World| unsafe { world.try_get_resource_ptr::() From a2aac25249cae87ea46a68cdf4d97c6707b88297 Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Sun, 31 Mar 2024 00:29:12 -0400 Subject: [PATCH 05/11] ecs, reflect: implement Bundle for (), use `nobuild` instead of `compile_fail` for reflect rustdocs --- lyra-ecs/src/bundle.rs | 18 +++++++++++++++ .../lyra-reflect-derive/src/enum_derive.rs | 22 +++++++++---------- .../lyra-reflect-derive/src/struct_derive.rs | 14 ++++++------ shell.nix | 1 + 4 files changed, 37 insertions(+), 18 deletions(-) diff --git a/lyra-ecs/src/bundle.rs b/lyra-ecs/src/bundle.rs index 31d1952..7e8274a 100644 --- a/lyra-ecs/src/bundle.rs +++ b/lyra-ecs/src/bundle.rs @@ -17,6 +17,24 @@ pub trait Bundle { fn is_dynamic(&self) -> bool; } +impl Bundle for () { + fn type_ids(&self) -> Vec { + vec![DynTypeId::of::<()>()] + } + + fn info(&self) -> Vec { + vec![ComponentInfo::new::<()>()] + } + + fn take(self, mut f: impl FnMut(NonNull, DynTypeId, usize)) { + f(NonNull::from(&self).cast(), DynTypeId::of::<()>(), size_of::<()>()); + } + + fn is_dynamic(&self) -> bool { + false + } +} + impl Bundle for C { fn type_ids(&self) -> Vec { vec![DynTypeId::of::()] diff --git a/lyra-reflect/lyra-reflect-derive/src/enum_derive.rs b/lyra-reflect/lyra-reflect-derive/src/enum_derive.rs index 306de32..1f97862 100644 --- a/lyra-reflect/lyra-reflect-derive/src/enum_derive.rs +++ b/lyra-reflect/lyra-reflect-derive/src/enum_derive.rs @@ -32,7 +32,7 @@ impl From<&Variant> for VariantType { /// Generates the following different outputs: /// -/// ```compile_fail +/// ```nobuild /// // for struct variants /// TestEnum::Error { msg, code } /// @@ -98,7 +98,7 @@ fn gen_variant_if(enum_id: &proc_macro2::Ident, variant: &Variant, if_body: proc /// Generates the following: /// -/// ```compile_fail +/// ```nobuild /// /// generated one field here /// if name == "msg" { /// return Some(msg); @@ -129,7 +129,7 @@ fn gen_if_field_names(variant: &Variant) -> proc_macro2::TokenStream { /// Generates the following rust code: /// -/// ```compile_fail +/// ```nobuild /// match name { /// "msg" | "code" => true, /// _ => false, @@ -153,7 +153,7 @@ fn gen_match_names(variant: &Variant) -> proc_macro2::TokenStream { /// Generates the following: /// -/// ```compile_fail +/// ```nobuild /// /// generated one field here /// if idx == 0 { /// return Some(a); @@ -190,7 +190,7 @@ fn gen_if_field_indices(variant: &Variant) -> proc_macro2::TokenStream { /// Generates the following: /// -/// ```compile_fail +/// ```nobuild /// /// generated one field here /// if idx == 0 { /// return Some("a"); @@ -226,7 +226,7 @@ fn gen_if_field_indices_names(variant: &Variant) -> proc_macro2::TokenStream { } /// Generates the following: -/// ```compile_fail +/// ```nobuild /// /// when `by_index` is false: /// /// if let TestEnum::Error{ msg, code} = self { @@ -300,7 +300,7 @@ fn gen_enum_if_stmts(enum_id: &proc_macro2::Ident, data: &DataEnum, by_index: bo /// Generates the following rust code: /// -/// ```compile_fail +/// ```nobuild /// if let TestEnum::Error { msg, code } = self { /// return match name { /// // expands for continuing struct fields @@ -331,7 +331,7 @@ fn gen_enum_has_field(enum_id: &proc_macro2::Ident, data: &DataEnum) -> proc_mac /// Generates the following code: /// -/// ```compile_fail +/// ```nobuild /// match self { /// TestEnum::Start => 0, /// TestEnum::Middle(a, b) => 2, @@ -358,7 +358,7 @@ fn gen_enum_fields_len(enum_id: &proc_macro2::Ident, data: &DataEnum) -> proc_ma /// Generates the following code: /// -/// ```compile_fail +/// ```nobuild /// if let TestEnum::Error { msg, code } = self { /// if idx == 0 { /// return Some("msg"); @@ -389,7 +389,7 @@ fn gen_enum_field_name_at(enum_id: &proc_macro2::Ident, data: &DataEnum) -> proc } /// Generates the following code: -/// ```compile_fail +/// ```nobuild /// match self { /// TestEnum::Start => 0, /// TestEnum::Middle(a, b) => 1, @@ -427,7 +427,7 @@ fn gen_enum_variant_name(enum_id: &proc_macro2::Ident, data: &DataEnum, gen_inde /// Generates a match statement that returns the types of the variants of the enum. /// /// Example: -/// ```compile_fail +/// ```nobuild /// match self { /// TestEnum::Start => EnumType::Unit, /// TestEnum::Middle(a, b) => EnumType::Tuple, diff --git a/lyra-reflect/lyra-reflect-derive/src/struct_derive.rs b/lyra-reflect/lyra-reflect-derive/src/struct_derive.rs index 6bea7a8..b8dad88 100644 --- a/lyra-reflect/lyra-reflect-derive/src/struct_derive.rs +++ b/lyra-reflect/lyra-reflect-derive/src/struct_derive.rs @@ -28,7 +28,7 @@ impl StructType { /// contains a borrow (mutable borrow if `is_mut` is true) to the matching struct field. /// /// Example: -/// ```compile_fail +/// ```nobuild /// // when `is_mut` = false /// match name { /// "x" => Some(&self.x), @@ -85,7 +85,7 @@ fn gen_struct_field_match(data: &DataStruct, is_mut: bool) -> proc_macro2::Token /// with the provided `val`. /// /// Example: -/// ```compile_fail +/// ```nobuild /// match name { /// "x" => self.x = any_val.downcast_ref::() /// .expect(&format!("Cannot set struct's field of {} type to the provided type of {}", "f32", val.name())) @@ -140,7 +140,7 @@ fn gen_struct_set_field_match(data: &DataStruct) -> proc_macro2::TokenStream { /// the type of the field. /// /// Example: -/// ```compile_fail +/// ```nobuild /// match name { /// "x" => Some("f32"), /// "y" => Some("f32"), @@ -177,7 +177,7 @@ fn gen_struct_field_name_match(data: &DataStruct) -> proc_macro2::TokenStream { /// with the provided `val`. /// /// Example: -/// ```compile_fail +/// ```nobuild /// match name { /// 0 => self.x = any_val.downcast_ref::() /// .expect(&format!("Cannot set struct's field of {} type to the provided type of {}", "f32", val.name())) @@ -243,7 +243,7 @@ fn gen_struct_set_field_match_idx(data: &DataStruct) -> proc_macro2::TokenStream /// type of the field. /// /// Example: -/// ```compile_fail +/// ```nobuild /// match name { /// 0 => Some("f32"), /// 1 => Some("f32"), @@ -274,7 +274,7 @@ fn gen_struct_field_name_match_idx(data: &DataStruct) -> proc_macro2::TokenStrea /// to the matching struct field. /// /// Example: -/// ```compile_fail +/// ```nobuild /// // when `is_mut` = false /// match idx { /// 0 => Some(&self.x), @@ -335,7 +335,7 @@ fn gen_struct_field_match_idx(data: &DataStruct, is_mut: bool) -> proc_macro2::T /// and returns an Option that contains the name of the field. /// /// Example: -/// ```compile_fail +/// ```nobuild /// match idx { /// 0 => Some("x"), /// 1 => Some("y"), diff --git a/shell.nix b/shell.nix index 98db997..f0620b7 100755 --- a/shell.nix +++ b/shell.nix @@ -19,6 +19,7 @@ in extensions = [ "rust-src" "rust-analysis" + "miri-preview" ]; }) ]; From a17c035c05238c696c127d1d0976150721309891 Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Sun, 31 Mar 2024 00:32:31 -0400 Subject: [PATCH 06/11] resource: use a SceneGraph for loading gltf nodes, make resources Send + Sync --- Cargo.lock | 15 ++++--- lyra-ecs/src/world.rs | 4 +- lyra-resource/Cargo.toml | 1 + lyra-resource/src/gltf/loader.rs | 60 +++++++++++++++++++++------ lyra-resource/src/gltf/mod.rs | 13 ++---- lyra-resource/src/gltf/scene.rs | 19 ++++++++- lyra-resource/src/lib.rs | 2 +- lyra-resource/src/resource.rs | 10 ++--- lyra-resource/src/resource_manager.rs | 10 ++--- lyra-resource/src/world_ext.rs | 4 +- lyra-scene/src/lib.rs | 12 ++++++ 11 files changed, 106 insertions(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 642fd0c..a87856e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -330,6 +330,12 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "atomic_refcell" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41e67cd8309bbd06cd603a9e693a784ac2e5d1e955f11286e355089fcab3047c" + [[package]] name = "autocfg" version = "1.1.0" @@ -708,12 +714,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.18" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3a430a770ebd84726f584a90ee7f020d28db52c6d02138900f22341f866d39c" -dependencies = [ - "cfg-if", -] +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" [[package]] name = "crunchy" @@ -1749,6 +1752,7 @@ name = "lyra-ecs" version = "0.1.0" dependencies = [ "anyhow", + "atomic_refcell", "lyra-ecs-derive", "lyra-math", "paste", @@ -1844,6 +1848,7 @@ dependencies = [ "lyra-ecs", "lyra-math", "lyra-reflect", + "lyra-scene", "mime", "notify", "notify-debouncer-full", diff --git a/lyra-ecs/src/world.rs b/lyra-ecs/src/world.rs index eb4593f..9919a65 100644 --- a/lyra-ecs/src/world.rs +++ b/lyra-ecs/src/world.rs @@ -367,8 +367,8 @@ impl World { } // TODO: Ensure that all non-send resources are only accessible on the main thread. -/* unsafe impl Send for World {} -unsafe impl Sync for World {} */ +unsafe impl Send for World {} +unsafe impl Sync for World {} #[cfg(test)] mod tests { diff --git a/lyra-resource/Cargo.toml b/lyra-resource/Cargo.toml index 77833fc..9e5e149 100644 --- a/lyra-resource/Cargo.toml +++ b/lyra-resource/Cargo.toml @@ -9,6 +9,7 @@ edition = "2021" lyra-ecs = { path = "../lyra-ecs", features = [ "math" ] } lyra-reflect = { path = "../lyra-reflect" } lyra-math = { path = "../lyra-math" } +lyra-scene = { path = "../lyra-scene" } anyhow = "1.0.75" base64 = "0.21.4" crossbeam = { version = "0.8.4", features = [ "crossbeam-channel" ] } diff --git a/lyra-resource/src/gltf/loader.rs b/lyra-resource/src/gltf/loader.rs index 665d101..a131df9 100644 --- a/lyra-resource/src/gltf/loader.rs +++ b/lyra-resource/src/gltf/loader.rs @@ -3,9 +3,10 @@ use std::{ffi::OsStr, path::{Path, PathBuf}, sync::Arc}; use glam::{Quat, Vec3}; use instant::Instant; use lyra_math::Transform; +use lyra_scene::{SceneGraph, SceneNode}; use thiserror::Error; -use crate::{gltf::GltfScene, loader::{LoaderError, PinedBoxLoaderFuture, ResourceLoader}, util, ResHandle, ResourceData, ResourceManager, ResourceStorage}; +use crate::{loader::{LoaderError, PinedBoxLoaderFuture, ResourceLoader}, util, ResHandle, ResourceData, ResourceManager, ResourceStorage}; use super::{Gltf, GltfNode, Material, Mesh, MeshIndices, MeshVertexAttribute, VertexAttributeData}; use tracing::debug; @@ -64,7 +65,7 @@ impl ModelLoader { } } */ - fn process_node(ctx: &mut GltfLoadContext, materials: &Vec>, gnode: gltf::Node<'_>) -> GltfNode { + fn process_node(ctx: &mut GltfLoadContext, materials: &Vec>, scene: &mut SceneGraph, scene_parent: &SceneNode, gnode: gltf::Node<'_>) -> GltfNode { let mut node = GltfNode::default(); node.transform = { @@ -75,6 +76,8 @@ impl ModelLoader { }; node.name = gnode.name().map(str::to_string); + let scene_node = scene.add_node_under(scene_parent, node.transform, ()); + if let Some(mesh) = gnode.mesh() { let mut new_mesh = Mesh::default(); @@ -127,11 +130,12 @@ impl ModelLoader { let handle = ResHandle::new_ready(None, new_mesh); ctx.resource_manager.store_uuid(handle.clone()); - node.mesh = Some(handle); + node.mesh = Some(handle.clone()); + scene.insert(&scene_node, handle); } for child in gnode.children() { - let cmesh = ModelLoader::process_node(ctx, materials, child); + let cmesh = ModelLoader::process_node(ctx, materials, scene, &scene_node, child); node.children.push(cmesh); } @@ -210,7 +214,21 @@ impl ResourceLoader for ModelLoader { debug!("Loaded {} materials in {}s", materials.len(), mat_time.as_secs_f32()); for (_idx, scene) in gltf.scenes().enumerate() { - let start_inst = Instant::now(); + let mut graph = SceneGraph::new(); + let root_node = graph.root_node(); + + for node in scene.nodes() { + let n = ModelLoader::process_node(&mut context, &materials, &mut graph, &root_node, node); + + if let Some(mesh) = n.mesh { + gltf_out.meshes.push(mesh.clone()); + } + } + + let graph = ResHandle::new_ready(Some(path.as_str()), graph); + gltf_out.scenes.push(graph); + + /* let start_inst = Instant::now(); let nodes: Vec = scene.nodes() .map(|node| ModelLoader::process_node(&mut context, &materials, node)) .collect(); @@ -228,7 +246,7 @@ impl ResourceLoader for ModelLoader { nodes, }; let scene = ResHandle::new_ready(Some(path.as_str()), scene); - gltf_out.scenes.push(scene); + gltf_out.scenes.push(scene); */ } gltf_out.materials = materials; @@ -249,6 +267,8 @@ impl ResourceLoader for ModelLoader { #[cfg(test)] mod tests { + use lyra_ecs::{query::Entities, relation::ChildOf}; + use crate::tests::busy_wait_resource; use super::*; @@ -272,14 +292,30 @@ mod tests { let scene = &gltf.scenes[0] .data_ref().unwrap(); - assert_eq!(scene.nodes.len(), 1); - let mnode = &scene.nodes[0]; + let mut node = None; + scene.traverse_down(|no, _tran| { + node = Some(no.clone()); + }); - assert!(mnode.mesh.is_some()); - assert_eq!(mnode.transform, Transform::from_xyz(0.0, 0.0, 0.0)); - assert_eq!(mnode.children.len(), 0); + let world = scene.world(); + let node = node.unwrap(); + + let data = world.view_one::<(&ResHandle, &Transform)>(node.entity()).get(); + debug_assert!(data.is_some(), "The mesh was not loaded"); // transform will always be there + let data = data.unwrap(); - let mesh = mnode.mesh.as_ref().unwrap(); + // ensure there are no children of the node + assert_eq!( + world.view::() + .relates_to::(node.entity()) + .into_iter() + .count(), + 0 + ); + + assert_eq!(*data.1, Transform::from_xyz(0.0, 0.0, 0.0)); + + let mesh = data.0; let mesh = mesh.data_ref().unwrap(); assert!(mesh.position().unwrap().len() > 0); assert!(mesh.normals().unwrap().len() > 0); diff --git a/lyra-resource/src/gltf/mod.rs b/lyra-resource/src/gltf/mod.rs index 5eef200..0b14924 100644 --- a/lyra-resource/src/gltf/mod.rs +++ b/lyra-resource/src/gltf/mod.rs @@ -3,6 +3,7 @@ pub use loader::*; pub mod material; use lyra_math::Transform; +use lyra_scene::SceneGraph; use crate::ResourceData; pub use material::*; @@ -17,7 +18,7 @@ use crate::ResHandle; /// A loaded Gltf file #[derive(Clone, Default)] pub struct Gltf { - pub scenes: Vec>, + pub scenes: Vec>, pub materials: Vec>, pub meshes: Vec>, } @@ -55,14 +56,6 @@ impl ResourceData for Gltf { impl Gltf { /// Collects all Gltf meshes and gets their world Transform. pub fn collect_world_meshes(&self) -> Vec<(ResHandle, Transform)> { - let mut v = vec![]; - - for scene in self.scenes.iter() { - let mut tmp = scene.data_ref() - .unwrap().collect_world_meshes(); - v.append(&mut tmp); - } - - v + todo!() } } \ No newline at end of file diff --git a/lyra-resource/src/gltf/scene.rs b/lyra-resource/src/gltf/scene.rs index 28ee149..9c3a47e 100644 --- a/lyra-resource/src/gltf/scene.rs +++ b/lyra-resource/src/gltf/scene.rs @@ -1,4 +1,5 @@ use lyra_math::Transform; +use lyra_scene::SceneGraph; use crate::{optionally_add_to_dep, ResourceData, UntypedResHandle}; use super::Mesh; @@ -33,8 +34,8 @@ impl ResourceData for GltfNode { } } -/// A Scene in a Gltf file -#[derive(Clone)] +// A Scene in a Gltf file +/* #[derive(Clone)] pub struct GltfScene { pub nodes: Vec, } @@ -92,4 +93,18 @@ impl GltfScene { v } +} */ + +impl ResourceData for SceneGraph { + fn dependencies(&self) -> Vec { + todo!() + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { + self + } } \ No newline at end of file diff --git a/lyra-resource/src/lib.rs b/lyra-resource/src/lib.rs index 0d6cbcd..a68d47e 100644 --- a/lyra-resource/src/lib.rs +++ b/lyra-resource/src/lib.rs @@ -31,4 +31,4 @@ pub(crate) mod lyra_engine { pub(crate) mod reflect { pub use lyra_reflect::*; } -} +} \ No newline at end of file diff --git a/lyra-resource/src/resource.rs b/lyra-resource/src/resource.rs index 7604f38..7a95a6f 100644 --- a/lyra-resource/src/resource.rs +++ b/lyra-resource/src/resource.rs @@ -214,12 +214,12 @@ impl UntypedResHandle { /// However, the only times it will be blocking is if another thread is reloading the resource /// and has a write lock on the data. This means that most of the time, it is not blocking. #[derive(Component)] -pub struct ResHandle { +pub struct ResHandle { pub(crate) handle: UntypedResHandle, _marker: PhantomData, } -impl Clone for ResHandle { +impl Clone for ResHandle { fn clone(&self) -> Self { Self { handle: self.handle.clone(), @@ -228,7 +228,7 @@ impl Clone for ResHandle { } } -impl Deref for ResHandle { +impl Deref for ResHandle { type Target = UntypedResHandle; fn deref(&self) -> &Self::Target { @@ -236,7 +236,7 @@ impl Deref for ResHandle { } } -impl DerefMut for ResHandle { +impl DerefMut for ResHandle { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.handle } @@ -285,7 +285,7 @@ impl ResHandle { } } -impl ResourceStorage for ResHandle { +impl ResourceStorage for ResHandle { fn as_any(&self) -> &dyn Any { self } diff --git a/lyra-resource/src/resource_manager.rs b/lyra-resource/src/resource_manager.rs index 1781434..d8326de 100644 --- a/lyra-resource/src/resource_manager.rs +++ b/lyra-resource/src/resource_manager.rs @@ -173,7 +173,7 @@ impl ResourceManager { /// Request a resource without downcasting to a `ResHandle`. /// Whenever you're ready to downcast, you can do so like this: - /// ```compile_fail + /// ```nobuild /// let arc_any = res_arc.as_arc_any(); /// let res: Arc> = res.downcast::>().expect("Failure to downcast resource"); /// ``` @@ -221,7 +221,7 @@ impl ResourceManager { /// /// The resource cannot be requested with [`ResourceManager::request`], it can only be /// retrieved with [`ResourceManager::request_uuid`]. - pub fn store_uuid(&self, res: ResHandle) { + pub fn store_uuid(&self, res: ResHandle) { let mut state = self.state_mut(); state.resources.insert(res.uuid().to_string(), Arc::new(res)); } @@ -230,7 +230,7 @@ impl ResourceManager { /// /// Returns `None` if the resource was not found. The resource must of had been /// stored with [`ResourceManager::request`] to return `Some`. - pub fn request_uuid(&self, uuid: &Uuid) -> Option> { + pub fn request_uuid(&self, uuid: &Uuid) -> Option> { let state = self.state(); match state.resources.get(&uuid.to_string()) .or_else(|| state.uuid_resources.get(&uuid)) @@ -291,7 +291,7 @@ impl ResourceManager { /// Requests bytes from the manager. pub fn request_loaded_bytes(&self, ident: &str) -> Result>, RequestError> where - T: Send + Sync + Any + 'static + T: ResourceData { let state = self.state(); match state.resources.get(&ident.to_string()) { @@ -366,7 +366,7 @@ impl ResourceManager { /// the handle. pub fn reload(&self, resource: ResHandle) -> Result<(), RequestError> where - T: Send + Sync + Any + 'static + T: ResourceData { let state = self.state(); diff --git a/lyra-resource/src/world_ext.rs b/lyra-resource/src/world_ext.rs index c885ca4..a010cc4 100644 --- a/lyra-resource/src/world_ext.rs +++ b/lyra-resource/src/world_ext.rs @@ -30,7 +30,7 @@ pub trait WorldAssetExt { /// automatically triggered if the resource is being watched. fn reload_res(&mut self, resource: ResHandle) -> Result<(), RequestError> where - T: Send + Sync + Any + 'static; + T: ResourceData; } impl WorldAssetExt for World { @@ -67,7 +67,7 @@ impl WorldAssetExt for World { fn reload_res(&mut self, resource: ResHandle) -> Result<(), RequestError> where - T: Send + Sync + Any + 'static + T: ResourceData { let man = self.get_resource_or_default::(); man.reload(resource) diff --git a/lyra-scene/src/lib.rs b/lyra-scene/src/lib.rs index d5dc820..e2ce2c0 100644 --- a/lyra-scene/src/lib.rs +++ b/lyra-scene/src/lib.rs @@ -116,6 +116,13 @@ impl SceneGraph { world_add_child_node(&mut self.world, parent, local_transform, bundle) } + /// Insert a component bundle to a SceneNode. + /// + /// See [`lyra_ecs::World::insert`]. + pub fn insert(&mut self, node: &SceneNode, bundle: B) { + self.world.insert(node.entity(), bundle); + } + pub fn add_empty_node_under(&mut self, parent: &SceneNode, local_transform: Transform) -> SceneNode { let e = self.world.spawn((SceneNodeFlag, local_transform)); self.world.add_relation(e, ChildOf, parent.entity()); @@ -155,6 +162,11 @@ impl SceneGraph { pub fn root_node(&self) -> SceneNode { self.root_node.clone() } + + /// Retrieve a borrow of the world that backs the Scene + pub fn world(&self) -> &World { + &self.world + } } /// Add a node under a parent node. From a39d259bb4cacebd54ae04095894d2088086384e Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Sun, 31 Mar 2024 10:56:04 -0400 Subject: [PATCH 07/11] Switch nix-shell to use oxalica overlay to get miri working, fix memory leak in archetypes --- lyra-ecs/src/archetype.rs | 4 +++- rust-toolchain.toml | 4 ++-- shell.nix | 16 +++++++--------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lyra-ecs/src/archetype.rs b/lyra-ecs/src/archetype.rs index 5ea7b55..9b431f4 100644 --- a/lyra-ecs/src/archetype.rs +++ b/lyra-ecs/src/archetype.rs @@ -19,7 +19,9 @@ impl Drop for ComponentColumn { unsafe { // TODO: trigger drop on the components - let layout = self.info.layout(); + // SAFETY: The size of the data buffer is the capcity times the size of a component + let size = self.info.layout().size() * self.capacity; + let layout = Layout::from_size_align_unchecked(size, self.info.layout().align()); dealloc(data, layout); } } diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 78cfb42..50b32f7 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "nightly" -date = "2023-11-21" +channel = "nightly-2023-11-21" +#date = "2023-11-21" targets = [ "x86_64-unknown-linux-gnu" ] \ No newline at end of file diff --git a/shell.nix b/shell.nix index f0620b7..a4c7fef 100755 --- a/shell.nix +++ b/shell.nix @@ -1,6 +1,10 @@ let - moz_overlay = import (builtins.fetchTarball https://github.com/mozilla/nixpkgs-mozilla/archive/master.tar.gz); - nixpkgs = import { overlays = [ moz_overlay ]; }; + rust_overlay = import (builtins.fetchTarball https://github.com/oxalica/rust-overlay/archive/master.tar.gz); + nixpkgs = import { overlays = [ rust_overlay ]; }; + + rust = (nixpkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml).override { + extensions = [ "rust-analysis" "rust-src" "miri-preview" ]; + }; in with nixpkgs; stdenv.mkDerivation rec { @@ -15,13 +19,7 @@ in mold udev lua5_4_compat - ((nixpkgs.rustChannelOf { rustToolchain = ./rust-toolchain.toml; }).rust.override { - extensions = [ - "rust-src" - "rust-analysis" - "miri-preview" - ]; - }) + rust ]; buildInputs = [ udev alsa-lib libGL gcc From aa8d94851cc8a2b2b63210c4bf7f9e4d3679989e Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Sun, 31 Mar 2024 13:24:32 -0400 Subject: [PATCH 08/11] game: rewrite EventQueue due to new ecs requirement of Send + Sync for resources, use new SceneGraph in renderer --- Cargo.lock | 1 + examples/testbed/src/main.rs | 10 +- lyra-game/Cargo.toml | 1 + lyra-game/src/{castable_any.rs => as_any.rs} | 5 +- lyra-game/src/events.rs | 135 +++++++++++++++++-- lyra-game/src/input/action.rs | 4 +- lyra-game/src/input/system.rs | 4 +- lyra-game/src/lib.rs | 2 +- lyra-game/src/render/renderer.rs | 41 +++--- lyra-game/src/render/window.rs | 4 +- lyra-resource/src/gltf/loader.rs | 2 +- lyra-scene/src/lib.rs | 8 +- 12 files changed, 169 insertions(+), 48 deletions(-) rename lyra-game/src/{castable_any.rs => as_any.rs} (62%) diff --git a/Cargo.lock b/Cargo.lock index a87856e..1aeda0f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1795,6 +1795,7 @@ dependencies = [ "lyra-math", "lyra-reflect", "lyra-resource", + "lyra-scene", "quote", "syn 2.0.51", "thiserror", diff --git a/examples/testbed/src/main.rs b/examples/testbed/src/main.rs index 372ff32..88548b0 100644 --- a/examples/testbed/src/main.rs +++ b/examples/testbed/src/main.rs @@ -1,6 +1,6 @@ -use std::ptr::NonNull; +use std::{ptr::NonNull, thread, time::Duration}; -use lyra_engine::{assets::gltf::Gltf, change_tracker::Ct, ecs::{query::{QueryBorrow, Res, View, ViewState}, system::{BatchedSystem, Criteria, CriteriaSchedule, IntoSystem}, Component, World}, game::Game, input::{Action, ActionHandler, ActionKind, ActionMapping, ActionMappingId, ActionSource, InputActionPlugin, KeyCode, LayoutId, MouseAxis, MouseInput}, math::{self, Quat, Transform, Vec3}, render::{light::{directional::DirectionalLight, PointLight, SpotLight}, window::{CursorGrabMode, WindowOptions}}, scene::{CameraComponent, FreeFlyCamera, FreeFlyCameraPlugin, ACTLBL_LOOK_LEFT_RIGHT, ACTLBL_LOOK_ROLL, ACTLBL_LOOK_UP_DOWN, ACTLBL_MOVE_FORWARD_BACKWARD, ACTLBL_MOVE_LEFT_RIGHT, ACTLBL_MOVE_UP_DOWN}, DeltaTime}; +use lyra_engine::{assets::gltf::Gltf, change_tracker::Ct, ecs::{query::{Res, View}, system::{Criteria, CriteriaSchedule, IntoSystem}, Component, World}, game::Game, input::{Action, ActionHandler, ActionKind, ActionMapping, ActionMappingId, ActionSource, InputActionPlugin, KeyCode, LayoutId, MouseAxis, MouseInput}, math::{self, Quat, Transform, Vec3}, render::{light::{directional::DirectionalLight, PointLight, SpotLight}, window::{CursorGrabMode, WindowOptions}}, scene::{CameraComponent, FreeFlyCamera, FreeFlyCameraPlugin, ACTLBL_LOOK_LEFT_RIGHT, ACTLBL_LOOK_ROLL, ACTLBL_LOOK_UP_DOWN, ACTLBL_MOVE_FORWARD_BACKWARD, ACTLBL_MOVE_LEFT_RIGHT, ACTLBL_MOVE_UP_DOWN}, DeltaTime}; use lyra_engine::assets::ResourceManager; struct FixedTimestep { @@ -79,7 +79,8 @@ async fn main() { let separate_gltf = resman.request::("assets/pos-testing/child-node-cubes.glb").unwrap(); */ //drop(resman); - cube_gltf.wait_recurse_dependencies_load(); + //cube_gltf.wait_recurse_dependencies_load(); + thread::sleep(Duration::from_millis(500)); let cube_mesh = &cube_gltf.data_ref() .unwrap().meshes[0]; /* let crate_mesh = &crate_gltf.data_ref() @@ -91,7 +92,8 @@ async fn main() { let sponza_model = resman.request::("../assets/sponza/Sponza.gltf").unwrap(); drop(resman); - sponza_model.wait_recurse_dependencies_load(); + //sponza_model.wait_recurse_dependencies_load(); + thread::sleep(Duration::from_millis(10000)); let sponza_scene = &sponza_model.data_ref() .unwrap().scenes[0]; diff --git a/lyra-game/Cargo.toml b/lyra-game/Cargo.toml index 070fc85..f270e09 100644 --- a/lyra-game/Cargo.toml +++ b/lyra-game/Cargo.toml @@ -8,6 +8,7 @@ lyra-resource = { path = "../lyra-resource" } lyra-ecs = { path = "../lyra-ecs", features = [ "math" ] } lyra-reflect = { path = "../lyra-reflect", features = [ "math" ] } lyra-math = { path = "../lyra-math" } +lyra-scene = { path = "../lyra-scene" } winit = "0.28.1" tracing = "0.1.37" diff --git a/lyra-game/src/castable_any.rs b/lyra-game/src/as_any.rs similarity index 62% rename from lyra-game/src/castable_any.rs rename to lyra-game/src/as_any.rs index f1d4727..2043286 100644 --- a/lyra-game/src/castable_any.rs +++ b/lyra-game/src/as_any.rs @@ -1,12 +1,13 @@ use std::any::Any; -pub trait CastableAny: Send + Sync + 'static { +/// A trait that is implemented for types that are `Send + Sync + 'static`. +pub trait AsSyncAny: Send + Sync + 'static { fn as_any(&self) -> &dyn Any; fn as_any_mut(&mut self) -> &mut dyn Any; } /// Implements this trait for anything that fits the type bounds -impl CastableAny for T { +impl AsSyncAny for T { fn as_any(&self) -> &dyn Any { self } diff --git a/lyra-game/src/events.rs b/lyra-game/src/events.rs index 11f538a..94eea9e 100644 --- a/lyra-game/src/events.rs +++ b/lyra-game/src/events.rs @@ -1,15 +1,82 @@ -use std::{collections::{HashMap, VecDeque}, any::{TypeId, Any}, cell::RefCell}; +use std::{any::{Any, TypeId}, collections::{HashMap, VecDeque}, marker::PhantomData, mem::{self, MaybeUninit}, ops::Range}; -use crate::{castable_any::CastableAny, plugin::Plugin}; +use crate::plugin::Plugin; pub trait Event: Clone + Send + Sync + 'static {} impl Event for T {} pub type Events = VecDeque; +#[derive(Clone)] +struct TypeErasedBuffer { + type_id: TypeId, + item_size: usize, + data: Vec>, + fn_drop_item: fn(item: *mut ()), +} + +impl Drop for TypeErasedBuffer { + fn drop(&mut self) { + let range = self.data.as_mut_ptr_range(); + let mut current = range.start; + let end = range.end; + + while current < end { + // SAFETY: current pointer will either be the start of the buffer, or at the start of + // the next item + (self.fn_drop_item)(current.cast()); + + // SAFETY: the items are placed right after eachother + current = unsafe { current.add(self.item_size) }; + } + + // Safety: all of the events were just dropped + unsafe { self.data.set_len(0) }; + } +} + +impl TypeErasedBuffer { + pub fn new() -> Self { + Self { + type_id: TypeId::of::(), + item_size: mem::size_of::(), + data: Default::default(), + // dropped immediately after the read + fn_drop_item: |item| unsafe { item.cast::().drop_in_place() }, + } + } + + pub fn push(&mut self, value: T) { + debug_assert!(TypeId::of::() == self.type_id, "The type of the buffer does not match the type that was added to it"); + + // reserve enough bytes to store T + let old_len = self.data.len(); + self.data.reserve(mem::size_of::()); + + // Get a pointer to the end of the data. + // SAFETY: we just reserved enough memory to store the data. + let end_ptr = unsafe { self.data.as_mut_ptr().add(old_len) }; + + unsafe { + // write the command and its runner into the buffer + end_ptr.cast::() + // written unaligned to keep everything packed + .write_unaligned(value); + + // we wrote to the vec's buffer without using its api, so we need manually + // set the length of the vec. + self.data.set_len(old_len + mem::size_of::()); + } + } + + pub fn len(&self) -> usize { + self.data.len() / self.item_size + } +} + pub struct EventQueue { - events: HashMap>>, - event_write_queue: HashMap>> + events: HashMap, + event_write_queue: HashMap, } impl Default for EventQueue { @@ -32,7 +99,7 @@ impl EventQueue { E: Event { // the compiler wants me to explicit right here for some reason - let default = || RefCell::new(Box::>::default() as Box); + /* let default = || RefCell::new(Box::>::default() as Box); // Get, or create, a list of events of this type let type_id = event.type_id(); @@ -42,7 +109,13 @@ impl EventQueue { // downcast resource as an events storage let e: &mut Events = events.as_mut().as_any_mut().downcast_mut().unwrap(); - e.push_back(event); + e.push_back(event); */ + + let type_id = event.type_id(); + let events = self.event_write_queue.entry(type_id) + .or_insert_with(|| TypeErasedBuffer::new::()); + + events.push(event); } // Clear events, this should happen at the start of every tick since events are cloned @@ -60,13 +133,55 @@ impl EventQueue { } } - pub fn read_events(&self) -> Option> + pub fn read_events(&self) -> Option> where E: Event { - if let Some(event ) = self.events.get(&TypeId::of::()) { - let eref = event.borrow(); - Some(eref.as_ref().as_any().downcast_ref::>().unwrap().clone()) + self.events.get(&TypeId::of::()) + .map(|event| EventReader::new(event.clone())) + } +} + +pub struct EventReader { + buf: TypeErasedBuffer, + range: Option>>, + _marker: PhantomData, +} + +impl EventReader { + fn new(buffer: TypeErasedBuffer) -> Self { + Self { + buf: buffer, + range: None, + _marker: PhantomData, + } + } + + pub fn len(&self) -> usize { + self.buf.len() + } +} + +impl Iterator for EventReader { + type Item = E; + + fn next(&mut self) -> Option { + if self.range.is_none() { + self.range = Some(self.buf.data.as_mut_ptr_range()); + } + + let range = self.range.as_mut().unwrap(); + + if range.start < range.end { + // Retrieve the event in the buffer. + // SAFETY: current pointer will either be the start of the buffer, or at the start + // of the next event. + let event = unsafe { range.start.cast::().read_unaligned() }; + + // Advance the pointer to be after this event. + range.start = unsafe { range.start.add(mem::size_of::()) }; + + Some(event) } else { None } diff --git a/lyra-game/src/input/action.rs b/lyra-game/src/input/action.rs index a99d1dc..229dcf5 100644 --- a/lyra-game/src/input/action.rs +++ b/lyra-game/src/input/action.rs @@ -521,10 +521,10 @@ fn actions_system(world: &mut World) -> anyhow::Result<()> { } } - let motion_avg = if let Some(mut mouse_events) = mouse_events { + let motion_avg = if let Some(mouse_events) = mouse_events { let count = mouse_events.len(); let mut sum = Vec2::ZERO; - while let Some(mm) = mouse_events.pop_front() { + for mm in mouse_events { sum += mm.delta; } Some(sum / count as f32) diff --git a/lyra-game/src/input/system.rs b/lyra-game/src/input/system.rs index b41a939..1ff55cc 100755 --- a/lyra-game/src/input/system.rs +++ b/lyra-game/src/input/system.rs @@ -110,8 +110,8 @@ impl crate::ecs::system::System for InputSystem { return Ok(()); } - let mut events = queue.unwrap(); - while let Some(event) = events.pop_front() { + let events = queue.unwrap(); + for event in events { self.process_event(world, &event); } diff --git a/lyra-game/src/lib.rs b/lyra-game/src/lib.rs index 85b2922..47de0fb 100644 --- a/lyra-game/src/lib.rs +++ b/lyra-game/src/lib.rs @@ -8,7 +8,7 @@ pub mod game; pub mod render; pub mod resources; pub mod input; -pub mod castable_any; +pub mod as_any; pub mod plugin; pub mod change_tracker; diff --git a/lyra-game/src/render/renderer.rs b/lyra-game/src/render/renderer.rs index c960081..295243c 100755 --- a/lyra-game/src/render/renderer.rs +++ b/lyra-game/src/render/renderer.rs @@ -10,7 +10,7 @@ use lyra_ecs::query::filter::{Has, Or}; use lyra_ecs::{Entity, Tick}; use lyra_ecs::query::{Entities, TickOf}; use lyra_ecs::World; -use lyra_resource::gltf::GltfScene; +use lyra_scene::SceneGraph; use tracing::{debug, warn}; use uuid::Uuid; use wgpu::{BindGroupLayout, Limits}; @@ -36,7 +36,7 @@ use super::{render_pipeline::FullRenderPipeline, render_buffer::BufferStorage, r use lyra_resource::{gltf::Mesh, ResHandle}; type MeshHandle = ResHandle; -type SceneHandle = ResHandle; +type SceneHandle = ResHandle; pub trait Renderer { fn prepare(&mut self, main_world: &mut World); @@ -465,27 +465,28 @@ impl Renderer for BasicRenderer { if let Some(scene) = scene_han.data_ref() { let interpo_pos = self.interpolate_transforms(now_inst, last_epoch, entity, &transform, transform_epoch); - let meshes = scene.collect_world_meshes(); - for (mesh_han, mesh_pos) in meshes.into_iter() { - if let Some(mesh) = mesh_han.data_ref() { - let mesh_interpo = interpo_pos + mesh_pos; - - // if process mesh did not just create a new mesh, and the epoch - // shows that the scene has changed, verify that the mesh buffers - // dont need to be resent to the gpu. - if !self.process_mesh(entity, mesh_interpo, &*mesh, mesh_han.uuid()) - && scene_epoch == last_epoch { - self.check_mesh_buffers(entity, &mesh_han); + scene.traverse_down(|sw: &World, mesh_han, pos| { + if let Some(mesh_han) = sw.view_one::<&MeshHandle>(mesh_han.entity()).get() { + if let Some(mesh) = mesh_han.data_ref() { + let mesh_interpo = interpo_pos + pos; + + // if process mesh did not just create a new mesh, and the epoch + // shows that the scene has changed, verify that the mesh buffers + // dont need to be resent to the gpu. + if !self.process_mesh(entity, mesh_interpo, &*mesh, mesh_han.uuid()) + && scene_epoch == last_epoch { + self.check_mesh_buffers(entity, &mesh_han); + } + + let material = mesh.material.as_ref().unwrap() + .data_ref().unwrap(); + let shader = material.shader_uuid.unwrap_or(0); + let job = RenderJob::new(entity, shader, mesh_han.uuid()); + self.render_jobs.push_back(job); } - - let material = mesh.material.as_ref().unwrap() - .data_ref().unwrap(); - let shader = material.shader_uuid.unwrap_or(0); - let job = RenderJob::new(entity, shader, mesh_han.uuid()); - self.render_jobs.push_back(job); } - } + }); } } } diff --git a/lyra-game/src/render/window.rs b/lyra-game/src/render/window.rs index c63c632..d7c9696 100644 --- a/lyra-game/src/render/window.rs +++ b/lyra-game/src/render/window.rs @@ -341,8 +341,8 @@ fn window_updater_system(world: &mut World) -> anyhow::Result<()> { let mut opts = world.get_resource_mut::>(); if let Some(event_queue) = world.try_get_resource_mut::() { - if let Some(mut events) = event_queue.read_events::() { - while let Some(ev) = events.pop_front() { + if let Some(events) = event_queue.read_events::() { + for ev in events { match ev { InputEvent::CursorEntered { .. } => { opts.cursor_inside_window = true; diff --git a/lyra-resource/src/gltf/loader.rs b/lyra-resource/src/gltf/loader.rs index a131df9..4fb05ba 100644 --- a/lyra-resource/src/gltf/loader.rs +++ b/lyra-resource/src/gltf/loader.rs @@ -293,7 +293,7 @@ mod tests { .data_ref().unwrap(); let mut node = None; - scene.traverse_down(|no, _tran| { + scene.traverse_down(|_, no, _tran| { node = Some(no.clone()); }); diff --git a/lyra-scene/src/lib.rs b/lyra-scene/src/lib.rs index e2ce2c0..56d1976 100644 --- a/lyra-scene/src/lib.rs +++ b/lyra-scene/src/lib.rs @@ -135,7 +135,7 @@ impl SceneGraph { /// The traversal does not include the root scene node. pub fn traverse_down(&self, mut callback: F) where - F: FnMut(&SceneNode, Transform), + F: FnMut(&World, &SceneNode, Transform), { self.traverse_down_from(self.root_node.clone(), &mut callback); } @@ -144,7 +144,7 @@ impl SceneGraph { /// SceneNode and its world transform. fn traverse_down_from(&self, start: SceneNode, callback: &mut F) where - F: FnMut(&SceneNode, Transform), + F: FnMut(&World, &SceneNode, Transform), { let v = self.world .view::<(Entities, &Transform)>() @@ -153,7 +153,7 @@ impl SceneGraph { for ((e, _), _rel) in v.iter() { let node = SceneNode::new(Some(start.entity()), e); let world_pos = node.world_transform(self); - callback(&node, world_pos); + callback(&self.world, &node, world_pos); self.traverse_down_from(node, callback); } @@ -222,7 +222,7 @@ pub mod tests { assert!(b.parent(&scene).unwrap() == a); let mut idx = 0; - scene.traverse_down(|_e, pos| { + scene.traverse_down(|_, _, pos| { if idx == 0 { assert_eq!(pos, Transform::from_translation(v2s[idx])); } else if idx == 1 { From a3118f32e2ad62712c034842761d76a724fb99f4 Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Sun, 31 Mar 2024 13:37:25 -0400 Subject: [PATCH 09/11] resource: implement retrieving loaded SceneGraph dependencies --- examples/testbed/src/main.rs | 6 ++---- lyra-resource/src/gltf/loader.rs | 2 +- lyra-resource/src/gltf/mod.rs | 8 -------- lyra-resource/src/gltf/scene.rs | 5 ++++- lyra-resource/src/resource.rs | 2 +- lyra-resource/src/world_ext.rs | 2 -- 6 files changed, 8 insertions(+), 17 deletions(-) diff --git a/examples/testbed/src/main.rs b/examples/testbed/src/main.rs index 88548b0..a43d115 100644 --- a/examples/testbed/src/main.rs +++ b/examples/testbed/src/main.rs @@ -79,8 +79,7 @@ async fn main() { let separate_gltf = resman.request::("assets/pos-testing/child-node-cubes.glb").unwrap(); */ //drop(resman); - //cube_gltf.wait_recurse_dependencies_load(); - thread::sleep(Duration::from_millis(500)); + cube_gltf.wait_recurse_dependencies_load(); let cube_mesh = &cube_gltf.data_ref() .unwrap().meshes[0]; /* let crate_mesh = &crate_gltf.data_ref() @@ -92,8 +91,7 @@ async fn main() { let sponza_model = resman.request::("../assets/sponza/Sponza.gltf").unwrap(); drop(resman); - //sponza_model.wait_recurse_dependencies_load(); - thread::sleep(Duration::from_millis(10000)); + sponza_model.wait_recurse_dependencies_load(); let sponza_scene = &sponza_model.data_ref() .unwrap().scenes[0]; diff --git a/lyra-resource/src/gltf/loader.rs b/lyra-resource/src/gltf/loader.rs index 4fb05ba..37dd5fd 100644 --- a/lyra-resource/src/gltf/loader.rs +++ b/lyra-resource/src/gltf/loader.rs @@ -131,7 +131,7 @@ impl ModelLoader { let handle = ResHandle::new_ready(None, new_mesh); ctx.resource_manager.store_uuid(handle.clone()); node.mesh = Some(handle.clone()); - scene.insert(&scene_node, handle); + scene.insert(&scene_node, (handle.clone(), handle.untyped_clone())); } for child in gnode.children() { diff --git a/lyra-resource/src/gltf/mod.rs b/lyra-resource/src/gltf/mod.rs index 0b14924..84910c4 100644 --- a/lyra-resource/src/gltf/mod.rs +++ b/lyra-resource/src/gltf/mod.rs @@ -2,7 +2,6 @@ pub mod loader; pub use loader::*; pub mod material; -use lyra_math::Transform; use lyra_scene::SceneGraph; use crate::ResourceData; pub use material::*; @@ -51,11 +50,4 @@ impl ResourceData for Gltf { } -} - -impl Gltf { - /// Collects all Gltf meshes and gets their world Transform. - pub fn collect_world_meshes(&self) -> Vec<(ResHandle, Transform)> { - todo!() - } } \ No newline at end of file diff --git a/lyra-resource/src/gltf/scene.rs b/lyra-resource/src/gltf/scene.rs index 9c3a47e..8bf83c8 100644 --- a/lyra-resource/src/gltf/scene.rs +++ b/lyra-resource/src/gltf/scene.rs @@ -97,7 +97,10 @@ impl GltfScene { impl ResourceData for SceneGraph { fn dependencies(&self) -> Vec { - todo!() + self.world().view::<&crate::UntypedResHandle>() + .iter() + .map(|han| han.clone()) + .collect() } fn as_any(&self) -> &dyn std::any::Any { diff --git a/lyra-resource/src/resource.rs b/lyra-resource/src/resource.rs index 7a95a6f..f20a8ac 100644 --- a/lyra-resource/src/resource.rs +++ b/lyra-resource/src/resource.rs @@ -96,7 +96,7 @@ pub struct UntypedResource { pub(crate) condvar: Arc<(Mutex, Condvar)>, } -#[derive(Clone)] +#[derive(Clone, Component)] pub struct UntypedResHandle{ pub(crate) res: Arc>, #[allow(dead_code)] diff --git a/lyra-resource/src/world_ext.rs b/lyra-resource/src/world_ext.rs index a010cc4..366bf48 100644 --- a/lyra-resource/src/world_ext.rs +++ b/lyra-resource/src/world_ext.rs @@ -1,5 +1,3 @@ -use std::any::Any; - use crossbeam::channel::Receiver; use lyra_ecs::World; use notify_debouncer_full::DebouncedEvent; From dd61e8e66cb1bbffc2bd64261e747fbc567babc6 Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Sun, 31 Mar 2024 23:02:18 -0400 Subject: [PATCH 10/11] render: hack to get rendering shared 3d modules working --- lyra-game/src/render/render_job.rs | 4 +- lyra-game/src/render/renderer.rs | 21 ++- .../src/render/transform_buffer_storage.rs | 153 ++++++------------ 3 files changed, 66 insertions(+), 112 deletions(-) diff --git a/lyra-game/src/render/render_job.rs b/lyra-game/src/render/render_job.rs index 90809b8..8a6d72d 100755 --- a/lyra-game/src/render/render_job.rs +++ b/lyra-game/src/render/render_job.rs @@ -4,14 +4,16 @@ pub struct RenderJob { pub entity: Entity, pub shader_id: u64, pub mesh_uuid: uuid::Uuid, + pub transform_id: u32, } impl RenderJob { - pub fn new(entity: Entity, shader_id: u64, mesh_buffer_id: uuid::Uuid) -> Self { + pub fn new(entity: Entity, shader_id: u64, mesh_buffer_id: uuid::Uuid, transform_id: u32) -> Self { Self { entity, shader_id, mesh_uuid: mesh_buffer_id, + transform_id } } } \ No newline at end of file diff --git a/lyra-game/src/render/renderer.rs b/lyra-game/src/render/renderer.rs index 295243c..5015e9c 100755 --- a/lyra-game/src/render/renderer.rs +++ b/lyra-game/src/render/renderer.rs @@ -374,12 +374,13 @@ impl BasicRenderer { /// Processes the mesh for the renderer, storing and creating buffers as needed. Returns true if a new mesh was processed. fn process_mesh(&mut self, entity: Entity, transform: Transform, mesh: &Mesh, mesh_uuid: Uuid) -> bool { - if self.transform_buffers.should_expand() { + let _ = transform; + /* if self.transform_buffers.should_expand() { self.transform_buffers.expand_buffers(&self.device); } self.transform_buffers.update_or_insert(&self.queue, &self.render_limits, - mesh_uuid, || ( transform.calculate_mat4(), glam::Mat3::from_quat(transform.rotation) )); + entity, || ( transform.calculate_mat4(), glam::Mat3::from_quat(transform.rotation) )); */ #[allow(clippy::map_entry)] if !self.mesh_buffers.contains_key(&mesh_uuid) { @@ -451,10 +452,12 @@ impl Renderer for BasicRenderer { self.check_mesh_buffers(entity, &mesh_han); } + let transform_id = self.transform_buffers.push_transform(&self.queue, &self.render_limits, interop_pos.calculate_mat4(), glam::Mat3::from_quat(interop_pos.rotation)); + let material = mesh.material.as_ref().unwrap() .data_ref().unwrap(); let shader = material.shader_uuid.unwrap_or(0); - let job = RenderJob::new(entity, shader, mesh_han.uuid()); + let job = RenderJob::new(entity, shader, mesh_han.uuid(), transform_id); self.render_jobs.push_back(job); } } @@ -478,11 +481,13 @@ impl Renderer for BasicRenderer { && scene_epoch == last_epoch { self.check_mesh_buffers(entity, &mesh_han); } + + let transform_id = self.transform_buffers.push_transform(&self.queue, &self.render_limits, mesh_interpo.calculate_mat4(), glam::Mat3::from_quat(mesh_interpo.rotation)); let material = mesh.material.as_ref().unwrap() .data_ref().unwrap(); let shader = material.shader_uuid.unwrap_or(0); - let job = RenderJob::new(entity, shader, mesh_han.uuid()); + let job = RenderJob::new(entity, shader, mesh_han.uuid(), transform_id); self.render_jobs.push_back(job); } } @@ -576,9 +581,11 @@ impl Renderer for BasicRenderer { } // Get the bindgroup for job's transform and bind to it using an offset. - let transform_indices = *self.transform_buffers.transform_indices(job.mesh_uuid).unwrap(); - let bindgroup = self.transform_buffers.bind_group(transform_indices).unwrap(); - let offset = TransformBuffers::index_offset(&self.render_limits, transform_indices) as u32; + /* let transform_indices = *self.transform_buffers.transform_indices(job.entity).unwrap(); + let bindgroup = self.transform_buffers.bind_group(transform_indices).unwrap(); */ + + let bindgroup = self.transform_buffers.bind_group(job.transform_id); + let offset = self.transform_buffers.buffer_offset(job.transform_id);//TransformBuffers::index_offset(&self.render_limits, transform_indices) as u32; render_pass.set_bind_group(1, bindgroup, &[ offset, offset, ]); render_pass.set_bind_group(2, &self.camera_buffer.bindgroup(), &[]); diff --git a/lyra-game/src/render/transform_buffer_storage.rs b/lyra-game/src/render/transform_buffer_storage.rs index 4223df5..0db9851 100644 --- a/lyra-game/src/render/transform_buffer_storage.rs +++ b/lyra-game/src/render/transform_buffer_storage.rs @@ -1,6 +1,5 @@ -use std::{collections::{VecDeque, HashMap}, num::NonZeroU64}; +use std::num::NonZeroU64; -use uuid::Uuid; use wgpu::Limits; use std::mem; @@ -14,9 +13,9 @@ pub(crate) struct TransformBufferIndices { /// A struct representing a single transform buffer. There can be multiple of these pub(crate) struct TransformBufferEntry { pub len: usize, - pub transform_buf: wgpu::Buffer, - pub normal_mat_buf: wgpu::Buffer, pub bindgroup: wgpu::BindGroup, + pub transform_buffer: wgpu::Buffer, + pub normal_buffer: wgpu::Buffer, } /// A helper struct for managing the Transform buffers for meshes. @@ -31,14 +30,8 @@ pub(crate) struct TransformBufferEntry { /// to insert, update, and retrieve the transforms. pub(crate) struct TransformBuffers { pub bindgroup_layout: wgpu::BindGroupLayout, - pub just_updated: HashMap, - pub not_updated: HashMap, - pub dead_indices: VecDeque, - pub next_indices: TransformBufferIndices, - /// (transform count, buffer, bindgroup) - pub buffer_bindgroups: Vec, - /// The max amount of transforms in a buffer - pub max_transform_count: usize, + pub entries: Vec, + limits: wgpu::Limits, } impl TransformBuffers { @@ -72,13 +65,9 @@ impl TransformBuffers { }); let mut s = Self { - max_transform_count: limits.max_uniform_buffer_binding_size as usize / (mem::size_of::() * 2), - buffer_bindgroups: Vec::new(), bindgroup_layout, - just_updated: HashMap::new(), - not_updated: HashMap::new(), - dead_indices: VecDeque::new(), - next_indices: TransformBufferIndices::default(), + entries: vec![], + limits, }; // create the first uniform buffer @@ -87,95 +76,29 @@ impl TransformBuffers { s } - /// Update an transform in the buffer. - /// - /// # Panics - /// Panics if the entity isn't stored, you can check if it is before with [`TransformBuffers:contains`]. - pub fn update_transform(&mut self, queue: &wgpu::Queue, limits: &Limits, uuid: Uuid, transform: glam::Mat4, normal_matrix: glam::Mat3) -> TransformBufferIndices { - let indices = self.not_updated.remove(&uuid) - .or_else(|| self.just_updated.remove(&uuid)) - .expect("Use 'insert_entity' for new entities"); - self.just_updated.insert(uuid, indices); + pub fn push_transform(&mut self, queue: &wgpu::Queue, limits: &Limits, transform: glam::Mat4, normal_matrix: glam::Mat3) -> u32 { + let entry = self.entries.iter_mut().next().unwrap(); // TODO: use other buffers than just the first let normal_matrix = glam::Mat4::from_mat3(normal_matrix); - let buffer = self.buffer_bindgroups.get(indices.buffer_index).unwrap(); - let offset = Self::index_offset(limits, indices); - queue.write_buffer(&buffer.transform_buf, offset, bytemuck::bytes_of(&transform)); - queue.write_buffer(&buffer.normal_mat_buf, offset, bytemuck::bytes_of(&normal_matrix)); - indices - } + // write the transform and normal to the end of the transform + //let offset = (mem::size_of::() * entry.len) as u64; + let offset = Self::get_buffer_offset(limits, entry.len as u32) as _; + queue.write_buffer(&entry.transform_buffer, offset, bytemuck::bytes_of(&transform)); + queue.write_buffer(&entry.normal_buffer, offset, bytemuck::bytes_of(&normal_matrix)); - /// Insert a new transform into the buffer, returns where in the buffer it was stored. - pub fn insert_transform(&mut self, queue: &wgpu::Queue, limits: &Limits, uuid: Uuid, transform: glam::Mat4, normal_matrix: glam::Mat3) -> TransformBufferIndices { - let indices = match self.dead_indices.pop_front() { - Some(indices) => indices, - None => { - let indices = &mut self.next_indices; - let this_idx = *indices; - let entry = self.buffer_bindgroups.get_mut(indices.buffer_index).unwrap(); - - if entry.len >= self.max_transform_count { - panic!("Transform buffer is filled and 'next_indices' was not incremented! Was a new buffer created?"); - } - - entry.len += 1; - indices.transform_index += 1; - this_idx - } - }; - - self.just_updated.insert(uuid, indices); - self.update_transform(queue, limits, uuid, transform, normal_matrix) - } - - /// Update or insert a transform - pub fn update_or_insert(&mut self, queue: &wgpu::Queue, limits: &Limits, uuid: Uuid, transform_fn: TFn) -> TransformBufferIndices - where TFn: Fn() -> (glam::Mat4, glam::Mat3) - { - let (tran, norm) = transform_fn(); - if self.contains(uuid) { - self.update_transform(queue, limits, uuid, tran, norm) - } else { - self.insert_transform(queue, limits, uuid, tran, norm) - } - } - - /// Returns true if the transform related to the `uuid` is stored (does not mean its up-to-date). - pub fn contains(&self, uuid: Uuid) -> bool { - self.not_updated.contains_key(&uuid) || self.just_updated.contains_key(&uuid) + let index = entry.len; + entry.len += 1; + index as _ } /// Collect the dead transforms and prepare self to check next time. pub fn tick(&mut self) { - // take the dead entities, these were ones that were not updated this tick - let dead: VecDeque = self.not_updated.values().copied().collect(); - self.dead_indices = dead; - - self.not_updated = self.just_updated.clone(); - self.just_updated.clear(); - } - - /// Returns the offset for the transform index in the buffer - pub fn index_offset(limits: &Limits, indices: TransformBufferIndices) -> u64 { - indices.transform_index as u64 * limits.min_uniform_buffer_offset_alignment as u64 - } - - /// Returns whether or not the transform buffers should be expanded - pub fn should_expand(&self) -> bool { - if let Some(entry) = self.buffer_bindgroups.last() { - entry.len >= self.max_transform_count - } else { - true + for entry in self.entries.iter_mut() { + entry.len = 0; } } - /// Returns the bind group for the index - pub fn bind_group(&self, index: TransformBufferIndices) -> Option<&wgpu::BindGroup> { - self.buffer_bindgroups.get(index.buffer_index) - .map(|entry| &entry.bindgroup) - } - /// Expand the Transform buffers by adding another uniform buffer binding. /// /// This object has a chain of uniform buffers, when the buffers are expanded, a new @@ -186,7 +109,7 @@ impl TransformBuffers { let transform_buffer = device.create_buffer( &wgpu::BufferDescriptor { - label: Some(&format!("B_Transform_{}", self.buffer_bindgroups.len())), + label: Some(&format!("B_Transform_{}", self.entries.len())), usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, size: max_buffer_sizes, mapped_at_creation: false, @@ -195,7 +118,7 @@ impl TransformBuffers { let normal_mat_buffer = device.create_buffer( &wgpu::BufferDescriptor { - label: Some(&format!("B_NormalMatrix_{}", self.buffer_bindgroups.len())), + label: Some(&format!("B_NormalMatrix_{}", self.entries.len())), usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, size: max_buffer_sizes, mapped_at_creation: false, @@ -236,15 +159,37 @@ impl TransformBuffers { let entry = TransformBufferEntry { bindgroup: transform_bind_group, - transform_buf: transform_buffer, - normal_mat_buf: normal_mat_buffer, + transform_buffer, + normal_buffer: normal_mat_buffer, len: 0, }; - self.buffer_bindgroups.push(entry); + self.entries.push(entry); } - /// Returns the indices of the Transform - pub fn transform_indices(&self, uuid: Uuid) -> Option<&TransformBufferIndices> { - self.just_updated.get(&uuid).or_else(|| self.not_updated.get(&uuid)) + pub fn bind_group(&self, transform_id: u32) -> &wgpu::BindGroup { + let _ = transform_id; + + let entry = self.entries.iter().next().unwrap(); // TODO: use other buffers than just the first + &entry.bindgroup } + + /// Get the buffer offset for a transform using wgpu limits. + /// + /// If its possible to borrow immutably, use [`TransformBuffers::buffer_offset`]. + fn get_buffer_offset(limits: &wgpu::Limits, transform_id: u32) -> u32 { + //let entry = self.entries.iter().next().unwrap(); // TODO: use other buffers than just the first + + transform_id * limits.min_uniform_buffer_offset_alignment as u32//mem::size_of::() as u32 + } + + pub fn buffer_offset(&self, transform_id: u32) -> u32 { + Self::get_buffer_offset(&self.limits, transform_id) + } +} + +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +struct TransformNormalMatPair { + transform: glam::Mat4, + normal_mat: glam::Mat4, } \ No newline at end of file From 0a9e5ebcdb1bb62c98e56cbe06a7a4fed8977213 Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Mon, 1 Apr 2024 10:50:17 -0400 Subject: [PATCH 11/11] render: improve fix for rendering shared 3d modules --- lyra-game/src/render/render_job.rs | 6 +- lyra-game/src/render/renderer.rs | 18 +- .../src/render/transform_buffer_storage.rs | 246 +++++++++++++++--- 3 files changed, 227 insertions(+), 43 deletions(-) diff --git a/lyra-game/src/render/render_job.rs b/lyra-game/src/render/render_job.rs index 8a6d72d..e6b0b56 100755 --- a/lyra-game/src/render/render_job.rs +++ b/lyra-game/src/render/render_job.rs @@ -1,14 +1,16 @@ use lyra_ecs::Entity; +use super::transform_buffer_storage::TransformIndex; + pub struct RenderJob { pub entity: Entity, pub shader_id: u64, pub mesh_uuid: uuid::Uuid, - pub transform_id: u32, + pub transform_id: TransformIndex, } impl RenderJob { - pub fn new(entity: Entity, shader_id: u64, mesh_buffer_id: uuid::Uuid, transform_id: u32) -> Self { + pub fn new(entity: Entity, shader_id: u64, mesh_buffer_id: uuid::Uuid, transform_id: TransformIndex) -> Self { Self { entity, shader_id, diff --git a/lyra-game/src/render/renderer.rs b/lyra-game/src/render/renderer.rs index 5015e9c..9369873 100755 --- a/lyra-game/src/render/renderer.rs +++ b/lyra-game/src/render/renderer.rs @@ -29,7 +29,7 @@ use super::light_cull_compute::LightCullCompute; use super::material::Material; use super::render_buffer::BufferWrapper; use super::texture::RenderTexture; -use super::transform_buffer_storage::TransformBuffers; +use super::transform_buffer_storage::{TransformBuffers, TransformGroup}; use super::vertex::Vertex; use super::{render_pipeline::FullRenderPipeline, render_buffer::BufferStorage, render_job::RenderJob}; @@ -452,8 +452,10 @@ impl Renderer for BasicRenderer { self.check_mesh_buffers(entity, &mesh_han); } - let transform_id = self.transform_buffers.push_transform(&self.queue, &self.render_limits, interop_pos.calculate_mat4(), glam::Mat3::from_quat(interop_pos.rotation)); - + let group = TransformGroup::EntityRes(entity, mesh_han.uuid()); + let transform_id = self.transform_buffers.update_or_push(&self.queue, &self.render_limits, + group, || ( interop_pos.calculate_mat4(), glam::Mat3::from_quat(interop_pos.rotation) )); + let material = mesh.material.as_ref().unwrap() .data_ref().unwrap(); let shader = material.shader_uuid.unwrap_or(0); @@ -482,7 +484,10 @@ impl Renderer for BasicRenderer { self.check_mesh_buffers(entity, &mesh_han); } - let transform_id = self.transform_buffers.push_transform(&self.queue, &self.render_limits, mesh_interpo.calculate_mat4(), glam::Mat3::from_quat(mesh_interpo.rotation)); + let scene_mesh_group = TransformGroup::Res(scene_han.uuid(), mesh_han.uuid()); + let group = TransformGroup::OwnedGroup(entity, scene_mesh_group.into()); + let transform_id = self.transform_buffers.update_or_push(&self.queue, &self.render_limits, + group, || ( mesh_interpo.calculate_mat4(), glam::Mat3::from_quat(mesh_interpo.rotation) )); let material = mesh.material.as_ref().unwrap() .data_ref().unwrap(); @@ -581,11 +586,8 @@ impl Renderer for BasicRenderer { } // Get the bindgroup for job's transform and bind to it using an offset. - /* let transform_indices = *self.transform_buffers.transform_indices(job.entity).unwrap(); - let bindgroup = self.transform_buffers.bind_group(transform_indices).unwrap(); */ - let bindgroup = self.transform_buffers.bind_group(job.transform_id); - let offset = self.transform_buffers.buffer_offset(job.transform_id);//TransformBuffers::index_offset(&self.render_limits, transform_indices) as u32; + let offset = self.transform_buffers.buffer_offset(job.transform_id); render_pass.set_bind_group(1, bindgroup, &[ offset, offset, ]); render_pass.set_bind_group(2, &self.camera_buffer.bindgroup(), &[]); diff --git a/lyra-game/src/render/transform_buffer_storage.rs b/lyra-game/src/render/transform_buffer_storage.rs index 0db9851..9044715 100644 --- a/lyra-game/src/render/transform_buffer_storage.rs +++ b/lyra-game/src/render/transform_buffer_storage.rs @@ -1,37 +1,167 @@ -use std::num::NonZeroU64; +use std::{collections::{HashMap, VecDeque}, hash::{BuildHasher, DefaultHasher, Hash, Hasher, RandomState}, num::NonZeroU64}; +use lyra_ecs::Entity; +use uuid::Uuid; use wgpu::Limits; use std::mem; +/// A group id created from a [`TransformGroup`]. +/// +/// This is mainly created so that [`TransformGroup::OwnedGroup`] can use another group inside of it. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct TransformGroupId(u64); + +impl From for TransformGroupId { + fn from(value: TransformGroup) -> Self { + let mut hasher = DefaultHasher::new(); + value.hash(&mut hasher); + let hash = hasher.finish(); + + TransformGroupId(hash) + } +} + +/// Used as a key into the [`TransformBuffers`]. +/// +/// This enum is used as a key to identify a transform for a RenderJob. The renderer uses this +/// to differentiate a transform between two entities that share a resource handle to the same +/// scene: +/// ```nobuild +/// // The group of the mesh in the scene. +/// let scene_mesh_group = TransformGroup::Res(scene_handle.uuid(), mesh_handle.uuid()); +/// // The group of the owned entity that has mesh in a scene. +/// let finished_group = TransformGroup::OwnedGroup(entity, scene_mesh_group.into()); +/// ``` +/// +/// A simpler example of the use of a transform group is when processing lone mesh handles +/// owned by entities: +/// ```nobuild +/// let group = TransformGroup::EntityRes(entity, mesh_handle.uuid()); +/// ``` +/// +/// These were made to fix [#6](https://git.seanomik.net/SeanOMik/lyra-engine/issues/6). +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum TransformGroup { + /// Just an entity. + Entity(Entity), + /// An entity that owns another group. + OwnedGroup(Entity, TransformGroupId), + /// A resource uuid grouped with an owning Entity. + EntityRes(Entity, Uuid), + /// A resource uuid grouped with another resource uuid. + Res(Uuid, Uuid), +} + +/// The index of a specific Transform inside of the buffers. #[derive(Default, Copy, Clone, PartialEq, Eq, Debug)] -pub(crate) struct TransformBufferIndices { - pub buffer_index: usize, - pub transform_index: usize, +pub struct TransformIndex { + /// The index of the entry in the buffer chain. + entry_index: usize, + /// The index of the transform in the entry. + transform_index: usize, } /// A struct representing a single transform buffer. There can be multiple of these -pub(crate) struct TransformBufferEntry { +struct BufferEntry { pub len: usize, pub bindgroup: wgpu::BindGroup, pub transform_buffer: wgpu::Buffer, pub normal_buffer: wgpu::Buffer, } +/// A HashMap that caches values for reuse. +/// +/// The map detects dead values by tracking which entries were not updated since the last time +/// [`CachedValMap::update`] was ran. When dead values are collected, they can be reused on an +/// [`insert`](CachedValMap::insert) into the map. +struct CachedValMap { + latest: HashMap, + old: HashMap, + dead: VecDeque, +} + +impl Default for CachedValMap { + fn default() -> Self { + Self { + latest: Default::default(), + old: Default::default(), + dead: Default::default() + } + } +} + +#[allow(dead_code)] +impl CachedValMap { + /// Insert a key, possibly reusing a value in the map. + /// + /// Returns the reused value, if one was reused. If its `None`, then the value was retrieved + /// from running `val_fn`. + pub fn insert(&mut self, key: K, mut val_fn: F) -> Option + where + F: FnMut() -> V + { + if self.latest.contains_key(&key) { + self.latest.insert(key, val_fn()); + None + } else { + let val = self.dead.pop_front() + .unwrap_or_else(val_fn); + self.latest.insert(key, val.clone()); + Some(val) + } + } + + /// Returns a reference to the value corresponding to the key. + pub fn get(&mut self, key: K) -> Option<&V> { + if let Some(v) = self.old.remove(&key) { + self.latest.insert(key.clone(), v); + } + + self.latest.get(&key) + } + + /// Keep a key alive without updating its value. + pub fn keep_alive(&mut self, key: K) { + if let Some(v) = self.old.remove(&key) { + self.latest.insert(key, v); + } + } + + /// Returns `true` if the map contains a value for the specified key. + pub fn contains(&self, key: K) -> bool { + self.old.contains_key(&key) || self.latest.contains_key(&key) + } + + /// Collects the now dead values for reuse. + /// + /// This detects dead values by tracking which entries were not updated since the last time + /// update was ran. + pub fn update(&mut self) { + // drain the dead values into the dead queue + self.dead.extend(self.old.drain().map(|(_, v)| v)); + + // now drain the latest entries into the old entries + self.old.extend(self.latest.drain()); + } +} + /// A helper struct for managing the Transform buffers for meshes. /// -/// This struct manages a "chain" of uniform buffers that store Transform for meshes. When -/// first created it only has a single "chain-link" with a buffer that is the maximum length +/// This struct manages a "chain" of uniform buffers that store Transform for [`TransformGroup`]s. +/// When first created it only has a single "chain-link" with a buffer that is the maximum length /// the GPU supports. When the first buffer fills up, a new one should be created which will also /// be the maximum length the GPU supports. When the new buffer fills up, a new one will be /// created once again, and so on. /// -/// `Uuid`'s are used to represent entries (usually Meshes) in the buffer. The Uuid's can be used -/// to insert, update, and retrieve the transforms. -pub(crate) struct TransformBuffers { +/// [`TransformGroup`]s are used to represent entries in the buffer. They are used to insert, +/// update, and retrieve the transforms. +pub struct TransformBuffers { pub bindgroup_layout: wgpu::BindGroupLayout, - pub entries: Vec, + groups: CachedValMap, + entries: Vec, limits: wgpu::Limits, + max_transform_count: usize, } impl TransformBuffers { @@ -66,7 +196,9 @@ impl TransformBuffers { let mut s = Self { bindgroup_layout, - entries: vec![], + groups: Default::default(), + entries: Default::default(), + max_transform_count: (limits.max_uniform_buffer_binding_size / 2) as usize / (mem::size_of::()), limits, }; @@ -76,26 +208,70 @@ impl TransformBuffers { s } - pub fn push_transform(&mut self, queue: &wgpu::Queue, limits: &Limits, transform: glam::Mat4, normal_matrix: glam::Mat3) -> u32 { - let entry = self.entries.iter_mut().next().unwrap(); // TODO: use other buffers than just the first + /// Update an existing transform in the buffers. + /// + /// # Panics + /// Panics if the `entity_group` is not already inside of the buffers. + pub fn update_transform(&mut self, queue: &wgpu::Queue, limits: &Limits, entity_group: TransformGroup, transform: glam::Mat4, normal_matrix: glam::Mat3) -> TransformIndex { + let index = *self.groups.get(entity_group.into()) + .expect("Use 'push_transform' for new entities"); + let entry = self.entries.get_mut(index.entry_index).unwrap(); let normal_matrix = glam::Mat4::from_mat3(normal_matrix); // write the transform and normal to the end of the transform - //let offset = (mem::size_of::() * entry.len) as u64; - let offset = Self::get_buffer_offset(limits, entry.len as u32) as _; + let offset = Self::get_buffer_offset(limits, index) as _; queue.write_buffer(&entry.transform_buffer, offset, bytemuck::bytes_of(&transform)); queue.write_buffer(&entry.normal_buffer, offset, bytemuck::bytes_of(&normal_matrix)); - let index = entry.len; - entry.len += 1; - index as _ + index + } + + /// Push a new transform into the buffers. + pub fn push_transform(&mut self, queue: &wgpu::Queue, limits: &Limits, entity_group: TransformGroup, transform: glam::Mat4, normal_matrix: glam::Mat3) -> TransformIndex { + self.groups.insert(entity_group.into(), || { + // this closure is only called when there are no values that can be reused, + // so we get a brand new index at the end of the last entry in the chain. + let last = self.entries.last_mut().unwrap(); + + // ensure the gpu buffer is not overflown + debug_assert!(last.len < self.max_transform_count, + "Transform buffer is filled and 'next_indices' was not incremented! \ + Was a new buffer created?"); + + let tidx = last.len; + last.len += 1; + + TransformIndex { + entry_index: self.entries.len() - 1, + transform_index: tidx + } + }); + + self.update_transform(queue, limits, entity_group, transform, normal_matrix) } /// Collect the dead transforms and prepare self to check next time. pub fn tick(&mut self) { - for entry in self.entries.iter_mut() { - entry.len = 0; + self.groups.update(); + } + + /// Returns a boolean indicating if the buffer contains this group. + pub fn contains(&self, group: TransformGroup) -> bool { + self.groups.contains(group.into()) + } + + /// Update an existing transform group or if its not existing yet, pushes it to the buffer. + /// + /// Returns: the index that the transform is at in the buffers. + pub fn update_or_push(&mut self, queue: &wgpu::Queue, limits: &Limits, group: TransformGroup, transform_fn: F) -> TransformIndex + where F: Fn() -> (glam::Mat4, glam::Mat3) + { + let (transform, normal_matrix) = transform_fn(); + if self.contains(group) { + self.update_transform(queue, limits, group, transform, normal_matrix) + } else { + self.push_transform(queue, limits, group, transform, normal_matrix) } } @@ -105,7 +281,7 @@ impl TransformBuffers { /// "chain-link" is created. pub fn expand_buffers(&mut self, device: &wgpu::Device) { let limits = device.limits(); - let max_buffer_sizes = (limits.max_uniform_buffer_binding_size as u64) / 2; + let max_buffer_sizes = self.max_transform_count as u64 * limits.min_uniform_buffer_offset_alignment as u64; let transform_buffer = device.create_buffer( &wgpu::BufferDescriptor { @@ -157,7 +333,7 @@ impl TransformBuffers { label: Some("BG_Transforms"), }); - let entry = TransformBufferEntry { + let entry = BufferEntry { bindgroup: transform_bind_group, transform_buffer, normal_buffer: normal_mat_buffer, @@ -166,24 +342,28 @@ impl TransformBuffers { self.entries.push(entry); } - pub fn bind_group(&self, transform_id: u32) -> &wgpu::BindGroup { - let _ = transform_id; - - let entry = self.entries.iter().next().unwrap(); // TODO: use other buffers than just the first + /// Returns the bind group for the transform index. + pub fn bind_group(&self, transform_id: TransformIndex) -> &wgpu::BindGroup { + let entry = self.entries.get(transform_id.entry_index).unwrap(); &entry.bindgroup } /// Get the buffer offset for a transform using wgpu limits. /// /// If its possible to borrow immutably, use [`TransformBuffers::buffer_offset`]. - fn get_buffer_offset(limits: &wgpu::Limits, transform_id: u32) -> u32 { - //let entry = self.entries.iter().next().unwrap(); // TODO: use other buffers than just the first - - transform_id * limits.min_uniform_buffer_offset_alignment as u32//mem::size_of::() as u32 + fn get_buffer_offset(limits: &wgpu::Limits, transform_index: TransformIndex) -> u32 { + transform_index.transform_index as u32 * limits.min_uniform_buffer_offset_alignment as u32 } - pub fn buffer_offset(&self, transform_id: u32) -> u32 { - Self::get_buffer_offset(&self.limits, transform_id) + /// Returns the offset of the transform inside the bind group buffer. + /// + /// ```nobuild + /// let bindgroup = transform_buffers.bind_group(job.transform_id); + /// let offset = transform_buffers.buffer_offset(job.transform_id); + /// render_pass.set_bind_group(1, bindgroup, &[ offset, offset, ]); + /// ``` + pub fn buffer_offset(&self, transform_index: TransformIndex) -> u32 { + Self::get_buffer_offset(&self.limits, transform_index) } }