diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a37c06..486fe6e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,6 +67,28 @@ jobs: run: | RUST_BACKTRACE=1 cargo fuzz run fuzz -- -max_total_time=60 + fuzz-simple-only: + name: fuzz (simple cases only) + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@master + with: + toolchain: nightly + components: rust-src + + - name: Install cargo-fuzz + uses: baptiste0928/cargo-install@v3 + with: + crate: cargo-fuzz + locked: false + + - name: Fuzz simple cases for a limited time + run: | + RUST_BACKTRACE=1 MAX_SHAPES=3 cargo fuzz run fuzz -- -max_total_time=60 + build-and-test-no-simd: name: CI with ${{ matrix.rust }} on ${{ matrix.os }} [no SIMD] runs-on: ${{ matrix.os }} diff --git a/fuzz/fuzz_targets/fuzz.rs b/fuzz/fuzz_targets/fuzz.rs index 66aec6f..4b0a319 100644 --- a/fuzz/fuzz_targets/fuzz.rs +++ b/fuzz/fuzz_targets/fuzz.rs @@ -18,6 +18,7 @@ use std::cmp::Ordering; use std::collections::HashSet; use std::fmt::{self, Debug, Formatter}; use std::hash::{Hash, Hasher}; +use std::str::FromStr; use arbitrary::Arbitrary; use bvh::aabb::{Aabb, Bounded, IntersectsAabb}; @@ -334,6 +335,20 @@ impl Workload { assert!(!intersects); } + // If this environment variable is present, only test limited-size BVH's without mutations. + // + // The motivation is two-fold: + // 1. fuzzer can analyze a greater number of cases + // 2. problems detected with more complex BVH's might be able to be minimized + // to simple BVH's + if let Some(max_shapes) = + option_env!("MAX_SHAPES").map(|s| usize::from_str(s).expect("MAX_SHAPES")) + { + if !self.mutations.is_empty() || self.shapes.len() > max_shapes { + return; + } + } + let mut bvh = Bvh::build(&mut self.shapes); if self.shapes.len() @@ -390,23 +405,69 @@ impl Workload { // Due to sphere geometry, `Mode::Grid` doesn't imply traversals will agree. self.fuzz_traversal(&bvh, &flat_bvh, &self.ball.ball(), false); + // In addition to collecting the output into a `HashSet`, make sure each subsequent + // item is further (or equally close) with respect to the ray, thus testing the + // correctness of the iterator. + let mut last_distance = f32::NEG_INFINITY; let nearest_traverse_iterator = bvh .nearest_traverse_iterator(&ray, &self.shapes) + .inspect(|shape| { + let distance = ray.intersection_slice_for_aabb(&shape.aabb()) + .expect("nearest_traverse_iterator returned aabb for which no intersection slice exists") + .0; + // Until https://github.com/svenstaro/bvh/issues/140 is completely fixed, the `distance` + // and the relationship between `distance` and `last_distance` only necessarily holds if + // ray-AABB intersection is definitive, so don't panic if `!assert_ray_traversal_agreement`. + assert!( + !assert_ray_traversal_agreement || distance >= last_distance, + "nearest_traverse_iterator returned shapes out of order: {distance} {last_distance}" + ); + last_distance = distance; + }) .map(ByPtr) .collect::>(); + + // Same as the above, but in the opposite direction. + let mut last_distance = f32::INFINITY; let farthest_traverse_iterator = bvh .farthest_traverse_iterator(&ray, &self.shapes) + .inspect(|shape| { + let distance = ray.intersection_slice_for_aabb(&shape.aabb()).expect("farthest_traverse_iterator returned aabb for which no intersection slice exists").1; + assert!( + !assert_ray_traversal_agreement || distance <= last_distance, + "farthest_traverse_iterator returned shapes out of order: {distance} {last_distance}" + ); + last_distance = distance; + }) + .map(ByPtr) + .collect::>(); + + // We do not assert the order, because we didn't check if any children overlap, the + // condition under which order isn't guaranteed. TODO: check this. + let nearest_child_traverse_iterator = bvh + .nearest_child_traverse_iterator(&ray, &self.shapes) + .map(ByPtr) + .collect::>(); + + // Same as the above, but in the opposite direction. + let farthest_child_traverse_iterator = bvh + .farthest_child_traverse_iterator(&ray, &self.shapes) .map(ByPtr) .collect::>(); if assert_ray_traversal_agreement { assert_eq!(traverse_ray, nearest_traverse_iterator); + assert_eq!(traverse_ray, nearest_child_traverse_iterator); } else { // Fails, probably due to normal rounding errors. } // Since the algorithm is similar, these should agree regardless of mode. assert_eq!(nearest_traverse_iterator, farthest_traverse_iterator); + assert_eq!( + nearest_child_traverse_iterator, + farthest_child_traverse_iterator + ); if let Some(mutation) = self.mutations.pop() { match mutation { diff --git a/justfile b/justfile index 2b047a0..6fb1df5 100644 --- a/justfile +++ b/justfile @@ -36,3 +36,13 @@ bench_simd: # fuzz the library fuzz: cargo fuzz run fuzz + +# fuzz the library, but stick to BVH's with at most 3 shapes +# that do not undergo mutations +fuzz-max-three-shapes: + MAX_SHAPES=3 cargo fuzz run fuzz + +# fuzz the library, but stick to BVH's with at most 5 shapes +# that do not undergo mutations +fuzz-max-five-shapes: + MAX_SHAPES=5 cargo fuzz run fuzz