Skip to content

Commit 80961d1

Browse files
committed
Fix sparse insert (#1748)
Removing the checks on this line https://github.com/bevyengine/bevy/blob/main/crates/bevy_sprite/src/frustum_culling.rs#L64 and running the "many_sprites" example revealed two corner case bugs in bevy_ecs. The first, a simple and honest missed line introduced in #1471. The other, an insidious monster that has been there since the ECS v2 rewrite, just waiting for the time to strike: 1. #1471 accidentally removed the "insert" line for sparse set components with the "mutated" bundle state. Re-adding it fixes the problem. I did a slight refactor here to make the implementation simpler and remove a branch. 2. The other issue is nastier. ECS v2 added an "archetype graph". When determining what components were added/mutated during an archetype change, we read the FromBundle edge (which encodes this state) on the "new" archetype. The problem is that unlike "add edges" which are guaranteed to be unique for a given ("graph node", "bundle id") pair, FromBundle edges are not necessarily unique: ```rust // OLD_ARCHETYPE -> NEW_ARCHETYPE // [] -> [usize] e.insert(2usize); // [usize] -> [usize, i32] e.insert(1i32); // [usize, i32] -> [usize, i32] e.insert(1i32); // [usize, i32] -> [usize] e.remove::<i32>(); // [usize] -> [usize, i32] e.insert(1i32); ``` Note that the second `e.insert(1i32)` command has a different "archetype graph edge" than the first, but they both lead to the same "new archetype". The fix here is simple: just remove FromBundle edges because they are broken and store the information in the "add edges", which are guaranteed to be unique. FromBundle edges were added to cut down on the number of archetype accesses / make the archetype access patterns nicer. But benching this change resulted in no significant perf changes and the addition of get_2_mut() for archetypes resolves the access pattern issue.
1 parent 78edec2 commit 80961d1

File tree

5 files changed

+69
-81
lines changed

5 files changed

+69
-81
lines changed

crates/bevy_ecs/src/archetype.rs

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -41,45 +41,34 @@ pub enum ComponentStatus {
4141
Mutated,
4242
}
4343

44-
pub struct FromBundle {
44+
pub struct AddBundle {
4545
pub archetype_id: ArchetypeId,
4646
pub bundle_status: Vec<ComponentStatus>,
4747
}
4848

4949
#[derive(Default)]
5050
pub struct Edges {
51-
pub add_bundle: SparseArray<BundleId, ArchetypeId>,
51+
pub add_bundle: SparseArray<BundleId, AddBundle>,
5252
pub remove_bundle: SparseArray<BundleId, Option<ArchetypeId>>,
5353
pub remove_bundle_intersection: SparseArray<BundleId, Option<ArchetypeId>>,
54-
pub from_bundle: SparseArray<BundleId, FromBundle>,
5554
}
5655

5756
impl Edges {
5857
#[inline]
59-
pub fn get_add_bundle(&self, bundle_id: BundleId) -> Option<ArchetypeId> {
60-
self.add_bundle.get(bundle_id).cloned()
58+
pub fn get_add_bundle(&self, bundle_id: BundleId) -> Option<&AddBundle> {
59+
self.add_bundle.get(bundle_id)
6160
}
6261

6362
#[inline]
64-
pub fn set_add_bundle(&mut self, bundle_id: BundleId, archetype_id: ArchetypeId) {
65-
self.add_bundle.insert(bundle_id, archetype_id);
66-
}
67-
68-
#[inline]
69-
pub fn get_from_bundle(&self, bundle_id: BundleId) -> Option<&FromBundle> {
70-
self.from_bundle.get(bundle_id)
71-
}
72-
73-
#[inline]
74-
pub fn set_from_bundle(
63+
pub fn set_add_bundle(
7564
&mut self,
7665
bundle_id: BundleId,
7766
archetype_id: ArchetypeId,
7867
bundle_status: Vec<ComponentStatus>,
7968
) {
80-
self.from_bundle.insert(
69+
self.add_bundle.insert(
8170
bundle_id,
82-
FromBundle {
71+
AddBundle {
8372
archetype_id,
8473
bundle_status,
8574
},
@@ -458,6 +447,21 @@ impl Archetypes {
458447
self.archetypes.get_mut(id.index())
459448
}
460449

450+
#[inline]
451+
pub(crate) fn get_2_mut(
452+
&mut self,
453+
a: ArchetypeId,
454+
b: ArchetypeId,
455+
) -> (&mut Archetype, &mut Archetype) {
456+
if a.index() > b.index() {
457+
let (b_slice, a_slice) = self.archetypes.split_at_mut(a.index());
458+
(&mut a_slice[0], &mut b_slice[b.index()])
459+
} else {
460+
let (a_slice, b_slice) = self.archetypes.split_at_mut(b.index());
461+
(&mut a_slice[a.index()], &mut b_slice[0])
462+
}
463+
}
464+
461465
#[inline]
462466
pub fn iter(&self) -> impl Iterator<Item = &Archetype> {
463467
self.archetypes.iter()

crates/bevy_ecs/src/bundle.rs

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -159,21 +159,7 @@ impl BundleInfo {
159159
}
160160
StorageType::SparseSet => {
161161
let sparse_set = sparse_sets.get_mut(component_id).unwrap();
162-
match component_status {
163-
ComponentStatus::Added => {
164-
sparse_set.insert(
165-
entity,
166-
component_ptr,
167-
ComponentTicks::new(change_tick),
168-
);
169-
}
170-
ComponentStatus::Mutated => {
171-
sparse_set
172-
.get_ticks(entity)
173-
.unwrap()
174-
.set_changed(change_tick);
175-
}
176-
}
162+
sparse_set.insert(entity, component_ptr, change_tick);
177163
}
178164
}
179165
bundle_component += 1;

crates/bevy_ecs/src/storage/sparse_set.rs

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -119,23 +119,18 @@ impl ComponentSparseSet {
119119
/// # Safety
120120
/// The `value` pointer must point to a valid address that matches the `Layout`
121121
/// inside the `ComponentInfo` given when constructing this sparse set.
122-
pub unsafe fn insert(
123-
&mut self,
124-
entity: Entity,
125-
value: *mut u8,
126-
component_ticks: ComponentTicks,
127-
) {
122+
pub unsafe fn insert(&mut self, entity: Entity, value: *mut u8, change_tick: u32) {
128123
let dense = &mut self.dense;
129124
let entities = &mut self.entities;
130125
let ticks_list = self.ticks.get_mut();
131126
let dense_index = *self.sparse.get_or_insert_with(entity, move || {
132-
ticks_list.push(component_ticks);
127+
ticks_list.push(ComponentTicks::new(change_tick));
133128
entities.push(entity);
134129
dense.push_uninit()
135130
});
136131
// SAFE: dense_index exists thanks to the call above
137132
self.dense.set_unchecked(dense_index, value);
138-
*(*self.ticks.get()).get_unchecked_mut(dense_index) = component_ticks;
133+
((*self.ticks.get()).get_unchecked_mut(dense_index)).set_changed(change_tick);
139134
}
140135

141136
#[inline]

crates/bevy_ecs/src/world/entity_ref.rs

Lines changed: 32 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ impl<'w> EntityMut<'w> {
195195
let bundle_info = self.world.bundles.init_info::<T>(components);
196196
let current_location = self.location;
197197

198-
let new_location = unsafe {
198+
let (archetype, bundle_status, archetype_index) = unsafe {
199199
// SAFE: component ids in `bundle_info` and self.location are valid
200200
let new_archetype_id = add_bundle_to_archetype(
201201
archetypes,
@@ -205,61 +205,66 @@ impl<'w> EntityMut<'w> {
205205
bundle_info,
206206
);
207207
if new_archetype_id == current_location.archetype_id {
208-
current_location
208+
let archetype = &archetypes[current_location.archetype_id];
209+
let edge = archetype.edges().get_add_bundle(bundle_info.id).unwrap();
210+
(archetype, &edge.bundle_status, current_location.index)
209211
} else {
210-
let old_table_row;
211-
let old_table_id;
212-
{
212+
let (old_table_row, old_table_id) = {
213213
let old_archetype = &mut archetypes[current_location.archetype_id];
214214
let result = old_archetype.swap_remove(current_location.index);
215215
if let Some(swapped_entity) = result.swapped_entity {
216-
// SAFE: entity is live and is contained in an archetype that exists
217216
entities.meta[swapped_entity.id as usize].location = current_location;
218217
}
219-
old_table_row = result.table_row;
220-
old_table_id = old_archetype.table_id()
221-
}
222-
let new_archetype = &mut archetypes[new_archetype_id];
218+
(result.table_row, old_archetype.table_id())
219+
};
220+
221+
let new_table_id = archetypes[new_archetype_id].table_id();
223222

224-
if old_table_id == new_archetype.table_id() {
225-
new_archetype.allocate(entity, old_table_row)
223+
let new_location = if old_table_id == new_table_id {
224+
archetypes[new_archetype_id].allocate(entity, old_table_row)
226225
} else {
227-
let (old_table, new_table) = storages
228-
.tables
229-
.get_2_mut(old_table_id, new_archetype.table_id());
226+
let (old_table, new_table) =
227+
storages.tables.get_2_mut(old_table_id, new_table_id);
230228
// PERF: store "non bundle" components in edge, then just move those to avoid
231229
// redundant copies
232230
let move_result =
233231
old_table.move_to_superset_unchecked(old_table_row, new_table);
234232

235-
let new_location = new_archetype.allocate(entity, move_result.new_row);
233+
let new_location =
234+
archetypes[new_archetype_id].allocate(entity, move_result.new_row);
236235
// if an entity was moved into this entity's table spot, update its table row
237236
if let Some(swapped_entity) = move_result.swapped_entity {
238237
let swapped_location = entities.get(swapped_entity).unwrap();
239238
archetypes[swapped_location.archetype_id]
240239
.set_entity_table_row(swapped_location.index, old_table_row);
241240
}
242241
new_location
243-
}
242+
};
243+
244+
self.location = new_location;
245+
entities.meta[self.entity.id as usize].location = new_location;
246+
let (old_archetype, new_archetype) =
247+
archetypes.get_2_mut(current_location.archetype_id, new_archetype_id);
248+
let edge = old_archetype
249+
.edges()
250+
.get_add_bundle(bundle_info.id)
251+
.unwrap();
252+
(&*new_archetype, &edge.bundle_status, new_location.index)
244253

245254
// Sparse set components are intentionally ignored here. They don't need to move
246255
}
247256
};
248-
self.location = new_location;
249-
entities.meta[self.entity.id as usize].location = new_location;
250257

251-
let archetype = &archetypes[new_location.archetype_id];
252258
let table = &storages.tables[archetype.table_id()];
253-
let table_row = archetype.entity_table_row(new_location.index);
254-
let from_bundle = archetype.edges().get_from_bundle(bundle_info.id).unwrap();
259+
let table_row = archetype.entity_table_row(archetype_index);
255260
// SAFE: table row is valid
256261
unsafe {
257262
bundle_info.write_components(
258263
&mut storages.sparse_sets,
259264
entity,
260265
table,
261266
table_row,
262-
&from_bundle.bundle_status,
267+
bundle_status,
263268
bundle,
264269
change_tick,
265270
)
@@ -650,11 +655,11 @@ pub(crate) unsafe fn add_bundle_to_archetype(
650655
archetype_id: ArchetypeId,
651656
bundle_info: &BundleInfo,
652657
) -> ArchetypeId {
653-
if let Some(archetype_id) = archetypes[archetype_id]
658+
if let Some(add_bundle) = archetypes[archetype_id]
654659
.edges()
655660
.get_add_bundle(bundle_info.id)
656661
{
657-
return archetype_id;
662+
return add_bundle.archetype_id;
658663
}
659664
let mut new_table_components = Vec::new();
660665
let mut new_sparse_set_components = Vec::new();
@@ -680,8 +685,7 @@ pub(crate) unsafe fn add_bundle_to_archetype(
680685
if new_table_components.is_empty() && new_sparse_set_components.is_empty() {
681686
let edges = current_archetype.edges_mut();
682687
// the archetype does not change when we add this bundle
683-
edges.set_add_bundle(bundle_info.id, archetype_id);
684-
edges.set_from_bundle(bundle_info.id, archetype_id, bundle_status);
688+
edges.set_add_bundle(bundle_info.id, archetype_id, bundle_status);
685689
archetype_id
686690
} else {
687691
let table_id;
@@ -718,11 +722,7 @@ pub(crate) unsafe fn add_bundle_to_archetype(
718722
let new_archetype_id =
719723
archetypes.get_id_or_insert(table_id, table_components, sparse_set_components);
720724
// add an edge from the old archetype to the new archetype
721-
archetypes[archetype_id]
722-
.edges_mut()
723-
.set_add_bundle(bundle_info.id, new_archetype_id);
724-
// add a "from bundle" edge from the new archetype to the old archetype
725-
archetypes[new_archetype_id].edges_mut().set_from_bundle(
725+
archetypes[archetype_id].edges_mut().set_add_bundle(
726726
bundle_info.id,
727727
new_archetype_id,
728728
bundle_status,

crates/bevy_ecs/src/world/spawn_batch.rs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::{
2-
archetype::{Archetype, ArchetypeId},
2+
archetype::{Archetype, ArchetypeId, ComponentStatus},
33
bundle::{Bundle, BundleInfo},
44
entity::{Entities, Entity},
55
storage::{SparseSets, Table},
@@ -17,6 +17,7 @@ where
1717
table: &'w mut Table,
1818
sparse_sets: &'w mut SparseSets,
1919
bundle_info: &'w BundleInfo,
20+
bundle_status: &'w [ComponentStatus],
2021
change_tick: u32,
2122
}
2223

@@ -46,11 +47,17 @@ where
4647
bundle_info,
4748
)
4849
};
49-
let archetype = &mut world.archetypes[archetype_id];
50+
let (empty_archetype, archetype) = world
51+
.archetypes
52+
.get_2_mut(ArchetypeId::empty(), archetype_id);
5053
let table = &mut world.storages.tables[archetype.table_id()];
5154
archetype.reserve(length);
5255
table.reserve(length);
5356
world.entities.reserve(length as u32);
57+
let edge = empty_archetype
58+
.edges()
59+
.get_add_bundle(bundle_info.id())
60+
.unwrap();
5461
Self {
5562
inner: iter,
5663
entities: &mut world.entities,
@@ -59,6 +66,7 @@ where
5966
sparse_sets: &mut world.storages.sparse_sets,
6067
bundle_info,
6168
change_tick: *world.change_tick.get_mut(),
69+
bundle_status: &edge.bundle_status,
6270
}
6371
}
6472
}
@@ -88,17 +96,12 @@ where
8896
unsafe {
8997
let table_row = self.table.allocate(entity);
9098
let location = self.archetype.allocate(entity, table_row);
91-
let from_bundle = self
92-
.archetype
93-
.edges()
94-
.get_from_bundle(self.bundle_info.id)
95-
.unwrap();
9699
self.bundle_info.write_components(
97100
self.sparse_sets,
98101
entity,
99102
self.table,
100103
table_row,
101-
&from_bundle.bundle_status,
104+
self.bundle_status,
102105
bundle,
103106
self.change_tick,
104107
);

0 commit comments

Comments
 (0)