// Copyright Epic Games, Inc. All Rights Reserved. #include "PoseSearch/PoseSearchInteractionIsland.h" #include "Animation/AnimClassInterface.h" #include "Animation/AnimInstance.h" #include "Components/SkeletalMeshComponent.h" #include "DrawDebugHelpers.h" #include "Engine/SkeletalMesh.h" #include "Features/IModularFeatures.h" #include "PoseSearch/AnimNode_PoseSearchHistoryCollector.h" #include "PoseSearch/MultiAnimAsset.h" #include "PoseSearch/PoseSearchContext.h" #include "PoseSearch/PoseSearchDatabase.h" #include "PoseSearch/PoseSearchInteractionLibrary.h" #include "PoseSearch/PoseSearchInteractionSubsystem.h" #include "PoseSearch/PoseSearchInteractionValidator.h" #include "PoseSearch/PoseSearchLibrary.h" #include "PoseSearch/PoseSearchSchema.h" #include "VisualLogger/VisualLogger.h" namespace UE::PoseSearch { #if ENABLE_ANIM_DEBUG static bool GVarPoseSearchInteractionDiagnoseTickDependencies = false; static FAutoConsoleVariableRef CVarPoseSearchInteractionDiagnoseTickDependencies(TEXT("a.PoseSearchInteraction.DiagnoseTickDependencies"), GVarPoseSearchInteractionDiagnoseTickDependencies, TEXT("Enable Pose Search Interaction Tick Dependencies Diagnostic (SLOW!)")); // recursion safe FTickFunction logging functions static void ShowPrerequistes(const FTickFunction& NestedTick, int32 Indent, int32 MaxIndent) { if (Indent >= MaxIndent) { UE_LOG(LogPoseSearch, Log, TEXT("%s==== REACHED MAX INDENT ===="), FCString::Spc(Indent * 2)); } else { for (const FTickPrerequisite& Prereq : NestedTick.GetPrerequisites()) { if (Prereq.PrerequisiteTickFunction) { UE_LOG(LogPoseSearch, Log, TEXT("%s prereq %s"), FCString::Spc(Indent * 2), *Prereq.PrerequisiteTickFunction->DiagnosticMessage()); ShowPrerequistes(*Prereq.PrerequisiteTickFunction, Indent + 1, MaxIndent); } } } } static void LogTickFunction(FTickFunction& Tick, ENamedThreads::Type CurrentThread, bool bLogPrerequisites, int32 Indent, int32 MaxIndent = 50) { if (Indent >= MaxIndent) { UE_LOG(LogPoseSearch, Log, TEXT("%s==== REACHED MAX INDENT ===="), FCString::Spc(Indent * 2)); } else { // scoping brackets to save some heap for the recursion { UE_LOG(LogPoseSearch, Log, TEXT("%stick %s [%1d, %1d] %6llu %2d %s"), FCString::Spc(Indent * 2), Tick.bHighPriority ? TEXT("*") : TEXT(" "), (int32)Tick.GetActualTickGroup(), (int32)Tick.GetActualEndTickGroup(), (uint64)GFrameCounter, (int32)CurrentThread, *Tick.DiagnosticMessage()); if (bLogPrerequisites) { ShowPrerequistes(Tick, Indent, MaxIndent); } } // Handle nested ticks Tick.ForEachNestedTick([CurrentThread, bLogPrerequisites, Indent, MaxIndent](FTickFunction& NestedTick) { LogTickFunction(NestedTick, CurrentThread, bLogPrerequisites, Indent + 1, MaxIndent); }); } } // check if there's any cycle within the prerequisites of Tick static bool ValidateTickDependenciesCycles(FTickFunction& Tick, TSet& VisitedTickFunctions) { bool bValidatedCorrectly = true; bool bAlreadyInSet = false; VisitedTickFunctions.Add(&Tick, &bAlreadyInSet); if (bAlreadyInSet) { UE_LOG(LogPoseSearch, Error, TEXT("ValidateTickDependencies: TickFunction '%s' form a cycle"), *Tick.DiagnosticMessage()); bValidatedCorrectly = false; } else { for (const FTickPrerequisite& Prereq : Tick.GetPrerequisites()) { if (Prereq.PrerequisiteTickFunction) { if (!ValidateTickDependenciesCycles(*Prereq.PrerequisiteTickFunction, VisitedTickFunctions)) { VisitedTickFunctions.Remove(&Tick); bValidatedCorrectly = false; break; } } } if (bValidatedCorrectly) { Tick.ForEachNestedTick([&VisitedTickFunctions, &bValidatedCorrectly](FTickFunction& NestedTick) { if (bValidatedCorrectly) { if (!ValidateTickDependenciesCycles(NestedTick, VisitedTickFunctions)) { bValidatedCorrectly = false; } } }); } } VisitedTickFunctions.Remove(&Tick); return bValidatedCorrectly; } #endif // ENABLE_ANIM_DEBUG static FInteractionSearchResult InitSearchResult(const FSearchResult& SearchResult, const FInteractionSearchContext& SearchContext, int32 SearchIndex) { FInteractionSearchResult InteractionSearchResult; static_cast(InteractionSearchResult) = SearchResult; InteractionSearchResult.SearchIndex = SearchIndex; const int32 AnimContextsNum = SearchContext.Num(); InteractionSearchResult.ActorRootTransforms.SetNum(AnimContextsNum); InteractionSearchResult.ActorRootBoneTransforms.SetNum(AnimContextsNum); for (int32 AnimContextIndex = 0; AnimContextIndex < AnimContextsNum; ++AnimContextIndex) { const UObject* AnimContext = SearchContext.GetAnimContext(AnimContextIndex); const USkeleton* Skeleton = AnimContext ? GetContextSkeleton(AnimContext) : nullptr; const IPoseHistory* PoseHistory = SearchContext.GetPoseHistory(AnimContextIndex); if (Skeleton && PoseHistory) { PoseHistory->GetTransformAtTime(0.f, InteractionSearchResult.ActorRootTransforms[AnimContextIndex], Skeleton, ComponentSpaceIndexType, WorldSpaceIndexType); PoseHistory->GetTransformAtTime(0.f, InteractionSearchResult.ActorRootBoneTransforms[AnimContextIndex], Skeleton, RootBoneIndexType, ComponentSpaceIndexType); } else { InteractionSearchResult.ActorRootTransforms[AnimContextIndex] = FTransform::Identity; InteractionSearchResult.ActorRootBoneTransforms[AnimContextIndex] = FTransform::Identity; } } return InteractionSearchResult; } typedef TSet, TInlineSetAllocator<64, TMemStackSetAllocator<>>> FVisitedAnimContexts; static bool IsPoseSearchResultUsable(int32 SearchIndex, TConstArrayView PoseSearchResults, TConstArrayView SearchContexts, const FVisitedAnimContexts& VisitedAnimContexts) { if (!PoseSearchResults[SearchIndex].IsValid()) { return false; } for (int32 AnimContextIndex = 0; AnimContextIndex < SearchContexts[SearchIndex].Num(); ++AnimContextIndex) { if (const UObject* ValidSearchAnimContext = SearchContexts[SearchIndex].GetAnimContext(AnimContextIndex)) { if (VisitedAnimContexts.Find(ValidSearchAnimContext)) { return false; } } else { return false; } } return true; } static void InitSearchResults(TArray& SearchResults, TConstArrayView PoseSearchResults, TConstArrayView SearchContexts) { SearchResults.Reset(); if (!PoseSearchResults.IsEmpty()) { TArray> SortedPoseSearchResults; SortedPoseSearchResults.SetNum(PoseSearchResults.Num()); for (int32 Index = 0; Index < PoseSearchResults.Num(); ++Index) { SortedPoseSearchResults[Index] = Index; } SortedPoseSearchResults.StableSort([&SearchContexts, &PoseSearchResults](int32 IndexA, int32 IndexB) { const bool IsValidA = PoseSearchResults[IndexA].IsValid(); const bool IsValidB = PoseSearchResults[IndexB].IsValid(); if (IsValidA && !IsValidB) { return true; } if (!IsValidA && IsValidB) { return false; } if (!IsValidA && !IsValidB) { return true; } const int32 NumRolesA = SearchContexts[IndexA].Num(); const int32 NumRolesB = SearchContexts[IndexB].Num(); if (NumRolesA > NumRolesB) { return true; } if (NumRolesA < NumRolesB) { return false; } return PoseSearchResults[IndexA].PoseCost < PoseSearchResults[IndexB].PoseCost; }); // assign from best to worst result FVisitedAnimContexts VisitedAnimContexts; for (int32 SearchIndex : SortedPoseSearchResults) { if (IsPoseSearchResultUsable(SearchIndex, PoseSearchResults, SearchContexts, VisitedAnimContexts)) { SearchResults.Add(InitSearchResult(PoseSearchResults[SearchIndex], SearchContexts[SearchIndex], SearchIndex)); for (int32 AnimContextIndex = 0; AnimContextIndex < SearchContexts[SearchIndex].Num(); ++AnimContextIndex) { const UObject* SearchAnimContext = SearchContexts[SearchIndex].GetAnimContext(AnimContextIndex); check(SearchAnimContext); VisitedAnimContexts.Add(SearchAnimContext); } } } } } static UActorComponent* FindComponentForTickDependencies(const UObject* AnimContext) { check(AnimContext); if (const UAnimInstance* AnimInstance = Cast(AnimContext)) { return AnimInstance->GetSkelMeshComponent(); } // this is the AnimNext case return const_cast(Cast(AnimContext)); } static void AddPrerequisite(FTickFunction& TickFunction, UObject* TargetObject, FTickFunction& TargetTickFunction) { if (TargetObject) { #if ENABLE_ANIM_DEBUG if (GVarPoseSearchInteractionDiagnoseTickDependencies) { const ENamedThreads::Type Type = IsInGameThread() ? ENamedThreads::GameThread : ENamedThreads::AnyThread; TSet VisitedTickFunctions; if (!ValidateTickDependenciesCycles(TickFunction, VisitedTickFunctions)) { LogTickFunction(TickFunction, Type, true, 1); } check(VisitedTickFunctions.IsEmpty()); if (!ValidateTickDependenciesCycles(TargetTickFunction, VisitedTickFunctions)) { LogTickFunction(TargetTickFunction, Type, true, 1); } check(VisitedTickFunctions.IsEmpty()); } #endif // ENABLE_ANIM_DEBUG TickFunction.AddPrerequisite(TargetObject, TargetTickFunction); #if ENABLE_ANIM_DEBUG if (!TickFunction.GetPrerequisites().Contains(FTickPrerequisite(TargetObject, TargetTickFunction))) { UE_LOG(LogPoseSearch, Error, TEXT("UE::PoseSearch::AddPrerequisite, Failed to add prerequisite from [%s] to [%s, %s]!"), *TickFunction.DiagnosticMessage(), *TargetObject->GetName(), *TargetTickFunction.DiagnosticMessage()); } if (GVarPoseSearchInteractionDiagnoseTickDependencies) { const ENamedThreads::Type Type = IsInGameThread() ? ENamedThreads::GameThread : ENamedThreads::AnyThread; TSet VisitedTickFunctions; if (!ValidateTickDependenciesCycles(TickFunction, VisitedTickFunctions)) { LogTickFunction(TickFunction, Type, true, 1); } check(VisitedTickFunctions.IsEmpty()); } #endif // ENABLE_ANIM_DEBUG } } // FInteractionSearchContextBase /////////////////////////////////////////////////////////// const UE::PoseSearch::IPoseHistory* FInteractionSearchContextBase::GetPoseHistory(int32 Index) const { check(PoseHistories.IsValidIndex(Index)); if (PoseHistories[Index].IsValid()) { return PoseHistories[Index].Pin().Get(); } return nullptr; } bool FInteractionSearchContextBase::IsEquivalent(const FInteractionSearchContextBase& Other) const { // skipping bDisableCollisions for equality return Database == Other.Database && AnimContexts == Other.AnimContexts && PoseHistories == Other.PoseHistories && Roles == Other.Roles; } #if ENABLE_VISUAL_LOG void FInteractionSearchContextBase::VLogContext(const FColor& Color) const { using namespace UE::PoseSearch; if (FVisualLogger::IsRecording()) { static const TCHAR* LogName = TEXT("PoseSearchInteraction"); const int32 AnimContextsNum = AnimContexts.Num(); TArray>> Locations; Locations.SetNum(AnimContextsNum); for (int32 Index = 0; Index < AnimContextsNum; ++Index) { if (const UObject* AnimContext = AnimContexts[Index].Get()) { Locations[Index] = GetContextLocation(AnimContext); } } for (int32 IndexA = 0; IndexA < AnimContextsNum; ++IndexA) { for (int32 IndexB = IndexA + 1; IndexB < AnimContextsNum; ++IndexB) { for (int32 IndexAll = 0; IndexAll < AnimContextsNum; ++IndexAll) { if (const UObject* AnimContext = AnimContexts[IndexAll].Get()) { UE_VLOG_SEGMENT(AnimContext, LogName, Display, Locations[IndexA], Locations[IndexB], Color, TEXT("")); } } } } } } #endif // ENABLE_VISUAL_LOG #if DO_CHECK bool FInteractionSearchContextBase::CheckForConsistency() const { if (Database == nullptr) { return false; } const int32 Num = AnimContexts.Num(); if (Num < 1) { return false; } if (Num != PoseHistories.Num()) { return false; } if (Num != Roles.Num()) { return false; } for (int32 IndexA = 0; IndexA < Num; ++IndexA) { if (AnimContexts[IndexA] == nullptr) { return false; } for (int32 IndexB = IndexA + 1; IndexB < Num; ++IndexB) { if (AnimContexts[IndexA] == AnimContexts[IndexB]) { return false; } } } for (int32 IndexA = 1; IndexA < Num; ++IndexA) { // AnimContexts must be sorted to have deterministic searches across multiple frames if (AnimContexts[IndexA - 1].Get() >= AnimContexts[IndexA].Get()) { return false; } } for (int32 IndexA = 0; IndexA < Num; ++IndexA) { for (int32 IndexB = IndexA + 1; IndexB < Num; ++IndexB) { if (Roles[IndexA] == Roles[IndexB]) { return false; } } } for (int32 IndexA = 0; IndexA < Num; ++IndexA) { if (PoseHistories[IndexA] == nullptr) { return false; } for (int32 IndexB = IndexA + 1; IndexB < Num; ++IndexB) { if (PoseHistories[IndexA] == PoseHistories[IndexB]) { return false; } } } return true; } void FInteractionSearchContextBase::TestHistoryCollectorsThreadingAccess() const { for (int32 Index = 0; Index < AnimContexts.Num(); ++Index) { if (const UAnimInstance* AnimInstance = Cast(AnimContexts[Index].Get())) { if (const IPoseHistory* PoseHistory = GetPoseHistory(Index)) { const USkeleton* Skeleton = AnimInstance->GetRequiredBonesOnAnyThread().GetSkeletonAsset(); check(Skeleton); FTransform ActorRootBoneTransform; PoseHistory->GetTransformAtTime(0.f, ActorRootBoneTransform, Skeleton, RootBoneIndexType, ComponentSpaceIndexType); } } } } #endif // DO_CHECK // FInteractionSearchResult /////////////////////////////////////////////////////////// bool FInteractionSearchResult::operator==(const FInteractionSearchResult& Other) const { // not checking SearchIndex, nor ActorRootTransforms, nor ActorRootBoneTransforms for equality return static_cast(*this) == static_cast(Other); } // FIslandPreTickFunction /////////////////////////////////////////////////////////// void FInteractionIsland::FPreTickFunction::ExecuteTick(float DeltaTime, enum ELevelTick TickType, ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent) { // Called before any skeletal mesh component tick, when there aren't animation jobs flying. No need to FScopeLock Lock(&Mutex); // generating trajectories before running any of the skeletal mesh component ticks check(Island); if (Island->HasTickDependencies()) { CheckInteractionThreadSafety(Island); for (const FInteractionSearchContext& SearchContext : Island->SearchContexts) { for (int32 Index = 0; Index < SearchContext.Num(); ++Index) { if (const UObject* AnimContext = SearchContext.GetAnimContext(Index)) { if (const IPoseHistory* PoseHistory = SearchContext.GetPoseHistory(Index)) { // since FInteractionIsland has a tick dependency with the USkeletalMeshComponent it's safe modify the IPoseHistory const_cast(PoseHistory)->GenerateTrajectory(AnimContext, DeltaTime); } } } } #if ENABLE_ANIM_DEBUG if (Island->bPreTickFunctionExecuted) { // @todo: need to figure out why when creating a new island FPreTickFunction gets called twice (it's not a real issue rather than a performance hit) // use GVarPoseSearchInteractionCacheIslands = false to debug the issue (it destroys the islands every frame) UE_LOG(LogPoseSearch, Warning, TEXT("FInteractionIsland::FPreTickFunction::ExecuteTick, called twice before UPoseSearchInteractionSubsystem::Tick!")); } else { if (Island->bPostTickFunctionExecuted) { UE_LOG(LogPoseSearch, Error, TEXT("FInteractionIsland::FPreTickFunction::ExecuteTick, FPostTickFunction::ExecuteTick alreay run?!")); Island->LogTickDependencies(); } Island->bPreTickFunctionExecuted = true; } #endif // ENABLE_ANIM_DEBUG } } // FPostTickFunction /////////////////////////////////////////////////////////// void FInteractionIsland::FPostTickFunction::ExecuteTick(float DeltaTime, enum ELevelTick TickType, ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent) { check(Island); #if ENABLE_ANIM_DEBUG if (Island->HasTickDependencies()) { CheckInteractionThreadSafety(Island); if (Island->bPostTickFunctionExecuted) { // @todo: need to figure out why when creating a new island FPostTickFunction gets called twice (it's not a real issue rather than a performance hit) // use GVarPoseSearchInteractionCacheIslands = false to debug the issue (it destroys the islands every frame) UE_LOG(LogPoseSearch, Warning, TEXT("FInteractionIsland::FPostTickFunction::ExecuteTick, called twice before UPoseSearchInteractionSubsystem::Tick!")); } else { if (!Island->bPreTickFunctionExecuted) { UE_LOG(LogPoseSearch, Error, TEXT("FInteractionIsland::FPostTickFunction::ExecuteTick, FPreTickFunction::ExecuteTick didn't run!")); Island->LogTickDependencies(); } Island->bPostTickFunctionExecuted = true; } } #endif // ENABLE_ANIM_DEBUG } // FInteractionIsland /////////////////////////////////////////////////////////// FInteractionIsland::FInteractionIsland(ULevel* Level, UPoseSearchInteractionSubsystem* Subsystem) { PreTickFunction.bAllowTickBatching = true; PreTickFunction.bRunOnAnyThread = true; PreTickFunction.Island = this; PreTickFunction.RegisterTickFunction(Level); PostTickFunction.bAllowTickBatching = true; PostTickFunction.bRunOnAnyThread = true; PostTickFunction.Island = this; PostTickFunction.RegisterTickFunction(Level); InteractionSubsystem = Subsystem; } FInteractionIsland::~FInteractionIsland() { Uninitialize(false); PreTickFunction.UnRegisterTickFunction(); PostTickFunction.UnRegisterTickFunction(); InteractionSubsystem = nullptr; } IInteractionIslandDependency* FInteractionIsland::FindCustomDependency(UActorComponent* InTickComponent) { IModularFeatures& ModularFeatures = IModularFeatures::Get(); const int32 NumFeatures = ModularFeatures.GetModularFeatureImplementationCount(IInteractionIslandDependency::FeatureName); // Add pre-tick function dependencies for (int32 FeatureIndex = 0; FeatureIndex < NumFeatures; ++FeatureIndex) { if (IInteractionIslandDependency* IslandDependency = static_cast(ModularFeatures.GetModularFeatureImplementation(IInteractionIslandDependency::FeatureName, FeatureIndex))) { if (IslandDependency->CanMakeDependency(nullptr, InTickComponent)) { return IslandDependency; } } } return nullptr; } void FInteractionIsland::AddTickDependencies(UActorComponent* TickActorComponent, bool bInIsMainActor) { check(TickActorComponent); if (IInteractionIslandDependency* IslandDependency = FindCustomDependency(TickActorComponent)) { if (const FTickFunction* TickActorComponentTickFunction = IslandDependency->FindTickFunction(TickActorComponent)) { if(bInIsMainActor) { // PostTickFunction prerequisites should be empty since we haven't add the main actor tick function yet check(PostTickFunction.GetPrerequisites().IsEmpty()); // adding to PreTickFunction all the tick dependencies TickActorComponent has, so it runs after all the tick dependencies of ALL the TickActorComponents in this FInteractionIsland for (const FTickPrerequisite& TickActorComponentPrerequisite : TickActorComponentTickFunction->GetPrerequisites()) { AddPrerequisite(PreTickFunction, TickActorComponentPrerequisite.PrerequisiteObject.Get(), *TickActorComponentPrerequisite.PrerequisiteTickFunction); } // Add post-tick function dependencies IslandDependency->AddSubsequent(InteractionSubsystem, PreTickFunction, TickActorComponent); IslandDependency->AddPrerequisite(InteractionSubsystem, PostTickFunction, TickActorComponent); check(!bHasTickDependencies); bHasTickDependencies = true; } else { // PostTickFunction should contain only the tick function to the main actor's one check(PostTickFunction.GetPrerequisites().Num()); // adding to PreTickFunction all the tick dependencies TickActorComponent has, so it runs after all the tick dependencies of ALL the TickActorComponents in this FInteractionIsland // BUT excluding the main actor tick fuction that is PostTickFunction.GetPrerequisites()[0] const FTickFunction* MainActorTickFunction = PostTickFunction.GetPrerequisites()[0].Get(); for (const FTickPrerequisite& TickActorComponentPrerequisite : TickActorComponentTickFunction->GetPrerequisites()) { if (TickActorComponentPrerequisite.PrerequisiteTickFunction != MainActorTickFunction) { AddPrerequisite(PreTickFunction, TickActorComponentPrerequisite.PrerequisiteObject.Get(), *TickActorComponentPrerequisite.PrerequisiteTickFunction); } } // Add post-tick function dependencies IslandDependency->AddSubsequent(InteractionSubsystem, PostTickFunction, TickActorComponent); check(bHasTickDependencies); } } else { UE_LOG(LogPoseSearch, Error, TEXT("FInteractionIsland::AddTickDependencies, error while retrieving the tick function for %s"), *TickActorComponent->GetName()); } } else { if (bInIsMainActor) { // PostTickFunction prerequisites should be empty since we haven't add the main actor tick function yet check(PostTickFunction.GetPrerequisites().IsEmpty()); // adding to PreTickFunction all the tick dependencies TickActorComponent has, so it runs after all the tick dependencies of ALL the TickActorComponents in this FInteractionIsland for (FTickPrerequisite& TickActorComponentPrerequisite : TickActorComponent->PrimaryComponentTick.GetPrerequisites()) { AddPrerequisite(PreTickFunction, TickActorComponentPrerequisite.PrerequisiteObject.Get(), *TickActorComponentPrerequisite.PrerequisiteTickFunction); } // Add post-tick function dependencies // it should be this island, but it's not a UObject, so we use the InteractionSubsystem AddPrerequisite(TickActorComponent->PrimaryComponentTick, InteractionSubsystem, PreTickFunction); AddPrerequisite(PostTickFunction, TickActorComponent, TickActorComponent->PrimaryComponentTick); check(!bHasTickDependencies); bHasTickDependencies = true; } else { // PostTickFunction should contain only the tick function to the main actor's one check(PostTickFunction.GetPrerequisites().Num()); // adding to PreTickFunction all the tick dependencies TickActorComponent has, so it runs after all the tick dependencies of ALL the TickActorComponents in this FInteractionIsland // BUT excluding the main actor tick fuction that is PostTickFunction.GetPrerequisites()[0] const FTickFunction* MainActorTickFunction = PostTickFunction.GetPrerequisites()[0].Get(); for (FTickPrerequisite& TickActorComponentPrerequisite : TickActorComponent->PrimaryComponentTick.GetPrerequisites()) { if (TickActorComponentPrerequisite.PrerequisiteTickFunction != MainActorTickFunction) { AddPrerequisite(PreTickFunction, TickActorComponentPrerequisite.PrerequisiteObject.Get(), *TickActorComponentPrerequisite.PrerequisiteTickFunction); } } // Add post-tick function dependencies // it should be this island, but it's not a UObject, so we use the InteractionSubsystem AddPrerequisite(TickActorComponent->PrimaryComponentTick, InteractionSubsystem, PostTickFunction); check(bHasTickDependencies); } } #if ENABLE_ANIM_DEBUG if (GVarPoseSearchInteractionDiagnoseTickDependencies) { TSet VisitedTickFunctions; bool bIsPreTickFunctionValid = ValidateTickDependenciesCycles(PreTickFunction, VisitedTickFunctions); check(VisitedTickFunctions.IsEmpty()); bool bIsPostTickFunctionValid = ValidateTickDependenciesCycles(PostTickFunction, VisitedTickFunctions); check(VisitedTickFunctions.IsEmpty()); if (!bIsPreTickFunctionValid || !bIsPostTickFunctionValid) { // if this validation triggers here, FInteractionIsland is not respectint the already present dependencies, creating cycles UE_LOG(LogPoseSearch, Error, TEXT("============== FInteractionIsland::AddTickDependencies ValidateTickDependencies failed! Analyze the log and tune the FPoseSearchInteractionAvailability::TickPriority ==============")); LogTickDependencies(); } } #endif // ENABLE_ANIM_DEBUG } void FInteractionIsland::RemoveTickDependencies(bool bValidateTickDependencies) { // Called by UPoseSearchInteractionSubsystem::Tick when there aren't animation jobs flying. check(IsInGameThread()); check(TickActorComponents.Num() == IslandAnimContexts.Num()); if (!bHasTickDependencies) { #if ENABLE_ANIM_DEBUG if (bValidateTickDependencies && (bPreTickFunctionExecuted || bPostTickFunctionExecuted)) { if (bPreTickFunctionExecuted) { UE_LOG(LogPoseSearch, Error, TEXT("FInteractionIsland::RemoveTickDependencies, unexpected FPreTickFunction::ExecuteTick run!")); } else { UE_LOG(LogPoseSearch, Error, TEXT("FInteractionIsland::RemoveTickDependencies, unexpected FPostTickFunction::ExecuteTick run!")); } LogTickDependencies(); } #endif // ENABLE_ANIM_DEBUG } else { #if ENABLE_ANIM_DEBUG if (bValidateTickDependencies && (!bPreTickFunctionExecuted || !bPostTickFunctionExecuted)) { if (!bPreTickFunctionExecuted) { UE_LOG(LogPoseSearch, Error, TEXT("FInteractionIsland::RemoveTickDependencies, expected FPreTickFunction::ExecuteTick didn't run!")); } else { UE_LOG(LogPoseSearch, Error, TEXT("FInteractionIsland::RemoveTickDependencies, expected FPostTickFunction::ExecuteTick didn't run!")); } LogTickDependencies(); } #endif // ENABLE_ANIM_DEBUG // removing ALL the prerequisites form the PreTickFunction while (!PreTickFunction.GetPrerequisites().IsEmpty()) { const FTickPrerequisite& PreTickFunctionPrerequisite = PreTickFunction.GetPrerequisites().Last(); PreTickFunction.RemovePrerequisite(PreTickFunctionPrerequisite.PrerequisiteObject.Get(), *PreTickFunctionPrerequisite.PrerequisiteTickFunction); } bool bMainActor = true; for (TWeakObjectPtr& TickActorComponentPtr : TickActorComponents) { if (UActorComponent* TickActorComponent = TickActorComponentPtr.Get()) { if(IInteractionIslandDependency* IslandDependency = FindCustomDependency(TickActorComponent)) { if (bMainActor) { IslandDependency->RemoveSubsequent(InteractionSubsystem, PreTickFunction, TickActorComponent); IslandDependency->RemovePrerequisite(InteractionSubsystem, PostTickFunction, TickActorComponent); } else { IslandDependency->RemoveSubsequent(InteractionSubsystem, PostTickFunction, TickActorComponent); } } else { if (bMainActor) { TickActorComponent->PrimaryComponentTick.RemovePrerequisite(InteractionSubsystem, PreTickFunction); PostTickFunction.RemovePrerequisite(TickActorComponent, TickActorComponent->PrimaryComponentTick); } else { TickActorComponent->PrimaryComponentTick.RemovePrerequisite(InteractionSubsystem, PostTickFunction); } } } bMainActor = false; } check(PreTickFunction.GetPrerequisites().IsEmpty()); check(PostTickFunction.GetPrerequisites().IsEmpty()); bHasTickDependencies = false; } #if ENABLE_ANIM_DEBUG bPreTickFunctionExecuted = false; bPostTickFunctionExecuted = false; #endif // ENABLE_ANIM_DEBUG } void FInteractionIsland::InjectToActor(const UObject* AnimContext, bool bAddTickDependencies) { check(IsInGameThread()); // Called by UPoseSearchInteractionSubsystem::Tick when there aren't animation jobs flying. No need to FScopeLock Lock(&Mutex); if (AnimContext) { #if ENABLE_ANIM_DEBUG if (bPreTickFunctionExecuted || bPostTickFunctionExecuted) { if (bPreTickFunctionExecuted) { UE_LOG(LogPoseSearch, Error, TEXT("FInteractionIsland::InjectToActor, unexpected FPreTickFunction::ExecuteTick run!")); } else { UE_LOG(LogPoseSearch, Error, TEXT("FInteractionIsland::InjectToActor, unexpected FPostTickFunction::ExecuteTick run!")); } LogTickDependencies(); } #endif // ENABLE_ANIM_DEBUG if (UActorComponent* TickActorComponent = FindComponentForTickDependencies(AnimContext)) { const bool bIsMainActor = IslandAnimContexts.IsEmpty(); // tick order: // ALL TickActorComponents prerequisites (ultimately we're looking to have UCharacterMovementComponent or UCharacterMoverComponent ticked) -> // Island.PreTickFunction -> // first injected TickActorComponent (USkeletalMeshComponent, or UAnimNextComponent) -> // Island.PostTickFunction -> // other TickActorComponent(s) TickActorComponents.AddUnique(TickActorComponent); IslandAnimContexts.AddUnique(AnimContext); // making sure that if we add a unique TickActorComponent, we add as well a unique PostTickComponent // (so we can remove them later on in a consistent fashion) check(TickActorComponents.Num() == IslandAnimContexts.Num()); if (bAddTickDependencies) { AddTickDependencies(TickActorComponent, bIsMainActor); } else { check(!bHasTickDependencies); } } } } void FInteractionIsland::AddSearchContext(const FInteractionSearchContext& SearchContext) { #if DO_CHECK check(SearchContext.CheckForConsistency()); #endif check(IsInGameThread()); SearchContexts.Add(SearchContext); } void FInteractionIsland::Uninitialize(bool bValidateTickDependencies) { #if ENABLE_ANIM_DEBUG if (GVarPoseSearchInteractionDiagnoseTickDependencies) { TSet VisitedTickFunctions; bool bIsPreTickFunctionValid = ValidateTickDependenciesCycles(PreTickFunction, VisitedTickFunctions); check(VisitedTickFunctions.IsEmpty()); bool bIsPostTickFunctionValid = ValidateTickDependenciesCycles(PostTickFunction, VisitedTickFunctions); check(VisitedTickFunctions.IsEmpty()); if (!bIsPreTickFunctionValid || !bIsPostTickFunctionValid) { // if this validation triggers here, some additional tick dependency outside FInteractionIsland has been injected witout respecting the already present dependencies, creating cycles UE_LOG(LogPoseSearch, Error, TEXT("============== FInteractionIsland::Uninitialize ValidateTickDependencies failed! ==============")); LogTickDependencies(); } } #endif // ENABLE_ANIM_DEBUG RemoveTickDependencies(bValidateTickDependencies); if (IsInitialized()) { TickActorComponents.Reset(); IslandAnimContexts.Reset(); SearchContexts.Reset(); SearchResults.Reset(); bSearchPerfomed = false; } else { check(TickActorComponents.IsEmpty() && IslandAnimContexts.IsEmpty() && SearchContexts.IsEmpty() && SearchResults.IsEmpty() && !bSearchPerfomed); } } bool FInteractionIsland::HasTickDependencies() const { return bHasTickDependencies; } bool FInteractionIsland::IsInitialized() const { return !SearchContexts.IsEmpty(); } const UObject* FInteractionIsland::GetMainAnimContext() const { return !IslandAnimContexts.IsEmpty() ? IslandAnimContexts[0].Get() : nullptr; } const AActor* FInteractionIsland::GetMainActor() const { if (IsInitialized()) { return GetContextOwningActor(GetMainAnimContext()); } return nullptr; } #if ENABLE_ANIM_DEBUG void FInteractionIsland::LogTickDependencies(const TConstArrayView> TickActorComponents, int32 InteractionIslandIndex) { check(IsInGameThread()); for (const TWeakObjectPtr& TickActorComponentPtr : TickActorComponents) { if (UActorComponent* TickActorComponent = TickActorComponentPtr.Get()) { UE_LOG(LogPoseSearch, Log, TEXT("============== %s (Island %d) =============="), *TickActorComponent->GetOwner()->GetName(), InteractionIslandIndex); if (IInteractionIslandDependency* IslandDependency = FindCustomDependency(TickActorComponent)) { // AnimNextComponent case if (const FTickFunction* TickActorComponentTickFunction = IslandDependency->FindTickFunction(TickActorComponent)) { LogTickFunction(*const_cast(TickActorComponentTickFunction), ENamedThreads::GameThread, true, 1); } else { UE_LOG(LogPoseSearch, Error, TEXT("FInteractionIsland::LogTickDependencies, error while retrieving the tick function for to %s"), *TickActorComponent->GetName()); } } else { // SkeletalMeshComponent / AnimInstance case LogTickFunction(TickActorComponent->PrimaryComponentTick, ENamedThreads::GameThread, true, 1); } } else { UE_LOG(LogPoseSearch, Log, TEXT("============== !!!Missing Actor!!! (Island %d) =============="), InteractionIslandIndex); } } } void FInteractionIsland::LogTickDependencies() const { const int32 InteractionIslandIndex = InteractionSubsystem->GetInteractionIslands().IndexOfByKey(this); if (IsInGameThread()) { LogTickDependencies(TickActorComponents, InteractionIslandIndex); } else { TArray> TickActorComponentsCopy = TickActorComponents; FFunctionGraphTask::CreateAndDispatchWhenReady([TickActorComponentsCopy, InteractionIslandIndex]() { LogTickDependencies(TickActorComponentsCopy, InteractionIslandIndex); }, TStatId(), nullptr, ENamedThreads::GameThread); } } #endif // ENABLE_ANIM_DEBUG bool FInteractionIsland::DoSearch_AnyThread(const UObject* AnimContext, const TConstArrayView ValidInteractionSearches, FPoseSearchBlueprintResult& Result) { check(AnimContext); if (bSearchPerfomed) { // we now support multiple searches from the same AnimContext, and we return the already performed // and cached result, so check(AnimContext != GetMainAnimContext()) is no longer valid! return GetResult_AnyThread(AnimContext, Result); } // searches are performed only on the MainAnimContext / MainActor if (AnimContext != GetMainAnimContext()) { // search failed, because AnimContext is not the context from the main actor. continuing the search would lead to threading issues, // since the threading model expect the main actor to perform the search! (this happens when recompiling animation blue prints..) Result = FPoseSearchBlueprintResult(); return false; } QUICK_SCOPE_CYCLE_COUNTER(STAT_UPoseSearchInteractionInteractionIsland_Search); FMemMark Mark(FMemStack::Get()); TArray>> AnimContexts; TArray>> PoseHistories; TArray> PoseSearchResults; // SearchContexts are modified only by UPoseSearchInteractionSubsystem::Tick and constant otherwise, so it's safe to access them in a threaded enviroment without locks PoseSearchResults.SetNum(SearchContexts.Num()); for (int32 SearchIndex = 0; SearchIndex < SearchContexts.Num(); ++SearchIndex) { FInteractionSearchContext& SearchContext = SearchContexts[SearchIndex]; const UPoseSearchDatabase* Database = SearchContext.Database.Get(); if (!Database) { UE_LOG(LogPoseSearch, Error, TEXT("FInteractionIsland::DoSearch_AnyThread invalid context database")); return false; } if (!Database->Schema) { UE_LOG(LogPoseSearch, Error, TEXT("FInteractionIsland::DoSearch_AnyThread invalid schema for context database %s"), *Database->GetName()); return false; } const int32 NumRoles = SearchContext.Num(); AnimContexts.Reset(); PoseHistories.Reset(); for (int32 RoleIndex = 0; RoleIndex < NumRoles; ++RoleIndex) { const UObject* SearchContextAnimContext = SearchContext.GetAnimContext(RoleIndex); if (!SearchContextAnimContext) { UE_LOG(LogPoseSearch, Error, TEXT("FInteractionIsland::DoSearch_AnyThread null anim context")); return false; } AnimContexts.Add(SearchContextAnimContext); const UE::PoseSearch::IPoseHistory* PoseHistory = SearchContext.GetPoseHistory(RoleIndex); if (!PoseHistory) { UE_LOG(LogPoseSearch, Error, TEXT("FInteractionIsland::DoSearch_AnyThread null PoseHistory")); return false; } PoseHistories.Add(PoseHistory); } const UObject* AssetsToSearch[] = { Database }; FPoseSearchFutureProperties PoseSearchFutureProperties; // @todo: we could perform multiple UPoseSearchLibrary::MotionMatch in parallel! PoseSearchResults[SearchIndex] = UPoseSearchLibrary::MotionMatch(AnimContexts, SearchContext.GetRoles(), PoseHistories, AssetsToSearch, SearchContext.GetContinuingProperties(), PoseSearchFutureProperties, FPoseSearchEvent()); } InitSearchResults(SearchResults, PoseSearchResults, SearchContexts); bSearchPerfomed = true; return GetResult_AnyThread(AnimContext, Result); } bool FInteractionIsland::GetResult_AnyThread(const UObject* AnimContext, FPoseSearchBlueprintResult& Result, bool bCompareOwningActors) { check(AnimContext); const AActor* Actor = bCompareOwningActors ? GetContextOwningActor(AnimContext) : nullptr; // looking for AnimContext in SearchResults to fill up Result for (const FInteractionSearchResult& SearchResult : SearchResults) { const FInteractionSearchContext& SearchContext = SearchContexts[SearchResult.SearchIndex]; for (int32 AnimContextIndex = 0; AnimContextIndex < SearchContext.Num(); ++AnimContextIndex) { bool bAnimContextFound = false; if (bCompareOwningActors) { bAnimContextFound = GetContextOwningActor(SearchContext.GetAnimContext(AnimContextIndex)) == Actor; } else { bAnimContextFound = SearchContext.GetAnimContext(AnimContextIndex) == AnimContext; } if (bAnimContextFound) { // @todo: perhaps add a custom Result.InitFrom(SearchResult, 1.f) for MM interactions const UPoseSearchDatabase* Database = SearchResult.Database.Get(); check(Database); const FSearchIndexAsset* SearchIndexAsset = SearchResult.GetSearchIndexAsset(); check(SearchIndexAsset); const FPoseSearchDatabaseAnimationAssetBase* DatabaseAnimationAssetBase = Database->GetDatabaseAnimationAsset(*SearchIndexAsset); check(DatabaseAnimationAssetBase); Result.SelectedAnim = DatabaseAnimationAssetBase->GetAnimationAsset(); Result.SelectedTime = SearchResult.AssetTime; Result.bIsContinuingPoseSearch = SearchResult.bIsContinuingPoseSearch; Result.bLoop = SearchIndexAsset->IsLooping(); Result.bIsMirrored = SearchIndexAsset->IsMirrored(); Result.BlendParameters = SearchIndexAsset->GetBlendParameters(); Result.SelectedDatabase = Database; Result.SearchCost = SearchResult.PoseCost; Result.bIsInteraction = true; Result.Role = SearchContext.GetRole(AnimContextIndex); // figuring out the WantedPlayRate Result.WantedPlayRate = 1.f; //if (Future.Animation && Future.IntervalTime > 0.f) //{ // if (const UPoseSearchFeatureChannel_PermutationTime* PermutationTimeChannel = Database->Schema->FindFirstChannelOfType()) // { // const FSearchIndex& SearchIndex = Database->GetSearchIndex(); // if (!SearchIndex.IsValuesEmpty()) // { // TConstArrayView ResultData = Database->GetSearchIndex().GetPoseValues(SearchResult.PoseIdx); // const float ActualIntervalTime = PermutationTimeChannel->GetPermutationTime(ResultData); // ProviderResult.WantedPlayRate = ActualIntervalTime / Future.IntervalTime; // } // } //} if (const UMultiAnimAsset* MultiAnimAsset = Cast(Result.SelectedAnim)) { const int32 NumRoles = MultiAnimAsset->GetNumRoles(); Result.ActorRootTransforms.SetNum(NumRoles); Result.ActorRootBoneTransforms.SetNum(NumRoles); Result.AnimContexts.SetNum(NumRoles); const FRoleToIndex InteractionSearchContextRoleToIndex = MakeRoleToIndex(SearchContext.GetRoles()); for (int32 MultiAnimAssetRoleIndex = 0; MultiAnimAssetRoleIndex < NumRoles; ++MultiAnimAssetRoleIndex) { if (const int32* InteractionSearchContextRoleIndex = InteractionSearchContextRoleToIndex.Find(MultiAnimAsset->GetRole(MultiAnimAssetRoleIndex))) { Result.ActorRootTransforms[MultiAnimAssetRoleIndex] = SearchResult.ActorRootTransforms[*InteractionSearchContextRoleIndex]; Result.ActorRootBoneTransforms[MultiAnimAssetRoleIndex] = SearchResult.ActorRootBoneTransforms[*InteractionSearchContextRoleIndex]; Result.AnimContexts[MultiAnimAssetRoleIndex] = SearchContext.GetAnimContext(*InteractionSearchContextRoleIndex); } else { Result.ActorRootTransforms[MultiAnimAssetRoleIndex] = FTransform::Identity; Result.ActorRootBoneTransforms[MultiAnimAssetRoleIndex] = FTransform::Identity; Result.AnimContexts[MultiAnimAssetRoleIndex] = nullptr; } } } else { // @todo: should we support trivial "interactions" with only a character defined using some other assets rather then UMultiAnimAsset? check(Result.AnimContexts.Num() == 1); Result.ActorRootTransforms = SearchResult.ActorRootTransforms; Result.ActorRootBoneTransforms = SearchResult.ActorRootBoneTransforms; Result.AnimContexts.SetNum(1); Result.AnimContexts[0] = SearchContext.GetAnimContext(0); } // we found our AnimContext: we can stop searching return true; } } } Result = FPoseSearchBlueprintResult(); return false; } const FInteractionSearchResult* FInteractionIsland::FindSearchResult(const FInteractionSearchContext& SearchContext) const { // called only by UPoseSearchInteractionSubsystem::Tick via UPoseSearchInteractionSubsystem::PopulateContinuingProperties so no need to lock SearchResultsMutex to protect the read of SearchResults check(IsInGameThread()); // searching for InSearchContext in all the SearchContexts referenced by valid active SearchResults for (const FInteractionSearchResult& SearchResult : SearchResults) { const FInteractionSearchContext& LocalSearchContext = SearchContexts[SearchResult.SearchIndex]; if (LocalSearchContext.IsEquivalent(SearchContext)) { return &SearchResult; } } return nullptr; } } // namespace UE::PoseSearch