Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
# Objective Fixes #564. Expands significantly on #576. For both benchmarking and optimizing Avian itself, and monitoring physics performance on the user side, it can be very useful to have timing information and various other statistics tracked by the physics engine. This is also done by engines such as Rapier and Box2D. ## Solution Summary: - Track physics timers and counters in resources like `CollisionDiagnostics` and `SolverDiagnostics`. - Add `bevy_diagnostic` feature and `PhysicsDiagnosticsPlugin` for optionally writing these diagnostics to `bevy_diagnostic::DiagnosticsStore`. - Add `diagnostic_ui` feature and `PhysicsDiagnosticsUiPlugin` for optionally displaying physics diagnostics with a debug UI. ### Physics Diagnostics Resources The natural place for diagnostics in Bevy would be `bevy_diagnostic::DiagnosticsStore`. However: - It is not suitable for tracking timers spanning across several substeps without storing a timer externally. - It likely has a small amount of additional overhead. - All measurements for Bevy's diagnostics use `f64`, which makes counters a bit more awkward. Thus, most diagnostics are tracked in separate resources such as `SolverDiagnostics`: ```rust /// Diagnostics for the physics solver. #[derive(Resource, Debug, Default, Reflect)] #[reflect(Resource, Debug)] pub struct SolverDiagnostics { /// Time spent integrating velocities. pub integrate_velocities: Duration, /// Time spent warm starting the solver. pub warm_start: Duration, /// Time spent solving constraints with bias. pub solve_constraints: Duration, /// Time spent integrating positions. pub integrate_positions: Duration, /// Time spent relaxing velocities. pub relax_velocities: Duration, /// Time spent applying restitution. pub apply_restitution: Duration, /// Time spent finalizing positions. pub finalize: Duration, /// Time spent storing impulses for warm starting. pub store_impulses: Duration, /// Time spent on swept CCD. pub swept_ccd: Duration, /// The number of contact constraints generated. pub contact_constraint_count: u32, } ``` These are updated in the relevant systems. Timers should have a very small, fixed cost, so they are currently tracked by default and *cannot* be disabled (same as Box2D), aside from disabling e.g. the `SolverPlugin` itself. If it is deemed to have measurable overhead down the line, we could try putting timers behind a feature flag, though it does add some complexity. ### Integration With `DiagnosticsStore` It can still be valuable to *also* record physics diagnostics to `bevy_diagnostic::DiagnosticsStore` to benefit from the history and smoothing functionality, and to monitor things from a single shared resource. This is supported by adding the `PhysicsDiagnosticsPlugin` with the `bevy_diagnostic` feature enabled. There is some boilerplate required for registering the diagnostics, clearing the timers and counters, and writing them to the `DiagnosticsStore`. To keep things more manageable, this has been abstracted with a `PhysicsDiagnostics` trait, `register_physics_diagnostics` method, and `impl_diagnostic_paths!` macro. For the earlier `SolverDiagnostics`, the implementation looks like this: ```rust impl PhysicsDiagnostics for SolverDiagnostics { fn timer_paths(&self) -> Vec<(&'static DiagnosticPath, Duration)> { vec![ (Self::INTEGRATE_VELOCITIES, self.integrate_velocities), (Self::WARM_START, self.warm_start), (Self::SOLVE_CONSTRAINTS, self.solve_constraints), (Self::INTEGRATE_POSITIONS, self.integrate_positions), (Self::RELAX_VELOCITIES, self.relax_velocities), (Self::APPLY_RESTITUTION, self.apply_restitution), (Self::FINALIZE, self.finalize), (Self::STORE_IMPULSES, self.store_impulses), (Self::SWEPT_CCD, self.swept_ccd), ] } fn counter_paths(&self) -> Vec<(&'static DiagnosticPath, u32)> { vec![( Self::CONTACT_CONSTRAINT_COUNT, self.contact_constraint_count, )] } } impl_diagnostic_paths! { impl SolverDiagnostics { INTEGRATE_VELOCITIES: "avian/solver/integrate_velocities", WARM_START: "avian/solver/warm_start", SOLVE_CONSTRAINTS: "avian/solver/solve_constraints", INTEGRATE_POSITIONS: "avian/solver/integrate_positions", RELAX_VELOCITIES: "avian/solver/relax_velocities", APPLY_RESTITUTION: "avian/solver/apply_restitution", FINALIZE: "avian/solver/finalize", STORE_IMPULSES: "avian/solver/store_impulses", SWEPT_CCD: "avian/solver/swept_ccd", CONTACT_CONSTRAINT_COUNT: "avian/solver/contact_constraint_count", } } ``` The `SolverPlugin` can then simply call `app.register_physics_diagnostics::<SolverDiagnostics>()`, and everything should work automatically. The timers will only be written to the `DiagnosticsStore` if `PhysicsDiagnosticsPlugin` is enabled, keeping overhead small if the use of `DiagnosticsStore` is not needed. A nice benefit here is that each plugin is responsible for adding its own diagnostics, rather than there being a single place where all diagnostics are registered and stored. This is nice for modularity, and means that e.g. `SpatialQueryDiagnostics` are only added if the `SpatialQueryPlugin` is added. ### Physics Diagnostics UI Having all of these diagnostics available is nice, but viewing and displaying them in a useful way involves a decent amount of code and effort. To make this easier (and prettier), an optional debug UI for displaying physics diagnostics is provided with the `diagnostic_ui` feature and `PhysicsDiagnosticsUiPlugin`. It displays all active built-in physics diagnostics in neat groups, with both current and smoothed times shown.  The visibility and settings of the UI can be configured using the `PhysicsDiagnosticsUiSettings`. ### Example Improvements The `ExampleCommonPlugin` has been updated to replace the FPS counter with these physics diagnostics, and there are now text instructions to show what the different keys do.  The diagnostics UI is hidden by default in the examples. ## Differences to #576 #576 by @ptsd has an initial WIP diagnostics implementation with a simpler approach that more closely matches my original proposal in #564. It was a valuable base to build this PR on top of, but as I iterated on it, I ultimately went with quite a different approach. That PR used a single `PhysicsDiagnosticsPlugin` that set up all diagnostics manually. Timers were implemented by adding system sets before and after various parts of the simulation, and adding systems there to record spans, which were then written to the `DiagnosticsStore`. I didn't go with this approach, because: - Adding so many system sets just for diagnostics didn't feel very appealing - Adding two systems for each span adds more overhead, and might not provide as accurate timing information as just tracking time inside the actual systems - The spans in that PR were not suitable for substepping, as they didn't support accumulating time over several substeps - Registering all diagnostics in a single plugin makes things less modular, and means that you might end up with unnecessary diagnostics Instead, I simply have each plugin define its own resource for its diagnostics where relevant. Timers are handled by tracking elapsed time inside systems with `bevy::utils::Instant` (for wide platform support), and stored as `Duration`s. Writing this information to the `DiagnosticsStore` is optional. This is more modular, has less overhead, and works with substepping. It does add some complexity to the actual diagnostic management though, and makes diagnostics more spread out over the codebase, for better and for worse. --------- Co-authored-by: Patrick Dobbs <[email protected]>
- Loading branch information