oxiui_render_wgpu/resource.rs
1//! Generation-checked resource handles and reference-counted registry.
2//!
3//! Resources (textures, shaders) are identified by [`TextureHandle`] and
4//! [`ShaderHandle`] — lightweight generation-checked tokens. The
5//! [`ResourceRegistry`] tracks reference counts and recycles slots so the
6//! same index can be safely reused without stale-handle confusion.
7//!
8//! RAII wrappers ([`TextureGuard`], [`ShaderGuard`]) automatically decrement
9//! the reference count when dropped.
10
11use std::cell::RefCell;
12use std::rc::Rc;
13
14// ── ResourceId ────────────────────────────────────────────────────────────────
15
16/// A generation-checked resource identifier.
17///
18/// The `gen` field is bumped each time a slot is recycled so that a stale
19/// handle cannot accidentally alias a newly allocated resource.
20#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
21pub struct ResourceId {
22 /// Generation counter for this slot.
23 pub gen: u32,
24 /// Index into the backing store.
25 pub idx: u32,
26}
27
28// ── Handle newtypes ───────────────────────────────────────────────────────────
29
30/// An opaque handle to a GPU texture resource.
31#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
32pub struct TextureHandle(
33 /// The underlying resource identifier.
34 pub ResourceId,
35);
36
37/// An opaque handle to a GPU shader resource.
38#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
39pub struct ShaderHandle(
40 /// The underlying resource identifier.
41 pub ResourceId,
42);
43
44// ── ResourceEntry ─────────────────────────────────────────────────────────────
45
46/// A slot in the registry backing store.
47struct ResourceEntry {
48 /// Stored value (e.g. an asset name or path).
49 value: String,
50 /// Live reference count. Zero means the slot is free.
51 ref_count: u32,
52 /// Current slot generation — incremented when the slot is recycled.
53 gen: u32,
54}
55
56// ── ResourceRegistry ──────────────────────────────────────────────────────────
57
58/// Shared registry of GPU textures and shaders, with generation-checked handles
59/// and reference counting.
60///
61/// Slots are recycled via a free list when their reference count reaches zero.
62/// The generation counter prevents stale handles from aliasing new allocations.
63pub struct ResourceRegistry {
64 /// Storage for texture entries.
65 texture_store: Vec<ResourceEntry>,
66 /// Storage for shader entries.
67 shader_store: Vec<ResourceEntry>,
68 /// Indices of free texture slots (ref_count == 0).
69 free_texture: Vec<usize>,
70 /// Indices of free shader slots (ref_count == 0).
71 free_shader: Vec<usize>,
72}
73
74impl ResourceRegistry {
75 /// Construct an empty registry.
76 pub fn new() -> Self {
77 Self {
78 texture_store: Vec::new(),
79 shader_store: Vec::new(),
80 free_texture: Vec::new(),
81 free_shader: Vec::new(),
82 }
83 }
84
85 // ── Texture API ──────────────────────────────────────────────────────────
86
87 /// Allocate a texture slot with an initial reference count of 1.
88 pub fn alloc_texture(&mut self, name: String) -> TextureHandle {
89 let id = if let Some(idx) = self.free_texture.pop() {
90 let entry = &mut self.texture_store[idx];
91 entry.value = name;
92 entry.ref_count = 1;
93 ResourceId {
94 gen: entry.gen,
95 idx: idx as u32,
96 }
97 } else {
98 let idx = self.texture_store.len();
99 self.texture_store.push(ResourceEntry {
100 value: name,
101 ref_count: 1,
102 gen: 0,
103 });
104 ResourceId {
105 gen: 0,
106 idx: idx as u32,
107 }
108 };
109 TextureHandle(id)
110 }
111
112 /// Increment the reference count of `h`.
113 ///
114 /// Returns `false` if the handle is stale (generation mismatch or
115 /// out-of-bounds); returns `true` on success.
116 pub fn retain_texture(&mut self, h: TextureHandle) -> bool {
117 let id = h.0;
118 let idx = id.idx as usize;
119 match self.texture_store.get_mut(idx) {
120 Some(entry) if entry.gen == id.gen && entry.ref_count > 0 => {
121 entry.ref_count += 1;
122 true
123 }
124 _ => false,
125 }
126 }
127
128 /// Decrement the reference count of `h`.
129 ///
130 /// When the count reaches zero the slot's generation is bumped and the
131 /// slot is pushed onto the free list for reuse. Stale or already-freed
132 /// handles are silently ignored.
133 pub fn release_texture(&mut self, h: TextureHandle) {
134 let id = h.0;
135 let idx = id.idx as usize;
136 if let Some(entry) = self.texture_store.get_mut(idx) {
137 if entry.gen == id.gen && entry.ref_count > 0 {
138 entry.ref_count -= 1;
139 if entry.ref_count == 0 {
140 entry.gen = entry.gen.wrapping_add(1);
141 self.free_texture.push(idx);
142 }
143 }
144 }
145 }
146
147 /// Return the name associated with `h`, or `None` if stale.
148 pub fn get_texture(&self, h: TextureHandle) -> Option<&str> {
149 let id = h.0;
150 let idx = id.idx as usize;
151 let entry = self.texture_store.get(idx)?;
152 if entry.gen == id.gen && entry.ref_count > 0 {
153 Some(&entry.value)
154 } else {
155 None
156 }
157 }
158
159 // ── Shader API ───────────────────────────────────────────────────────────
160
161 /// Allocate a shader slot with an initial reference count of 1.
162 pub fn alloc_shader(&mut self, name: String) -> ShaderHandle {
163 let id = if let Some(idx) = self.free_shader.pop() {
164 let entry = &mut self.shader_store[idx];
165 entry.value = name;
166 entry.ref_count = 1;
167 ResourceId {
168 gen: entry.gen,
169 idx: idx as u32,
170 }
171 } else {
172 let idx = self.shader_store.len();
173 self.shader_store.push(ResourceEntry {
174 value: name,
175 ref_count: 1,
176 gen: 0,
177 });
178 ResourceId {
179 gen: 0,
180 idx: idx as u32,
181 }
182 };
183 ShaderHandle(id)
184 }
185
186 /// Increment the reference count of `h`.
187 ///
188 /// Returns `false` if the handle is stale; returns `true` on success.
189 pub fn retain_shader(&mut self, h: ShaderHandle) -> bool {
190 let id = h.0;
191 let idx = id.idx as usize;
192 match self.shader_store.get_mut(idx) {
193 Some(entry) if entry.gen == id.gen && entry.ref_count > 0 => {
194 entry.ref_count += 1;
195 true
196 }
197 _ => false,
198 }
199 }
200
201 /// Decrement the reference count of `h`.
202 ///
203 /// When the count reaches zero the slot is freed and the generation is
204 /// bumped. Stale handles are silently ignored.
205 pub fn release_shader(&mut self, h: ShaderHandle) {
206 let id = h.0;
207 let idx = id.idx as usize;
208 if let Some(entry) = self.shader_store.get_mut(idx) {
209 if entry.gen == id.gen && entry.ref_count > 0 {
210 entry.ref_count -= 1;
211 if entry.ref_count == 0 {
212 entry.gen = entry.gen.wrapping_add(1);
213 self.free_shader.push(idx);
214 }
215 }
216 }
217 }
218
219 /// Return the name associated with `h`, or `None` if stale.
220 pub fn get_shader(&self, h: ShaderHandle) -> Option<&str> {
221 let id = h.0;
222 let idx = id.idx as usize;
223 let entry = self.shader_store.get(idx)?;
224 if entry.gen == id.gen && entry.ref_count > 0 {
225 Some(&entry.value)
226 } else {
227 None
228 }
229 }
230}
231
232impl Default for ResourceRegistry {
233 fn default() -> Self {
234 Self::new()
235 }
236}
237
238// ── RAII guards ───────────────────────────────────────────────────────────────
239
240/// RAII wrapper that releases a [`TextureHandle`] when dropped.
241pub struct TextureGuard {
242 /// The guarded texture handle.
243 pub handle: TextureHandle,
244 /// Shared access to the registry that owns `handle`.
245 registry: Rc<RefCell<ResourceRegistry>>,
246}
247
248impl TextureGuard {
249 /// Construct a guard that will release `handle` on drop.
250 pub fn new(handle: TextureHandle, registry: Rc<RefCell<ResourceRegistry>>) -> Self {
251 Self { handle, registry }
252 }
253}
254
255impl Drop for TextureGuard {
256 fn drop(&mut self) {
257 self.registry.borrow_mut().release_texture(self.handle);
258 }
259}
260
261/// RAII wrapper that releases a [`ShaderHandle`] when dropped.
262pub struct ShaderGuard {
263 /// The guarded shader handle.
264 pub handle: ShaderHandle,
265 /// Shared access to the registry that owns `handle`.
266 registry: Rc<RefCell<ResourceRegistry>>,
267}
268
269impl ShaderGuard {
270 /// Construct a guard that will release `handle` on drop.
271 pub fn new(handle: ShaderHandle, registry: Rc<RefCell<ResourceRegistry>>) -> Self {
272 Self { handle, registry }
273 }
274}
275
276impl Drop for ShaderGuard {
277 fn drop(&mut self) {
278 self.registry.borrow_mut().release_shader(self.handle);
279 }
280}
281
282// ── Tests ─────────────────────────────────────────────────────────────────────
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 #[test]
289 fn resource_alloc_retain_release_raii() {
290 let registry = Rc::new(RefCell::new(ResourceRegistry::new()));
291
292 // Allocate with ref_count = 1.
293 let handle = registry.borrow_mut().alloc_texture("tex_a".to_string());
294 assert_eq!(registry.borrow().get_texture(handle), Some("tex_a"));
295
296 // retain bumps to 2.
297 assert!(registry.borrow_mut().retain_texture(handle));
298
299 // First release → count 1, still live.
300 registry.borrow_mut().release_texture(handle);
301 assert_eq!(registry.borrow().get_texture(handle), Some("tex_a"));
302
303 // RAII guard releases the second ref on drop.
304 {
305 let _guard = TextureGuard::new(handle, Rc::clone(®istry));
306 }
307 // After guard drop, count == 0 → slot freed, handle stale.
308 assert_eq!(registry.borrow().get_texture(handle), None);
309 }
310
311 #[test]
312 fn resource_double_release_is_safe() {
313 let mut reg = ResourceRegistry::new();
314 let h = reg.alloc_texture("tex_b".to_string());
315 reg.release_texture(h);
316 // Second release on the now-stale handle must not panic.
317 reg.release_texture(h);
318 // Slot is freed; a new alloc should reuse it.
319 let h2 = reg.alloc_texture("tex_c".to_string());
320 assert_eq!(reg.get_texture(h2), Some("tex_c"));
321 // The old handle is still stale.
322 assert_eq!(reg.get_texture(h), None);
323 }
324}