Skip to main content

astrelis_render/
buffer_pool.rs

1//! Dynamic buffer management with ring buffers and staging pools.
2//!
3//! Provides efficient GPU buffer allocation patterns for streaming data.
4//!
5//! # Ring Buffer
6//!
7//! A ring buffer allows continuous writing without stalling by cycling through
8//! buffer regions. Perfect for per-frame uniform data.
9//!
10//! ```ignore
11//! use astrelis_render::*;
12//!
13//! let mut ring = RingBuffer::new(&ctx, 1024 * 1024, wgpu::BufferUsages::UNIFORM);
14//!
15//! // Each frame
16//! if let Some(allocation) = ring.allocate(256, 256) {
17//!     allocation.write(&data);
18//!     // Use allocation.buffer() and allocation.offset() for binding
19//! }
20//!
21//! // At frame end
22//! ring.next_frame();
23//! ```
24//!
25//! # Staging Buffer Pool
26//!
27//! A pool of staging buffers for efficient CPU-to-GPU transfers.
28//!
29//! ```ignore
30//! use astrelis_render::*;
31//!
32//! let mut pool = StagingBufferPool::new();
33//!
34//! // Allocate staging buffer
35//! let staging = pool.allocate(&ctx, 4096);
36//! staging.write(&data);
37//! staging.copy_to_buffer(&mut encoder, &target_buffer, 0);
38//!
39//! // Return to pool when done
40//! pool.recycle(staging);
41//! ```
42
43use astrelis_core::profiling::profile_function;
44
45use crate::GraphicsContext;
46use std::sync::Arc;
47
48/// Number of frames to buffer (triple buffering).
49const RING_BUFFER_FRAMES: usize = 3;
50
51/// A region allocated from a ring buffer.
52pub struct RingBufferAllocation {
53    /// The underlying buffer
54    buffer: Arc<wgpu::Buffer>,
55    /// Offset into the buffer
56    offset: u64,
57    /// Size of the allocation
58    size: u64,
59}
60
61impl RingBufferAllocation {
62    /// Get the buffer.
63    pub fn buffer(&self) -> &wgpu::Buffer {
64        &self.buffer
65    }
66
67    /// Get the offset into the buffer.
68    pub fn offset(&self) -> u64 {
69        self.offset
70    }
71
72    /// Get the size of the allocation.
73    pub fn size(&self) -> u64 {
74        self.size
75    }
76
77    /// Write data to this allocation.
78    ///
79    /// # Panics
80    ///
81    /// Panics if data size exceeds allocation size.
82    pub fn write(&self, queue: &wgpu::Queue, data: &[u8]) {
83        assert!(
84            data.len() as u64 <= self.size,
85            "Data size {} exceeds allocation size {}",
86            data.len(),
87            self.size
88        );
89        queue.write_buffer(&self.buffer, self.offset, data);
90    }
91
92    /// Get a binding resource for this allocation.
93    pub fn as_binding(&self) -> wgpu::BindingResource<'_> {
94        wgpu::BindingResource::Buffer(wgpu::BufferBinding {
95            buffer: &self.buffer,
96            offset: self.offset,
97            size: Some(std::num::NonZeroU64::new(self.size).unwrap()),
98        })
99    }
100}
101
102/// A ring buffer for streaming per-frame data.
103///
104/// Ring buffers cycle through multiple frames worth of buffer space to avoid
105/// stalling the GPU pipeline.
106pub struct RingBuffer {
107    /// The underlying GPU buffer
108    buffer: Arc<wgpu::Buffer>,
109    /// Total size of the buffer
110    size: u64,
111    /// Current write offset
112    offset: u64,
113    /// Current frame number
114    frame: u64,
115}
116
117impl RingBuffer {
118    /// Create a new ring buffer.
119    ///
120    /// # Arguments
121    ///
122    /// * `context` - Graphics context
123    /// * `size` - Total size in bytes (will be multiplied by RING_BUFFER_FRAMES)
124    /// * `usage` - Buffer usage flags (UNIFORM, STORAGE, etc.)
125    pub fn new(context: Arc<GraphicsContext>, size: u64, usage: wgpu::BufferUsages) -> Self {
126        let total_size = size * RING_BUFFER_FRAMES as u64;
127
128        let buffer = context.device().create_buffer(&wgpu::BufferDescriptor {
129            label: Some("Ring Buffer"),
130            size: total_size,
131            usage: usage | wgpu::BufferUsages::COPY_DST,
132            mapped_at_creation: false,
133        });
134
135        Self {
136            buffer: Arc::new(buffer),
137            size: total_size,
138            offset: 0,
139            frame: 0,
140        }
141    }
142
143    /// Allocate a region from the ring buffer.
144    ///
145    /// # Arguments
146    ///
147    /// * `size` - Size in bytes to allocate
148    /// * `alignment` - Required alignment (typically 256 for uniforms)
149    ///
150    /// # Returns
151    ///
152    /// Returns `Some(allocation)` if space is available, `None` if the buffer is full.
153    pub fn allocate(&mut self, size: u64, alignment: u64) -> Option<RingBufferAllocation> {
154        profile_function!();
155        // Align offset
156        let aligned_offset = if !self.offset.is_multiple_of(alignment) {
157            self.offset + (alignment - (self.offset % alignment))
158        } else {
159            self.offset
160        };
161
162        // Check if we have space in current frame
163        let frame_size = self.size / RING_BUFFER_FRAMES as u64;
164        let frame_start = (self.frame % RING_BUFFER_FRAMES as u64) * frame_size;
165        let frame_end = frame_start + frame_size;
166
167        if aligned_offset + size > frame_end {
168            return None;
169        }
170
171        let allocation = RingBufferAllocation {
172            buffer: self.buffer.clone(),
173            offset: aligned_offset,
174            size,
175        };
176
177        self.offset = aligned_offset + size;
178
179        Some(allocation)
180    }
181
182    /// Advance to the next frame.
183    ///
184    /// Call this at the beginning or end of each frame to reset the ring buffer
185    /// for the next frame's allocations.
186    pub fn next_frame(&mut self) {
187        self.frame += 1;
188        let frame_size = self.size / RING_BUFFER_FRAMES as u64;
189        self.offset = (self.frame % RING_BUFFER_FRAMES as u64) * frame_size;
190    }
191
192    /// Reset the ring buffer (useful for testing or manual control).
193    pub fn reset(&mut self) {
194        self.frame = 0;
195        self.offset = 0;
196    }
197
198    /// Get the current frame number.
199    pub fn frame(&self) -> u64 {
200        self.frame
201    }
202
203    /// Get the current offset.
204    pub fn offset(&self) -> u64 {
205        self.offset
206    }
207
208    /// Get the total size.
209    pub fn size(&self) -> u64 {
210        self.size
211    }
212
213    /// Get remaining space in current frame.
214    pub fn remaining(&self) -> u64 {
215        let frame_size = self.size / RING_BUFFER_FRAMES as u64;
216        let frame_end = ((self.frame % RING_BUFFER_FRAMES as u64) + 1) * frame_size;
217        frame_end.saturating_sub(self.offset)
218    }
219}
220
221/// A staging buffer for CPU-to-GPU transfers.
222pub struct StagingBuffer {
223    /// The GPU buffer
224    buffer: wgpu::Buffer,
225    /// Size of the buffer
226    size: u64,
227}
228
229impl StagingBuffer {
230    /// Create a new staging buffer.
231    fn new(context: &GraphicsContext, size: u64) -> Self {
232        let buffer = context.device().create_buffer(&wgpu::BufferDescriptor {
233            label: Some("Staging Buffer"),
234            size,
235            usage: wgpu::BufferUsages::MAP_WRITE | wgpu::BufferUsages::COPY_SRC,
236            mapped_at_creation: false,
237        });
238
239        Self { buffer, size }
240    }
241
242    /// Get the buffer.
243    pub fn buffer(&self) -> &wgpu::Buffer {
244        &self.buffer
245    }
246
247    /// Get the size.
248    pub fn size(&self) -> u64 {
249        self.size
250    }
251
252    /// Write data to the staging buffer.
253    pub fn write(&self, queue: &wgpu::Queue, data: &[u8]) {
254        assert!(
255            data.len() as u64 <= self.size,
256            "Data size {} exceeds buffer size {}",
257            data.len(),
258            self.size
259        );
260        queue.write_buffer(&self.buffer, 0, data);
261    }
262
263    /// Copy this staging buffer to a destination buffer.
264    pub fn copy_to_buffer(
265        &self,
266        encoder: &mut wgpu::CommandEncoder,
267        dst: &wgpu::Buffer,
268        dst_offset: u64,
269    ) {
270        encoder.copy_buffer_to_buffer(&self.buffer, 0, dst, dst_offset, self.size);
271    }
272
273    /// Copy a region of this staging buffer to a destination buffer.
274    pub fn copy_region_to_buffer(
275        &self,
276        encoder: &mut wgpu::CommandEncoder,
277        src_offset: u64,
278        dst: &wgpu::Buffer,
279        dst_offset: u64,
280        size: u64,
281    ) {
282        encoder.copy_buffer_to_buffer(&self.buffer, src_offset, dst, dst_offset, size);
283    }
284}
285
286/// A pool of staging buffers for reuse.
287pub struct StagingBufferPool {
288    /// Available buffers, grouped by size
289    available: Vec<StagingBuffer>,
290}
291
292impl StagingBufferPool {
293    /// Create a new staging buffer pool.
294    pub fn new() -> Self {
295        Self {
296            available: Vec::new(),
297        }
298    }
299
300    /// Allocate a staging buffer from the pool.
301    ///
302    /// If a suitable buffer is available, it will be reused. Otherwise, a new
303    /// buffer will be created.
304    pub fn allocate(&mut self, context: &GraphicsContext, size: u64) -> StagingBuffer {
305        profile_function!();
306        // Try to find a buffer of suitable size
307        // We look for a buffer that's >= size but not too much bigger
308        let mut best_idx = None;
309        let mut best_size = u64::MAX;
310
311        for (idx, buffer) in self.available.iter().enumerate() {
312            if buffer.size >= size && buffer.size < best_size {
313                best_idx = Some(idx);
314                best_size = buffer.size;
315            }
316        }
317
318        if let Some(idx) = best_idx {
319            self.available.swap_remove(idx)
320        } else {
321            // No suitable buffer found, create a new one
322            // Round up to next power of 2 for better reuse
323            let rounded_size = size.next_power_of_two();
324            StagingBuffer::new(context, rounded_size)
325        }
326    }
327
328    /// Return a staging buffer to the pool for reuse.
329    pub fn recycle(&mut self, buffer: StagingBuffer) {
330        self.available.push(buffer);
331    }
332
333    /// Clear all buffers from the pool.
334    pub fn clear(&mut self) {
335        self.available.clear();
336    }
337
338    /// Get the number of available buffers.
339    pub fn available_count(&self) -> usize {
340        self.available.len()
341    }
342
343    /// Get the total size of available buffers.
344    pub fn total_available_size(&self) -> u64 {
345        self.available.iter().map(|b| b.size).sum()
346    }
347}
348
349impl Default for StagingBufferPool {
350    fn default() -> Self {
351        Self::new()
352    }
353}
354
355#[cfg(test)]
356mod tests {
357    use super::*;
358
359    #[test]
360    fn test_ring_buffer_allocation() {
361        let ctx = GraphicsContext::new_owned_sync().expect("Failed to create graphics context");
362        let mut ring = RingBuffer::new(ctx, 1024, wgpu::BufferUsages::UNIFORM);
363
364        // Allocate some space
365        let alloc1 = ring.allocate(256, 256);
366        assert!(alloc1.is_some());
367        let alloc1 = alloc1.unwrap();
368        assert_eq!(alloc1.offset, 0);
369        assert_eq!(alloc1.size, 256);
370
371        // Allocate more
372        let alloc2 = ring.allocate(256, 256);
373        assert!(alloc2.is_some());
374        let alloc2 = alloc2.unwrap();
375        assert_eq!(alloc2.offset, 256);
376        assert_eq!(alloc2.size, 256);
377    }
378
379    #[test]
380    fn test_ring_buffer_frame_advance() {
381        let ctx = GraphicsContext::new_owned_sync().expect("Failed to create graphics context");
382        let mut ring = RingBuffer::new(ctx, 1024, wgpu::BufferUsages::UNIFORM);
383
384        // Fill first frame
385        let alloc1 = ring.allocate(512, 256);
386        assert!(alloc1.is_some());
387
388        // Advance to next frame
389        ring.next_frame();
390        assert_eq!(ring.frame(), 1);
391
392        // Should be able to allocate in new frame
393        let alloc2 = ring.allocate(512, 256);
394        assert!(alloc2.is_some());
395        let alloc2 = alloc2.unwrap();
396        assert_eq!(alloc2.offset, 1024); // Second frame starts at 1024
397    }
398
399    #[test]
400    fn test_staging_pool() {
401        let ctx = GraphicsContext::new_owned_sync().expect("Failed to create graphics context");
402        let mut pool = StagingBufferPool::new();
403
404        // Allocate a buffer
405        let buffer1 = pool.allocate(&ctx, 1024);
406        assert_eq!(buffer1.size(), 1024);
407        assert_eq!(pool.available_count(), 0);
408
409        // Return it to pool
410        pool.recycle(buffer1);
411        assert_eq!(pool.available_count(), 1);
412
413        // Allocate again - should reuse
414        let buffer2 = pool.allocate(&ctx, 1024);
415        assert_eq!(buffer2.size(), 1024);
416        assert_eq!(pool.available_count(), 0);
417
418        pool.recycle(buffer2);
419    }
420
421    #[test]
422    fn test_staging_pool_size_matching() {
423        let ctx = GraphicsContext::new_owned_sync().expect("Failed to create graphics context");
424        let mut pool = StagingBufferPool::new();
425
426        // Add buffers of different sizes
427        pool.recycle(StagingBuffer::new(&ctx, 512));
428        pool.recycle(StagingBuffer::new(&ctx, 1024));
429        pool.recycle(StagingBuffer::new(&ctx, 2048));
430
431        // Request 600 bytes - should get the 1024 buffer (smallest that fits)
432        let buffer = pool.allocate(&ctx, 600);
433        assert_eq!(buffer.size(), 1024);
434        assert_eq!(pool.available_count(), 2);
435    }
436}