Skip to main content

concinnity_core/render/
skinned_pool.rs

1//! Free pool for pre-reserved skinned instance slots. A skinned mesh that opts
2//! into runtime spawning (SkinnedMesh.max_instances > 0) has that many hidden
3//! bind-pose copies appended to the skinned geometry at load. Each copy is its
4//! own skinned draw object with its own vertex region in the shared skinned
5//! buffer, which is required because the GPU skin fold writes the deformed
6//! buffer keyed by global vertex index: two live instances sharing a region
7//! would clobber each other's pose. This pool tracks, per template, which of
8//! those copies are currently free so a spawn can claim one and a despawn can
9//! return it. Slot indices are stable skinned-draw-object indices; nothing is
10//! compacted, so the per-frame skinned arrays that parallel them stay valid.
11//!
12//! Built and consumed by every graphics backend's runtime skinned-spawn path
13//! (Metal, DirectX, Vulkan).
14
15use alloc::vec::Vec;
16use hashbrown::HashMap;
17
18#[derive(Debug, Default)]
19/// Recycles skinned draw slots as skinned instances spawn and despawn.
20pub struct SkinnedInstancePool {
21    // template skinned-draw-object index -> its currently free instance slots.
22    free: HashMap<usize, Vec<usize>>,
23    // instance slot -> the template it belongs to, so `release` returns it to
24    // the right pool. Set once at `reserve` and never changed (a copy always
25    // belongs to the template it was expanded from).
26    owner: HashMap<usize, usize>,
27}
28
29impl SkinnedInstancePool {
30    /// An empty pool.
31    pub fn new() -> Self {
32        Self::default()
33    }
34
35    /// Record a pre-reserved instance slot as free and owned by `template`.
36    /// Called once per expanded copy at load.
37    pub fn reserve(&mut self, template: usize, instance: usize) {
38        self.owner.insert(instance, template);
39        self.free.entry(template).or_default().push(instance);
40    }
41
42    /// Claim a free instance slot for `template`, or `None` when the reserve is
43    /// exhausted (more live copies than were pre-reserved).
44    pub fn acquire(&mut self, template: usize) -> Option<usize> {
45        self.free.get_mut(&template).and_then(|slots| slots.pop())
46    }
47
48    /// Return a live instance slot to its template's free list. Returns false if
49    /// the slot was never a pre-reserved instance (e.g. an authored template
50    /// slot), so the caller can tell a recyclable slot from a fixed one.
51    pub fn release(&mut self, instance: usize) -> bool {
52        let Some(&template) = self.owner.get(&instance) else {
53            return false;
54        };
55        self.free.entry(template).or_default().push(instance);
56        true
57    }
58
59    /// Total free slots across every template. Surfaced through the debug
60    /// profile so a probe can watch the pool drain on spawn and refill on
61    /// despawn, a direct check on the free-list recycle.
62    pub fn total_free(&self) -> usize {
63        self.free.values().map(Vec::len).sum()
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn acquire_then_release_recycles_the_same_slot() {
73        let mut pool = SkinnedInstancePool::new();
74        // Template 0 owns two pre-reserved copies: slots 1 and 2.
75        pool.reserve(0, 1);
76        pool.reserve(0, 2);
77        assert_eq!(pool.total_free(), 2);
78
79        let a = pool.acquire(0).expect("first claim");
80        let b = pool.acquire(0).expect("second claim");
81        assert!(pool.acquire(0).is_none(), "reserve exhausted");
82        assert_eq!(pool.total_free(), 0);
83
84        // Releasing a claimed slot makes it available again, and the next claim
85        // hands it back out instead of growing.
86        assert!(pool.release(a));
87        assert_eq!(pool.total_free(), 1);
88        let reused = pool.acquire(0).expect("reuse after release");
89        assert_eq!(reused, a, "a freed instance slot is recycled");
90        let _ = b;
91    }
92
93    #[test]
94    fn slots_return_only_to_their_own_template() {
95        let mut pool = SkinnedInstancePool::new();
96        pool.reserve(0, 10); // template 0
97        pool.reserve(5, 20); // template 5
98        let s0 = pool.acquire(0).unwrap();
99        let s5 = pool.acquire(5).unwrap();
100        assert_eq!((s0, s5), (10, 20));
101        pool.release(s0);
102        pool.release(s5);
103        // Each slot went back to its own template's pool, not the other's.
104        assert_eq!(pool.acquire(0), Some(10));
105        assert_eq!(pool.acquire(5), Some(20));
106    }
107
108    #[test]
109    fn releasing_an_unknown_slot_is_a_clean_false() {
110        let mut pool = SkinnedInstancePool::new();
111        pool.reserve(0, 1);
112        // Slot 99 was never reserved (e.g. an authored template slot): release
113        // reports it is not a pool slot and changes nothing.
114        assert!(!pool.release(99));
115        assert_eq!(pool.total_free(), 1);
116    }
117
118    #[test]
119    fn total_free_sums_across_templates() {
120        let mut pool = SkinnedInstancePool::new();
121        pool.reserve(0, 1);
122        pool.reserve(0, 2);
123        pool.reserve(3, 4);
124        assert_eq!(pool.total_free(), 3);
125        pool.acquire(0);
126        assert_eq!(pool.total_free(), 2);
127    }
128}