Repository: godotengine/godot (language: cpp)

Issue:
`get_global_transform_interpolated()` may return wrong value when FPS < physics ticks per second
### Tested versions

- Reproducible in v3.6.stable.official [de2f0f147]
- Reproducible in v4.4.dev1.official [28a72fa43]
- Reproducible in v4.4.beta1.official [d33da79d3]
- Reproducible in v4.4.beta.custom_build [a013481b0]

### System information

Godot v4.4.beta (a013481b0) - CachyOS Linux #1 SMP PREEMPT_DYNAMIC Mon, 20 Jan 2025 21:26:55 +0000 on Wayland - X11 display driver, Multi-window, 1 monitor - Vulkan (Forward+) - integrated Intel(R) HD Graphics 620 (KBL GT2) - Intel(R) Core(TM) i7-7500U CPU @ 2.70GHz (4 threads)

### Issue description

When FPS drops below `physics_ticks_per_second` the function `get_global_transform_interpolated()` sometimes returns wrong value.

When calling `get_global_transform_interpolated()` in a `_process()` I expect to get the most recent interpolated transform regardless of how many physics ticks happen between successive process frames. I think the actual returned value may be interpolated transform only after 1 physics tick that occurred directly after the previous process frame.

The issue is about functionality introduced to `4.4.dev1` in https://github.com/godotengine/godot/pull/92391 but it is also reproducible in `3.6` version of physics interpolation.

### Steps to reproduce

Run the MRP and notice NinePatchRect is constantly jittering.

In order to reproduce the issue the project has `max_fps` set lower than `physics_ticks_per_second`.

If needed I can provide even more simplified MRP without the usage of `Camera3D.unproject_position()`.

### Minimal reproduction project (MRP)

Godot 4.x project:
[test-physics-interpolation-maxfps.zip](https://github.com/user-attachments/files/18580802/test-physics-interpolation-maxfps.zip)

Godot 3.x project:
[Test Physics Interpolation max_fps Godot3.zip](https://github.com/user-attachments/files/18580804/Test.Physics.Interpolation.max_fps.Godot3.zip)


Discussion / hints:
Related #94060.

UPDATE:
Ah! I think I understand the difference here, maybe the timeout is not working. There should be a timeout which prevents this. Will investigate.
You are absolutely right, I must have subconsciously assumed the frame rate would be higher than the tick rate, because it only updates the previous transform during the frame call. I'll put in a mechanism to fix this. 👍 

UPDATE:
Turns out there is already a mechanism to deal with tick rate higher than the frame rate, and it is working fine in a test project. So there is something special about your MRP which is causing the behaviour seen, still investigating.

Modifying your project so the target is not a physics object seems to work fine (as in my MRP), so it looks to be something to do with the timing that the physics engine is updating the Ball, relative to when the interpolation records it. Will probably have to look more tomorrow.
Think I've got it, the client interpolation pump was being updated _before_ the physics had called back to update the item transforms during the iteration. This seemed not to be a problem when the frame rate was higher than tick rate, but caused a glitch the other way round when there was more than 1 tick per frame.

It looks fairly simple to fix by moving the point in the iteration we update the client interpolation pump.

Well done for spotting this! 👍 

Also likely it was missed because most of the testing was on non-physics objects (to simplify things).

Fix patch:
diff --git a/scene/main/scene_tree.cpp b/scene/main/scene_tree.cpp
index 3c1809c3586b..99273ffa81c7 100644
--- a/scene/main/scene_tree.cpp
+++ b/scene/main/scene_tree.cpp
@@ -559,11 +559,6 @@ void SceneTree::iteration_prepare() {
 		// are flushed before pumping the interpolation prev and currents.
 		flush_transform_notifications();
 		VisualServer::get_singleton()->tick();
-
-		// Any objects performing client physics interpolation
-		// should be given an opportunity to keep their previous transforms
-		// up to date before each new physics tick.
-		_client_physics_interpolation.physics_process();
 	}
 }
 
@@ -572,6 +567,11 @@ void SceneTree::iteration_end() {
 	// to be flushed to the VisualServer before finishing a physics tick.
 	if (_physics_interpolation_enabled) {
 		flush_transform_notifications();
+
+		// Any objects performing client physics interpolation
+		// should be given an opportunity to keep their previous transforms
+		// up to date.
+		_client_physics_interpolation.physics_process();
 	}
 }
 


Test patch: