Skip to content

Commit 3c13c75

Browse files
TheRawMeatballcart
andcommitted
Optimize rendering slow-down at high entity counts (#5509)
# Objective - Improve #3953 ## Solution - The very specific circumstances under which the render world is reset meant that the flush_as_invalid function could be replaced with one that had a noop as its init method. - This removes a double-writing issue leading to greatly increased performance. Running the reproduction code in the linked issue, this change nearly doubles the framerate. Co-authored-by: Carter Anderson <[email protected]>
1 parent 3689d5d commit 3c13c75

File tree

3 files changed

+39
-9
lines changed

3 files changed

+39
-9
lines changed

crates/bevy_ecs/src/archetype.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,14 @@ use std::{
1414
};
1515

1616
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
17+
#[repr(transparent)]
1718
pub struct ArchetypeId(usize);
1819

1920
impl ArchetypeId {
2021
pub const EMPTY: ArchetypeId = ArchetypeId(0);
22+
/// # Safety:
23+
///
24+
/// This must always have an all-1s bit pattern to ensure soundness in fast entity id space allocation.
2125
pub const INVALID: ArchetypeId = ArchetypeId(usize::MAX);
2226

2327
#[inline]

crates/bevy_ecs/src/entity/mod.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,9 @@ impl Entities {
563563
/// Flush _must_ set the entity location to the correct [`ArchetypeId`] for the given [`Entity`]
564564
/// each time init is called. This _can_ be [`ArchetypeId::INVALID`], provided the [`Entity`]
565565
/// has not been assigned to an [`Archetype`][crate::archetype::Archetype].
566+
///
567+
/// Note: freshly-allocated entities (ones which don't come from the pending list) are guaranteed
568+
/// to be initialized with the invalid archetype.
566569
pub unsafe fn flush(&mut self, mut init: impl FnMut(Entity, &mut EntityLocation)) {
567570
let free_cursor = self.free_cursor.get_mut();
568571
let current_free_cursor = *free_cursor;
@@ -613,6 +616,20 @@ impl Entities {
613616
}
614617
}
615618

619+
/// # Safety
620+
///
621+
/// This function is safe if and only if the world this Entities is on has no entities.
622+
pub unsafe fn flush_and_reserve_invalid_assuming_no_entities(&mut self, count: usize) {
623+
let free_cursor = self.free_cursor.get_mut();
624+
*free_cursor = 0;
625+
self.meta.reserve(count);
626+
// the EntityMeta struct only contains integers, and it is valid to have all bytes set to u8::MAX
627+
self.meta.as_mut_ptr().write_bytes(u8::MAX, count);
628+
self.meta.set_len(count);
629+
630+
self.len = count as u32;
631+
}
632+
616633
/// Accessor for getting the length of the vec in `self.meta`
617634
#[inline]
618635
pub fn meta_len(&self) -> usize {
@@ -630,7 +647,10 @@ impl Entities {
630647
}
631648
}
632649

650+
// Safety:
651+
// This type must not contain any pointers at any level, and be safe to fully fill with u8::MAX.
633652
#[derive(Copy, Clone, Debug)]
653+
#[repr(C)]
634654
pub struct EntityMeta {
635655
pub generation: u32,
636656
pub location: EntityLocation,
@@ -648,6 +668,7 @@ impl EntityMeta {
648668

649669
/// A location of an entity in an archetype.
650670
#[derive(Copy, Clone, Debug)]
671+
#[repr(C)]
651672
pub struct EntityLocation {
652673
/// The archetype index
653674
pub archetype_id: ArchetypeId,

crates/bevy_render/src/lib.rs

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -244,15 +244,20 @@ impl Plugin for RenderPlugin {
244244
// reserve all existing app entities for use in render_app
245245
// they can only be spawned using `get_or_spawn()`
246246
let meta_len = app_world.entities().meta_len();
247-
render_app
248-
.world
249-
.entities()
250-
.reserve_entities(meta_len as u32);
251-
252-
// flushing as "invalid" ensures that app world entities aren't added as "empty archetype" entities by default
253-
// these entities cannot be accessed without spawning directly onto them
254-
// this _only_ works as expected because clear_entities() is called at the end of every frame.
255-
unsafe { render_app.world.entities_mut() }.flush_as_invalid();
247+
248+
assert_eq!(
249+
render_app.world.entities().len(),
250+
0,
251+
"An entity was spawned after the entity list was cleared last frame and before the extract stage began. This is not supported",
252+
);
253+
254+
// This is safe given the clear_entities call in the past frame and the assert above
255+
unsafe {
256+
render_app
257+
.world
258+
.entities_mut()
259+
.flush_and_reserve_invalid_assuming_no_entities(meta_len);
260+
}
256261
}
257262

258263
{

0 commit comments

Comments
 (0)