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