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