Fix thread-safety crash in LipSync anim node: TMap race condition

GetCurrentBlendshapes() was copying CurrentBlendshapes on the anim worker
thread while the game thread mutated it (TSet::UnhashElements crash).
Use a snapshot pattern: game thread copies to ThreadSafeBlendshapes under
FCriticalSection at end of TickComponent, anim node reads the snapshot.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
j.foucher 2026-03-05 15:16:10 +01:00
parent 8d4065944c
commit ca10689bb6
2 changed files with 23 additions and 3 deletions

View File

@ -1109,6 +1109,12 @@ void UPS_AI_ConvAgent_LipSyncComponent::TickComponent(float DeltaTime, ELevelTic
} }
} }
// Snapshot for thread-safe anim node read (GetCurrentBlendshapes)
{
FScopeLock Lock(&BlendshapeLock);
ThreadSafeBlendshapes = CurrentBlendshapes;
}
// Auto-apply morph targets if a target mesh is set // Auto-apply morph targets if a target mesh is set
if (TargetMesh) if (TargetMesh)
{ {
@ -1201,6 +1207,10 @@ void UPS_AI_ConvAgent_LipSyncComponent::ResetToNeutral()
// Clear blendshapes so the mouth returns to fully neutral // Clear blendshapes so the mouth returns to fully neutral
CurrentBlendshapes.Reset(); CurrentBlendshapes.Reset();
{
FScopeLock Lock(&BlendshapeLock);
ThreadSafeBlendshapes.Reset();
}
PreviousBlendshapes.Reset(); PreviousBlendshapes.Reset();
LastConsumedVisemes.Reset(); LastConsumedVisemes.Reset();
} }

View File

@ -170,9 +170,14 @@ public:
UFUNCTION(BlueprintCallable, Category = "PS AI ConvAgent|LipSync") UFUNCTION(BlueprintCallable, Category = "PS AI ConvAgent|LipSync")
TMap<FName, float> GetCurrentVisemes() const { return SmoothedVisemes; } TMap<FName, float> GetCurrentVisemes() const { return SmoothedVisemes; }
/** Get current ARKit blendshape weights (MetaHuman compatible: jawOpen, mouthFunnel, mouthClose, etc.). */ /** Get current ARKit blendshape weights (MetaHuman compatible: jawOpen, mouthFunnel, mouthClose, etc.).
* Thread-safe: returns a snapshot updated each tick. Safe to call from anim worker threads. */
UFUNCTION(BlueprintCallable, Category = "PS AI ConvAgent|LipSync") UFUNCTION(BlueprintCallable, Category = "PS AI ConvAgent|LipSync")
TMap<FName, float> GetCurrentBlendshapes() const { return CurrentBlendshapes; } TMap<FName, float> GetCurrentBlendshapes() const
{
FScopeLock Lock(&BlendshapeLock);
return ThreadSafeBlendshapes;
}
/** True when the agent is currently producing speech audio. /** True when the agent is currently producing speech audio.
* When false, lip sync releases mouth curves to let emotion curves through. */ * When false, lip sync releases mouth curves to let emotion curves through. */
@ -268,9 +273,14 @@ private:
// Smoothed viseme weights (interpolated each tick, exposed via GetCurrentVisemes) // Smoothed viseme weights (interpolated each tick, exposed via GetCurrentVisemes)
TMap<FName, float> SmoothedVisemes; TMap<FName, float> SmoothedVisemes;
// ARKit blendshape weights derived from SmoothedVisemes (exposed via GetCurrentBlendshapes) // ARKit blendshape weights derived from SmoothedVisemes (game-thread working copy)
TMap<FName, float> CurrentBlendshapes; TMap<FName, float> CurrentBlendshapes;
// Thread-safe snapshot of CurrentBlendshapes, updated each tick under BlendshapeLock.
// Read by the anim worker thread via GetCurrentBlendshapes().
TMap<FName, float> ThreadSafeBlendshapes;
mutable FCriticalSection BlendshapeLock;
// Previous frame's blendshape values for additional output smoothing // Previous frame's blendshape values for additional output smoothing
TMap<FName, float> PreviousBlendshapes; TMap<FName, float> PreviousBlendshapes;