Skip to main content

fast_mesh_store/
incidence.rs

1//! Vertex → triangles incidence index.
2//!
3//! Lists, for every vertex slot, all alive triangles that use it. Unlike
4//! [`crate::Adjacency::triangles_around`] it covers every fan of a non-manifold
5//! vertex, which is what BVH refits need after vertices move.
6//!
7//! Once built, the index follows topology edits instead of being rebuilt: the
8//! rows of the vertices an edit touches are rewritten into an overlay that
9//! shadows the compressed rows. When the overlay grows past a fraction of the
10//! index, the index is dropped and the next [`Mesh::ensure_incidence`] rebuilds
11//! it compactly.
12
13use std::collections::HashMap;
14
15use crate::adjacency::ZERO_PER_ELEMENT;
16use crate::column::{try_reserve, try_reserve_exact, try_reserve_map, try_vec};
17use crate::{Mesh, Result, StoreError, TriangleId, VertexId};
18
19/// Overlay size (entries plus rows) always allowed before the index is rebuilt.
20const MIN_OVERLAY: usize = 4096;
21
22/// Every alive triangle using each vertex, stored in compressed rows.
23#[derive(Debug)]
24pub struct Incidence {
25    /// `offsets[v]..offsets[v + 1]` is the range of `triangles` for vertex `v`.
26    offsets: Vec<u32>,
27    triangles: Vec<TriangleId>,
28    /// Rows rewritten since the compressed rows were built. A vertex present
29    /// here ignores its compressed row.
30    overlay: HashMap<VertexId, Vec<TriangleId>>,
31    /// Total entries in `overlay`.
32    overlay_len: usize,
33}
34
35/// The distinct vertices of a triangle: one repeating a vertex (`[a, a, b]`)
36/// is listed once for it.
37fn distinct(tri: [VertexId; 3]) -> impl Iterator<Item = VertexId> {
38    let [a, b, c] = tri;
39    [
40        Some(a),
41        (b != a).then_some(b),
42        (c != a && c != b).then_some(c),
43    ]
44    .into_iter()
45    .flatten()
46}
47
48impl Incidence {
49    pub(crate) fn build(mesh: &Mesh) -> Result<Self> {
50        let mut build = IncidenceBuild::new(mesh);
51        build.step(mesh, usize::MAX)?;
52        build.take().ok_or(StoreError::OutOfMemory)
53    }
54
55    /// Every alive triangle using `v`, each once, in ascending id order. Empty for
56    /// unused, dead or out-of-range vertices.
57    pub fn triangles(&self, v: VertexId) -> &[TriangleId] {
58        match self.overlay.get(&v) {
59            Some(row) => row,
60            None => compressed_row(&self.offsets, &self.triangles, v),
61        }
62    }
63
64    /// Lists triangle `t`, whose vertices are `tri`, in the index.
65    fn insert(&mut self, t: TriangleId, tri: [VertexId; 3]) -> Result<()> {
66        for v in distinct(tri) {
67            if self.triangles(v).binary_search(&t).is_ok() {
68                continue;
69            }
70            let row = self.row_mut(v)?;
71            if let Err(i) = row.binary_search(&t) {
72                try_reserve(row, 1)?;
73                row.insert(i, t);
74                self.overlay_len += 1;
75            }
76        }
77        Ok(())
78    }
79
80    /// The overlay row of `v`, created from its compressed row if needed.
81    fn row_mut(&mut self, v: VertexId) -> Result<&mut Vec<TriangleId>> {
82        if !self.overlay.contains_key(&v) {
83            let base = compressed_row(&self.offsets, &self.triangles, v);
84            let mut row = Vec::new();
85            try_reserve(&mut row, base.len() + 1)?;
86            row.extend_from_slice(base);
87            try_reserve_map(&mut self.overlay, 1)?;
88            self.overlay_len += row.len();
89            self.overlay.insert(v, row);
90        }
91        Ok(self.overlay.get_mut(&v).expect("inserted above"))
92    }
93
94    /// Takes `ids` that are alive now out of the index, before they are rewritten.
95    pub(crate) fn remove_alive(&mut self, mesh: &Mesh, ids: &[&[TriangleId]]) -> Result<()> {
96        for &t in ids.iter().flat_map(|ids| ids.iter()) {
97            if let Some(tri) = mesh.triangle(t) {
98                self.remove(t, tri)?;
99            }
100        }
101        Ok(())
102    }
103
104    /// Lists `ids` that are alive now, after they were written.
105    pub(crate) fn insert_alive(&mut self, mesh: &Mesh, ids: &[&[TriangleId]]) -> Result<()> {
106        for &t in ids.iter().flat_map(|ids| ids.iter()) {
107            if let Some(tri) = mesh.triangle(t) {
108                self.insert(t, tri)?;
109            }
110        }
111        Ok(())
112    }
113
114    /// Takes triangle `t`, whose vertices were `tri`, out of the index. Does
115    /// nothing for rows that do not list it.
116    pub(crate) fn remove(&mut self, t: TriangleId, tri: [VertexId; 3]) -> Result<()> {
117        for v in distinct(tri) {
118            if self.triangles(v).binary_search(&t).is_err() {
119                continue;
120            }
121            let row = self.row_mut(v)?;
122            if let Ok(i) = row.binary_search(&t) {
123                row.remove(i);
124                self.overlay_len -= 1;
125            }
126        }
127        Ok(())
128    }
129
130    /// True once the overlay is large enough that a compact rebuild pays off.
131    /// Rows count too, so emptied rows of removed vertices are not free.
132    fn overgrown(&self) -> bool {
133        self.overlay_len + self.overlay.len() > MIN_OVERLAY.max(self.triangles.len() / 8)
134    }
135}
136
137/// Where an [`IncidenceBuild`] stands: the pass it is in, with that pass's
138/// cursor (a triangle or vertex slot).
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140enum Pass {
141    /// Counting the triangles of each vertex, triangle by triangle.
142    Count(usize),
143    /// Turning the counts into row offsets, vertex by vertex.
144    Prefix(usize),
145    /// Zeroing the rows, up to this many entries.
146    Zero(usize),
147    /// Listing each vertex's triangles, triangle by triangle.
148    Fill(usize),
149    /// Finished: the index is ready to be taken.
150    Done,
151}
152
153/// A resumable build of a mesh's [`Incidence`]: the build of
154/// [`Mesh::ensure_incidence`] cut into steps of a bounded number of elements
155/// (triangle or vertex slots), so that a caller can yield between them.
156///
157/// Each step runs the same loop bodies as the one-call build, over the next
158/// elements in the same order, so the finished index is the same, entry for
159/// entry. A build is tied to the mesh state it started from (its uid, epoch,
160/// revision and slot counts); [`Mesh::step_incidence`] starts over when the mesh has
161/// changed since, and caches the index when the build is done.
162#[derive(Debug)]
163pub struct IncidenceBuild {
164    uid: u64,
165    epoch: u32,
166    revision: u64,
167    /// The slot counts: an undo brings the revision back but keeps slots
168    /// it added, so the revision alone does not tell the state.
169    slots: (usize, usize),
170    pass: Pass,
171    offsets: Vec<u32>,
172    triangles: Vec<TriangleId>,
173    cursor: Vec<u32>,
174}
175
176impl IncidenceBuild {
177    /// A build of `mesh`'s incidence index as the mesh is now, with nothing
178    /// done.
179    pub fn new(mesh: &Mesh) -> IncidenceBuild {
180        IncidenceBuild {
181            uid: mesh.uid(),
182            epoch: mesh.epoch(),
183            revision: mesh.revision(),
184            slots: (mesh.vertex_slots(), mesh.triangle_slots()),
185            pass: Pass::Count(0),
186            offsets: Vec::new(),
187            triangles: Vec::new(),
188            cursor: Vec::new(),
189        }
190    }
191
192    /// Whether the build started from `mesh` as it is now.
193    pub fn is_for(&self, mesh: &Mesh) -> bool {
194        self.uid == mesh.uid()
195            && self.epoch == mesh.epoch()
196            && self.revision == mesh.revision()
197            && self.slots == (mesh.vertex_slots(), mesh.triangle_slots())
198    }
199
200    /// Whether every pass is done.
201    pub fn is_done(&self) -> bool {
202        self.pass == Pass::Done
203    }
204
205    /// Runs the build over up to `budget` more elements (at least one, unless
206    /// it is done) of `mesh`, which must be the mesh state it started from.
207    /// Returns whether it is done.
208    fn step(&mut self, mesh: &Mesh, budget: usize) -> Result<bool> {
209        debug_assert!(
210            self.is_for(mesh),
211            "an incidence build stepped on another mesh"
212        );
213        let tris = mesh.triangles();
214        let slots = mesh.vertex_slots();
215        let mut left = budget.max(1);
216        while left > 0 {
217            match self.pass {
218                Pass::Count(from) => {
219                    if from == 0 {
220                        self.offsets = try_vec(slots + 1, 0u32)?;
221                    }
222                    let to = tris.len().min(from.saturating_add(left));
223                    for (t, &tri) in tris.iter().enumerate().take(to).skip(from) {
224                        if mesh.is_triangle_alive(t as TriangleId) {
225                            for v in distinct(tri) {
226                                self.offsets[v as usize + 1] += 1;
227                            }
228                        }
229                    }
230                    left -= (to - from).min(left);
231                    self.pass = if to == tris.len() {
232                        Pass::Prefix(0)
233                    } else {
234                        Pass::Count(to)
235                    };
236                }
237                Pass::Prefix(from) => {
238                    let to = slots.min(from.saturating_add(left));
239                    for v in from..to {
240                        self.offsets[v + 1] += self.offsets[v];
241                    }
242                    left -= (to - from).min(left);
243                    if to < slots {
244                        self.pass = Pass::Prefix(to);
245                        continue;
246                    }
247                    try_reserve_exact(&mut self.triangles, self.offsets[slots] as usize)?;
248                    self.pass = Pass::Zero(0);
249                }
250                Pass::Zero(from) => {
251                    let total = self.offsets[slots] as usize;
252                    let to = total.min(from.saturating_add(left.saturating_mul(ZERO_PER_ELEMENT)));
253                    self.triangles.resize(to, 0 as TriangleId);
254                    left -= (to - from).div_ceil(ZERO_PER_ELEMENT).min(left);
255                    if to < total {
256                        self.pass = Pass::Zero(to);
257                        continue;
258                    }
259                    self.cursor = try_vec(slots, 0u32)?;
260                    self.cursor.copy_from_slice(&self.offsets[..slots]);
261                    self.pass = Pass::Fill(0);
262                }
263                Pass::Fill(from) => {
264                    let to = tris.len().min(from.saturating_add(left));
265                    // Triangles are visited in ascending id order, so every row comes
266                    // out sorted.
267                    for (t, &tri) in tris.iter().enumerate().take(to).skip(from) {
268                        if mesh.is_triangle_alive(t as TriangleId) {
269                            for v in distinct(tri) {
270                                let at = &mut self.cursor[v as usize];
271                                self.triangles[*at as usize] = t as TriangleId;
272                                *at += 1;
273                            }
274                        }
275                    }
276                    left -= (to - from).min(left);
277                    if to < tris.len() {
278                        self.pass = Pass::Fill(to);
279                        continue;
280                    }
281                    self.cursor = Vec::new();
282                    self.pass = Pass::Done;
283                }
284                Pass::Done => return Ok(true),
285            }
286        }
287        Ok(self.pass == Pass::Done)
288    }
289
290    /// The finished index, once [`IncidenceBuild::step`] returned `true`.
291    fn take(&mut self) -> Option<Incidence> {
292        (self.pass == Pass::Done).then(|| Incidence {
293            offsets: std::mem::take(&mut self.offsets),
294            triangles: std::mem::take(&mut self.triangles),
295            overlay: HashMap::new(),
296            overlay_len: 0,
297        })
298    }
299}
300
301fn compressed_row<'a>(
302    offsets: &[u32],
303    triangles: &'a [TriangleId],
304    v: VertexId,
305) -> &'a [TriangleId] {
306    let v = v as usize;
307    if v + 1 >= offsets.len() {
308        return &[];
309    }
310    &triangles[offsets[v] as usize..offsets[v + 1] as usize]
311}
312
313impl Mesh {
314    /// Builds the vertex → triangles index if it is not cached. Once built, it
315    /// follows edits, rollbacks, undo and redo. It is dropped by [`Mesh::compact`],
316    /// by edits that leave so many rewritten rows that rebuilding is cheaper, and
317    /// if memory runs out while following an edit.
318    pub fn ensure_incidence(&mut self) -> Result<()> {
319        debug_assert!(!self.incidence_pending, "called inside a transaction");
320        if self.incidence.is_none() {
321            self.incidence = Some(Incidence::build(self)?);
322        }
323        Ok(())
324    }
325
326    /// The cached index, if [`Mesh::ensure_incidence`] built it. `None` inside a
327    /// transaction that changed topology, until it commits or rolls back.
328    pub fn incidence(&self) -> Option<&Incidence> {
329        self.incidence.as_ref().filter(|_| !self.incidence_pending)
330    }
331
332    /// [`Mesh::ensure_incidence`] in steps: runs `build` over up to `budget`
333    /// more elements and, once it is done, caches its index. Returns whether
334    /// the index is cached, at once when it already is. When the mesh changed
335    /// since `build` started, `build` starts over first. The index is the
336    /// same as from [`Mesh::ensure_incidence`], entry for entry.
337    ///
338    /// # Errors
339    /// As for [`Mesh::ensure_incidence`]; `build` then starts over.
340    pub fn step_incidence(&mut self, build: &mut IncidenceBuild, budget: usize) -> Result<bool> {
341        debug_assert!(!self.incidence_pending, "called inside a transaction");
342        if self.incidence.is_some() {
343            return Ok(true);
344        }
345        if !build.is_for(self) {
346            *build = IncidenceBuild::new(self);
347        }
348        match build.step(self, budget) {
349            Ok(done) => {
350                if done {
351                    self.incidence = build.take();
352                }
353                Ok(done)
354            }
355            Err(e) => {
356                *build = IncidenceBuild::new(self);
357                Err(e)
358            }
359        }
360    }
361
362    /// Runs `f` on the cached index. Drops the index if `f` fails (out of
363    /// memory) or the overlay grew large enough that a rebuild pays off.
364    pub(crate) fn with_incidence(&mut self, f: impl FnOnce(&mut Incidence, &Mesh) -> Result<()>) {
365        if let Some(mut inc) = self.incidence.take()
366            && f(&mut inc, self).is_ok()
367            && !inc.overgrown()
368        {
369            self.incidence = Some(inc);
370        }
371    }
372}
373
374#[cfg(test)]
375mod tests {
376    use crate::Mesh;
377    use crate::column::fail_allocations_after;
378
379    /// Three triangles share vertex 0; a fourth touches it only at vertex 0
380    /// (a bowtie), so the vertex is non-manifold.
381    fn fan_and_bowtie() -> Mesh {
382        Mesh::from_buffers(
383            &[
384                0., 0., 0., 1., 0., 0., 1., 1., 0., 0., 1., 0., -1., 0., 0., -1., -1., 0.,
385            ],
386            &[0, 1, 2, 0, 2, 3, 3, 2, 1, 0, 4, 5],
387        )
388        .unwrap()
389    }
390
391    /// Brute-force rows for every vertex slot.
392    fn expected(m: &Mesh) -> Vec<Vec<u32>> {
393        let mut rows = vec![Vec::new(); m.vertex_slots()];
394        for t in 0..m.triangle_slots() as u32 {
395            if let Some(tri) = m.triangle(t) {
396                for v in tri {
397                    if rows[v as usize].last() != Some(&t) {
398                        rows[v as usize].push(t);
399                    }
400                }
401            }
402        }
403        rows
404    }
405
406    fn assert_matches(m: &Mesh) {
407        let inc = m.incidence().expect("index kept");
408        for (v, row) in expected(m).iter().enumerate() {
409            assert_eq!(inc.triangles(v as u32), row.as_slice(), "vertex {v}");
410        }
411    }
412
413    #[test]
414    fn lists_every_triangle_of_every_fan() {
415        let mut m = fan_and_bowtie();
416        m.ensure_incidence().unwrap();
417        let inc = m.incidence().unwrap();
418        assert_eq!(inc.triangles(0), &[0, 1, 3]);
419        assert_eq!(inc.triangles(2), &[0, 1, 2]);
420        assert_eq!(inc.triangles(5), &[3]);
421        assert!(inc.triangles(99).is_empty());
422    }
423
424    #[test]
425    fn repeated_vertices_are_listed_once() {
426        let mut m = Mesh::from_buffers(&[0., 0., 0., 1., 0., 0.], &[0, 0, 1]).unwrap();
427        m.ensure_incidence().unwrap();
428        assert_eq!(m.incidence().unwrap().triangles(0), &[0]);
429        assert_eq!(m.incidence().unwrap().triangles(1), &[0]);
430    }
431
432    #[test]
433    fn topology_edits_undo_and_redo_keep_the_index_current() {
434        let mut m = fan_and_bowtie();
435        m.ensure_incidence().unwrap();
436        let mut tx = m.begin_edit();
437        tx.set_positions(&[0], &[[0., 0., 1.]]).unwrap();
438        tx.commit().unwrap();
439        assert_matches(&m);
440
441        let mut tx = m.begin_edit();
442        tx.remove_triangles(&[1]).unwrap();
443        assert!(tx.mesh().incidence().is_none(), "hidden while stale");
444        let v = tx.add_vertices(&[[2., 2., 0.]]).unwrap()[0];
445        tx.add_triangles(&[[v, 2, 3], [0, 0, v]]).unwrap();
446        tx.set_triangles(&[2], &[[3, 1, 5]]).unwrap();
447        let patch = tx.commit().unwrap();
448        assert_matches(&m);
449        assert_eq!(m.incidence().unwrap().triangles(0), &[0, 3, 5]);
450
451        patch.revert(&mut m).unwrap();
452        assert_matches(&m);
453        patch.apply(&mut m).unwrap();
454        assert_matches(&m);
455    }
456
457    #[test]
458    fn rollback_restores_the_index() {
459        let mut m = fan_and_bowtie();
460        m.ensure_incidence().unwrap();
461        let mut tx = m.begin_edit();
462        tx.remove_triangles(&[0, 3]).unwrap();
463        tx.add_triangles(&[[1, 2, 3]]).unwrap();
464        tx.rollback();
465        assert_matches(&m);
466    }
467
468    /// A strip of `n` triangles over `n + 2` vertices.
469    fn strip(n: u32) -> Mesh {
470        let positions: Vec<f32> = (0..n + 2)
471            .flat_map(|i| [(i / 2) as f32, (i % 2) as f32, 0.])
472            .collect();
473        let indices: Vec<u32> = (0..n).flat_map(|i| [i, i + 1, i + 2]).collect();
474        Mesh::from_buffers(&positions, &indices).unwrap()
475    }
476
477    #[test]
478    fn small_edits_keep_the_index_and_large_ones_drop_it() {
479        let mut m = strip(10_000);
480        m.ensure_incidence().unwrap();
481        let mut tx = m.begin_edit();
482        tx.set_triangles(&[5_000], &[[5_002, 5_001, 5_000]])
483            .unwrap();
484        tx.commit().unwrap();
485        assert_matches(&m);
486
487        // Rewriting every triangle would leave the whole index in the overlay.
488        let ids: Vec<u32> = (0..10_000).collect();
489        let flipped: Vec<[u32; 3]> = ids.iter().map(|&i| [i + 2, i + 1, i]).collect();
490        let mut tx = m.begin_edit();
491        tx.set_triangles(&ids, &flipped).unwrap();
492        let patch = tx.commit().unwrap();
493        assert!(m.incidence().is_none());
494        m.ensure_incidence().unwrap();
495        assert_matches(&m);
496        patch.revert(&mut m).unwrap();
497        assert!(m.incidence().is_none());
498    }
499
500    #[test]
501    fn compaction_drops_the_index() {
502        let mut m = fan_and_bowtie();
503        m.ensure_incidence().unwrap();
504        let mut tx = m.begin_edit();
505        tx.remove_triangles(&[1]).unwrap();
506        tx.commit().unwrap();
507        m.compact().unwrap();
508        assert!(m.incidence().is_none());
509        m.ensure_incidence().unwrap();
510        assert_matches(&m);
511    }
512
513    #[test]
514    fn running_out_of_memory_drops_the_index_but_not_the_edit() {
515        let mut dropped = false;
516        for n in 0..64 {
517            let mut m = fan_and_bowtie();
518            m.ensure_incidence().unwrap();
519            let mut tx = m.begin_edit();
520            tx.remove_triangles(&[1]).unwrap();
521            fail_allocations_after(Some(n));
522            let committed = tx.commit().is_ok();
523            fail_allocations_after(None);
524            match m.incidence() {
525                Some(_) => assert_matches(&m),
526                None => {
527                    assert!(committed, "a rollback keeps the index");
528                    dropped = true;
529                }
530            }
531        }
532        assert!(dropped, "some budget must drop the index");
533    }
534
535    #[test]
536    fn stepped_build_matches_one_call() {
537        use crate::IncidenceBuild;
538        let mixed = crate::adjacency::tests::mixed;
539        let mut whole = mixed();
540        whole.ensure_incidence().unwrap();
541        let reference = whole.incidence().unwrap();
542        for budget in [1, 3, 50, usize::MAX] {
543            let mut m = mixed();
544            let mut build = IncidenceBuild::new(&m);
545            while !m.step_incidence(&mut build, budget).unwrap() {
546                assert!(m.incidence().is_none(), "cached before the last step");
547            }
548            assert!(build.is_done());
549            let got = m.incidence().unwrap();
550            assert_eq!(got.offsets, reference.offsets, "budget {budget}");
551            assert_eq!(got.triangles, reference.triangles, "budget {budget}");
552            assert_matches(&m);
553        }
554        // Started over after an edit.
555        let mut m = mixed();
556        let mut build = IncidenceBuild::new(&m);
557        assert!(!m.step_incidence(&mut build, 5).unwrap());
558        let mut tx = m.begin_edit();
559        tx.remove_triangles(&[7]).unwrap();
560        tx.commit().unwrap();
561        assert!(!build.is_for(&m));
562        while !m.step_incidence(&mut build, 5).unwrap() {}
563        assert_matches(&m);
564    }
565
566    #[test]
567    fn stepped_build_starts_over_after_edit_and_undo() {
568        use crate::IncidenceBuild;
569        let mut m = crate::adjacency::tests::mixed();
570        let mut build = IncidenceBuild::new(&m);
571        for _ in 0..(m.triangle_slots() + 3) {
572            m.step_incidence(&mut build, 1).unwrap();
573        }
574        crate::adjacency::tests::grow_and_undo(&mut m);
575        assert!(!build.is_for(&m));
576        while !m.step_incidence(&mut build, 1000).unwrap() {}
577        assert_matches(&m);
578    }
579}