Skip to content

Commit

Permalink
Move EntityHash related types into bevy_ecs (#11498)
Browse files Browse the repository at this point in the history
# Objective

Reduce the size of `bevy_utils`
(#11478)

## Solution

Move `EntityHash` related types into `bevy_ecs`. This also allows us
access to `Entity`, which means we no longer need `EntityHashMap`'s
first generic argument.

---

## Changelog

- Moved `bevy::utils::{EntityHash, EntityHasher, EntityHashMap,
EntityHashSet}` into `bevy::ecs::entity::hash` .
- Removed `EntityHashMap`'s first generic argument. It is now hardcoded
to always be `Entity`.

## Migration Guide

- Uses of `bevy::utils::{EntityHash, EntityHasher, EntityHashMap,
EntityHashSet}` now have to be imported from `bevy::ecs::entity::hash`.
- Uses of `EntityHashMap` no longer have to specify the first generic
parameter. It is now hardcoded to always be `Entity`.
  • Loading branch information
doonv authored Feb 12, 2024
1 parent c1a4e29 commit 1c67e02
Show file tree
Hide file tree
Showing 35 changed files with 262 additions and 229 deletions.
4 changes: 2 additions & 2 deletions benches/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,6 @@ path = "benches/bevy_math/bezier.rs"
harness = false

[[bench]]
name = "utils"
path = "benches/bevy_utils/entity_hash.rs"
name = "entity_hash"
path = "benches/bevy_ecs/world/entity_hash.rs"
harness = false
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use bevy_ecs::entity::Entity;
use bevy_utils::EntityHashSet;
use bevy_ecs::entity::{Entity, EntityHashMap, EntityHashSet};
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;
Expand Down Expand Up @@ -28,7 +27,7 @@ fn make_entity(rng: &mut impl Rng, size: usize) -> Entity {
e
}

fn entity_set_build_and_lookup(c: &mut Criterion) {
pub fn entity_set_build_and_lookup(c: &mut Criterion) {
let mut group = c.benchmark_group("entity_hash");
for size in SIZES {
// Get some random-but-consistent entities to use for all the benches below.
Expand Down
11 changes: 8 additions & 3 deletions benches/benches/bevy_ecs/world/mod.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
use criterion::criterion_group;

mod commands;
mod spawn;
mod world_get;

use commands::*;

mod spawn;
use spawn::*;

mod world_get;
use world_get::*;

mod entity_hash;
use entity_hash::*;

criterion_group!(
world_benches,
empty_commands,
Expand All @@ -30,4 +34,5 @@ criterion_group!(
query_get_many::<2>,
query_get_many::<5>,
query_get_many::<10>,
entity_set_build_and_lookup
);
1 change: 1 addition & 0 deletions crates/bevy_ecs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ thiserror = "1.0"

[dev-dependencies]
rand = "0.8"
static_assertions = "1.1.0"

[[example]]
name = "events"
Expand Down
99 changes: 99 additions & 0 deletions crates/bevy_ecs/src/entity/hash.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
use std::hash::{BuildHasher, Hasher};

#[cfg(feature = "bevy_reflect")]
use bevy_reflect::Reflect;
use bevy_utils::hashbrown;

use super::Entity;

/// A [`BuildHasher`] that results in a [`EntityHasher`].
#[derive(Default, Clone)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
pub struct EntityHash;

impl BuildHasher for EntityHash {
type Hasher = EntityHasher;

fn build_hasher(&self) -> Self::Hasher {
Self::Hasher::default()
}
}

/// A very fast hash that is only designed to work on generational indices
/// like [`Entity`]. It will panic if attempting to hash a type containing
/// non-u64 fields.
///
/// This is heavily optimized for typical cases, where you have mostly live
/// entities, and works particularly well for contiguous indices.
///
/// If you have an unusual case -- say all your indices are multiples of 256
/// or most of the entities are dead generations -- then you might want also to
/// try [`AHasher`](bevy_utils::AHasher) for a slower hash computation but fewer lookup conflicts.
#[derive(Debug, Default)]
pub struct EntityHasher {
hash: u64,
}

impl Hasher for EntityHasher {
#[inline]
fn finish(&self) -> u64 {
self.hash
}

fn write(&mut self, _bytes: &[u8]) {
panic!("EntityHasher can only hash u64 fields.");
}

#[inline]
fn write_u64(&mut self, bits: u64) {
// SwissTable (and thus `hashbrown`) cares about two things from the hash:
// - H1: low bits (masked by `2ⁿ-1`) to pick the slot in which to store the item
// - H2: high 7 bits are used to SIMD optimize hash collision probing
// For more see <https://abseil.io/about/design/swisstables#metadata-layout>

// This hash function assumes that the entity ids are still well-distributed,
// so for H1 leaves the entity id alone in the low bits so that id locality
// will also give memory locality for things spawned together.
// For H2, take advantage of the fact that while multiplication doesn't
// spread entropy to the low bits, it's incredibly good at spreading it
// upward, which is exactly where we need it the most.

// While this does include the generation in the output, it doesn't do so
// *usefully*. H1 won't care until you have over 3 billion entities in
// the table, and H2 won't care until something hits generation 33 million.
// Thus the comment suggesting that this is best for live entities,
// where there won't be generation conflicts where it would matter.

// The high 32 bits of this are ⅟φ for Fibonacci hashing. That works
// particularly well for hashing for the same reason as described in
// <https://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/>
// It loses no information because it has a modular inverse.
// (Specifically, `0x144c_bc89_u32 * 0x9e37_79b9_u32 == 1`.)
//
// The low 32 bits make that part of the just product a pass-through.
const UPPER_PHI: u64 = 0x9e37_79b9_0000_0001;

// This is `(MAGIC * index + generation) << 32 + index`, in a single instruction.
self.hash = bits.wrapping_mul(UPPER_PHI);
}
}

/// A [`HashMap`](hashbrown::HashMap) pre-configured to use [`EntityHash`] hashing.
pub type EntityHashMap<V> = hashbrown::HashMap<Entity, V, EntityHash>;

/// A [`HashSet`](hashbrown::HashSet) pre-configured to use [`EntityHash`] hashing.
pub type EntityHashSet = hashbrown::HashSet<Entity, EntityHash>;

#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "bevy_reflect")]
use bevy_reflect::Reflect;
use static_assertions::assert_impl_all;

// Check that the HashMaps are Clone if the key/values are Clone
assert_impl_all!(EntityHashMap::<usize>: Clone);
// EntityHashMap should implement Reflect
#[cfg(feature = "bevy_reflect")]
assert_impl_all!(EntityHashMap::<i32>: Reflect);
}
35 changes: 17 additions & 18 deletions crates/bevy_ecs/src/entity/map_entities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@ use crate::{
identifier::masks::{IdentifierMask, HIGH_MASK},
world::World,
};
use bevy_utils::EntityHashMap;

use super::EntityHashMap;

/// Operation to map all contained [`Entity`] fields in a type to new values.
///
/// As entity IDs are valid only for the [`World`] they're sourced from, using [`Entity`]
/// as references in components copied from another world will be invalid. This trait
/// allows defining custom mappings for these references via [`EntityMappers`](EntityMapper), which
/// inject the entity mapping strategy between your `MapEntities` type and the current world
/// (usually by using an [`EntityHashMap<Entity, Entity>`] between source entities and entities in the
/// (usually by using an [`EntityHashMap<Entity>`] between source entities and entities in the
/// current world).
///
/// Implementing this trait correctly is required for properly loading components
Expand Down Expand Up @@ -47,7 +48,7 @@ pub trait MapEntities {

/// An implementor of this trait knows how to map an [`Entity`] into another [`Entity`].
///
/// Usually this is done by using an [`EntityHashMap<Entity, Entity>`] to map source entities
/// Usually this is done by using an [`EntityHashMap<Entity>`] to map source entities
/// (mapper inputs) to the current world's entities (mapper outputs).
///
/// More generally, this can be used to map [`Entity`] references between any two [`Worlds`](World).
Expand All @@ -56,10 +57,10 @@ pub trait MapEntities {
///
/// ```
/// # use bevy_ecs::entity::{Entity, EntityMapper};
/// # use bevy_utils::EntityHashMap;
/// # use bevy_ecs::entity::EntityHashMap;
/// #
/// pub struct SimpleEntityMapper {
/// map: EntityHashMap<Entity, Entity>,
/// map: EntityHashMap<Entity>,
/// }
///
/// // Example implementation of EntityMapper where we map an entity to another entity if it exists
Expand Down Expand Up @@ -97,7 +98,7 @@ impl EntityMapper for SceneEntityMapper<'_> {
}
}

/// A wrapper for [`EntityHashMap<Entity, Entity>`], augmenting it with the ability to allocate new [`Entity`] references in a destination
/// A wrapper for [`EntityHashMap<Entity>`], augmenting it with the ability to allocate new [`Entity`] references in a destination
/// world. These newly allocated references are guaranteed to never point to any living entity in that world.
///
/// References are allocated by returning increasing generations starting from an internally initialized base
Expand All @@ -110,9 +111,9 @@ pub struct SceneEntityMapper<'m> {
/// or over the network. This is required as [`Entity`] identifiers are opaque; you cannot and do not want to reuse
/// identifiers directly.
///
/// On its own, a [`EntityHashMap<Entity, Entity>`] is not capable of allocating new entity identifiers, which is needed to map references
/// On its own, a [`EntityHashMap<Entity>`] is not capable of allocating new entity identifiers, which is needed to map references
/// to entities that lie outside the source entity set. This functionality can be accessed through [`SceneEntityMapper::world_scope()`].
map: &'m mut EntityHashMap<Entity, Entity>,
map: &'m mut EntityHashMap<Entity>,
/// A base [`Entity`] used to allocate new references.
dead_start: Entity,
/// The number of generations this mapper has allocated thus far.
Expand All @@ -129,18 +130,18 @@ impl<'m> SceneEntityMapper<'m> {
self.map_entity(entity)
}

/// Gets a reference to the underlying [`EntityHashMap<Entity, Entity>`].
pub fn get_map(&'m self) -> &'m EntityHashMap<Entity, Entity> {
/// Gets a reference to the underlying [`EntityHashMap<Entity>`].
pub fn get_map(&'m self) -> &'m EntityHashMap<Entity> {
self.map
}

/// Gets a mutable reference to the underlying [`EntityHashMap<Entity, Entity>`].
pub fn get_map_mut(&'m mut self) -> &'m mut EntityHashMap<Entity, Entity> {
/// Gets a mutable reference to the underlying [`EntityHashMap<Entity>`].
pub fn get_map_mut(&'m mut self) -> &'m mut EntityHashMap<Entity> {
self.map
}

/// Creates a new [`SceneEntityMapper`], spawning a temporary base [`Entity`] in the provided [`World`]
fn new(map: &'m mut EntityHashMap<Entity, Entity>, world: &mut World) -> Self {
fn new(map: &'m mut EntityHashMap<Entity>, world: &mut World) -> Self {
Self {
map,
// SAFETY: Entities data is kept in a valid state via `EntityMapper::world_scope`
Expand All @@ -160,14 +161,14 @@ impl<'m> SceneEntityMapper<'m> {
assert!(entities.reserve_generations(self.dead_start.index(), self.generations));
}

/// Creates an [`SceneEntityMapper`] from a provided [`World`] and [`EntityHashMap<Entity, Entity>`], then calls the
/// Creates an [`SceneEntityMapper`] from a provided [`World`] and [`EntityHashMap<Entity>`], then calls the
/// provided function with it. This allows one to allocate new entity references in this [`World`] that are
/// guaranteed to never point at a living entity now or in the future. This functionality is useful for safely
/// mapping entity identifiers that point at entities outside the source world. The passed function, `f`, is called
/// within the scope of this world. Its return value is then returned from `world_scope` as the generic type
/// parameter `R`.
pub fn world_scope<R>(
entity_map: &'m mut EntityHashMap<Entity, Entity>,
entity_map: &'m mut EntityHashMap<Entity>,
world: &mut World,
f: impl FnOnce(&mut World, &mut Self) -> R,
) -> R {
Expand All @@ -180,10 +181,8 @@ impl<'m> SceneEntityMapper<'m> {

#[cfg(test)]
mod tests {
use bevy_utils::EntityHashMap;

use crate::{
entity::{Entity, EntityMapper, SceneEntityMapper},
entity::{Entity, EntityHashMap, EntityMapper, SceneEntityMapper},
world::World,
};

Expand Down
19 changes: 15 additions & 4 deletions crates/bevy_ecs/src/entity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,14 @@
//! [`EntityWorldMut::insert`]: crate::world::EntityWorldMut::insert
//! [`EntityWorldMut::remove`]: crate::world::EntityWorldMut::remove
mod map_entities;
#[cfg(feature = "bevy_reflect")]
use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize};
pub use map_entities::*;

mod hash;
pub use hash::*;

use bevy_utils::tracing::warn;
pub use map_entities::*;

use crate::{
archetype::{ArchetypeId, ArchetypeRow},
Expand Down Expand Up @@ -123,6 +128,11 @@ type IdCursor = isize;
/// [`Query::get`]: crate::system::Query::get
/// [`World`]: crate::world::World
#[derive(Clone, Copy)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
#[cfg_attr(
feature = "bevy_reflect",
reflect_value(Hash, PartialEq, Serialize, Deserialize)
)]
// Alignment repr necessary to allow LLVM to better output
// optimised codegen for `to_bits`, `PartialEq` and `Ord`.
#[repr(C, align(8))]
Expand Down Expand Up @@ -218,7 +228,7 @@ impl Entity {
/// // ... replace the entities with valid ones.
/// ```
///
/// Deriving [`Reflect`](bevy_reflect::Reflect) for a component that has an `Entity` field:
/// Deriving [`Reflect`] for a component that has an `Entity` field:
///
/// ```no_run
/// # use bevy_ecs::{prelude::*, component::*};
Expand Down Expand Up @@ -1092,7 +1102,7 @@ mod tests {
#[test]
fn entity_hash_keeps_similar_ids_together() {
use std::hash::BuildHasher;
let hash = bevy_utils::EntityHash;
let hash = EntityHash;

let first_id = 0xC0FFEE << 8;
let first_hash = hash.hash_one(Entity::from_raw(first_id));
Expand All @@ -1107,7 +1117,8 @@ mod tests {
#[test]
fn entity_hash_id_bitflip_affects_high_7_bits() {
use std::hash::BuildHasher;
let hash = bevy_utils::EntityHash;

let hash = EntityHash;

let first_id = 0xC0FFEE;
let first_hash = hash.hash_one(Entity::from_raw(first_id)) >> 57;
Expand Down
21 changes: 8 additions & 13 deletions crates/bevy_ecs/src/reflect/map_entities.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
use crate::{
component::Component,
entity::{Entity, MapEntities, SceneEntityMapper},
entity::{Entity, EntityHashMap, MapEntities, SceneEntityMapper},
world::World,
};
use bevy_reflect::FromType;
use bevy_utils::EntityHashMap;

/// For a specific type of component, this maps any fields with values of type [`Entity`] to a new world.
/// Since a given `Entity` ID is only valid for the world it came from, when performing deserialization
Expand All @@ -18,33 +17,29 @@ pub struct ReflectMapEntities {
}

impl ReflectMapEntities {
/// A general method for applying [`MapEntities`] behavior to all elements in an [`EntityHashMap<Entity, Entity>`].
/// A general method for applying [`MapEntities`] behavior to all elements in an [`EntityHashMap<Entity>`].
///
/// Be mindful in its usage: Works best in situations where the entities in the [`EntityHashMap<Entity, Entity>`] are newly
/// Be mindful in its usage: Works best in situations where the entities in the [`EntityHashMap<Entity>`] are newly
/// created, before systems have a chance to add new components. If some of the entities referred to
/// by the [`EntityHashMap<Entity, Entity>`] might already contain valid entity references, you should use [`map_entities`](Self::map_entities).
/// by the [`EntityHashMap<Entity>`] might already contain valid entity references, you should use [`map_entities`](Self::map_entities).
///
/// An example of this: A scene can be loaded with `Parent` components, but then a `Parent` component can be added
/// to these entities after they have been loaded. If you reload the scene using [`map_all_entities`](Self::map_all_entities), those `Parent`
/// components with already valid entity references could be updated to point at something else entirely.
pub fn map_all_entities(
&self,
world: &mut World,
entity_map: &mut EntityHashMap<Entity, Entity>,
) {
pub fn map_all_entities(&self, world: &mut World, entity_map: &mut EntityHashMap<Entity>) {
SceneEntityMapper::world_scope(entity_map, world, self.map_all_entities);
}

/// A general method for applying [`MapEntities`] behavior to elements in an [`EntityHashMap<Entity, Entity>`]. Unlike
/// A general method for applying [`MapEntities`] behavior to elements in an [`EntityHashMap<Entity>`]. Unlike
/// [`map_all_entities`](Self::map_all_entities), this is applied to specific entities, not all values
/// in the [`EntityHashMap<Entity, Entity>`].
/// in the [`EntityHashMap<Entity>`].
///
/// This is useful mostly for when you need to be careful not to update components that already contain valid entity
/// values. See [`map_all_entities`](Self::map_all_entities) for more details.
pub fn map_entities(
&self,
world: &mut World,
entity_map: &mut EntityHashMap<Entity, Entity>,
entity_map: &mut EntityHashMap<Entity>,
entities: &[Entity],
) {
SceneEntityMapper::world_scope(entity_map, world, |world, mapper| {
Expand Down
Loading

0 comments on commit 1c67e02

Please sign in to comment.