concinnity_render/draw_slot.rs
1//! Free-list allocator for backend draw-object slots. A backend appends draw
2//! objects into a single `Vec` and stores raw indices into it on each entity's
3//! RenderHandle, so a despawned object's slot cannot be compacted away without
4//! invalidating every later index. Instead the allocator hands out a vacated
5//! slot before growing the vec: `retire` pushes a freed index, the next runtime
6//! spawn pops it. Streamed chunks were the first consumer (one freed chunk's
7//! slot reused by the next); runtime entity spawn/despawn is the second. All
8//! three backends (Metal, DirectX, Vulkan) route their draw-slot allocation
9//! through this.
10
11use alloc::vec::Vec;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14/// Where a newly allocated draw record lands.
15pub enum SlotAlloc {
16 /// Reuse this vacated slot: overwrite the existing draw_objects entry.
17 Reuse(usize),
18 /// No free slot was available: append at this index (== the prior length).
19 Append(usize),
20}
21
22#[derive(Debug, Default)]
23/// Hands out draw-record slots, reusing vacated ones before growing.
24pub struct DrawSlotAllocator {
25 free: Vec<usize>,
26 len: usize,
27}
28
29impl DrawSlotAllocator {
30 /// Start with `len` slots already in use (the draw objects built at init).
31 pub fn with_len(len: usize) -> Self {
32 Self {
33 free: Vec::new(),
34 len,
35 }
36 }
37
38 /// Hand out a slot: a vacated one if any is free, else the next new index.
39 /// The caller writes its draw object at the returned slot and, on Append,
40 /// grows whatever side tables run parallel to draw_objects.
41 pub fn allocate(&mut self) -> SlotAlloc {
42 if let Some(slot) = self.free.pop() {
43 SlotAlloc::Reuse(slot)
44 } else {
45 let idx = self.len;
46 self.len += 1;
47 SlotAlloc::Append(idx)
48 }
49 }
50
51 /// Return a slot to the free list for a later allocate to reuse.
52 pub fn free(&mut self, slot: usize) {
53 self.free.push(slot);
54 }
55}
56
57#[cfg(test)]
58mod tests {
59 use super::*;
60
61 #[test]
62 fn appends_past_initial_len_then_reuses_freed_slots() {
63 let mut alloc = DrawSlotAllocator::with_len(3);
64 // No free slots yet: allocation appends past the initial three.
65 assert_eq!(alloc.allocate(), SlotAlloc::Append(3));
66 assert_eq!(alloc.allocate(), SlotAlloc::Append(4));
67
68 // Freeing a slot makes the next allocation reuse it instead of growing.
69 alloc.free(3);
70 assert_eq!(alloc.allocate(), SlotAlloc::Reuse(3));
71
72 // The reuse did not advance the high-water mark: with the free list
73 // empty again, allocation resumes appending at 5 (not 6).
74 assert_eq!(alloc.allocate(), SlotAlloc::Append(5));
75 }
76
77 #[test]
78 fn freed_slots_pop_in_lifo_order() {
79 let mut alloc = DrawSlotAllocator::with_len(10);
80 alloc.free(4);
81 alloc.free(7);
82 assert_eq!(alloc.allocate(), SlotAlloc::Reuse(7));
83 assert_eq!(alloc.allocate(), SlotAlloc::Reuse(4));
84 assert_eq!(alloc.allocate(), SlotAlloc::Append(10));
85 }
86}