1use bevy::diagnostic::{
4 Diagnostic, DiagnosticPath, Diagnostics, DiagnosticsStore, RegisterDiagnostic,
5};
6use bevy::ecs::entity::EntityHashSet;
7use bevy::ecs::{intern::Interned, schedule::ScheduleLabel};
8use bevy::prelude::*;
9
10use crate::plugin::context::{
11 RapierContextColliders, RapierContextJoints, RapierContextSimulation, RapierRigidBodySet,
12};
13use crate::plugin::PhysicsSet;
14
15pub struct RapierDiagnosticsPlugin {
32 schedule: Interned<dyn ScheduleLabel>,
33 pub max_history_length: usize,
35 pub per_context: bool,
38}
39
40impl Default for RapierDiagnosticsPlugin {
41 fn default() -> Self {
42 Self {
43 schedule: PostUpdate.intern(),
44 max_history_length: bevy::diagnostic::DEFAULT_MAX_HISTORY_LENGTH,
45 per_context: false,
46 }
47 }
48}
49
50impl RapierDiagnosticsPlugin {
51 pub const STEP_TIME: DiagnosticPath = DiagnosticPath::const_new("rapier/step_time");
54 pub const STEPS: DiagnosticPath = DiagnosticPath::const_new("rapier/steps");
56 pub const COLLISION_DETECTION_TIME: DiagnosticPath =
58 DiagnosticPath::const_new("rapier/collision_detection_time");
59 pub const BROAD_PHASE_TIME: DiagnosticPath =
61 DiagnosticPath::const_new("rapier/broad_phase_time");
62 pub const NARROW_PHASE_TIME: DiagnosticPath =
64 DiagnosticPath::const_new("rapier/narrow_phase_time");
65 pub const ISLAND_CONSTRUCTION_TIME: DiagnosticPath =
68 DiagnosticPath::const_new("rapier/island_construction_time");
69 pub const SOLVER_TIME: DiagnosticPath = DiagnosticPath::const_new("rapier/solver_time");
71 pub const CCD_TIME: DiagnosticPath = DiagnosticPath::const_new("rapier/ccd_time");
74 pub const UPDATE_TIME: DiagnosticPath = DiagnosticPath::const_new("rapier/update_time");
77 pub const CCD_SUBSTEPS: DiagnosticPath = DiagnosticPath::const_new("rapier/ccd_substeps");
79 pub const RIGID_BODIES: DiagnosticPath = DiagnosticPath::const_new("rapier/rigid_bodies");
81 pub const ACTIVE_BODIES: DiagnosticPath = DiagnosticPath::const_new("rapier/active_bodies");
83 pub const COLLIDERS: DiagnosticPath = DiagnosticPath::const_new("rapier/colliders");
85 pub const IMPULSE_JOINTS: DiagnosticPath = DiagnosticPath::const_new("rapier/impulse_joints");
87 pub const MULTIBODY_JOINTS: DiagnosticPath =
89 DiagnosticPath::const_new("rapier/multibody_joints");
90 pub const CONTACT_PAIRS: DiagnosticPath = DiagnosticPath::const_new("rapier/contact_pairs");
92 pub const ACTIVE_CONTACT_PAIRS: DiagnosticPath =
94 DiagnosticPath::const_new("rapier/active_contact_pairs");
95 pub const CONTACT_CONSTRAINTS: DiagnosticPath =
98 DiagnosticPath::const_new("rapier/contact_constraints");
99 pub const SOLVER_CONSTRAINTS: DiagnosticPath =
102 DiagnosticPath::const_new("rapier/solver_constraints");
103 pub const INTERSECTIONS: DiagnosticPath = DiagnosticPath::const_new("rapier/intersections");
105
106 const ALWAYS_MEASURED: [(DiagnosticPath, &'static str); 13] = [
108 (Self::STEP_TIME, "ms"),
109 (Self::STEPS, ""),
110 (Self::CCD_SUBSTEPS, ""),
111 (Self::RIGID_BODIES, ""),
112 (Self::ACTIVE_BODIES, ""),
113 (Self::COLLIDERS, ""),
114 (Self::IMPULSE_JOINTS, ""),
115 (Self::MULTIBODY_JOINTS, ""),
116 (Self::CONTACT_PAIRS, ""),
117 (Self::ACTIVE_CONTACT_PAIRS, ""),
118 (Self::CONTACT_CONSTRAINTS, ""),
119 (Self::SOLVER_CONSTRAINTS, ""),
120 (Self::INTERSECTIONS, ""),
121 ];
122
123 #[cfg(feature = "profiler")]
125 const PROFILER_TIMINGS: [DiagnosticPath; 7] = [
126 Self::COLLISION_DETECTION_TIME,
127 Self::BROAD_PHASE_TIME,
128 Self::NARROW_PHASE_TIME,
129 Self::ISLAND_CONSTRUCTION_TIME,
130 Self::SOLVER_TIME,
131 Self::CCD_TIME,
132 Self::UPDATE_TIME,
133 ];
134
135 pub fn in_schedule(mut self, schedule: impl ScheduleLabel) -> Self {
140 self.schedule = schedule.intern();
141 self
142 }
143
144 pub fn with_per_context_diagnostics(mut self, per_context: bool) -> Self {
146 self.per_context = per_context;
147 self
148 }
149
150 pub fn context_path(path: &DiagnosticPath, context: Entity) -> DiagnosticPath {
155 let name = path.as_str();
156 let name = name.strip_prefix("rapier/").unwrap_or(name);
157 DiagnosticPath::new(format!("rapier/{context}/{name}"))
158 }
159
160 fn measured_paths() -> impl Iterator<Item = (DiagnosticPath, &'static str)> {
162 let always = Self::ALWAYS_MEASURED.into_iter();
163 #[cfg(feature = "profiler")]
164 let always = always.chain(Self::PROFILER_TIMINGS.into_iter().map(|path| (path, "ms")));
165 always
166 }
167
168 fn measure_context(
171 simulation: &mut Mut<RapierContextSimulation>,
172 colliders: &RapierContextColliders,
173 bodies: &RapierRigidBodySet,
174 joints: &RapierContextJoints,
175 ) -> Option<Vec<(DiagnosticPath, f64)>> {
176 if !simulation.pipeline.counters.enabled() {
177 simulation
178 .bypass_change_detection()
179 .pipeline
180 .counters
181 .enable();
182 }
183
184 let stats = *simulation.step_stats();
185 if stats.num_steps == 0 {
186 return None;
187 }
188
189 #[allow(unused_mut)] let mut measurements = vec![
191 (Self::STEP_TIME, stats.step_time_ms),
192 (Self::STEPS, stats.num_steps as f64),
193 (Self::CCD_SUBSTEPS, stats.ccd_substeps as f64),
194 (Self::RIGID_BODIES, bodies.bodies.len() as f64),
195 (
196 Self::ACTIVE_BODIES,
197 simulation.islands.active_bodies().count() as f64,
198 ),
199 (Self::COLLIDERS, colliders.colliders.len() as f64),
200 (Self::IMPULSE_JOINTS, joints.impulse_joints.len() as f64),
201 (
202 Self::MULTIBODY_JOINTS,
203 joints.multibody_joints.iter().count() as f64,
204 ),
205 (Self::CONTACT_PAIRS, stats.num_contact_pairs as f64),
206 (
207 Self::ACTIVE_CONTACT_PAIRS,
208 simulation
209 .narrow_phase
210 .contact_pairs()
211 .filter(|pair| pair.has_any_active_contact())
212 .count() as f64,
213 ),
214 (Self::CONTACT_CONSTRAINTS, stats.num_solver_contacts as f64),
215 (
216 Self::SOLVER_CONSTRAINTS,
217 stats.num_solver_constraints as f64,
218 ),
219 (
220 Self::INTERSECTIONS,
221 simulation
222 .narrow_phase
223 .intersection_pairs()
224 .filter(|(_, _, intersecting)| *intersecting)
225 .count() as f64,
226 ),
227 ];
228 #[cfg(feature = "profiler")]
229 measurements.extend([
230 (
231 Self::COLLISION_DETECTION_TIME,
232 stats.collision_detection_time_ms,
233 ),
234 (Self::BROAD_PHASE_TIME, stats.broad_phase_time_ms),
235 (Self::NARROW_PHASE_TIME, stats.narrow_phase_time_ms),
236 (
237 Self::ISLAND_CONSTRUCTION_TIME,
238 stats.island_construction_time_ms,
239 ),
240 (Self::SOLVER_TIME, stats.solver_time_ms),
241 (Self::CCD_TIME, stats.ccd_time_ms),
242 (Self::UPDATE_TIME, stats.update_time_ms),
243 ]);
244 Some(measurements)
245 }
246
247 fn diagnostic_system(
252 per_context: bool,
253 max_history_length: usize,
254 commands: &mut Commands,
255 diagnostics: &mut Diagnostics,
256 contexts: &mut Query<(
257 Entity,
258 &mut RapierContextSimulation,
259 &RapierContextColliders,
260 &RapierRigidBodySet,
261 &RapierContextJoints,
262 )>,
263 registered: &mut EntityHashSet,
264 ) {
265 let mut totals: Option<Vec<(DiagnosticPath, f64)>> = None;
266
267 for (context, mut simulation, colliders, bodies, joints) in contexts.iter_mut() {
268 let Some(measurements) =
269 Self::measure_context(&mut simulation, colliders, bodies, joints)
270 else {
271 continue;
272 };
273
274 if per_context {
275 if registered.insert(context) {
276 commands.queue(move |world: &mut World| {
277 let mut store = world.resource_mut::<DiagnosticsStore>();
278 for (path, suffix) in Self::measured_paths() {
279 store.add(
280 Diagnostic::new(Self::context_path(&path, context))
281 .with_suffix(suffix)
282 .with_max_history_length(max_history_length),
283 );
284 }
285 });
286 }
287 for (path, value) in &measurements {
288 diagnostics.add_measurement(&Self::context_path(path, context), || *value);
289 }
290 }
291
292 match &mut totals {
293 Some(totals) => {
294 for ((_, total), (_, value)) in totals.iter_mut().zip(&measurements) {
295 *total += value;
296 }
297 }
298 None => totals = Some(measurements),
299 }
300 }
301
302 for (path, total) in totals.into_iter().flatten() {
303 diagnostics.add_measurement(&path, || total);
304 }
305 }
306}
307
308impl Plugin for RapierDiagnosticsPlugin {
309 fn build(&self, app: &mut App) {
310 for (path, suffix) in Self::measured_paths() {
311 app.register_diagnostic(
312 Diagnostic::new(path)
313 .with_suffix(suffix)
314 .with_max_history_length(self.max_history_length),
315 );
316 }
317
318 let per_context = self.per_context;
319 let max_history_length = self.max_history_length;
320 app.add_systems(
321 self.schedule,
322 (move |mut commands: Commands,
323 mut diagnostics: Diagnostics,
324 mut contexts: Query<(
325 Entity,
326 &mut RapierContextSimulation,
327 &RapierContextColliders,
328 &RapierRigidBodySet,
329 &RapierContextJoints,
330 )>,
331 mut registered: Local<EntityHashSet>| {
332 Self::diagnostic_system(
333 per_context,
334 max_history_length,
335 &mut commands,
336 &mut diagnostics,
337 &mut contexts,
338 &mut registered,
339 )
340 })
341 .after(PhysicsSet::Writeback),
342 );
343 }
344}