Skip to main content

oximedia_gpu/
gpu_buffer.rs

1//! GPU buffer management for `oximedia-gpu`.
2//!
3//! Provides buffer usage flags, a logical `GpuBuffer` abstraction, and a
4//! pooled allocator (`GpuBufferPool`) that recycles buffers to reduce
5//! allocation overhead.
6
7#![allow(dead_code)]
8
9/// Describes how a GPU buffer will be used by the pipeline.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub enum BufferUsage {
12    /// Read-only storage, typically for shader inputs.
13    StorageRead,
14    /// Read/write storage, for shader outputs or in-place ops.
15    StorageReadWrite,
16    /// Uniform/constant data uploaded from the CPU.
17    Uniform,
18    /// Staging buffer for CPU→GPU uploads.
19    Upload,
20    /// Staging buffer for GPU→CPU readback.
21    Readback,
22    /// Index buffer for draw calls.
23    Index,
24    /// Vertex buffer for draw calls.
25    Vertex,
26}
27
28impl BufferUsage {
29    /// Returns `true` when the pipeline may write to this buffer.
30    #[must_use]
31    pub fn is_writable(&self) -> bool {
32        matches!(self, Self::StorageReadWrite | Self::Upload | Self::Readback)
33    }
34
35    /// Returns `true` when the buffer is used for transferring data between
36    /// CPU and GPU (upload or readback).
37    #[must_use]
38    pub fn is_staging(&self) -> bool {
39        matches!(self, Self::Upload | Self::Readback)
40    }
41
42    /// Returns a human-readable label.
43    #[must_use]
44    pub fn label(&self) -> &'static str {
45        match self {
46            Self::StorageRead => "storage_read",
47            Self::StorageReadWrite => "storage_read_write",
48            Self::Uniform => "uniform",
49            Self::Upload => "upload",
50            Self::Readback => "readback",
51            Self::Index => "index",
52            Self::Vertex => "vertex",
53        }
54    }
55}
56
57/// A logical GPU buffer (CPU-side descriptor; no actual WGPU resource here).
58#[derive(Debug, Clone)]
59pub struct GpuBuffer {
60    /// Unique identifier assigned by the pool.
61    pub id: u64,
62    /// Intended usage.
63    pub usage: BufferUsage,
64    /// Allocated size in bytes.
65    size: usize,
66    /// Whether this buffer is currently mapped for CPU access.
67    mapped: bool,
68    /// Debug label shown in GPU profiling tools.
69    pub label: Option<String>,
70}
71
72impl GpuBuffer {
73    /// Creates a new GPU buffer descriptor.
74    #[must_use]
75    pub fn new(id: u64, usage: BufferUsage, size: usize) -> Self {
76        Self {
77            id,
78            usage,
79            size,
80            mapped: false,
81            label: None,
82        }
83    }
84
85    /// Creates a buffer with a debug label.
86    #[must_use]
87    pub fn with_label(mut self, label: impl Into<String>) -> Self {
88        self.label = Some(label.into());
89        self
90    }
91
92    /// Returns the buffer size in bytes.
93    #[must_use]
94    pub fn size_bytes(&self) -> usize {
95        self.size
96    }
97
98    /// Returns `true` if the buffer is currently mapped for CPU access.
99    #[must_use]
100    pub fn is_mapped(&self) -> bool {
101        self.mapped
102    }
103
104    /// Simulates mapping the buffer for CPU access.
105    ///
106    /// Returns an error string if the buffer usage does not allow mapping.
107    pub fn map(&mut self) -> Result<(), String> {
108        if !self.usage.is_staging() {
109            return Err(format!(
110                "Buffer (usage={}) cannot be mapped; only Upload/Readback buffers support mapping.",
111                self.usage.label()
112            ));
113        }
114        self.mapped = true;
115        Ok(())
116    }
117
118    /// Unmaps the buffer (no-op if not mapped).
119    pub fn unmap(&mut self) {
120        self.mapped = false;
121    }
122
123    /// Returns the size in 4-byte aligned units (useful for uniform offsets).
124    #[must_use]
125    pub fn aligned_size(&self) -> usize {
126        (self.size + 3) & !3
127    }
128}
129
130/// A simple pool that allocates and recycles [`GpuBuffer`] instances.
131#[derive(Debug, Default)]
132pub struct GpuBufferPool {
133    next_id: u64,
134    active: Vec<GpuBuffer>,
135    free_list: Vec<GpuBuffer>,
136}
137
138impl GpuBufferPool {
139    /// Creates a new, empty buffer pool.
140    #[must_use]
141    pub fn new() -> Self {
142        Self::default()
143    }
144
145    /// Allocates a buffer of the given usage and size.
146    ///
147    /// If a compatible free buffer exists it is reused; otherwise a new one
148    /// is created.
149    pub fn allocate(&mut self, usage: BufferUsage, size: usize) -> GpuBuffer {
150        // Try to recycle a free buffer with the same usage and sufficient size.
151        if let Some(pos) = self
152            .free_list
153            .iter()
154            .position(|b| b.usage == usage && b.size_bytes() >= size)
155        {
156            return self.free_list.remove(pos);
157        }
158        // Allocate a new buffer.
159        let id = self.next_id;
160        self.next_id += 1;
161        let buf = GpuBuffer::new(id, usage, size);
162        self.active.push(buf.clone());
163        buf
164    }
165
166    /// Returns a buffer to the pool for future reuse.
167    pub fn release(&mut self, mut buf: GpuBuffer) {
168        buf.unmap(); // ensure it is not left mapped
169                     // Remove from active list (best-effort; id may not be present if already released)
170        self.active.retain(|b| b.id != buf.id);
171        self.free_list.push(buf);
172    }
173
174    /// Returns the total number of bytes currently allocated across all active
175    /// and free buffers.
176    #[must_use]
177    pub fn total_allocated(&self) -> usize {
178        let active: usize = self.active.iter().map(GpuBuffer::size_bytes).sum();
179        let free: usize = self.free_list.iter().map(GpuBuffer::size_bytes).sum();
180        active + free
181    }
182
183    /// Returns the number of active (in-use) buffers.
184    #[must_use]
185    pub fn active_count(&self) -> usize {
186        self.active.len()
187    }
188
189    /// Returns the number of buffers waiting in the free list.
190    #[must_use]
191    pub fn free_count(&self) -> usize {
192        self.free_list.len()
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    #[test]
201    fn test_buffer_usage_is_writable_storage_rw() {
202        assert!(BufferUsage::StorageReadWrite.is_writable());
203    }
204
205    #[test]
206    fn test_buffer_usage_not_writable_storage_read() {
207        assert!(!BufferUsage::StorageRead.is_writable());
208    }
209
210    #[test]
211    fn test_buffer_usage_is_staging_upload() {
212        assert!(BufferUsage::Upload.is_staging());
213    }
214
215    #[test]
216    fn test_buffer_usage_not_staging_uniform() {
217        assert!(!BufferUsage::Uniform.is_staging());
218    }
219
220    #[test]
221    fn test_buffer_usage_label_non_empty() {
222        for usage in [
223            BufferUsage::StorageRead,
224            BufferUsage::StorageReadWrite,
225            BufferUsage::Uniform,
226            BufferUsage::Upload,
227            BufferUsage::Readback,
228            BufferUsage::Index,
229            BufferUsage::Vertex,
230        ] {
231            assert!(!usage.label().is_empty());
232        }
233    }
234
235    #[test]
236    fn test_gpu_buffer_size_bytes() {
237        let b = GpuBuffer::new(0, BufferUsage::Uniform, 256);
238        assert_eq!(b.size_bytes(), 256);
239    }
240
241    #[test]
242    fn test_gpu_buffer_not_mapped_by_default() {
243        let b = GpuBuffer::new(0, BufferUsage::Upload, 1024);
244        assert!(!b.is_mapped());
245    }
246
247    #[test]
248    fn test_gpu_buffer_map_upload_ok() {
249        let mut b = GpuBuffer::new(0, BufferUsage::Upload, 1024);
250        assert!(b.map().is_ok());
251        assert!(b.is_mapped());
252    }
253
254    #[test]
255    fn test_gpu_buffer_map_non_staging_err() {
256        let mut b = GpuBuffer::new(0, BufferUsage::StorageRead, 512);
257        assert!(b.map().is_err());
258    }
259
260    #[test]
261    fn test_gpu_buffer_unmap_clears_flag() {
262        let mut b = GpuBuffer::new(0, BufferUsage::Readback, 512);
263        b.map().expect("buffer map should succeed");
264        b.unmap();
265        assert!(!b.is_mapped());
266    }
267
268    #[test]
269    fn test_gpu_buffer_aligned_size() {
270        let b = GpuBuffer::new(0, BufferUsage::Uniform, 13);
271        assert_eq!(b.aligned_size(), 16);
272    }
273
274    #[test]
275    fn test_pool_allocate_creates_buffer() {
276        let mut pool = GpuBufferPool::new();
277        let buf = pool.allocate(BufferUsage::StorageRead, 1024);
278        assert_eq!(buf.size_bytes(), 1024);
279        assert_eq!(buf.usage, BufferUsage::StorageRead);
280    }
281
282    #[test]
283    fn test_pool_release_and_reuse() {
284        let mut pool = GpuBufferPool::new();
285        let buf = pool.allocate(BufferUsage::Upload, 512);
286        let id = buf.id;
287        pool.release(buf);
288        let reused = pool.allocate(BufferUsage::Upload, 512);
289        assert_eq!(reused.id, id); // same buffer recycled
290    }
291
292    #[test]
293    fn test_pool_total_allocated() {
294        let mut pool = GpuBufferPool::new();
295        pool.allocate(BufferUsage::Uniform, 256);
296        pool.allocate(BufferUsage::Uniform, 512);
297        assert_eq!(pool.total_allocated(), 768);
298    }
299
300    #[test]
301    fn test_pool_free_count_after_release() {
302        let mut pool = GpuBufferPool::new();
303        let buf = pool.allocate(BufferUsage::Readback, 1024);
304        pool.release(buf);
305        assert_eq!(pool.free_count(), 1);
306    }
307}