ferravex_render/
retained.rs

1//! Retained low-poly geometry and ordered instanced draw recording.
2use crate::{Mat4, Mesh, shaders::LitSettings};
3use std::{
4    cell::RefCell,
5    rc::{Rc, Weak},
6};
7
8const MAX_MESHES: usize = 1024;
9const MAX_BYTES: usize = 64 * 1024 * 1024;
10const MAX_INSTANCES: usize = 65536;
11#[derive(Clone)]
12pub struct ResidentMesh(Rc<Geometry>);
13pub(crate) struct Geometry {
14    pub id: u32,
15    pub vertices: Vec<[f32; 10]>,
16    opaque: bool,
17    bounds: ferravex_graphics::Bounds,
18}
19#[derive(Default)]
20struct Registry {
21    next: u32,
22    meshes: Vec<Weak<Geometry>>,
23}
24thread_local! {static REGISTRY: RefCell<Registry> = RefCell::new(Registry::default());}
25impl ResidentMesh {
26    /// Snapshot validated geometry. Changes to the source Mesh require a new snapshot.
27    pub fn new(mesh: &Mesh) -> Result<Self, &'static str> {
28        if mesh.indices.is_empty() || !mesh.indices.len().is_multiple_of(3) {
29            return Err("mesh requires complete triangles");
30        }
31        if mesh.indices.len() > MAX_BYTES / 40 {
32            return Err("mesh exceeds retained storage budget");
33        }
34        if mesh.indices.iter().any(|i| {
35            mesh.vertices
36                .get(*i as usize)
37                .is_none_or(|v| !v.position.is_finite() || !v.normal.is_finite())
38        }) {
39            return Err("invalid retained mesh geometry");
40        }
41        REGISTRY.with(|registry| {
42            let mut r = registry.borrow_mut();
43            r.meshes.retain(|m| m.strong_count() > 0);
44            let bytes: usize = r
45                .meshes
46                .iter()
47                .filter_map(Weak::upgrade)
48                .map(|m| m.vertices.len() * 40)
49                .sum();
50            if r.meshes.len() >= MAX_MESHES || bytes + mesh.indices.len() * 40 > MAX_BYTES {
51                return Err("retained storage capacity exceeded");
52            }
53            r.next = r.next.checked_add(1).ok_or("mesh identity exhausted")?;
54            let data = Rc::new(Geometry {
55                id: r.next,
56                bounds: mesh
57                    .indices
58                    .iter()
59                    .map(|i| mesh.vertices[*i as usize].position)
60                    .fold(
61                        ferravex_graphics::Bounds {
62                            min: crate::Vec3::splat(f32::INFINITY),
63                            max: crate::Vec3::splat(f32::NEG_INFINITY),
64                        },
65                        |b, p| ferravex_graphics::Bounds {
66                            min: b.min.min(p),
67                            max: b.max.max(p),
68                        },
69                    ),
70                opaque: mesh
71                    .indices
72                    .iter()
73                    .all(|i| mesh.vertices[*i as usize].color[3] == 255),
74                vertices: mesh
75                    .indices
76                    .iter()
77                    .map(|i| {
78                        let v = mesh.vertices[*i as usize];
79                        [
80                            v.position.x,
81                            v.position.y,
82                            v.position.z,
83                            v.normal.x,
84                            v.normal.y,
85                            v.normal.z,
86                            v.color[0] as f32 / 255.,
87                            v.color[1] as f32 / 255.,
88                            v.color[2] as f32 / 255.,
89                            v.color[3] as f32 / 255.,
90                        ]
91                    })
92                    .collect(),
93            });
94            r.meshes.push(Rc::downgrade(&data));
95            Ok(Self(data))
96        })
97    }
98    pub fn id(&self) -> u32 {
99        self.0.id
100    }
101    pub fn vertex_count(&self) -> usize {
102        self.0.vertices.len()
103    }
104    pub(crate) fn is_opaque(&self) -> bool {
105        self.0.opaque
106    }
107    pub fn bounds(&self) -> ferravex_graphics::Bounds {
108        self.0.bounds
109    }
110}
111pub(crate) fn geometry(id: u32) -> Option<Rc<Geometry>> {
112    REGISTRY.with(|r| {
113        r.borrow()
114            .meshes
115            .iter()
116            .filter_map(Weak::upgrade)
117            .find(|m| m.id == id)
118    })
119}
120#[cfg(not(target_arch = "wasm32"))]
121pub(crate) fn is_live(id: u32) -> bool {
122    geometry(id).is_some()
123}
124/// ABI: kind 0 = immediate batch, kind 1 = retained batch. Preserves transparency/UI order.
125#[repr(C)]
126#[derive(Clone, Copy, Debug)]
127pub struct Command {
128    pub kind: u32,
129    pub index: u32,
130}
131/// ABI: 224 bytes. Lighting: direction.xyz/enabled, eye.xyz/ambient, fog.rgba, start/end/padding.
132#[repr(C)]
133#[derive(Clone, Copy, Debug, PartialEq)]
134pub struct RetainedBatch {
135    pub mesh: u32,
136    pub first: u32,
137    pub count: u32,
138    /// Bit 0: depth test; bit 1: suppress depth writes; bit 2: additive blend.
139    pub depth: u32,
140    pub camera: [f32; 16],
141    pub light: [f32; 16],
142    pub params: [f32; 16],
143    pub material: u32,
144    pub revision: u32,
145    pub reserved: [u32; 2],
146}
147#[derive(Default)]
148pub(crate) struct Frame {
149    pub commands: Vec<Command>,
150    pub batches: Vec<RetainedBatch>,
151    pub instances: Vec<[f32; 28]>,
152    pub environment: crate::lighting::Environment,
153    scratch_instances: Vec<[f32; 28]>,
154    scratch_batches: Vec<RetainedBatch>,
155    keep_alive: Vec<ResidentMesh>,
156    keep_materials: Vec<crate::material::ShaderMaterial>,
157}
158#[derive(Default, Clone, Copy)]
159pub(crate) struct DrawStyle<'a> {
160    pub light: Option<LitSettings>,
161    pub material: Option<&'a crate::material::ShaderMaterial>,
162    pub params: [f32; 16],
163    pub shadow_only: bool,
164    pub skip_shadow: bool,
165}
166impl RetainedBatch {
167    fn same_pass(&self, other: &Self) -> bool {
168        self.camera == other.camera
169            && self.light == other.light
170            && self.params == other.params
171            && self.material == other.material
172            && self.revision == other.revision
173            && self.depth & 7 == other.depth & 7
174    }
175    fn compatible(&self, other: &Self) -> bool {
176        self.mesh == other.mesh && self.depth == other.depth && self.same_pass(other)
177    }
178}
179impl Frame {
180    pub fn clear(&mut self) {
181        self.commands.clear();
182        self.batches.clear();
183        self.instances.clear();
184        self.keep_alive.clear();
185        self.keep_materials.clear();
186    }
187    #[cfg(test)]
188    pub fn record(
189        &mut self,
190        mesh: &ResidentMesh,
191        model: Mat4,
192        camera: Mat4,
193        depth: bool,
194        light: Option<LitSettings>,
195    ) -> bool {
196        self.record_style(
197            mesh,
198            model,
199            camera,
200            depth,
201            DrawStyle {
202                light,
203                ..Default::default()
204            },
205        )
206    }
207    pub fn record_style(
208        &mut self,
209        mesh: &ResidentMesh,
210        model: Mat4,
211        camera: Mat4,
212        depth: bool,
213        style: DrawStyle<'_>,
214    ) -> bool {
215        let light = style.light;
216        if self.instances.len() >= MAX_INSTANCES || !model.is_finite() || !camera.is_finite() {
217            return false;
218        }
219        let Some(inverse) = model.try_inverse() else {
220            return false;
221        };
222        if model.to_cols_array()[3] != 0.
223            || model.to_cols_array()[7] != 0.
224            || model.to_cols_array()[11] != 0.
225            || model.to_cols_array()[15] != 1.
226        {
227            return false;
228        }
229        let normal = inverse.transpose().to_cols_array();
230        let mut instance = [0.; 28];
231        instance[..16].copy_from_slice(&model.to_cols_array());
232        for i in 0..3 {
233            instance[16 + i * 4..19 + i * 4].copy_from_slice(&normal[i * 4..i * 4 + 3]);
234        }
235        let l = light.unwrap_or_default().sanitized();
236        let lighting = [
237            l.light_direction.x,
238            l.light_direction.y,
239            l.light_direction.z,
240            if light.is_some() { 1. } else { 0. },
241            l.camera_position.x,
242            l.camera_position.y,
243            l.camera_position.z,
244            l.ambient,
245            l.fog_color.r,
246            l.fog_color.g,
247            l.fog_color.b,
248            l.fog_color.a,
249            l.fog_start,
250            l.fog_end,
251            0.,
252            if mesh.0.opaque
253                && style
254                    .material
255                    .is_none_or(|m| m.0.options.blend == crate::material::BlendMode::Opaque)
256            {
257                1.
258            } else {
259                0.
260            },
261        ];
262        let mut depth_flags = depth as u32;
263        if let Some(m) = style.material {
264            if m.0.options.blend != crate::material::BlendMode::Opaque {
265                depth_flags |= 2;
266            }
267            if m.0.options.blend == crate::material::BlendMode::Additive {
268                depth_flags |= 4;
269            }
270            if !m.0.options.casts_shadows {
271                depth_flags |= 8;
272            }
273            if !m.0.options.receives_shadows {
274                depth_flags |= 16;
275            }
276        }
277        if style.skip_shadow {
278            depth_flags |= 8;
279        }
280        if style.shadow_only {
281            depth_flags |= 32;
282        }
283        let batch = RetainedBatch {
284            mesh: mesh.id(),
285            first: self.instances.len() as u32,
286            count: 1,
287            depth: depth_flags,
288            camera: camera.to_cols_array(),
289            light: lighting,
290            params: style.params,
291            material: style.material.map_or(0, |m| m.id()),
292            revision: style.material.map_or(0, |m| m.0.revision.get()),
293            reserved: [0; 2],
294        };
295        if let Some(last) = self.batches.last_mut().filter(|last| {
296            self.commands.last().is_some_and(|c| c.kind == 1) && last.compatible(&batch)
297        }) {
298            last.count += 1;
299        } else {
300            self.commands.push(Command {
301                kind: 1,
302                index: self.batches.len() as u32,
303            });
304            self.batches.push(batch);
305            self.keep_alive.push(mesh.clone());
306            if let Some(material) = style.material {
307                self.keep_materials.push(material.clone());
308            }
309        }
310        self.instances.push(instance);
311        true
312    }
313    /// Group opaque depth-tested draws within an uninterrupted camera/material pass.
314    /// Transparent draws and every immediate command are strict ordering barriers.
315    pub fn finalize(&mut self) {
316        let opaque = |c: &Command| {
317            c.kind == 1
318                && self.batches[c.index as usize].depth & 7 == 1
319                && self.batches[c.index as usize].light[15] > 0.5
320        };
321        let mut begin = 0;
322        while begin < self.commands.len() {
323            if !opaque(&self.commands[begin]) {
324                begin += 1;
325                continue;
326            }
327            let first = self.batches[self.commands[begin].index as usize];
328            let mut end = begin + 1;
329            while end < self.commands.len() && opaque(&self.commands[end]) {
330                let b = self.batches[self.commands[end].index as usize];
331                if !b.same_pass(&first) {
332                    break;
333                }
334                end += 1;
335            }
336            self.commands[begin..end].sort_unstable_by_key(|c| {
337                let b = self.batches[c.index as usize];
338                (b.mesh, b.depth)
339            });
340            begin = end;
341        }
342        self.scratch_instances.clear();
343        self.scratch_batches.clear();
344        let mut written = 0;
345        let mut previous_retained = false;
346        for i in 0..self.commands.len() {
347            let c = self.commands[i];
348            if c.kind == 0 {
349                self.commands[written] = c;
350                written += 1;
351                previous_retained = false;
352                continue;
353            }
354            let mut batch = self.batches[c.index as usize];
355            let source =
356                &self.instances[batch.first as usize..(batch.first + batch.count) as usize];
357            batch.first = self.scratch_instances.len() as u32;
358            self.scratch_instances.extend_from_slice(source);
359            if let Some(last) = self
360                .scratch_batches
361                .last_mut()
362                .filter(|last| previous_retained && last.compatible(&batch))
363            {
364                last.count += batch.count;
365            } else {
366                self.commands[written] = Command {
367                    kind: 1,
368                    index: self.scratch_batches.len() as u32,
369                };
370                written += 1;
371                self.scratch_batches.push(batch);
372            }
373            previous_retained = true;
374        }
375        self.commands.truncate(written);
376        std::mem::swap(&mut self.instances, &mut self.scratch_instances);
377        std::mem::swap(&mut self.batches, &mut self.scratch_batches);
378    }
379}
380
381/// Same vertex-lighting/fog equations as the CPU immediate path, evaluated by the GPU.
382pub const VERTEX_BODY: &str = r#"
383attribute vec3 a_position;
384attribute vec3 a_normal;
385attribute vec4 a_color;
386attribute vec4 a_model0; attribute vec4 a_model1; attribute vec4 a_model2; attribute vec4 a_model3;
387attribute vec4 a_normal0; attribute vec4 a_normal1; attribute vec4 a_normal2;
388uniform mat4 u_camera;
389uniform vec4 u_light[4];
390uniform vec4 u_params[4];
391varying vec4 v_color;
392varying vec3 v_world;
393varying vec3 v_normal;
394vec3 ferravex_vertex(vec3 position);
395vec3 ferravex_normal(vec3 normal);
396void main(){
397 mat4 model=mat4(a_model0,a_model1,a_model2,a_model3);
398 vec4 world=model*vec4(ferravex_vertex(a_position),1.0);
399 vec3 normal=mat3(a_normal0.xyz,a_normal1.xyz,a_normal2.xyz)*ferravex_normal(a_normal);
400 float n=length(normal);normal=n>0.0?normal/n:vec3(0.0);
401 gl_Position=u_camera*world;v_color=a_color;v_world=world.xyz;v_normal=normal;
402}
403"#;
404#[cfg(test)]
405mod tests {
406    use super::*;
407    use crate::{Color, Vec3, mesh::Mesh3d};
408    fn mesh() -> ResidentMesh {
409        ResidentMesh::new(&Mesh3d::cuboid(Vec3::ONE, Color::new(1., 1., 1., 1.)).mesh).unwrap()
410    }
411    #[test]
412    fn compatible_instances_merge_but_camera_and_order_are_preserved() {
413        let mesh = mesh();
414        let mut f = Frame::default();
415        assert!(f.record(&mesh, Mat4::IDENTITY, Mat4::IDENTITY, true, None));
416        f.record(
417            &mesh,
418            Mat4::from_translation(Vec3::X),
419            Mat4::IDENTITY,
420            true,
421            None,
422        );
423        assert_eq!(f.batches.len(), 1);
424        assert_eq!(f.batches[0].count, 2);
425        f.commands.push(Command { kind: 0, index: 0 });
426        f.record(&mesh, Mat4::IDENTITY, Mat4::IDENTITY, true, None);
427        f.record(
428            &mesh,
429            Mat4::IDENTITY,
430            Mat4::from_translation(Vec3::X),
431            true,
432            None,
433        );
434        assert_eq!(f.batches.len(), 3);
435        assert_eq!(f.commands.len(), 4);
436        assert!(!f.record(
437            &mesh,
438            Mat4::from_scale(Vec3::ZERO),
439            Mat4::IDENTITY,
440            true,
441            None
442        ));
443    }
444    #[test]
445    fn lifetime_keeps_draws_valid_and_ids_never_recycle() {
446        let mut f = Frame::default();
447        let id;
448        {
449            let mesh = mesh();
450            id = mesh.id();
451            f.record(&mesh, Mat4::IDENTITY, Mat4::IDENTITY, true, None);
452        }
453        assert!(is_live(id));
454        f.clear();
455        assert!(!is_live(id));
456        assert_ne!(id, mesh().id());
457    }
458    #[test]
459    fn wire_layout_and_normal_matrix_are_defined() {
460        assert_eq!(std::mem::size_of::<Command>(), 8);
461        assert_eq!(std::mem::size_of::<RetainedBatch>(), 224);
462        assert_eq!(std::mem::offset_of!(RetainedBatch, camera), 16);
463        assert_eq!(std::mem::offset_of!(RetainedBatch, light), 80);
464        assert_eq!(std::mem::offset_of!(RetainedBatch, params), 144);
465        assert_eq!(std::mem::offset_of!(RetainedBatch, material), 208);
466        let mut f = Frame::default();
467        f.record(
468            &mesh(),
469            Mat4::from_scale(Vec3::new(2., 4., 8.)),
470            Mat4::IDENTITY,
471            true,
472            None,
473        );
474        assert_eq!(f.instances[0][16], 0.5);
475        assert_eq!(f.instances[0][21], 0.25);
476        assert_eq!(f.instances[0][26], 0.125);
477    }
478    #[test]
479    fn material_and_parameter_changes_cannot_be_merged() {
480        use crate::material::{MaterialOptions, ShaderMaterial};
481        let mesh = mesh();
482        let a = ShaderMaterial::new(
483            Default::default(),
484            MaterialOptions {
485                blend: crate::material::BlendMode::Opaque,
486                ..Default::default()
487            },
488        )
489        .unwrap();
490        let b = ShaderMaterial::new(
491            Default::default(),
492            MaterialOptions {
493                blend: crate::material::BlendMode::Opaque,
494                ..Default::default()
495            },
496        )
497        .unwrap();
498        let mut f = Frame::default();
499        for (m, v) in [(&a, 1.), (&b, 1.), (&b, 2.)] {
500            f.record_style(
501                &mesh,
502                Mat4::IDENTITY,
503                Mat4::IDENTITY,
504                true,
505                DrawStyle {
506                    material: Some(m),
507                    params: [v; 16],
508                    ..Default::default()
509                },
510            );
511        }
512        f.finalize();
513        assert_eq!(f.batches.len(), 3);
514        assert_eq!(
515            f.batches
516                .iter()
517                .map(|b| (b.material, b.params[0]))
518                .collect::<Vec<_>>(),
519            [(a.id(), 1.), (b.id(), 1.), (b.id(), 2.)]
520        );
521    }
522    #[test]
523    fn opaque_reordering_copies_instances_and_stops_at_ui_barriers() {
524        let a = mesh();
525        let b = mesh();
526        let mut f = Frame::default();
527        for (i, m) in [&a, &b, &a].into_iter().enumerate() {
528            f.record(
529                m,
530                Mat4::from_translation(Vec3::X * i as f32),
531                Mat4::IDENTITY,
532                true,
533                None,
534            );
535        }
536        f.commands.push(Command { kind: 0, index: 0 });
537        f.record(&a, Mat4::IDENTITY, Mat4::IDENTITY, true, None);
538        f.finalize();
539        assert_eq!(f.commands.len(), 4);
540        assert_eq!(f.batches[0].count, 2);
541        assert_eq!(
542            f.instances.iter().map(|i| i[12]).collect::<Vec<_>>(),
543            [0., 2., 1., 0.]
544        );
545    }
546}