[Mover][PoseSearch] GetPredictedTrajectory is not thread-safe — SetGravityOverride causes crash on Simulated Proxy when called from Animation Worker Thread

When bSyncInputsForSimProxy is enabled on UMoverComponent, calling UPoseSearchTrajectoryLibrary::PoseSearchGenerateTransformTrajectoryWithPredictor on a Simulated Proxy client causes a crash in Chaos physics (PBDRigidsSolver::PushPhysicsState).

The root cause is that UMoverComponent::GetPredictedTrajectory internally calls SetGravityOverride() to temporarily modify and then restore the MoverComponent’s gravity state. Since PoseSearchGenerateTransformTrajectoryWithPredictor is marked BlueprintThreadSafe and is called from the Animation Worker Thread, this write to MoverComponent members creates a race condition with the Game Thread’s physics update.

When bSyncInputsForSimProxy is disabled, GetLastInputCmd() returns empty/default input on the Simulated Proxy, so GenerateMove produces near-zero movement and the prediction loop completes almost instantly — the race window is negligible. When enabled, GetLastInputCmd() returns real synced input, GenerateMove produces meaningful movement across ~20-30 iterations, significantly widening the race window and making the crash reproducible.

The call chain on the Simulated Proxy is:

[Animation Worker Thread]

PoseSearchGenerateTransformTrajectoryWithPredictor() // marked BlueprintThreadSafe

→ UMoverTrajectoryPredictor::Predict()

→ UMoverComponent::GetPredictedTrajectory()

→ SetGravityOverride(true, FVector::ZeroVector) // :warning: non-thread-safe WRITE

→ GenerateMove() × N iterations // reads modified state

→ SetGravityOverride(origValue, origAccel) // :warning: non-thread-safe WRITE (restore)

SetGravityOverride modifies 4 member variables on UMoverComponent (bHasGravityOverride, GravityAccelOverride, WorldToGravityTransform, GravityToWorldTransform) without any synchronization, while the Game Thread is concurrently running physics updates.

Additionally, the IPoseSearchTrajectoryPredictorInterface::Predict virtual function is not declared const, which appears to be a consequence of this implementation pattern — the interface’s const-correctness was sacrificed to accommodate the temporary state mutation inside GetPredictedTrajectory.

[Attachment Removed]

Set up a networked multiplayer session (dedicated server or listen server with at least 2 clients).

On UMoverComponent, enable bSyncInputsForSimProxy = true.

Set up PoseSearch Motion Matching on the character using UMoverTrajectoryPredictor as the trajectory predictor (via IPoseSearchTrajectoryPredictorInterface).

In the Animation Blueprint (or Animation Thread-Safe update), call UPoseSearchTrajectoryLibrary::PoseSearchGenerateTransformTrajectoryWithPredictor with the UMoverTrajectoryPredictor, which internally calls UMoverComponent::GetPredictedTrajectory.

Observe the other client’s Simulated Proxy — the game crashes.

Expected: No crash. Trajectory prediction should be a read-only query with no side effects.

Actual: Crash in Chaos physics with the following assertion:

Assertion failed: (Index >= 0) & (Index < ArrayNum)

Array index out of bounds: 0 into an array of size 0

Chaos::TGeometryParticle<double,3>::SyncRemoteData() [ParticleHandle.h:3077]

Chaos::FPBDRigidsSolver::PushPhysicsState() [PBDRigidsSolver.cpp:1518]

ParallelForImpl::ParallelForInternal<…> [ParallelFor.h:358]

Crash in runnable thread Background Worker #11

[Attachment Removed]

Hey there,

Just to check, if you turn off bDisableGravity in your FMoverPredictTrajectoryParams. Does the crash still happen on the simulation proxy?

Also, could you provide a base unreal repro project or the code for your trajectory predictor?

Dustin

[Attachment Removed]