Skip to content

Commit c890cc9

Browse files
authored
Overhaul picking benchmarks (#17033)
# Objective - Part of #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 #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" />
1 parent db5c31e commit c890cc9

File tree

1 file changed

+134
-85
lines changed

1 file changed

+134
-85
lines changed
Lines changed: 134 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,53 @@
11
use core::hint::black_box;
2+
use std::time::Duration;
23

4+
use benches::bench;
35
use bevy_math::{Dir3, Mat4, Ray3d, Vec3};
4-
use bevy_picking::mesh_picking::ray_cast;
5-
use criterion::{criterion_group, Criterion};
6+
use bevy_picking::mesh_picking::ray_cast::{self, Backfaces};
7+
use criterion::{criterion_group, AxisScale, BenchmarkId, Criterion, PlotConfiguration};
68

7-
fn ptoxznorm(p: u32, size: u32) -> (f32, f32) {
8-
let ij = (p / (size), p % (size));
9-
(ij.0 as f32 / size as f32, ij.1 as f32 / size as f32)
10-
}
9+
criterion_group!(benches, bench);
1110

11+
/// A mesh that can be passed to [`ray_cast::ray_mesh_intersection()`].
1212
struct SimpleMesh {
1313
positions: Vec<[f32; 3]>,
1414
normals: Vec<[f32; 3]>,
1515
indices: Vec<u32>,
1616
}
1717

18-
fn mesh_creation(vertices_per_side: u32) -> SimpleMesh {
18+
/// Selects a point within a normal square.
19+
///
20+
/// `p` is an index within `0..vertices_per_side.pow(2)`. The returned value is a coordinate where
21+
/// both `x` and `z` are within `0..1`.
22+
fn p_to_xz_norm(p: u32, vertices_per_side: u32) -> (f32, f32) {
23+
let x = (p / vertices_per_side) as f32;
24+
let z = (p % vertices_per_side) as f32;
25+
26+
let vertices_per_side = vertices_per_side as f32;
27+
28+
// Scale `x` and `z` to be between 0 and 1.
29+
(x / vertices_per_side, z / vertices_per_side)
30+
}
31+
32+
fn create_mesh(vertices_per_side: u32) -> SimpleMesh {
1933
let mut positions = Vec::new();
2034
let mut normals = Vec::new();
35+
let mut indices = Vec::new();
36+
2137
for p in 0..vertices_per_side.pow(2) {
22-
let xz = ptoxznorm(p, vertices_per_side);
23-
positions.push([xz.0 - 0.5, 0.0, xz.1 - 0.5]);
38+
let (x, z) = p_to_xz_norm(p, vertices_per_side);
39+
40+
// Push a new vertice to the mesh. We translate all vertices so the final square is
41+
// centered at (0, 0), instead of (0.5, 0.5).
42+
positions.push([x - 0.5, 0.0, z - 0.5]);
43+
44+
// All vertices have the same normal.
2445
normals.push([0.0, 1.0, 0.0]);
25-
}
2646

27-
let mut indices = vec![];
28-
for p in 0..vertices_per_side.pow(2) {
29-
if p % (vertices_per_side) != vertices_per_side - 1
30-
&& p / (vertices_per_side) != vertices_per_side - 1
47+
// Extend the indices for for all vertices except for the final row and column, since
48+
// indices are "between" points.
49+
if p % vertices_per_side != vertices_per_side - 1
50+
&& p / vertices_per_side != vertices_per_side - 1
3151
{
3252
indices.extend_from_slice(&[p, p + 1, p + vertices_per_side]);
3353
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 {
4161
}
4262
}
4363

44-
fn ray_mesh_intersection(c: &mut Criterion) {
45-
let mut group = c.benchmark_group("ray_mesh_intersection");
46-
group.warm_up_time(std::time::Duration::from_millis(500));
47-
48-
for vertices_per_side in [10_u32, 100, 1000] {
49-
group.bench_function(format!("{}_vertices", vertices_per_side.pow(2)), |b| {
50-
let ray = Ray3d::new(Vec3::new(0.0, 1.0, 0.0), Dir3::NEG_Y);
51-
let mesh_to_world = Mat4::IDENTITY;
52-
let mesh = mesh_creation(vertices_per_side);
53-
54-
b.iter(|| {
55-
black_box(ray_cast::ray_mesh_intersection(
56-
ray,
57-
&mesh_to_world,
58-
&mesh.positions,
59-
Some(&mesh.normals),
60-
Some(&mesh.indices),
61-
ray_cast::Backfaces::Cull,
62-
));
63-
});
64-
});
65-
}
64+
/// An enum that represents the configuration for all variations of the ray mesh intersection
65+
/// benchmarks.
66+
enum Benchmarks {
67+
/// The ray intersects the mesh, and culling is enabled.
68+
CullHit,
69+
70+
/// The ray intersects the mesh, and culling is disabled.
71+
NoCullHit,
72+
73+
/// The ray does not intersect the mesh, and culling is enabled.
74+
CullMiss,
6675
}
6776

68-
fn ray_mesh_intersection_no_cull(c: &mut Criterion) {
69-
let mut group = c.benchmark_group("ray_mesh_intersection_no_cull");
70-
group.warm_up_time(std::time::Duration::from_millis(500));
71-
72-
for vertices_per_side in [10_u32, 100, 1000] {
73-
group.bench_function(format!("{}_vertices", vertices_per_side.pow(2)), |b| {
74-
let ray = Ray3d::new(Vec3::new(0.0, 1.0, 0.0), Dir3::NEG_Y);
75-
let mesh_to_world = Mat4::IDENTITY;
76-
let mesh = mesh_creation(vertices_per_side);
77-
78-
b.iter(|| {
79-
black_box(ray_cast::ray_mesh_intersection(
80-
ray,
81-
&mesh_to_world,
82-
&mesh.positions,
83-
Some(&mesh.normals),
84-
Some(&mesh.indices),
85-
ray_cast::Backfaces::Include,
86-
));
87-
});
88-
});
77+
impl Benchmarks {
78+
const WARM_UP_TIME: Duration = Duration::from_millis(500);
79+
const VERTICES_PER_SIDE: [u32; 3] = [10, 100, 1000];
80+
81+
/// Returns an iterator over every variant in this enum.
82+
fn iter() -> impl Iterator<Item = Self> {
83+
[Self::CullHit, Self::NoCullHit, Self::CullMiss].into_iter()
8984
}
90-
}
9185

92-
fn ray_mesh_intersection_no_intersection(c: &mut Criterion) {
93-
let mut group = c.benchmark_group("ray_mesh_intersection_no_intersection");
94-
group.warm_up_time(std::time::Duration::from_millis(500));
95-
96-
for vertices_per_side in [10_u32, 100, 1000] {
97-
group.bench_function(format!("{}_vertices", (vertices_per_side).pow(2)), |b| {
98-
let ray = Ray3d::new(Vec3::new(0.0, 1.0, 0.0), Dir3::X);
99-
let mesh_to_world = Mat4::IDENTITY;
100-
let mesh = mesh_creation(vertices_per_side);
101-
102-
b.iter(|| {
103-
black_box(ray_cast::ray_mesh_intersection(
104-
ray,
105-
&mesh_to_world,
106-
&mesh.positions,
107-
Some(&mesh.normals),
108-
Some(&mesh.indices),
109-
ray_cast::Backfaces::Cull,
110-
));
111-
});
112-
});
86+
/// Returns the benchmark group name.
87+
fn name(&self) -> &'static str {
88+
match *self {
89+
Self::CullHit => bench!("cull_intersect"),
90+
Self::NoCullHit => bench!("no_cull_intersect"),
91+
Self::CullMiss => bench!("cull_no_intersect"),
92+
}
93+
}
94+
95+
fn ray(&self) -> Ray3d {
96+
Ray3d::new(
97+
Vec3::new(0.0, 1.0, 0.0),
98+
match *self {
99+
Self::CullHit | Self::NoCullHit => Dir3::NEG_Y,
100+
// `NoIntersection` should not hit the mesh, so it goes an orthogonal direction.
101+
Self::CullMiss => Dir3::X,
102+
},
103+
)
104+
}
105+
106+
fn mesh_to_world(&self) -> Mat4 {
107+
Mat4::IDENTITY
108+
}
109+
110+
fn backface_culling(&self) -> Backfaces {
111+
match *self {
112+
Self::CullHit | Self::CullMiss => Backfaces::Cull,
113+
Self::NoCullHit => Backfaces::Include,
114+
}
115+
}
116+
117+
/// Returns whether the ray should intersect with the mesh.
118+
#[cfg(test)]
119+
fn should_intersect(&self) -> bool {
120+
match *self {
121+
Self::CullHit | Self::NoCullHit => true,
122+
Self::CullMiss => false,
123+
}
113124
}
114125
}
115126

116-
criterion_group!(
117-
benches,
118-
ray_mesh_intersection,
119-
ray_mesh_intersection_no_cull,
120-
ray_mesh_intersection_no_intersection
121-
);
127+
/// A benchmark that times [`ray_cast::ray_mesh_intersection()`].
128+
///
129+
/// There are multiple different scenarios that are tracked, which are described by the
130+
/// [`Benchmarks`] enum. Each scenario has its own benchmark group, where individual benchmarks
131+
/// track a ray intersecting a square mesh of an increasing amount of vertices.
132+
fn bench(c: &mut Criterion) {
133+
for benchmark in Benchmarks::iter() {
134+
let mut group = c.benchmark_group(benchmark.name());
135+
136+
group
137+
.warm_up_time(Benchmarks::WARM_UP_TIME)
138+
// Make the scale logarithmic, to match `VERTICES_PER_SIDE`.
139+
.plot_config(PlotConfiguration::default().summary_scale(AxisScale::Logarithmic));
140+
141+
for vertices_per_side in Benchmarks::VERTICES_PER_SIDE {
142+
group.bench_with_input(
143+
BenchmarkId::from_parameter(format!("{}_vertices", vertices_per_side.pow(2))),
144+
&vertices_per_side,
145+
|b, &vertices_per_side| {
146+
let ray = black_box(benchmark.ray());
147+
let mesh_to_world = black_box(benchmark.mesh_to_world());
148+
let mesh = black_box(create_mesh(vertices_per_side));
149+
let backface_culling = black_box(benchmark.backface_culling());
150+
151+
b.iter(|| {
152+
let intersected = ray_cast::ray_mesh_intersection(
153+
ray,
154+
&mesh_to_world,
155+
&mesh.positions,
156+
Some(&mesh.normals),
157+
Some(&mesh.indices),
158+
backface_culling,
159+
);
160+
161+
#[cfg(test)]
162+
assert_eq!(intersected.is_some(), benchmark.should_intersect());
163+
164+
intersected
165+
});
166+
},
167+
);
168+
}
169+
}
170+
}

0 commit comments

Comments
 (0)