diff --git a/crates/parry2d-f64/Cargo.toml b/crates/parry2d-f64/Cargo.toml index a2c3e429..624fce90 100644 --- a/crates/parry2d-f64/Cargo.toml +++ b/crates/parry2d-f64/Cargo.toml @@ -85,7 +85,18 @@ smallvec = { workspace = true } foldhash = { workspace = true } [dev-dependencies] +criterion = "0.8" simba = { workspace = true, features = ["wide"] } oorandom = { workspace = true } ptree = { workspace = true } rand = { workspace = true } + +[[bench]] +name = "contact_manifolds" +path = "benches/contact_manifolds.rs" +harness = false + +[[bench]] +name = "gjk_epa" +path = "benches/gjk_epa.rs" +harness = false diff --git a/crates/parry2d-f64/benches/contact_manifolds.rs b/crates/parry2d-f64/benches/contact_manifolds.rs new file mode 100644 index 00000000..81c4dcca --- /dev/null +++ b/crates/parry2d-f64/benches/contact_manifolds.rs @@ -0,0 +1,5 @@ +//! f64 variant of the parry2d contact-manifold benchmarks (shared body, +//! aliased crate). +extern crate parry2d_f64 as parry2d; + +include!("../../parry2d/benches/contact_manifolds.rs"); diff --git a/crates/parry2d-f64/benches/gjk_epa.rs b/crates/parry2d-f64/benches/gjk_epa.rs new file mode 100644 index 00000000..aebe0720 --- /dev/null +++ b/crates/parry2d-f64/benches/gjk_epa.rs @@ -0,0 +1,4 @@ +//! f64 variant of the parry2d GJK/EPA benchmarks (shared body, aliased crate). +extern crate parry2d_f64 as parry2d; + +include!("../../parry2d/benches/gjk_epa.rs"); diff --git a/crates/parry2d/Cargo.toml b/crates/parry2d/Cargo.toml index 3100b23e..1ace8731 100644 --- a/crates/parry2d/Cargo.toml +++ b/crates/parry2d/Cargo.toml @@ -87,6 +87,7 @@ foldhash = { workspace = true } encase = { workspace = true, optional = true } [dev-dependencies] +criterion = "0.8" simba = { workspace = true, features = ["wide"] } oorandom = { workspace = true } ptree = { workspace = true } @@ -186,3 +187,13 @@ doc-scrape-examples = true name = "time_of_impact_query2d" path = "examples/time_of_impact_query2d.rs" doc-scrape-examples = true + +[[bench]] +name = "contact_manifolds" +path = "benches/contact_manifolds.rs" +harness = false + +[[bench]] +name = "gjk_epa" +path = "benches/gjk_epa.rs" +harness = false diff --git a/crates/parry2d/benches/contact_manifolds.rs b/crates/parry2d/benches/contact_manifolds.rs new file mode 100644 index 00000000..a92690ac --- /dev/null +++ b/crates/parry2d/benches/contact_manifolds.rs @@ -0,0 +1,201 @@ +// Criterion benchmarks for steady-state 2D contact-manifold updates, +// mirroring the parry3d suite: dedicated convex pairs, the generic PFM +// fallback, and trimesh-vs-convex persistent manifolds. The body is written +// against `parry2d::math::Real` so the parry2d-f64 crate reuses it verbatim. + +use criterion::{criterion_group, criterion_main, Criterion}; +use parry2d::math::{Pose, Real, Vector}; +use parry2d::query::{ + ContactManifold, ContactManifoldsWorkspace, DefaultQueryDispatcher, PersistentQueryDispatcher, +}; +use parry2d::shape::{Ball, Capsule, ConvexPolygon, Cuboid, TriMesh}; +use std::hint::black_box; +use std::time::Duration; + +type Manifold = ContactManifold<(), ()>; + +fn drifting_pose(step: u32) -> Pose { + let t = step as Real * 0.02; + Pose::translation(t.sin() * 0.03, 1.2 + t.cos() * 0.02) +} + +fn hexagon(radius: Real) -> ConvexPolygon { + let pts: Vec = (0..6) + .map(|i| { + let a = i as Real * core::f64::consts::PI as Real / 3.0; + Vector::new(a.cos() * radius, a.sin() * radius) + }) + .collect(); + ConvexPolygon::from_convex_polyline(pts).unwrap() +} + +fn bench_convex_pairs(c: &mut Criterion) { + let dispatcher = DefaultQueryDispatcher; + let prediction = 0.1; + + let mut g = c.benchmark_group("contact_manifold_convex_convex"); + + g.bench_function("cuboid_cuboid", |b| { + let c1 = Cuboid::new(Vector::new(1.0, 1.0)); + let c2 = Cuboid::new(Vector::new(0.8, 0.8)); + let mut manifold = Manifold::new(); + let mut step = 0u32; + b.iter(|| { + step = step.wrapping_add(1); + let pos12 = drifting_pose(step) * Pose::translation(0.0, 0.35); + dispatcher + .contact_manifold_convex_convex( + &pos12, + &c1, + &c2, + None, + None, + prediction, + &mut manifold, + ) + .unwrap(); + black_box(manifold.points.len()) + }) + }); + + // Hexagon/capsule: generic support-map fallback (GJK + EPA on penetration). + g.bench_function("pfm_hexagon_capsule", |b| { + let poly = hexagon(1.0); + let cap = Capsule::new_y(0.6, 0.3); + let mut manifold = Manifold::new(); + let mut step = 0u32; + b.iter(|| { + step = step.wrapping_add(1); + let pos12 = drifting_pose(step); + dispatcher + .contact_manifold_convex_convex( + &pos12, + &poly, + &cap, + None, + None, + prediction, + &mut manifold, + ) + .unwrap(); + black_box(manifold.points.len()) + }) + }); + + g.bench_function("capsule_capsule", |b| { + let cap1 = Capsule::new_y(0.8, 0.4); + let cap2 = Capsule::new_y(0.6, 0.3); + let mut manifold = Manifold::new(); + let mut step = 0u32; + b.iter(|| { + step = step.wrapping_add(1); + let pos12 = drifting_pose(step) * Pose::translation(0.0, -0.2); + dispatcher + .contact_manifold_convex_convex( + &pos12, + &cap1, + &cap2, + None, + None, + prediction, + &mut manifold, + ) + .unwrap(); + black_box(manifold.points.len()) + }) + }); + + g.finish(); +} + +/// A triangulated band: a sine-curve top edge over a flat bottom edge. +fn make_terrain(columns: u32) -> TriMesh { + let n = columns as usize; + let mut vertices = Vec::with_capacity((n + 1) * 2); + let mut indices = Vec::with_capacity(n * 2); + for ix in 0..=n { + let x = ix as Real / n as Real * 40.0 - 20.0; + let y = (x * 0.8).sin() * 0.4; + vertices.push(Vector::new(x, y)); // top: 2*ix + vertices.push(Vector::new(x, -1.5)); // bottom: 2*ix + 1 + } + for ix in 0..n { + let top0 = (ix * 2) as u32; + let bot0 = top0 + 1; + let top1 = top0 + 2; + let bot1 = top0 + 3; + indices.push([top0, bot0, top1]); + indices.push([bot0, bot1, top1]); + } + TriMesh::new(vertices, indices).unwrap() +} + +fn bench_trimesh_shape(c: &mut Criterion) { + let dispatcher = DefaultQueryDispatcher; + let prediction = 0.1; + let terrain = make_terrain(512); // 1024 triangles + + let mut g = c.benchmark_group("contact_manifolds_trimesh"); + + g.bench_function("trimesh_vs_ball", |b| { + let ball = Ball::new(0.7); + let mut manifolds: Vec = Vec::new(); + let mut workspace: Option = None; + let mut step = 0u32; + b.iter(|| { + step = step.wrapping_add(1); + let t = step as Real * 0.02; + let pos12 = Pose::translation(t.sin() * 0.5, 0.55); + dispatcher + .contact_manifolds( + &pos12, + &terrain, + &ball, + prediction, + &mut manifolds, + &mut workspace, + ) + .unwrap(); + black_box(manifolds.len()) + }) + }); + + g.bench_function("trimesh_vs_cuboid", |b| { + let cuboid = Cuboid::new(Vector::new(0.6, 0.6)); + let mut manifolds: Vec = Vec::new(); + let mut workspace: Option = None; + let mut step = 0u32; + b.iter(|| { + step = step.wrapping_add(1); + let t = step as Real * 0.02; + let pos12 = Pose::translation(t.sin() * 0.5, 0.5); + dispatcher + .contact_manifolds( + &pos12, + &terrain, + &cuboid, + prediction, + &mut manifolds, + &mut workspace, + ) + .unwrap(); + black_box(manifolds.len()) + }) + }); + + g.finish(); +} + +fn config() -> Criterion { + Criterion::default() + .warm_up_time(Duration::from_millis(800)) + .measurement_time(Duration::from_secs(3)) + .sample_size(60) +} + +criterion_group! { + name = benches; + config = config(); + targets = bench_convex_pairs, bench_trimesh_shape +} +criterion_main!(benches); diff --git a/crates/parry2d/benches/gjk_epa.rs b/crates/parry2d/benches/gjk_epa.rs new file mode 100644 index 00000000..30e32676 --- /dev/null +++ b/crates/parry2d/benches/gjk_epa.rs @@ -0,0 +1,59 @@ +// Criterion benchmarks for 2D GJK/EPA contact queries between smooth convex +// shapes (support-map path), mirroring the parry3d suite. Written against +// `parry2d::math::Real` so parry2d-f64 reuses the body verbatim. + +use criterion::{criterion_group, criterion_main, Criterion}; +use parry2d::math::{Pose, Real, Vector}; +use parry2d::query; +use parry2d::shape::{Capsule, ConvexPolygon}; +use std::hint::black_box; +use std::time::Duration; + +fn hexagon(radius: Real) -> ConvexPolygon { + let pts: Vec = (0..6) + .map(|i| { + let a = i as Real * core::f64::consts::PI as Real / 3.0; + Vector::new(a.cos() * radius, a.sin() * radius) + }) + .collect(); + ConvexPolygon::from_convex_polyline(pts).unwrap() +} + +fn bench_gjk_epa(c: &mut Criterion) { + let poly = hexagon(1.0); + let cap = Capsule::new_y(0.8, 0.4); + let identity = Pose::identity(); + + let mut g = c.benchmark_group("gjk_epa_contact"); + + g.bench_function("gjk_separated", |b| { + let pos2 = Pose::translation(2.9, 0.4); + b.iter(|| black_box(query::contact(&identity, &poly, &pos2, &cap, 1.5).unwrap())) + }); + + g.bench_function("gjk_shallow_penetration", |b| { + let pos2 = Pose::translation(1.32, 0.05); + b.iter(|| black_box(query::contact(&identity, &poly, &pos2, &cap, 0.1).unwrap())) + }); + + g.bench_function("epa_deep_penetration", |b| { + let pos2 = Pose::translation(0.35, 0.1); + b.iter(|| black_box(query::contact(&identity, &poly, &pos2, &cap, 0.1).unwrap())) + }); + + g.finish(); +} + +fn config() -> Criterion { + Criterion::default() + .warm_up_time(Duration::from_millis(500)) + .measurement_time(Duration::from_secs(2)) + .sample_size(80) +} + +criterion_group! { + name = benches; + config = config(); + targets = bench_gjk_epa +} +criterion_main!(benches); diff --git a/crates/parry3d-f64/Cargo.toml b/crates/parry3d-f64/Cargo.toml index 427f95d4..ed9addfe 100644 --- a/crates/parry3d-f64/Cargo.toml +++ b/crates/parry3d-f64/Cargo.toml @@ -88,6 +88,7 @@ ordered-float = { workspace = true } thiserror = { workspace = true } [dev-dependencies] +criterion = "0.8" oorandom = { workspace = true } ptree = { workspace = true } rand = { workspace = true } @@ -95,3 +96,13 @@ rand = { workspace = true } [package.metadata.docs.rs] rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] features = ["wavefront"] + +[[bench]] +name = "contact_manifolds" +path = "benches/contact_manifolds.rs" +harness = false + +[[bench]] +name = "gjk_epa" +path = "benches/gjk_epa.rs" +harness = false diff --git a/crates/parry3d-f64/benches/contact_manifolds.rs b/crates/parry3d-f64/benches/contact_manifolds.rs new file mode 100644 index 00000000..d713f27e --- /dev/null +++ b/crates/parry3d-f64/benches/contact_manifolds.rs @@ -0,0 +1,6 @@ +//! f64 variant of the parry3d contact-manifold benchmarks: the bench body is +//! shared with parry3d (it is written against `parry3d::math::Real`) and the +//! extern-crate alias below rebinds it to the f64 build. +extern crate parry3d_f64 as parry3d; + +include!("../../parry3d/benches/contact_manifolds.rs"); diff --git a/crates/parry3d-f64/benches/gjk_epa.rs b/crates/parry3d-f64/benches/gjk_epa.rs new file mode 100644 index 00000000..653ebbb6 --- /dev/null +++ b/crates/parry3d-f64/benches/gjk_epa.rs @@ -0,0 +1,4 @@ +//! f64 variant of the parry3d GJK/EPA benchmarks (shared body, aliased crate). +extern crate parry3d_f64 as parry3d; + +include!("../../parry3d/benches/gjk_epa.rs"); diff --git a/crates/parry3d/Cargo.toml b/crates/parry3d/Cargo.toml index c23827e0..09f120b6 100644 --- a/crates/parry3d/Cargo.toml +++ b/crates/parry3d/Cargo.toml @@ -97,6 +97,29 @@ rand = { workspace = true } kiss3d = { workspace = true } rand_isaac = { workspace = true } web-time = { workspace = true } +criterion = "0.8" + +# Declaring any [[bench]] disables bench auto-discovery, so the pre-existing +# nightly libtest bench (`benches/all.rs`) is declared explicitly to keep it +# available unchanged. +[[bench]] +name = "all" +path = "benches/all.rs" + +[[bench]] +name = "contact_manifolds" +path = "benches/contact_manifolds.rs" +harness = false + +[[bench]] +name = "gjk_epa" +path = "benches/gjk_epa.rs" +harness = false + +[[bench]] +name = "bvh_queries" +path = "benches/bvh_queries.rs" +harness = false [package.metadata.docs.rs] rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] diff --git a/crates/parry3d/benches/bvh_queries.rs b/crates/parry3d/benches/bvh_queries.rs new file mode 100644 index 00000000..3f68fbfb --- /dev/null +++ b/crates/parry3d/benches/bvh_queries.rs @@ -0,0 +1,118 @@ +// Criterion benchmarks for BVH-backed queries on a trimesh: AABB +// interference enumeration, ray casts, and point projection. These exercise +// the BvhNode predicate hot paths (intersects/contains/cast_ray) through the +// public TriMesh query API. Written against `Real` so f64 crates can reuse +// the body through an alias shim. + +use criterion::{criterion_group, criterion_main, Criterion}; +use parry3d::bounding_volume::Aabb; +use parry3d::math::{Real, Vector}; +use parry3d::query::{PointQuery, Ray, RayCast}; +use parry3d::shape::TriMesh; +use std::hint::black_box; +use std::time::Duration; + +fn make_terrain(subdivisions: u32) -> TriMesh { + let n = subdivisions as usize; + let mut vertices = Vec::with_capacity((n + 1) * (n + 1)); + let mut indices = Vec::with_capacity(n * n * 2); + for iz in 0..=n { + for ix in 0..=n { + let x = ix as Real / n as Real * 20.0 - 10.0; + let z = iz as Real / n as Real * 20.0 - 10.0; + let y = (x * 0.8).sin() * 0.4 + (z * 0.7).cos() * 0.4; + vertices.push(Vector::new(x, y, z)); + } + } + let stride = n + 1; + for iz in 0..n { + for ix in 0..n { + let a = (iz * stride + ix) as u32; + let b = (iz * stride + ix + 1) as u32; + let c = ((iz + 1) * stride + ix) as u32; + let d = ((iz + 1) * stride + ix + 1) as u32; + indices.push([a, b, c]); + indices.push([b, d, c]); + } + } + TriMesh::new(vertices, indices).unwrap() +} + +fn bench_bvh_queries(c: &mut Criterion) { + let terrain = make_terrain(64); // 8192 triangles + let bvh = terrain.bvh(); + + let mut g = c.benchmark_group("bvh_queries"); + + g.bench_function("intersect_aabb_64", |b| { + let queries: Vec = (0..64) + .map(|i| { + let t = i as Real * 0.37; + let center = Vector::new(t.sin() * 8.0, 0.0, t.cos() * 8.0); + let he = Vector::new(0.8, 2.0, 0.8); + Aabb::new(center - he, center + he) + }) + .collect(); + b.iter(|| { + let mut count = 0usize; + for q in &queries { + count += bvh.intersect_aabb(q).count(); + } + black_box(count) + }) + }); + + g.bench_function("cast_ray_64", |b| { + let rays: Vec = (0..64) + .map(|i| { + let t = i as Real * 0.61; + Ray::new( + Vector::new(t.sin() * 9.0, 5.0, t.cos() * 9.0), + Vector::new(t.cos() * 0.1, -1.0, t.sin() * 0.1), + ) + }) + .collect(); + b.iter(|| { + let mut acc = 0.0; + for r in &rays { + if let Some(toi) = terrain.cast_local_ray(r, 100.0, true) { + acc += toi; + } + } + black_box(acc) + }) + }); + + g.bench_function("project_point_64", |b| { + let points: Vec = (0..64) + .map(|i| { + let t = i as Real * 0.53; + Vector::new(t.sin() * 8.0, 1.5 + t.cos(), t.cos() * 7.0) + }) + .collect(); + b.iter(|| { + let mut acc = 0.0; + for p in &points { + let proj = terrain.project_local_point(*p, true); + acc += proj.point.x; + } + black_box(acc) + }) + }); + + g.finish(); +} + +fn config() -> Criterion { + Criterion::default() + .warm_up_time(Duration::from_millis(600)) + .measurement_time(Duration::from_secs(3)) + .sample_size(60) +} + +criterion_group! { + name = benches; + config = config(); + targets = bench_bvh_queries +} +criterion_main!(benches); diff --git a/crates/parry3d/benches/contact_manifolds.rs b/crates/parry3d/benches/contact_manifolds.rs new file mode 100644 index 00000000..c1b51baf --- /dev/null +++ b/crates/parry3d/benches/contact_manifolds.rs @@ -0,0 +1,259 @@ +// Criterion benchmarks for steady-state contact-manifold updates. +// +// These target the narrow-phase hot paths from the 2026-08 optimization audit: +// per-rebuild `manifold.points.clone()` (P1), per-contact `EPA::new()` (P2), +// and loop-invariant work in the trimesh manifold loop (P6). Poses drift a +// little every iteration so contact tracking stays on the realistic +// "rebuild + match old points" path. + +use criterion::{criterion_group, criterion_main, Criterion}; +use parry3d::math::{Pose, Real, Vector}; +use parry3d::query::{ + ContactManifold, ContactManifoldsWorkspace, DefaultQueryDispatcher, PersistentQueryDispatcher, +}; +use parry3d::shape::{Ball, Capsule, Cone, Cuboid, Cylinder, TriMesh}; +use std::hint::black_box; +use std::time::Duration; + +type Manifold = ContactManifold<(), ()>; + +fn drifting_pose(step: u32) -> Pose { + let t = step as Real * 0.02; + Pose::translation(t.sin() * 0.03, 1.4 + t.cos() * 0.02, 0.0) +} + +fn bench_convex_pairs(c: &mut Criterion) { + let dispatcher = DefaultQueryDispatcher; + let prediction = 0.1; + + let mut g = c.benchmark_group("contact_manifold_convex_convex"); + + // Cuboid/cuboid: dedicated SAT-based path with the points.clone() pattern. + g.bench_function("cuboid_cuboid", |b| { + let c1 = Cuboid::new(Vector::new(1.0, 1.0, 1.0)); + let c2 = Cuboid::new(Vector::new(0.8, 0.8, 0.8)); + let mut manifold = Manifold::new(); + let mut step = 0u32; + b.iter(|| { + step = step.wrapping_add(1); + let pos12 = drifting_pose(step) * Pose::translation(0.0, 0.35, 0.0); + dispatcher + .contact_manifold_convex_convex( + &pos12, + &c1, + &c2, + None, + None, + prediction, + &mut manifold, + ) + .unwrap(); + black_box(manifold.points.len()) + }) + }); + + // Cylinder/cone: generic PFM fallback (GJK + EPA on penetration). + g.bench_function("pfm_pfm_cylinder_cone", |b| { + let c1 = Cylinder::new(0.8, 0.6); + let c2 = Cone::new(0.7, 0.5); + let mut manifold = Manifold::new(); + let mut step = 0u32; + b.iter(|| { + step = step.wrapping_add(1); + let pos12 = drifting_pose(step); + dispatcher + .contact_manifold_convex_convex( + &pos12, + &c1, + &c2, + None, + None, + prediction, + &mut manifold, + ) + .unwrap(); + black_box(manifold.points.len()) + }) + }); + + // Capsule/capsule: dedicated path, also carries the clone pattern. + g.bench_function("capsule_capsule", |b| { + let cap1 = Capsule::new_y(0.8, 0.4); + let cap2 = Capsule::new_y(0.6, 0.3); + let mut manifold = Manifold::new(); + let mut step = 0u32; + b.iter(|| { + step = step.wrapping_add(1); + let pos12 = drifting_pose(step) * Pose::translation(0.0, -0.2, 0.0); + dispatcher + .contact_manifold_convex_convex( + &pos12, + &cap1, + &cap2, + None, + None, + prediction, + &mut manifold, + ) + .unwrap(); + black_box(manifold.points.len()) + }) + }); + + g.finish(); +} + +fn make_terrain(subdivisions: u32) -> TriMesh { + let n = subdivisions as usize; + let mut vertices = Vec::with_capacity((n + 1) * (n + 1)); + let mut indices = Vec::with_capacity(n * n * 2); + for iz in 0..=n { + for ix in 0..=n { + let x = ix as Real / n as Real * 20.0 - 10.0; + let z = iz as Real / n as Real * 20.0 - 10.0; + let y = (x * 0.8).sin() * 0.4 + (z * 0.7).cos() * 0.4; + vertices.push(Vector::new(x, y, z)); + } + } + let stride = n + 1; + for iz in 0..n { + for ix in 0..n { + let a = (iz * stride + ix) as u32; + let b = (iz * stride + ix + 1) as u32; + let c = ((iz + 1) * stride + ix) as u32; + let d = ((iz + 1) * stride + ix + 1) as u32; + indices.push([a, b, c]); + indices.push([b, d, c]); + } + } + TriMesh::new(vertices, indices).unwrap() +} + +fn bench_trimesh_shape(c: &mut Criterion) { + let dispatcher = DefaultQueryDispatcher; + let prediction = 0.1; + let terrain = make_terrain(48); // 4608 triangles + + // Same terrain with internal-edge fixing: pseudo-normals are computed and + // fetched per candidate triangle every frame. + let (fixed_vtx, fixed_idx) = { + let t = make_terrain(48); + (t.vertices().to_vec(), t.indices().to_vec()) + }; + let terrain_fixed = TriMesh::with_flags( + fixed_vtx, + fixed_idx, + parry3d::shape::TriMeshFlags::FIX_INTERNAL_EDGES, + ) + .unwrap(); + + let mut g = c.benchmark_group("contact_manifolds_trimesh"); + + g.bench_function("trimesh_fixed_edges_vs_cuboid", |b| { + let cuboid = Cuboid::new(Vector::new(0.6, 0.6, 0.6)); + let mut manifolds: Vec = Vec::new(); + let mut workspace: Option = None; + let mut step = 0u32; + b.iter(|| { + step = step.wrapping_add(1); + let t = step as Real * 0.02; + let pos12 = Pose::translation(t.sin() * 0.5, 0.5, t.cos() * 0.5); + dispatcher + .contact_manifolds( + &pos12, + &terrain_fixed, + &cuboid, + prediction, + &mut manifolds, + &mut workspace, + ) + .unwrap(); + black_box(manifolds.len()) + }) + }); + + g.bench_function("trimesh_fixed_edges_vs_ball", |b| { + let ball = Ball::new(0.7); + let mut manifolds: Vec = Vec::new(); + let mut workspace: Option = None; + let mut step = 0u32; + b.iter(|| { + step = step.wrapping_add(1); + let t = step as Real * 0.02; + let pos12 = Pose::translation(t.sin() * 0.5, 0.55, t.cos() * 0.5); + dispatcher + .contact_manifolds( + &pos12, + &terrain_fixed, + &ball, + prediction, + &mut manifolds, + &mut workspace, + ) + .unwrap(); + black_box(manifolds.len()) + }) + }); + + g.bench_function("trimesh_vs_ball", |b| { + let ball = Ball::new(0.7); + let mut manifolds: Vec = Vec::new(); + let mut workspace: Option = None; + let mut step = 0u32; + b.iter(|| { + step = step.wrapping_add(1); + let t = step as Real * 0.02; + let pos12 = Pose::translation(t.sin() * 0.5, 0.55, t.cos() * 0.5); + dispatcher + .contact_manifolds( + &pos12, + &terrain, + &ball, + prediction, + &mut manifolds, + &mut workspace, + ) + .unwrap(); + black_box(manifolds.len()) + }) + }); + + g.bench_function("trimesh_vs_cuboid", |b| { + let cuboid = Cuboid::new(Vector::new(0.6, 0.6, 0.6)); + let mut manifolds: Vec = Vec::new(); + let mut workspace: Option = None; + let mut step = 0u32; + b.iter(|| { + step = step.wrapping_add(1); + let t = step as Real * 0.02; + let pos12 = Pose::translation(t.sin() * 0.5, 0.5, t.cos() * 0.5); + dispatcher + .contact_manifolds( + &pos12, + &terrain, + &cuboid, + prediction, + &mut manifolds, + &mut workspace, + ) + .unwrap(); + black_box(manifolds.len()) + }) + }); + + g.finish(); +} + +fn config() -> Criterion { + Criterion::default() + .warm_up_time(Duration::from_millis(800)) + .measurement_time(Duration::from_secs(3)) + .sample_size(60) +} + +criterion_group! { + name = benches; + config = config(); + targets = bench_convex_pairs, bench_trimesh_shape +} +criterion_main!(benches); diff --git a/crates/parry3d/benches/gjk_epa.rs b/crates/parry3d/benches/gjk_epa.rs new file mode 100644 index 00000000..6e176584 --- /dev/null +++ b/crates/parry3d/benches/gjk_epa.rs @@ -0,0 +1,56 @@ +// Criterion benchmarks for GJK/EPA contact queries between smooth convex +// shapes (support-map path). Deep-penetration cases exercise the per-call +// `EPA::new()` allocation (audit finding P2); separated/shallow cases stay +// in pure GJK (finding P5's per-iteration sqrt). + +use criterion::{criterion_group, criterion_main, Criterion}; +use parry3d::math::Pose; +use parry3d::query; +use parry3d::shape::{Capsule, Cone, Cylinder}; +use std::hint::black_box; +use std::time::Duration; + +fn bench_gjk_epa(c: &mut Criterion) { + let cyl = Cylinder::new(0.8, 0.6); + let cone = Cone::new(0.7, 0.5); + let cap = Capsule::new_y(0.8, 0.4); + let identity = Pose::identity(); + + let mut g = c.benchmark_group("gjk_epa_contact"); + + g.bench_function("gjk_separated", |b| { + let pos2 = Pose::translation(2.6, 0.4, 0.1); + b.iter(|| black_box(query::contact(&identity, &cyl, &pos2, &cone, 1.5).unwrap())) + }); + + g.bench_function("gjk_shallow_penetration", |b| { + let pos2 = Pose::translation(1.02, 0.05, 0.0); + b.iter(|| black_box(query::contact(&identity, &cyl, &pos2, &cone, 0.1).unwrap())) + }); + + g.bench_function("epa_deep_penetration", |b| { + let pos2 = Pose::translation(0.35, 0.1, 0.05); + b.iter(|| black_box(query::contact(&identity, &cyl, &pos2, &cone, 0.1).unwrap())) + }); + + g.bench_function("epa_deep_capsule_cylinder", |b| { + let pos2 = Pose::translation(0.25, 0.2, 0.0); + b.iter(|| black_box(query::contact(&identity, &cap, &pos2, &cyl, 0.1).unwrap())) + }); + + g.finish(); +} + +fn config() -> Criterion { + Criterion::default() + .warm_up_time(Duration::from_millis(500)) + .measurement_time(Duration::from_secs(2)) + .sample_size(80) +} + +criterion_group! { + name = benches; + config = config(); + targets = bench_gjk_epa +} +criterion_main!(benches);