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 UniformBufferPool {
111    pub(crate) fn new(alignment: u64, retirement: Shared<Retirement>) -> Self {
112        Self {
113            alignment: alignment.max(1),
114            state: Mutex::default(),
115            retirement,
116        }
117    }
118
119    /// Leases an arena of at least `capacity` bytes.
120    ///
121    /// Capacities round up to a power of two, so graphs whose uniform needs
122    /// differ a little share arenas instead of each wanting an exact size.
123    pub(crate) fn acquire(
124        self: &Shared<Self>,
125        device: &wgpu::Device,
126        capacity: u64,
127    ) -> UniformArenaLease {
128        let alignment = self.alignment;
129        let capacity = align_up(capacity.max(1), alignment)
130            .checked_next_power_of_two()
131            .unwrap_or(u64::MAX);
132        let reused = {
133            let mut state = self
134                .state
135                .lock()
136                .unwrap_or_else(std::sync::PoisonError::into_inner);
137            let untrusted = self.drop_untrusted(&mut state);
138            let index = state
139                .available
140                .iter()
141                .position(|(available_capacity, _, _, _)| *available_capacity == capacity);
142            let reused = index.map(|index| {
143                state.reused = state.reused.saturating_add(1);
144                state.available_bytes = state.available_bytes.saturating_sub(capacity);
145                let (_, buffer, bytes, generation) = state.available.remove(index);
146                (buffer, bytes, generation)
147            });
148            drop(state);
149            self.retirement.retire(untrusted);
150            reused
151        };
152        let (buffer, bytes, generation) = reused.unwrap_or_else(|| {
153            let mut state = self
154                .state
155                .lock()
156                .unwrap_or_else(std::sync::PoisonError::into_inner);
157            state.created = state.created.saturating_add(1);
158            drop(state);
159            let generation = self.retirement.generation();
160            (
161                device.create_buffer(&wgpu::BufferDescriptor {
162                    label: Some("alpha2 uniform arena"),
163                    size: capacity,
164                    usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
165                    mapped_at_creation: false,
166                }),
167                vec![0; usize::try_from(capacity).expect("uniform arena fits host indexing")],
168                generation,
169            )
170        });
171        {
172            let mut state = self
173                .state
174                .lock()
175                .unwrap_or_else(std::sync::PoisonError::into_inner);
176            state.active = state.active.saturating_add(1);
177            state.active_bytes = state.active_bytes.saturating_add(capacity);
178            state.high_water_bytes = state.high_water_bytes.max(state.active_bytes);
179            let active = state.active_by_capacity.entry(capacity).or_default();
180            *active = active.saturating_add(1);
181            let active = *active;
182            let peak = state.peak_active_by_capacity.entry(capacity).or_default();
183            *peak = (*peak).max(active);
184        }
185        UniformArenaLease {
186            pool: Shared::clone(self),
187            buffer: Some(buffer),
188            capacity,
189            alignment,
190            cursor: 0,
191            bytes,
192            generation,
193        }
194    }
195
196    /// Takes the idle arenas created before the GPU last reported an error
197    /// out of the pool, once per such report, for the caller to retire.
198    fn drop_untrusted(&self, state: &mut UniformBufferPoolState) -> Vec<(Retired, u64)> {
199        let generation = self.retirement.generation();
200        if state.checked_generation == generation {
201            return Vec::new();
202        }
203        state.checked_generation = generation;
204        let (kept, untrusted): (Vec<_>, Vec<_>) = std::mem::take(&mut state.available)
205            .into_iter()
206            .partition(|(_, _, _, created)| *created == generation);
207        state.available = kept;
208        state.available_bytes = state
209            .available
210            .iter()
211            .map(|(capacity, _, _, _)| *capacity)
212            .sum();
213        state.discarded = state
214            .discarded
215            .saturating_add(u64::try_from(untrusted.len()).unwrap_or(u64::MAX));
216        untrusted
217            .into_iter()
218            .map(|(capacity, buffer, _, _)| {
219                prune_capacity(state, capacity);
220                (Retired::Buffer(buffer), capacity)
221            })
222            .collect()
223    }
224
225    /// Destroys every idle uniform buffer.
226    pub(crate) fn clear(&self) {
227        let mut state = self
228            .state
229            .lock()
230            .unwrap_or_else(std::sync::PoisonError::into_inner);
231        let discarded = u64::try_from(state.available.len()).unwrap_or(u64::MAX);
232        let idle = std::mem::take(&mut state.available);
233        state.discarded = state.discarded.saturating_add(discarded);
234        state.available_bytes = 0;
235        drop(state);
236        self.retirement.retire(
237            idle.into_iter()
238                .map(|(capacity, buffer, _, _)| (Retired::Buffer(buffer), capacity)),
239        );
240    }
241
242    pub(crate) fn stats(&self) -> UniformBufferPoolStats {
243        let state = self
244            .state
245            .lock()
246            .unwrap_or_else(std::sync::PoisonError::into_inner);
247        UniformBufferPoolStats {
248            created: state.created,
249            reused: state.reused,
250            discarded: state.discarded,
251            active: state.active,
252            active_bytes: state.active_bytes,
253            high_water_bytes: state.high_water_bytes,
254            available: state.available.len(),
255            available_bytes: state.available_bytes,
256        }
257    }
258
259    /// Keeps a finished arena for reuse while its capacity has fewer idle
260    /// arenas than were ever in use at once, within the idle bounds, which
261    /// evict the oldest release first. What the pool does not keep is
262    /// destroyed.
263    fn release(&self, capacity: u64, buffer: wgpu::Buffer, bytes: Vec<u8>, generation: u64) {
264        if !self.retirement.trusts(generation) {
265            self.discard(capacity, buffer);
266            return;
267        }
268        let mut retired = Vec::new();
269        self.keep_or_retire(capacity, buffer, bytes, generation, &mut retired);
270        self.retirement.retire(retired);
271    }
272
273    fn keep_or_retire(
274        &self,
275        capacity: u64,
276        buffer: wgpu::Buffer,
277        bytes: Vec<u8>,
278        generation: u64,
279        retired: &mut Vec<(Retired, u64)>,
280    ) {
281        let mut state = self
282            .state
283            .lock()
284            .unwrap_or_else(std::sync::PoisonError::into_inner);
285        finish_active(&mut state, capacity);
286        let retention_target = state
287            .peak_active_by_capacity
288            .get(&capacity)
289            .copied()
290            .unwrap_or(1);
291        let idle_at_capacity = state
292            .available
293            .iter()
294            .filter(|(available, _, _, _)| *available == capacity)
295            .count();
296        if idle_at_capacity >= retention_target || capacity > MAX_IDLE_UNIFORM_BYTES {
297            state.discarded = state.discarded.saturating_add(1);
298            retired.push((Retired::Buffer(buffer), capacity));
299            return;
300        }
301        while state.available.len() >= MAX_IDLE_UNIFORM_BUFFERS
302            || state.available_bytes.saturating_add(capacity) > MAX_IDLE_UNIFORM_BYTES
303        {
304            let (discarded, discarded_buffer, _bytes, _) = state.available.remove(0);
305            state.available_bytes = state.available_bytes.saturating_sub(discarded);
306            state.discarded = state.discarded.saturating_add(1);
307            prune_capacity(&mut state, discarded);
308            retired.push((Retired::Buffer(discarded_buffer), discarded));
309        }
310        state.available.push((capacity, buffer, bytes, generation));
311        state.available_bytes = state.available_bytes.saturating_add(capacity);
312    }
313
314    /// Takes back an arena whose encode failed or was abandoned.
315    fn discard(&self, capacity: u64, buffer: wgpu::Buffer) {
316        let mut state = self
317            .state
318            .lock()
319            .unwrap_or_else(std::sync::PoisonError::into_inner);
320        finish_active(&mut state, capacity);
321        state.discarded = state.discarded.saturating_add(1);
322        prune_capacity(&mut state, capacity);
323        drop(state);
324        self.retirement
325            .retire([(Retired::Buffer(buffer), capacity)]);
326    }
327}
328
329impl UniformArenaLease {
330    pub(crate) fn write(&mut self, contents: &[u8]) -> UniformSlice {
331        let offset = align_up(self.cursor, self.alignment);
332        let size = u64::try_from(contents.len()).expect("uniform contents fit u64");
333        let end = offset.checked_add(size).expect("uniform arena offset");
334        assert!(end <= self.capacity, "uniform arena capacity was reserved");
335        let start = usize::try_from(offset).expect("uniform offset fits host indexing");
336        let end = usize::try_from(end).expect("uniform end fits host indexing");
337        self.bytes[start..end].copy_from_slice(contents);
338        self.cursor = u64::try_from(end).expect("uniform cursor fits u64");
339        UniformSlice {
340            offset,
341            size: NonZeroU64::new(size).expect("uniform binding is non-empty"),
342        }
343    }
344
345    pub(crate) fn binding(&self, slice: UniformSlice) -> wgpu::BindingResource<'_> {
346        wgpu::BindingResource::Buffer(wgpu::BufferBinding {
347            buffer: self.buffer.as_ref().expect("live uniform arena"),
348            offset: slice.offset,
349            size: Some(slice.size),
350        })
351    }
352
353    #[cfg(test)]
354    pub(crate) fn buffer(&self) -> &wgpu::Buffer {
355        self.buffer.as_ref().expect("live uniform arena")
356    }
357
358    pub(crate) fn flush(&self, queue: &wgpu::Queue) {
359        if self.cursor > 0 {
360            let used = usize::try_from(self.cursor).expect("uniform cursor fits host indexing");
361            queue.write_buffer(
362                self.buffer.as_ref().expect("live uniform arena"),
363                0,
364                &self.bytes[..used],
365            );
366        }
367    }
368
369    pub(crate) fn recycle(mut self) {
370        let buffer = self.buffer.take().expect("live uniform arena");
371        let bytes = std::mem::take(&mut self.bytes);
372        self.pool
373            .release(self.capacity, buffer, bytes, self.generation);
374    }
375}
376
377impl Drop for UniformArenaLease {
378    fn drop(&mut self) {
379        if let Some(buffer) = self.buffer.take() {
380            self.pool.discard(self.capacity, buffer);
381        }
382    }
383}
384
385fn finish_active(state: &mut UniformBufferPoolState, capacity: u64) {
386    state.active = state.active.saturating_sub(1);
387    state.active_bytes = state.active_bytes.saturating_sub(capacity);
388    if let Some(active) = state.active_by_capacity.get_mut(&capacity) {
389        *active = active.saturating_sub(1);
390        if *active == 0 {
391            state.active_by_capacity.remove(&capacity);
392        }
393    }
394}
395
396/// Forgets a capacity's peak once nothing of it is active or idle, so the
397/// retention map stays as small as the capacities in use.
398fn prune_capacity(state: &mut UniformBufferPoolState, capacity: u64) {
399    if !state.active_by_capacity.contains_key(&capacity)
400        && !state
401            .available
402            .iter()
403            .any(|(available, _, _, _)| *available == capacity)
404    {
405        state.peak_active_by_capacity.remove(&capacity);
406    }
407}
408
409pub(crate) const fn aligned_uniform_end(cursor: u64, size: u64, alignment: u64) -> u64 {
410    align_up(cursor, alignment).saturating_add(size)
411}
412
413const fn align_up(value: u64, alignment: u64) -> u64 {
414    value
415        .saturating_add(alignment.saturating_sub(1))
416        .div_euclid(alignment)
417        .saturating_mul(alignment)
418}