Skip to main content

appcore_render/
assets.rs

1// =============================================================================
2//        #######
3//     ###       ###     F: assets.rs
4//    ##   ## ##   ##    P: AppCore-Runtime
5//         ## ##
6//                       C: 2026/10/14 17:50:33 by dnettoRaw
7//    ##   ## ##   ##    U: 2026/10/15 10:24:40 by dnettoRaw
8//      ###########      S: 0.1.0-alpha.1
9// =============================================================================
10
11use crate::{
12    asset_data::{bounds, AssetLease, Material},
13    scene_storage::reserved,
14    MeshAsset, MeshVertex, PreparedInstance, PreparedScene, RenderError, RenderLimits,
15    ResourceArena, ResourceId, SceneSnapshot,
16};
17use std::sync::{
18    atomic::{AtomicU64, Ordering},
19    Arc,
20};
21/// Bounded static asset/CPU preparation profile, separate from GPU/driver allocations.
22#[derive(Debug, Clone, Copy)]
23pub struct AssetLimits {
24    /// Maximum registered meshes and materials each, in 1..65536.
25    pub max_assets: u32,
26    /// Per-mesh vertex count/capacity, in 3..65536.
27    pub max_vertices: u32,
28    /// Per-mesh index count/capacity, in 3..196608.
29    pub max_indices: u32,
30    /// Instances prepared per frame, in 1..65536.
31    pub max_instances: u32,
32    /// Shared payload ceiling, including assets retired but held by prepared frames.
33    pub managed_cpu_bytes: u64,
34}
35impl Default for AssetLimits {
36    fn default() -> Self {
37        Self {
38            max_assets: 512,
39            max_vertices: 65536,
40            max_indices: 196608,
41            max_instances: 8192,
42            managed_cpu_bytes: 128 * 1024 * 1024,
43        }
44    }
45}
46impl AssetLimits {
47    /// Validate limits before creating any retained table.
48    pub fn validate(self) -> Result<Self, RenderError> {
49        if !(1..=65536).contains(&self.max_assets)
50            || !(3..=65536).contains(&self.max_vertices)
51            || !(3..=196608).contains(&self.max_indices)
52            || !(1..=65536).contains(&self.max_instances)
53            || self.managed_cpu_bytes == 0
54            || self.managed_cpu_bytes > 512 * 1024 * 1024
55        {
56            return Err(RenderError::InvalidLimits);
57        }
58        Ok(self)
59    }
60}
61/// Static mesh/material registry. IDs are typed by separate arenas and generations.
62/// Geometry is immutable; deleting a registry entry does not invalidate already prepared frames.
63pub struct AssetRegistry {
64    limits: AssetLimits,
65    meshes: ResourceArena<Arc<MeshAsset>>,
66    materials: ResourceArena<Arc<Material>>,
67    bytes: Arc<AtomicU64>,
68}
69impl AssetRegistry {
70    /// Initialize explicit per-registry limits; no file/network access or GPU allocation.
71    pub fn new(limits: AssetLimits) -> Result<Self, RenderError> {
72        let limits = limits.validate()?;
73        let arena = RenderLimits {
74            resource_slots: limits.max_assets,
75            managed_bytes: limits.managed_cpu_bytes,
76            ..Default::default()
77        };
78        Ok(Self {
79            limits,
80            meshes: ResourceArena::new(arena)?,
81            materials: ResourceArena::new(arena)?,
82            bytes: Arc::new(AtomicU64::new(0)),
83        })
84    }
85    fn reserve(&self, bytes: u64) -> Result<AssetLease, RenderError> {
86        self.bytes
87            .fetch_update(Ordering::AcqRel, Ordering::Acquire, |n| {
88                n.checked_add(bytes)
89                    .filter(|total| *total <= self.limits.managed_cpu_bytes)
90            })
91            .map_err(|_| RenderError::BudgetExceeded)?;
92        Ok(AssetLease {
93            counter: self.bytes.clone(),
94            bytes,
95        })
96    }
97    /// Accounted CPU payload, including prepared frames and retired shared assets.
98    pub fn managed_bytes(&self) -> u64 {
99        self.bytes.load(Ordering::Acquire)
100    }
101    /// Retain validated static triangles. Both Vec capacities are checked/accounted, not just length.
102    pub fn register_mesh(
103        &mut self,
104        vertices: Vec<MeshVertex>,
105        indices: Vec<u32>,
106    ) -> Result<ResourceId, RenderError> {
107        if vertices.len() < 3
108            || vertices.capacity() > self.limits.max_vertices as usize
109            || indices.is_empty()
110            || !indices.len().is_multiple_of(3)
111            || indices.capacity() > self.limits.max_indices as usize
112            || indices.iter().any(|i| *i as usize >= vertices.len())
113        {
114            return Err(RenderError::InvalidLimits);
115        }
116        for triangle in indices.chunks_exact(3) {
117            let a = vertices[triangle[0] as usize].position();
118            let b = vertices[triangle[1] as usize].position();
119            let c = vertices[triangle[2] as usize].position();
120            let u: [f32; 3] = std::array::from_fn(|i| b[i] - a[i]);
121            let v: [f32; 3] = std::array::from_fn(|i| c[i] - a[i]);
122            let cross = [
123                u[1] * v[2] - u[2] * v[1],
124                u[2] * v[0] - u[0] * v[2],
125                u[0] * v[1] - u[1] * v[0],
126            ];
127            if cross.iter().all(|v| *v == 0.0) {
128                return Err(RenderError::InvalidLimits);
129            }
130        }
131        let bytes = vertices.capacity() as u64 * std::mem::size_of::<MeshVertex>() as u64
132            + indices.capacity() as u64 * 4;
133        let lease = self.reserve(bytes)?;
134        let sphere = bounds(&vertices);
135        self.meshes.insert(
136            Arc::new(MeshAsset {
137                vertices,
138                indices,
139                bounds: sphere,
140                _lease: lease,
141            }),
142            bytes,
143        )
144    }
145    /// Register finite linear RGBA in 0..1; no implicit clamping or textures in this profile.
146    pub fn register_material(&mut self, color: [f32; 4]) -> Result<ResourceId, RenderError> {
147        if !color
148            .iter()
149            .all(|v| v.is_finite() && (0.0..=1.0).contains(v))
150        {
151            return Err(RenderError::InvalidLimits);
152        }
153        let bytes = 16;
154        let lease = self.reserve(bytes)?;
155        self.materials.insert(
156            Arc::new(Material {
157                color,
158                _lease: lease,
159            }),
160            bytes,
161        )
162    }
163    /// Remove registration; payload reservation remains until its last prepared owner drops.
164    pub fn remove_mesh(&mut self, id: ResourceId) -> Result<(), RenderError> {
165        self.meshes.remove(id)?;
166        Ok(())
167    }
168    /// Remove material registration without invalidating already prepared frames.
169    pub fn remove_material(&mut self, id: ResourceId) -> Result<(), RenderError> {
170        self.materials.remove(id)?;
171        Ok(())
172    }
173    /// Resolve every identity/selection before preparation. Static assets accept animation/morph 0 only.
174    /// Consumes snapshot admission on success and releases it on controlled failure.
175    pub fn prepare(&self, snapshot: SceneSnapshot) -> Result<PreparedScene, RenderError> {
176        if snapshot.instances().len() > self.limits.max_instances as usize {
177            return Err(RenderError::BudgetExceeded);
178        }
179        for instance in snapshot.instances() {
180            self.meshes.get(instance.binding.mesh)?;
181            self.materials.get(instance.binding.material)?;
182            if instance.binding.animation != 0 || instance.binding.morph != 0 {
183                return Err(RenderError::InvalidResource);
184            }
185            instance.binding.validate()?;
186        }
187        let lease = self.reserve(
188            snapshot.instances().len() as u64 * std::mem::size_of::<PreparedInstance>() as u64,
189        )?;
190        let mut instances = reserved(snapshot.instances().len())?;
191        for instance in snapshot.instances() {
192            instances.push(PreparedInstance {
193                entity: instance.entity,
194                transform: instance.transform,
195                mesh_id: instance.binding.mesh,
196                mesh: self.meshes.get(instance.binding.mesh)?.clone(),
197                material: self.materials.get(instance.binding.material)?.clone(),
198                tint: instance.binding.tint,
199            });
200        }
201        Ok(PreparedScene {
202            instances,
203            _snapshot: snapshot,
204            _lease: lease,
205        })
206    }
207}