Skip to content

Commit ce43395

Browse files
authored
Don't relocate the meshes when mesh slabs grow. (#17793)
Currently, when a mesh slab overflows, we recreate the allocator and reinsert all the meshes that were in it in an arbitrary order. This can result in the meshes moving around. Before `MeshInputUniform`s were retained, this was slow but harmless, because the `MeshInputUniform` that contained the positions of the vertex and index data in the slab would be recreated every frame. However, with mesh retention, there's no guarantee that the `MeshInputUniform`, which could be cached from the previous frame, will reflect the new position of the mesh data within the buffer if that buffer happened to grow. This manifested itself as seeming mesh data corruption when adding many meshes dynamically to the scene. There are three possible ways that I could have fixed this that I can see: 1. Invalidate and rebuild all the `MeshInputUniform`s belonging to all meshes in a slab when that mesh grows. 2. Introduce a second layer of indirection so that the `MeshInputUniform` points to a *mesh allocation table* that contains the current locations of the data of each mesh. 3. Avoid moving meshes when reallocating the buffer. To be efficient, option (1) would require scanning meshes to see if their positions changed, a la `mark_meshes_as_changed_if_their_materials_changed`. Option (2) would add more runtime indirection and would require additional bookkeeping on the part of the allocator. Therefore, this PR chooses option (3), which was remarkably simple to implement. The key is that the offset allocator happens to allocate addresses from low addresses to high addresses. So all we have to do is to *conceptually* allocate the full 512 MiB mesh slab as far as the offset allocator is concerned, and grow the underlying backing store from 1 MiB to 512 MiB as needed. In other words, the allocator now allocates *virtual* GPU memory, and the actual backing slab resizes to fit the virtual memory. This ensures that the location of mesh data remains constant for the lifetime of the mesh asset, and we can remove the code that reinserts meshes one by one when the slab grows in favor of a single buffer copy. Closes #17766.
1 parent 7a62a4f commit ce43395

File tree

1 file changed

+95
-111
lines changed

1 file changed

+95
-111
lines changed

crates/bevy_render/src/mesh/allocator.rs

Lines changed: 95 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use bevy_ecs::{
1616
system::{Res, ResMut},
1717
world::{FromWorld, World},
1818
};
19-
use bevy_platform_support::collections::{HashMap, HashSet};
19+
use bevy_platform_support::collections::{hash_map::Entry, HashMap, HashSet};
2020
use bevy_utils::default;
2121
use offset_allocator::{Allocation, Allocator};
2222
use tracing::error;
@@ -196,7 +196,7 @@ struct GeneralSlab {
196196
element_layout: ElementLayout,
197197

198198
/// The size of this slab in slots.
199-
slot_capacity: u32,
199+
current_slot_capacity: u32,
200200
}
201201

202202
/// A slab that contains a single object.
@@ -224,6 +224,18 @@ enum ElementClass {
224224
Index,
225225
}
226226

227+
/// The results of [`GeneralSlab::grow_if_necessary`].
228+
enum SlabGrowthResult {
229+
/// The mesh data already fits in the slab; the slab doesn't need to grow.
230+
NoGrowthNeeded,
231+
/// The slab needed to grow.
232+
///
233+
/// The [`SlabToReallocate`] contains the old capacity of the slab.
234+
NeededGrowth(SlabToReallocate),
235+
/// The slab wanted to grow but couldn't because it hit its maximum size.
236+
CantGrow,
237+
}
238+
227239
/// Information about the size of individual elements (vertices or indices)
228240
/// within a slab.
229241
///
@@ -278,9 +290,8 @@ struct SlabsToReallocate(HashMap<SlabId, SlabToReallocate>);
278290
/// reallocated.
279291
#[derive(Default)]
280292
struct SlabToReallocate {
281-
/// Maps all allocations that need to be relocated to their positions within
282-
/// the *new* slab.
283-
allocations_to_copy: HashMap<AssetId<Mesh>, SlabAllocation>,
293+
/// The capacity of the slab before we decided to grow it.
294+
old_slot_capacity: u32,
284295
}
285296

286297
impl Display for SlabId {
@@ -694,32 +705,39 @@ impl MeshAllocator {
694705
// and try to allocate the mesh inside them. We go with the first one
695706
// that succeeds.
696707
let mut mesh_allocation = None;
697-
'slab: for &slab_id in &*candidate_slabs {
698-
loop {
699-
let Some(Slab::General(ref mut slab)) = self.slabs.get_mut(&slab_id) else {
700-
unreachable!("Slab not found")
701-
};
708+
for &slab_id in &*candidate_slabs {
709+
let Some(Slab::General(ref mut slab)) = self.slabs.get_mut(&slab_id) else {
710+
unreachable!("Slab not found")
711+
};
702712

703-
if let Some(allocation) = slab.allocator.allocate(data_slot_count) {
704-
mesh_allocation = Some(MeshAllocation {
705-
slab_id,
706-
slab_allocation: SlabAllocation {
707-
allocation,
708-
slot_count: data_slot_count,
709-
},
710-
});
711-
break 'slab;
712-
}
713+
let Some(allocation) = slab.allocator.allocate(data_slot_count) else {
714+
continue;
715+
};
713716

714-
// Try to grow the slab. If this fails, the slab is full; go on
715-
// to the next slab.
716-
match slab.try_grow(settings) {
717-
Ok(new_mesh_allocation_records) => {
718-
slabs_to_grow.insert(slab_id, new_mesh_allocation_records);
717+
// Try to fit the object in the slab, growing if necessary.
718+
match slab.grow_if_necessary(allocation.offset + data_slot_count, settings) {
719+
SlabGrowthResult::NoGrowthNeeded => {}
720+
SlabGrowthResult::NeededGrowth(slab_to_reallocate) => {
721+
// If we already grew the slab this frame, don't replace the
722+
// `SlabToReallocate` entry. We want to keep the entry
723+
// corresponding to the size that the slab had at the start
724+
// of the frame, so that we can copy only the used portion
725+
// of the initial buffer to the new one.
726+
if let Entry::Vacant(vacant_entry) = slabs_to_grow.entry(slab_id) {
727+
vacant_entry.insert(slab_to_reallocate);
719728
}
720-
Err(()) => continue 'slab,
721729
}
730+
SlabGrowthResult::CantGrow => continue,
722731
}
732+
733+
mesh_allocation = Some(MeshAllocation {
734+
slab_id,
735+
slab_allocation: SlabAllocation {
736+
allocation,
737+
slot_count: data_slot_count,
738+
},
739+
});
740+
break;
723741
}
724742

725743
// If we still have no allocation, make a new slab.
@@ -774,10 +792,11 @@ impl MeshAllocator {
774792

775793
/// Reallocates a slab that needs to be resized, or allocates a new slab.
776794
///
777-
/// This performs the actual growth operation that [`GeneralSlab::try_grow`]
778-
/// scheduled. We do the growth in two phases so that, if a slab grows
779-
/// multiple times in the same frame, only one new buffer is reallocated,
780-
/// rather than reallocating the buffer multiple times.
795+
/// This performs the actual growth operation that
796+
/// [`GeneralSlab::grow_if_necessary`] scheduled. We do the growth in two
797+
/// phases so that, if a slab grows multiple times in the same frame, only
798+
/// one new buffer is reallocated, rather than reallocating the buffer
799+
/// multiple times.
781800
fn reallocate_slab(
782801
&mut self,
783802
render_device: &RenderDevice,
@@ -805,38 +824,28 @@ impl MeshAllocator {
805824
slab_id,
806825
buffer_usages_to_str(buffer_usages)
807826
)),
808-
size: slab.slot_capacity as u64 * slab.element_layout.slot_size(),
827+
size: slab.current_slot_capacity as u64 * slab.element_layout.slot_size(),
809828
usage: buffer_usages,
810829
mapped_at_creation: false,
811830
});
812831

813832
slab.buffer = Some(new_buffer.clone());
814833

834+
let Some(old_buffer) = old_buffer else { return };
835+
815836
// In order to do buffer copies, we need a command encoder.
816837
let mut encoder = render_device.create_command_encoder(&CommandEncoderDescriptor {
817838
label: Some("slab resize encoder"),
818839
});
819840

820-
// If we have no objects to copy over, we're done.
821-
let Some(old_buffer) = old_buffer else {
822-
return;
823-
};
824-
825-
for (mesh_id, src_slab_allocation) in &mut slab.resident_allocations {
826-
let Some(dest_slab_allocation) = slab_to_grow.allocations_to_copy.get(mesh_id) else {
827-
continue;
828-
};
829-
830-
encoder.copy_buffer_to_buffer(
831-
&old_buffer,
832-
src_slab_allocation.allocation.offset as u64 * slab.element_layout.slot_size(),
833-
&new_buffer,
834-
dest_slab_allocation.allocation.offset as u64 * slab.element_layout.slot_size(),
835-
dest_slab_allocation.slot_count as u64 * slab.element_layout.slot_size(),
836-
);
837-
// Now that we've done the copy, we can update the allocation record.
838-
*src_slab_allocation = dest_slab_allocation.clone();
839-
}
841+
// Copy the data from the old buffer into the new one.
842+
encoder.copy_buffer_to_buffer(
843+
&old_buffer,
844+
0,
845+
&new_buffer,
846+
0,
847+
slab_to_grow.old_slot_capacity as u64 * slab.element_layout.slot_size(),
848+
);
840849

841850
let command_buffer = encoder.finish();
842851
render_queue.submit([command_buffer]);
@@ -872,16 +881,19 @@ impl GeneralSlab {
872881
layout: ElementLayout,
873882
data_slot_count: u32,
874883
) -> GeneralSlab {
875-
let slab_slot_capacity = (settings.min_slab_size.div_ceil(layout.slot_size()) as u32)
884+
let initial_slab_slot_capacity = (settings.min_slab_size.div_ceil(layout.slot_size())
885+
as u32)
886+
.max(offset_allocator::ext::min_allocator_size(data_slot_count));
887+
let max_slab_slot_capacity = (settings.max_slab_size.div_ceil(layout.slot_size()) as u32)
876888
.max(offset_allocator::ext::min_allocator_size(data_slot_count));
877889

878890
let mut new_slab = GeneralSlab {
879-
allocator: Allocator::new(slab_slot_capacity),
891+
allocator: Allocator::new(max_slab_slot_capacity),
880892
buffer: None,
881893
resident_allocations: HashMap::default(),
882894
pending_allocations: HashMap::default(),
883895
element_layout: layout,
884-
slot_capacity: slab_slot_capacity,
896+
current_slot_capacity: initial_slab_slot_capacity,
885897
};
886898

887899
// This should never fail.
@@ -898,68 +910,40 @@ impl GeneralSlab {
898910
new_slab
899911
}
900912

901-
/// Attempts to grow a slab that's just run out of space.
913+
/// Checks to see if the size of this slab is at least `new_size_in_slots`
914+
/// and grows the slab if it isn't.
902915
///
903-
/// Returns a structure the allocations that need to be relocated if the
904-
/// growth succeeded. If the slab is full, returns `Err`.
905-
fn try_grow(&mut self, settings: &MeshAllocatorSettings) -> Result<SlabToReallocate, ()> {
906-
// In extremely rare cases due to allocator fragmentation, it may happen
907-
// that we fail to re-insert every object that was in the slab after
908-
// growing it. Even though this will likely never happen, we use this
909-
// loop to handle this unlikely event properly if it does.
910-
'grow: loop {
911-
let new_slab_slot_capacity = ((self.slot_capacity as f64 * settings.growth_factor)
912-
.ceil() as u32)
913-
.min((settings.max_slab_size / self.element_layout.slot_size()) as u32);
914-
if new_slab_slot_capacity == self.slot_capacity {
915-
// The slab is full.
916-
return Err(());
917-
}
918-
919-
// Grow the slab.
920-
self.allocator = Allocator::new(new_slab_slot_capacity);
921-
self.slot_capacity = new_slab_slot_capacity;
922-
923-
let mut slab_to_grow = SlabToReallocate::default();
924-
925-
// Place every resident allocation that was in the old slab in the
926-
// new slab.
927-
for (allocated_mesh_id, old_allocation_range) in &self.resident_allocations {
928-
let allocation_size = old_allocation_range.slot_count;
929-
match self.allocator.allocate(allocation_size) {
930-
Some(allocation) => {
931-
slab_to_grow.allocations_to_copy.insert(
932-
*allocated_mesh_id,
933-
SlabAllocation {
934-
allocation,
935-
slot_count: allocation_size,
936-
},
937-
);
938-
}
939-
None => {
940-
// We failed to insert one of the allocations that we
941-
// had before.
942-
continue 'grow;
943-
}
944-
}
945-
}
916+
/// The returned [`SlabGrowthResult`] describes whether the slab needed to
917+
/// grow and whether, if so, it was successful in doing so.
918+
fn grow_if_necessary(
919+
&mut self,
920+
new_size_in_slots: u32,
921+
settings: &MeshAllocatorSettings,
922+
) -> SlabGrowthResult {
923+
// Is the slab big enough already?
924+
let initial_slot_capacity = self.current_slot_capacity;
925+
if self.current_slot_capacity >= new_size_in_slots {
926+
return SlabGrowthResult::NoGrowthNeeded;
927+
}
946928

947-
// Move every allocation that was pending in the old slab to the new
948-
// slab.
949-
for slab_allocation in self.pending_allocations.values_mut() {
950-
let allocation_size = slab_allocation.slot_count;
951-
match self.allocator.allocate(allocation_size) {
952-
Some(allocation) => slab_allocation.allocation = allocation,
953-
None => {
954-
// We failed to insert one of the allocations that we
955-
// had before.
956-
continue 'grow;
957-
}
958-
}
929+
// Try to grow in increments of `MeshAllocatorSettings::growth_factor`
930+
// until we're big enough.
931+
while self.current_slot_capacity < new_size_in_slots {
932+
let new_slab_slot_capacity =
933+
((self.current_slot_capacity as f64 * settings.growth_factor).ceil() as u32)
934+
.min((settings.max_slab_size / self.element_layout.slot_size()) as u32);
935+
if new_slab_slot_capacity == self.current_slot_capacity {
936+
// The slab is full.
937+
return SlabGrowthResult::CantGrow;
959938
}
960939

961-
return Ok(slab_to_grow);
940+
self.current_slot_capacity = new_slab_slot_capacity;
962941
}
942+
943+
// Tell our caller what we did.
944+
SlabGrowthResult::NeededGrowth(SlabToReallocate {
945+
old_slot_capacity: initial_slot_capacity,
946+
})
963947
}
964948
}
965949

0 commit comments

Comments
 (0)