engine/collections/
sparse_array.rs

1// SPDX-FileCopyrightText: 2025 Jens Pitkänen <jens.pitkanen@helsinki.fi>
2//
3// SPDX-License-Identifier: GPL-3.0-or-later
4
5use core::{
6    num::NonZeroU32,
7    sync::atomic::{AtomicU32, Ordering},
8};
9
10use bytemuck::Zeroable;
11
12use crate::{allocators::LinearAllocator, collections::FixedVec};
13
14struct LoadedElementInfo {
15    age: AtomicU32,
16    array_index: u32,
17}
18
19impl LoadedElementInfo {
20    fn new(array_index: u32) -> LoadedElementInfo {
21        LoadedElementInfo {
22            age: AtomicU32::new(0),
23            array_index,
24        }
25    }
26}
27
28/// Sparse array of `T`.
29///
30/// Can operate with less memory than a [`FixedVec`] of the same length, since
31/// the array length and the backing `T` storage have separate capacities.
32/// Useful for cases where stable indices are important, but having the entire
33/// array in-memory is infeasible.
34pub struct SparseArray<'a, T> {
35    /// The indirection array: contains indexes to `loaded_elements` for the
36    /// array elements which are currently loaded.
37    index_map: FixedVec<'a, OptionalU32>,
38    /// Reusable indexes to `loaded_elements`.
39    free_indices: FixedVec<'a, u32>,
40    /// The currently loaded instances of `T`.
41    loaded_elements: FixedVec<'a, T>,
42    /// Ages of loaded elements, each element being the age of the loaded
43    /// element in the same index in `loaded_elements`.
44    ///
45    /// The values are atomic to allow reading from the [`SparseArray`] from
46    /// multiple threads at once, while still being able to maintain the ages of
47    /// the loaded elements.
48    loaded_element_infos: FixedVec<'a, LoadedElementInfo>,
49}
50
51impl<T> SparseArray<'_, T> {
52    /// Creates a new sparse array of `T` with length `array_len`, allowing for
53    /// `loaded_len` elements to be loaded at a time.
54    pub fn new<'a>(
55        allocator: &'a LinearAllocator,
56        array_len: u32,
57        loaded_len: u32,
58    ) -> Option<SparseArray<'a, T>> {
59        let mut index_map = FixedVec::new(allocator, array_len as usize)?;
60        index_map.fill_with_zeroes();
61
62        Some(SparseArray {
63            index_map,
64            free_indices: FixedVec::new(allocator, loaded_len as usize)?,
65            loaded_elements: FixedVec::new(allocator, loaded_len as usize)?,
66            loaded_element_infos: FixedVec::new(allocator, loaded_len as usize)?,
67        })
68    }
69
70    /// Increments the age of each loaded element.
71    ///
72    /// These ages are used to determine which elements get discarded first when
73    /// calling [`SparseArray::insert`]. [`SparseArray::get`] resets the age of
74    /// the returned element.
75    pub fn increment_ages(&mut self) {
76        for info in &mut *self.loaded_element_infos {
77            let age = info.age.get_mut();
78            *age = age.saturating_add(1);
79        }
80    }
81
82    /// Removes the value from the index, freeing space for another
83    /// value to be inserted anywhere.
84    pub fn unload(&mut self, index: u32) {
85        if let Some(loaded_index) = self
86            .index_map
87            .get_mut(index as usize)
88            .and_then(OptionalU32::take)
89        {
90            self.free_indices.push(loaded_index).unwrap();
91        }
92    }
93
94    /// Allocates space for the index, returning a mutable borrow to fill it
95    /// with.
96    ///
97    /// If reusing an old unloaded `T` is not possible, but there's and a new
98    /// `T` needs to be created, `init_fn` is used. `init_fn` can fail by
99    /// returning `None`, in which case nothing else happens and `None` is
100    /// returned.
101    ///
102    /// If the backing memory is full, the least recently used `T` is assigned
103    /// to this index and returned, implicitly "unloading the value at its old
104    /// index."
105    ///
106    /// If the backing memory is full, and every `T` has been used since the
107    /// last call to [`SparseArray::increment_ages`], this returns `None`.
108    pub fn insert(&mut self, index: u32, init_fn: impl FnOnce() -> Option<T>) -> Option<&mut T> {
109        let now_loaded_index = if let Some(unloaded_index) = self.free_indices.pop() {
110            unloaded_index
111        } else if self.loaded_elements.is_full() {
112            let mut least_recent_age = 0;
113            let mut least_recent_loaded_index = None;
114            for (i, info) in self.loaded_element_infos.iter_mut().enumerate() {
115                let age = *info.age.get_mut();
116                if age > least_recent_age {
117                    least_recent_age = age;
118                    least_recent_loaded_index = Some(i as u32);
119                }
120            }
121            let least_recent_loaded_index = least_recent_loaded_index?;
122
123            let info = &mut self.loaded_element_infos[least_recent_loaded_index as usize];
124            self.index_map[info.array_index as usize].take();
125            *info = LoadedElementInfo::new(index);
126
127            least_recent_loaded_index
128        } else {
129            let new_data = init_fn()?;
130            let new_loaded_index = self.loaded_elements.len() as u32;
131            self.loaded_element_infos
132                .push(LoadedElementInfo::new(index))
133                .ok()
134                .unwrap();
135            self.loaded_elements.push(new_data).ok().unwrap();
136            new_loaded_index
137        };
138        self.index_map[index as usize].set(now_loaded_index);
139        Some(&mut self.loaded_elements[now_loaded_index as usize])
140    }
141
142    /// Returns the value at the index if it's loaded.
143    pub fn get(&self, index: u32) -> Option<&T> {
144        let loaded_index = self.index_map[index as usize].get()? as usize;
145        self.loaded_element_infos[loaded_index]
146            .age
147            .store(0, Ordering::Release);
148        Some(&self.loaded_elements[loaded_index])
149    }
150
151    /// Returns the length of the whole array (not the amount of loaded
152    /// elements).
153    pub fn array_len(&self) -> usize {
154        self.index_map.len()
155    }
156}
157
158/// `Option<u32>` but Zeroable and u32-sized.
159///
160/// But can't hold the value `0xFFFFFFFF`.
161#[derive(Clone, Copy)]
162struct OptionalU32 {
163    /// Contains the value represented by this struct, except that the inner
164    /// value from [`NonZeroU32::get`] is 1 more than the value this struct
165    /// represents. [`OptionalU32::set`] and [`OptionalU32::get`] handle
166    /// applying this bias in both directions.
167    biased_index: Option<NonZeroU32>,
168}
169
170// Safety: OptionalU32 is inhabited and all zeroes is a valid value for it (it'd
171// have `index_plus_one: None`). For another perspective, Option<T> is Zeroable
172// if T is PodInOption, and NonZeroU32 is PodInOption.
173unsafe impl Zeroable for OptionalU32 {}
174
175impl OptionalU32 {
176    pub fn set(&mut self, index: u32) {
177        self.biased_index = Some(NonZeroU32::new(index + 1).unwrap());
178    }
179
180    pub fn get(self) -> Option<u32> {
181        self.biased_index
182            .map(|index_plus_one| index_plus_one.get() - 1)
183    }
184
185    pub fn take(&mut self) -> Option<u32> {
186        let result = self.get();
187        self.biased_index = None;
188        result
189    }
190}