Skip to content

Commit

Permalink
Adding a feature-controllable event queue that tracks the creation an…
Browse files Browse the repository at this point in the history
…d destruction of entities within each archetype, and a function to the world to collect and drain them all.
  • Loading branch information
recatek committed Jan 13, 2025
1 parent 2bf68c4 commit 518f0b7
Show file tree
Hide file tree
Showing 10 changed files with 296 additions and 18 deletions.
14 changes: 12 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,21 @@ keywords = ["ecs", "entity"]
categories = ["data-structures", "game-engines"]

[features]
default = []
default = ['events']

# Allow archetypes to have up to 32 components (default is 16). Worsens compile times.
32_components = []
# Wrap rather than panic when a version number overflows (4,294,967,295 max)

# Wrap rather than panic when a version number overflows (4,294,967,295 max). Yields some small perf gains when
# creating/destroying entities, but has a (probabilistically insignificant) risk of allowing reuse of stale handles.
wrapping_version = []

# Adds event tracking for entity creation/destruction. Has the following perf consequences:
# - Adds some additional allocation overhead to the ECS system for each per-archetype queue.
# - Every entity creation or destruction pushes an event to an archetype's queue.
# - Event queues will accumulate indefinitely and must be regularly drained.
events = ['gecs_macros/events']

[dependencies]
gecs_macros = { version = "0.3.0", path = "macros", default-features = false }

Expand Down
1 change: 1 addition & 0 deletions macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ proc-macro = true

[features]
default = []
events = []

[dependencies]
convert_case = { version = "0.6" }
Expand Down
55 changes: 55 additions & 0 deletions macros/src/generate/world.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ pub fn generate_world(world_data: &DataWorld, raw_input: &str) -> TokenStream {
.collect::<Vec<_>>();
let num_archetypes = world_data.archetypes.len();

// Functions
let drain_events = world_drain_events(world_data);

// Generated subsections
let section_archetype = world_data
.archetypes
Expand Down Expand Up @@ -134,6 +137,9 @@ pub fn generate_world(world_data: &DataWorld, raw_input: &str) -> TokenStream {

type Capacities = #WorldCapacity;

// Will only appear if we have the events feature enabled.
#drain_events

#[inline(always)]
fn new() -> Self {
Self {
Expand Down Expand Up @@ -607,6 +613,9 @@ fn section_archetype(archetype_data: &DataArchetype) -> TokenStream {
.map(|component| format_ident!("{}", to_snake(&component.name)))
.collect::<Vec<_>>();

// Functions
let drain_events = archetype_drain_events();

// Documentation helpers
let archetype_doc_component_types = archetype_data
.components
Expand Down Expand Up @@ -645,6 +654,9 @@ fn section_archetype(archetype_data: &DataArchetype) -> TokenStream {
type IterArgs<'a> = #IterArgs;
type IterMutArgs<'a> = #IterMutArgs;

// Will only appear if we have the events feature enabled.
#drain_events

#[inline(always)]
fn new() -> Self {
Self { data: #StorageN::new() }
Expand Down Expand Up @@ -1057,3 +1069,46 @@ fn with_capacity_new(archetype_data: &DataArchetype) -> TokenStream {
let archetype = format_ident!("{}", to_snake(&archetype_data.name));
quote!(with_capacity(capacity.#archetype))
}

#[allow(non_snake_case)]
fn world_drain_events(world_data: &DataWorld) -> TokenStream {
if cfg!(feature = "events") {
let archetype_fields = world_data
.archetypes
.iter()
.rev() // Reverse the list because we'll chain inside-out
.map(|archetype| format_ident!("{}", to_snake(&archetype.name)))
.collect::<Vec<_>>();

// We throw a compile error if we don't have any archetypes, so we must have at least one.
let archetype = &archetype_fields[0];
let mut body = quote!(self.#archetype.drain_events());

// Keep nesting the chain operations
for archetype in archetype_fields[1..].iter() {
body = quote!(self.#archetype.drain_events().chain(#body));
}

quote!(
#[inline(always)]
fn drain_events(&mut self) -> impl Iterator<Item = EcsEvent> {
#body
}
)
} else {
quote!()
}
}

fn archetype_drain_events() -> TokenStream {
if cfg!(feature = "events") {
quote!(
#[inline(always)]
fn drain_events(&mut self) -> impl Iterator<Item = EcsEvent> {
self.data.drain_events()
}
)
} else {
quote!()
}
}
7 changes: 7 additions & 0 deletions macros/src/parse/world.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,13 @@ impl Parse for ParseEcsWorld {

// TODO: Check for duplicates?

if archetypes.is_empty() {
return Err(syn::Error::new(
input.span(),
"ecs world must have at least one archetype",
));
}

Ok(Self { name, archetypes })
}
}
Expand Down
57 changes: 45 additions & 12 deletions src/archetype/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ use crate::traits::{Archetype, EntityKey, StorageCanResolve};
use crate::util::{debug_checked_assume, num_assert_leq};
use crate::version::ArchetypeVersion;

#[cfg(feature = "events")]
use crate::event::EcsEvent;

macro_rules! declare_storage_dynamic_n {
(
$name:ident,
Expand All @@ -37,6 +40,9 @@ macro_rules! declare_storage_dynamic_n {
// No RefCell here since we never grant mutable access externally
entities: DataPtr<Entity<A>>,
#(d~I: RefCell<DataPtr<T~I>>,)*

#[cfg(feature = "events")]
events: Vec<EcsEvent>, // Optional queue tracking create/destroy events
}

impl<A: Archetype, #(T~I,)*> $name<A, #(T~I,)*>
Expand Down Expand Up @@ -68,6 +74,9 @@ macro_rules! declare_storage_dynamic_n {
slots,
entities: DataPtr::with_capacity(capacity),
#(d~I: RefCell::new(DataPtr::with_capacity(capacity)),)*

#[cfg(feature = "events")]
events: Vec::new(),
}
}

Expand Down Expand Up @@ -114,7 +123,7 @@ macro_rules! declare_storage_dynamic_n {
}
}

unsafe { self.force_push(data) }
unsafe { self.force_create(data) }
}

/// Adds a new entity if there is sufficient spare capacity to store it.
Expand All @@ -136,7 +145,7 @@ macro_rules! declare_storage_dynamic_n {
return Err(data);
}

Ok(unsafe { self.force_push(data) })
Ok(unsafe { self.force_create(data) })
}

/// Removes the given entity from storage if it exists there.
Expand Down Expand Up @@ -303,6 +312,13 @@ macro_rules! declare_storage_dynamic_n {
}
)*

/// Drains the active event queue of its entity create/destroy events.
#[cfg(feature = "events")]
#[inline(always)]
pub fn drain_events(&mut self) -> impl Iterator<Item = EcsEvent> + '_ {
self.events.drain(..)
}

/// Resolves the slot index and data index for a given entity.
/// Both indices are guaranteed to point to valid corresponding cells.
#[inline(always)]
Expand Down Expand Up @@ -459,7 +475,7 @@ macro_rules! declare_storage_dynamic_n {
/// It is up to the caller to guarantee the following:
/// - The storage has enough allocated room for the data.
#[inline(always)]
unsafe fn force_push(&mut self, data: (#(T~I,)*)) -> Entity<A> {
unsafe fn force_create(&mut self, data: (#(T~I,)*)) -> Entity<A> {
debug_assert!(self.len < self.capacity);

unsafe {
Expand Down Expand Up @@ -489,17 +505,22 @@ macro_rules! declare_storage_dynamic_n {
self.entities.write(index, entity);
#(self.d~I.get_mut().write(index, data.I);)*

#[cfg(feature = "events")]
{
self.events.push(EcsEvent::Created(entity.into()));
}

entity
}
}

/// Destroys the given slot and data.
///
/// # Safety
///
/// The caller must guarantee that slot_index and dense_index refer to valid
/// corresponding slot and data cells within range (and implicitly, self.len > 0).
unsafe fn destroy_resolved(
/// Destroys the given slot and data.
///
/// # Safety
///
/// The caller must guarantee that slot_index and dense_index refer to valid
/// corresponding slot and data cells within range (and implicitly, self.len > 0).
unsafe fn force_destroy(
&mut self,
indices: (TrimmedIndex, TrimmedIndex), // (slot_index, dense_index)
) -> (#(T~I,)*) {
Expand All @@ -517,6 +538,18 @@ macro_rules! declare_storage_dynamic_n {
let entities = self.entities.slice(self.len);
debug_assert!(entities.len() == self.len);

// Make sure the entity backtracks to the same slot we're removing now.
debug_assert!(entities[dense_index_usize].slot_index() == slot_index);
debug_assert_eq!(
entities[dense_index_usize].version(),
self.slots.slice(self.capacity())[slot_index_usize].version());

#[cfg(feature = "events")]
{
let entity = *entities.get_unchecked(dense_index_usize);
self.events.push(EcsEvent::Destroyed(entity.into()));
}

// SAFETY: We know self.len > 0 because we got Some from resolve_slot.
let last_dense_index = self.len - 1;
// SAFETY: We know the entity slice has a length of self.len.
Expand Down Expand Up @@ -587,7 +620,7 @@ macro_rules! declare_storage_dynamic_n {
fn resolve_destroy(&mut self, entity: Entity<A>) -> Option<A::Components> {
unsafe {
// SAFETY: We know that resolve_entity returns valid corresponding slots.
Some(self.destroy_resolved(self.resolve_entity(entity)?))
Some(self.force_destroy(self.resolve_entity(entity)?))
}
}
}
Expand Down Expand Up @@ -621,7 +654,7 @@ macro_rules! declare_storage_dynamic_n {
fn resolve_destroy(&mut self, entity: EntityDirect<A>) -> Option<(#(T~I,)*)> {
unsafe {
// SAFETY: We know that resolve_direct returns valid corresponding slots.
Some(self.destroy_resolved(self.resolve_direct(entity)?))
Some(self.force_destroy(self.resolve_direct(entity)?))
}
}
}
Expand Down
14 changes: 14 additions & 0 deletions src/event.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
use crate::entity::EntityAny;

#[cfg(doc)]
use crate::traits::{Archetype, World};

/// A struct reporting ordered entity creation and destruction events for archetypes and worlds.
/// See [`World::drain_events`] and [`Archetype::drain_events`] for more information.
#[cfg(feature = "events")]
#[derive(Clone, Copy, PartialEq)]
#[cfg_attr(debug_assertions, derive(Debug))]
pub enum EcsEvent {
Created(EntityAny),
Destroyed(EntityAny),
}
1 change: 1 addition & 0 deletions src/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub(crate) const MAX_DATA_INDEX: u32 = MAX_DATA_CAPACITY - 1;

/// A size-checked index that can always fit in an entity and live slot.
#[derive(Clone, Copy, Eq, Ord, PartialEq, PartialOrd)]
#[cfg_attr(debug_assertions, derive(Debug))]
pub(crate) struct TrimmedIndex(u32);

impl TrimmedIndex {
Expand Down
17 changes: 13 additions & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ pub mod version;
/// Enums for controlling iteration stepping.
pub mod iter;

/// Events for reporting entity creation and destruction.
#[cfg(feature = "events")]
pub mod event;

mod macros {
/// Macro for declaring a new ECS world struct with archetype storage.
///
Expand Down Expand Up @@ -772,7 +776,7 @@ pub struct OneOf {
/// }
/// ```
#[cfg(doc)]
pub enum ArchetypeSelectId { }
pub enum ArchetypeSelectId {}

/// A dispatch enum for resolving a dynamic [`EntityAny`](crate::entity::EntityAny)
/// key to a typed [`Entity`](crate::entity::Entity) key. Use `try_into` to perform the
Expand Down Expand Up @@ -807,7 +811,7 @@ pub enum ArchetypeSelectId { }
/// }
/// ```
#[cfg(doc)]
pub enum ArchetypeSelectEntity { }
pub enum ArchetypeSelectEntity {}

/// A dispatch enum for resolving a dynamic [`EntityDirectAny`](crate::entity::EntityDirectAny)
/// key to a typed [`EntityDirect`](crate::entity::EntityDirect) key. Use `try_into` to perform the
Expand Down Expand Up @@ -843,8 +847,7 @@ pub enum ArchetypeSelectEntity { }
/// }
/// ```
#[cfg(doc)]
pub enum ArchetypeSelectEntityDirect { }

pub enum ArchetypeSelectEntityDirect {}

#[cfg(not(doc))]
pub use gecs_macros::{ecs_component_id, ecs_world};
Expand All @@ -867,6 +870,9 @@ pub mod prelude {
pub use traits::{Archetype, ArchetypeHas};
pub use traits::{View, ViewHas};
pub use traits::{Borrow, BorrowHas};

#[cfg(feature = "events")]
pub use event::EcsEvent;
}

#[doc(hidden)]
Expand Down Expand Up @@ -902,4 +908,7 @@ pub mod __internal {
pub use traits::{Archetype, ArchetypeHas};
pub use traits::{View, ViewHas};
pub use traits::{Borrow, BorrowHas};

#[cfg(feature = "events")]
pub use event::EcsEvent;
}
Loading

0 comments on commit 518f0b7

Please sign in to comment.