Skip to main content

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}