Skip to content

Commit bac2021

Browse files
authored
Optimize contact constraint generation (#699)
# Objective Currently, contact constraints are generated serially after the narrow phase. This requires iterating over all contact pairs and redoing various queries and computations, which can add meaningful overhead. (Note: The commit history also includes most commits from #683, sorry about that 😅) ## Solution Optimize contact constraint generation by generating constraints directly in the parallel contact update loop of the narrow phase. This way, we get "free" parallelism while getting rid of the extra iteration, and we don't need to query for the rigid bodies or colliders a second time. We can even reuse some values computed for the contact update, like `effective_speculative_margin`. Constraint generation is multi-threaded by storing constraints in thread-local `Vec`s and draining them serially into `ContactConstraints`. This preserves determinism. ## Performance For physics diagnostics, the "Generate Constraints" step has been removed, and the cost is now included in the "Narrow Phase" step. This means that the narrow phase is seemingly more expensive, but the total cost is reduced quite drastically. Improvements can be seen in both single-threaded and multi-threaded performance, with the latter having a larger difference. Note that the "Store Impulses" step is now slightly more expensive. This is because the contact pair lookup can no longer be performed with the edge index directly, as constraints are generated before contact pair removal, and pair removal can invalidate indices. ### Single-threaded Before: ![Before](https://github.com/user-attachments/assets/e4be0870-cfdb-4eb0-9d0e-c9e751aa26f1) After: ![After](https://github.com/user-attachments/assets/74866fcb-a1d9-4d17-aca8-07304b57e592) ### Multi-threaded Before: ![Before](https://github.com/user-attachments/assets/ad930bd1-9cea-45d2-b870-6803d44b9909) After: ![After](https://github.com/user-attachments/assets/07ca32c5-b690-4c6c-8337-9d2fc35b7c8b) --- ## Migration Guide `NarrowPhaseSet::GenerateConstraints` has been removed. Contact constraints are now generated as part of `NarrowPhaseSet::Update`.
1 parent 58f8117 commit bac2021

File tree

8 files changed

+240
-348
lines changed

8 files changed

+240
-348
lines changed

src/collision/diagnostics.rs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ pub struct CollisionDiagnostics {
1515
pub broad_phase: Duration,
1616
/// Time spent updating contacts in the narrow phase.
1717
pub narrow_phase: Duration,
18-
/// Time spent generating constraints from contacts.
19-
pub generate_constraints: Duration,
2018
/// The number of contacts.
2119
pub contact_count: u32,
2220
}
@@ -26,7 +24,6 @@ impl PhysicsDiagnostics for CollisionDiagnostics {
2624
vec![
2725
(Self::BROAD_PHASE, self.broad_phase),
2826
(Self::NARROW_PHASE, self.narrow_phase),
29-
(Self::GENERATE_CONSTRAINTS, self.generate_constraints),
3027
]
3128
}
3229

@@ -39,7 +36,6 @@ impl_diagnostic_paths! {
3936
impl CollisionDiagnostics {
4037
BROAD_PHASE: "avian/collision/broad_phase",
4138
NARROW_PHASE: "avian/collision/update_contacts",
42-
GENERATE_CONSTRAINTS: "avian/collision/generate_constraints",
4339
CONTACT_COUNT: "avian/collision/contact_count",
4440
}
4541
}

src/collision/narrow_phase/mod.rs

Lines changed: 6 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,13 @@
44
55
mod system_param;
66
use system_param::ContactStatusBits;
7-
#[cfg(feature = "parallel")]
8-
use system_param::ContactStatusBitsThreadLocal;
97
pub use system_param::NarrowPhase;
8+
#[cfg(feature = "parallel")]
9+
use system_param::NarrowPhaseThreadLocals;
1010

1111
use core::marker::PhantomData;
1212

13-
use crate::{
14-
dynamics::solver::{ContactConstraints, ContactSoftnessCoefficients},
15-
prelude::*,
16-
};
13+
use crate::{dynamics::solver::ContactConstraints, prelude::*};
1714
use bevy::{
1815
ecs::{
1916
entity_disabling::Disabled,
@@ -100,7 +97,7 @@ where
10097
.init_resource::<DefaultRestitution>();
10198

10299
#[cfg(feature = "parallel")]
103-
app.init_resource::<ContactStatusBitsThreadLocal>();
100+
app.init_resource::<NarrowPhaseThreadLocals>();
104101

105102
app.register_type::<(NarrowPhaseConfig, DefaultFriction, DefaultRestitution)>();
106103

@@ -117,7 +114,6 @@ where
117114
(
118115
NarrowPhaseSet::First,
119116
NarrowPhaseSet::Update,
120-
NarrowPhaseSet::GenerateConstraints,
121117
NarrowPhaseSet::Last,
122118
)
123119
.chain()
@@ -138,18 +134,6 @@ where
138134
// to have multiple collision backends at the same time.
139135
.ambiguous_with_all(),
140136
);
141-
142-
if self.generate_constraints {
143-
// Generate contact constraints.
144-
app.add_systems(
145-
self.schedule,
146-
generate_constraints::<C>
147-
.in_set(NarrowPhaseSet::GenerateConstraints)
148-
// Allowing ambiguities is required so that it's possible
149-
// to have multiple collision backends at the same time.
150-
.ambiguous_with_all(),
151-
);
152-
}
153137
}
154138

155139
fn finish(&self, app: &mut App) {
@@ -224,10 +208,6 @@ pub enum NarrowPhaseSet {
224208
First,
225209
/// Updates contacts in the [`ContactGraph`] and processes contact state changes.
226210
Update,
227-
/// Generates [`ContactConstraint`]s and adds them to [`ContactConstraints`].
228-
///
229-
/// [`ContactConstraint`]: dynamics::solver::contact::ContactConstraint
230-
GenerateConstraints,
231211
/// Runs at the end of the narrow phase. Empty by default.
232212
Last,
233213
}
@@ -241,6 +221,7 @@ fn update_narrow_phase<C: AnyCollider, H: CollisionHooks + 'static>(
241221
context: StaticSystemParam<C::Context>,
242222
mut commands: ParallelCommands,
243223
mut diagnostics: ResMut<CollisionDiagnostics>,
224+
solver_diagnostics: Option<ResMut<SolverDiagnostics>>,
244225
) where
245226
for<'w, 's> SystemParamItem<'w, 's, H>: CollisionHooks,
246227
{
@@ -257,83 +238,9 @@ fn update_narrow_phase<C: AnyCollider, H: CollisionHooks + 'static>(
257238

258239
diagnostics.narrow_phase = start.elapsed();
259240
diagnostics.contact_count = narrow_phase.contact_graph.internal.edge_count() as u32;
260-
}
261-
262-
fn generate_constraints<C: AnyCollider>(
263-
narrow_phase: NarrowPhase<C>,
264-
mut constraints: ResMut<ContactConstraints>,
265-
contact_softness: Res<ContactSoftnessCoefficients>,
266-
time: Res<Time>,
267-
mut collision_diagnostics: ResMut<CollisionDiagnostics>,
268-
solver_diagnostics: Option<ResMut<SolverDiagnostics>>,
269-
) {
270-
let start = crate::utils::Instant::now();
271-
272-
let delta_secs = time.delta_seconds_adjusted();
273-
274-
constraints.clear();
275-
276-
// TODO: Parallelize.
277-
for (i, contacts) in narrow_phase.contact_graph.iter().enumerate() {
278-
let Ok([collider1, collider2]) = narrow_phase
279-
.collider_query
280-
.get_many([contacts.entity1, contacts.entity2])
281-
else {
282-
continue;
283-
};
284-
285-
let body1_bundle = collider1
286-
.rigid_body
287-
.and_then(|&ColliderOf { rigid_body }| narrow_phase.body_query.get(rigid_body).ok());
288-
let body2_bundle = collider2
289-
.rigid_body
290-
.and_then(|&ColliderOf { rigid_body }| narrow_phase.body_query.get(rigid_body).ok());
291-
if let (Some((body1, rb_collision_margin1)), Some((body2, rb_collision_margin2))) = (
292-
body1_bundle.map(|(body, rb_collision_margin1, _)| (body, rb_collision_margin1)),
293-
body2_bundle.map(|(body, rb_collision_margin2, _)| (body, rb_collision_margin2)),
294-
) {
295-
// At least one of the bodies must be dynamic for contact constraints
296-
// to be generated.
297-
if !body1.rb.is_dynamic() && !body2.rb.is_dynamic() {
298-
continue;
299-
}
300-
301-
// Use the collider's own collision margin if specified, and fall back to the body's
302-
// collision margin.
303-
//
304-
// The collision margin adds artificial thickness to colliders for performance
305-
// and stability. See the `CollisionMargin` documentation for more details.
306-
let collision_margin1 = collider1
307-
.collision_margin
308-
.or(rb_collision_margin1)
309-
.map_or(0.0, |margin| margin.0);
310-
let collision_margin2 = collider2
311-
.collision_margin
312-
.or(rb_collision_margin2)
313-
.map_or(0.0, |margin| margin.0);
314-
let collision_margin_sum = collision_margin1 + collision_margin2;
315-
316-
// Generate contact constraints for the computed contacts
317-
// and add them to `constraints`.
318-
narrow_phase.generate_constraints(
319-
i,
320-
contacts,
321-
&mut constraints,
322-
&body1,
323-
&body2,
324-
&collider1,
325-
&collider2,
326-
collision_margin_sum,
327-
*contact_softness,
328-
delta_secs,
329-
);
330-
}
331-
}
332-
333-
collision_diagnostics.generate_constraints = start.elapsed();
334241

335242
if let Some(mut solver_diagnostics) = solver_diagnostics {
336-
solver_diagnostics.contact_constraint_count = constraints.len() as u32;
243+
solver_diagnostics.contact_constraint_count = narrow_phase.contact_constraints.len() as u32;
337244
}
338245
}
339246

0 commit comments

Comments
 (0)