Skip to main content

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}