Improve performance of changing compound shapes when using Jolt Physics #101189
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
(This relates to the to-do list item in #99895 titled "Implement the deferred compound shape mentioned above".)
This PR lessens (albeit not a lot) a long-standing performance discrepancy between the Jolt Physics module and Godot Physics, where multiple additions/removals/modifications to a compound shape (i.e. an object with multiple shapes) during a single physics frame will incur a significantly heavier cost when compared to Godot Physics.
Problem
The reason for this performance discrepancy is due to several factors, but in short, there's just more work happening when adding shapes with the Jolt Physics module when compared to Godot Physics. To start with, the Jolt Physics module uses a compound shape that's better optimized for complex shape structures, called
JPH::StaticCompoundShape
, which incurs a heavier cost upon creation in order to build its spatial partitioning. On top of that, we also calculate the object's mass properties and update the broadphase on any shape addition/removal/modification, unlike Godot Physics, which instead defers these things to the next physics step1.This isn't so much a problem when you're dealing with an object that hasn't yet been added to a scene tree, since we have no real reason to (and currently do not) create/commit the underlying Jolt shape at that time, but as soon as the object is added to a scene tree we have no choice but to assume that every shape addition/removal/modification is the last one before any number of operations happen that will require an up-to-date Jolt shape, bounds and/or mass properties (such as the simulation itself) and as a result we're forced to rebuild the compound shape from scratch2 with every change.
Change
The original plan for addressing this (as discussed in jrouwe/JoltPhysics#1165) was to make a custom compound shape type that acted as a lazily built compound shape, which would within itself preserve a
JPH::StaticCompoundShapeSettings
(its "blueprint" if you will) and only actually create/commit theJPH::StaticCompoundShape
when any of the lazy compound shape's methods were invoked, such as when getting any of its properties or performing a query against it (e.g. during simulation).This however proved to be somewhat messy and error-prone in practice, since we now had to (similar to Godot Physics and its mass properties update) be mindful about always committing the shape before doing anything to the object that required its shape, bounds and/or mass properties to be up-to-date.
As a result, this PR takes a more conservative approach, by simply switching between creating a
JPH::StaticCompoundShape
and the cheaperJPH::MutableCompoundShape
depending on the context, with the latter always being "upgraded" to aJPH::StaticCompoundShape
at the next physics step. This allows us to at least avoid paying for some of the cost ofJPH::StaticCompoundShape
while also ensuring that we always have an up-to-date shape, bounds and mass properties. However, things can now also be a bit slower if you, for example, perform a physics query against an object that hasn't yet had itsJPH::MutableCompoundShape
upgraded to aJPH::StaticCompoundShape
, but I would consider this a rare edge-case.The heuristic for picking between the two compound shape types is as follows:
JPH::StaticCompoundShape
right away.JPH::MutableCompoundShape
, and queue it up to be upgraded to aJPH::StaticCompoundShape
later.There are also some other improvements included in this:
JPH::CompoundShapeSettings
, reducing the amount of memory allocations needed.JPH::TempAllocator
toJPH::StaticCompoundShapeSettings::Create
, reducing the amount of memory allocations needed.Area3D
no longer (incorrectly) builds its compound shapes twice when added to a scene tree.Results
This is a before-and-after profiling of
SceneTree::physics_process
, in a scene where aStaticBody3D
is being created every_physics_process
, with 200BoxShape3D
added to the body after it's has been added to the scene tree:For reference, here is the same scene in Godot Physics:
Note that these numbers don't include the cost you pay at the next physics step. That cost is quite small in this particular scene, with either engine, but could become more relevant as you modify more objects instead.
You may find the project here: jolt-compound-performance.zip
Footnotes
This is currently the source of some bugs, such as #75934, and in general seems quite error-prone. ↩
Note that the individual shapes themselves are still cached inbetween rebuilds. ↩
This will be the vast majority of cases for compound shapes, as this is what happens when you instantiate a scene, as opposed to programmatically creating/adding the shapes. ↩