Skip to main content

oximedia_gpu/
resource_manager.rs

1//! GPU resource allocation and lifetime tracking.
2#![allow(dead_code)]
3
4use std::collections::HashMap;
5
6/// Category of a GPU resource.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
8pub enum ResourceType {
9    /// Device-local buffer (lives on the GPU).
10    GpuBuffer,
11    /// Host-visible / staging buffer.
12    HostBuffer,
13    /// 2-D texture / image.
14    Texture2D,
15    /// 3-D texture / volume.
16    Texture3D,
17    /// Sampler object.
18    Sampler,
19    /// Shader resource view / descriptor.
20    Descriptor,
21}
22
23impl ResourceType {
24    /// Returns `true` for resources that reside in device (GPU) memory.
25    #[must_use]
26    pub fn is_gpu_memory(&self) -> bool {
27        matches!(self, Self::GpuBuffer | Self::Texture2D | Self::Texture3D)
28    }
29
30    /// Returns `true` for resources that are textures of any dimension.
31    #[must_use]
32    pub fn is_texture(&self) -> bool {
33        matches!(self, Self::Texture2D | Self::Texture3D)
34    }
35}
36
37/// Opaque handle representing an allocated GPU resource.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
39pub struct ResourceHandle {
40    id: u64,
41}
42
43impl ResourceHandle {
44    /// Create a new handle from a raw id.
45    #[must_use]
46    pub(crate) fn new(id: u64) -> Self {
47        Self { id }
48    }
49
50    /// Returns `true` if this handle has a non-zero id (i.e. was produced by
51    /// a successful allocation, not a default / null construction).
52    #[must_use]
53    pub fn is_valid(&self) -> bool {
54        self.id != 0
55    }
56
57    /// Raw numeric id (useful for logging).
58    #[must_use]
59    pub fn raw_id(&self) -> u64 {
60        self.id
61    }
62}
63
64/// Metadata stored alongside each allocated resource.
65#[derive(Debug, Clone)]
66struct ResourceMeta {
67    resource_type: ResourceType,
68    size_bytes: usize,
69    label: String,
70}
71
72/// Simple GPU resource manager that tracks allocations and their sizes.
73pub struct GpuResourceManager {
74    resources: HashMap<ResourceHandle, ResourceMeta>,
75    next_id: u64,
76}
77
78impl GpuResourceManager {
79    /// Create a new, empty resource manager.
80    #[must_use]
81    pub fn new() -> Self {
82        Self {
83            resources: HashMap::new(),
84            next_id: 1,
85        }
86    }
87
88    /// Allocate a new resource.
89    ///
90    /// Returns a valid [`ResourceHandle`] on success, or an error string if
91    /// `size_bytes` is zero.
92    pub fn allocate(
93        &mut self,
94        resource_type: ResourceType,
95        size_bytes: usize,
96        label: impl Into<String>,
97    ) -> Result<ResourceHandle, String> {
98        if size_bytes == 0 {
99            return Err("Cannot allocate a zero-byte resource".to_string());
100        }
101        let handle = ResourceHandle::new(self.next_id);
102        self.next_id += 1;
103        self.resources.insert(
104            handle,
105            ResourceMeta {
106                resource_type,
107                size_bytes,
108                label: label.into(),
109            },
110        );
111        Ok(handle)
112    }
113
114    /// Free a previously allocated resource.
115    ///
116    /// Returns `true` if the resource existed and was freed, `false` otherwise.
117    pub fn free(&mut self, handle: ResourceHandle) -> bool {
118        self.resources.remove(&handle).is_some()
119    }
120
121    /// Total bytes currently allocated in GPU memory (device-local resources).
122    #[allow(clippy::cast_precision_loss)]
123    #[must_use]
124    pub fn total_gpu_bytes(&self) -> usize {
125        self.resources
126            .values()
127            .filter(|m| m.resource_type.is_gpu_memory())
128            .map(|m| m.size_bytes)
129            .sum()
130    }
131
132    /// Total bytes across all resource types.
133    #[must_use]
134    pub fn total_bytes(&self) -> usize {
135        self.resources.values().map(|m| m.size_bytes).sum()
136    }
137
138    /// Number of live allocations.
139    #[must_use]
140    pub fn allocation_count(&self) -> usize {
141        self.resources.len()
142    }
143
144    /// Query the type of a resource by handle.
145    #[must_use]
146    pub fn resource_type(&self, handle: ResourceHandle) -> Option<ResourceType> {
147        self.resources.get(&handle).map(|m| m.resource_type)
148    }
149
150    /// Query the size of a resource in bytes.
151    #[must_use]
152    pub fn resource_size(&self, handle: ResourceHandle) -> Option<usize> {
153        self.resources.get(&handle).map(|m| m.size_bytes)
154    }
155
156    /// Query the label of a resource.
157    #[must_use]
158    pub fn resource_label(&self, handle: ResourceHandle) -> Option<&str> {
159        self.resources.get(&handle).map(|m| m.label.as_str())
160    }
161
162    /// Returns `true` if the handle refers to a live allocation.
163    #[must_use]
164    pub fn is_alive(&self, handle: ResourceHandle) -> bool {
165        self.resources.contains_key(&handle)
166    }
167}
168
169impl Default for GpuResourceManager {
170    fn default() -> Self {
171        Self::new()
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    fn make_manager() -> GpuResourceManager {
180        GpuResourceManager::new()
181    }
182
183    // --- ResourceType tests ---
184
185    #[test]
186    fn test_gpu_buffer_is_gpu_memory() {
187        assert!(ResourceType::GpuBuffer.is_gpu_memory());
188    }
189
190    #[test]
191    fn test_host_buffer_not_gpu_memory() {
192        assert!(!ResourceType::HostBuffer.is_gpu_memory());
193    }
194
195    #[test]
196    fn test_texture2d_is_gpu_memory() {
197        assert!(ResourceType::Texture2D.is_gpu_memory());
198    }
199
200    #[test]
201    fn test_sampler_not_gpu_memory() {
202        assert!(!ResourceType::Sampler.is_gpu_memory());
203    }
204
205    #[test]
206    fn test_texture3d_is_texture() {
207        assert!(ResourceType::Texture3D.is_texture());
208    }
209
210    #[test]
211    fn test_gpu_buffer_not_texture() {
212        assert!(!ResourceType::GpuBuffer.is_texture());
213    }
214
215    // --- ResourceHandle tests ---
216
217    #[test]
218    fn test_default_handle_invalid() {
219        let h = ResourceHandle::default();
220        assert!(!h.is_valid());
221    }
222
223    #[test]
224    fn test_allocated_handle_valid() {
225        let mut mgr = make_manager();
226        let h = mgr
227            .allocate(ResourceType::GpuBuffer, 1024, "buf")
228            .expect("allocation should succeed");
229        assert!(h.is_valid());
230    }
231
232    // --- GpuResourceManager tests ---
233
234    #[test]
235    fn test_allocate_returns_valid_handle() {
236        let mut mgr = make_manager();
237        let h = mgr
238            .allocate(ResourceType::Texture2D, 4096, "tex")
239            .expect("allocation should succeed");
240        assert!(h.is_valid());
241    }
242
243    #[test]
244    fn test_allocate_zero_bytes_returns_error() {
245        let mut mgr = make_manager();
246        assert!(mgr.allocate(ResourceType::GpuBuffer, 0, "bad").is_err());
247    }
248
249    #[test]
250    fn test_allocation_count_increases() {
251        let mut mgr = make_manager();
252        mgr.allocate(ResourceType::GpuBuffer, 256, "a")
253            .expect("allocation should succeed");
254        mgr.allocate(ResourceType::HostBuffer, 512, "b")
255            .expect("allocation should succeed");
256        assert_eq!(mgr.allocation_count(), 2);
257    }
258
259    #[test]
260    fn test_total_gpu_bytes_only_counts_gpu_resources() {
261        let mut mgr = make_manager();
262        mgr.allocate(ResourceType::GpuBuffer, 1024, "gpu")
263            .expect("allocation should succeed");
264        mgr.allocate(ResourceType::HostBuffer, 2048, "host")
265            .expect("operation should succeed in test");
266        assert_eq!(mgr.total_gpu_bytes(), 1024);
267    }
268
269    #[test]
270    fn test_total_bytes_counts_all() {
271        let mut mgr = make_manager();
272        mgr.allocate(ResourceType::GpuBuffer, 1024, "gpu")
273            .expect("allocation should succeed");
274        mgr.allocate(ResourceType::HostBuffer, 2048, "host")
275            .expect("operation should succeed in test");
276        assert_eq!(mgr.total_bytes(), 3072);
277    }
278
279    #[test]
280    fn test_free_removes_resource() {
281        let mut mgr = make_manager();
282        let h = mgr
283            .allocate(ResourceType::GpuBuffer, 512, "buf")
284            .expect("allocation should succeed");
285        assert!(mgr.free(h));
286        assert_eq!(mgr.allocation_count(), 0);
287    }
288
289    #[test]
290    fn test_free_invalid_handle_returns_false() {
291        let mut mgr = make_manager();
292        let h = ResourceHandle::default();
293        assert!(!mgr.free(h));
294    }
295
296    #[test]
297    fn test_resource_type_query() {
298        let mut mgr = make_manager();
299        let h = mgr
300            .allocate(ResourceType::Sampler, 64, "s")
301            .expect("allocation should succeed");
302        assert_eq!(mgr.resource_type(h), Some(ResourceType::Sampler));
303    }
304
305    #[test]
306    fn test_resource_size_query() {
307        let mut mgr = make_manager();
308        let h = mgr
309            .allocate(ResourceType::Texture2D, 8192, "t")
310            .expect("allocation should succeed");
311        assert_eq!(mgr.resource_size(h), Some(8192));
312    }
313
314    #[test]
315    fn test_resource_label_query() {
316        let mut mgr = make_manager();
317        let h = mgr
318            .allocate(ResourceType::GpuBuffer, 128, "my_label")
319            .expect("operation should succeed in test");
320        assert_eq!(mgr.resource_label(h), Some("my_label"));
321    }
322
323    #[test]
324    fn test_is_alive_after_free_is_false() {
325        let mut mgr = make_manager();
326        let h = mgr
327            .allocate(ResourceType::GpuBuffer, 256, "buf")
328            .expect("allocation should succeed");
329        mgr.free(h);
330        assert!(!mgr.is_alive(h));
331    }
332}