diff --git a/benches/benches/bevy_picking/ray_mesh_intersection.rs b/benches/benches/bevy_picking/ray_mesh_intersection.rs index e8b35078f7..ee81f1ac7f 100644 --- a/benches/benches/bevy_picking/ray_mesh_intersection.rs +++ b/benches/benches/bevy_picking/ray_mesh_intersection.rs @@ -1,33 +1,53 @@ use core::hint::black_box; +use std::time::Duration; +use benches::bench; use bevy_math::{Dir3, Mat4, Ray3d, Vec3}; -use bevy_picking::mesh_picking::ray_cast; -use criterion::{criterion_group, Criterion}; +use bevy_picking::mesh_picking::ray_cast::{self, Backfaces}; +use criterion::{criterion_group, AxisScale, BenchmarkId, Criterion, PlotConfiguration}; -fn ptoxznorm(p: u32, size: u32) -> (f32, f32) { - let ij = (p / (size), p % (size)); - (ij.0 as f32 / size as f32, ij.1 as f32 / size as f32) -} +criterion_group!(benches, bench); +/// A mesh that can be passed to [`ray_cast::ray_mesh_intersection()`]. struct SimpleMesh { positions: Vec<[f32; 3]>, normals: Vec<[f32; 3]>, indices: Vec, } -fn mesh_creation(vertices_per_side: u32) -> SimpleMesh { +/// Selects a point within a normal square. +/// +/// `p` is an index within `0..vertices_per_side.pow(2)`. The returned value is a coordinate where +/// both `x` and `z` are within `0..1`. +fn p_to_xz_norm(p: u32, vertices_per_side: u32) -> (f32, f32) { + let x = (p / vertices_per_side) as f32; + let z = (p % vertices_per_side) as f32; + + let vertices_per_side = vertices_per_side as f32; + + // Scale `x` and `z` to be between 0 and 1. + (x / vertices_per_side, z / vertices_per_side) +} + +fn create_mesh(vertices_per_side: u32) -> SimpleMesh { let mut positions = Vec::new(); let mut normals = Vec::new(); - for p in 0..vertices_per_side.pow(2) { - let xz = ptoxznorm(p, vertices_per_side); - positions.push([xz.0 - 0.5, 0.0, xz.1 - 0.5]); - normals.push([0.0, 1.0, 0.0]); - } + let mut indices = Vec::new(); - let mut indices = vec![]; for p in 0..vertices_per_side.pow(2) { - if p % (vertices_per_side) != vertices_per_side - 1 - && p / (vertices_per_side) != vertices_per_side - 1 + let (x, z) = p_to_xz_norm(p, vertices_per_side); + + // Push a new vertice to the mesh. We translate all vertices so the final square is + // centered at (0, 0), instead of (0.5, 0.5). + positions.push([x - 0.5, 0.0, z - 0.5]); + + // All vertices have the same normal. + normals.push([0.0, 1.0, 0.0]); + + // Extend the indices for for all vertices except for the final row and column, since + // indices are "between" points. + if p % vertices_per_side != vertices_per_side - 1 + && p / vertices_per_side != vertices_per_side - 1 { indices.extend_from_slice(&[p, p + 1, p + vertices_per_side]); indices.extend_from_slice(&[p + vertices_per_side, p + 1, p + vertices_per_side + 1]); @@ -41,81 +61,110 @@ fn mesh_creation(vertices_per_side: u32) -> SimpleMesh { } } -fn ray_mesh_intersection(c: &mut Criterion) { - let mut group = c.benchmark_group("ray_mesh_intersection"); - group.warm_up_time(std::time::Duration::from_millis(500)); +/// An enum that represents the configuration for all variations of the ray mesh intersection +/// benchmarks. +enum Benchmarks { + /// The ray intersects the mesh, and culling is enabled. + CullHit, - for vertices_per_side in [10_u32, 100, 1000] { - group.bench_function(format!("{}_vertices", vertices_per_side.pow(2)), |b| { - let ray = Ray3d::new(Vec3::new(0.0, 1.0, 0.0), Dir3::NEG_Y); - let mesh_to_world = Mat4::IDENTITY; - let mesh = mesh_creation(vertices_per_side); + /// The ray intersects the mesh, and culling is disabled. + NoCullHit, - b.iter(|| { - black_box(ray_cast::ray_mesh_intersection( - ray, - &mesh_to_world, - &mesh.positions, - Some(&mesh.normals), - Some(&mesh.indices), - ray_cast::Backfaces::Cull, - )); - }); - }); + /// The ray does not intersect the mesh, and culling is enabled. + CullMiss, +} + +impl Benchmarks { + const WARM_UP_TIME: Duration = Duration::from_millis(500); + const VERTICES_PER_SIDE: [u32; 3] = [10, 100, 1000]; + + /// Returns an iterator over every variant in this enum. + fn iter() -> impl Iterator { + [Self::CullHit, Self::NoCullHit, Self::CullMiss].into_iter() + } + + /// Returns the benchmark group name. + fn name(&self) -> &'static str { + match *self { + Self::CullHit => bench!("cull_intersect"), + Self::NoCullHit => bench!("no_cull_intersect"), + Self::CullMiss => bench!("cull_no_intersect"), + } + } + + fn ray(&self) -> Ray3d { + Ray3d::new( + Vec3::new(0.0, 1.0, 0.0), + match *self { + Self::CullHit | Self::NoCullHit => Dir3::NEG_Y, + // `NoIntersection` should not hit the mesh, so it goes an orthogonal direction. + Self::CullMiss => Dir3::X, + }, + ) + } + + fn mesh_to_world(&self) -> Mat4 { + Mat4::IDENTITY + } + + fn backface_culling(&self) -> Backfaces { + match *self { + Self::CullHit | Self::CullMiss => Backfaces::Cull, + Self::NoCullHit => Backfaces::Include, + } + } + + /// Returns whether the ray should intersect with the mesh. + #[cfg(test)] + fn should_intersect(&self) -> bool { + match *self { + Self::CullHit | Self::NoCullHit => true, + Self::CullMiss => false, + } } } -fn ray_mesh_intersection_no_cull(c: &mut Criterion) { - let mut group = c.benchmark_group("ray_mesh_intersection_no_cull"); - group.warm_up_time(std::time::Duration::from_millis(500)); +/// A benchmark that times [`ray_cast::ray_mesh_intersection()`]. +/// +/// There are multiple different scenarios that are tracked, which are described by the +/// [`Benchmarks`] enum. Each scenario has its own benchmark group, where individual benchmarks +/// track a ray intersecting a square mesh of an increasing amount of vertices. +fn bench(c: &mut Criterion) { + for benchmark in Benchmarks::iter() { + let mut group = c.benchmark_group(benchmark.name()); - for vertices_per_side in [10_u32, 100, 1000] { - group.bench_function(format!("{}_vertices", vertices_per_side.pow(2)), |b| { - let ray = Ray3d::new(Vec3::new(0.0, 1.0, 0.0), Dir3::NEG_Y); - let mesh_to_world = Mat4::IDENTITY; - let mesh = mesh_creation(vertices_per_side); + group + .warm_up_time(Benchmarks::WARM_UP_TIME) + // Make the scale logarithmic, to match `VERTICES_PER_SIDE`. + .plot_config(PlotConfiguration::default().summary_scale(AxisScale::Logarithmic)); - b.iter(|| { - black_box(ray_cast::ray_mesh_intersection( - ray, - &mesh_to_world, - &mesh.positions, - Some(&mesh.normals), - Some(&mesh.indices), - ray_cast::Backfaces::Include, - )); - }); - }); + for vertices_per_side in Benchmarks::VERTICES_PER_SIDE { + group.bench_with_input( + BenchmarkId::from_parameter(format!("{}_vertices", vertices_per_side.pow(2))), + &vertices_per_side, + |b, &vertices_per_side| { + let ray = black_box(benchmark.ray()); + let mesh_to_world = black_box(benchmark.mesh_to_world()); + let mesh = black_box(create_mesh(vertices_per_side)); + let backface_culling = black_box(benchmark.backface_culling()); + + b.iter(|| { + let intersected = ray_cast::ray_mesh_intersection( + ray, + &mesh_to_world, + &mesh.positions, + Some(&mesh.normals), + Some(&mesh.indices), + backface_culling, + ); + + #[cfg(test)] + assert_eq!(intersected.is_some(), benchmark.should_intersect()); + + intersected + }); + }, + ); + } } } - -fn ray_mesh_intersection_no_intersection(c: &mut Criterion) { - let mut group = c.benchmark_group("ray_mesh_intersection_no_intersection"); - group.warm_up_time(std::time::Duration::from_millis(500)); - - for vertices_per_side in [10_u32, 100, 1000] { - group.bench_function(format!("{}_vertices", (vertices_per_side).pow(2)), |b| { - let ray = Ray3d::new(Vec3::new(0.0, 1.0, 0.0), Dir3::X); - let mesh_to_world = Mat4::IDENTITY; - let mesh = mesh_creation(vertices_per_side); - - b.iter(|| { - black_box(ray_cast::ray_mesh_intersection( - ray, - &mesh_to_world, - &mesh.positions, - Some(&mesh.normals), - Some(&mesh.indices), - ray_cast::Backfaces::Cull, - )); - }); - }); - } -} - -criterion_group!( - benches, - ray_mesh_intersection, - ray_mesh_intersection_no_cull, - ray_mesh_intersection_no_intersection -);