Skip to content

Commit

Permalink
Overhaul picking benchmarks (bevyengine#17033)
Browse files Browse the repository at this point in the history
# Objective

- Part of bevyengine#16647.
- This PR goes through our `ray_cast::ray_mesh_intersection()`
benchmarks and overhauls them with more comments and better
extensibility. The code is also a lot less duplicated!

## Solution

- Create a `Benchmarks` enum that describes all of the different kind of
scenarios we want to benchmark.
- Merge all of our existing benchmark functions into a single one,
`bench()`, which sets up the scenarios all at once.
- Add comments to `mesh_creation()` and `ptoxznorm()`, and move some
lines around to be a bit clearer.
- Make the benchmarks use the new `bench!` macro, as part of bevyengine#16647.
- Rename many functions and benchmarks to be clearer.

## For reviewers

I split this PR up into several, easier to digest commits. You might
find it easier to review by looking through each commit, instead of the
complete file changes.

None of my changes actually modifies the behavior of the benchmarks;
they still track the exact same test cases. There shouldn't be
significant changes in benchmark performance before and after this PR.

## Testing

List all picking benchmarks: `cargo bench -p benches --bench picking --
--list`

Run the benchmarks once in debug mode: `cargo test -p benches --bench
picking`

Run the benchmarks and analyze their performance: `cargo bench -p
benches --bench picking`

- Check out the generated HTML report in
`./target/criterion/report/index.html` once you're done!

---

## Showcase

List of all picking benchmarks, after having been renamed:

<img width="524" alt="image"
src="https://github.com/user-attachments/assets/a1b53daf-4a8b-4c45-a25a-c6306c7175d1"
/>

Example report for
`picking::ray_mesh_intersection::cull_intersect/100_vertices`:

<img width="992" alt="image"
src="https://github.com/user-attachments/assets/a1aaf53f-ce21-4bef-89c4-b982bb158f5d"
/>
  • Loading branch information
BD103 authored and ecoskey committed Jan 6, 2025
1 parent eb89c37 commit e731c6e
Showing 1 changed file with 134 additions and 85 deletions.
219 changes: 134 additions & 85 deletions benches/benches/bevy_picking/ray_mesh_intersection.rs
Original file line number Diff line number Diff line change
@@ -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<u32>,
}

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();
let mut indices = 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]);
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]);
}

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
// 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]);
Expand All @@ -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));

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);

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,
));
});
});
}
/// 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,

/// The ray intersects the mesh, and culling is disabled.
NoCullHit,

/// The ray does not intersect the mesh, and culling is enabled.
CullMiss,
}

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));

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);

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,
));
});
});
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<Item = Self> {
[Self::CullHit, Self::NoCullHit, Self::CullMiss].into_iter()
}
}

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,
));
});
});
/// 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,
}
}
}

criterion_group!(
benches,
ray_mesh_intersection,
ray_mesh_intersection_no_cull,
ray_mesh_intersection_no_intersection
);
/// 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());

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));

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
});
},
);
}
}
}

0 comments on commit e731c6e

Please sign in to comment.