Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion src/partitioning/bvh/bvh_tests.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use crate::bounding_volume::Aabb;
use crate::math::{Real, Vector};
use crate::partitioning::{Bvh, BvhBuildStrategy, BvhNode, BvhNodeIndex, TraversalAction};
use crate::partitioning::{
Bvh, BvhBuildStrategy, BvhNode, BvhNodeIndex, BvhWorkspace, TraversalAction,
};

fn make_test_aabb(i: usize) -> Aabb {
Aabb::from_half_extents(Vector::splat(i as Real).into(), Vector::splat(1.0))
Expand Down Expand Up @@ -242,3 +244,38 @@ fn bvh_build_and_removal() {
}
}
}

#[test]
fn bvh_remove_to_partial_root_then_optimize() {
// Regression test for the bug reported in #409 where `Bvh::remove` would leave orphaned
// wide nodes in `self.nodes`/`self.parents` after reducing the tree to a
// partial root (a single surviving leaf at node 0). Earlier removals on a
// tree with more than one wide node would intentionally leave orphans for
// the next `refit` to compact, but if a partial root was created before
// any refit, those orphans remained reachable from `self.nodes` and
// `optimize_incremental` would walk them as if they were live, crashing
// on the corrupt structure.
//
// We pick enough leaves to ensure at least one orphan-leaving removal
// (`wide_node_index != 0`) before the final partial-root removal.
let leaves: std::vec::Vec<_> = (0..10).map(make_test_aabb).collect();
let mut bvh = Bvh::from_leaves(BvhBuildStrategy::Binned, &leaves);

// Remove all but the last leaf, without ever calling refit in between.
for i in 0..(leaves.len() as u32 - 1) {
bvh.remove(i);
}

// After the final remove, the tree should be a partial root with exactly
// one surviving leaf, and no orphaned wide nodes left over.
assert_eq!(bvh.leaf_count(), 1);

// Without the fix this call walks the orphan-laden tree as if it were
// live and ends up corrupting/crashing on the partial root.
let mut workspace = BvhWorkspace::default();
bvh.optimize_incremental(&mut workspace);

assert_eq!(bvh.nodes.len(), 1);
assert_eq!(bvh.parents.len(), 1);
bvh.assert_well_formed();
}
8 changes: 8 additions & 0 deletions src/partitioning/bvh/bvh_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2383,6 +2383,14 @@ impl Bvh {

// Now we can just clear the right leaf.
self.nodes[0].right = BvhNode::zeros();

// Clean up orphaned nodes. With a partial root, only node[0] is
// reachable. Previous removes may have left orphaned wide nodes
// that were waiting for refit to compact them. If we don't truncate
// here, the tree appears as a single-leaf tree with unreachable
// nodes, which corrupts optimize_incremental.
self.nodes.truncate(1);
self.parents.truncate(1);
} else {
// The sibling isn’t a leaf. It becomes the new root at index 0.
self.nodes[0] = self.nodes[self.nodes[sibling].children as usize];
Expand Down
Loading