Skip to main content

primase_core/
ledger.rs

1//! What a pass leaves out on purpose, by name: the measures a drop is counted in ([`Measure`]) and the helper that
2//! despawns an entity declaring what it held ([`despawn`]). A pass records a drop with [`PassLog::dropped`]; a
3//! conversion reports every drop as a selection notice ([`crate::convert::ConvertReport::notices`]).
4//!
5//! **Conservation diagnostics** (opt in: [`crate::pass::PassManager::with_conservation`]; off by default, and then
6//! nothing below runs): the manager counts the four measures before and after each pass and compares what it lost
7//! with what it declared ([`Conservation::Report`] records each difference in the step's report and as a diagnostic
8//! notice; [`Conservation::Enforce`] fails the pass, for tests). A difference is a coverage finding (a pass that leaves
9//! something out without saying so, or says so wrongly), never a reason to change content to satisfy the count.
10//!
11//! - **Placements**: entities with a `core.instance`. Exact: each one despawned, or that lost its instance, is a drop.
12//! - **Textures**: entities with a `core.texture`, a `core.texture_layers` or a `core.texture_file`. Exact, by entity:
13//!   loading a file into pixels keeps the entity and is no drop; a texture despawned or left with none is one.
14//! - **Primitives**: the primitives of a `core.mesh`. Exact through [`World::rebuild_primitives`]: a source primitive
15//!   none of the rebuilt ones comes from is a drop. Meshes are told apart by identity ([`Mesh::new`] makes a new one; a
16//!   clone, an in-place edit and [`World::apply_linear`] keep it), never by their primitive count, and followed by it:
17//!   a mesh gone from its entity that stands on another with its identity (moved with [`World::move_components`], or a
18//!   [`World::duplicate`] or clone standing in for an entity despawned) is compared there as if it had stayed, exactly.
19//!   A copy made beside a mesh that stays is an addition. A mesh replaced by another identity (a new [`Mesh`] inserted
20//!   or assigned, or despawned with its entity and found nowhere), before, between or after rebuilds, is counted net
21//!   over all such meshes; the meshes that appeared (a loaded file, a form's new mesh) add to that balance.
22//! - **Triangles**: inside kept primitives only (a dropped primitive's triangles go with it): per source primitive of a
23//!   rebuild, what its pieces lost; per mesh edited in place ([`Mesh::primitives_mut`]), what each primitive lost. Net
24//!   per primitive. Triangles are those of [`Topology::Triangles`] primitives, core's only triangle topology.
25//!
26//! What it does not see: the triangle losses of a mesh replaced by another identity (counted net by primitives only),
27//! topologies other than triangle lists, anything outside the four measures (collision soups, lights, markers, source
28//! namespaces) and changes that keep the counts (a moved vertex, a recoloured texture). It is a check of declarations,
29//! not proof of fidelity.
30//!
31//! - **Collision triangles** ([`Measure::CollisionTriangles`]) are declared and reported, never measured: collision
32//!   soups are outside what it counts, so a declared collision drop is reported as a notice like any other and left
33//!   out of the comparison.
34//!
35//! Adding is never a drop and needs no declaration.
36
37use std::collections::{BTreeMap, BTreeSet};
38use std::fmt;
39
40use crate::TextureLayers;
41use crate::asset::Asset;
42use crate::clip::ClipBinding;
43use crate::components::{Instance, Mesh, Texture, TextureFile, Topology};
44use crate::pass::PassLog;
45use crate::world::{Entity, RebuildLog, Rebuilt, World};
46
47/// What a drop is counted in.
48#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
49pub enum Measure {
50    Placements,
51    Primitives,
52    Triangles,
53    Textures,
54    /// Triangles of collision soups (`core.collision`): declared and reported, not measured (see the module).
55    CollisionTriangles,
56}
57
58impl Measure {
59    pub const ALL: [Measure; 5] = [
60        Measure::Placements,
61        Measure::Primitives,
62        Measure::Triangles,
63        Measure::Textures,
64        Measure::CollisionTriangles,
65    ];
66
67    /// Whether the diagnostics count it before and after a pass (every measure but collision triangles).
68    pub const fn measured(self) -> bool {
69        !matches!(self, Measure::CollisionTriangles)
70    }
71}
72
73impl fmt::Display for Measure {
74    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75        f.write_str(match self {
76            Measure::Placements => "placements",
77            Measure::Primitives => "primitives",
78            Measure::Triangles => "triangles",
79            Measure::Textures => "textures",
80            Measure::CollisionTriangles => "collision triangles",
81        })
82    }
83}
84
85/// What `e` holds of the measures: a placement, a texture, its mesh's primitives.
86pub fn held(w: &World, e: Entity) -> Vec<(Measure, u64)> {
87    let mut out = Vec::new();
88    if w.has::<Instance>(e) {
89        out.push((Measure::Placements, 1));
90    }
91    if w.has::<Texture>(e) || w.has::<TextureLayers>(e) || w.has::<TextureFile>(e) {
92        out.push((Measure::Textures, 1));
93    }
94    if let Some(m) = w.get::<Mesh>(e) {
95        out.push((Measure::Primitives, m.primitives().len() as u64));
96    }
97    out
98}
99
100/// Despawn `e` on purpose, declaring in `log` what it held ([`held`]) as left out for `why`. False, and nothing
101/// declared, when it was not alive. Every `core.asset` that lists `e` as a member stops listing it (an asset naming an
102/// entity that is gone is not an asset the world verifies), counted as "asset members dropped with the entity they
103/// listed", and every `core.clip_binding` that names `e` as a mesh it drives stops naming it ([`unbind`]); a pass that
104/// despawns entities an asset or a clip binding may name declares that it modifies `core.asset` or `core.clip_binding`.
105pub fn despawn(w: &mut World, e: Entity, log: &mut PassLog, why: &str) -> bool {
106    if !w.is_alive(e) {
107        return false;
108    }
109    for (m, n) in held(w, e) {
110        log.dropped(m, why, n);
111    }
112    let gone = w.despawn(e);
113    unlist(w, e, log);
114    unbind(w, e, log);
115    gone
116}
117
118/// Every `core.clip_binding` that names `e` among the mesh occurrences its clip drives stops naming it, counted as
119/// "clip binding meshes dropped with the entity they named": the clip still drives the occurrences that remain, and a
120/// binding naming an entity that is gone is not one the world verifies. The binding's clip and rig are not touched.
121pub fn unbind(w: &mut World, e: Entity, log: &mut PassLog) {
122    let naming: Vec<Entity> = w
123        .iter::<ClipBinding>()
124        .filter(|(_, b)| b.meshes.contains(&e))
125        .map(|(b, _)| b)
126        .collect();
127    for b in naming {
128        if let Some(binding) = w.get_mut::<ClipBinding>(b) {
129            let before = binding.meshes.len();
130            binding.meshes.retain(|m| *m != e);
131            log.count(
132                "clip binding meshes dropped with the entity they named",
133                (before - binding.meshes.len()) as u64,
134            );
135        }
136    }
137}
138
139/// Every `core.asset` that lists `e` as a member stops listing it, counted as "asset members dropped with the entity they
140/// listed". For a pass that despawns an entity whose primitives or placements are kept elsewhere, and so declares nothing
141/// left out ([`despawn`] declares what the entity held); it declares that it modifies `core.asset`.
142pub fn unlist(w: &mut World, e: Entity, log: &mut PassLog) {
143    let listing: Vec<Entity> = w
144        .iter::<Asset>()
145        .filter(|(_, a)| a.members.iter().any(|(m, _)| *m == e))
146        .map(|(a, _)| a)
147        .collect();
148    for a in listing {
149        if let Some(asset) = w.get_mut::<Asset>(a) {
150            let before = asset.members.len();
151            asset.members.retain(|(m, _)| *m != e);
152            log.count(
153                "asset members dropped with the entity they listed",
154                (before - asset.members.len()) as u64,
155            );
156        }
157    }
158}
159
160/// Whether the pass manager runs the conservation diagnostics (see the module documentation).
161#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
162pub enum Conservation {
163    /// Not counted (the default): no scan, no cost.
164    #[default]
165    Off,
166    /// Counted; each difference recorded in the step's report ([`crate::pass::StepReport::conservation`]).
167    Report,
168    /// Counted; a difference fails the pass ([`crate::pass::Error::Conservation`]).
169    Enforce,
170}
171
172/// A pass whose losses in `measure` differ from what it declared.
173#[derive(Clone, Copy, Debug, PartialEq, Eq)]
174pub struct Gap {
175    pub measure: Measure,
176    /// What it left out, counted.
177    pub dropped: u64,
178    /// What it declared ([`PassLog::declared`]).
179    pub declared: u64,
180}
181
182impl fmt::Display for Gap {
183    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184        write!(
185            f,
186            "{} left out {} but declared {}",
187            self.measure, self.dropped, self.declared
188        )
189    }
190}
191
192/// Triangles of each primitive of a mesh (0 for other topologies).
193fn triangles(mesh: &Mesh) -> Vec<u64> {
194    mesh.primitives()
195        .iter()
196        .map(|p| match p.topology {
197            Topology::Triangles => (p.indices.as_ref().map_or(p.vertex_count(), Vec::len) / 3) as u64,
198            _ => 0,
199        })
200        .collect()
201}
202
203/// What a mesh `before` of identity `id` lost, primitives and triangles, if what now stands in its place is it,
204/// rebuilt (the last rebuild `rebuild` made `now` from it) or edited in place at most; `None` when it is not.
205fn exact(
206    before: &[u64],
207    id: u64,
208    rebuild: Option<&Option<Rebuilt>>,
209    now: Option<&(u64, Vec<u64>)>,
210) -> Option<(u64, u64)> {
211    let (now_id, now) = now?;
212    match rebuild {
213        Some(Some(r)) if r.source == id && r.made == *now_id => {
214            let kept: BTreeSet<usize> = r.from.iter().copied().collect();
215            let mut tris = 0;
216            for &j in &kept {
217                let pieces: u64 = r
218                    .from
219                    .iter()
220                    .zip(now)
221                    .filter(|(i, _)| **i == j)
222                    .map(|(_, t)| t)
223                    .sum();
224                tris += before[j].saturating_sub(pieces);
225            }
226            Some(((r.first - kept.len()) as u64, tris))
227        }
228        None if now_id == &id => Some((0, before.iter().zip(now).map(|(b, a)| b.saturating_sub(*a)).sum())),
229        _ => None,
230    }
231}
232
233/// The world as the diagnostics see it before a pass.
234#[derive(Debug)]
235pub(crate) struct Before {
236    placements: BTreeSet<Entity>,
237    textures: BTreeSet<Entity>,
238    /// Per mesh, its identity and its primitives' triangles.
239    meshes: BTreeMap<Entity, (u64, Vec<u64>)>,
240}
241
242impl Before {
243    pub(crate) fn take(w: &World) -> Self {
244        let mut textures: BTreeSet<Entity> = w.iter::<Texture>().map(|(e, _)| e).collect();
245        textures.extend(w.iter::<TextureLayers>().map(|(e, _)| e));
246        textures.extend(w.iter::<TextureFile>().map(|(e, _)| e));
247        Before {
248            placements: w.iter::<Instance>().map(|(e, _)| e).collect(),
249            textures,
250            meshes: w
251                .iter::<Mesh>()
252                .map(|(e, m)| (e, (m.id(), triangles(m))))
253                .collect(),
254        }
255    }
256
257    /// What the pass left out, per measure, given the world after it and its primitive rebuilds.
258    pub(crate) fn dropped(&self, w: &World, rebuilds: &RebuildLog) -> BTreeMap<Measure, u64> {
259        let placements = self.placements.iter().filter(|&&e| !w.has::<Instance>(e)).count() as u64;
260        let textures = self
261            .textures
262            .iter()
263            .filter(|&&e| !w.has::<Texture>(e) && !w.has::<TextureLayers>(e) && !w.has::<TextureFile>(e))
264            .count() as u64;
265        let (mut primitives, mut tris) = (0u64, 0u64);
266        let after: BTreeMap<Entity, (u64, Vec<u64>)> = w
267            .iter::<Mesh>()
268            .map(|(e, m)| (e, (m.id(), triangles(m))))
269            .collect();
270        // each mesh compared where it stood; one that is no longer there (its entity gone, or its mesh replaced) is
271        // left for its identity to find
272        let mut gone: Vec<(u64, &Vec<u64>)> = Vec::new();
273        let mut matched: BTreeSet<Entity> = BTreeSet::new();
274        for (e, (id, before)) in &self.meshes {
275            match exact(before, *id, rebuilds.get(e), after.get(e)) {
276                Some((p, t)) => {
277                    primitives += p;
278                    tris += t;
279                    matched.insert(*e);
280                }
281                None => gone.push((*id, before)),
282            }
283        }
284        // a mesh that went from where it stood and stands elsewhere with its identity (moved with
285        // World::move_components, or a duplicate or clone standing in for an entity despawned) is compared there
286        // as if it had stayed; the rest are counted net, primitives only, all together
287        let (mut net_before, mut net_after) = (0u64, 0u64);
288        let mut unmatched: Vec<(Entity, &(u64, Vec<u64>))> = after
289            .iter()
290            .filter(|(e, _)| !matched.contains(*e))
291            .map(|(e, a)| (*e, a))
292            .collect();
293        for (id, before) in gone {
294            let found = unmatched.iter().position(|(e, (now_id, _))| {
295                *now_id == id
296                    || rebuilds
297                        .get(e)
298                        .is_some_and(|r| r.as_ref().is_some_and(|r| r.source == id))
299            });
300            match found.and_then(|i| {
301                let (e, now) = unmatched[i];
302                exact(before, id, rebuilds.get(&e), Some(now)).map(|c| (i, c))
303            }) {
304                Some((i, (p, t))) => {
305                    primitives += p;
306                    tris += t;
307                    unmatched.remove(i);
308                }
309                None => net_before += before.len() as u64,
310            }
311        }
312        // meshes that appeared (a loaded file, a form's new mesh, what a pass made) and replacements balance the net
313        // count
314        net_after += unmatched
315            .iter()
316            .map(|(_, (_, now))| now.len() as u64)
317            .sum::<u64>();
318        primitives += net_before.saturating_sub(net_after);
319        BTreeMap::from([
320            (Measure::Placements, placements),
321            (Measure::Primitives, primitives),
322            (Measure::Triangles, tris),
323            (Measure::Textures, textures),
324        ])
325    }
326
327    /// Each measure whose count differs from what `log` declared. Collision triangles are left out: collision soups are
328    /// outside what the diagnostics count, so a declared collision drop can only be reported, not measured.
329    pub(crate) fn gaps(&self, w: &World, rebuilds: &RebuildLog, log: &PassLog) -> Vec<Gap> {
330        self.dropped(w, rebuilds)
331            .into_iter()
332            .filter(|(measure, _)| measure.measured())
333            .map(|(measure, dropped)| Gap {
334                measure,
335                dropped,
336                declared: log.declared(measure),
337            })
338            .filter(|g| g.dropped != g.declared)
339            .collect()
340    }
341}
342
343#[cfg(test)]
344mod tests {
345    use super::*;
346    use crate::components::Primitive;
347
348    use crate::components::{ColorSpace, Semantic, Stream, TexRole};
349    use crate::pass::{Error, Pass, PassManager, PassRegistry, Pipeline, RunReport};
350    use crate::texformat::TexFormat;
351    use std::collections::BTreeMap as Map;
352
353    /// A triangle list of `n` triangles.
354    /// Primitive `i` of `m` given `p`'s contents, field by field (a pass editing it in place).
355    fn set(m: &mut Mesh, i: usize, p: Primitive) {
356        let mut q = m.primitive_mut(i).unwrap();
357        let f = q.fields();
358        (*f.topology, *f.streams, *f.indices, *f.material) = (p.topology, p.streams, p.indices, p.material);
359    }
360
361    fn prim(n: u32) -> Primitive {
362        Primitive {
363            topology: Topology::Triangles,
364            streams: Map::from([(Semantic::Position, Stream::F32x3(vec![[0.; 3]; 3]))]),
365            indices: Some((0..n).flat_map(|_| [0, 1, 2]).collect()),
366            material: None,
367        }
368    }
369
370    fn texture() -> Texture {
371        Texture {
372            width: 1,
373            height: 1,
374            format: TexFormat::Rgba8Unorm,
375            levels: vec![vec![0; 4]],
376            color_space: ColorSpace::Srgb,
377            role: TexRole::Color,
378            normal: None,
379        }
380    }
381
382    /// e0: a prototype of three primitives (1, 2, 3 triangles); e1, e2: its placements; e3: a texture file.
383    fn world() -> World {
384        let mut w = World::new();
385        let p = w.spawn();
386        w.insert(p, Mesh::new(vec![prim(1), prim(2), prim(3)]));
387        for _ in 0..2 {
388            let e = w.spawn();
389            w.insert(
390                e,
391                Instance {
392                    prototype: p,
393                    variant: 0,
394                },
395            );
396            w.insert(e, crate::Transform::IDENTITY);
397        }
398        let t = w.spawn();
399        w.insert(t, TextureFile { path: "a.png".into() });
400        w
401    }
402
403    /// A pass that runs `f` and declares `drops`.
404    struct Do {
405        f: fn(&mut World),
406        drops: Vec<(Measure, u64)>,
407    }
408    impl Pass for Do {
409        fn name(&self) -> &'static str {
410            "do"
411        }
412        fn removes(&self) -> &[&'static str] {
413            &["core.instance", "core.texture_file", "core.mesh"]
414        }
415        fn provides(&self) -> &[&'static str] {
416            &["core.texture", "core.mesh"]
417        }
418        fn modifies(&self) -> &[&'static str] {
419            &["core.mesh"]
420        }
421        fn despawns_entities(&self) -> bool {
422            true
423        }
424        fn run(&self, w: &mut World, log: &mut PassLog) -> Result<(), String> {
425            (self.f)(w);
426            for &(m, n) in &self.drops {
427                log.dropped(m, "on purpose", n);
428            }
429            Ok(())
430        }
431    }
432
433    fn run_with(c: Conservation, f: fn(&mut World), drops: Vec<(Measure, u64)>) -> Result<RunReport, Error> {
434        let mut report = RunReport::default();
435        PassManager::new(PassRegistry::new()).with_conservation(c).run(
436            &mut world(),
437            &Pipeline {
438                passes: vec![Box::new(Do { f, drops })],
439            },
440            &mut report,
441        )?;
442        Ok(report)
443    }
444
445    fn run(f: fn(&mut World), drops: Vec<(Measure, u64)>) -> Result<RunReport, Error> {
446        run_with(Conservation::Enforce, f, drops)
447    }
448
449    fn left_out(f: fn(&mut World), drops: Vec<(Measure, u64)>) -> (Measure, u64, u64) {
450        match run(f, drops) {
451            Err(Error::Conservation {
452                measure,
453                dropped,
454                declared,
455                ..
456            }) => (measure, dropped, declared),
457            other => panic!("{other:?}"),
458        }
459    }
460
461    /// Off by default: an undeclared drop passes unseen; Report records it in the step and the run goes on; Enforce
462    /// fails the pass.
463    #[test]
464    fn the_check_is_off_unless_asked_and_reports_or_fails_as_asked() {
465        let despawn = |w: &mut World| {
466            w.despawn(Entity::from_index(1));
467        };
468        let r = run_with(Conservation::Off, despawn, vec![]).unwrap();
469        assert!(r.steps[0].conservation.is_empty());
470        let r = run_with(Conservation::Report, despawn, vec![]).unwrap();
471        assert_eq!(
472            r.steps[0].conservation,
473            vec![Gap {
474                measure: Measure::Placements,
475                dropped: 1,
476                declared: 0
477            }]
478        );
479        assert!(
480            r.to_string().contains("!placements left out 1 but declared 0"),
481            "{r}"
482        );
483        assert_eq!(left_out(despawn, vec![]), (Measure::Placements, 1, 0));
484    }
485
486    /// A declared collision drop is reported, never compared: the soups are outside what the check counts.
487    #[test]
488    fn collision_drops_are_reported_not_measured() {
489        let nothing = |_: &mut World| {};
490        let r = run(nothing, vec![(Measure::CollisionTriangles, 3)]).unwrap();
491        assert!(r.steps[0].conservation.is_empty());
492        assert_eq!(r.steps[0].log.declared(Measure::CollisionTriangles), 3);
493    }
494
495    #[test]
496    fn placements_and_textures_are_counted_by_entity() {
497        // a despawned placement must be declared; declaring it passes; declaring more than happened fails too
498        let despawn = |w: &mut World| {
499            w.despawn(Entity::from_index(1));
500        };
501        run(despawn, vec![(Measure::Placements, 1)]).unwrap();
502        assert_eq!(
503            left_out(despawn, vec![(Measure::Placements, 2)]),
504            (Measure::Placements, 1, 2)
505        );
506        // spawning a placement doesn't excuse losing another
507        let swap = |w: &mut World| {
508            w.despawn(Entity::from_index(1));
509            let e = w.spawn();
510            w.insert(
511                e,
512                Instance {
513                    prototype: Entity::from_index(0),
514                    variant: 0,
515                },
516            );
517            w.insert(e, crate::Transform::IDENTITY);
518        };
519        assert_eq!(left_out(swap, vec![]), (Measure::Placements, 1, 0));
520        // loading a texture file keeps the texture: no drop; losing both is one
521        let load = |w: &mut World| {
522            let t = Entity::from_index(3);
523            w.remove::<TextureFile>(t);
524            w.insert(t, texture());
525        };
526        run(load, vec![]).unwrap();
527        let lose = |w: &mut World| {
528            w.remove::<TextureFile>(Entity::from_index(3));
529        };
530        assert_eq!(left_out(lose, vec![]), (Measure::Textures, 1, 0));
531    }
532
533    #[test]
534    fn a_rebuild_is_exact_so_a_split_cannot_hide_a_drop() {
535        // drop primitive 0 and split primitive 2 in two: one more primitive than before, still one dropped
536        let split = |w: &mut World| {
537            w.rebuild_primitives(Entity::from_index(0), |_| {
538                Ok((vec![prim(2), prim(1), prim(2)], vec![1, 2, 2]))
539            })
540            .unwrap();
541        };
542        // and primitive 2's pieces hold 3 of its 3 triangles: no triangle lost
543        assert_eq!(left_out(split, vec![]), (Measure::Primitives, 1, 0));
544        run(split, vec![(Measure::Primitives, 1)]).unwrap();
545        // two rebuilds in one pass compose: the second drops a piece of primitive 2 (1 triangle of it lost)
546        let twice = |w: &mut World| {
547            let e = Entity::from_index(0);
548            w.rebuild_primitives(e, |_| Ok((vec![prim(2), prim(1), prim(2)], vec![1, 2, 2])))
549                .unwrap();
550            w.rebuild_primitives(e, |_| Ok((vec![prim(2), prim(2)], vec![0, 2])))
551                .unwrap();
552        };
553        assert_eq!(
554            left_out(twice, vec![(Measure::Primitives, 1)]),
555            (Measure::Triangles, 1, 0)
556        );
557        run(twice, vec![(Measure::Primitives, 1), (Measure::Triangles, 1)]).unwrap();
558    }
559
560    #[test]
561    fn triangles_are_counted_inside_kept_primitives() {
562        // decimate primitive 2 from 3 triangles to 1 in place
563        let decimate = |w: &mut World| {
564            set(w.get_mut::<Mesh>(Entity::from_index(0)).unwrap(), 2, prim(1));
565        };
566        assert_eq!(left_out(decimate, vec![]), (Measure::Triangles, 2, 0));
567        run(decimate, vec![(Measure::Triangles, 2)]).unwrap();
568    }
569
570    #[test]
571    fn a_replaced_mesh_is_counted_net_whatever_its_primitive_count() {
572        // replaced whole by one of the same count with fewer triangles (inserted, or assigned through get_mut): its
573        // primitives are not the old ones, so nothing is compared primitive by primitive and nothing is left out
574        let insert = |w: &mut World| {
575            w.insert(Entity::from_index(0), Mesh::new(vec![prim(1), prim(1), prim(1)]));
576        };
577        run(insert, vec![]).unwrap();
578        let assign = |w: &mut World| {
579            *w.get_mut::<Mesh>(Entity::from_index(0)).unwrap() = Mesh::new(vec![prim(1), prim(1), prim(1)]);
580        };
581        run(assign, vec![]).unwrap();
582        // a replacement between two rebuilds breaks their composition: what is left out is the net one primitive
583        let between = |w: &mut World| {
584            let e = Entity::from_index(0);
585            w.rebuild_primitives(e, |_| Ok((vec![prim(2), prim(3), prim(1)], vec![1, 2, 0])))
586                .unwrap();
587            w.insert(e, Mesh::new(vec![prim(1), prim(1), prim(1)]));
588            w.rebuild_primitives(e, |_| Ok((vec![prim(1), prim(1)], vec![1, 2])))
589                .unwrap();
590        };
591        assert_eq!(left_out(between, vec![]), (Measure::Primitives, 1, 0));
592        run(between, vec![(Measure::Primitives, 1)]).unwrap();
593        // and one before the first rebuild: the rebuild starts from the replacement, not the mesh the pass was given
594        let before = |w: &mut World| {
595            let e = Entity::from_index(0);
596            w.insert(e, Mesh::new(vec![prim(1), prim(1), prim(1)]));
597            w.rebuild_primitives(e, |_| Ok((vec![prim(1), prim(1)], vec![1, 2])))
598                .unwrap();
599        };
600        run(before, vec![(Measure::Primitives, 1)]).unwrap();
601    }
602
603    /// A mesh edited through [`World::edit_mesh`] counts as exactly as a rebuild: a split that adds a primitive cannot
604    /// hide one dropped (a net count over a hand-made replacement would see none), and an edit that keeps the list is
605    /// compared primitive by primitive. A failing edit puts the mesh back.
606    #[test]
607    fn an_edited_mesh_is_counted_exactly() {
608        let split_and_drop = |w: &mut World| {
609            w.edit_mesh(Entity::from_index(0), |m| {
610                let p = m.primitives().to_vec();
611                *m = Mesh::new(vec![p[1].clone(), prim(1), prim(2)]);
612                Ok(Some(vec![1, 2, 2]))
613            })
614            .unwrap();
615        };
616        assert_eq!(left_out(split_and_drop, vec![]), (Measure::Primitives, 1, 0));
617        run(split_and_drop, vec![(Measure::Primitives, 1)]).unwrap();
618        let in_place = |w: &mut World| {
619            w.edit_mesh(Entity::from_index(0), |m| {
620                set(m, 0, prim(0));
621                Ok(None)
622            })
623            .unwrap();
624        };
625        assert_eq!(left_out(in_place, vec![]), (Measure::Triangles, 1, 0));
626        // an edit that rebuilds the mesh object but keeps its primitives in order (None) stays the same mesh, so a
627        // later rebuild in the pass composes with it: a split there cannot hide a drop either
628        let renewed_then_split = |w: &mut World| {
629            let e = Entity::from_index(0);
630            w.edit_mesh(e, |m| {
631                *m = Mesh::new(m.primitives().to_vec());
632                Ok(None)
633            })
634            .unwrap();
635            w.edit_mesh(e, |m| {
636                let p = m.primitives().to_vec();
637                *m = Mesh::new(vec![p[1].clone(), prim(1), prim(2)]);
638                Ok(Some(vec![1, 2, 2]))
639            })
640            .unwrap();
641        };
642        assert_eq!(left_out(renewed_then_split, vec![]), (Measure::Primitives, 1, 0));
643        let mut w = world();
644        let e = Entity::from_index(0);
645        assert!(w.edit_mesh(e, |_| Err("no".into())).is_err());
646        assert_eq!(w.get::<Mesh>(e).map(|m| m.primitives().len()), Some(3));
647        assert!(w.edit_mesh(e, |_| Ok(Some(vec![5]))).is_err());
648        assert_eq!(w.get::<Mesh>(e).map(|m| m.primitives().len()), Some(3));
649        assert_eq!(w.edit_mesh(Entity::from_index(1), |_| Ok(None)), Ok(false));
650    }
651
652    /// The World APIs as the diagnostics see them: a clone keeps its mesh's identity (put back after an edit, it is
653    /// compared primitive by primitive), a mesh moved between entities is no drop (the one it replaced is), a duplicate
654    /// standing in for its original is no drop, a linear map keeps the mesh (no triangle lost), and a world clone or
655    /// subset starts with no rebuild log of its own.
656    #[test]
657    fn clone_move_duplicate_and_linear_maps_count_as_they_should() {
658        let clone_back = |w: &mut World| {
659            let e = Entity::from_index(0);
660            let mut m = w.get::<Mesh>(e).unwrap().clone();
661            set(&mut m, 1, prim(1));
662            w.insert(e, m);
663        };
664        assert_eq!(left_out(clone_back, vec![]), (Measure::Triangles, 1, 0));
665        let moved = |w: &mut World| {
666            let form = w.spawn();
667            w.insert(form, Mesh::new(vec![prim(1)]));
668            w.move_components(form, Entity::from_index(0)).unwrap();
669        };
670        assert_eq!(left_out(moved, vec![]), (Measure::Primitives, 2, 0));
671        run(moved, vec![(Measure::Primitives, 2)]).unwrap();
672        let duplicated = |w: &mut World| {
673            let p = Entity::from_index(0);
674            let copy = w.duplicate(p).unwrap();
675            for e in [Entity::from_index(1), Entity::from_index(2)] {
676                w.get_mut::<Instance>(e).unwrap().prototype = copy;
677            }
678            w.despawn(p);
679        };
680        run(duplicated, vec![]).unwrap();
681        let mirrored = |w: &mut World| {
682            let l = crate::Linear::mirror_x();
683            w.apply_linear(Entity::from_index(0), &l).unwrap();
684        };
685        run(mirrored, vec![]).unwrap();
686        let mut w = world();
687        w.start_rebuild_log();
688        w.rebuild_primitives(Entity::from_index(0), |_| Ok((vec![prim(1)], vec![0])))
689            .unwrap();
690        assert!(w.clone().take_rebuild_log().is_empty());
691        assert!(w.subset(&w.entities().collect()).take_rebuild_log().is_empty());
692        assert_eq!(w.take_rebuild_log().len(), 1);
693    }
694
695    /// A mesh is followed by its identity, not its entity: one moved to another entity, or a duplicate standing in for
696    /// an entity despawned, is compared where it went, as if it had stayed (a rebuild there or an edit in place counted
697    /// exactly); a copy made beside a mesh that stays is an addition, whatever is done to it after.
698    #[test]
699    fn a_mesh_is_followed_by_its_identity_across_entities() {
700        fn stand_in(w: &mut World) -> Entity {
701            let p = Entity::from_index(0);
702            let copy = w.duplicate(p).unwrap();
703            for e in [Entity::from_index(1), Entity::from_index(2)] {
704                w.get_mut::<Instance>(e).unwrap().prototype = copy;
705            }
706            w.despawn(p);
707            copy
708        }
709        fn moved(w: &mut World) -> Entity {
710            let to = w.spawn();
711            w.move_components(Entity::from_index(0), to).unwrap();
712            for e in [Entity::from_index(1), Entity::from_index(2)] {
713                w.get_mut::<Instance>(e).unwrap().prototype = to;
714            }
715            to
716        }
717        // the stand-in edited in place (primitive 2 from 3 triangles to 1), and rebuilt without primitive 2
718        let stand_in_edited = |w: &mut World| {
719            let copy = stand_in(w);
720            set(w.get_mut::<Mesh>(copy).unwrap(), 2, prim(1));
721        };
722        assert_eq!(left_out(stand_in_edited, vec![]), (Measure::Triangles, 2, 0));
723        run(stand_in_edited, vec![(Measure::Triangles, 2)]).unwrap();
724        let stand_in_rebuilt = |w: &mut World| {
725            let copy = stand_in(w);
726            w.rebuild_primitives(copy, |_| Ok((vec![prim(1), prim(2)], vec![0, 1])))
727                .unwrap();
728        };
729        assert_eq!(left_out(stand_in_rebuilt, vec![]), (Measure::Primitives, 1, 0));
730        // moved, then edited in place or split so that a primitive is hidden by a piece
731        let moved_edited = |w: &mut World| {
732            let to = moved(w);
733            set(w.get_mut::<Mesh>(to).unwrap(), 2, prim(1));
734        };
735        assert_eq!(left_out(moved_edited, vec![]), (Measure::Triangles, 2, 0));
736        let moved_split = |w: &mut World| {
737            let to = moved(w);
738            w.rebuild_primitives(to, |_| Ok((vec![prim(2), prim(1), prim(2)], vec![1, 2, 2])))
739                .unwrap();
740        };
741        assert_eq!(left_out(moved_split, vec![]), (Measure::Primitives, 1, 0));
742        // a copy beside the mesh, which stays whole: the copy's losses are not the source's
743        let copy_edited = |w: &mut World| {
744            let copy = w.duplicate(Entity::from_index(0)).unwrap();
745            set(w.get_mut::<Mesh>(copy).unwrap(), 2, prim(1));
746        };
747        run(copy_edited, vec![]).unwrap();
748        let copy_rebuilt = |w: &mut World| {
749            let to = w.spawn();
750            w.copy_components(Entity::from_index(0), to);
751            w.rebuild_primitives(to, |_| Ok((vec![prim(1)], vec![0])))
752                .unwrap();
753        };
754        run(copy_rebuilt, vec![]).unwrap();
755        let clone_elsewhere = |w: &mut World| {
756            let m = w.get::<Mesh>(Entity::from_index(0)).unwrap().clone();
757            let to = w.spawn();
758            w.insert(to, m);
759            set(w.get_mut::<Mesh>(to).unwrap(), 0, prim(0));
760        };
761        run(clone_elsewhere, vec![]).unwrap();
762        // and the source losing a primitive is its loss, a whole copy beside it or not
763        let source_rebuilt = |w: &mut World| {
764            w.duplicate(Entity::from_index(0)).unwrap();
765            w.rebuild_primitives(Entity::from_index(0), |_| {
766                Ok((vec![prim(1), prim(2)], vec![0, 1]))
767            })
768            .unwrap();
769        };
770        assert_eq!(left_out(source_rebuilt, vec![]), (Measure::Primitives, 1, 0));
771    }
772
773    /// An entity's drop names what it held: a placement and its mesh's primitives; nothing for one gone already.
774    #[test]
775    fn despawn_declares_what_the_entity_held() {
776        let mut w = World::new();
777        let proto = w.spawn();
778        let e = w.spawn();
779        w.insert(
780            e,
781            Instance {
782                prototype: proto,
783                variant: 0,
784            },
785        );
786        w.insert(e, crate::Transform::IDENTITY);
787        let prim = Primitive {
788            topology: Topology::Triangles,
789            streams: Default::default(),
790            indices: None,
791            material: None,
792        };
793        w.insert(e, Mesh::new(vec![prim.clone(), prim]));
794        let mut log = PassLog::default();
795        assert!(despawn(&mut w, e, &mut log, "left out"));
796        assert!(!despawn(&mut w, e, &mut log, "left out"));
797        assert_eq!(log.declared(Measure::Placements), 1);
798        assert_eq!(log.declared(Measure::Primitives), 2);
799        assert_eq!(log.declared(Measure::Textures), 0);
800        assert_eq!(
801            log.drops.get(&(Measure::Primitives, "left out".to_owned())),
802            Some(&2)
803        );
804    }
805
806    /// An entity an asset lists as a member is dropped from the asset when it is despawned, counted: the asset stays
807    /// whole (what it lists is alive), and an entity no asset lists is not counted.
808    #[test]
809    fn despawn_drops_the_entity_from_the_assets_that_list_it() {
810        use crate::asset::Role;
811        let mut w = World::new();
812        let (part, rig, other) = (w.spawn(), w.spawn(), w.spawn());
813        let (a, b) = (w.spawn(), w.spawn());
814        w.insert(
815            a,
816            Asset {
817                members: vec![(rig, Role::Rig), (part, Role::Part)],
818            },
819        );
820        w.insert(
821            b,
822            Asset {
823                members: vec![(part, Role::Part), (other, Role::Part)],
824            },
825        );
826        let mut log = PassLog::default();
827        assert!(despawn(&mut w, part, &mut log, "left out"));
828        assert_eq!(w.get::<Asset>(a).unwrap().members, [(rig, Role::Rig)]);
829        assert_eq!(w.get::<Asset>(b).unwrap().members, [(other, Role::Part)]);
830        assert_eq!(log.counts["asset members dropped with the entity they listed"], 2);
831        let mut log = PassLog::default();
832        assert!(despawn(&mut w, other, &mut log, "left out"));
833        assert_eq!(log.counts["asset members dropped with the entity they listed"], 1);
834        // an entity no asset lists counts nothing
835        let loose = w.spawn();
836        let mut log = PassLog::default();
837        assert!(despawn(&mut w, loose, &mut log, "left out"));
838        assert!(
839            !log.counts
840                .contains_key("asset members dropped with the entity they listed")
841        );
842    }
843
844    /// An occurrence a clip binding drives is dropped from the binding when it is despawned, counted: the binding keeps
845    /// its clip and its other meshes, and an entity no binding names counts nothing.
846    #[test]
847    fn despawn_drops_the_entity_from_the_clip_bindings_that_name_it() {
848        let mut w = World::new();
849        let (clip, a, b) = (w.spawn(), w.spawn(), w.spawn());
850        let (first, second) = (w.spawn(), w.spawn());
851        for (e, meshes) in [(first, vec![a, b]), (second, vec![a])] {
852            w.insert(
853                e,
854                ClipBinding {
855                    clip,
856                    rig: None,
857                    meshes,
858                },
859            );
860        }
861        let mut log = PassLog::default();
862        assert!(despawn(&mut w, a, &mut log, "left out"));
863        assert_eq!(w.get::<ClipBinding>(first).unwrap().meshes, [b]);
864        assert!(w.get::<ClipBinding>(second).unwrap().meshes.is_empty());
865        assert_eq!(w.get::<ClipBinding>(first).unwrap().clip, clip);
866        assert_eq!(
867            log.counts["clip binding meshes dropped with the entity they named"],
868            2
869        );
870        let loose = w.spawn();
871        let mut log = PassLog::default();
872        assert!(despawn(&mut w, loose, &mut log, "left out"));
873        assert!(
874            !log.counts
875                .contains_key("clip binding meshes dropped with the entity they named")
876        );
877    }
878}