diff --git a/Justfile b/Justfile index 43b44fc2..e3f9694c 100644 --- a/Justfile +++ b/Justfile @@ -26,3 +26,11 @@ all-scenes: profile *ARGS: cargo build --profile profiling; \ samply record -- ./target/profiling/clovers-cli {{ARGS}} + +# Run all tests +test: + cargo nextest run --cargo-quiet + +# Run all benchmarks +bench: + cargo bench --quiet diff --git a/clovers/Cargo.toml b/clovers/Cargo.toml index 08caecbe..0097ede0 100644 --- a/clovers/Cargo.toml +++ b/clovers/Cargo.toml @@ -31,8 +31,25 @@ stl_io = { version = "0.7.0", optional = true } tracing = { version = "0.1.40", optional = true } [dev-dependencies] -criterion = "0.4.0" +divan = "0.1.11" +proptest = "1" [[bench]] name = "random" harness = false + +[[bench]] +name = "interval" +harness = false + +[[bench]] +name = "aabb" +harness = false + +[[bench]] +name = "wavelength" +harness = false + +[[bench]] +name = "spectrum" +harness = false diff --git a/clovers/benches/aabb.rs b/clovers/benches/aabb.rs new file mode 100644 index 00000000..891f35be --- /dev/null +++ b/clovers/benches/aabb.rs @@ -0,0 +1,88 @@ +use std::f32::{INFINITY, NEG_INFINITY}; + +use clovers::interval::Interval; +use clovers::ray::Ray; +use clovers::wavelength::random_wavelength; +use clovers::{aabb::*, Vec3}; +use divan::black_box; +use rand::rngs::SmallRng; +use rand::{Rng, SeedableRng}; + +fn main() { + divan::main(); +} + +#[divan::bench] +fn new(bencher: divan::Bencher) { + bencher + .with_inputs(|| { + let mut rng = SmallRng::from_entropy(); + random_intervals(&mut rng) + }) + .bench_values(|(ab, cd, ef)| black_box(AABB::new(ab, cd, ef))) +} + +#[divan::bench] +fn hit(bencher: divan::Bencher) { + bencher + .with_inputs(random_aabb_and_ray) + .bench_values(|(aabb, ray)| black_box(aabb.hit(&ray, NEG_INFINITY, INFINITY))) +} + +#[divan::bench] +fn hit_old(bencher: divan::Bencher) { + bencher + .with_inputs(random_aabb_and_ray) + .bench_values(|(aabb, ray)| { + #[allow(deprecated)] + black_box(aabb.hit_old(&ray, NEG_INFINITY, INFINITY)) + }) +} + +#[divan::bench] +fn hit_new(bencher: divan::Bencher) { + bencher + .with_inputs(random_aabb_and_ray) + .bench_values(|(aabb, ray)| { + #[allow(deprecated)] + black_box(aabb.hit_new(&ray, NEG_INFINITY, INFINITY)) + }) +} + +// Helper functions + +fn random_intervals(rng: &mut SmallRng) -> (Interval, Interval, Interval) { + let (a, b, c, d, e, f) = black_box(( + rng.gen(), + rng.gen(), + rng.gen(), + rng.gen(), + rng.gen(), + rng.gen(), + )); + let ab = Interval::new(a, b); + let cd = Interval::new(c, d); + let ef = Interval::new(e, f); + (ab, cd, ef) +} + +fn random_aabb(rng: &mut SmallRng) -> AABB { + let (ab, cd, ef) = random_intervals(rng); + black_box(AABB::new(ab, cd, ef)) +} + +fn random_ray(rng: &mut SmallRng) -> Ray { + black_box(Ray { + origin: Vec3::new(0.0, 0.0, 0.0), + direction: Vec3::new(rng.gen(), rng.gen(), rng.gen()), + time: rng.gen(), + wavelength: random_wavelength(rng), + }) +} + +fn random_aabb_and_ray() -> (AABB, Ray) { + let mut rng = SmallRng::from_entropy(); + let aabb = black_box(random_aabb(&mut rng)); + let ray = black_box(random_ray(&mut rng)); + (aabb, ray) +} diff --git a/clovers/benches/interval.rs b/clovers/benches/interval.rs new file mode 100644 index 00000000..50c29369 --- /dev/null +++ b/clovers/benches/interval.rs @@ -0,0 +1,59 @@ +use clovers::interval::*; +use divan::black_box; +use rand::rngs::SmallRng; +use rand::{Rng, SeedableRng}; + +fn main() { + divan::main(); +} + +#[divan::bench] +fn new(bencher: divan::Bencher) { + bencher + .with_inputs(|| { + let mut rng = SmallRng::from_entropy(); + (rng.gen(), rng.gen()) + }) + .bench_values(|(a, b)| black_box(Interval::new(a, b))) +} + +#[divan::bench] +fn new_from_intervals(bencher: divan::Bencher) { + bencher + .with_inputs(|| { + let mut rng = SmallRng::from_entropy(); + let ab = random_interval(&mut rng); + let cd = random_interval(&mut rng); + (ab, cd) + }) + .bench_values(|(ab, cd)| black_box(Interval::new_from_intervals(ab, cd))) +} + +#[divan::bench] +fn expand(bencher: divan::Bencher) { + bencher + .with_inputs(|| { + let mut rng = SmallRng::from_entropy(); + let ab = random_interval(&mut rng); + let delta = rng.gen(); + (ab, delta) + }) + .bench_values(|(ab, delta)| black_box(ab.expand(delta))) +} + +#[divan::bench] +fn size(bencher: divan::Bencher) { + bencher + .with_inputs(|| { + let mut rng = SmallRng::from_entropy(); + random_interval(&mut rng) + }) + .bench_values(|ab| black_box(ab.size())) +} + +// Helper functions + +fn random_interval(rng: &mut SmallRng) -> Interval { + let (a, b) = (rng.gen(), rng.gen()); + Interval::new(a, b) +} diff --git a/clovers/benches/random.rs b/clovers/benches/random.rs index d4afc018..b388ee82 100644 --- a/clovers/benches/random.rs +++ b/clovers/benches/random.rs @@ -1,33 +1,39 @@ use clovers::{random::*, Vec3}; -use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use divan::black_box; use rand::rngs::SmallRng; -#[allow(unused_imports)] use rand::SeedableRng; -pub fn criterion_benchmark(c: &mut Criterion) { - let mut rng = SmallRng::from_entropy(); - - c.bench_function("random in unit sphere", |b| { - b.iter(|| random_unit_vector(black_box(&mut rng))) - }); - - c.bench_function("random in unit disk", |b| { - b.iter(|| random_in_unit_disk(black_box(&mut rng))) - }); +fn main() { + divan::main(); +} - c.bench_function("random unit vector", |b| { - b.iter(|| random_unit_vector(black_box(&mut rng))) - }); +#[divan::bench] +fn unit_vector(bencher: divan::Bencher) { + bencher + .with_inputs(SmallRng::from_entropy) + .bench_values(|mut rng| random_unit_vector(black_box(&mut rng))) +} - c.bench_function("random cosine direction", |b| { - b.iter(|| random_cosine_direction(black_box(&mut rng))) - }); +#[divan::bench] +fn unit_disk(bencher: divan::Bencher) { + bencher + .with_inputs(SmallRng::from_entropy) + .bench_values(|mut rng| random_in_unit_disk(black_box(&mut rng))) +} - let normal = Vec3::new(1.0, 0.0, 0.0); - c.bench_function("random in hemisphere", |b| { - b.iter(|| random_on_hemisphere(normal, black_box(&mut rng))) - }); +#[divan::bench] +fn cosine_direction(bencher: divan::Bencher) { + bencher + .with_inputs(SmallRng::from_entropy) + .bench_values(|mut rng| random_cosine_direction(black_box(&mut rng))) } -criterion_group!(benches, criterion_benchmark); -criterion_main!(benches); +#[divan::bench] +fn hemisphere(bencher: divan::Bencher) { + bencher + .with_inputs(SmallRng::from_entropy) + .bench_values(|mut rng| { + let normal = Vec3::new(1.0, 0.0, 0.0); + random_on_hemisphere(normal, black_box(&mut rng)) + }) +} diff --git a/clovers/benches/spectrum.rs b/clovers/benches/spectrum.rs new file mode 100644 index 00000000..65e148d8 --- /dev/null +++ b/clovers/benches/spectrum.rs @@ -0,0 +1,23 @@ +use clovers::spectrum::*; +use clovers::wavelength::*; +use divan::black_box; +use palette::white_point::E; +use palette::Xyz; +use rand::rngs::SmallRng; +use rand::SeedableRng; + +fn main() { + divan::main(); +} + +#[divan::bench] +fn xyz_to_p(bencher: divan::Bencher) { + bencher + .with_inputs(|| { + let mut rng = SmallRng::from_entropy(); + let wave = random_wavelength(&mut rng); + let xyz: Xyz = Xyz::new(1.0, 1.0, 1.0); + (wave, xyz) + }) + .bench_values(|(wave, xyz)| black_box(spectrum_xyz_to_p(wave, xyz))) +} diff --git a/clovers/benches/wavelength.rs b/clovers/benches/wavelength.rs new file mode 100644 index 00000000..f3fc6cc2 --- /dev/null +++ b/clovers/benches/wavelength.rs @@ -0,0 +1,35 @@ +use clovers::wavelength::*; +use divan::black_box; +use rand::rngs::SmallRng; +use rand::SeedableRng; + +fn main() { + divan::main(); +} + +#[divan::bench] +fn random(bencher: divan::Bencher) { + bencher + .with_inputs(SmallRng::from_entropy) + .bench_values(|mut rng| black_box(random_wavelength(&mut rng))) +} + +#[divan::bench] +fn rotate(bencher: divan::Bencher) { + bencher + .with_inputs(|| { + let mut rng = SmallRng::from_entropy(); + random_wavelength(&mut rng) + }) + .bench_values(|wave| black_box(rotate_wavelength(wave))) +} + +#[divan::bench] +fn into_xyz(bencher: divan::Bencher) { + bencher + .with_inputs(|| { + let mut rng = SmallRng::from_entropy(); + random_wavelength(&mut rng) + }) + .bench_values(|wave| black_box(wavelength_into_xyz(wave))) +} diff --git a/clovers/src/aabb.rs b/clovers/src/aabb.rs index 35434826..2635c7de 100644 --- a/clovers/src/aabb.rs +++ b/clovers/src/aabb.rs @@ -39,40 +39,12 @@ impl AABB { } } - /// Given a [Ray], returns whether the ray hits the bounding box or not. Based on ["An Optimized AABB Hit Method"](https://raytracing.github.io/books/RayTracingTheNextWeek.html) + /// Given a [Ray], returns whether the ray hits the bounding box or not. Current default method, based on ["An Optimized AABB Hit Method"](https://raytracing.github.io/books/RayTracingTheNextWeek.html) #[must_use] pub fn hit(&self, ray: &Ray, mut tmin: Float, mut tmax: Float) -> bool { // TODO: Create an improved hit method with more robust handling of zeroes. See https://github.com/RayTracing/raytracing.github.io/issues/927 // Both methods below are susceptible for NaNs and infinities, and have subtly different edge cases. - // "New method" - // for axis in 0..3 { - // let a = (self.axis(axis).min - ray.origin[axis]) / ray.direction[axis]; - // let b = (self.axis(axis).max - ray.origin[axis]) / ray.direction[axis]; - // let t0: Float = a.min(b); - // let t1: Float = a.max(b); - // tmin = t0.max(tmin); - // tmax = t1.min(tmax); - // if tmax <= tmin { - // return false; - // } - // } - - // "Old method" - // for axis in 0..3 { - // let invd = 1.0 / ray.direction[axis]; - // let mut t0: Float = (self.axis(axis).min - ray.origin[axis]) * invd; - // let mut t1: Float = (self.axis(axis).max - ray.origin[axis]) * invd; - // if invd < 0.0 { - // core::mem::swap(&mut t0, &mut t1); - // } - // tmin = if t0 > tmin { t0 } else { tmin }; - // tmax = if t1 < tmax { t1 } else { tmax }; - // if tmax <= tmin { - // return false; - // } - // } - // "My adjusted method" - possibly more zero-resistant? // TODO: validate for axis in 0..3 { @@ -102,6 +74,46 @@ impl AABB { true } + /// Given a [Ray], returns whether the ray hits the bounding box or not. Old method from a GitHub issue. Exists mostly for testing purposes. + #[must_use] + #[deprecated] + pub fn hit_old(&self, ray: &Ray, mut tmin: Float, mut tmax: Float) -> bool { + // "Old method" + for axis in 0..3 { + let invd = 1.0 / ray.direction[axis]; + let mut t0: Float = (self.axis(axis).min - ray.origin[axis]) * invd; + let mut t1: Float = (self.axis(axis).max - ray.origin[axis]) * invd; + if invd < 0.0 { + core::mem::swap(&mut t0, &mut t1); + } + tmin = if t0 > tmin { t0 } else { tmin }; + tmax = if t1 < tmax { t1 } else { tmax }; + if tmax <= tmin { + return false; + } + } + true + } + + /// Given a [Ray], returns whether the ray hits the bounding box or not. Newer method from a GitHub issue. Exists mostly for testing purposes. + #[must_use] + #[deprecated] + pub fn hit_new(&self, ray: &Ray, mut tmin: Float, mut tmax: Float) -> bool { + // "New method" + for axis in 0..3 { + let a = (self.axis(axis).min - ray.origin[axis]) / ray.direction[axis]; + let b = (self.axis(axis).max - ray.origin[axis]) / ray.direction[axis]; + let t0: Float = a.min(b); + let t1: Float = a.max(b); + tmin = t0.max(tmin); + tmax = t1.min(tmax); + if tmax <= tmin { + return false; + } + } + true + } + /// Given two axis-aligned bounding boxes, return a new [AABB] that contains both. #[must_use] pub fn surrounding_box(box0: &AABB, box1: &AABB) -> AABB { diff --git a/clovers/src/wavelength.rs b/clovers/src/wavelength.rs index d79e8a8e..4c799347 100644 --- a/clovers/src/wavelength.rs +++ b/clovers/src/wavelength.rs @@ -60,16 +60,3 @@ pub fn wavelength_into_xyz(lambda: Wavelength) -> Xyz { // The functions above have been designed for the whitepoint E Xyz::::new(x, y, z) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn rotate_wavelength_spread() { - let hero = 380; - let rotations = rotate_wavelength(hero); - let expected = [380, 480, 580, 680]; - assert_eq!(rotations, expected); - } -} diff --git a/clovers/tests/spectrum.rs b/clovers/tests/spectrum.rs new file mode 100644 index 00000000..c91b64c1 --- /dev/null +++ b/clovers/tests/spectrum.rs @@ -0,0 +1,29 @@ +use clovers::{spectrum::*, wavelength::SPECTRUM}; +use palette::{white_point::E, Xyz}; +use proptest::prelude::*; + +proptest! { + #[test] + fn converts_all_wavelengths_black(lambda in SPECTRUM) { + let xyz: Xyz = Xyz::new(0.0, 0.0, 0.0); + let _ = spectrum_xyz_to_p(lambda, xyz); + } +} + +proptest! { + #[test] + fn converts_all_wavelengths_grey(lambda in SPECTRUM) { + let xyz: Xyz = Xyz::new(0.5, 0.5, 0.5); + let _ = spectrum_xyz_to_p(lambda, xyz); + } +} + +proptest! { + #[test] + fn converts_all_wavelengths_white(lambda in SPECTRUM) { + let xyz: Xyz = Xyz::new(1.0, 1.0, 1.0); + let _ = spectrum_xyz_to_p(lambda, xyz); + } +} + +// TODO: add more comprehensive tests for varying Xyz diff --git a/clovers/tests/wavelength.rs b/clovers/tests/wavelength.rs new file mode 100644 index 00000000..533f1639 --- /dev/null +++ b/clovers/tests/wavelength.rs @@ -0,0 +1,22 @@ +use clovers::wavelength::*; +use proptest::prelude::*; + +proptest! { + #[test] + fn converts_all_wavelengths(lambda in SPECTRUM) { + let _ = wavelength_into_xyz(lambda); + } +} + +proptest! { + #[test] + fn rotates_all_wavelengths(lambda in SPECTRUM) { + let mut waves = rotate_wavelength(lambda); + prop_assert_eq!(waves[0], lambda); + waves.sort(); + let [a, b, c, d] = waves; + let diff = a.abs_diff(b); + prop_assert_eq!(diff, b.abs_diff(c)); + prop_assert_eq!(diff, c.abs_diff(d)); + } +}