Skip to main content

fast_mesh_bvh/
update.rs

1//! Incremental updates driven by the store's `ChangeSet`.
2//!
3//! Every fallible allocation happens in a preparation phase before the tree is
4//! touched; the mutation phase cannot fail. Two follow-up steps — rebuilding
5//! degraded treelets and compacting storage — are optimizations: if memory runs
6//! out there they are skipped and the tree stays correct.
7
8use fast_mesh_store::{ChangeSet, Mesh, TriangleId};
9
10use crate::Aabb;
11use crate::arena::{NONE, Node};
12use crate::build::{Prims, build_subtree, max_nodes, reserve_headroom};
13use crate::error::{BvhError, Result, try_reserve, try_reserve_map, try_vec};
14use crate::tree::{Bvh, Treelet, next_generation};
15
16/// How `update` brought the tree up to date.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum Strategy {
19    /// Nothing relevant changed.
20    Unchanged,
21    /// Only boxes were recomputed.
22    Refit,
23    /// Some treelets were rebuilt (and boxes refit).
24    Partial,
25    /// The whole tree was rebuilt.
26    Full,
27}
28
29/// What `update` did, for diagnostics and benchmarks.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub struct UpdateReport {
32    pub strategy: Strategy,
33    /// Inner node boxes rewritten while propagating refits.
34    pub refit_nodes: usize,
35    /// Treelets rebuilt (topology changes plus degradation).
36    pub rebuilt_treelets: usize,
37    /// Whether garbage was compacted away.
38    pub compacted: bool,
39}
40
41impl UpdateReport {
42    fn new(strategy: Strategy) -> Self {
43        Self {
44            strategy,
45            refit_nodes: 0,
46            rebuilt_treelets: 0,
47            compacted: false,
48        }
49    }
50}
51
52/// Sorted union of two sorted, deduplicated id lists.
53fn merge(a: &[u32], b: &[u32]) -> Result<Vec<u32>> {
54    let mut out = Vec::new();
55    try_reserve(&mut out, a.len() + b.len())?;
56    out.extend_from_slice(a);
57    out.extend_from_slice(b);
58    out.sort_unstable();
59    out.dedup();
60    Ok(out)
61}
62
63impl Bvh {
64    /// Brings the BVH up to date with `mesh`, given the [`ChangeSet`]s of every
65    /// commit, undo and redo since the BVH last matched (merge them when there are
66    /// several). Builds the mesh's incidence index when vertices moved.
67    ///
68    /// Chooses the cheapest correct strategy: refit for moves, treelet rebuilds for
69    /// local topology changes, a full rebuild when more than
70    /// `UpdatePolicy::full_rebuild_fraction` of the triangles are dirty. On error the
71    /// BVH is unchanged.
72    ///
73    /// When the BVH already matches the mesh's revision, `changes` is ignored and
74    /// the result is [`Strategy::Unchanged`], so applying a change set twice is
75    /// harmless. When the mesh was compacted since the last full build
76    /// ([`Mesh::epoch`] changed), every slot id is stale and the tree is rebuilt
77    /// from scratch ([`Strategy::Full`]) whatever `changes` says.
78    pub fn update(&mut self, mesh: &mut Mesh, changes: &ChangeSet) -> Result<UpdateReport> {
79        if mesh.uid() != self.mesh_uid {
80            return Err(BvhError::MeshMismatch);
81        }
82        if mesh.epoch() != self.epoch {
83            self.rebuild_from(mesh)?;
84            return Ok(UpdateReport::new(Strategy::Full));
85        }
86        if mesh.revision() == self.revision {
87            return Ok(UpdateReport::new(Strategy::Unchanged));
88        }
89        if !changes.vertices_moved.is_empty() {
90            mesh.ensure_incidence()?;
91        }
92        let mesh = &*mesh;
93        let removed = merge(&changes.triangles_removed, &changes.triangles_changed)?;
94        let added = merge(&changes.triangles_added, &changes.triangles_changed)?;
95        let mut moved = Vec::new();
96        if let Some(inc) = mesh
97            .incidence()
98            .filter(|_| !changes.vertices_moved.is_empty())
99        {
100            let total: usize = changes
101                .vertices_moved
102                .iter()
103                .map(|&v| inc.triangles(v).len())
104                .sum();
105            try_reserve(&mut moved, total)?;
106            for &v in &changes.vertices_moved {
107                moved.extend_from_slice(inc.triangles(v));
108            }
109            moved.sort_unstable();
110            moved.dedup();
111            // Triangles that are rebuilt anyway need no refit.
112            moved.retain(|t| removed.binary_search(t).is_err() && added.binary_search(t).is_err());
113        }
114
115        let dirty = removed.len().max(added.len()) + moved.len();
116        if removed.is_empty() && added.is_empty() && moved.is_empty() {
117            self.revision = mesh.revision();
118            return Ok(UpdateReport::new(Strategy::Unchanged));
119        }
120        let alive = mesh.triangle_count().max(1);
121        if self.root == NONE || dirty as f32 > self.policy.full_rebuild_fraction * alive as f32 {
122            self.rebuild_from(mesh)?;
123            return Ok(UpdateReport::new(Strategy::Full));
124        }
125
126        let mut report = UpdateReport::new(if removed.is_empty() && added.is_empty() {
127            Strategy::Refit
128        } else {
129            Strategy::Partial
130        });
131        // Leaves of moved triangles, gathered after the rebuilds (which move
132        // triangles to new leaves); reserved now so the refit cannot fail.
133        let mut leaves = Vec::new();
134        try_reserve(&mut leaves, moved.len())?;
135        let mut touched = self.rebuild_treelets(mesh, &removed, &added)?;
136        report.rebuilt_treelets = touched.rebuilt;
137        report.refit_nodes += touched.refit_nodes;
138
139        // Refit each leaf of a moved triangle once and propagate upwards.
140        let (positions, triangles) = (mesh.positions(), mesh.triangles());
141        for &t in &moved {
142            match self.arena.tri_leaf.get(t as usize) {
143                Some(&leaf) if leaf != NONE => leaves.push(leaf),
144                _ => {}
145            }
146        }
147        leaves.sort_unstable();
148        leaves.dedup();
149        for &leaf in &leaves {
150            self.arena.refit_leaf(leaf, positions, triangles);
151            report.refit_nodes += self.arena.refit_ancestors(leaf);
152            if let Some(i) = self.treelet_of(leaf) {
153                touched.mark(i);
154            }
155        }
156
157        // Best effort: rebuild treelets whose quality degraded. Treelets split
158        // off during this update (indices past `touched`) were just built.
159        for i in 0..touched.refit.len() {
160            if !touched.refit[i] || touched.rebuilt_now[i] {
161                continue;
162            }
163            let t = self.treelets[i];
164            if self.arena.sah_cost(t.root) > self.policy.degradation_factor * t.build_cost
165                && self.rebuild_one(mesh, i).is_ok()
166            {
167                report.rebuilt_treelets += 1;
168                report.strategy = Strategy::Partial;
169            }
170        }
171
172        // Best effort: drop garbage once it exceeds half of the live tree.
173        let live_refs = self.arena.refs.len() - self.garbage_refs;
174        if (2 * self.garbage_nodes > self.node_count() || 2 * self.garbage_refs > live_refs)
175            && self.compact_storage().is_ok()
176        {
177            report.compacted = true;
178        }
179        // The rebuilds and refits above changed the tree, so the generation
180        // moves on. `compact_storage` bumps it too when it ran (it renumbers
181        // the nodes); this second bump is harmless.
182        self.generation = next_generation();
183        self.revision = mesh.revision();
184        Ok(report)
185    }
186
187    /// Recomputes the boxes of the leaves holding `triangles` and their ancestors.
188    ///
189    /// If the mesh was compacted since the last full build ([`Mesh::epoch`]
190    /// changed), slot ids are stale, so this rebuilds the whole tree instead (like
191    /// [`Bvh::rebuild`], which also records the mesh revision). On error the BVH
192    /// is unchanged.
193    pub fn refit(&mut self, mesh: &Mesh, triangles: &[TriangleId]) -> Result<()> {
194        if mesh.uid() != self.mesh_uid {
195            return Err(BvhError::MeshMismatch);
196        }
197        if mesh.epoch() != self.epoch {
198            return self.rebuild(mesh);
199        }
200        for &t in triangles {
201            let leaf = self.arena.tri_leaf.get(t as usize).copied().unwrap_or(NONE);
202            if leaf != NONE {
203                self.arena
204                    .refit_leaf(leaf, mesh.positions(), mesh.triangles());
205                self.arena.refit_ancestors(leaf);
206            }
207        }
208        self.generation = next_generation();
209        Ok(())
210    }
211
212    /// Rebuilds the treelets containing `triangles` from their current contents
213    /// (triangles that died since are dropped).
214    ///
215    /// If the mesh was compacted since the last full build ([`Mesh::epoch`]
216    /// changed), slot ids are stale, so this rebuilds the whole tree instead (like
217    /// [`Bvh::rebuild`], which also records the mesh revision). Memory for every
218    /// affected treelet is reserved before the first one is rebuilt, so on error
219    /// the BVH is unchanged.
220    pub fn rebuild_region(&mut self, mesh: &Mesh, triangles: &[TriangleId]) -> Result<()> {
221        if mesh.uid() != self.mesh_uid {
222            return Err(BvhError::MeshMismatch);
223        }
224        if mesh.epoch() != self.epoch {
225            return self.rebuild(mesh);
226        }
227        let mut which = Vec::new();
228        try_reserve(&mut which, triangles.len())?;
229        for &t in triangles {
230            if let Some(i) = self.treelet_of_triangle(t) {
231                which.push(i);
232            }
233        }
234        which.sort_unstable();
235        which.dedup();
236        let mut budget = Budget::default();
237        for &i in &which {
238            budget.add(
239                self.arena.subtree_size(self.treelets[i].root).1,
240                self.options.treelet_size,
241            );
242        }
243        let mut scratch = self.reserve_rebuilds(&budget)?;
244        // Mutation phase: nothing below can fail.
245        for &i in &which {
246            self.replace_treelet(mesh, i, &[], &[], &[], &mut scratch);
247        }
248        self.generation = next_generation();
249        Ok(())
250    }
251
252    /// Rebuilds the whole tree from `mesh`. On error the BVH is unchanged.
253    pub fn rebuild(&mut self, mesh: &Mesh) -> Result<()> {
254        if mesh.uid() != self.mesh_uid {
255            return Err(BvhError::MeshMismatch);
256        }
257        self.rebuild_from(mesh)
258    }
259
260    /// Removes `removed` from the tree and inserts `added`, rebuilding each
261    /// affected treelet. All allocation happens before the first write.
262    fn rebuild_treelets(&mut self, mesh: &Mesh, removed: &[u32], added: &[u32]) -> Result<Touched> {
263        let mut touched = Touched::new(self.treelets.len())?;
264        if removed.is_empty() && added.is_empty() {
265            return Ok(touched);
266        }
267        // tri_leaf must cover new slots; growing it with NONE changes nothing else.
268        if self.arena.tri_leaf.len() < mesh.triangle_slots() {
269            let extra = mesh.triangle_slots() - self.arena.tri_leaf.len();
270            reserve_headroom(&mut self.arena.tri_leaf, extra)?;
271            self.arena.tri_leaf.resize(mesh.triangle_slots(), NONE);
272        }
273        let positions = mesh.positions();
274        let triangles = mesh.triangles();
275        // Which treelet each removal and insertion belongs to. An added triangle
276        // that is already in the tree (a change set applied twice, or merged sets
277        // re-listing an addition) is also treated as removed from where it is, so
278        // it can never be gathered twice.
279        let mut queued: Vec<Vec<u32>> = Vec::new();
280        try_reserve(&mut queued, self.treelets.len())?;
281        queued.resize_with(self.treelets.len(), Vec::new);
282        for &t in removed.iter().chain(added) {
283            if let Some(i) = self.treelet_of_triangle(t) {
284                touched.rebuild[i] = true;
285            }
286        }
287        for &t in added {
288            if !mesh.is_triangle_alive(t) {
289                continue;
290            }
291            let i = self.choose_treelet(&Aabb::of_triangle(positions, triangles[t as usize]));
292            touched.rebuild[i] = true;
293            try_reserve(&mut queued[i], 1)?;
294            queued[i].push(t);
295        }
296        // Reserve for the largest rebuild and for all appended nodes and refs.
297        let mut budget = Budget::default();
298        for (i, q) in queued.iter().enumerate() {
299            if touched.rebuild[i] {
300                let (_, refs) = self.arena.subtree_size(self.treelets[i].root);
301                budget.add(refs + q.len(), self.options.treelet_size);
302            }
303        }
304        let mut scratch = self.reserve_rebuilds(&budget)?;
305
306        // Mutation phase: nothing below can fail. Clear removed and re-added
307        // triangles first so a rebuilt treelet never overwrites the leaf of a
308        // triangle inserted elsewhere.
309        for &t in removed.iter().chain(added) {
310            if let Some(leaf) = self.arena.tri_leaf.get_mut(t as usize) {
311                *leaf = NONE;
312            }
313        }
314        for (i, q) in queued.iter().enumerate() {
315            if touched.rebuild[i] {
316                touched.refit_nodes +=
317                    self.replace_treelet(mesh, i, removed, added, q, &mut scratch);
318                touched.rebuilt += 1;
319                touched.rebuilt_now[i] = true;
320            }
321        }
322        Ok(touched)
323    }
324
325    /// Rebuilds one treelet with its own allocation (used for degradation). On
326    /// error the tree is unchanged.
327    fn rebuild_one(&mut self, mesh: &Mesh, i: usize) -> Result<()> {
328        let mut budget = Budget::default();
329        budget.add(
330            self.arena.subtree_size(self.treelets[i].root).1,
331            self.options.treelet_size,
332        );
333        let mut scratch = self.reserve_rebuilds(&budget)?;
334        self.replace_treelet(mesh, i, &[], &[], &[], &mut scratch);
335        Ok(())
336    }
337
338    /// Treelet holding triangle `t`, if the tree holds it.
339    fn treelet_of_triangle(&self, t: TriangleId) -> Option<usize> {
340        match self.arena.tri_leaf.get(t as usize) {
341            Some(&leaf) if leaf != NONE => self.treelet_of(leaf),
342            _ => None,
343        }
344    }
345
346    /// Reserves everything a batch of treelet rebuilds described by `budget`
347    /// needs, so that [`Bvh::replace_treelet`] cannot fail. Only capacity
348    /// changes, so on error the tree is unchanged.
349    fn reserve_rebuilds(&mut self, budget: &Budget) -> Result<Scratch> {
350        let prims = Prims::with_capacity(budget.max_n)?;
351        reserve_headroom(&mut self.arena.nodes, budget.nodes)?;
352        reserve_headroom(&mut self.arena.parents, budget.nodes)?;
353        reserve_headroom(&mut self.counts, budget.nodes)?;
354        reserve_headroom(&mut self.arena.refs, budget.refs)?;
355        if budget.extra_treelets > 0 {
356            try_reserve(&mut self.treelets, budget.extra_treelets)?;
357        }
358        // Every rebuild inserts its new root; splits insert their extra roots.
359        try_reserve_map(
360            &mut self.treelet_of_root,
361            budget.rebuilds + budget.extra_treelets,
362        )?;
363        Ok(Scratch { prims })
364    }
365
366    /// Builds a new subtree for treelet `i` from its triangles that are still
367    /// alive and in neither `removed` nor `added`, plus `insert`, links it in
368    /// place of the old one and refits the ancestors, and counts the new
369    /// nodes' triangles (and the ancestors' again). A result holding more than
370    /// twice `treelet_size` triangles is split into several treelets. Capacity
371    /// must already be reserved with [`Bvh::reserve_rebuilds`]. Dropped
372    /// triangles whose `tri_leaf` still points into the old treelet get `NONE`.
373    /// Returns how many ancestor boxes changed.
374    fn replace_treelet(
375        &mut self,
376        mesh: &Mesh,
377        i: usize,
378        removed: &[u32],
379        added: &[u32],
380        insert: &[u32],
381        scratch: &mut Scratch,
382    ) -> usize {
383        let old_root = self.treelets[i].root;
384        let (positions, triangles) = (mesh.positions(), mesh.triangles());
385        let prims = &mut scratch.prims;
386        prims.clear();
387        let (mut old_nodes, mut old_refs) = (0usize, 0usize);
388        // Taken out (without allocating) so the traversal can clear entries.
389        let mut tri_leaf = std::mem::take(&mut self.arena.tri_leaf);
390        let arena = &self.arena;
391        arena.traverse(old_root, |node, n| {
392            old_nodes += 1;
393            if n.is_leaf() {
394                old_refs += n.leaf_range().len();
395                for &t in &arena.refs[n.leaf_range()] {
396                    if mesh.is_triangle_alive(t)
397                        && removed.binary_search(&t).is_err()
398                        && added.binary_search(&t).is_err()
399                    {
400                        prims.push(t, Aabb::of_triangle(positions, triangles[t as usize]));
401                    } else if tri_leaf[t as usize] == node {
402                        tri_leaf[t as usize] = NONE;
403                    }
404                }
405            }
406            true
407        });
408        self.arena.tri_leaf = tri_leaf;
409        for &t in insert {
410            prims.push(t, Aabb::of_triangle(positions, triangles[t as usize]));
411        }
412        let parent = self.arena.parents[old_root as usize];
413        let first = self.arena.nodes.len();
414        let new_root = build_subtree(&mut self.arena, prims, &self.options, parent)
415            .expect("capacity for the rebuild was reserved before mutating");
416        // The new nodes are the tail of the arena, children after parents.
417        let nodes = &self.arena.nodes;
418        self.counts.resize(nodes.len(), 0);
419        for k in (first..nodes.len()).rev() {
420            let n = &nodes[k];
421            self.counts[k] = if n.is_leaf() {
422                n.leaf_range().len() as u32
423            } else {
424                self.counts[n.a as usize] + self.counts[n.b as usize]
425            };
426        }
427        if parent == NONE {
428            self.root = new_root;
429        } else {
430            let p = &mut self.arena.nodes[parent as usize];
431            if p.a == old_root {
432                p.a = new_root;
433            } else {
434                p.b = new_root;
435            }
436        }
437        self.garbage_nodes += old_nodes;
438        self.garbage_refs += old_refs;
439        self.treelet_of_root.remove(&old_root);
440        self.treelet_of_root.insert(new_root, i);
441        self.treelets[i] = Treelet {
442            root: new_root,
443            build_cost: self.arena.sah_cost(new_root),
444        };
445        if prims.len() > 2 * self.options.treelet_size as usize {
446            self.split_treelet(i);
447        }
448        // The ancestors' counts, from the parent up.
449        let mut up = parent;
450        while up != NONE {
451            let n = &self.arena.nodes[up as usize];
452            self.counts[up as usize] = self.counts[n.a as usize] + self.counts[n.b as usize];
453            up = self.arena.parents[up as usize];
454        }
455        self.arena.refit_ancestors(new_root)
456    }
457
458    /// Splits treelet `i`, whose nodes' triangles are counted, by the rule
459    /// `assign_treelets` uses: the topmost subtrees with at most `treelet_size`
460    /// triangles, or single leaves. Best effort and allocation-free: skipped when
461    /// the reserved treelet capacity is too small.
462    fn split_treelet(&mut self, i: usize) {
463        let root = self.treelets[i].root;
464        let limit = self.options.treelet_size;
465        let counts = &self.counts;
466        let is_root = |k: u32, n: &Node| n.is_leaf() || counts[k as usize] <= limit;
467        let mut found = 0usize;
468        self.arena.traverse(root, |k, n| {
469            let r = is_root(k, n);
470            found += usize::from(r);
471            !r
472        });
473        let extra = found.saturating_sub(1);
474        // The map needs room for all `found` inserts: removing the old root
475        // may leave a tombstone that does not free its room.
476        if extra == 0
477            || self.treelets.capacity() - self.treelets.len() < extra
478            || self.treelet_of_root.capacity() - self.treelet_of_root.len() < found
479        {
480            return;
481        }
482        let arena = &self.arena;
483        let treelets = &mut self.treelets;
484        let map = &mut self.treelet_of_root;
485        map.remove(&root);
486        let mut slot = Some(i);
487        arena.traverse(root, |k, n| {
488            let r = is_root(k, n);
489            if r {
490                let t = Treelet {
491                    root: k,
492                    build_cost: arena.sah_cost(k),
493                };
494                if let Some(i) = slot.take() {
495                    treelets[i] = t;
496                    map.insert(k, i);
497                } else {
498                    map.insert(k, treelets.len());
499                    treelets.push(t);
500                }
501            }
502            !r
503        });
504    }
505
506    /// Descends the top tree, at each node taking the child whose box grows least,
507    /// until reaching a treelet root.
508    fn choose_treelet(&self, b: &Aabb) -> usize {
509        let mut node = self.root;
510        loop {
511            if let Some(&i) = self.treelet_of_root.get(&node) {
512                return i;
513            }
514            let n = &self.arena.nodes[node as usize];
515            let growth = |c: u32| {
516                let cb = &self.arena.nodes[c as usize].bounds;
517                cb.union(b).surface_area() - cb.surface_area()
518            };
519            node = if growth(n.b) < growth(n.a) { n.b } else { n.a };
520        }
521    }
522}
523
524/// Per-treelet bookkeeping for one update.
525struct Touched {
526    rebuild: Vec<bool>,
527    rebuilt_now: Vec<bool>,
528    refit: Vec<bool>,
529    rebuilt: usize,
530    refit_nodes: usize,
531}
532
533impl Touched {
534    fn new(n: usize) -> Result<Self> {
535        Ok(Self {
536            rebuild: try_vec(n, false)?,
537            rebuilt_now: try_vec(n, false)?,
538            refit: try_vec(n, false)?,
539            rebuilt: 0,
540            refit_nodes: 0,
541        })
542    }
543
544    /// Records a refit in `treelet`. Treelets split off during this update are
545    /// not tracked: they were just built.
546    fn mark(&mut self, treelet: usize) {
547        if let Some(r) = self.refit.get_mut(treelet) {
548            *r = true;
549        }
550    }
551}
552
553/// Sizes of a batch of treelet rebuilds, accumulated before reserving.
554#[derive(Default)]
555struct Budget {
556    /// Largest rebuild, in triangles.
557    max_n: usize,
558    nodes: usize,
559    refs: usize,
560    rebuilds: usize,
561    /// Upper bound on the treelets added by splitting oversized results.
562    extra_treelets: usize,
563}
564
565impl Budget {
566    /// Adds a rebuild of at most `n` triangles.
567    fn add(&mut self, n: usize, treelet_size: u32) {
568        self.max_n = self.max_n.max(n);
569        self.nodes += max_nodes(n);
570        self.refs += n;
571        self.rebuilds += 1;
572        let size = treelet_size as usize;
573        if n > 2 * size {
574            self.extra_treelets += n.div_ceil(size) * 2 + 1;
575        }
576    }
577}
578
579/// Scratch memory reserved for a batch of treelet rebuilds.
580struct Scratch {
581    prims: Prims,
582}
583
584#[cfg(test)]
585mod tests {
586    use fast_mesh_store::{History, Mesh};
587
588    use crate::{Aabb, BuildOptions, Bvh, BvhError, Strategy, UpdatePolicy};
589
590    /// n×n grid of quads in the XY plane.
591    fn grid(n: u32) -> Mesh {
592        let mut positions = Vec::new();
593        for y in 0..=n {
594            for x in 0..=n {
595                positions.extend([x as f32, y as f32, 0.0]);
596            }
597        }
598        let mut indices = Vec::new();
599        for y in 0..n {
600            for x in 0..n {
601                let a = y * (n + 1) + x;
602                indices.extend([a, a + 1, a + n + 2, a, a + n + 2, a + n + 1]);
603            }
604        }
605        Mesh::from_buffers(&positions, &indices).unwrap()
606    }
607
608    fn opts() -> BuildOptions {
609        BuildOptions {
610            max_leaf_size: 4,
611            treelet_size: 32,
612            ..Default::default()
613        }
614    }
615
616    fn hits(bvh: &Bvh, mesh: &Mesh, min: [f32; 3], max: [f32; 3]) -> Vec<u32> {
617        let mut out = Vec::new();
618        bvh.query_aabb(mesh, min, max, &mut out);
619        out.sort();
620        out
621    }
622
623    #[test]
624    fn moving_a_vertex_refits() {
625        let mut m = grid(16);
626        let mut bvh = Bvh::build(&m, &opts()).unwrap();
627        let mut tx = m.begin_edit();
628        tx.set_positions(&[0], &[[0.0, 0.0, 5.0]]).unwrap();
629        let p = tx.commit().unwrap();
630        let report = bvh.update(&mut m, p.changes()).unwrap();
631        assert_eq!(report.strategy, Strategy::Refit);
632        assert!(report.refit_nodes > 0);
633        bvh.validate(&m).unwrap();
634        assert!(bvh.is_current(&m));
635        assert_eq!(
636            hits(&bvh, &m, [-1.0, -1.0, 4.0], [0.5, 0.5, 6.0]),
637            vec![0, 1]
638        );
639    }
640
641    #[test]
642    fn local_topology_edits_rebuild_only_their_treelets() {
643        let mut m = grid(16);
644        let mut bvh = Bvh::build(&m, &opts()).unwrap();
645        let treelets = bvh.treelet_count();
646        assert!(treelets > 4);
647        let mut tx = m.begin_edit();
648        tx.remove_triangles(&[0, 1]).unwrap();
649        let v = tx.add_vertices(&[[0.5, 0.5, 2.0]]).unwrap()[0];
650        tx.add_triangles(&[[0, 1, v]]).unwrap();
651        let p = tx.commit().unwrap();
652        let report = bvh.update(&mut m, p.changes()).unwrap();
653        assert_eq!(report.strategy, Strategy::Partial);
654        assert!(report.rebuilt_treelets >= 1 && report.rebuilt_treelets < treelets);
655        bvh.validate(&m).unwrap();
656        assert_eq!(
657            hits(&bvh, &m, [0.0, 0.0, 1.5], [1.0, 1.0, 2.5]),
658            p.changes().triangles_added
659        );
660    }
661
662    #[test]
663    fn large_edits_rebuild_everything() {
664        let mut m = grid(8);
665        let mut bvh = Bvh::build(&m, &opts()).unwrap();
666        let all: Vec<u32> = (0..m.vertex_slots() as u32).collect();
667        let lifted: Vec<[f32; 3]> = all
668            .iter()
669            .map(|&v| {
670                let p = m.position(v).unwrap();
671                [p[0], p[1], 1.0]
672            })
673            .collect();
674        let mut tx = m.begin_edit();
675        tx.set_positions(&all, &lifted).unwrap();
676        let p = tx.commit().unwrap();
677        let report = bvh.update(&mut m, p.changes()).unwrap();
678        assert_eq!(report.strategy, Strategy::Full);
679        bvh.validate(&m).unwrap();
680    }
681
682    #[test]
683    fn empty_changes_only_record_the_revision() {
684        let mut m = grid(2);
685        let mut bvh = Bvh::build(&m, &opts()).unwrap();
686        let mut tx = m.begin_edit();
687        tx.add_vertices(&[[9.0; 3]]).unwrap();
688        let p = tx.commit().unwrap();
689        assert!(!bvh.is_current(&m));
690        let report = bvh.update(&mut m, p.changes()).unwrap();
691        assert_eq!(report.strategy, Strategy::Unchanged);
692        assert!(bvh.is_current(&m));
693    }
694
695    #[test]
696    fn undo_and_redo_change_sets_keep_the_tree_correct() {
697        let mut m = grid(12);
698        let mut bvh = Bvh::build(&m, &opts()).unwrap();
699        let mut h = History::new(1 << 20);
700        let mut tx = m.begin_edit();
701        tx.remove_triangles(&[10, 11, 12]).unwrap();
702        tx.set_positions(&[40], &[[3.0, 3.0, 3.0]]).unwrap();
703        let p = tx.commit().unwrap();
704        bvh.update(&mut m, p.changes()).unwrap();
705        h.push(&mut m, p).unwrap();
706        bvh.validate(&m).unwrap();
707
708        let undo = h.undo(&mut m).unwrap().unwrap();
709        bvh.update(&mut m, &undo).unwrap();
710        bvh.validate(&m).unwrap();
711        let redo = h.redo(&mut m).unwrap().unwrap();
712        bvh.update(&mut m, &redo).unwrap();
713        bvh.validate(&m).unwrap();
714    }
715
716    #[test]
717    fn emptying_and_refilling_the_mesh() {
718        let mut m = grid(2);
719        let mut bvh = Bvh::build(&m, &opts()).unwrap();
720        let all: Vec<u32> = (0..8).collect();
721        let mut tx = m.begin_edit();
722        tx.remove_triangles(&all).unwrap();
723        let p = tx.commit().unwrap();
724        bvh.update(&mut m, p.changes()).unwrap();
725        bvh.validate(&m).unwrap();
726        assert_eq!(bvh.root_bounds(), None);
727        let undo = p.revert(&mut m).unwrap();
728        bvh.update(&mut m, &undo).unwrap();
729        bvh.validate(&m).unwrap();
730        assert_eq!(
731            bvh.root_bounds(),
732            Some(Aabb::new([0.0; 3], [2.0, 2.0, 0.0]))
733        );
734    }
735
736    #[test]
737    fn another_mesh_is_rejected() {
738        let m = grid(2);
739        let mut other = grid(2);
740        let mut bvh = Bvh::build(&m, &opts()).unwrap();
741        let mut tx = other.begin_edit();
742        tx.set_positions(&[0], &[[1.0; 3]]).unwrap();
743        let p = tx.commit().unwrap();
744        assert_eq!(
745            bvh.update(&mut other, p.changes()),
746            Err(BvhError::MeshMismatch)
747        );
748        assert_eq!(bvh.refit(&other, &[0]), Err(BvhError::MeshMismatch));
749        assert_eq!(bvh.rebuild(&other), Err(BvhError::MeshMismatch));
750        bvh.validate(&m).unwrap();
751    }
752
753    #[test]
754    fn degradation_triggers_a_treelet_rebuild() {
755        let mut m = grid(16);
756        let mut bvh = Bvh::build(&m, &opts()).unwrap();
757        bvh.set_policy(UpdatePolicy {
758            degradation_factor: 1.0,
759            full_rebuild_fraction: 1.0,
760        })
761        .unwrap();
762        // Scatter a few vertices far away: refits blow up the boxes they touch.
763        let ids = [0u32, 50, 100, 150];
764        let far: Vec<[f32; 3]> = ids
765            .iter()
766            .map(|&v| [v as f32, 0.0, 100.0 + v as f32])
767            .collect();
768        let mut tx = m.begin_edit();
769        tx.set_positions(&ids, &far).unwrap();
770        let p = tx.commit().unwrap();
771        let report = bvh.update(&mut m, p.changes()).unwrap();
772        assert!(report.rebuilt_treelets > 0, "{report:?}");
773        bvh.validate(&m).unwrap();
774    }
775
776    #[test]
777    fn generation_changes_on_every_structural_change() {
778        let mut m = grid(16);
779        let mut bvh = Bvh::build(&m, &opts()).unwrap();
780        let mut seen = vec![bvh.generation()];
781        let mut check = |bvh: &Bvh, what: &str| {
782            let g = bvh.generation();
783            assert!(!seen.contains(&g), "{what}: generation {g} repeated");
784            seen.push(g);
785        };
786
787        let mut tx = m.begin_edit();
788        tx.set_positions(&[0], &[[0.0, 0.0, 5.0]]).unwrap();
789        let p = tx.commit().unwrap();
790        assert_eq!(
791            bvh.update(&mut m, p.changes()).unwrap().strategy,
792            Strategy::Refit
793        );
794        check(&bvh, "update refit");
795
796        let mut tx = m.begin_edit();
797        tx.remove_triangles(&[0, 1]).unwrap();
798        let p = tx.commit().unwrap();
799        assert_eq!(
800            bvh.update(&mut m, p.changes()).unwrap().strategy,
801            Strategy::Partial
802        );
803        check(&bvh, "update partial");
804
805        bvh.refit(&m, &[2]).unwrap();
806        check(&bvh, "refit");
807        bvh.rebuild_region(&m, &[2]).unwrap();
808        check(&bvh, "rebuild_region");
809        bvh.compact_storage().unwrap();
810        check(&bvh, "compact_storage");
811        bvh.rebuild(&m).unwrap();
812        check(&bvh, "rebuild");
813
814        // A full rebuild inside `update` replaces the whole value but must
815        // still move the counter on.
816        let all: Vec<u32> = (0..m.vertex_slots() as u32).collect();
817        let lifted: Vec<[f32; 3]> = all
818            .iter()
819            .map(|&v| {
820                let p = m.position(v).unwrap();
821                [p[0], p[1], 1.0]
822            })
823            .collect();
824        let mut tx = m.begin_edit();
825        tx.set_positions(&all, &lifted).unwrap();
826        let p = tx.commit().unwrap();
827        assert_eq!(
828            bvh.update(&mut m, p.changes()).unwrap().strategy,
829            Strategy::Full
830        );
831        check(&bvh, "update full");
832
833        // So does a rebuild forced by a compacted mesh.
834        m.compact().unwrap();
835        bvh.update(&mut m, &fast_mesh_store::ChangeSet::default())
836            .unwrap();
837        check(&bvh, "update after mesh compaction");
838    }
839
840    #[test]
841    fn generation_unchanged_when_update_is_unchanged() {
842        let mut m = grid(2);
843        let mut bvh = Bvh::build(&m, &opts()).unwrap();
844        let g = bvh.generation();
845        // Revision matches: nothing happens.
846        let none = fast_mesh_store::ChangeSet::default();
847        assert_eq!(
848            bvh.update(&mut m, &none).unwrap().strategy,
849            Strategy::Unchanged
850        );
851        assert_eq!(bvh.generation(), g);
852        // Revision moved but no triangle is affected.
853        let mut tx = m.begin_edit();
854        tx.add_vertices(&[[9.0; 3]]).unwrap();
855        let p = tx.commit().unwrap();
856        assert_eq!(
857            bvh.update(&mut m, p.changes()).unwrap().strategy,
858            Strategy::Unchanged
859        );
860        assert_eq!(bvh.generation(), g);
861    }
862
863    #[test]
864    fn manual_refit_and_region_rebuild() {
865        let mut m = grid(8);
866        let mut bvh = Bvh::build(&m, &opts()).unwrap();
867        let mut tx = m.begin_edit();
868        tx.set_positions(&[0], &[[0.0, 0.0, 2.0]]).unwrap();
869        tx.commit().unwrap();
870        bvh.refit(&m, &[0, 1]).unwrap();
871        bvh.validate(&m).unwrap();
872        bvh.rebuild_region(&m, &[0]).unwrap();
873        bvh.validate(&m).unwrap();
874        bvh.rebuild(&m).unwrap();
875        bvh.validate(&m).unwrap();
876        assert!(bvh.is_current(&m));
877    }
878
879    /// Every reachable node's [`Bvh::subtree_triangles`] against a count
880    /// of its leaves' triangles.
881    fn assert_counts(bvh: &Bvh, tag: &str) {
882        fn count(bvh: &Bvh, id: crate::NodeId) -> usize {
883            match bvh.node(id).kind {
884                crate::NodeKind::Leaf { triangles } => triangles.len(),
885                crate::NodeKind::Inner { left, right } => count(bvh, left) + count(bvh, right),
886            }
887        }
888        let mut nodes = 0;
889        bvh.traverse(|id, _| {
890            nodes += 1;
891            assert_eq!(
892                bvh.subtree_triangles(id),
893                count(bvh, id),
894                "{tag}: node {id:?}"
895            );
896            true
897        });
898        assert!(nodes > 0 || bvh.root_bounds().is_none(), "{tag}");
899    }
900
901    #[test]
902    fn subtree_triangle_counts_follow_updates() {
903        let mut m = grid(16);
904        let mut bvh = Bvh::build(&m, &opts()).unwrap();
905        assert_counts(&bvh, "build");
906        let mut compacted = false;
907        for round in 0..40u32 {
908            // Removals and additions (treelet rebuilds, some split, garbage
909            // compacted now and then), and moves (refits).
910            let t = (round * 37) % m.triangle_slots() as u32;
911            let old = m.triangle(t);
912            let mut tx = m.begin_edit();
913            if let Some(tri) = old {
914                tx.remove_triangles(&[t]).unwrap();
915                if round % 3 != 0 {
916                    tx.add_triangles(&[tri, tri]).unwrap();
917                }
918            }
919            tx.set_positions(&[round], &[[round as f32, 0.5, 0.3]])
920                .unwrap();
921            let p = tx.commit().unwrap();
922            compacted |= bvh.update(&mut m, p.changes()).unwrap().compacted;
923            assert_counts(&bvh, &format!("round {round}"));
924        }
925        assert!(compacted);
926        // Many triangles in one place: the rebuilt treelet is split.
927        let treelets = bvh.treelet_count();
928        let tri = m.triangle(m.triangle_slots() as u32 - 1).unwrap();
929        let mut tx = m.begin_edit();
930        tx.add_triangles(&[tri; 100]).unwrap();
931        let p = tx.commit().unwrap();
932        bvh.update(&mut m, p.changes()).unwrap();
933        assert!(bvh.treelet_count() > treelets + 1);
934        assert_counts(&bvh, "split");
935        bvh.rebuild_region(&m, &[5, 9, 200]).unwrap();
936        assert_counts(&bvh, "region");
937        bvh.refit(&m, &[1, 2]).unwrap();
938        assert_counts(&bvh, "refit");
939        bvh.rebuild(&m).unwrap();
940        assert_counts(&bvh, "rebuild");
941    }
942
943    #[test]
944    fn garbage_is_compacted_away() {
945        let mut m = grid(16);
946        let mut bvh = Bvh::build(&m, &opts()).unwrap();
947        let mut compacted = false;
948        for round in 0..40u32 {
949            let t = (round * 37) % m.triangle_slots() as u32;
950            let Some(tri) = m.triangle(t) else { continue };
951            let mut tx = m.begin_edit();
952            tx.remove_triangles(&[t]).unwrap();
953            tx.add_triangles(&[tri]).unwrap();
954            let p = tx.commit().unwrap();
955            compacted |= bvh.update(&mut m, p.changes()).unwrap().compacted;
956            bvh.validate(&m).unwrap();
957        }
958        assert!(compacted);
959    }
960
961    #[test]
962    fn replaying_a_change_set_does_not_duplicate_triangles() {
963        let mut m = grid(16);
964        let mut bvh = Bvh::build(&m, &opts()).unwrap();
965        let mut tx = m.begin_edit();
966        let v = tx.add_vertices(&[[0.5, 0.5, 2.0]]).unwrap()[0];
967        tx.add_triangles(&[[0, 1, v]]).unwrap();
968        let p = tx.commit().unwrap();
969        assert_eq!(
970            bvh.update(&mut m, p.changes()).unwrap().strategy,
971            Strategy::Partial
972        );
973        assert_eq!(
974            bvh.update(&mut m, p.changes()).unwrap().strategy,
975            Strategy::Unchanged
976        );
977        bvh.validate(&m).unwrap();
978        assert_eq!(bvh.revision(), m.revision());
979    }
980
981    #[test]
982    fn re_listed_additions_are_inserted_once() {
983        let mut m = grid(16);
984        let mut bvh = Bvh::build(&m, &opts()).unwrap();
985        let mut tx = m.begin_edit();
986        let v = tx.add_vertices(&[[0.5, 0.5, 2.0]]).unwrap()[0];
987        tx.add_triangles(&[[0, 1, v]]).unwrap();
988        let first = tx.commit().unwrap().changes().clone();
989        bvh.update(&mut m, &first).unwrap();
990        let mut tx = m.begin_edit();
991        tx.add_triangles(&[[20, 21, v]]).unwrap();
992        let second = tx.commit().unwrap().changes().clone();
993        // A merged set that re-lists the earlier addition.
994        let mut merged = second.clone();
995        merged
996            .triangles_added
997            .extend_from_slice(&first.triangles_added);
998        merged.triangles_added.sort_unstable();
999        merged.triangles_added.dedup();
1000        let report = bvh.update(&mut m, &merged).unwrap();
1001        assert_eq!(report.strategy, Strategy::Partial);
1002        bvh.validate(&m).unwrap();
1003        let all = hits(&bvh, &m, [-1.0; 3], [20.0, 20.0, 5.0]);
1004        assert_eq!(all.len(), m.triangle_count());
1005    }
1006
1007    #[test]
1008    fn compaction_of_the_mesh_forces_a_full_rebuild() {
1009        let mut m = grid(8);
1010        let mut bvh = Bvh::build(&m, &opts()).unwrap();
1011        let mut tx = m.begin_edit();
1012        tx.remove_triangles(&[0, 1, 2, 30]).unwrap();
1013        let p = tx.commit().unwrap();
1014        bvh.update(&mut m, p.changes()).unwrap();
1015        m.compact().unwrap();
1016        assert!(!bvh.is_current(&m));
1017        let report = bvh
1018            .update(&mut m, &fast_mesh_store::ChangeSet::default())
1019            .unwrap();
1020        assert_eq!(report.strategy, Strategy::Full);
1021        assert!(bvh.is_current(&m));
1022        bvh.validate(&m).unwrap();
1023        assert_eq!(
1024            hits(&bvh, &m, [-1.0; 3], [9.0, 9.0, 1.0]).len(),
1025            m.triangle_count()
1026        );
1027
1028        // Manual refit and region rebuilds also notice the renumbering.
1029        let mut tx = m.begin_edit();
1030        tx.remove_triangles(&[5, 6]).unwrap();
1031        tx.commit().unwrap();
1032        let mut a = Bvh::build(&m, &opts()).unwrap();
1033        let mut b = Bvh::build(&m, &opts()).unwrap();
1034        m.compact().unwrap();
1035        a.refit(&m, &[0]).unwrap();
1036        a.validate(&m).unwrap();
1037        assert!(a.is_current(&m));
1038        b.rebuild_region(&m, &[0]).unwrap();
1039        b.validate(&m).unwrap();
1040        assert!(b.is_current(&m));
1041    }
1042
1043    #[test]
1044    fn region_rebuild_on_a_stale_tree_then_update() {
1045        let mut m = grid(8);
1046        let mut bvh = Bvh::build(&m, &opts()).unwrap();
1047        let mut tx = m.begin_edit();
1048        tx.remove_triangles(&[5]).unwrap();
1049        let p = tx.commit().unwrap();
1050        bvh.rebuild_region(&m, &[4, 5]).unwrap();
1051        bvh.validate(&m).unwrap();
1052        bvh.update(&mut m, p.changes()).unwrap();
1053        bvh.validate(&m).unwrap();
1054        assert!(bvh.is_current(&m));
1055    }
1056
1057    #[test]
1058    fn treelets_stay_bounded_as_the_mesh_grows() {
1059        let mut m = grid(8);
1060        let options = BuildOptions {
1061            max_leaf_size: 4,
1062            treelet_size: 64,
1063            ..Default::default()
1064        };
1065        let mut bvh = Bvh::build(&m, &options).unwrap();
1066        for round in 0..14u32 {
1067            // Add about 20% more triangles, spread over the mesh.
1068            let alive: Vec<u32> = (0..m.triangle_slots() as u32)
1069                .filter(|&t| m.is_triangle_alive(t))
1070                .collect();
1071            let extra: Vec<[u32; 3]> = alive
1072                .iter()
1073                .step_by(5)
1074                .map(|&t| m.triangle(t).unwrap())
1075                .collect();
1076            let mut tx = m.begin_edit();
1077            tx.add_triangles(&extra).unwrap();
1078            let p = tx.commit().unwrap();
1079            let report = bvh.update(&mut m, p.changes()).unwrap();
1080            assert_eq!(report.strategy, Strategy::Partial, "round {round}");
1081            bvh.validate(&m).unwrap();
1082            let largest = bvh.treelet_triangle_counts().into_iter().max().unwrap();
1083            assert!(largest <= 128, "round {round}: treelet of {largest}");
1084        }
1085        // A one-triangle edit now rebuilds a bounded amount.
1086        let mut tx = m.begin_edit();
1087        tx.remove_triangles(&[7]).unwrap();
1088        let p = tx.commit().unwrap();
1089        assert_eq!(bvh.update(&mut m, p.changes()).unwrap().rebuilt_treelets, 1);
1090        bvh.validate(&m).unwrap();
1091    }
1092
1093    /// Splitting removes the old root and inserts one entry per new treelet.
1094    /// The removal can leave a tombstone that does not give its room back, so
1095    /// the map needs spare room for every inserted entry, not just the net gain.
1096    #[test]
1097    fn treelet_split_needs_room_for_every_new_root() {
1098        let m = grid(8);
1099        // One treelet over the whole mesh: its nodes are the whole arena.
1100        let options = BuildOptions {
1101            max_leaf_size: 4,
1102            treelet_size: 1024,
1103            ..Default::default()
1104        };
1105        let base = Bvh::build(&m, &options).unwrap();
1106        assert_eq!(base.treelet_count(), 1);
1107        // Number of treelets a split at size 32 produces.
1108        let found = {
1109            let mut probe = Bvh::build(&m, &options).unwrap();
1110            probe.options.treelet_size = 32;
1111            probe.treelets.reserve(64);
1112            probe.treelet_of_root.reserve(64);
1113            probe.split_treelet(0);
1114            probe.validate(&m).unwrap();
1115            probe.treelet_count()
1116        };
1117        assert!(found > 1);
1118        for spare in [found - 1, found] {
1119            let mut bvh = Bvh::build(&m, &options).unwrap();
1120            bvh.options.treelet_size = 32;
1121            bvh.treelets.reserve(64);
1122            // Fill a fresh map with placeholder keys until exactly `spare`
1123            // entries fit without growing.
1124            let mut map = std::collections::HashMap::with_capacity(found + 16);
1125            map.insert(bvh.root, 0usize);
1126            let mut filler = u32::MAX;
1127            while map.capacity() - map.len() > spare {
1128                map.insert(filler, usize::MAX);
1129                filler -= 1;
1130            }
1131            assert_eq!(map.capacity() - map.len(), spare);
1132            let capacity = map.capacity();
1133            bvh.treelet_of_root = map;
1134            bvh.split_treelet(0);
1135            // Removing the old root may free its slot, raising `capacity()` by
1136            // one without reallocating; a reallocation would at least double it.
1137            assert!(
1138                bvh.treelet_of_root.capacity() <= capacity + 1,
1139                "spare {spare}: the map was reallocated"
1140            );
1141            let expected = if spare == found { found } else { 1 };
1142            assert_eq!(bvh.treelet_count(), expected, "spare {spare}");
1143            bvh.treelet_of_root.retain(|_, &mut i| i != usize::MAX);
1144            bvh.validate(&m).unwrap();
1145        }
1146    }
1147}
1148
1149#[cfg(test)]
1150mod oom_tests {
1151    use fast_mesh_store::Mesh;
1152
1153    use crate::error::fail_allocations_after;
1154    use crate::{BuildOptions, Bvh, BvhError};
1155
1156    fn wavy(n: u32) -> Mesh {
1157        let mut positions = Vec::new();
1158        for y in 0..=n {
1159            for x in 0..=n {
1160                positions.extend([x as f32, y as f32, ((x + y) % 3) as f32]);
1161            }
1162        }
1163        let mut indices = Vec::new();
1164        for y in 0..n {
1165            for x in 0..n {
1166                let a = y * (n + 1) + x;
1167                indices.extend([a, a + 1, a + n + 2, a, a + n + 2, a + n + 1]);
1168            }
1169        }
1170        Mesh::from_buffers(&positions, &indices).unwrap()
1171    }
1172
1173    /// Fails the k-th allocation of an update for every k until the update
1174    /// succeeds; each failure must leave the tree exactly as it was.
1175    fn assert_atomic(edit: impl Fn(&mut Mesh) -> fast_mesh_store::ChangeSet) {
1176        let opts = BuildOptions {
1177            max_leaf_size: 2,
1178            treelet_size: 8,
1179            ..Default::default()
1180        };
1181        for k in 0.. {
1182            let mut m = wavy(8);
1183            let mut bvh = Bvh::build(&m, &opts).unwrap();
1184            m.ensure_incidence().unwrap();
1185            let changes = edit(&mut m);
1186            let before = bvh.fingerprint();
1187            fail_allocations_after(Some(k));
1188            let result = bvh.update(&mut m, &changes);
1189            fail_allocations_after(None);
1190            match result {
1191                Err(BvhError::OutOfMemory) => {
1192                    assert_eq!(bvh.fingerprint(), before, "allocation {k}")
1193                }
1194                Err(e) => panic!("unexpected error {e}"),
1195                Ok(_) => {
1196                    bvh.validate(&m).unwrap();
1197                    assert!(k > 0, "the update must allocate at least once");
1198                    return;
1199                }
1200            }
1201        }
1202    }
1203
1204    #[test]
1205    fn failed_partial_updates_leave_the_tree_unchanged() {
1206        assert_atomic(|m| {
1207            let mut tx = m.begin_edit();
1208            tx.remove_triangles(&[3, 40]).unwrap();
1209            tx.add_triangles(&[[0, 10, 20]]).unwrap();
1210            tx.set_positions(&[30], &[[3.0, 3.0, 4.0]]).unwrap();
1211            tx.commit().unwrap().changes().clone()
1212        });
1213    }
1214
1215    #[test]
1216    fn failed_region_rebuilds_leave_the_tree_unchanged() {
1217        let opts = BuildOptions {
1218            max_leaf_size: 2,
1219            treelet_size: 8,
1220            ..Default::default()
1221        };
1222        // Triangles far apart, so the region spans several treelets.
1223        let region = [0u32, 40, 80, 120];
1224        for k in 0.. {
1225            let m = wavy(8);
1226            let mut bvh = Bvh::build(&m, &opts).unwrap();
1227            let mut which: Vec<_> = region
1228                .iter()
1229                .map(|&t| bvh.treelet_of_triangle(t).unwrap())
1230                .collect();
1231            which.dedup();
1232            assert!(which.len() > 1, "the region must span several treelets");
1233            let before = bvh.fingerprint();
1234            fail_allocations_after(Some(k));
1235            let result = bvh.rebuild_region(&m, &region);
1236            fail_allocations_after(None);
1237            match result {
1238                Err(BvhError::OutOfMemory) => {
1239                    assert_eq!(bvh.fingerprint(), before, "allocation {k}")
1240                }
1241                Err(e) => panic!("unexpected error {e}"),
1242                Ok(()) => {
1243                    assert!(k > 1, "the rebuild must allocate several times");
1244                    assert_ne!(bvh.fingerprint(), before);
1245                    bvh.validate(&m).unwrap();
1246                    return;
1247                }
1248            }
1249        }
1250    }
1251
1252    #[test]
1253    fn failed_full_rebuilds_leave_the_tree_unchanged() {
1254        assert_atomic(|m| {
1255            let all: Vec<u32> = (0..m.triangle_slots() as u32).step_by(2).collect();
1256            let mut tx = m.begin_edit();
1257            tx.remove_triangles(&all).unwrap();
1258            tx.commit().unwrap().changes().clone()
1259        });
1260    }
1261}