Skip to main content

appcore_render/
resources.rs

1// =============================================================================
2//        #######
3//     ###       ###     F: resources.rs
4//    ##   ## ##   ##    P: AppCore-Runtime
5//         ## ##
6//                       C: 2026/10/14 16:20:57 by dnettoRaw
7//    ##   ## ##   ##    U: 2026/10/15 10:24:40 by dnettoRaw
8//      ###########      S: 0.1.0-alpha.1
9// =============================================================================
10
11use crate::{RenderError, RenderLimits};
12use std::sync::atomic::{AtomicU64, Ordering};
13static NEXT_OWNER: AtomicU64 = AtomicU64::new(1);
14
15/// Opaque arena-scoped generational identity; carries no pointer or authority.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct ResourceId {
18    owner: u64,
19    index: u32,
20    generation: u64,
21}
22struct Slot<T> {
23    generation: u64,
24    value: Option<T>,
25    bytes: u64,
26}
27/// Bounded resource ownership. GPU users must remove only after last submission completes.
28/// This arena does not infer GPU completion or provide a submission timeline.
29pub struct ResourceArena<T> {
30    owner: u64,
31    limits: RenderLimits,
32    slots: Vec<Slot<T>>,
33    bytes: u64,
34}
35impl<T> ResourceArena<T> {
36    /// Create a validated empty arena without preallocating payload storage.
37    pub fn new(limits: RenderLimits) -> Result<Self, RenderError> {
38        let limits = limits.validate()?;
39        let owner = NEXT_OWNER
40            .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |n| n.checked_add(1))
41            .map_err(|_| RenderError::IdentityExhausted)?;
42        Ok(Self {
43            owner,
44            limits,
45            slots: Vec::new(),
46            bytes: 0,
47        })
48    }
49    /// Admit a resource with its accounted bytes; rejects before arena mutation.
50    pub fn insert(&mut self, value: T, bytes: u64) -> Result<ResourceId, RenderError> {
51        let total = self
52            .bytes
53            .checked_add(bytes)
54            .ok_or(RenderError::BudgetExceeded)?;
55        if total > self.limits.managed_bytes {
56            return Err(RenderError::BudgetExceeded);
57        }
58        let index = if let Some(i) = self
59            .slots
60            .iter()
61            .position(|s| s.value.is_none() && s.generation < u64::MAX)
62        {
63            i
64        } else {
65            if self.slots.len() >= self.limits.resource_slots as usize {
66                return Err(RenderError::BudgetExceeded);
67            }
68            self.slots.push(Slot {
69                generation: 0,
70                value: None,
71                bytes: 0,
72            });
73            self.slots.len() - 1
74        };
75        let slot = &mut self.slots[index];
76        slot.generation += 1;
77        slot.value = Some(value);
78        slot.bytes = bytes;
79        self.bytes = total;
80        Ok(ResourceId {
81            owner: self.owner,
82            index: index as u32,
83            generation: slot.generation,
84        })
85    }
86    fn slot(&self, id: ResourceId) -> Result<&Slot<T>, RenderError> {
87        if id.owner != self.owner {
88            return Err(RenderError::InvalidResource);
89        }
90        self.slots
91            .get(id.index as usize)
92            .filter(|s| s.generation == id.generation && s.value.is_some())
93            .ok_or(RenderError::InvalidResource)
94    }
95    /// Resolve only a live handle from this arena.
96    pub fn get(&self, id: ResourceId) -> Result<&T, RenderError> {
97        self.slot(id)?
98            .value
99            .as_ref()
100            .ok_or(RenderError::InvalidResource)
101    }
102    /// Remove a resource. Caller must first prove GPU completion for GPU-owned values.
103    pub fn remove(&mut self, id: ResourceId) -> Result<T, RenderError> {
104        self.slot(id)?;
105        let slot = &mut self.slots[id.index as usize];
106        let value = slot.value.take().ok_or(RenderError::InvalidResource)?;
107        self.bytes -= slot.bytes;
108        slot.bytes = 0;
109        Ok(value)
110    }
111    /// Current accounted live bytes; not total driver allocation.
112    pub fn managed_bytes(&self) -> u64 {
113        self.bytes
114    }
115}