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}