Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve performance of changing compound shapes when using Jolt Physics #101189

Merged
merged 1 commit into from
Jan 13, 2025

Conversation

mihe
Copy link
Contributor

@mihe mihe commented Jan 6, 2025

(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 the JPH::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 cheaper JPH::MutableCompoundShape depending on the context, with the latter always being "upgraded" to a JPH::StaticCompoundShape at the next physics step. This allows us to at least avoid paying for some of the cost of JPH::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 its JPH::MutableCompoundShape upgraded to a JPH::StaticCompoundShape, but I would consider this a rare edge-case.

The heuristic for picking between the two compound shape types is as follows:

  1. If we're adding an object to a scene tree, and the object only has one shape, no compound shape is built at all (same as before) and this PR has no relevance/impact.
  2. If we're adding an object to a scene tree, and the object has multiple shapes already3, assume that no more shape changes will be made and build a JPH::StaticCompoundShape right away.
  3. If we're changing an object that's already in a scene tree, and the object only has one shape, no compound shape is built at all (same as before) and this PR has no relevance/impact.
  4. If we're changing an object that's already in a scene tree, and the object has multiple shapes, assume that more changes will be made and build a JPH::MutableCompoundShape, and queue it up to be upgraded to a JPH::StaticCompoundShape later.

There are also some other improvements included in this:

  1. We now reserve the memory used for the sub-shapes in JPH::CompoundShapeSettings, reducing the amount of memory allocations needed.
  2. We now pass a JPH::TempAllocator to JPH::StaticCompoundShapeSettings::Create, reducing the amount of memory allocations needed.
  3. 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 a StaticBody3D is being created every _physics_process, with 200 BoxShape3D added to the body after it's has been added to the scene tree:

Before
After

For reference, here is the same scene in Godot Physics:

GodotPhysics

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

  1. This is currently the source of some bugs, such as #75934, and in general seems quite error-prone.

  2. Note that the individual shapes themselves are still cached inbetween rebuilds.

  3. 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.

@mihe mihe added this to the 4.x milestone Jan 6, 2025
@mihe mihe requested a review from a team as a code owner January 6, 2025 19:08
@mihe mihe force-pushed the jolt/defer-static-compound branch from bdd8f74 to 053d924 Compare January 6, 2025 20:43
@akien-mga akien-mga modified the milestones: 4.x, 4.4 Jan 12, 2025
@akien-mga akien-mga merged commit a297198 into godotengine:master Jan 13, 2025
20 checks passed
@akien-mga
Copy link
Member

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants