Skip to content

Commit bca3439

Browse files
committed
Intern entity paths for faster comparisons.
This patch places all entity paths into a shared table so that comparing them is as cheap as a pointer comparison. We don't use the pre-existing `Interner` type because that leaks all strings added to it, and I was uncomfortable with leaking names, as Bevy apps might dynamically generate them. Instead, I reference count all the names and use a linked list to stitch together paths into a tree. The interner uses a weak hash set from the [`weak-table`] crate. This patch is especially helpful for the two-phase animation PR bevyengine#11707, because two-phase animation gets rid of the cache from name to animation-specific slot, thus increasing the load on the hash table that maps paths to bone indices. Note that the interned table is a global variable behind a `OnceLock` instead of a resource. This is because it must be accessed from the glTF `AssetLoader`, which unfortunately has no access to Bevy resources. [`weak-table`]: https://crates.io/crates/weak-table
1 parent 71be08a commit bca3439

File tree

5 files changed

+174
-29
lines changed

5 files changed

+174
-29
lines changed

crates/bevy_animation/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ keywords = ["bevy"]
1313
bevy_app = { path = "../bevy_app", version = "0.12.0" }
1414
bevy_asset = { path = "../bevy_asset", version = "0.12.0" }
1515
bevy_core = { path = "../bevy_core", version = "0.12.0" }
16+
bevy_derive = { path = "../bevy_derive", version = "0.12.0" }
1617
bevy_math = { path = "../bevy_math", version = "0.12.0" }
1718
bevy_reflect = { path = "../bevy_reflect", version = "0.12.0", features = [
1819
"bevy",
@@ -24,5 +25,8 @@ bevy_ecs = { path = "../bevy_ecs", version = "0.12.0" }
2425
bevy_transform = { path = "../bevy_transform", version = "0.12.0" }
2526
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.12.0" }
2627

28+
# others
29+
weak-table = "0.3"
30+
2731
[lints]
2832
workspace = true
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
//! Entity paths for referring to bones.
2+
3+
use std::{
4+
fmt::{self, Debug, Formatter, Write},
5+
hash::{Hash, Hasher},
6+
sync::{Arc, Mutex, OnceLock, Weak},
7+
};
8+
9+
use bevy_core::Name;
10+
use bevy_reflect::Reflect;
11+
use bevy_utils::prelude::default;
12+
use weak_table::WeakHashSet;
13+
14+
static ENTITY_PATH_STORE: OnceLock<Mutex<EntityPathStore>> = OnceLock::new();
15+
16+
/// Path to an entity, with [`Name`]s. Each entity in a path must have a name.
17+
#[derive(Clone, Reflect)]
18+
#[reflect_value]
19+
pub struct EntityPath(Arc<EntityPathNode>);
20+
21+
#[derive(PartialEq, Eq, Hash)]
22+
struct EntityPathNode {
23+
name: Name,
24+
parent: Option<EntityPath>,
25+
}
26+
27+
// This could use a `RwLock`, but we actually never read from this, so a mutex
28+
// is actually slightly more efficient!
29+
#[derive(Default)]
30+
struct EntityPathStore(WeakHashSet<Weak<EntityPathNode>>);
31+
32+
pub struct EntityPathIter<'a>(Option<&'a EntityPath>);
33+
34+
impl EntityPathStore {
35+
fn create_path(&mut self, node: EntityPathNode) -> EntityPath {
36+
match self.0.get(&node) {
37+
Some(node) => EntityPath(node),
38+
None => {
39+
let node = Arc::new(node);
40+
self.0.insert(node.clone());
41+
EntityPath(node)
42+
}
43+
}
44+
}
45+
}
46+
47+
impl EntityPath {
48+
pub fn from_name(name: Name) -> EntityPath {
49+
ENTITY_PATH_STORE
50+
.get_or_init(|| default())
51+
.lock()
52+
.unwrap()
53+
.create_path(EntityPathNode { name, parent: None })
54+
}
55+
56+
pub fn from_names(names: &[Name]) -> EntityPath {
57+
let mut store = ENTITY_PATH_STORE.get_or_init(|| default()).lock().unwrap();
58+
59+
let mut names = names.iter();
60+
let root_name = names
61+
.next()
62+
.expect("Entity path must have at least one name in it");
63+
64+
let mut path = store.create_path(EntityPathNode {
65+
name: root_name.clone(),
66+
parent: None,
67+
});
68+
for name in names {
69+
path = store.create_path(EntityPathNode {
70+
name: name.clone(),
71+
parent: Some(path),
72+
});
73+
}
74+
75+
path
76+
}
77+
78+
pub fn extend(&self, name: Name) -> EntityPath {
79+
ENTITY_PATH_STORE
80+
.get_or_init(|| default())
81+
.lock()
82+
.unwrap()
83+
.create_path(EntityPathNode {
84+
name,
85+
parent: Some(self.clone()),
86+
})
87+
}
88+
89+
pub fn iter(&self) -> EntityPathIter {
90+
EntityPathIter(Some(self))
91+
}
92+
93+
pub fn root(&self) -> &Name {
94+
&self.iter().last().unwrap().0.name
95+
}
96+
97+
pub fn len(&self) -> usize {
98+
self.iter().count()
99+
}
100+
101+
pub fn name(&self) -> &Name {
102+
&self.0.name
103+
}
104+
}
105+
106+
impl PartialEq for EntityPath {
107+
fn eq(&self, other: &Self) -> bool {
108+
Arc::ptr_eq(&self.0, &other.0)
109+
}
110+
}
111+
112+
impl Eq for EntityPath {}
113+
114+
impl Hash for EntityPath {
115+
fn hash<H: Hasher>(&self, state: &mut H) {
116+
// Hash by address. This is safe because entity paths are unique.
117+
(self.0.as_ref() as *const EntityPathNode).hash(state)
118+
}
119+
}
120+
121+
impl Debug for EntityPath {
122+
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
123+
let mut names = vec![];
124+
let mut current_path = Some(self.clone());
125+
while let Some(path) = current_path {
126+
names.push(path.0.name.clone());
127+
current_path = path.0.parent.clone();
128+
}
129+
130+
for (name_index, name) in names.iter().rev().enumerate() {
131+
if name_index > 0 {
132+
f.write_char('/')?;
133+
}
134+
f.write_str(name)?;
135+
}
136+
137+
Ok(())
138+
}
139+
}
140+
141+
impl<'a> Iterator for EntityPathIter<'a> {
142+
type Item = &'a EntityPath;
143+
144+
fn next(&mut self) -> Option<Self::Item> {
145+
match self.0 {
146+
None => None,
147+
Some(node) => {
148+
self.0 = node.0.parent.as_ref();
149+
Some(node)
150+
}
151+
}
152+
}
153+
}

crates/bevy_animation/src/lib.rs

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! Animation for the game engine Bevy
22
33
mod animatable;
4+
mod entity_path;
45
mod util;
56

67
use std::ops::{Add, Deref, Mul};
@@ -16,17 +17,20 @@ use bevy_reflect::Reflect;
1617
use bevy_render::mesh::morph::MorphWeights;
1718
use bevy_time::Time;
1819
use bevy_transform::{prelude::Transform, TransformSystem};
20+
use bevy_utils::smallvec::SmallVec;
1921
use bevy_utils::{tracing::warn, HashMap};
2022

2123
#[allow(missing_docs)]
2224
pub mod prelude {
2325
#[doc(hidden)]
2426
pub use crate::{
25-
animatable::*, AnimationClip, AnimationPlayer, AnimationPlugin, EntityPath, Interpolation,
26-
Keyframes, VariableCurve,
27+
animatable::*, entity_path::*, AnimationClip, AnimationPlayer, AnimationPlugin,
28+
Interpolation, Keyframes, VariableCurve,
2729
};
2830
}
2931

32+
pub use crate::entity_path::EntityPath;
33+
3034
/// List of keyframes for one of the attribute of a [`Transform`].
3135
#[derive(Reflect, Clone, Debug)]
3236
pub enum Keyframes {
@@ -138,13 +142,6 @@ pub enum Interpolation {
138142
CubicSpline,
139143
}
140144

141-
/// Path to an entity, with [`Name`]s. Each entity in a path must have a name.
142-
#[derive(Reflect, Clone, Debug, Hash, PartialEq, Eq, Default)]
143-
pub struct EntityPath {
144-
/// Parts of the path
145-
pub parts: Vec<Name>,
146-
}
147-
148145
/// A list of [`VariableCurve`], and the [`EntityPath`] to which they apply.
149146
#[derive(Asset, Reflect, Clone, Debug, Default)]
150147
pub struct AnimationClip {
@@ -199,7 +196,7 @@ impl AnimationClip {
199196

200197
/// Whether this animation clip can run on entity with given [`Name`].
201198
pub fn compatible_with(&self, name: &Name) -> bool {
202-
self.paths.keys().any(|path| &path.parts[0] == name)
199+
self.paths.keys().any(|path| &path.root() == &name)
203200
}
204201
}
205202

@@ -488,9 +485,10 @@ fn entity_from_path(
488485
) -> Option<Entity> {
489486
// PERF: finding the target entity can be optimised
490487
let mut current_entity = root;
491-
path_cache.resize(path.parts.len(), None);
488+
path_cache.resize(path.len(), None);
492489

493-
let mut parts = path.parts.iter().enumerate();
490+
let parts_vec: SmallVec<[&Name; 8]> = path.iter().map(|path| path.name()).collect();
491+
let mut parts = parts_vec.iter().rev().enumerate();
494492

495493
// check the first name is the root node which we already have
496494
let Some((_, root_name)) = parts.next() else {
@@ -506,7 +504,7 @@ fn entity_from_path(
506504
if let Some(cached) = path_cache[idx] {
507505
if children.contains(&cached) {
508506
if let Ok(name) = names.get(cached) {
509-
if name == part {
507+
if name == *part {
510508
current_entity = cached;
511509
found = true;
512510
}
@@ -516,7 +514,7 @@ fn entity_from_path(
516514
if !found {
517515
for child in children.deref() {
518516
if let Ok(name) = names.get(*child) {
519-
if name == part {
517+
if name == *part {
520518
// Found a children with the right name, continue to the next part
521519
current_entity = *child;
522520
path_cache[idx] = Some(*child);

crates/bevy_gltf/src/loader.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -260,9 +260,7 @@ async fn load_gltf<'a, 'b, 'c>(
260260
if let Some((root_index, path)) = paths.get(&node.index()) {
261261
animation_roots.insert(root_index);
262262
animation_clip.add_curve_to_path(
263-
bevy_animation::EntityPath {
264-
parts: path.clone(),
265-
},
263+
bevy_animation::EntityPath::from_names(&path),
266264
bevy_animation::VariableCurve {
267265
keyframe_timestamps,
268266
keyframes,

examples/animation/animated_transform.rs

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,7 @@ fn setup(
3636
let mut animation = AnimationClip::default();
3737
// A curve can modify a single part of a transform, here the translation
3838
animation.add_curve_to_path(
39-
EntityPath {
40-
parts: vec![planet.clone()],
41-
},
39+
EntityPath::from_name(planet.clone()),
4240
VariableCurve {
4341
keyframe_timestamps: vec![0.0, 1.0, 2.0, 3.0, 4.0],
4442
keyframes: Keyframes::Translation(vec![
@@ -57,9 +55,7 @@ fn setup(
5755
// To find the entity to modify, the hierarchy will be traversed looking for
5856
// an entity with the right name at each level
5957
animation.add_curve_to_path(
60-
EntityPath {
61-
parts: vec![planet.clone(), orbit_controller.clone()],
62-
},
58+
EntityPath::from_names(&[planet.clone(), orbit_controller.clone()]),
6359
VariableCurve {
6460
keyframe_timestamps: vec![0.0, 1.0, 2.0, 3.0, 4.0],
6561
keyframes: Keyframes::Rotation(vec![
@@ -76,9 +72,7 @@ fn setup(
7672
// until all other curves are finished. In that case, another animation should
7773
// be created for each part that would have a different duration / period
7874
animation.add_curve_to_path(
79-
EntityPath {
80-
parts: vec![planet.clone(), orbit_controller.clone(), satellite.clone()],
81-
},
75+
EntityPath::from_names(&[planet.clone(), orbit_controller.clone(), satellite.clone()]),
8276
VariableCurve {
8377
keyframe_timestamps: vec![0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0],
8478
keyframes: Keyframes::Scale(vec![
@@ -97,9 +91,7 @@ fn setup(
9791
);
9892
// There can be more than one curve targeting the same entity path
9993
animation.add_curve_to_path(
100-
EntityPath {
101-
parts: vec![planet.clone(), orbit_controller.clone(), satellite.clone()],
102-
},
94+
EntityPath::from_names(&[planet.clone(), orbit_controller.clone(), satellite.clone()]),
10395
VariableCurve {
10496
keyframe_timestamps: vec![0.0, 1.0, 2.0, 3.0, 4.0],
10597
keyframes: Keyframes::Rotation(vec![

0 commit comments

Comments
 (0)