1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
//! The `unsync-callbacks` contract: a `PhysicsHooks` / `EventHandler` that is **not** `Sync`
//! compiles and runs, whatever `parallel` is doing.
//!
//! Most of the value here is that this file compiles at all: the hook and the handler
//! below hold a `Cell`, so `Sync` is not satisfied and any `Sync` bound creeping back onto
//! either trait turns into a build error. The assertions then confirm the callbacks were
//! actually reached rather than silently skipped.
//!
//! Run with `--features parallel,unsync-callbacks` for the combination that matters — the
//! engine has to keep the callbacks on the thread driving the step while the solver stays
//! threaded.
#![cfg(feature = "unsync-callbacks")]
use rapier3d::prelude::*;
use std::cell::Cell;
/// Counts its own invocations through a `Cell`, which is `Send` but not `Sync`.
#[derive(Default)]
struct UnsyncHooks {
filtered: Cell<usize>,
}
impl PhysicsHooks for UnsyncHooks {
fn filter_contact_pair(&self, _: &PairFilterContext) -> Option<SolverFlags> {
self.filtered.set(self.filtered.get() + 1);
Some(SolverFlags::COMPUTE_IMPULSES)
}
}
#[derive(Default)]
struct UnsyncEvents {
collisions: Cell<usize>,
}
impl EventHandler for UnsyncEvents {
fn handle_collision_event(
&self,
_bodies: &RigidBodySet,
_colliders: &ColliderSet,
_event: CollisionEvent,
_contact_pair: Option<&ContactPair>,
) {
self.collisions.set(self.collisions.get() + 1);
}
fn handle_contact_force_event(
&self,
_dt: Real,
_bodies: &RigidBodySet,
_colliders: &ColliderSet,
_contact_pair: &ContactPair,
_total_force_magnitude: Real,
) {
}
}
/// Enough falling boxes to push the step onto its parallel paths where `parallel` is on.
fn scene() -> (RigidBodySet, ColliderSet, IslandManager) {
let mut bodies = RigidBodySet::new();
let mut colliders = ColliderSet::new();
colliders.insert(
ColliderBuilder::cuboid(100.0, 1.0, 100.0)
.translation(Vector::new(0.0, -1.0, 0.0))
.active_hooks(ActiveHooks::FILTER_CONTACT_PAIRS)
.active_events(ActiveEvents::COLLISION_EVENTS)
.build(),
);
for i in 0..8 {
for j in 0..8 {
for k in 0..8 {
let rb = bodies.insert(
RigidBodyBuilder::dynamic()
.translation(Vector::new(
i as Real * 1.1 - 4.0,
j as Real * 1.1 + 1.0,
k as Real * 1.1 - 4.0,
))
.build(),
);
colliders.insert_with_parent(
ColliderBuilder::cuboid(0.5, 0.5, 0.5)
.active_hooks(ActiveHooks::FILTER_CONTACT_PAIRS)
.active_events(ActiveEvents::COLLISION_EVENTS)
.build(),
rb,
&mut bodies,
);
}
}
}
(bodies, colliders, IslandManager::new())
}
#[test]
fn non_sync_hooks_and_events_are_invoked() {
let (mut bodies, mut colliders, mut islands) = scene();
let mut pipeline = PhysicsPipeline::new();
let mut broad_phase = BroadPhaseBvh::new();
let mut narrow_phase = NarrowPhase::new();
let mut impulse_joints = ImpulseJointSet::new();
let mut multibody_joints = MultibodyJointSet::new();
let mut ccd_solver = CCDSolver::new();
let params = IntegrationParameters::default();
let hooks = UnsyncHooks::default();
let events = UnsyncEvents::default();
for _ in 0..60 {
pipeline.step(
Vector::new(0.0, -9.81, 0.0),
¶ms,
&mut islands,
&mut broad_phase,
&mut narrow_phase,
&mut bodies,
&mut colliders,
&mut impulse_joints,
&mut multibody_joints,
&mut ccd_solver,
&hooks,
&events,
);
}
assert!(
hooks.filtered.get() > 0,
"filter_contact_pair was never called"
);
assert!(
events.collisions.get() > 0,
"handle_collision_event was never called"
);
}
/// A dedicated thread pool is still usable under `unsync-callbacks` — the caller enters it
/// themselves, since the pipeline's own pool API is compiled out along with the bound.
///
/// `ThreadPool::install` runs its body on the calling thread whenever that thread is already
/// a member of the same pool (rayon's `Registry::in_worker` only migrates for outsiders), and
/// every nested rayon construct then targets that pool. So stepping from inside the pool
/// gives the pool's threads *and* keeps the callbacks on one known thread. The callback is
/// built inside the closure because `install` needs its body to be `Send`, which a
/// thread-affine callback is not.
#[cfg(feature = "parallel")]
#[test]
fn dedicated_pool_is_usable_when_the_caller_enters_it() {
let pool = rapier3d::rayon::ThreadPoolBuilder::new()
.num_threads(3)
.build()
.unwrap();
let (hook_calls, workers) = pool.install(|| {
// Built here: nothing non-`Send` crosses into the pool.
let hooks = UnsyncHooks::default();
let events = UnsyncEvents::default();
let (mut bodies, mut colliders, mut islands) = scene();
let mut pipeline = PhysicsPipeline::new();
let mut broad_phase = BroadPhaseBvh::new();
let mut narrow_phase = NarrowPhase::new();
let mut impulse_joints = ImpulseJointSet::new();
let mut multibody_joints = MultibodyJointSet::new();
let mut ccd_solver = CCDSolver::new();
let params = IntegrationParameters::default();
for _ in 0..30 {
pipeline.step(
Vector::new(0.0, -9.81, 0.0),
¶ms,
&mut islands,
&mut broad_phase,
&mut narrow_phase,
&mut bodies,
&mut colliders,
&mut impulse_joints,
&mut multibody_joints,
&mut ccd_solver,
&hooks,
&events,
);
}
// Reports the pool this thread currently belongs to — which is what we want to
// observe: the step's parallel regions went to our pool, not the global one.
(hooks.filtered.get(), rapier3d::rayon::current_num_threads())
});
assert_eq!(workers, 3, "the step ran against the global pool, not ours");
assert!(hook_calls > 0, "filter_contact_pair was never called");
}