Skip to main content

alpha2_render/
uniform.rs

1use std::collections::HashMap;
2use std::num::NonZeroU64;
3use std::sync::Mutex;
4
5use crate::Shared;
6use crate::retire::{Retired, Retirement};
7
8/// The bytes one effect parameter takes in the generated `EffectParams`
9/// struct: every type is padded to one 16-byte slot.
10pub(crate) const PARAM_SLOT_BYTES: usize = 16;
11
12/// The bytes of an effect's parameter uniform: one slot per parameter, and
13/// one for an effect without any, since a uniform cannot be empty. The graph
14/// reserves this much and the pass writes it, so both read it from here.
15pub(crate) const fn param_uniform_bytes(count: usize) -> usize {
16    let slots = if count == 0 { 1 } else { count };
17    slots.saturating_mul(PARAM_SLOT_BYTES)
18}
19
20/// The 32-bit words of a runtime effect pass's runtime uniform: two `vec4f`
21/// of values, then a `vec4u` holding the time in ticks.
22pub(crate) const RUNTIME_UNIFORM_WORDS: usize = 12;
23
24/// The `f32` values of one composite pass's `BatchParams` uniform
25/// (composite.wesl): three `vec4f` of controls, then four for each of
26/// [`crate::composite::COMPOSITE_BATCH_LAYERS`] layers.
27pub(crate) const BATCH_UNIFORM_FLOATS: usize = 12 + 16 * crate::composite::COMPOSITE_BATCH_LAYERS;
28
29/// The bytes of `values` floats, as the graph reserves them.
30pub(crate) const fn float_uniform_bytes(values: usize) -> u64 {
31    (values * size_of::<f32>()) as u64
32}
33
34// Idle arenas are retained up to the most of one capacity in use at once, so a frame with
35// many sources reuses them all; these bound what an unusual frame can leave behind.
36const MAX_IDLE_UNIFORM_BUFFERS: usize = 64;
37const MAX_IDLE_UNIFORM_BYTES: u64 = 4 * 1024 * 1024;
38
39pub(crate) fn f32_bytes<const N: usize>(values: [f32; N]) -> Vec<u8> {
40    values
41        .into_iter()
42        .flat_map(f32::to_ne_bytes)
43        .collect::<Vec<_>>()
44}
45
46/// Allocation and retention counters for submission-scoped uniform arenas.
47#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
48pub struct UniformBufferPoolStats {
49    /// Physical buffers created since renderer construction.
50    pub created: u64,
51    /// Graph executions served by an idle buffer.
52    pub reused: u64,
53    /// Buffers dropped instead of retained, including failed graph encodes.
54    pub discarded: u64,
55    /// Arenas currently being encoded or awaiting submission.
56    pub active: usize,
57    /// Bytes currently owned by active arenas.
58    pub active_bytes: u64,
59    /// Maximum active bytes observed since renderer construction.
60    pub high_water_bytes: u64,
61    /// Unmapped buffers currently available for reuse.
62    pub available: usize,
63    /// Bytes retained by idle reusable buffers.
64    pub available_bytes: u64,
65}
66
67#[derive(Debug)]
68pub(crate) struct UniformBufferPool {
69    /// The device's `min_uniform_buffer_offset_alignment`.
70    alignment: u64,
71    state: Mutex<UniformBufferPoolState>,
72    retirement: Shared<Retirement>,
73}
74
75#[derive(Debug, Default)]
76struct UniformBufferPoolState {
77    /// Oldest release first: capacity, buffer, its bytes, and the
78    /// [`Retirement::generation`] it was created in.
79    available: Vec<(u64, wgpu::Buffer, Vec<u8>, u64)>,
80    /// The generation whose untrusted idle buffers were last dropped.
81    checked_generation: u64,
82    active_by_capacity: HashMap<u64, usize>,
83    peak_active_by_capacity: HashMap<u64, usize>,
84    created: u64,
85    reused: u64,
86    discarded: u64,
87    active: usize,
88    active_bytes: u64,
89    high_water_bytes: u64,
90    available_bytes: u64,
91}
92
93#[derive(Debug)]
94pub(crate) struct UniformArenaLease {
95    pool: Shared<UniformBufferPool>,
96    buffer: Option<wgpu::Buffer>,
97    capacity: u64,
98    alignment: u64,
99    cursor: u64,
100    bytes: Vec<u8>,
101    generation: u64,
102}
103
104#[derive(Copy, Clone, Debug)]
105pub(crate) struct UniformSlice {
106    offset: u64,
107    size: NonZeroU64,
108}
109
110impl UniformSlice {
111    /// Where the slice starts in its arena: a multiple of the device's
112    /// uniform offset alignment.
113    pub(crate) const fn offset(self) -> u64 {
114        self.offset
115    }
116}
117
118impl UniformBufferPool {
119    pub(crate) fn new(alignment: u64, retirement: Shared<Retirement>) -> Self {
120        Self {
121            alignment: alignment.max(1),
122            state: Mutex::default(),
123            retirement,
124        }
125    }
126
127    /// Leases an arena of at least `capacity` bytes.
128    ///
129    /// Capacities round up to a power of two, so graphs whose uniform needs
130    /// differ a little share arenas instead of each wanting an exact size.
131    pub(crate) fn acquire(
132        self: &Shared<Self>,
133        device: &wgpu::Device,
134        capacity: u64,
135    ) -> UniformArenaLease {
136        let alignment = self.alignment;
137        let capacity = align_up(capacity.max(1), alignment)
138            .checked_next_power_of_two()
139            .unwrap_or(u64::MAX);
140        let reused = {
141            let mut state = self
142                .state
143                .lock()
144                .unwrap_or_else(std::sync::PoisonError::into_inner);
145            let untrusted = self.drop_untrusted(&mut state);
146            let index = state
147                .available
148                .iter()
149                .position(|(available_capacity, _, _, _)| *available_capacity == capacity);
150            let reused = index.map(|index| {
151                state.reused = state.reused.saturating_add(1);
152                state.available_bytes = state.available_bytes.saturating_sub(capacity);
153                let (_, buffer, bytes, generation) = state.available.remove(index);
154                (buffer, bytes, generation)
155            });
156            drop(state);
157            self.retirement.retire(untrusted);
158            reused
159        };
160        let (buffer, bytes, generation) = reused.unwrap_or_else(|| {
161            let mut state = self
162                .state
163                .lock()
164                .unwrap_or_else(std::sync::PoisonError::into_inner);
165            state.created = state.created.saturating_add(1);
166            drop(state);
167            let generation = self.retirement.generation();
168            (
169                device.create_buffer(&wgpu::BufferDescriptor {
170                    label: Some("alpha2 uniform arena"),
171                    size: capacity,
172                    usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
173                    mapped_at_creation: false,
174                }),
175                vec![0; usize::try_from(capacity).expect("uniform arena fits host indexing")],
176                generation,
177            )
178        });
179        {
180            let mut state = self
181                .state
182                .lock()
183                .unwrap_or_else(std::sync::PoisonError::into_inner);
184            state.active = state.active.saturating_add(1);
185            state.active_bytes = state.active_bytes.saturating_add(capacity);
186            state.high_water_bytes = state.high_water_bytes.max(state.active_bytes);
187            let active = state.active_by_capacity.entry(capacity).or_default();
188            *active = active.saturating_add(1);
189            let active = *active;
190            let peak = state.peak_active_by_capacity.entry(capacity).or_default();
191            *peak = (*peak).max(active);
192        }
193        UniformArenaLease {
194            pool: Shared::clone(self),
195            buffer: Some(buffer),
196            capacity,
197            alignment,
198            cursor: 0,
199            bytes,
200            generation,
201        }
202    }
203
204    /// Takes the idle arenas created before the GPU last reported an error
205    /// out of the pool, once per such report, for the caller to retire.
206    fn drop_untrusted(&self, state: &mut UniformBufferPoolState) -> Vec<(Retired, u64)> {
207        let generation = self.retirement.generation();
208        if state.checked_generation == generation {
209            return Vec::new();
210        }
211        state.checked_generation = generation;
212        let (kept, untrusted): (Vec<_>, Vec<_>) = std::mem::take(&mut state.available)
213            .into_iter()
214            .partition(|(_, _, _, created)| *created == generation);
215        state.available = kept;
216        state.available_bytes = state
217            .available
218            .iter()
219            .map(|(capacity, _, _, _)| *capacity)
220            .sum();
221        state.discarded = state
222            .discarded
223            .saturating_add(u64::try_from(untrusted.len()).unwrap_or(u64::MAX));
224        untrusted
225            .into_iter()
226            .map(|(capacity, buffer, _, _)| {
227                prune_capacity(state, capacity);
228                (Retired::Buffer(buffer), capacity)
229            })
230            .collect()
231    }
232
233    /// Destroys every idle uniform buffer.
234    pub(crate) fn clear(&self) {
235        let mut state = self
236            .state
237            .lock()
238            .unwrap_or_else(std::sync::PoisonError::into_inner);
239        let discarded = u64::try_from(state.available.len()).unwrap_or(u64::MAX);
240        let idle = std::mem::take(&mut state.available);
241        state.discarded = state.discarded.saturating_add(discarded);
242        state.available_bytes = 0;
243        drop(state);
244        self.retirement.retire(
245            idle.into_iter()
246                .map(|(capacity, buffer, _, _)| (Retired::Buffer(buffer), capacity)),
247        );
248    }
249
250    pub(crate) fn stats(&self) -> UniformBufferPoolStats {
251        let state = self
252            .state
253            .lock()
254            .unwrap_or_else(std::sync::PoisonError::into_inner);
255        UniformBufferPoolStats {
256            created: state.created,
257            reused: state.reused,
258            discarded: state.discarded,
259            active: state.active,
260            active_bytes: state.active_bytes,
261            high_water_bytes: state.high_water_bytes,
262            available: state.available.len(),
263            available_bytes: state.available_bytes,
264        }
265    }
266
267    /// Keeps a finished arena for reuse while its capacity has fewer idle
268    /// arenas than were ever in use at once, within the idle bounds, which
269    /// evict the oldest release first. What the pool does not keep is
270    /// destroyed.
271    fn release(&self, capacity: u64, buffer: wgpu::Buffer, bytes: Vec<u8>, generation: u64) {
272        if !self.retirement.trusts(generation) {
273            self.discard(capacity, buffer);
274            return;
275        }
276        let mut retired = Vec::new();
277        self.keep_or_retire(capacity, buffer, bytes, generation, &mut retired);
278        self.retirement.retire(retired);
279    }
280
281    fn keep_or_retire(
282        &self,
283        capacity: u64,
284        buffer: wgpu::Buffer,
285        bytes: Vec<u8>,
286        generation: u64,
287        retired: &mut Vec<(Retired, u64)>,
288    ) {
289        let mut state = self
290            .state
291            .lock()
292            .unwrap_or_else(std::sync::PoisonError::into_inner);
293        finish_active(&mut state, capacity);
294        let retention_target = state
295            .peak_active_by_capacity
296            .get(&capacity)
297            .copied()
298            .unwrap_or(1);
299        let idle_at_capacity = state
300            .available
301            .iter()
302            .filter(|(available, _, _, _)| *available == capacity)
303            .count();
304        if idle_at_capacity >= retention_target || capacity > MAX_IDLE_UNIFORM_BYTES {
305            state.discarded = state.discarded.saturating_add(1);
306            retired.push((Retired::Buffer(buffer), capacity));
307            return;
308        }
309        while state.available.len() >= MAX_IDLE_UNIFORM_BUFFERS
310            || state.available_bytes.saturating_add(capacity) > MAX_IDLE_UNIFORM_BYTES
311        {
312            let (discarded, discarded_buffer, _bytes, _) = state.available.remove(0);
313            state.available_bytes = state.available_bytes.saturating_sub(discarded);
314            state.discarded = state.discarded.saturating_add(1);
315            prune_capacity(&mut state, discarded);
316            retired.push((Retired::Buffer(discarded_buffer), discarded));
317        }
318        state.available.push((capacity, buffer, bytes, generation));
319        state.available_bytes = state.available_bytes.saturating_add(capacity);
320    }
321
322    /// Takes back an arena whose encode failed or was abandoned.
323    fn discard(&self, capacity: u64, buffer: wgpu::Buffer) {
324        let mut state = self
325            .state
326            .lock()
327            .unwrap_or_else(std::sync::PoisonError::into_inner);
328        finish_active(&mut state, capacity);
329        state.discarded = state.discarded.saturating_add(1);
330        prune_capacity(&mut state, capacity);
331        drop(state);
332        self.retirement
333            .retire([(Retired::Buffer(buffer), capacity)]);
334    }
335}
336
337impl UniformArenaLease {
338    pub(crate) fn write(&mut self, contents: &[u8]) -> UniformSlice {
339        let offset = align_up(self.cursor, self.alignment);
340        let size = u64::try_from(contents.len()).expect("uniform contents fit u64");
341        let end = offset.checked_add(size).expect("uniform arena offset");
342        assert!(end <= self.capacity, "uniform arena capacity was reserved");
343        let start = usize::try_from(offset).expect("uniform offset fits host indexing");
344        let end = usize::try_from(end).expect("uniform end fits host indexing");
345        self.bytes[start..end].copy_from_slice(contents);
346        self.cursor = u64::try_from(end).expect("uniform cursor fits u64");
347        UniformSlice {
348            offset,
349            size: NonZeroU64::new(size).expect("uniform binding is non-empty"),
350        }
351    }
352
353    /// The arena's first `size` bytes as a binding whose offset a pass
354    /// sets dynamically, to each slice's [`UniformSlice::offset`].
355    pub(crate) fn dynamic_binding(&self, size: NonZeroU64) -> wgpu::BindingResource<'_> {
356        wgpu::BindingResource::Buffer(wgpu::BufferBinding {
357            buffer: self.buffer.as_ref().expect("live uniform arena"),
358            offset: 0,
359            size: Some(size),
360        })
361    }
362
363    pub(crate) fn binding(&self, slice: UniformSlice) -> wgpu::BindingResource<'_> {
364        wgpu::BindingResource::Buffer(wgpu::BufferBinding {
365            buffer: self.buffer.as_ref().expect("live uniform arena"),
366            offset: slice.offset,
367            size: Some(slice.size),
368        })
369    }
370
371    #[cfg(test)]
372    pub(crate) fn buffer(&self) -> &wgpu::Buffer {
373        self.buffer.as_ref().expect("live uniform arena")
374    }
375
376    pub(crate) fn flush(&self, queue: &wgpu::Queue) {
377        if self.cursor > 0 {
378            let used = usize::try_from(self.cursor).expect("uniform cursor fits host indexing");
379            queue.write_buffer(
380                self.buffer.as_ref().expect("live uniform arena"),
381                0,
382                &self.bytes[..used],
383            );
384        }
385    }
386
387    pub(crate) fn recycle(mut self) {
388        let buffer = self.buffer.take().expect("live uniform arena");
389        let bytes = std::mem::take(&mut self.bytes);
390        self.pool
391            .release(self.capacity, buffer, bytes, self.generation);
392    }
393}
394
395impl Drop for UniformArenaLease {
396    fn drop(&mut self) {
397        if let Some(buffer) = self.buffer.take() {
398            self.pool.discard(self.capacity, buffer);
399        }
400    }
401}
402
403fn finish_active(state: &mut UniformBufferPoolState, capacity: u64) {
404    state.active = state.active.saturating_sub(1);
405    state.active_bytes = state.active_bytes.saturating_sub(capacity);
406    if let Some(active) = state.active_by_capacity.get_mut(&capacity) {
407        *active = active.saturating_sub(1);
408        if *active == 0 {
409            state.active_by_capacity.remove(&capacity);
410        }
411    }
412}
413
414/// Forgets a capacity's peak once nothing of it is active or idle, so the
415/// retention map stays as small as the capacities in use.
416fn prune_capacity(state: &mut UniformBufferPoolState, capacity: u64) {
417    if !state.active_by_capacity.contains_key(&capacity)
418        && !state
419            .available
420            .iter()
421            .any(|(available, _, _, _)| *available == capacity)
422    {
423        state.peak_active_by_capacity.remove(&capacity);
424    }
425}
426
427pub(crate) const fn aligned_uniform_end(cursor: u64, size: u64, alignment: u64) -> u64 {
428    align_up(cursor, alignment).saturating_add(size)
429}
430
431const fn align_up(value: u64, alignment: u64) -> u64 {
432    value
433        .saturating_add(alignment.saturating_sub(1))
434        .div_euclid(alignment)
435        .saturating_mul(alignment)
436}