unlit_wgpu/array_pool.rs
1//! A pool that packs many variable-size slices into one array.
2//!
3//! The arrays a frame reads are frame-wide: one [`Array`] per kind serves every
4//! draw, and a draw names its slice inside it rather than binding a resource of
5//! its own. That works when the slices are of a fixed size — the metadata
6//! entries are — but a mesh's morph displacements are as long as the mesh has
7//! vertices, so its slice has to be sub-allocated rather than indexed.
8//!
9//! An [`ArrayPool`] is that: an [`Array`] plus an [`Allocator`] over its
10//! elements, handing out [`ArrayRange`]s. It keeps a CPU mirror of the array's
11//! bytes, so writing a slice and publishing the array are separate steps —
12//! [`ArrayPool::write`] fills the mirror, [`ArrayPool::upload`] copies it to
13//! the resource. That is what lets several meshes' slices be written into the
14//! one array before a single upload reaches the GPU.
15//!
16//! # Element granularity
17//!
18//! Ranges are counted in *elements*, the unit the array's [`ArrayHandle`]
19//! indexes by, so a range's element offset is exactly what a shader adds to its
20//! own base index. Elements are the smallest unit the pool deals in, so an
21//! allocator over them needs no alignment beyond one element.
22//!
23//! # Growing
24//!
25//! The pool doubles rather than re-packs: an [`Array`] cannot be resized, so
26//! growing replaces the resource and every range keeps its element offset. The
27//! CPU mirror carries the live data across, which is why the caller has to
28//! [`upload`](ArrayPool::upload) after an allocation that grew the pool.
29//!
30//! # Example
31//!
32//! ```rust
33//! use unlit_wgpu::array_pool::ArrayPool;
34//!
35//! let (device, _queue) = wgpu::Device::noop(&wgpu::DeviceDescriptor::default());
36//! // Elements of four bytes: one `f32` each.
37//! let mut pool = ArrayPool::new(&device, "example::arrays", 4, 16, None);
38//!
39//! let range = pool.allocate(&device, 3).expect("the pool has room");
40//! pool.write(range, &[1.0f32.to_le_bytes(), 2.0f32.to_le_bytes(), 3.0f32.to_le_bytes()]
41//! .concat());
42//!
43//! let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
44//! label: Some("example"),
45//! });
46//! pool.upload(&device, &mut encoder);
47//! pool.release(range.allocation);
48//! ```
49
50use crate::offset_allocator::{Allocation, Allocator, min_allocator_size};
51use crate::texel_array::{Array, ArrayHandle};
52use core::num::NonZeroU32;
53
54/// The alignment elements are handed out at.
55///
56/// One element is the unit, so an offset never needs rounding.
57const ELEMENT_ALIGNMENT: NonZeroU32 = match NonZeroU32::new(1) {
58 Some(alignment) => alignment,
59 None => unreachable!(),
60};
61
62/// How many ranges a pool tracks unless the caller says otherwise.
63///
64/// The allocator's own default (128 Ki nodes, roughly 3.5 MB of metadata) is
65/// sized for a general-purpose allocator; a pool holds one range per mesh part,
66/// so it needs far fewer.
67const DEFAULT_MAX_RANGES: u32 = 4096;
68
69/// The smallest pool size, so the first grow has something to grow from.
70const MIN_ELEMENTS: u32 = 1;
71
72/// A range of elements inside an [`ArrayPool`]'s array.
73///
74/// The range stays valid — and its data stays put — until its
75/// [`allocation`](Self::allocation) field is handed back with
76/// [`ArrayPool::release`].
77/// Growing the pool replaces the array but keeps every element offset, so read
78/// the handle from [`ArrayPool::handle`] instead of holding one.
79#[derive(Clone, Copy, Debug, PartialEq, Eq)]
80pub struct ArrayRange {
81 /// The allocation that keeps the range reserved.
82 pub allocation: Allocation,
83 /// How many elements the range covers.
84 pub count: u32,
85}
86
87/// One array sub-allocated into element ranges, with a CPU mirror of its bytes.
88///
89/// See the [module documentation](self) for what a pool is for and how growing
90/// works.
91pub struct ArrayPool {
92 /// The array every range lives in.
93 array: Array,
94 /// The label the array is created with, reused when it is replaced.
95 label: Option<String>,
96 /// Where the free elements are.
97 allocator: Allocator,
98 /// The array's bytes, element for element, kept so a slice can be written
99 /// without re-reading the GPU resource.
100 data: Vec<u8>,
101 /// Whether the mirror holds bytes the array has not received yet.
102 dirty: bool,
103 /// Whether the array was replaced since the last [`Self::upload`].
104 ///
105 /// A replacement is what a caller has to act on: every bind group built
106 /// from the old handle is stale. Reporting it from the upload keeps the
107 /// check and the copy in one place, so a caller cannot upload without
108 /// learning that the array moved.
109 replaced: bool,
110}
111
112impl ArrayPool {
113 /// Create a pool whose array holds `initial_elements` elements of
114 /// `element_size` bytes each.
115 ///
116 /// `max_dimension` is `Some` of the device's
117 /// [`wgpu::Limits::max_texture_dimension_2d`] to hold the elements in a
118 /// texture, or `None` to hold them in a storage buffer.
119 ///
120 /// # Panics
121 ///
122 /// Under the conditions [`Array::new`] panics.
123 pub fn new(
124 device: &wgpu::Device,
125 label: &str,
126 element_size: u64,
127 initial_elements: u32,
128 max_dimension: Option<u32>,
129 ) -> Self {
130 let elements = initial_elements.max(MIN_ELEMENTS);
131 let array = Array::new(
132 device,
133 Some(label),
134 element_size,
135 u64::from(elements),
136 max_dimension,
137 );
138 // The array may round its capacity up — a texel array does, to keep a
139 // whole number of elements per row — so the mirror follows the array
140 // rather than the request.
141 let capacity = array.capacity();
142 Self {
143 array,
144 label: Some(label.to_owned()),
145 allocator: Allocator::with_max_nodes_and_alignment(
146 min_allocator_size(elements, ELEMENT_ALIGNMENT),
147 DEFAULT_MAX_RANGES,
148 ELEMENT_ALIGNMENT,
149 ),
150 data: vec![0; (capacity * element_size) as usize],
151 dirty: false,
152 replaced: false,
153 }
154 }
155
156 /// The bindable handle to the pool's array.
157 ///
158 /// Cloned rather than borrowed so a caller can move it into the resource
159 /// graph, which is what makes a replaced array mark the bind groups built
160 /// from it dirty.
161 pub fn handle(&self) -> ArrayHandle {
162 self.array.handle()
163 }
164
165 /// The array itself, for a caller that needs more than the handle.
166 pub fn array(&self) -> &Array {
167 &self.array
168 }
169
170 /// How many elements the array can hold.
171 pub fn capacity(&self) -> u64 {
172 self.array.capacity()
173 }
174
175 /// Reserves `count` elements, growing the array if they do not fit.
176 ///
177 /// Returns `None` only if the pool cannot grow any further (its size would
178 /// overflow a `u32`, or it has run out of node slots for its ranges).
179 ///
180 /// Growing replaces the array, so a caller that holds a handle has to read
181 /// it again — and has to [`upload`](Self::upload) before the GPU sees the
182 /// data the mirror holds.
183 ///
184 /// # Panics
185 ///
186 /// If `count` is zero, since a range covers at least one element.
187 pub fn allocate(&mut self, device: &wgpu::Device, count: u32) -> Option<ArrayRange> {
188 assert!(count > 0, "an array allocation covers at least one element");
189
190 if let Some(allocation) = self.allocator.allocate(count) {
191 self.reserve(device, allocation.offset + count);
192 return Some(ArrayRange { allocation, count });
193 }
194
195 // The allocation may fail because the pool is full or because its
196 // ranges are fragmented; growing helps in both cases as long as there
197 // is a free node to describe the appended space.
198 self.grow(device, count)?;
199 let allocation = self
200 .allocator
201 .allocate(count)
202 .expect("the grown pool has room for the range that did not fit");
203 self.reserve(device, allocation.offset + count);
204 Some(ArrayRange { allocation, count })
205 }
206
207 /// Hands a range's elements back to the pool.
208 ///
209 /// # Panics
210 ///
211 /// Panics if the allocation was already released.
212 pub fn release(&mut self, allocation: Allocation) {
213 self.allocator.free(allocation);
214 }
215
216 /// Write `bytes` — one element per `element_size` — into `range`.
217 ///
218 /// This only fills the CPU mirror; the GPU sees the data once
219 /// [`upload`](Self::upload) runs.
220 ///
221 /// # Panics
222 ///
223 /// If `bytes` is not exactly `range`'s elements, or if the range does not
224 /// belong to this pool.
225 pub fn write(&mut self, range: ArrayRange, bytes: &[u8]) {
226 let element_size = self.array.element_size();
227 assert_eq!(
228 bytes.len() as u64,
229 u64::from(range.count) * element_size,
230 "the bytes must be exactly the range's elements"
231 );
232 let start = u64::from(range.allocation.offset) * element_size;
233 let end = start + bytes.len() as u64;
234 assert!(
235 end <= self.data.len() as u64,
236 "the range lies inside the array the pool allocated from"
237 );
238 self.data[start as usize..end as usize].copy_from_slice(bytes);
239 self.dirty = true;
240 }
241
242 /// Copy the CPU mirror to the array, recording the copy into `encoder`.
243 ///
244 /// Returns whether the array had to be *replaced* since the caller last
245 /// read its handle, which is what says every bind group built from the old
246 /// one is stale. An upload of an unchanged pool does nothing.
247 pub fn upload(&mut self, device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder) -> bool {
248 let replaced = core::mem::take(&mut self.replaced);
249 if core::mem::take(&mut self.dirty) {
250 self.array.upload(device, encoder, &self.data);
251 }
252 replaced
253 }
254
255 /// Grow the array until it covers `end` elements.
256 ///
257 /// The mirror is resized with it, so the data already written stays put.
258 fn reserve(&mut self, device: &wgpu::Device, end: u32) {
259 let end = u64::from(end);
260 if end <= self.array.capacity() {
261 return;
262 }
263 let wanted = growth_step(self.array.capacity() as u32, end as u32);
264 if self
265 .array
266 .grow_to(device, self.label.as_deref(), u64::from(wanted))
267 {
268 self.data.resize(
269 (self.array.capacity() * self.array.element_size()) as usize,
270 0,
271 );
272 // The replacement has to reach the GPU before it is read, so the
273 // whole mirror is rewritten; a grow that copied nothing would
274 // otherwise leave the new resource empty.
275 self.dirty = true;
276 self.replaced = true;
277 }
278 }
279
280 /// Grow the allocator until `count` elements fit.
281 ///
282 /// Returns `None` if it cannot grow, leaving the pool unchanged.
283 fn grow(&mut self, device: &wgpu::Device, count: u32) -> Option<()> {
284 // Grow by 1.5x and at least as much as the allocation that did not fit
285 // needs, so one grow is always enough for it.
286 //
287 // The size to grow by is the allocator's minimum for `count`, not
288 // `count` itself: a free range is filed under its rounded-down size
289 // while an allocation searches from its rounded-up size, so a range
290 // sized exactly `count` would not be found by an allocation of `count`.
291 let current = self.allocator.size();
292 let needed = min_allocator_size(count, ELEMENT_ALIGNMENT);
293 let additional = needed.max(current / 2);
294 let target = current.checked_add(additional)?;
295 if !self.allocator.extend(additional) {
296 return None;
297 }
298 debug_assert_eq!(self.allocator.size(), target);
299 self.reserve(device, target);
300 Some(())
301 }
302}
303
304impl core::fmt::Debug for ArrayPool {
305 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
306 f.debug_struct("ArrayPool")
307 .field("label", &self.label)
308 .field("element_size", &self.array.element_size())
309 .field("capacity", &self.capacity())
310 .field("is_texel", &self.array.is_texel())
311 .finish()
312 }
313}
314
315/// The element count to grow `current` to so it covers `end`.
316///
317/// Growth is geometric (1.5x) so repeated growth does not reallocate every
318/// time, and never rounds below the minimum the pool needs.
319fn growth_step(current: u32, end: u32) -> u32 {
320 end.max(current + current / 2).max(MIN_ELEMENTS)
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326
327 fn noop_device() -> (wgpu::Device, wgpu::Queue) {
328 wgpu::Device::noop(&wgpu::DeviceDescriptor::default())
329 }
330
331 fn pool(device: &wgpu::Device, elements: u32) -> ArrayPool {
332 ArrayPool::new(device, "test::arrays", 4, elements, None)
333 }
334
335 #[test]
336 fn ranges_are_distinct_and_a_released_one_comes_back() {
337 let (device, _queue) = noop_device();
338 let mut pool = pool(&device, 64);
339
340 let first = pool.allocate(&device, 8).expect("the pool has room");
341 let second = pool.allocate(&device, 8).expect("the pool has room");
342 assert!(
343 first.allocation.offset + first.count <= second.allocation.offset
344 || second.allocation.offset + second.count <= first.allocation.offset,
345 "the two ranges do not overlap"
346 );
347
348 pool.release(first.allocation);
349 let third = pool.allocate(&device, 8).expect("the pool has room");
350 assert_eq!(
351 third.allocation.offset, first.allocation.offset,
352 "the freed range comes back"
353 );
354 }
355
356 #[test]
357 fn writing_a_range_fills_only_that_range() {
358 let (device, _queue) = noop_device();
359 let mut pool = pool(&device, 16);
360 let first = pool.allocate(&device, 2).expect("the pool has room");
361 let second = pool.allocate(&device, 2).expect("the pool has room");
362
363 pool.write(first, &[1, 2, 3, 4, 5, 6, 7, 8]);
364 pool.write(second, &[9, 10, 11, 12, 13, 14, 15, 16]);
365
366 let first_start = first.allocation.offset as usize * 4;
367 assert_eq!(
368 &pool.data[first_start..first_start + 8],
369 &[1, 2, 3, 4, 5, 6, 7, 8]
370 );
371 let second_start = second.allocation.offset as usize * 4;
372 assert_eq!(
373 &pool.data[second_start..second_start + 8],
374 &[9, 10, 11, 12, 13, 14, 15, 16]
375 );
376 }
377
378 #[test]
379 #[should_panic(expected = "exactly the range's elements")]
380 fn writing_the_wrong_length_panics() {
381 let (device, _queue) = noop_device();
382 let mut pool = pool(&device, 16);
383 let range = pool.allocate(&device, 2).expect("the pool has room");
384 pool.write(range, &[1, 2, 3, 4]);
385 }
386
387 #[test]
388 fn growing_keeps_offsets_and_preserves_the_written_data() {
389 let (device, _queue) = noop_device();
390 let mut pool = pool(&device, 4);
391 let first = pool.allocate(&device, 2).expect("the pool has room");
392 pool.write(first, &[1, 2, 3, 4, 5, 6, 7, 8]);
393
394 // This does not fit in the remaining elements, so the pool grows.
395 let second = pool.allocate(&device, 32).expect("the pool grows");
396 assert_eq!(first.allocation.offset, 0, "the first range did not move");
397 assert!(pool.capacity() >= u64::from(second.allocation.offset + second.count));
398 let start = first.allocation.offset as usize * 4;
399 assert_eq!(
400 &pool.data[start..start + 8],
401 &[1, 2, 3, 4, 5, 6, 7, 8],
402 "the data written before the grow survives it"
403 );
404 }
405
406 #[test]
407 fn an_allocation_reports_whether_the_array_was_replaced() {
408 let (device, _queue) = noop_device();
409 let mut pool = pool(&device, 4);
410 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
411 label: Some("test::encoder"),
412 });
413
414 let first = pool.allocate(&device, 2).expect("the pool has room");
415 pool.write(first, &[1, 2, 3, 4, 5, 6, 7, 8]);
416 assert!(
417 !pool.upload(&device, &mut encoder),
418 "an in-place write replaces nothing"
419 );
420
421 // A range that does not fit grows the array, which every bind group
422 // built from the old handle has to learn about.
423 pool.allocate(&device, 32).expect("the pool grows");
424 assert!(
425 pool.upload(&device, &mut encoder),
426 "a grow replaces the array"
427 );
428 assert!(
429 !pool.upload(&device, &mut encoder),
430 "the replacement is reported once"
431 );
432 }
433
434 #[test]
435 fn an_upload_of_an_unchanged_pool_reports_nothing() {
436 let (device, _queue) = noop_device();
437 let mut pool = pool(&device, 4);
438 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
439 label: Some("test::encoder"),
440 });
441 pool.allocate(&device, 2).expect("the pool has room");
442 assert!(!pool.upload(&device, &mut encoder));
443 }
444
445 #[test]
446 #[should_panic(expected = "at least one element")]
447 fn a_zero_element_allocation_panics() {
448 let (device, _queue) = noop_device();
449 let mut pool = pool(&device, 4);
450 pool.allocate(&device, 0);
451 }
452}