From ca10689bb6b79dc7a27f76fe31ee3ee7484d3099 Mon Sep 17 00:00:00 2001 From: "j.foucher" Date: Thu, 5 Mar 2026 15:16:10 +0100 Subject: [PATCH] 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 --- .../Private/PS_AI_ConvAgent_LipSyncComponent.cpp | 10 ++++++++++ .../Public/PS_AI_ConvAgent_LipSyncComponent.h | 16 +++++++++++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Private/PS_AI_ConvAgent_LipSyncComponent.cpp b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Private/PS_AI_ConvAgent_LipSyncComponent.cpp index 4a17a8a..cd75601 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Private/PS_AI_ConvAgent_LipSyncComponent.cpp +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Private/PS_AI_ConvAgent_LipSyncComponent.cpp @@ -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 if (TargetMesh) { @@ -1201,6 +1207,10 @@ void UPS_AI_ConvAgent_LipSyncComponent::ResetToNeutral() // Clear blendshapes so the mouth returns to fully neutral CurrentBlendshapes.Reset(); + { + FScopeLock Lock(&BlendshapeLock); + ThreadSafeBlendshapes.Reset(); + } PreviousBlendshapes.Reset(); LastConsumedVisemes.Reset(); } diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Public/PS_AI_ConvAgent_LipSyncComponent.h b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Public/PS_AI_ConvAgent_LipSyncComponent.h index 6fe3807..c66d949 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Public/PS_AI_ConvAgent_LipSyncComponent.h +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Public/PS_AI_ConvAgent_LipSyncComponent.h @@ -170,9 +170,14 @@ public: UFUNCTION(BlueprintCallable, Category = "PS AI ConvAgent|LipSync") TMap 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") - TMap GetCurrentBlendshapes() const { return CurrentBlendshapes; } + TMap GetCurrentBlendshapes() const + { + FScopeLock Lock(&BlendshapeLock); + return ThreadSafeBlendshapes; + } /** True when the agent is currently producing speech audio. * 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) TMap SmoothedVisemes; - // ARKit blendshape weights derived from SmoothedVisemes (exposed via GetCurrentBlendshapes) + // ARKit blendshape weights derived from SmoothedVisemes (game-thread working copy) TMap CurrentBlendshapes; + // Thread-safe snapshot of CurrentBlendshapes, updated each tick under BlendshapeLock. + // Read by the anim worker thread via GetCurrentBlendshapes(). + TMap ThreadSafeBlendshapes; + mutable FCriticalSection BlendshapeLock; + // Previous frame's blendshape values for additional output smoothing TMap PreviousBlendshapes;