アニメーション並列実行中にFPoseHistoryでデータレースクラッシュが発生する

お世話になっております。

並列アニメーションがWorkerThreadで FPoseHistory を更新中にGameThreadで UPoseSearchLibrary::MotionMatch を呼び出すと、スレッドセーフチェックのアサートでクラッシュが発生します。クラッシュ時のコールスタックもペーストしておきます。

公式サンプルのCBP_SandboxCharacterクラスでも使用されていることから通常使用の範囲だと認識しています。

https://www.docswell.com/s/EpicGamesJapan/56VGGW\-CEDEC2024\_MotionMatching\#p65

以上、よろしくお願いアイマス。​

[Attachment Removed]

再現手順
AnimBP内で FAnimNode_PoseSearchHistoryCollector を使用したポーズ検索を構築する。

アニメーションの並列実行(Parallel Animation Evaluation)を有効化する。

アニメーションスレッドがPoseHistoryコレクターノードを評価しているタイミングで、GameThread側(GA等)からBlueprintで UPoseSearchLibrary::MotionMatch を同時に呼び出す。

[Attachment Removed]

お世話になっております。

ご報告ありがとうございます。

またコールスタックのご提供もありがとうございました。大変助かりました。

ご報告いただいた内容とコールスタックの手がかりから、//UE5/Main CL50988209の修正をバックポートすることで改善が見られるのではないかと考えております。

UE5.7にも容易にバックポート可能な差分となっておりますので、一度導入のうえ、問題が再発するか否かご確認いただくことは可能でしょうか。

以上、よろしくお願いいたします。

[Attachment Removed]

void UPoseSearchLibrary::MotionMatch(
	const TArrayView<UAnimInstance*> AnimInstances,
	const TArrayView<const UE::PoseSearch::FRole> Roles,
	const TArrayView<const UObject*> AssetsToSearch,
	const FName PoseHistoryName,
	const FPoseSearchContinuingProperties& ContinuingProperties,
	const FPoseSearchFutureProperties& Future,
	FPoseSearchBlueprintResult& Result)
{
	using namespace UE::Anim;
	using namespace UE::PoseSearch;
 
	Result = FPoseSearchBlueprintResult();
 
	if (AnimInstances.IsEmpty() || AnimInstances.Num() != Roles.Num())
	{
		UE_LOG(LogPoseSearch, Error, TEXT("UPoseSearchLibrary::MotionMatch - invalid input AnimInstances or Roles"));
		return;
	}
	
	for (UAnimInstance* AnimInstance : AnimInstances)
	{
		if (!AnimInstance)
		{
			UE_LOG(LogPoseSearch, Error, TEXT("UPoseSearchLibrary::MotionMatch - null AnimInstances"));
			return;
		}
 
		if (!AnimInstance->CurrentSkeleton)
		{
			UE_LOG(LogPoseSearch, Error, TEXT("UPoseSearchLibrary::MotionMatch - null AnimInstances->CurrentSkeleton"));
			return;
		}
	}
 
	FMemMark Mark(FMemStack::Get());
 
	TArray<const IPoseHistory*, TInlineAllocator<PreallocatedRolesNum, TMemStackAllocator<>>> PoseHistories;
	TArray<const UObject*, TInlineAllocator<PreallocatedRolesNum, TMemStackAllocator<>>> AnimContexts;
	for (UAnimInstance* AnimInstance : AnimInstances)
	{
		if (const FAnimNode_PoseSearchHistoryCollector_Base* PoseHistoryNode = FindPoseHistoryNode(PoseHistoryName, AnimInstance))
		{
			PoseHistories.Add(&PoseHistoryNode->GetPoseHistory());
		}
 
		AnimContexts.Add(AnimInstance);
	}
 
	if (PoseHistories.Num() != AnimInstances.Num())
	{
		UE_LOG(LogPoseSearch, Error, TEXT("UPoseSearchLibrary::MotionMatch - Couldn't find pose history with name '%s'"), *PoseHistoryName.ToString());
		return;
	}
 
	const FSearchResult SearchResult = MotionMatch(AnimContexts, Roles, PoseHistories, AssetsToSearch, ContinuingProperties, Future, FPoseSearchEvent());
	if (SearchResult.IsValid())
	{
		const UPoseSearchDatabase* Database = SearchResult.Database.Get();
		check(Database);
		
		// figuring out the WantedPlayRate
		float WantedPlayRate = 1.f;
		if (Future.Animation && Future.IntervalTime > 0.f)
		{
			if (const UPoseSearchFeatureChannel_PermutationTime* PermutationTimeChannel = Database->Schema->FindFirstChannelOfType<UPoseSearchFeatureChannel_PermutationTime>())
			{
				const FSearchIndex& SearchIndex = Database->GetSearchIndex();
				if (!SearchIndex.IsValuesEmpty())
				{
					TConstArrayView<float> ResultData = Database->GetSearchIndex().GetPoseValues(SearchResult.PoseIdx);
					const float ActualIntervalTime = PermutationTimeChannel->GetPermutationTime(ResultData);
					WantedPlayRate = ActualIntervalTime / Future.IntervalTime;
				}
			}
		}
 
		Result.InitFrom(SearchResult, WantedPlayRate);
	}
}

適用コード

    // [Content removed] perhaps promoting UAnimInstance::GetProxyOnAnyThread from protected to public
    // making sure there're no flying animation tasks when on game thread by calling GetProxyOnAnyThread so it's safe to access animinstance variables etc
    class UFinishFlyingAnimInstanceTasks : public UAnimInstance
    {
    public:
        static void Execute(UAnimInstance* AnimInstance)
        {
            static_cast<UFinishFlyingAnimInstanceTasks*>(AnimInstance)->GetProxyOnAnyThread<FAnimInstanceProxy>();
        }
    };
    UFinishFlyingAnimInstanceTasks::Execute(AnimInstance);

UE5.7.4ではパッチ適用箇所のMotionMatch関数の引数がAnimInstancesです。

for (UAnimInstance* AnimInstance : AnimInstances)

このループ内でUFinishFlyingAnimInstanceTasks::Execute(AnimInstance)を呼べばよいでしょうか?

それから、UPoseSearchLibrary::MotionMatchは複数のオーバーロード関数が存在します。それらはGetProxyOnAnyThreadでブロックしなくてよいのでしょうか?

以上、よろしくお願いします。

[Attachment Removed]

お世話になっております。

頂いたパッチを適用して修正されたことを確認しました。

チケットはクローズしても大丈夫です。

ありがとうございました。

[Attachment Removed]

お世話になっております。

ご確認ありがとうございます。

> UE5.7.4ではパッチ適用箇所のMotionMatch関数の引数がAnimInstancesです。

> for (UAnimInstance* AnimInstance : AnimInstances)

> このループ内でUFinishFlyingAnimInstanceTasks::Execute(AnimInstance)を呼べばよいでしょうか?

前回「容易にバックポート可能」とご返信してしまい、大変失礼いたしました。確かに、シグニチャが異なるため、当てる箇所に慎重な検討が必要でした。ご指摘のとおり、ループ内で呼び出すという手当の仕方で合っております。ただし、関数冒頭のnull用チェックループの外側に独立したループを組んで行うのが安全と思います。遅まきながら.patch形式のファイルを本返信に添付しますので、参考になれば幸いです。

> それから、UPoseSearchLibrary::MotionMatchは複数のオーバーロード関数が存在します。それらはGetProxyOnAnyThreadでブロックしなくてよいのでしょうか?

ご質問ありがとうございます。​

当方でも再確認いたしましたが、​他のオーバーロード関数へのブロックの追加は不要と考えております。

ご承知の通り、UE5.7には合計8つのオーバーロードが用意されておりますが、UE5.8 の公式修正でも同期が入ったのは AnimInstance から PoseHistory を自ら解決する版のみとなっております。UE5.7にパッチするにあたり、1番目のオーバーロード関数は内部的に2番目の関数を呼び出しますので、後者にパッチを当てれば両方手当てできます。

​​

3番目以降のオーバーロード関数については、解決済みの PoseHistory を受け取る(呼び出し元が同期に責任を持つ)設計ですので、ブロックは不要となります。ゲームスレッドから3番目以降のオーバーロード関数を直接呼び出す場合のみ、PoseHistory取得前に呼び出し元で(CL50988165と)同様の完了待ちを行っていただければと思います。

以上、よろしくお願いいたします。

[Attachment Removed]

ご確認ありがとうございます。

修正できたと聞き、安心いたしました!

それでは本件は対応済みとしてCloseさせていただきます。

また何かございましたら、お気軽にEPSをご利用ください。

[Attachment Removed]