ferravex_graphics/
cache.rs

1use crate::{Bounds, Mat4, Mesh, MeshError};
2
3/// Stable CPU mesh handle; generation prevents reuse of a released slot.
4#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
5pub struct MeshId {
6    pub index: u32,
7    pub generation: u64,
8}
9struct Entry {
10    mesh: Mesh,
11    bounds: Bounds,
12}
13struct Slot {
14    generation: u64,
15    entry: Option<Entry>,
16}
17#[derive(Default)]
18pub struct MeshCache {
19    slots: Vec<Slot>,
20    free: Vec<u32>,
21    live: usize,
22}
23#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
24pub struct CacheStats {
25    pub meshes: usize,
26    pub allocated_slots: usize,
27    pub vertices: usize,
28    pub indices: usize,
29}
30impl MeshCache {
31    pub fn new() -> Self {
32        Self::default()
33    }
34    pub fn insert(&mut self, mesh: Mesh) -> Result<MeshId, MeshError> {
35        let bounds = mesh.validate()?;
36        let entry = Some(Entry { mesh, bounds });
37        let id = if let Some(index) = self.free.pop() {
38            let slot = &mut self.slots[index as usize];
39            slot.entry = entry;
40            MeshId {
41                index,
42                generation: slot.generation,
43            }
44        } else {
45            let index = u32::try_from(self.slots.len()).map_err(|_| MeshError::TooLarge)?;
46            self.slots.push(Slot {
47                generation: 1,
48                entry,
49            });
50            MeshId {
51                index,
52                generation: 1,
53            }
54        };
55        self.live += 1;
56        Ok(id)
57    }
58    pub fn get(&self, id: MeshId) -> Option<&Mesh> {
59        self.entry(id).map(|entry| &entry.mesh)
60    }
61    pub fn bounds(&self, id: MeshId) -> Option<Bounds> {
62        self.entry(id).map(|entry| entry.bounds)
63    }
64    fn entry(&self, id: MeshId) -> Option<&Entry> {
65        let slot = self.slots.get(id.index as usize)?;
66        if slot.generation != id.generation {
67            return None;
68        }
69        slot.entry.as_ref()
70    }
71    /// Replaces geometry in-place after complete validation. Old data survives a bad update.
72    pub fn update(&mut self, id: MeshId, mesh: Mesh) -> Result<bool, MeshError> {
73        let bounds = mesh.validate()?;
74        let Some(slot) = self.slots.get_mut(id.index as usize) else {
75            return Ok(false);
76        };
77        if slot.generation != id.generation || slot.entry.is_none() {
78            return Ok(false);
79        }
80        slot.entry = Some(Entry { mesh, bounds });
81        Ok(true)
82    }
83    pub fn remove(&mut self, id: MeshId) -> Option<Mesh> {
84        let slot = self.slots.get_mut(id.index as usize)?;
85        if slot.generation != id.generation {
86            return None;
87        }
88        let entry = slot.entry.take()?;
89        self.live -= 1;
90        // Retire the slot rather than wrapping a generation and reviving stale handles.
91        if let Some(next) = slot.generation.checked_add(1) {
92            slot.generation = next;
93            self.free.push(id.index);
94        }
95        Some(entry.mesh)
96    }
97    pub fn stats(&self) -> CacheStats {
98        let mut result = CacheStats {
99            meshes: self.live,
100            allocated_slots: self.slots.len(),
101            ..Default::default()
102        };
103        for entry in self.slots.iter().filter_map(|slot| slot.entry.as_ref()) {
104            result.vertices += entry.mesh.vertices.len();
105            result.indices += entry.mesh.indices.len();
106        }
107        result
108    }
109}
110#[derive(Clone, Copy, Debug)]
111pub struct RenderItem {
112    pub mesh: MeshId,
113    pub transform: Mat4,
114}
115impl RenderItem {
116    pub const fn new(mesh: MeshId, transform: Mat4) -> Self {
117        Self { mesh, transform }
118    }
119}
120#[derive(Debug)]
121pub struct InstanceBatch {
122    pub mesh: MeshId,
123    pub transforms: Vec<Mat4>,
124}
125/// Groups identical mesh handles. This is CPU submission grouping, not a GPU draw call.
126pub fn instance_batches(items: &[RenderItem]) -> Vec<InstanceBatch> {
127    let mut sorted = items.to_vec();
128    sorted.sort_by_key(|item| item.mesh);
129    let mut batches: Vec<InstanceBatch> = Vec::new();
130    for item in sorted {
131        if let Some(last) = batches.last_mut().filter(|batch| batch.mesh == item.mesh) {
132            last.transforms.push(item.transform);
133        } else {
134            batches.push(InstanceBatch {
135                mesh: item.mesh,
136                transforms: vec![item.transform],
137            });
138        }
139    }
140    batches
141}