avian2d/diagnostics/mod.rs
1//! Diagnostics support for tracking physics timers and counters. Useful for profiling and debugging.
2//!
3//! # Overview
4//!
5//! Each physics plugin such as [`NarrowPhasePlugin`] and [`SolverPlugin`] is responsible
6//! for implementing its own diagnostics resource using the [`PhysicsDiagnostics`] trait
7//! and registering it using [`AppDiagnosticsExt::register_physics_diagnostics`].
8//!
9//! If the `bevy_diagnostic` feature is enabled and the [`PhysicsDiagnosticsPlugin`] is added to the app,
10//! these diagnostics will also be automatically written to the [`DiagnosticsStore`] resource.
11//!
12//! If the `diagnostic_ui` feature is enabled and the [`PhysicsDiagnosticsUiPlugin`] is added to the app,
13//! a debug UI will also be available for displaying these diagnostics in real-time.
14//!
15//! [`NarrowPhasePlugin`]: crate::collision::narrow_phase::NarrowPhasePlugin
16//! [`SolverPlugin`]: crate::dynamics::solver::SolverPlugin
17//! [`DiagnosticsStore`]: bevy::diagnostic::DiagnosticsStore
18//! [`PhysicsDiagnosticsUiPlugin`]: crate::diagnostics::ui::PhysicsDiagnosticsUiPlugin
19//!
20//! # Example
21//!
22//! ```no_run
23#![cfg_attr(feature = "2d", doc = "use avian2d::prelude::*;")]
24#![cfg_attr(feature = "3d", doc = "use avian3d::prelude::*;")]
25//! use bevy::prelude::*;
26//!
27//! fn main() {
28//! App::new()
29//! .add_plugins((
30//! DefaultPlugins,
31//! PhysicsPlugins::default(),
32//! // Add the `PhysicsDiagnosticsPlugin` to write physics diagnostics
33//! // to the `DiagnosticsStore` resource in `bevy_diagnostic`.
34//! // Requires the `bevy_diagnostic` feature.
35//! # #[cfg(feature = "bevy_diagnostic")]
36//! PhysicsDiagnosticsPlugin,
37//! // Add the `PhysicsDiagnosticsUiPlugin` to display physics diagnostics
38//! // in a debug UI. Requires the `diagnostic_ui` feature.
39//! # #[cfg(feature = "diagnostic_ui")]
40//! PhysicsDiagnosticsUiPlugin,
41//! ))
42//! // ...your other plugins, systems and resources
43//! .run();
44//! }
45//! ```
46//!
47//! # Supported Diagnostics
48//!
49//! The following diagnostics are available but not added by default:
50//!
51//! - [`PhysicsTotalDiagnostics`]: Total physics timers and counters.
52//! - [`PhysicsEntityDiagnostics`]: Physics entity counters.
53//!
54//! Additionally, physics plugins may implement their own diagnostics that *are* enabled by default.
55//! These include:
56//!
57//! - [`CollisionDiagnostics`]: Diagnostics for collision detection.
58//! - [`SolverDiagnostics`]: Diagnostics for the physics solver.
59//! - [`SpatialQueryDiagnostics`]: Diagnostics for spatial queries.
60//! - [`PhysicsPickingDiagnostics`]: Diagnostics for physics picking.
61//!
62//! [`CollisionDiagnostics`]: crate::collision::CollisionDiagnostics
63//! [`SolverDiagnostics`]: crate::dynamics::solver::SolverDiagnostics
64//! [`SpatialQueryDiagnostics`]: crate::spatial_query::SpatialQueryDiagnostics
65//! [`PhysicsPickingDiagnostics`]: crate::picking::PhysicsPickingDiagnostics
66
67#[cfg(feature = "bevy_diagnostic")]
68mod entity_counters;
69mod path_macro;
70#[cfg(feature = "bevy_diagnostic")]
71mod total;
72
73#[cfg(feature = "diagnostic_ui")]
74pub mod ui;
75#[cfg(feature = "bevy_diagnostic")]
76pub use entity_counters::{PhysicsEntityDiagnostics, PhysicsEntityDiagnosticsPlugin};
77pub(crate) use path_macro::impl_diagnostic_paths;
78#[cfg(feature = "bevy_diagnostic")]
79pub use total::{PhysicsTotalDiagnostics, PhysicsTotalDiagnosticsPlugin};
80
81use crate::{PhysicsStepSystems, schedule::PhysicsSchedule};
82use bevy::{
83 diagnostic::DiagnosticPath,
84 prelude::{App, IntoScheduleConfigs, ResMut, Resource, SystemSet},
85};
86#[cfg(feature = "bevy_diagnostic")]
87use bevy::{
88 diagnostic::{Diagnostic, Diagnostics, RegisterDiagnostic},
89 prelude::{Plugin, Res},
90};
91use core::time::Duration;
92
93/// A plugin that enables writing [physics diagnostics](crate::diagnostics)
94/// to [`bevy::diagnostic::DiagnosticsStore`]. It is not enabled by default
95/// and must be added manually.
96///
97/// To add a debug UI for physics diagnostics, enable the `diagnostic_ui` feature, and add the
98/// [`PhysicsDiagnosticsUiPlugin`] to your app.
99///
100/// See the [module-level documentation](crate::diagnostics) for more information.
101///
102/// [`PhysicsDiagnosticsUiPlugin`]: crate::diagnostics::ui::PhysicsDiagnosticsUiPlugin
103#[cfg(feature = "bevy_diagnostic")]
104pub struct PhysicsDiagnosticsPlugin;
105
106#[cfg(feature = "bevy_diagnostic")]
107impl Plugin for PhysicsDiagnosticsPlugin {
108 fn build(&self, app: &mut App) {
109 // Configure system sets for physics diagnostics.
110 app.configure_sets(
111 PhysicsSchedule,
112 (
113 PhysicsDiagnosticsSystems::Reset.before(PhysicsStepSystems::First),
114 PhysicsDiagnosticsSystems::WriteDiagnostics.after(PhysicsStepSystems::Last),
115 ),
116 );
117 }
118}
119
120/// A system set for [physics diagnostics](crate::diagnostics).
121#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)]
122pub enum PhysicsDiagnosticsSystems {
123 /// Resets diagnostics to their default values.
124 Reset,
125 /// Writes physics diagnostics to other resources, commonly `DiagnosticsStore`.
126 WriteDiagnostics,
127}
128
129/// A trait for resources storing timers and counters for [physics diagnostics](crate::diagnostics).
130pub trait PhysicsDiagnostics: Default + Resource {
131 /// Maps diagnostic paths to their respective duration fields.
132 fn timer_paths(&self) -> Vec<(&'static DiagnosticPath, Duration)> {
133 Vec::new()
134 }
135
136 /// Maps diagnostic paths to their respective counter fields.
137 fn counter_paths(&self) -> Vec<(&'static DiagnosticPath, u32)> {
138 Vec::new()
139 }
140
141 /// A system that resets the diagnostics to their default values.
142 fn reset(mut physics_diagnostics: ResMut<Self>) {
143 *physics_diagnostics = Self::default();
144 }
145
146 /// A system that writes diagnostics to the given [`Diagnostics`] instance.
147 #[cfg(feature = "bevy_diagnostic")]
148 fn write_diagnostics(physics_diagnostics: Res<Self>, mut diagnostics: Diagnostics) {
149 for (path, duration) in physics_diagnostics.timer_paths() {
150 diagnostics.add_measurement(path, || duration.as_secs_f64() * 1000.0);
151 }
152
153 for (path, count) in physics_diagnostics.counter_paths() {
154 diagnostics.add_measurement(path, || count as f64);
155 }
156 }
157}
158
159/// An extension trait for registering [physics diagnostics](crate::diagnostics) in an [`App`].
160pub trait AppDiagnosticsExt {
161 /// Registers timer and counter diagnostics for a resource implementing [`PhysicsDiagnostics`].
162 ///
163 /// This method should be called in [`Plugin::finish`] to ensure that [`Diagnostic`]s
164 /// are only tracked if the [`PhysicsDiagnosticsPlugin`] was added to the app.
165 fn register_physics_diagnostics<T: PhysicsDiagnostics>(&mut self);
166}
167
168impl AppDiagnosticsExt for App {
169 fn register_physics_diagnostics<T: PhysicsDiagnostics>(&mut self) {
170 // Avoid duplicate registrations.
171 if self.world().is_resource_added::<T>() {
172 return;
173 }
174
175 // Initialize the diagnostics resource.
176 self.init_resource::<T>();
177
178 // Make sure the system set exists, even if `PhysicsDiagnosticsPlugin` is not added.
179 self.configure_sets(
180 PhysicsSchedule,
181 PhysicsDiagnosticsSystems::Reset.before(PhysicsStepSystems::First),
182 );
183
184 // Add a system to reset the resource, even if `PhysicsDiagnosticsPlugin` is not added.
185 self.add_systems(
186 PhysicsSchedule,
187 T::reset
188 .in_set(PhysicsDiagnosticsSystems::Reset)
189 .ambiguous_with_all(),
190 );
191
192 #[cfg(feature = "bevy_diagnostic")]
193 {
194 // If physics diagnostics are not enabled, return early.
195 if !self.is_plugin_added::<PhysicsDiagnosticsPlugin>() {
196 return;
197 }
198
199 // Register diagnostics for the paths returned by the diagnostics resource.
200 let diagnostics = T::default();
201 let timer_paths = diagnostics.timer_paths();
202 let counter_paths = diagnostics.counter_paths();
203
204 for path in timer_paths.iter().map(|(path, _)| *path) {
205 // All timers are in milliseconds.
206 self.register_diagnostic(Diagnostic::new(path.clone()).with_suffix("ms"));
207 }
208
209 for path in counter_paths.iter().map(|(path, _)| *path) {
210 // All counters are in whole numbers.
211 self.register_diagnostic(Diagnostic::new(path.clone()).with_smoothing_factor(0.0));
212 }
213
214 // Add systems to reset the diagnostics and write them to the `DiagnosticsStore` resource.
215 self.add_systems(
216 PhysicsSchedule,
217 T::write_diagnostics
218 .in_set(PhysicsDiagnosticsSystems::WriteDiagnostics)
219 .ambiguous_with_all(),
220 );
221 }
222 }
223}