Repository: godotengine/godot (language: cpp)

Issue:
StaticBody3D detection instability in JoltPhysics
### Tested versions

Godot v4.4.beta1, regression from 4.3 stable with Jolt addon.

### System information

Windows 10 (build 19044) - Single-window, 1 monitor - Vulkan (Forward+) - dedicated NVIDIA GeForce RTX 3080 (NVIDIA; 32.0.15.6636) - AMD Ryzen 9 5950X 16-Core Processor (32 threads)

### Issue description

I have two scenes that work as parametrized stair and ladder generators. They both use most of the same code, with the stairs generated as arranged StaticBody3Ds, and ladders generated as arranged CSGCylinders with collision enabled.

In 4.3 stable with the Jolt addon, using an Area3D with "Areas Detect Static Bodies" enabled in settings gave mostly reliable results, as the Area3D came into proximity with the ladders and stairs they triggered the enter and exit signals with reasonable stability. The ladders give phantom exit signals when they shouldn't, but these only occur when the Area3D is moved, so they do not impact performance very much.

However, in 4.4, while the ladder bodies (composed of CSGCylinders) detect the same way, but the steps made of StaticBodies all enter and exit repeatedly every  frame, adding a massive performance drop in my project whenever they are encountered by my player.

### Steps to reproduce

For the purposes of demonstration, I have arranged my ladder constructor, an ordinary StaticBody, and my stairs constructor side by side.

![Image](https://github.com/user-attachments/assets/f6d9ce31-c154-4bee-b585-9ddce4f6bc00)

In both projects, pressing "A" or "D" on the keyboard will move the Area3D, and the game will print entrances and exits to output.


### Minimal reproduction project (MRP)

Expected behavior in 4.3 can be seen with [jolt-staticbody-mrp-4.3.zip](https://github.com/user-attachments/files/18459848/jolt-staticbody-mrp-4.3.zip), addon is included.
Problem can be seen in 4.4 with [jolt-staticbody-mrp-4.4.zip](https://github.com/user-attachments/files/18465925/jolt-staticbody-mrp-4.4.zip).
[4.4 Beta 1] Toggling the disabled property on CollisionShape3D with Jolt Physics will spam the area_entered signal
### Tested versions

Occurs on:
- v4.4.beta1.mono.official [d33da79d3]

Does not occur on:
- v4.4.dev7.mono.official [46c8f8c5c]

### System information

Godot v4.4.beta1.mono - SteamOS holo on X11 - X11 display driver, Multi-window, 1 monitor - Vulkan (Forward+) - integrated AMD Custom GPU 0405 (RADV VANGOGH) - AMD Custom APU 0405 (8 threads)

### Issue description

When the disabled property on CollisionShape3D is set from false to true while using Jolt Physics, any collision that happens shortly after will spam the area_entered signal. 

### Steps to reproduce

1. Enable Jolt Physics from the Project Settings (Physics>3D)
2. Add multiple Area3D nodes with CollisionShape3D nodes as children (Mesh3D as well for visibility)
3. Add a script that will move the shapes through each other
4. Run the project
5. During runtime, toggle the Disabled property on the CollisionShape3D in the inspector

Note: You can script the disabled toggle behaviour as well, but through the inspector is how I reproduce it in the test project.

### Minimal reproduction project (MRP)

[CollisionShape3DDisabledPropertyJolt.zip](https://github.com/user-attachments/files/18456599/CollisionShape3DDisabledPropertyJolt.zip)


Discussion / hints:
The stairs thing looks to be another regression caused by #100983, but I haven't looked into why just yet.

> The ladders give phantom exit signals when they shouldn't, but these only occur when the Area3D is moved, so they do not impact performance very much.

This seems somewhat problematic, and was not something I was aware of. I'll see about figuring out why this is happening as well.
I updated the 4.4 MRP since it had broken dependencies.
Actually, it looks like the ladders' phantom exit problem might be fixed in the beta.
I upgraded your 4.3 MRP to 4.4 instead, and I'm still (on latest `master`) seeing some weird behavior with regards to the ladder's area events, where it'll spam a bunch of exit and enter events when no `CSGCylinder` have seemingly exited the `Area3D`.

Also, while this is irrelevant to the bug at hand, I should probably point out that this approach in general isn't great from a performance standpoint with Jolt, as collision for CSG shapes are done using `ConcavePolygonShape3D` under the hood, so with enough CSG shapes (or if you crank up their tessellation) you can end up falling into the performance pitfall that's mentioned in the tooltip of the "Areas Detect Static Bodies" project setting:

![Image](https://github.com/user-attachments/assets/6571bd14-0387-4230-8518-8b81c4d3e006)

I should probably add a mention of CSG shapes in there actually.
I appreciate your warning, I do intend to replace the ladder constructor with similar code that uses a CylinderShape3D / MeshInstance system like the stairs do, I never got around to it for some reason. Hopefully the minor difference in approach between the two goes some way in helping diagnose the root cause of this.

Also, while I understand the potential performance cost of this aspect of Jolt, I do worry about this setting defaulting to false [confusing](https://www.reddit.com/r/godot/comments/18shih8/jolt_area3d_not_colliding_with_static_bodies/) [people](https://www.reddit.com/r/godot/comments/17eu12u/comment/k666t5v/), especially with the parity Jolt aims for with GodotPhysics in every other regard. I know I was puzzled for a while. Would it not be possible to make this setting per-Area3D, to better expose it to the user, and to further narrow its performance cost?
> Would it not be possible to make this setting per-Area3D, to better expose it to the user, and to further narrow its performance cost?

Yes, that's the plan. I just need to sit down and finish the server-provided node properties functionality, which has been on my to-do list for a while now, so that we can add more Jolt-specific stuff like this without burdening the shared interfaces.

But yes, I know this setting is confusing and has become something that most people just enable without thinking much about, if they can find it in the first place. Not having in-editor documentation for it in the extension probably didn't help either.

I've sort of been hoping to see issue reports about the performance impact of enabling this setting, just so I can get a sense for how it's affecting real projects out there, but I suspect it's difficult to corelate the two once you do find yourself in trouble.
CC @mihe 
Looks to be a regression from #100983. I'll take a closer look.
Thankfully it turned out to be a silly one-liner fix: #101727

I appreciate the repro, @This-Gooch. I get the sense that this one might not have been easy for you to boil down.
Thanks a bunch! Was not too bad to track down since my current pipeline was pretty narrow. 

Fix patch:
diff --git a/modules/jolt_physics/objects/jolt_area_3d.cpp b/modules/jolt_physics/objects/jolt_area_3d.cpp
index eeaeca0faa1e..7b8b4b59ca06 100644
--- a/modules/jolt_physics/objects/jolt_area_3d.cpp
+++ b/modules/jolt_physics/objects/jolt_area_3d.cpp
@@ -59,26 +59,6 @@ JPH::ObjectLayer JoltArea3D::_get_object_layer() const {
 	return space->map_to_object_layer(_get_broad_phase_layer(), collision_layer, collision_mask);
 }
 
-bool JoltArea3D::_has_pending_events() const {
-	if (body_monitor_callback.is_valid()) {
-		for (const KeyValue<JPH::BodyID, Overlap> &E : bodies_by_id) {
-			if (!E.value.pending_added.is_empty() || !E.value.pending_removed.is_empty()) {
-				return true;
-			}
-		}
-	}
-
-	if (area_monitor_callback.is_valid()) {
-		for (const KeyValue<JPH::BodyID, Overlap> &E : areas_by_id) {
-			if (!E.value.pending_added.is_empty() || !E.value.pending_removed.is_empty()) {
-				return true;
-			}
-		}
-	}
-
-	return false;
-}
-
 void JoltArea3D::_add_to_space() {
 	jolt_shape = build_shapes(true);
 
@@ -136,6 +116,8 @@ void JoltArea3D::_add_shape_pair(Overlap &p_overlap, const JPH::BodyID &p_body_i
 	shape_indices.self = find_shape_index(p_self_shape_id);
 
 	p_overlap.pending_added.push_back(shape_indices);
+
+	_events_changed();
 }
 
 bool JoltArea3D::_remove_shape_pair(Overlap &p_overlap, const JPH::SubShapeID &p_other_shape_id, const JPH::SubShapeID &p_self_shape_id) {
@@ -148,6 +130,8 @@ bool JoltArea3D::_remove_shape_pair(Overlap &p_overlap, const JPH::SubShapeID &p
 	p_overlap.pending_removed.push_back(shape_pair->value);
 	p_overlap.shape_pairs.remove(shape_pair);
 
+	_events_changed();
+
 	return true;
 }
 
@@ -224,10 +208,16 @@ void JoltArea3D::_force_bodies_entered() {
 	for (KeyValue<JPH::BodyID, Overlap> &E : bodies_by_id) {
 		Overlap &body = E.value;
 
+		if (unlikely(body.shape_pairs.is_empty())) {
+			continue;
+		}
+
 		for (const KeyValue<ShapeIDPair, ShapeIndexPair> &P : body.shape_pairs) {
 			body.pending_removed.erase(P.value);
 			body.pending_added.push_back(P.value);
 		}
+
+		_events_changed();
 	}
 }
 
@@ -236,11 +226,17 @@ void JoltArea3D::_force_bodies_exited(bool p_remove) {
 		const JPH::BodyID &id = E.key;
 		Overlap &body = E.value;
 
+		if (unlikely(body.shape_pairs.is_empty())) {
+			continue;
+		}
+
 		for (const KeyValue<ShapeIDPair, ShapeIndexPair> &P : body.shape_pairs) {
 			body.pending_added.erase(P.value);
 			body.pending_removed.push_back(P.value);
 		}
 
+		_events_changed();
+
 		if (p_remove) {
 			body.shape_pairs.clear();
 			_notify_body_exited(id);
@@ -252,10 +248,16 @@ void JoltArea3D::_force_areas_entered() {
 	for (KeyValue<JPH::BodyID, Overlap> &E : areas_by_id) {
 		Overlap &area = E.value;
 
+		if (unlikely(area.shape_pairs.is_empty())) {
+			continue;
+		}
+
 		for (const KeyValue<ShapeIDPair, ShapeIndexPair> &P : area.shape_pairs) {
 			area.pending_removed.erase(P.value);
 			area.pending_added.push_back(P.value);
 		}
+
+		_events_changed();
 	}
 }
 
@@ -263,11 +265,17 @@ void JoltArea3D::_force_areas_exited(bool p_remove) {
 	for (KeyValue<JPH::BodyID, Overlap> &E : areas_by_id) {
 		Overlap &area = E.value;
 
+		if (unlikely(area.shape_pairs.is_empty())) {
+			continue;
+		}
+
 		for (const KeyValue<ShapeIDPair, ShapeIndexPair> &P : area.shape_pairs) {
 			area.pending_added.erase(P.value);
 			area.pending_removed.push_back(P.value);
 		}
 
+		_events_changed();
+
 		if (p_remove) {
 			area.shape_pairs.clear();
 		}
@@ -313,6 +321,10 @@ void JoltArea3D::_space_changed() {
 	_update_default_gravity();
 }
 
+void JoltArea3D::_events_changed() {
+	_enqueue_call_queries();
+}
+
 void JoltArea3D::_body_monitoring_changed() {
 	if (has_body_monitor_callback()) {
 		_force_bodies_entered();
@@ -664,11 +676,13 @@ void JoltArea3D::body_exited(const JPH::BodyID &p_body_id, bool p_notify) {
 		return;
 	}
 
-	for (KeyValue<ShapeIDPair, ShapeIndexPair> &E : overlap->shape_pairs) {
+	for (const KeyValue<ShapeIDPair, ShapeIndexPair> &E : overlap->shape_pairs) {
 		overlap->pending_added.erase(E.value);
 		overlap->pending_removed.push_back(E.value);
 	}
 
+	_events_changed();
+
 	overlap->shape_pairs.clear();
 
 	if (p_notify) {
@@ -691,6 +705,8 @@ void JoltArea3D::area_exited(const JPH::BodyID &p_body_id) {
 		overlap->pending_removed.push_back(E.value);
 	}
 
+	_events_changed();
+
 	overlap->shape_pairs.clear();
 }
 
@@ -698,9 +714,3 @@ void JoltArea3D::call_queries() {
 	_flush_events(bodies_by_id, body_monitor_callback);
 	_flush_events(areas_by_id, area_monitor_callback);
 }
-
-void JoltArea3D::post_step(float p_step, JPH::Body &p_jolt_body) {
-	if (_has_pending_events()) {
-		_enqueue_call_queries();
-	}
-}
diff --git a/modules/jolt_physics/objects/jolt_area_3d.h b/modules/jolt_physics/objects/jolt_area_3d.h
index 18410c34cfc2..30e79efe48d3 100644
--- a/modules/jolt_physics/objects/jolt_area_3d.h
+++ b/modules/jolt_physics/objects/jolt_area_3d.h
@@ -117,8 +117,6 @@ class JoltArea3D final : public JoltShapedObject3D {
 
 	virtual JPH::EMotionType _get_motion_type() const override { return JPH::EMotionType::Kinematic; }
 
-	bool _has_pending_events() const;
-
 	virtual void _add_to_space() override;
 
 	void _enqueue_call_queries();
@@ -145,6 +143,7 @@ class JoltArea3D final : public JoltShapedObject3D {
 
 	virtual void _space_changing() override;
 	virtual void _space_changed() override;
+	void _events_changed();
 	void _body_monitoring_changed();
 	void _area_monitoring_changed();
 	void _monitorable_changed();
@@ -228,8 +227,6 @@ class JoltArea3D final : public JoltShapedObject3D {
 
 	virtual bool has_custom_center_of_mass() const override { return false; }
 	virtual Vector3 get_center_of_mass_custom() const override { return Vector3(); }
-
-	virtual void post_step(float p_step, JPH::Body &p_jolt_body) override;
 };
 
 #endif // JOLT_AREA_3D_H
diff --git a/modules/jolt_physics/objects/jolt_object_3d.cpp b/modules/jolt_physics/objects/jolt_object_3d.cpp
index 7ee46acbeff0..1c2019d9fa3f 100644
--- a/modules/jolt_physics/objects/jolt_object_3d.cpp
+++ b/modules/jolt_physics/objects/jolt_object_3d.cpp
@@ -136,12 +136,6 @@ bool JoltObject3D::can_interact_with(const JoltObject3D &p_other) const {
 	}
 }
 
-void JoltObject3D::pre_step(float p_step, JPH::Body &p_jolt_body) {
-}
-
-void JoltObject3D::post_step(float p_step, JPH::Body &p_jolt_body) {
-}
-
 String JoltObject3D::to_string() const {
 	Object *instance = get_instance();
 	return instance != nullptr ? instance->to_string() : "<unknown>";
diff --git a/modules/jolt_physics/objects/jolt_object_3d.h b/modules/jolt_physics/objects/jolt_object_3d.h
index 6fa9cc79050d..b040e135e13b 100644
--- a/modules/jolt_physics/objects/jolt_object_3d.h
+++ b/modules/jolt_physics/objects/jolt_object_3d.h
@@ -148,8 +148,7 @@ class JoltObject3D {
 
 	virtual bool reports_contacts() const = 0;
 
-	virtual void pre_step(float p_step, JPH::Body &p_jolt_body);
-	virtual void post_step(float p_step, JPH::Body &p_jolt_body);
+	virtual void pre_step(float p_step, JPH::Body &p_jolt_body) {}
 
 	String to_string() const;
 };
diff --git a/modules/jolt_physics/objects/jolt_shaped_object_3d.cpp b/modules/jolt_physics/objects/jolt_shaped_object_3d.cpp
index 6ceb77733b89..bfe77e008d59 100644
--- a/modules/jolt_physics/objects/jolt_shaped_object_3d.cpp
+++ b/modules/jolt_physics/objects/jolt_shaped_object_3d.cpp
@@ -140,14 +140,26 @@ JPH::ShapeRefC JoltShapedObject3D::_try_build_compound_shape(bool p_optimize) {
 	return shape_result.Get();
 }
 
+void JoltShapedObject3D::_enqueue_shapes_changed() {
+	if (space != nullptr) {
+		space->enqueue_shapes_changed(&shapes_changed_element);
+	}
+}
+
+void JoltShapedObject3D::_dequeue_shapes_changed() {
+	if (space != nullptr) {
+		space->dequeue_shapes_changed(&shapes_changed_element);
+	}
+}
+
 void JoltShapedObject3D::_enqueue_needs_optimization() {
-	if (!needs_optimization_element.in_list()) {
+	if (space != nullptr) {
 		space->enqueue_needs_optimization(&needs_optimization_element);
 	}
 }
 
 void JoltShapedObject3D::_dequeue_needs_optimization() {
-	if (needs_optimization_element.in_list()) {
+	if (space != nullptr) {
 		space->dequeue_needs_optimization(&needs_optimization_element);
 	}
 }
@@ -160,8 +172,11 @@ void JoltShapedObject3D::_shapes_changed() {
 void JoltShapedObject3D::_space_changing() {
 	JoltObject3D::_space_changing();
 
+	_dequeue_shapes_changed();
 	_dequeue_needs_optimization();
 
+	previous_jolt_shape = nullptr;
+
 	if (space != nullptr) {
 		const JoltWritableBody3D body = space->write_body(jolt_id);
 		ERR_FAIL_COND(body.is_invalid());
@@ -172,6 +187,7 @@ void JoltShapedObject3D::_space_changing() {
 
 JoltShapedObject3D::JoltShapedObject3D(ObjectType p_object_type) :
 		JoltObject3D(p_object_type),
+		shapes_changed_element(this),
 		needs_optimization_element(this) {
 	jolt_settings->mAllowSleeping = true;
 	jolt_settings->mFriction = 1.0f;
@@ -317,6 +333,8 @@ void JoltShapedObject3D::commit_shapes(bool p_optimize_compound) {
 
 	space->get_body_iface().SetShape(jolt_id, jolt_shape, false, JPH::EActivation::DontActivate);
 
+	_enqueue_shapes_changed();
+
 	if (!p_optimize_compound && jolt_shape->GetType() == JPH::EShapeType::Compound) {
 		_enqueue_needs_optimization();
 	} else {
@@ -369,6 +387,10 @@ void JoltShapedObject3D::clear_shapes() {
 	_shapes_changed();
 }
 
+void JoltShapedObject3D::clear_previous_shape() {
+	previous_jolt_shape = nullptr;
+}
+
 int JoltShapedObject3D::find_shape_index(uint32_t p_shape_instance_id) const {
 	for (int i = 0; i < (int)shapes.size(); ++i) {
 		if (shapes[i].get_id() == p_shape_instance_id) {
@@ -450,9 +472,3 @@ void JoltShapedObject3D::set_shape_disabled(int p_index, bool p_disabled) {
 
 	_shapes_changed();
 }
-
-void JoltShapedObject3D::post_step(float p_step, JPH::Body &p_jolt_body) {
-	JoltObject3D::post_step(p_step, p_jolt_body);
-
-	previous_jolt_shape = nullptr;
-}
diff --git a/modules/jolt_physics/objects/jolt_shaped_object_3d.h b/modules/jolt_physics/objects/jolt_shaped_object_3d.h
index fee9f5f6b0e2..dbb054de0f13 100644
--- a/modules/jolt_physics/objects/jolt_shaped_object_3d.h
+++ b/modules/jolt_physics/objects/jolt_shaped_object_3d.h
@@ -44,6 +44,7 @@ class JoltShapedObject3D : public JoltObject3D {
 	friend class JoltShape3D;
 
 protected:
+	SelfList<JoltShapedObject3D> shapes_changed_element;
 	SelfList<JoltShapedObject3D> needs_optimization_element;
 
 	Vector3 scale = Vector3(1, 1, 1);
@@ -61,6 +62,9 @@ class JoltShapedObject3D : public JoltObject3D {
 	JPH::ShapeRefC _try_build_single_shape();
 	JPH::ShapeRefC _try_build_compound_shape(bool p_optimize);
 
+	void _enqueue_shapes_changed();
+	void _dequeue_shapes_changed();
+
 	void _enqueue_needs_optimization();
 	void _dequeue_needs_optimization();
 
@@ -106,6 +110,7 @@ class JoltShapedObject3D : public JoltObject3D {
 	void set_shape(int p_index, JoltShape3D *p_shape);
 
 	void clear_shapes();
+	void clear_previous_shape();
 
 	int get_shape_count() const { return shapes.size(); }
 
@@ -123,8 +128,6 @@ class JoltShapedObject3D : public JoltObject3D {
 
 	bool is_shape_disabled(int p_index) const;
 	void set_shape_disabled(int p_index, bool p_disabled);
-
-	virtual void post_step(float p_step, JPH::Body &p_jolt_body) override;
 };
 
 #endif // JOLT_SHAPED_OBJECT_3D_H
diff --git a/modules/jolt_physics/spaces/jolt_space_3d.cpp b/modules/jolt_physics/spaces/jolt_space_3d.cpp
index 06afe72a1d07..1304ee25f224 100644
--- a/modules/jolt_physics/spaces/jolt_space_3d.cpp
+++ b/modules/jolt_physics/spaces/jolt_space_3d.cpp
@@ -85,16 +85,10 @@ void JoltSpace3D::_pre_step(float p_step) {
 void JoltSpace3D::_post_step(float p_step) {
 	contact_listener->post_step();
 
-	// WARNING: The list of active bodies may have changed between `pre_step` and `post_step`.
-
-	const JPH::BodyLockInterface &lock_iface = get_lock_iface();
-	const JPH::BodyID *active_rigid_bodies = physics_system->GetActiveBodiesUnsafe(JPH::EBodyType::RigidBody);
-	const JPH::uint32 active_rigid_body_count = physics_system->GetNumActiveBodies(JPH::EBodyType::RigidBody);
-
-	for (JPH::uint32 i = 0; i < active_rigid_body_count; i++) {
-		JPH::Body *jolt_body = lock_iface.TryGetBody(active_rigid_bodies[i]);
-		JoltObject3D *object = reinterpret_cast<JoltObject3D *>(jolt_body->GetUserData());
-		object->post_step(p_step, *jolt_body);
+	while (shapes_changed_list.first()) {
+		JoltShapedObject3D *object = shapes_changed_list.first()->self();
+		shapes_changed_list.remove(shapes_changed_list.first());
+		object->clear_previous_shape();
 	}
 }
 
@@ -433,6 +427,18 @@ void JoltSpace3D::dequeue_call_queries(SelfList<JoltArea3D> *p_area) {
 	}
 }
 
+void JoltSpace3D::enqueue_shapes_changed(SelfList<JoltShapedObject3D> *p_object) {
+	if (!p_object->in_list()) {
+		shapes_changed_list.add(p_object);
+	}
+}
+
+void JoltSpace3D::dequeue_shapes_changed(SelfList<JoltShapedObject3D> *p_object) {
+	if (p_object->in_list()) {
+		shapes_changed_list.remove(p_object);
+	}
+}
+
 void JoltSpace3D::enqueue_needs_optimization(SelfList<JoltShapedObject3D> *p_object) {
 	if (!p_object->in_list()) {
 		needs_optimization_list.add(p_object);
diff --git a/modules/jolt_physics/spaces/jolt_space_3d.h b/modules/jolt_physics/spaces/jolt_space_3d.h
index c12b5c5f0fe4..16a8f362c635 100644
--- a/modules/jolt_physics/spaces/jolt_space_3d.h
+++ b/modules/jolt_physics/spaces/jolt_space_3d.h
@@ -59,6 +59,7 @@ class JoltShapedObject3D;
 class JoltSpace3D {
 	SelfList<JoltBody3D>::List body_call_queries_list;
 	SelfList<JoltArea3D>::List area_call_queries_list;
+	SelfList<JoltShapedObject3D>::List shapes_changed_list;
 	SelfList<JoltShapedObject3D>::List needs_optimization_list;
 
 	RID rid;
@@ -142,6 +143,9 @@ class JoltSpace3D {
 	void dequeue_call_queries(SelfList<JoltBody3D> *p_body);
 	void dequeue_call_queries(SelfList<JoltArea3D> *p_area);
 
+	void enqueue_shapes_changed(SelfList<JoltShapedObject3D> *p_object);
+	void dequeue_shapes_changed(SelfList<JoltShapedObject3D> *p_object);
+
 	void enqueue_needs_optimization(SelfList<JoltShapedObject3D> *p_object);
 	void dequeue_needs_optimization(SelfList<JoltShapedObject3D> *p_object);
 


Test patch: