2864 lines
110 KiB
C++
2864 lines
110 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#if WITH_EDITOR
|
|
|
|
#include "PoseSearch/PoseSearchDerivedData.h"
|
|
#include "Animation/AnimComposite.h"
|
|
#include "Animation/AnimMontage.h"
|
|
#include "Animation/AnimSequence.h"
|
|
#include "Animation/BlendSpace.h"
|
|
#include "AssetRegistry/ARFilter.h"
|
|
#include "AssetRegistry/AssetRegistryModule.h"
|
|
#include "Chooser/Internal/Chooser.h"
|
|
#include "DerivedDataCache.h"
|
|
#include "DerivedDataRequestOwner.h"
|
|
#include "StructUtils/InstancedStruct.h"
|
|
#include "Misc/CoreDelegates.h"
|
|
#if ENABLE_ANIM_DEBUG
|
|
#include "Misc/FileHelper.h"
|
|
#endif //ENABLE_ANIM_DEBUG
|
|
#include "PoseSearch/PoseSearchAnimNotifies.h"
|
|
#include "PoseSearch/PoseSearchAssetIndexer.h"
|
|
#include "PoseSearch/PoseSearchDatabase.h"
|
|
#include "PoseSearch/PoseSearchDefines.h"
|
|
#include "PoseSearch/PoseSearchDerivedDataKey.h"
|
|
#include "PoseSearch/PoseSearchFeatureChannel.h"
|
|
#include "PoseSearch/PoseSearchNormalizationSet.h"
|
|
#include "PoseSearch/PoseSearchSchema.h"
|
|
#include "PoseSearchEigenHelper.h"
|
|
#include "ProfilingDebugging/CookStats.h"
|
|
#include "ScopedTransaction.h"
|
|
#include "Serialization/BulkDataRegistry.h"
|
|
#include "UObject/NoExportTypes.h"
|
|
#include "UObject/PackageReload.h"
|
|
#include "Interfaces/ITargetPlatformManagerModule.h"
|
|
|
|
#define LOCTEXT_NAMESPACE "PoseSearchDerivedData"
|
|
|
|
namespace UE::PoseSearch
|
|
{
|
|
#if ENABLE_ANIM_DEBUG
|
|
|
|
enum EMotionMatchTestFlags
|
|
{
|
|
// no additional tests will be performed
|
|
None = 0,
|
|
|
|
// cache will be invalidated every frame (to stress test DDC cancellation while tasks are flying if !WaitForTaskCompletion)
|
|
InvalidateCache = 1 << 0,
|
|
|
|
// cache will be invalidated once the flying tasks are ended
|
|
WaitForTaskCompletion = 1 << 1,
|
|
|
|
// we'll force the database re-indexing and compare the result SearchIndex with the one retrieved via DDC
|
|
ForceIndexing = 1 << 2,
|
|
|
|
// test KDTree Construct determinism
|
|
TestKDTreeConstructDeterminism = 1 << 3,
|
|
|
|
// validating the kdtree construction
|
|
ValidateKDTreeConstruct = 1 << 4,
|
|
|
|
// test VPTree Construct determinism
|
|
TestVPTreeConstructDeterminism = 1 << 5,
|
|
|
|
// validating the vptree construction
|
|
ValidateVPTreeConstruct = 1 << 6,
|
|
|
|
// test IndexDatabase determinism
|
|
TestIndexDatabaseDeterminism = 1 << 7,
|
|
|
|
// test PruneDuplicateValues determinism
|
|
TestPruneDuplicateValuesDeterminism = 1 << 8,
|
|
|
|
// test PruneDuplicatePCAValues determinism
|
|
TestPruneDuplicatePCAValuesDeterminism = 1 << 9,
|
|
|
|
// validating the data we gave to DDC is stored correctly
|
|
ValidateDDC = 1 << 10,
|
|
|
|
// validating SynchronizeWithExternalDependencies doesn't alter the database AnimationAssets order
|
|
ValidateSynchronizeWithExternalDependenciesDeterminism = 1 << 11,
|
|
|
|
// test FAnimationAssetSampler determinism
|
|
TestAssetSamplerDeterminism = 1 << 12,
|
|
|
|
// test FAnimationAssetSampler determinism across multiple editro executions. It'll store some bin files in \Engine\TestAssetSamplerDeterminism
|
|
TestAssetSamplerDeterminismFromPreviousExecution = 1 << 13,
|
|
|
|
// test DDC key generation determinism
|
|
TestDDCKeyDeterminism = 1 << 14,
|
|
};
|
|
|
|
static int32 GVarMotionMatchTestFlags = EMotionMatchTestFlags::None;
|
|
static FAutoConsoleVariableRef CVarMotionMatchTestFlags(TEXT("a.MotionMatch.TestFlags"), GVarMotionMatchTestFlags, TEXT("Test Motion Matching using EMotionMatchTestFlags"));
|
|
|
|
static int32 GVarMotionMatchTestNumIterations = 10;
|
|
static FAutoConsoleVariableRef CVarMotionMatchTestNumIterations(TEXT("a.MotionMatch.TestNumIterations"), GVarMotionMatchTestNumIterations, TEXT("Test Motion Matching Num Iterations"));
|
|
static bool AnyTestFlags(int32 Flags) { return (GVarMotionMatchTestFlags & Flags) != 0; }
|
|
#endif // ENABLE_ANIM_DEBUG
|
|
|
|
static bool GVarMotionMatchReindexCancelledDatabases = false;
|
|
static FAutoConsoleVariableRef CVarMotionMatchReindexCancelledDatabases(TEXT("a.MotionMatch.ReindexCancelledDatabases"), GVarMotionMatchReindexCancelledDatabases, TEXT("Reindex Cancelled Databases"));
|
|
|
|
// Experimental, this feature might be removed without warning, not for production use
|
|
static bool GVarMotionMatchReindexAllReferencedDatabases = true;
|
|
static FAutoConsoleVariableRef CVarMotionMatchReindexAllReferencedDatabases(TEXT("a.MotionMatch.ReindexAllReferencedDatabases"), GVarMotionMatchReindexAllReferencedDatabases, TEXT("Reindex All Referenced Databases"));
|
|
|
|
// Experimental, this feature might be removed without warning, not for production use
|
|
static int32 GVarMotionMatchPartialKeyHashesMode = 0;
|
|
static FAutoConsoleVariableRef CVarMotionMatchPartialKeyHashesMode(TEXT("a.MotionMatch.PartialKeyHashesMode"), GVarMotionMatchPartialKeyHashesMode, TEXT("0: use partial key hashes, 1: do not use partial key hashes, 2: do not use and validate partial key hashes"));
|
|
|
|
static const UE::DerivedData::FValueId Id(UE::DerivedData::FValueId::FromName("Data"));
|
|
static const UE::DerivedData::FCacheBucket Bucket("PoseSearchDatabase");
|
|
|
|
#if ENABLE_COOK_STATS
|
|
static FCookStats::FDDCResourceUsageStats UsageStats;
|
|
static FCookStatsManager::FAutoRegisterCallback RegisterCookStats([](FCookStatsManager::AddStatFuncRef AddStat)
|
|
{
|
|
UsageStats.LogStats(AddStat, TEXT("MotionMatching.Usage"), TEXT(""));
|
|
});
|
|
#endif // ENABLE_COOK_STATS
|
|
|
|
typedef TSet<const UPoseSearchDatabase*, DefaultKeyFuncs<const UPoseSearchDatabase*>, TInlineSetAllocator<256>> FDatabaseSet;
|
|
|
|
static void RecursivePopulateDependentDatabases(const UPoseSearchDatabase* Database, FDatabaseSet& DatabaseSet)
|
|
{
|
|
if (Database)
|
|
{
|
|
bool bIsAlreadyInSet = false;
|
|
DatabaseSet.Add(Database, &bIsAlreadyInSet);
|
|
|
|
if (!bIsAlreadyInSet)
|
|
{
|
|
if (const UPoseSearchNormalizationSet* NormalizationSet = Database->NormalizationSet)
|
|
{
|
|
for (const UPoseSearchDatabase* DependentDatabase : NormalizationSet->Databases)
|
|
{
|
|
RecursivePopulateDependentDatabases(DependentDatabase, DatabaseSet);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// helper struct to calculate mean deviations
|
|
struct FMeanDeviationCalculator
|
|
{
|
|
private:
|
|
struct FEntry
|
|
{
|
|
int32 SchemaIndex = INDEX_NONE; // index of the Channel associated Schemas / SearchIndexBases index
|
|
const UPoseSearchFeatureChannel* Channel = nullptr;
|
|
};
|
|
|
|
typedef TArray<FEntry, TInlineAllocator<16>> FEntries; // array of FEntry with channels that can be normalized together (for example it contains all the phases of the left foot from different schemas)
|
|
typedef TArray<FEntries, TInlineAllocator<16>> FEntriesGroup; // array of FEntries incompatible between each other
|
|
|
|
static void Add(const UPoseSearchFeatureChannel* Channel, int32 SchemaIndex, FEntriesGroup& EntriesGroup)
|
|
{
|
|
bool bEntryFound = false;
|
|
for (FEntries& Entries : EntriesGroup)
|
|
{
|
|
if (Entries[0].Channel->CanBeNormalizedWith(Channel))
|
|
{
|
|
Entries.Add({ SchemaIndex, Channel });
|
|
bEntryFound = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!bEntryFound)
|
|
{
|
|
FEntries& Entries = EntriesGroup[EntriesGroup.AddDefaulted()];
|
|
Entries.Add({ SchemaIndex, Channel });
|
|
}
|
|
}
|
|
|
|
static void AnalyzeChannelRecursively(const UPoseSearchFeatureChannel* Channel, int32 SchemaIndex, FEntriesGroup& EntriesGroup)
|
|
{
|
|
const TConstArrayView<TObjectPtr<UPoseSearchFeatureChannel>> SubChannels = Channel->GetSubChannels();
|
|
if (SubChannels.Num() == 0)
|
|
{
|
|
Add(Channel, SchemaIndex, EntriesGroup);
|
|
}
|
|
else
|
|
{
|
|
// the channel is a group channel, so we AnalyzeChannelRecursively
|
|
for (const TObjectPtr<UPoseSearchFeatureChannel>& SubChannelPtr : Channel->GetSubChannels())
|
|
{
|
|
if (const UPoseSearchFeatureChannel* SubChannel = SubChannelPtr.Get())
|
|
{
|
|
AnalyzeChannelRecursively(SubChannel, SchemaIndex, EntriesGroup);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void AnalyzeSchemas(TConstArrayView<const UPoseSearchSchema*> Schemas, FEntriesGroup& EntriesGroup)
|
|
{
|
|
for (int32 SchemaIndex = 0; SchemaIndex < Schemas.Num(); ++SchemaIndex)
|
|
{
|
|
const UPoseSearchSchema* Schema = Schemas[SchemaIndex];
|
|
for (const TObjectPtr<UPoseSearchFeatureChannel>& ChannelPtr : Schema->GetChannels())
|
|
{
|
|
AnalyzeChannelRecursively(ChannelPtr.Get(), SchemaIndex, EntriesGroup);
|
|
}
|
|
}
|
|
}
|
|
|
|
// given an array of channels that can be normalized together (Entries), with the same cardinality (Entries[0].Channel->GetChannelCardinality()),
|
|
// it'll calculate the mean deviation of the associated data (from SearchIndexBases)
|
|
static float CalculateEntriesMeanDeviation(const FEntries& Entries, TConstArrayView<FSearchIndexBase> SearchIndexBases, TConstArrayView<const UPoseSearchSchema*> Schemas)
|
|
{
|
|
check(Schemas.Num() == SearchIndexBases.Num());
|
|
|
|
const int32 EntriesNum = Entries.Num();
|
|
check(EntriesNum > 0);
|
|
|
|
const int32 Cardinality = Entries[0].Channel->GetChannelCardinality();
|
|
check(Cardinality > 0);
|
|
|
|
int32 TotalNumValuesVectors = 0;
|
|
for (int32 EntryIdx = 0; EntryIdx < EntriesNum; ++EntryIdx)
|
|
{
|
|
const FEntry& Entry = Entries[EntryIdx];
|
|
check(Cardinality == Entry.Channel->GetChannelCardinality());
|
|
|
|
const int32 DataSetIdx = Entry.SchemaIndex;
|
|
const UPoseSearchSchema* Schema = Schemas[DataSetIdx];
|
|
const FSearchIndexBase& SearchIndexBase = SearchIndexBases[DataSetIdx];
|
|
|
|
TotalNumValuesVectors += SearchIndexBase.GetNumValuesVectors(Schema->SchemaCardinality);
|
|
}
|
|
|
|
int32 AccumulatedNumValuesVectors = 0;
|
|
RowMajorMatrix CenteredSubPoseMatrix(TotalNumValuesVectors, Cardinality);
|
|
for (int32 EntryIdx = 0; EntryIdx < EntriesNum; ++EntryIdx)
|
|
{
|
|
const FEntry& Entry = Entries[EntryIdx];
|
|
const int32 DataSetIdx = Entry.SchemaIndex;
|
|
|
|
const UPoseSearchSchema* Schema = Schemas[DataSetIdx];
|
|
const FSearchIndexBase& SearchIndexBase = SearchIndexBases[DataSetIdx];
|
|
|
|
const int32 NumValuesVectors = SearchIndexBase.GetNumValuesVectors(Schema->SchemaCardinality);
|
|
|
|
// Map input buffer with NumValuesVectors as rows and NumDimensions as cols
|
|
RowMajorMatrixMapConst PoseMatrixSourceMap(SearchIndexBase.Values.GetData(), NumValuesVectors, Schema->SchemaCardinality);
|
|
|
|
// Given the sub matrix for the features, find the average distance to the feature's centroid.
|
|
CenteredSubPoseMatrix.block(AccumulatedNumValuesVectors, 0, NumValuesVectors, Cardinality) = PoseMatrixSourceMap.block(0, Entry.Channel->GetChannelDataOffset(), NumValuesVectors, Cardinality);
|
|
AccumulatedNumValuesVectors += NumValuesVectors;
|
|
}
|
|
|
|
RowMajorVector SampleMean = CenteredSubPoseMatrix.colwise().mean();
|
|
CenteredSubPoseMatrix = CenteredSubPoseMatrix.rowwise() - SampleMean;
|
|
|
|
// after mean centering the data, the average distance to the centroid is simply the average norm.
|
|
const float FeatureMeanDeviation = CenteredSubPoseMatrix.rowwise().norm().mean();
|
|
|
|
return FeatureMeanDeviation;
|
|
}
|
|
|
|
public:
|
|
|
|
// it returns an array of dimension Schemas[0]->SchemaCardinality containing the mean deviation calculated from the data passed in with SearchIndexBases following the layout described in the schemas channels:
|
|
// channels from all the schemas get collected in groups that can be normalized together (FEntriesGroup, populated in AnalyzeSchemas) and then those homogeneous (in cardinality and meaning) groups get processed
|
|
// one by one in CalculateEntriesMeanDeviation to extract the group mean deviation against the input data contained in SearchIndexBases
|
|
static TArray<float> Calculate(TConstArrayView<FSearchIndexBase> SearchIndexBases, TConstArrayView<const UPoseSearchSchema*> Schemas)
|
|
{
|
|
// This method performs a modified z-score normalization where features are normalized
|
|
// by mean absolute deviation rather than standard deviation. Both methods are preferable
|
|
// here to min-max scaling because they preserve outliers.
|
|
//
|
|
// Mean absolute deviation is preferred here over standard deviation because the latter
|
|
// emphasizes outliers since squaring the distance from the mean increases variance
|
|
// exponentially rather than additively and square rooting the sum of squares does not
|
|
// remove that bias. [1]
|
|
//
|
|
// References:
|
|
// [1] Gorard, S. (2005), "Revisiting a 90-Year-Old Debate: The Advantages of the Mean Deviation."
|
|
// British Journal of Educational Studies, 53: 417-430.
|
|
|
|
int32 ThisSchemaIndex = 0;
|
|
check(SearchIndexBases.Num() == Schemas.Num() && Schemas.Num() > ThisSchemaIndex);
|
|
const UPoseSearchSchema* ThisSchema = Schemas[ThisSchemaIndex];
|
|
const int32 NumDimensions = ThisSchema->SchemaCardinality;
|
|
|
|
TArray<float> MeanDeviations;
|
|
MeanDeviations.Init(1.f, NumDimensions);
|
|
RowMajorVectorMap MeanDeviationsMap(MeanDeviations.GetData(), 1, NumDimensions);
|
|
|
|
const EPoseSearchDataPreprocessor DataPreprocessor = ThisSchema->DataPreprocessor;
|
|
if (SearchIndexBases[ThisSchemaIndex].GetNumPoses() > 0 && (DataPreprocessor != EPoseSearchDataPreprocessor::None))
|
|
{
|
|
FEntriesGroup EntriesGroup;
|
|
|
|
AnalyzeSchemas(Schemas, EntriesGroup);
|
|
|
|
for (const FEntries& Entries : EntriesGroup)
|
|
{
|
|
for (const FEntry& Entry : Entries)
|
|
{
|
|
if (Entry.Channel->GetChannelCardinality() > 0 && Entry.SchemaIndex == ThisSchemaIndex)
|
|
{
|
|
const float FeatureMeanDeviation = CalculateEntriesMeanDeviation(Entries, SearchIndexBases, Schemas);
|
|
// the associated data to all the Entries data is going to be used to calculate the deviation of Deviation[Entry.Channel->GetChannelDataOffset()] to Deviation[Entry.Channel->GetChannelDataOffset() + Entry.Channel->GetChannelCardinality()]
|
|
|
|
// Fill the feature's corresponding scaling axes with the average distance
|
|
// Avoid scaling by zero by leaving near-zero deviations as 1.0
|
|
static const float MinFeatureMeanDeviation = 0.1f;
|
|
MeanDeviationsMap.segment(Entry.Channel->GetChannelDataOffset(), Entry.Channel->GetChannelCardinality()).setConstant(FeatureMeanDeviation > MinFeatureMeanDeviation ? FeatureMeanDeviation : 1.f);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return MeanDeviations;
|
|
}
|
|
};
|
|
|
|
typedef TArray<FFloatRange, TInlineAllocator<32>> FValidRanges;
|
|
|
|
static void FindValidRanges(const FPoseSearchDatabaseAnimationAssetBase* DatabaseAsset, const FVector& BlendParameters, const FFloatInterval& ExcludeFromDatabaseParameters, FValidRanges& ValidRanges)
|
|
{
|
|
check(DatabaseAsset);
|
|
|
|
const bool bIsLooping = DatabaseAsset->IsLooping();
|
|
const float PlayLength = DatabaseAsset->GetPlayLength(BlendParameters);
|
|
|
|
const FFloatInterval EffectiveSamplingInterval = DatabaseAsset->GetEffectiveSamplingRange(BlendParameters);
|
|
FFloatRange EffectiveSamplingRange = FFloatRange::Inclusive(EffectiveSamplingInterval.Min, EffectiveSamplingInterval.Max);
|
|
if (!bIsLooping)
|
|
{
|
|
const FFloatRange ExcludeFromDatabaseRange(ExcludeFromDatabaseParameters.Min, PlayLength + ExcludeFromDatabaseParameters.Max);
|
|
EffectiveSamplingRange = FFloatRange::Intersection(EffectiveSamplingRange, ExcludeFromDatabaseRange);
|
|
}
|
|
|
|
// start from a single interval defined by the database sequence sampling range
|
|
ValidRanges.Reset();
|
|
ValidRanges.Add(EffectiveSamplingRange);
|
|
|
|
for (int32 RoleIndex = 0; RoleIndex < DatabaseAsset->GetNumRoles(); ++RoleIndex)
|
|
{
|
|
const FRole Role = DatabaseAsset->GetRole(RoleIndex);
|
|
if (UAnimSequenceBase* SequenceBase = Cast<UAnimSequenceBase>(DatabaseAsset->GetAnimationAssetForRole(Role)))
|
|
{
|
|
FAnimNotifyContext NotifyContext;
|
|
SequenceBase->GetAnimNotifies(0.0f, PlayLength, NotifyContext);
|
|
|
|
for (const FAnimNotifyEventReference& EventReference : NotifyContext.ActiveNotifies)
|
|
{
|
|
if (const FAnimNotifyEvent* NotifyEvent = EventReference.GetNotify())
|
|
{
|
|
if (const UAnimNotifyState_PoseSearchExcludeFromDatabase* ExclusionNotifyState = Cast<const UAnimNotifyState_PoseSearchExcludeFromDatabase>(NotifyEvent->NotifyStateClass))
|
|
{
|
|
FFloatRange ExclusionRange = FFloatRange::Inclusive(NotifyEvent->GetTime(), NotifyEvent->GetTime() + NotifyEvent->GetDuration());
|
|
|
|
// Split every valid range based on the exclusion range just found. Because this might increase the
|
|
// number of ranges in ValidRanges, the algorithm iterates from end to start.
|
|
for (int32 RangeIdx = ValidRanges.Num() - 1; RangeIdx >= 0; --RangeIdx)
|
|
{
|
|
const FFloatRange EvaluatedRange = ValidRanges[RangeIdx];
|
|
ValidRanges.RemoveAt(RangeIdx);
|
|
|
|
const TArray<FFloatRange> Diff = FFloatRange::Difference(EvaluatedRange, ExclusionRange);
|
|
ValidRanges.Append(Diff);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// returns false in case of errors
|
|
static bool InitSearchIndexAssets(FSearchIndexBase& SearchIndex, const UPoseSearchDatabase* DatabaseToLookForAssets, const UPoseSearchSchema* Schema, const FFloatInterval& ExcludeFromDatabaseParameters)
|
|
{
|
|
using namespace UE::PoseSearch;
|
|
|
|
check(Schema);
|
|
|
|
SearchIndex.Assets.Reset();
|
|
FValidRanges ValidRanges;
|
|
|
|
bool bAnyErrors = false;
|
|
|
|
int32 TotalPoses = 0;
|
|
for (int32 AnimationAssetIndex = 0; AnimationAssetIndex < DatabaseToLookForAssets->GetNumAnimationAssets(); ++AnimationAssetIndex)
|
|
{
|
|
if (const FPoseSearchDatabaseAnimationAssetBase* DatabaseAsset = DatabaseToLookForAssets->GetDatabaseAnimationAsset<FPoseSearchDatabaseAnimationAssetBase>(AnimationAssetIndex))
|
|
{
|
|
if (!DatabaseAsset->IsEnabled() || !DatabaseAsset->GetAnimationAsset())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// checking for duplicated roles in DatabaseAsset
|
|
TSet<FRole, DefaultKeyFuncs<FRole>, TInlineSetAllocator<PreallocatedRolesNum>> DatabaseAssetRoles;
|
|
const int32 NumRoles = DatabaseAsset->GetNumRoles();
|
|
for (int32 RoleIndex = 0; RoleIndex < NumRoles; ++RoleIndex)
|
|
{
|
|
bool bIsAlreadyInSet = false;
|
|
const FRole Role = DatabaseAsset->GetRole(RoleIndex);
|
|
DatabaseAssetRoles.Add(Role, &bIsAlreadyInSet);
|
|
if (bIsAlreadyInSet)
|
|
{
|
|
UE_LOG(LogPoseSearch, Error, TEXT("DatabaseAsset '%s' contains duplicate Role '%s'"), *DatabaseAsset->GetAnimationAsset()->GetName(), *Role.ToString());
|
|
bAnyErrors = true;
|
|
}
|
|
}
|
|
|
|
// checking for valid roles in DatabaseMultiAnimAsset against the Schema
|
|
bool bAreAllRolesSupported = true;
|
|
for (const FPoseSearchRoledSkeleton& RoledSkeleton : Schema->GetRoledSkeletons())
|
|
{
|
|
if (!DatabaseAsset->GetAnimationAssetForRole(RoledSkeleton.Role))
|
|
{
|
|
UE_LOG(LogPoseSearch, Error, TEXT("DatabaseAsset '%s' doesn't support Role '%s' required by Schema '%s' Skeletons"), *DatabaseAsset->GetAnimationAsset()->GetName(), *RoledSkeleton.Role.ToString(), *Schema->GetName());
|
|
bAreAllRolesSupported = false;
|
|
}
|
|
}
|
|
if (!bAreAllRolesSupported)
|
|
{
|
|
bAnyErrors = true;
|
|
continue;
|
|
}
|
|
|
|
const bool bAddUnmirrored = DatabaseAsset->GetMirrorOption() == EPoseSearchMirrorOption::UnmirroredOnly || DatabaseAsset->GetMirrorOption() == EPoseSearchMirrorOption::UnmirroredAndMirrored;
|
|
const bool bAddMirrored = DatabaseAsset->GetMirrorOption() == EPoseSearchMirrorOption::MirroredOnly || DatabaseAsset->GetMirrorOption() == EPoseSearchMirrorOption::UnmirroredAndMirrored;
|
|
const bool bIsLooping = DatabaseAsset->IsLooping();
|
|
const bool bDisableReselection = DatabaseAsset->IsDisableReselection();
|
|
|
|
// @todo: add better support for IMultiAnimAsset: currently we fix blend space parameters for only one role,
|
|
// that implies having homogeneous blendspaces for ALL the roles, and having either ONLY blend spaces or only NOT blendspaces..
|
|
bool bIsBlendSpace = false;
|
|
if (UAnimationAsset* DefaultRoleAnimationAsset = DatabaseAsset->GetAnimationAssetForRole(DatabaseToLookForAssets->Schema->GetDefaultRole()))
|
|
{
|
|
bIsBlendSpace = DefaultRoleAnimationAsset->IsA<UBlendSpace>();
|
|
}
|
|
|
|
DatabaseAsset->IterateOverSamplingParameter(
|
|
[DatabaseAsset, ExcludeFromDatabaseParameters, &ValidRanges, Schema, bAddUnmirrored, bAddMirrored, bIsLooping, bIsBlendSpace, bDisableReselection, AnimationAssetIndex, &TotalPoses, &SearchIndex]
|
|
(const FVector& BlendParameters)
|
|
{
|
|
FindValidRanges(DatabaseAsset, BlendParameters, ExcludeFromDatabaseParameters, ValidRanges);
|
|
|
|
for (const FFloatRange& Range : ValidRanges)
|
|
{
|
|
for (int32 PermutationIdx = 0; PermutationIdx < Schema->NumberOfPermutations; ++PermutationIdx)
|
|
{
|
|
const FFloatInterval RangeInterval(Range.GetLowerBoundValue(), Range.GetUpperBoundValue());
|
|
|
|
float ToRealTimeFactor = 1.f;
|
|
if (bIsBlendSpace)
|
|
{
|
|
const float PlayLength = DatabaseAsset->GetPlayLength(BlendParameters);
|
|
if (PlayLength > UE_KINDA_SMALL_NUMBER)
|
|
{
|
|
ToRealTimeFactor = PlayLength;
|
|
}
|
|
}
|
|
|
|
if (bAddUnmirrored)
|
|
{
|
|
const FSearchIndexAsset PoseSearchIndexAsset(AnimationAssetIndex, TotalPoses, false, bIsLooping,
|
|
bDisableReselection, RangeInterval, Schema->SampleRate, PermutationIdx, BlendParameters, ToRealTimeFactor);
|
|
if (PoseSearchIndexAsset.GetNumPoses() > 0)
|
|
{
|
|
SearchIndex.Assets.Add(PoseSearchIndexAsset);
|
|
TotalPoses += PoseSearchIndexAsset.GetNumPoses();
|
|
}
|
|
}
|
|
|
|
if (bAddMirrored)
|
|
{
|
|
const FSearchIndexAsset PoseSearchIndexAsset(AnimationAssetIndex, TotalPoses, true, bIsLooping,
|
|
bDisableReselection, RangeInterval, Schema->SampleRate, PermutationIdx, BlendParameters, ToRealTimeFactor);
|
|
if (PoseSearchIndexAsset.GetNumPoses() > 0)
|
|
{
|
|
SearchIndex.Assets.Add(PoseSearchIndexAsset);
|
|
TotalPoses += PoseSearchIndexAsset.GetNumPoses();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
return !bAnyErrors;
|
|
}
|
|
|
|
static void PreprocessSearchIndexWeights(FSearchIndex& SearchIndex, const UPoseSearchSchema* Schema, TConstArrayView<float> Deviation)
|
|
{
|
|
#if WITH_EDITORONLY_DATA
|
|
// Copy deviation into search index for weights display in editor
|
|
SearchIndex.DeviationEditorOnly = Deviation;
|
|
#endif // WITH_EDITORONLY_DATA
|
|
|
|
const int32 NumDimensions = Schema->SchemaCardinality;
|
|
SearchIndex.WeightsSqrt.Init(1.f, NumDimensions);
|
|
|
|
for (const TObjectPtr<UPoseSearchFeatureChannel>& ChannelPtr : Schema->GetChannels())
|
|
{
|
|
ChannelPtr->FillWeights(SearchIndex.WeightsSqrt);
|
|
}
|
|
|
|
EPoseSearchDataPreprocessor DataPreprocessor = Schema->DataPreprocessor;
|
|
if (DataPreprocessor == EPoseSearchDataPreprocessor::Normalize || DataPreprocessor == EPoseSearchDataPreprocessor::NormalizeWithCommonSchema)
|
|
{
|
|
// normalizing user weights: the idea behind this step is to be able to compare poses from databases using different schemas
|
|
RowMajorVectorMap MapWeights(SearchIndex.WeightsSqrt.GetData(), 1, NumDimensions);
|
|
const float WeightsSum = MapWeights.sum();
|
|
if (!FMath::IsNearlyZero(WeightsSum))
|
|
{
|
|
MapWeights *= (1.f / WeightsSum);
|
|
}
|
|
}
|
|
|
|
// extracting the square root
|
|
for (int32 Dimension = 0; Dimension != NumDimensions; ++Dimension)
|
|
{
|
|
SearchIndex.WeightsSqrt[Dimension] = FMath::Sqrt(SearchIndex.WeightsSqrt[Dimension]);
|
|
}
|
|
|
|
if (DataPreprocessor != EPoseSearchDataPreprocessor::None)
|
|
{
|
|
for (int32 Dimension = 0; Dimension != NumDimensions; ++Dimension)
|
|
{
|
|
// the idea here is to pre-multiply the weights by the inverse of the variance (proportional to the square of the deviation) to have a "weighted Mahalanobis" distance
|
|
SearchIndex.WeightsSqrt[Dimension] /= Deviation[Dimension];
|
|
}
|
|
}
|
|
}
|
|
|
|
// it calculates Mean, PCAValues, and PCAProjectionMatrix
|
|
static Eigen::ComputationInfo PreprocessSearchIndexPCAData(FSearchIndex& SearchIndex, int32 NumDimensions, int32 NumberOfPrincipalComponents, EPoseSearchMode PoseSearchMode)
|
|
{
|
|
#if ENABLE_ANIM_DEBUG
|
|
if (AnyTestFlags(EMotionMatchTestFlags::ValidateKDTreeConstruct))
|
|
{
|
|
// @todo: move this into a unit test.
|
|
// this code will fail with nanoflann 1.5.5
|
|
|
|
int NumPoses = 61;
|
|
int DataCardinality = 8;
|
|
TArray<float> Values;
|
|
Values.SetNumZeroed(NumPoses * DataCardinality);
|
|
|
|
for (int PoseIndex = 0; PoseIndex < NumPoses; ++PoseIndex)
|
|
{
|
|
Values[PoseIndex * DataCardinality + 0] = -5.54383405e-07;
|
|
Values[PoseIndex * DataCardinality + 1] = 2.77555756e-16;
|
|
}
|
|
|
|
FKDTree KDTree(NumPoses, DataCardinality, Values.GetData());
|
|
}
|
|
#endif // ENABLE_ANIM_DEBUG
|
|
|
|
|
|
// binding SearchIndex.Values and SearchIndex.PCAValues Eigen row major matrix maps
|
|
const int32 NumPoses = SearchIndex.GetNumPoses();
|
|
|
|
#if WITH_EDITORONLY_DATA
|
|
SearchIndex.PCAExplainedVarianceEditorOnly = 0.f;
|
|
#endif
|
|
PRAGMA_DISABLE_DEPRECATION_WARNINGS
|
|
SearchIndex.PCAExplainedVariance = 0.f;
|
|
PRAGMA_ENABLE_DEPRECATION_WARNINGS
|
|
|
|
SearchIndex.PCAValues.Reset();
|
|
SearchIndex.Mean.Reset();
|
|
SearchIndex.PCAProjectionMatrix.Reset();
|
|
|
|
Eigen::ComputationInfo ComputationInfo = Eigen::Success;
|
|
if (PoseSearchMode == EPoseSearchMode::PCAKDTree && NumDimensions > 0 && NumPoses > 0 && NumberOfPrincipalComponents > 0)
|
|
{
|
|
SearchIndex.PCAValues.AddZeroed(NumPoses * NumberOfPrincipalComponents);
|
|
SearchIndex.Mean.AddZeroed(NumDimensions);
|
|
SearchIndex.PCAProjectionMatrix.AddZeroed(NumDimensions * NumberOfPrincipalComponents);
|
|
|
|
// recreating the full pose values data to have a 1:1 mapping between PCAValues/NumDimensions and PoseIdx
|
|
TArray<float> AllValuesWithDuplicateData;
|
|
AllValuesWithDuplicateData.SetNum(NumPoses * NumDimensions);
|
|
for (int32 PoseIdx = 0; PoseIdx < NumPoses; ++PoseIdx)
|
|
{
|
|
FMemory::Memcpy(AllValuesWithDuplicateData.GetData() + PoseIdx * NumDimensions, SearchIndex.GetPoseValuesBase(PoseIdx, NumDimensions).GetData(), NumDimensions * sizeof(float));
|
|
}
|
|
|
|
const RowMajorVectorMapConst MapWeightsSqrt(SearchIndex.WeightsSqrt.GetData(), 1, NumDimensions);
|
|
const RowMajorMatrixMapConst MapValues(AllValuesWithDuplicateData.GetData(), NumPoses, NumDimensions);
|
|
const RowMajorMatrix WeightedValues = MapValues.array().rowwise() * MapWeightsSqrt.array();
|
|
RowMajorMatrixMap MapPCAValues(SearchIndex.PCAValues.GetData(), NumPoses, NumberOfPrincipalComponents);
|
|
|
|
// calculating the mean
|
|
RowMajorVectorMap MapMean(SearchIndex.Mean.GetData(), 1, NumDimensions);
|
|
MapMean = WeightedValues.colwise().mean();
|
|
|
|
// use the mean to center the data points
|
|
const RowMajorMatrix CenteredValues = WeightedValues.rowwise() - MapMean;
|
|
|
|
// estimating the covariance matrix (with dimensionality of NumDimensions, NumDimensions)
|
|
// formula: https://en.wikipedia.org/wiki/Covariance_matrix#Estimation
|
|
// details: https://en.wikipedia.org/wiki/Estimation_of_covariance_matrices
|
|
const ColMajorMatrix CovariantMatrix = (CenteredValues.transpose() * CenteredValues) / float(NumPoses - 1);
|
|
const Eigen::SelfAdjointEigenSolver<ColMajorMatrix> EigenSolver(CovariantMatrix);
|
|
|
|
ComputationInfo = EigenSolver.info();
|
|
if (ComputationInfo == Eigen::Success)
|
|
{
|
|
// validating EigenSolver results
|
|
const ColMajorMatrix EigenVectors = EigenSolver.eigenvectors().real();
|
|
|
|
#if ENABLE_ANIM_DEBUG
|
|
if (AnyTestFlags(EMotionMatchTestFlags::ValidateKDTreeConstruct) && NumberOfPrincipalComponents == NumDimensions)
|
|
{
|
|
const RowMajorVector ReciprocalWeightsSqrt = MapWeightsSqrt.cwiseInverse();
|
|
const RowMajorMatrix ProjectedValues = CenteredValues * EigenVectors;
|
|
for (Eigen::Index RowIndex = 0; RowIndex < MapValues.rows(); ++RowIndex)
|
|
{
|
|
const RowMajorVector WeightedReconstructedPoint = ProjectedValues.row(RowIndex) * EigenVectors.transpose() + MapMean;
|
|
const RowMajorVector ReconstructedPoint = WeightedReconstructedPoint.array() * ReciprocalWeightsSqrt.array();
|
|
const float Error = (ReconstructedPoint - MapValues.row(RowIndex)).squaredNorm();
|
|
check(Error < UE_KINDA_SMALL_NUMBER);
|
|
}
|
|
}
|
|
#endif // ENABLE_ANIM_DEBUG
|
|
|
|
// sorting EigenVectors by EigenValues, so we pick the most significant ones to compose our PCA projection matrix.
|
|
const RowMajorVector EigenValues = EigenSolver.eigenvalues().real();
|
|
TArray<int32> Indexer;
|
|
Indexer.Reserve(NumDimensions);
|
|
for (int32 DimensionIndex = 0; DimensionIndex < NumDimensions; ++DimensionIndex)
|
|
{
|
|
Indexer.Push(DimensionIndex);
|
|
}
|
|
Indexer.Sort([&EigenValues](int32 a, int32 b)
|
|
{
|
|
return EigenValues[a] > EigenValues[b];
|
|
});
|
|
|
|
// composing the PCA projection matrix with the PCANumComponents most significant EigenVectors
|
|
ColMajorMatrixMap PCAProjectionMatrix(SearchIndex.PCAProjectionMatrix.GetData(), NumDimensions, NumberOfPrincipalComponents);
|
|
float AccumulatedVariance = 0.f;
|
|
for (int32 PCAComponentIndex = 0; PCAComponentIndex < NumberOfPrincipalComponents; ++PCAComponentIndex)
|
|
{
|
|
PCAProjectionMatrix.col(PCAComponentIndex) = EigenVectors.col(Indexer[PCAComponentIndex]);
|
|
AccumulatedVariance += EigenValues[Indexer[PCAComponentIndex]];
|
|
}
|
|
|
|
PRAGMA_DISABLE_DEPRECATION_WARNINGS
|
|
// @todo: move this code under WITH_EDITORONLY_DATA once SearchIndex.PCAExplainedVariance has been removed
|
|
// calculating the total variance knowing that eigen values measure variance along the principal components:
|
|
const float TotalVariance = EigenValues.sum();
|
|
// and explained variance as ratio between AccumulatedVariance and TotalVariance: https://ro-che.info/articles/2017-12-11-pca-explained-variance
|
|
SearchIndex.PCAExplainedVariance = TotalVariance > UE_KINDA_SMALL_NUMBER ? AccumulatedVariance / TotalVariance : 0.f;
|
|
|
|
#if WITH_EDITORONLY_DATA
|
|
SearchIndex.PCAExplainedVarianceEditorOnly = SearchIndex.PCAExplainedVariance;
|
|
#endif // WITH_EDITORONLY_DATA
|
|
PRAGMA_ENABLE_DEPRECATION_WARNINGS
|
|
|
|
MapPCAValues = CenteredValues * PCAProjectionMatrix;
|
|
|
|
#if ENABLE_ANIM_DEBUG
|
|
if (AnyTestFlags(EMotionMatchTestFlags::ValidateKDTreeConstruct) && NumberOfPrincipalComponents == NumDimensions)
|
|
{
|
|
const RowMajorVector ReciprocalWeightsSqrt = MapWeightsSqrt.cwiseInverse();
|
|
for (Eigen::Index RowIndex = 0; RowIndex < MapValues.rows(); ++RowIndex)
|
|
{
|
|
const RowMajorVector WeightedReconstructedValues = MapPCAValues.row(RowIndex) * PCAProjectionMatrix.transpose() + MapMean;
|
|
const RowMajorVector ReconstructedValues = WeightedReconstructedValues.array() * ReciprocalWeightsSqrt.array();
|
|
const float Error = (ReconstructedValues - MapValues.row(RowIndex)).squaredNorm();
|
|
check(Error < UE_KINDA_SMALL_NUMBER);
|
|
}
|
|
|
|
TArray<float> ReconstructedPoseValues;
|
|
ReconstructedPoseValues.SetNumZeroed(NumDimensions);
|
|
for (int32 PoseIdx = 0; PoseIdx < NumPoses; ++PoseIdx)
|
|
{
|
|
SearchIndex.GetReconstructedPoseValues(PoseIdx, ReconstructedPoseValues);
|
|
TConstArrayView<float> PoseValues = SearchIndex.GetPoseValues(PoseIdx);
|
|
|
|
check(ReconstructedPoseValues.Num() == PoseValues.Num());
|
|
Eigen::Map<const Eigen::ArrayXf> VA(ReconstructedPoseValues.GetData(), ReconstructedPoseValues.Num());
|
|
Eigen::Map<const Eigen::ArrayXf> VB(PoseValues.GetData(), PoseValues.Num());
|
|
|
|
const float Error = (VA - VB).square().sum();
|
|
check(Error < UE_KINDA_SMALL_NUMBER);
|
|
}
|
|
}
|
|
#endif // ENABLE_ANIM_DEBUG
|
|
}
|
|
}
|
|
|
|
return ComputationInfo;
|
|
}
|
|
|
|
static void PreprocessSearchIndexKDTree(FSearchIndex& SearchIndex, const UPoseSearchDatabase* Database)
|
|
{
|
|
const int32 NumDimensions = Database->Schema->SchemaCardinality;
|
|
const EPoseSearchMode PoseSearchMode = Database->PoseSearchMode;
|
|
|
|
SearchIndex.KDTree.Reset();
|
|
if (NumDimensions > 0 && PoseSearchMode == EPoseSearchMode::PCAKDTree)
|
|
{
|
|
const uint32 NumberOfPrincipalComponents = Database->GetNumberOfPrincipalComponents();
|
|
const int32 KDTreeMaxLeafSize = Database->KDTreeMaxLeafSize;
|
|
|
|
const int32 NumPCAValuesVectors = SearchIndex.GetNumPCAValuesVectors(NumberOfPrincipalComponents);
|
|
SearchIndex.KDTree.Construct(NumPCAValuesVectors, NumberOfPrincipalComponents, SearchIndex.PCAValues.GetData(), KDTreeMaxLeafSize);
|
|
|
|
#if ENABLE_ANIM_DEBUG
|
|
// testing kdtree Construct determinism
|
|
if (AnyTestFlags(EMotionMatchTestFlags::TestKDTreeConstructDeterminism))
|
|
{
|
|
const int32 NumIterations = GVarMotionMatchTestNumIterations;
|
|
TAlignedArray<float> PCAValuesTest;
|
|
for (int32 Iteration = 0; Iteration < NumIterations; ++Iteration)
|
|
{
|
|
// copy PCAValues in a different container to ensure input data has different memory addresses
|
|
PCAValuesTest = SearchIndex.PCAValues;
|
|
|
|
FKDTree KDTreeTest;
|
|
KDTreeTest.Construct(NumPCAValuesVectors, NumberOfPrincipalComponents, PCAValuesTest.GetData(), KDTreeMaxLeafSize);
|
|
|
|
if (KDTreeTest != SearchIndex.KDTree)
|
|
{
|
|
UE_LOG(LogPoseSearch, Warning, TEXT("PreprocessSearchIndexKDTree - FKDTree::Construct is not deterministic"));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (AnyTestFlags(EMotionMatchTestFlags::ValidateKDTreeConstruct))
|
|
{
|
|
// testing the KDTree is returning the proper searches for all the points in pca space
|
|
const int32 KDTreeQueryNumNeighbors = Database->KDTreeQueryNumNeighbors;
|
|
|
|
TArray<FKDTree::FKNNMaxHeapResultSet::FResult, TInlineAllocator<256>> Results;
|
|
Results.SetNumUninitialized(KDTreeQueryNumNeighbors);
|
|
int32 MaxNumNeighborToFindAPoint = 0;
|
|
for (int32 PointIndex = 0; PointIndex < NumPCAValuesVectors; ++PointIndex)
|
|
{
|
|
// searching the kdtree for PointIndex
|
|
FKDTree::FRadiusMaxHeapResultSet ResultSet(Results, UE_SMALL_NUMBER);
|
|
const int32 NumResults = SearchIndex.KDTree.FindNeighbors(ResultSet, MakeArrayView(&SearchIndex.PCAValues[PointIndex * NumberOfPrincipalComponents], NumberOfPrincipalComponents));
|
|
|
|
bool bFound = false;
|
|
for (int32 ResultIndex = 0; ResultIndex < NumResults; ++ResultIndex)
|
|
{
|
|
if (PointIndex == Results[ResultIndex].Index)
|
|
{
|
|
// PointIndex is the ResultIndex-th candidates out of the kdtree. if ResultIndex-th is greater than KDTreeQueryNumNeighbors,
|
|
// we wouldn't have found it in a runtime search, so we log the errot (later on only once, with the worst case scenario)
|
|
check(Results[ResultIndex].Distance < UE_KINDA_SMALL_NUMBER);
|
|
MaxNumNeighborToFindAPoint = FMath::Max(MaxNumNeighborToFindAPoint, ResultIndex);
|
|
bFound = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!bFound)
|
|
{
|
|
UE_LOG(LogPoseSearch, Error, TEXT("PreprocessSearchIndexKDTree - kdtree for %s is not properly constructed! Couldn't find the Point %d in it"), *Database->GetName(), PointIndex);
|
|
}
|
|
}
|
|
|
|
if (MaxNumNeighborToFindAPoint >= KDTreeQueryNumNeighbors)
|
|
{
|
|
UE_LOG(LogPoseSearch, Warning, TEXT("Not enough 'KDTreeQueryNumNeighbors' (%d) for database '%s'. Pose values projected in PCA space have too many duplicates, so try to prune duplicates by tuning 'PCAValuesPruningSimilarityThreshold' or increase 'KDTreeQueryNumNeighbors' at least to %d"), KDTreeQueryNumNeighbors, *Database->GetName(), MaxNumNeighborToFindAPoint);
|
|
}
|
|
|
|
// if bArePCAValuesPruned PointIndex is the index of the point in the kdtree, NOT necessary the pose index, so doing the PCAProject would lead to the wrong data
|
|
const bool bArePCAValuesPruned = SearchIndex.PCAValuesVectorToPoseIndexes.Num() > 0;
|
|
if (!bArePCAValuesPruned)
|
|
{
|
|
// testing the KDTree is returning the proper searches for all the original points transformed in pca space
|
|
TArrayView<float> ProjectedValues((float*)FMemory_Alloca(NumberOfPrincipalComponents * sizeof(float)), NumberOfPrincipalComponents);
|
|
for (int32 PointIndex = 0; PointIndex < NumPCAValuesVectors; ++PointIndex)
|
|
{
|
|
FKDTree::FKNNMaxHeapResultSet ResultSet(Results);
|
|
const int32 NumResults = SearchIndex.KDTree.FindNeighbors(ResultSet, SearchIndex.PCAProject(SearchIndex.GetPoseValuesBase(PointIndex, NumDimensions), ProjectedValues));
|
|
|
|
int32 ResultIndex = 0;
|
|
for (; ResultIndex < NumResults; ++ResultIndex)
|
|
{
|
|
if (PointIndex == Results[ResultIndex].Index)
|
|
{
|
|
if (Results[ResultIndex].Distance > UE_KINDA_SMALL_NUMBER)
|
|
{
|
|
UE_LOG(LogPoseSearch, Error, TEXT("PreprocessSearchIndexKDTree - kdtree for %s is not properly constructed! Couldn't find the Point %d in it within UE_KINDA_SMALL_NUMBER tolerance, after PCA projection"), *Database->GetName(), PointIndex);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
if (ResultIndex == NumResults)
|
|
{
|
|
UE_LOG(LogPoseSearch, Error, TEXT("PreprocessSearchIndexKDTree - kdtree for %s is not properly constructed! Couldn't find the Point %d in it, after PCA projection"), *Database->GetName(), PointIndex);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif // ENABLE_ANIM_DEBUG
|
|
}
|
|
}
|
|
|
|
// creating a vantage point tree
|
|
static void PreprocessSearchIndexVPTree(FSearchIndex& SearchIndex, const UPoseSearchDatabase* Database, int32 RandomSeed)
|
|
{
|
|
const int32 NumDimensions = Database->Schema->SchemaCardinality;
|
|
const int32 NumValuesVectors = SearchIndex.GetNumValuesVectors(NumDimensions);
|
|
const EPoseSearchMode PoseSearchMode = Database->PoseSearchMode;
|
|
|
|
SearchIndex.VPTree.Reset();
|
|
if (PoseSearchMode == EPoseSearchMode::VPTree && NumValuesVectors > 0)
|
|
{
|
|
const FVPTreeDataSource DataSource(SearchIndex);
|
|
FRandomStream RandStream(RandomSeed);
|
|
SearchIndex.VPTree.Construct(DataSource, RandStream);
|
|
|
|
#if ENABLE_ANIM_DEBUG
|
|
|
|
if (AnyTestFlags(EMotionMatchTestFlags::TestVPTreeConstructDeterminism))
|
|
{
|
|
if (!SearchIndex.VPTree.TestConstruct(DataSource))
|
|
{
|
|
UE_LOG(LogPoseSearch, Error, TEXT("PreprocessSearchIndexVPTree - FVPTree construction failed"));
|
|
}
|
|
|
|
const int32 NumIterations = GVarMotionMatchTestNumIterations;
|
|
for (int32 Iteration = 0; Iteration < NumIterations; ++Iteration)
|
|
{
|
|
FVPTree VPTreeTest;
|
|
FRandomStream RandStreamTest(RandomSeed);
|
|
VPTreeTest.Construct(DataSource, RandStreamTest);
|
|
|
|
if (VPTreeTest != SearchIndex.VPTree)
|
|
{
|
|
UE_LOG(LogPoseSearch, Warning, TEXT("PreprocessSearchIndexVPTree - FVPTree construction is not deterministic"));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (AnyTestFlags(EMotionMatchTestFlags::ValidateVPTreeConstruct))
|
|
{
|
|
// we can validate vantage point tree only if there are no duplicates poses (points)
|
|
if (Database->PosePruningSimilarityThreshold > 0.f)
|
|
{
|
|
// testing the VPTree is returning the proper searches for all the points
|
|
for (int32 PointIndex = 0; PointIndex < NumValuesVectors; ++PointIndex)
|
|
{
|
|
FVPTreeResultSet ResultSet(Database->KDTreeQueryNumNeighbors);
|
|
SearchIndex.VPTree.FindNeighbors(SearchIndex.GetValuesVector(PointIndex, NumDimensions), ResultSet, DataSource);
|
|
|
|
bool bFound = false;
|
|
for (const FIndexDistance& Result : ResultSet.GetUnsortedResults())
|
|
{
|
|
if (Result.Index == PointIndex)
|
|
{
|
|
if (!FMath::IsNearlyZero(Result.Distance))
|
|
{
|
|
UE_LOG(LogPoseSearch, Error, TEXT("PreprocessSearchIndexVPTree - VPTree for %s is malformed because foud PointIndex %d distance %f from itself (distance should be zero)!"), *Database->GetName(), PointIndex, Result.Distance);
|
|
}
|
|
|
|
bFound = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!bFound)
|
|
{
|
|
UE_LOG(LogPoseSearch, Error, TEXT("PreprocessSearchIndexVPTree - VPTree for %s is malformed and couldn't find PointIndex %d"), *Database->GetName(), PointIndex);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogPoseSearch, Warning, TEXT("PreprocessSearchIndexVPTree - cannot ValidateVPTreeConstruct for %s if there could be potential duplicate poses: set PosePruningSimilarityThreshold > 0 to enforce duplicate pruning"), *Database->GetName());
|
|
}
|
|
}
|
|
#endif // ENABLE_ANIM_DEBUG
|
|
}
|
|
}
|
|
|
|
// this struct exists because FTransform doesn't implement operator== (or it'd be TTuple<const UAnimationAsset*, FTransform, FVector>)
|
|
struct FSamplerMapKey
|
|
{
|
|
FSamplerMapKey(const UAnimationAsset* InAnimationAsset, const FTransform& InRootTransformOrigin, const FVector& InBlendParameters = FVector::ZeroVector)
|
|
: AnimationAsset(InAnimationAsset)
|
|
, RootTransformOrigin(InRootTransformOrigin)
|
|
, BlendParameters(InBlendParameters)
|
|
{
|
|
}
|
|
|
|
bool operator==(const FSamplerMapKey& Other) const
|
|
{
|
|
return AnimationAsset == Other.AnimationAsset &&
|
|
RootTransformOrigin.Equals(Other.RootTransformOrigin, 0.f) &&
|
|
BlendParameters == Other.BlendParameters;
|
|
}
|
|
|
|
friend FORCEINLINE uint32 GetTypeHash(const FSamplerMapKey& SamplerMapKey)
|
|
{
|
|
const uint32 AnimationAssetHash = GetTypeHash(SamplerMapKey.AnimationAsset);
|
|
const uint32 RootTransformOriginHash = GetTypeHash(SamplerMapKey.RootTransformOrigin);
|
|
const uint32 BlendParametersHash = GetTypeHash(SamplerMapKey.BlendParameters);
|
|
return HashCombineFast(HashCombineFast(AnimationAssetHash, RootTransformOriginHash), BlendParametersHash);
|
|
}
|
|
|
|
const UAnimationAsset* AnimationAsset = nullptr;
|
|
FTransform RootTransformOrigin = FTransform::Identity;
|
|
FVector BlendParameters = FVector::ZeroVector;
|
|
};
|
|
|
|
static void InitRoledBoneContainers(TMap<FRole, FBoneContainer>& RoledBoneContainers, const UPoseSearchDatabase* DatabaseToLookForAssets, const UPoseSearchSchema* Schema)
|
|
{
|
|
auto AddBoneIndex = [](int32 BoneIndex, TArray<uint16>& BoneIndicesWithParents, const UMirrorDataTable* MirrorDataTable)
|
|
{
|
|
BoneIndicesWithParents.AddUnique(BoneIndex);
|
|
|
|
// adding mirrored BoneIndex if there's a valid MirrorDataTable
|
|
if (MirrorDataTable)
|
|
{
|
|
if (MirrorDataTable->BoneToMirrorBoneIndex.IsValidIndex(BoneIndex))
|
|
{
|
|
const FSkeletonPoseBoneIndex MirroredBoneIndex = MirrorDataTable->BoneToMirrorBoneIndex[BoneIndex];
|
|
if (MirroredBoneIndex.IsValid())
|
|
{
|
|
BoneIndicesWithParents.AddUnique(MirroredBoneIndex.GetInt());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogPoseSearch, Warning, TEXT("InitBoneContainersFromRoledSkeleton: MirrorDataTable %s doesn't contain bone with index %d."), *MirrorDataTable->GetName(), BoneIndex);
|
|
}
|
|
}
|
|
};
|
|
|
|
RoledBoneContainers.Reset();
|
|
RoledBoneContainers.Reserve(Schema->GetRoledSkeletons().Num());
|
|
|
|
// filling up BoneIndicesWithParents with all the bone indexes from the bones in the
|
|
// schema roled skeletons and from UAnimNotifyState_PoseSearchSamplingAttribute
|
|
TArray<uint16> BoneIndicesWithParents;
|
|
BoneIndicesWithParents.Reserve(128);
|
|
|
|
for (const FPoseSearchRoledSkeleton& RoledSkeleton : Schema->GetRoledSkeletons())
|
|
{
|
|
BoneIndicesWithParents.Reset();
|
|
|
|
FBoneContainer& RoledBoneContainer = RoledBoneContainers.Add(RoledSkeleton.Role);
|
|
// Add a curve filter to our bone container to only eval curves actually used by the schema.
|
|
const UE::Anim::FCurveFilterSettings CurveFilterSettings(UE::Anim::ECurveFilterMode::AllowOnlyFiltered, &RoledSkeleton.RequiredCurves);
|
|
|
|
// Initialize references to obtain bone indices and fill out bone index array
|
|
for (const FBoneReference& BoneRef : RoledSkeleton.BoneReferences)
|
|
{
|
|
check(BoneRef.HasValidSetup());
|
|
AddBoneIndex(BoneRef.BoneIndex, BoneIndicesWithParents, RoledSkeleton.MirrorDataTable);
|
|
}
|
|
|
|
for (int32 AnimationAssetIndex = 0; AnimationAssetIndex < DatabaseToLookForAssets->GetNumAnimationAssets(); ++AnimationAssetIndex)
|
|
{
|
|
if (const FPoseSearchDatabaseAnimationAssetBase* DatabaseAsset = DatabaseToLookForAssets->GetDatabaseAnimationAsset<FPoseSearchDatabaseAnimationAssetBase>(AnimationAssetIndex))
|
|
{
|
|
if (!DatabaseAsset->IsEnabled() || !DatabaseAsset->GetAnimationAsset())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
const FAnimationAssetSampler Sampler(DatabaseAsset->GetAnimationAssetForRole(RoledSkeleton.Role), FTransform::Identity, FVector::ZeroVector, FAnimationAssetSampler::DefaultRootTransformSamplingRate, false, true);
|
|
for (const FAnimNotifyEvent& AnimNotifyEvent : Sampler.GetAllAnimNotifyEvents())
|
|
{
|
|
if (const UAnimNotifyState_PoseSearchSamplingAttribute* SamplingAttribute = Cast<UAnimNotifyState_PoseSearchSamplingAttribute>(AnimNotifyEvent.NotifyStateClass))
|
|
{
|
|
FBoneReference TempBoneReference = SamplingAttribute->Bone;
|
|
TempBoneReference.Initialize(RoledSkeleton.Skeleton);
|
|
if (TempBoneReference.HasValidSetup())
|
|
{
|
|
AddBoneIndex(TempBoneReference.BoneIndex, BoneIndicesWithParents, RoledSkeleton.MirrorDataTable);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sort bone indexes and add eventual missing parent bone indexes
|
|
BoneIndicesWithParents.Sort();
|
|
FAnimationRuntime::EnsureParentsPresent(BoneIndicesWithParents, RoledSkeleton.Skeleton->GetReferenceSkeleton());
|
|
|
|
RoledBoneContainer.InitializeTo(BoneIndicesWithParents, CurveFilterSettings, *RoledSkeleton.Skeleton);
|
|
}
|
|
}
|
|
|
|
static bool IndexDatabase(FSearchIndexBase& SearchIndexBase, const UPoseSearchDatabase* DatabaseToLookForAssets, const UPoseSearchSchema* Schema, const FAssetSamplingContext& SamplingContext, const FFloatInterval& AdditionalExtrapolationTime, UE::DerivedData::FRequestOwner& Owner)
|
|
{
|
|
if (!ensure(Schema))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Prepare samplers for all animation assets.
|
|
TArray<FAnimationAssetSampler> Samplers;
|
|
Samplers.Reserve(256);
|
|
|
|
TMap<FSamplerMapKey, int32> SamplerMap;
|
|
SamplerMap.Reserve(256);
|
|
|
|
for (int32 AssetIdx = 0; AssetIdx != SearchIndexBase.Assets.Num(); ++AssetIdx)
|
|
{
|
|
FSearchIndexAsset& SearchIndexAsset = SearchIndexBase.Assets[AssetIdx];
|
|
|
|
const FPoseSearchDatabaseAnimationAssetBase* DatabaseAnimationAssetBase = DatabaseToLookForAssets->GetDatabaseAnimationAsset<FPoseSearchDatabaseAnimationAssetBase>(SearchIndexAsset.GetSourceAssetIdx());
|
|
check(DatabaseAnimationAssetBase);
|
|
|
|
const int32 NumRoles = DatabaseAnimationAssetBase->GetNumRoles();
|
|
for (int32 RoleIndex = 0; RoleIndex < NumRoles; ++RoleIndex)
|
|
{
|
|
const FRole& Role = DatabaseAnimationAssetBase->GetRole(RoleIndex);
|
|
UAnimationAsset* AnimationAsset = DatabaseAnimationAssetBase->GetAnimationAssetForRole(Role);
|
|
const FTransform RootTransformOrigin = DatabaseAnimationAssetBase->GetRootTransformOriginForRole(Role);
|
|
const FVector BlendParameters = SearchIndexAsset.GetBlendParameters();
|
|
|
|
const FSamplerMapKey SamplerMapKey(AnimationAsset, RootTransformOrigin, BlendParameters);
|
|
if (!SamplerMap.Contains(SamplerMapKey))
|
|
{
|
|
SamplerMap.Add(SamplerMapKey, Samplers.Num());
|
|
Samplers.Emplace(AnimationAsset, RootTransformOrigin, BlendParameters, FAnimationAssetSampler::DefaultRootTransformSamplingRate, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
ParallelFor(Samplers.Num(), [&Samplers](int32 SamplerIdx) { Samplers[SamplerIdx].Process(); }, ParallelForFlags);
|
|
if (Owner.IsCanceled())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// prepare indexers
|
|
TArray<FAssetIndexer> Indexers;
|
|
Indexers.Reserve(SearchIndexBase.Assets.Num());
|
|
|
|
TMap<FRole, FBoneContainer> RoledBoneContainers;
|
|
InitRoledBoneContainers(RoledBoneContainers, DatabaseToLookForAssets, Schema);
|
|
|
|
TMap<FRole, FMirrorDataCache> RoledMirrorDataCaches;
|
|
RoledMirrorDataCaches.Reserve(RoledBoneContainers.Num());
|
|
for (const TPair<FRole, FBoneContainer>& RoledBoneContainerPair : RoledBoneContainers)
|
|
{
|
|
const FRole& Role = RoledBoneContainerPair.Key;
|
|
RoledMirrorDataCaches.Add(Role).Init(Schema->GetMirrorDataTable(Role), RoledBoneContainers[Role]);
|
|
}
|
|
|
|
FAnimationAssetSamplers TempAssetSamplers;
|
|
TArray<FBoneContainer, TInlineAllocator<PreallocatedRolesNum>> TempBoneContainers;
|
|
FRoleToIndex TempRoleToIndex;
|
|
|
|
int32 TotalPoses = 0;
|
|
for (int32 AssetIdx = 0; AssetIdx != SearchIndexBase.Assets.Num(); ++AssetIdx)
|
|
{
|
|
FSearchIndexAsset& SearchIndexAsset = SearchIndexBase.Assets[AssetIdx];
|
|
check(SearchIndexAsset.GetFirstPoseIdx() == TotalPoses);
|
|
|
|
const FPoseSearchDatabaseAnimationAssetBase* DatabaseAnimationAssetBase = DatabaseToLookForAssets->GetDatabaseAnimationAsset<FPoseSearchDatabaseAnimationAssetBase>(SearchIndexAsset.GetSourceAssetIdx());
|
|
check(DatabaseAnimationAssetBase);
|
|
|
|
TempAssetSamplers.Reset();
|
|
TempBoneContainers.Reset();
|
|
TempRoleToIndex.Reset();
|
|
|
|
const int32 NumRoles = DatabaseAnimationAssetBase->GetNumRoles();
|
|
for (int32 RoleIndex = 0; RoleIndex < NumRoles; ++RoleIndex)
|
|
{
|
|
const FRole& Role = DatabaseAnimationAssetBase->GetRole(RoleIndex);
|
|
if (const FMirrorDataCache* MirrorDataCache = RoledMirrorDataCaches.Find(Role))
|
|
{
|
|
UAnimationAsset* AnimationAsset = DatabaseAnimationAssetBase->GetAnimationAssetForRole(Role);
|
|
const FTransform RootTransformOrigin = DatabaseAnimationAssetBase->GetRootTransformOriginForRole(Role);
|
|
const FVector BlendParameters = SearchIndexAsset.GetBlendParameters();
|
|
|
|
const FSamplerMapKey SamplerMapKey(AnimationAsset, RootTransformOrigin, BlendParameters);
|
|
const int32* SamplerIndex = SamplerMap.Find(SamplerMapKey);
|
|
check(SamplerIndex && Samplers.IsValidIndex(*SamplerIndex));
|
|
|
|
TempAssetSamplers.AnimationAssetSamplers.Emplace(&Samplers[*SamplerIndex]);
|
|
TempAssetSamplers.MirrorDataCaches.Emplace(MirrorDataCache);
|
|
TempBoneContainers.Emplace(RoledBoneContainers[Role]);
|
|
TempRoleToIndex.Add(Role) = RoleIndex;
|
|
}
|
|
}
|
|
|
|
const FFloatInterval ExtrapolationTimeInterval = SearchIndexAsset.GetExtrapolationTimeInterval(Schema->SampleRate, AdditionalExtrapolationTime);
|
|
Indexers.Emplace(TempBoneContainers, SearchIndexAsset, SamplingContext, *Schema, TempAssetSamplers, TempRoleToIndex, ExtrapolationTimeInterval);
|
|
TotalPoses += SearchIndexAsset.GetNumPoses();
|
|
}
|
|
|
|
// allocating Values and PoseMetadata
|
|
SearchIndexBase.AllocateData(Schema->SchemaCardinality, TotalPoses);
|
|
|
|
// assigning local data to each Indexer
|
|
TotalPoses = 0;
|
|
for (int32 AssetIdx = 0; AssetIdx != SearchIndexBase.Assets.Num(); ++AssetIdx)
|
|
{
|
|
Indexers[AssetIdx].AssignWorkingData(TotalPoses, SearchIndexBase.Values, SearchIndexBase.PoseMetadata);
|
|
TotalPoses += Indexers[AssetIdx].GetNumIndexedPoses();
|
|
}
|
|
|
|
if (Owner.IsCanceled())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Index asset data
|
|
ParallelFor(Indexers.Num(), [&Indexers](int32 AssetIdx) { Indexers[AssetIdx].Process(AssetIdx); }, ParallelForFlags);
|
|
|
|
for (const FAssetIndexer& Indexer : Indexers)
|
|
{
|
|
if (Indexer.IsProcessFailed())
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (Owner.IsCanceled())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Joining EventData
|
|
FEventDataCollector EventDataCollector;
|
|
for (int32 AssetIdx = 0; AssetIdx != SearchIndexBase.Assets.Num(); ++AssetIdx)
|
|
{
|
|
EventDataCollector.MergeWith(Indexers[AssetIdx].GetEventDataCollector());
|
|
}
|
|
// sorting EventData to make it deterministic across multiple indexing
|
|
SearchIndexBase.EventData.Initialize(EventDataCollector);
|
|
|
|
// Joining Metadata.Flags into OverallFlags
|
|
SearchIndexBase.bAnyBlockTransition = false;
|
|
for (const FPoseMetadata& Metadata : SearchIndexBase.PoseMetadata)
|
|
{
|
|
if (Metadata.IsBlockTransition())
|
|
{
|
|
SearchIndexBase.bAnyBlockTransition = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Joining Stats
|
|
PRAGMA_DISABLE_DEPRECATION_WARNINGS
|
|
int32 NumAccumulatedSamples = 0;
|
|
SearchIndexBase.Stats = FSearchStats();
|
|
for (int32 AssetIdx = 0; AssetIdx != SearchIndexBase.Assets.Num(); ++AssetIdx)
|
|
{
|
|
const FAssetIndexer::FStats& Stats = Indexers[AssetIdx].GetStats();
|
|
SearchIndexBase.Stats.AverageSpeed += Stats.AccumulatedSpeed;
|
|
SearchIndexBase.Stats.MaxSpeed = FMath::Max(SearchIndexBase.Stats.MaxSpeed, Stats.MaxSpeed);
|
|
SearchIndexBase.Stats.AverageAcceleration += Stats.AccumulatedAcceleration;
|
|
SearchIndexBase.Stats.MaxAcceleration = FMath::Max(SearchIndexBase.Stats.MaxAcceleration, Stats.MaxAcceleration);
|
|
|
|
NumAccumulatedSamples += Stats.NumAccumulatedSamples;
|
|
}
|
|
|
|
if (NumAccumulatedSamples > 0)
|
|
{
|
|
const float Denom = 1.f / float(NumAccumulatedSamples);
|
|
SearchIndexBase.Stats.AverageSpeed *= Denom;
|
|
SearchIndexBase.Stats.AverageAcceleration *= Denom;
|
|
}
|
|
PRAGMA_ENABLE_DEPRECATION_WARNINGS
|
|
|
|
// Calculate Min Cost Addend
|
|
SearchIndexBase.MinCostAddend = 0.f;
|
|
if (!SearchIndexBase.PoseMetadata.IsEmpty())
|
|
{
|
|
SearchIndexBase.MinCostAddend = MAX_FLT;
|
|
for (const FPoseMetadata& PoseMetadata : SearchIndexBase.PoseMetadata)
|
|
{
|
|
if (PoseMetadata.GetCostAddend() < SearchIndexBase.MinCostAddend)
|
|
{
|
|
SearchIndexBase.MinCostAddend = PoseMetadata.GetCostAddend();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (Owner.IsCanceled())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// validating the SearchIndex against the Database to catch any data corruption, that can be caused by not updating or having conflicts over DDC key
|
|
static bool ValidateSearchInfoAgainstDatabase(const FSearchIndex& SearchIndex, const UPoseSearchDatabase* Database, const UE::DerivedData::FCacheKey& FullIndexKey)
|
|
{
|
|
check(Database && Database->Schema);
|
|
|
|
if (SearchIndex.GetNumDimensions() != Database->Schema->SchemaCardinality)
|
|
{
|
|
UE_LOG(LogPoseSearch, Warning, TEXT("%s - %s BuildIndex From Cache Corrupted! SchemaCardinality mismatch %d vs %d"), *LexToString(FullIndexKey.Hash), *Database->GetName(), SearchIndex.GetNumDimensions(), Database->Schema->SchemaCardinality);
|
|
return false;
|
|
}
|
|
|
|
TArray<bool> SourceAssetIdxs;
|
|
SourceAssetIdxs.SetNum(Database->GetNumAnimationAssets());
|
|
|
|
for (const FSearchIndexAsset& SearchIndexAsset : SearchIndex.Assets)
|
|
{
|
|
if (!SourceAssetIdxs.IsValidIndex(SearchIndexAsset.GetSourceAssetIdx()))
|
|
{
|
|
UE_LOG(LogPoseSearch, Warning, TEXT("%s - %s BuildIndex From Cache Corrupted! SearchIndex.Assets referencing missing asset with index %d"), *LexToString(FullIndexKey.Hash), *Database->GetName(), SearchIndexAsset.GetSourceAssetIdx());
|
|
return false;
|
|
}
|
|
|
|
SourceAssetIdxs[SearchIndexAsset.GetSourceAssetIdx()] = true;
|
|
}
|
|
|
|
for (int32 AnimationAssetIndex = 0; AnimationAssetIndex < Database->GetNumAnimationAssets(); ++AnimationAssetIndex)
|
|
{
|
|
if (const FPoseSearchDatabaseAnimationAssetBase* DatabaseAsset = Database->GetDatabaseAnimationAsset<FPoseSearchDatabaseAnimationAssetBase>(AnimationAssetIndex))
|
|
{
|
|
if (const UObject* AnimationAsset = DatabaseAsset->GetAnimationAsset())
|
|
{
|
|
if (DatabaseAsset->IsEnabled() && !SourceAssetIdxs[AnimationAssetIndex])
|
|
{
|
|
UE_LOG(LogPoseSearch, Warning, TEXT("%s - %s BuildIndex From Cache Corrupted! Couldn't find references to enabled asset %s in the SearchIndex"), *LexToString(FullIndexKey.Hash), *Database->GetName(), *AnimationAsset->GetName());
|
|
return false;
|
|
}
|
|
else if (!DatabaseAsset->IsEnabled() && SourceAssetIdxs[AnimationAssetIndex])
|
|
{
|
|
UE_LOG(LogPoseSearch, Warning, TEXT("%s - %s BuildIndex From Cache Corrupted! Found references to disabled asset %s in the SearchIndex"), *LexToString(FullIndexKey.Hash), *Database->GetName(), *AnimationAsset->GetName());
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (SourceAssetIdxs[AnimationAssetIndex])
|
|
{
|
|
UE_LOG(LogPoseSearch, Warning, TEXT("%s - %s BuildIndex From Cache Corrupted! Found references to null asset at index %d in the SearchIndex"), *LexToString(FullIndexKey.Hash), *Database->GetName(), AnimationAssetIndex);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogPoseSearch, Warning, TEXT("%s - %s BuildIndex From Cache Corrupted! null FPoseSearchDatabaseAnimationAssetBase asset at index %d!?"), *LexToString(FullIndexKey.Hash), *Database->GetName(), AnimationAssetIndex);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static void SynchronizeDatabaseChooser(UObject* Object)
|
|
{
|
|
if (UPoseSearchDatabase* Database = Cast<UPoseSearchDatabase>(Object))
|
|
{
|
|
Database->SynchronizeChooser();
|
|
}
|
|
else if (const UChooserTable* ChooserTable = Cast<UChooserTable>(Object))
|
|
{
|
|
const UChooserTable* RootChooser = ChooserTable->GetRootChooser();
|
|
|
|
IAssetRegistry& AssetRegistry = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry").Get();
|
|
TArray<FAssetIdentifier> Referencers;
|
|
AssetRegistry.GetReferencers(RootChooser->GetPackage()->GetFName(), Referencers);
|
|
|
|
TArray<FAssetData> Assets;
|
|
Assets.Reserve(256);
|
|
for (const FAssetIdentifier& Referencer : Referencers)
|
|
{
|
|
Assets.Reset();
|
|
AssetRegistry.GetAssetsByPackageName(Referencer.PackageName, Assets);
|
|
|
|
for (const FAssetData& Asset : Assets)
|
|
{
|
|
if (Asset.IsInstanceOf(UPoseSearchDatabase::StaticClass()))
|
|
{
|
|
if (UPoseSearchDatabase* ReferencedDatabase = CastChecked<UPoseSearchDatabase>(Asset.FastGetAsset(true)))
|
|
{
|
|
ReferencedDatabase->SynchronizeChooser();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#if ENABLE_ANIM_DEBUG
|
|
|
|
static void CompareChannelValues(int32 RecursionIndex, int32 PoseIndex, const TConstArrayView<float>& PoseA, const TConstArrayView<float>& PoseB, const TConstArrayView<TObjectPtr<UPoseSearchFeatureChannel>>& Channels, FStringBuilderBase& StringBuilder)
|
|
{
|
|
bool bPrintHeader = true;
|
|
for (const TObjectPtr<UPoseSearchFeatureChannel>& ChannelPtr : Channels)
|
|
{
|
|
const int32 ChannelCardinality = ChannelPtr->GetChannelCardinality();
|
|
const int32 ChannelDataOffset = ChannelPtr->GetChannelDataOffset();
|
|
|
|
for (int32 Index = 0; Index < ChannelCardinality; ++Index)
|
|
{
|
|
const int32 DataOffset = ChannelDataOffset + Index;
|
|
const float ValueA = PoseA[DataOffset];
|
|
const float ValueB = PoseB[DataOffset];
|
|
if (ValueA != ValueB)
|
|
{
|
|
if (bPrintHeader && RecursionIndex == 0)
|
|
{
|
|
StringBuilder.Appendf(TEXT("Values mismatch at pose %d\n"), PoseIndex);
|
|
bPrintHeader = false;
|
|
}
|
|
|
|
for (int32 Indentation = 0; Indentation < RecursionIndex; ++Indentation)
|
|
{
|
|
StringBuilder.Append(TEXT(" "));
|
|
}
|
|
|
|
StringBuilder.Appendf(TEXT("%s - %d (%f, %f)\n"), *ChannelPtr->GetName(), Index, ValueA, ValueB);
|
|
}
|
|
}
|
|
|
|
CompareChannelValues(RecursionIndex + 1, PoseIndex, PoseA, PoseB, ChannelPtr->GetSubChannels(), StringBuilder);
|
|
}
|
|
}
|
|
|
|
static void CompareSearchIndexBase(const FSearchIndexBase& A, const FSearchIndexBase& B, const UPoseSearchSchema* Schema, FStringBuilderBase& StringBuilder)
|
|
{
|
|
check(Schema);
|
|
|
|
if (A.Values.Num() != B.Values.Num())
|
|
{
|
|
StringBuilder.Append(TEXT("Values.Num mismatch\n"));
|
|
}
|
|
else if ((A.Values.Num() % Schema->SchemaCardinality) != 0)
|
|
{
|
|
StringBuilder.Append(TEXT("Values.Num is not a multiple of Schema->SchemaCardinality!\n"));
|
|
}
|
|
else if (Schema->SchemaCardinality > 0)
|
|
{
|
|
// cannot use A.GetNumPoses() since A.Values can be pruned out from duplicates
|
|
int32 ValuesDataOffset = 0;
|
|
const int32 NumValuePoses = A.Values.Num() / Schema->SchemaCardinality;
|
|
for (int32 ValuePoseIndex = 0; ValuePoseIndex < NumValuePoses; ++ValuePoseIndex)
|
|
{
|
|
const TConstArrayView<float> PoseA = MakeArrayView(A.Values).Slice(ValuePoseIndex * Schema->SchemaCardinality, Schema->SchemaCardinality);
|
|
const TConstArrayView<float> PoseB = MakeArrayView(B.Values).Slice(ValuePoseIndex * Schema->SchemaCardinality, Schema->SchemaCardinality);
|
|
CompareChannelValues(0, ValuePoseIndex, PoseA, PoseB, Schema->GetChannels(), StringBuilder);
|
|
}
|
|
}
|
|
|
|
if (A.ValuesVectorToPoseIndexes != B.ValuesVectorToPoseIndexes)
|
|
{
|
|
StringBuilder.Append(TEXT("ValuesVectorToPoseIndexes mismatch\n"));
|
|
}
|
|
|
|
if (A.PoseMetadata != B.PoseMetadata)
|
|
{
|
|
StringBuilder.Append(TEXT("PoseMetadata mismatch\n"));
|
|
}
|
|
|
|
if (A.bAnyBlockTransition != B.bAnyBlockTransition)
|
|
{
|
|
StringBuilder.Append(TEXT("bAnyBlockTransition mismatch\n"));
|
|
}
|
|
|
|
if (A.Assets != B.Assets)
|
|
{
|
|
StringBuilder.Append(TEXT("Assets mismatch\n"));
|
|
}
|
|
|
|
if (A.MinCostAddend != B.MinCostAddend)
|
|
{
|
|
StringBuilder.Append(TEXT("MinCostAddend mismatch\n"));
|
|
}
|
|
|
|
PRAGMA_DISABLE_DEPRECATION_WARNINGS
|
|
if (A.Stats != B.Stats)
|
|
{
|
|
StringBuilder.Append(TEXT("Stats mismatch\n"));
|
|
}
|
|
PRAGMA_ENABLE_DEPRECATION_WARNINGS
|
|
}
|
|
|
|
static void CompareSearchIndex(const FSearchIndex& A, const FSearchIndex& B, const UPoseSearchSchema* Schema, FStringBuilderBase& StringBuilder)
|
|
{
|
|
CompareSearchIndexBase(A, B, Schema, StringBuilder);
|
|
|
|
if (A.WeightsSqrt != B.WeightsSqrt)
|
|
{
|
|
StringBuilder.Append(TEXT("WeightsSqrt mismatch\n"));
|
|
}
|
|
|
|
if (A.PCAValues != B.PCAValues)
|
|
{
|
|
StringBuilder.Append(TEXT("PCAValues mismatch\n"));
|
|
}
|
|
|
|
if (A.PCAValuesVectorToPoseIndexes != B.PCAValuesVectorToPoseIndexes)
|
|
{
|
|
StringBuilder.Append(TEXT("PCAValuesVectorToPoseIndexes mismatch\n"));
|
|
}
|
|
|
|
if (A.PCAProjectionMatrix != B.PCAProjectionMatrix)
|
|
{
|
|
StringBuilder.Append(TEXT("PCAProjectionMatrix mismatch\n"));
|
|
}
|
|
|
|
if (A.Mean != B.Mean)
|
|
{
|
|
StringBuilder.Append(TEXT("Mean mismatch\n"));
|
|
}
|
|
|
|
if (A.KDTree != B.KDTree)
|
|
{
|
|
StringBuilder.Append(TEXT("KDTree mismatch\n"));
|
|
}
|
|
|
|
if (A.VPTree != B.VPTree)
|
|
{
|
|
StringBuilder.Append(TEXT("VPTree mismatch\n"));
|
|
}
|
|
|
|
#if WITH_EDITORONLY_DATA
|
|
if (A.DeviationEditorOnly != B.DeviationEditorOnly)
|
|
{
|
|
StringBuilder.Append(TEXT("DeviationEditorOnly mismatch\n"));
|
|
}
|
|
|
|
if (A.PCAExplainedVarianceEditorOnly != B.PCAExplainedVarianceEditorOnly)
|
|
{
|
|
StringBuilder.Append(TEXT("PCAExplainedVarianceEditorOnly mismatch\n"));
|
|
}
|
|
#endif // WITH_EDITORONLY_DATA
|
|
|
|
PRAGMA_DISABLE_DEPRECATION_WARNINGS
|
|
if (A.PCAExplainedVariance != B.PCAExplainedVariance)
|
|
{
|
|
StringBuilder.Append(TEXT("PCAExplainedVariance mismatch\n"));
|
|
}
|
|
PRAGMA_ENABLE_DEPRECATION_WARNINGS
|
|
}
|
|
|
|
#endif // ENABLE_ANIM_DEBUG
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// FPoseSearchDatabaseAsyncCacheTask
|
|
struct FPoseSearchDatabaseAsyncCacheTask
|
|
{
|
|
enum class EState
|
|
{
|
|
Notstarted, // key generation failed (not all the asset has been post loaded). It'll be retried to StartNewRequestIfNeeded the next Update
|
|
Prestarted, // key has been successfully generated and we kicked the DDC get
|
|
PreCancelled, // the task has been requested to be cancelled
|
|
Cancelled, // the task cancellation has been finalized
|
|
Ended, // the task has ended successfully
|
|
Failed // the task has ended unsuccessfully
|
|
};
|
|
|
|
FPoseSearchDatabaseAsyncCacheTask(UPoseSearchDatabase* InDatabase, bool bPerformConditionalPostLoadIfRequired, FPartialKeyHashes& PartialKeyHashes);
|
|
void ClearAnimSequenceResidency();
|
|
void StartNewRequestIfNeeded(bool bPerformConditionalPostLoadIfRequired, FPartialKeyHashes& PartialKeyHashes);
|
|
void Update(FCriticalSection& OuterMutex, FPartialKeyHashes& PartialKeyHashes);
|
|
void Wait(FCriticalSection& OuterMutex);
|
|
void PreCancelIfDependsOn(const UObject* Object);
|
|
void Cancel();
|
|
bool Poll() const;
|
|
void AddReferencedObjects(FReferenceCollector& Collector);
|
|
bool IsValid() const;
|
|
const FIoHash& GetDerivedDataKey() const { return DerivedDataKey; }
|
|
const UPoseSearchDatabase* GetDatabase() const { return Database.Get(); }
|
|
|
|
~FPoseSearchDatabaseAsyncCacheTask();
|
|
EState GetState() const { return EState(ThreadSafeState.GetValue()); }
|
|
|
|
#if ENABLE_ANIM_DEBUG
|
|
void TestSynchronizeWithExternalDependencies();
|
|
#endif //ENABLE_ANIM_DEBUG
|
|
|
|
private:
|
|
FPoseSearchDatabaseAsyncCacheTask(const FPoseSearchDatabaseAsyncCacheTask& Other) = delete;
|
|
FPoseSearchDatabaseAsyncCacheTask(FPoseSearchDatabaseAsyncCacheTask&& Other) = delete;
|
|
FPoseSearchDatabaseAsyncCacheTask& operator=(const FPoseSearchDatabaseAsyncCacheTask& Other) = delete;
|
|
FPoseSearchDatabaseAsyncCacheTask& operator=(FPoseSearchDatabaseAsyncCacheTask&& Other) = delete;
|
|
|
|
void OnGetComplete(UE::DerivedData::FCacheGetResponse&& Response);
|
|
void SetState(EState State) { ThreadSafeState.Set(int32(State)); }
|
|
void ResetSearchIndex();
|
|
|
|
TWeakObjectPtr<UPoseSearchDatabase> Database;
|
|
FSearchIndex SearchIndex;
|
|
|
|
#if ENABLE_ANIM_DEBUG
|
|
FSearchIndex SearchIndexCompare;
|
|
#endif //ENABLE_ANIM_DEBUG
|
|
|
|
UE::DerivedData::FRequestOwner Owner;
|
|
FIoHash DerivedDataKey = FIoHash::Zero;
|
|
TSet<TWeakObjectPtr<const UObject>> DatabaseDependencies;
|
|
|
|
FThreadSafeCounter ThreadSafeState = int32(EState::Notstarted);
|
|
bool bBroadcastOnDerivedDataRebuild = false;
|
|
bool bResidencyCleared = true;
|
|
};
|
|
|
|
class FPoseSearchDatabaseAsyncCacheTasks : public TArray<TUniquePtr<FPoseSearchDatabaseAsyncCacheTask>, TInlineAllocator<64>> {};
|
|
|
|
FPoseSearchDatabaseAsyncCacheTask::FPoseSearchDatabaseAsyncCacheTask(UPoseSearchDatabase* InDatabase, bool bPerformConditionalPostLoadIfRequired, FPartialKeyHashes& PartialKeyHashes)
|
|
: Database(InDatabase)
|
|
, Owner(UE::DerivedData::EPriority::Normal)
|
|
, DerivedDataKey(FIoHash::Zero)
|
|
{
|
|
if (IsInGameThread())
|
|
{
|
|
// it is safe to compose DDC key only on the game thread, since assets can modified in this thread execution
|
|
StartNewRequestIfNeeded(bPerformConditionalPostLoadIfRequired, PartialKeyHashes);
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogPoseSearch, Log, TEXT("Delaying DDC until on the game thread - %s"), *Database->GetName());
|
|
}
|
|
}
|
|
|
|
void FPoseSearchDatabaseAsyncCacheTask::ClearAnimSequenceResidency()
|
|
{
|
|
if (bResidencyCleared)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const ITargetPlatform* TargetPlatform = GetTargetPlatformManager()->GetRunningTargetPlatform();
|
|
const uint32 DatabaseHash = GetTypeHash(Database.Get());
|
|
for (TWeakObjectPtr<const UObject>& Dependency : DatabaseDependencies)
|
|
{
|
|
if (Dependency.IsValid())
|
|
{
|
|
if (UAnimSequence* AnimSequence = Cast<UAnimSequence>(const_cast<UObject*>(Dependency.Get())))
|
|
{
|
|
if (AnimSequence->HasResidency(DatabaseHash))
|
|
{
|
|
AnimSequence->ReleaseResidency(TargetPlatform, DatabaseHash);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bResidencyCleared = true;
|
|
}
|
|
|
|
FPoseSearchDatabaseAsyncCacheTask::~FPoseSearchDatabaseAsyncCacheTask()
|
|
{
|
|
// Owner.Cancel must be performed before SearchIndex.Reset() in case any task is flying (launched by Owner.LaunchTask)
|
|
Owner.Cancel();
|
|
|
|
Database = nullptr;
|
|
|
|
ResetSearchIndex();
|
|
|
|
DerivedDataKey = FIoHash::Zero;
|
|
DatabaseDependencies.Reset();
|
|
}
|
|
|
|
void FPoseSearchDatabaseAsyncCacheTask::ResetSearchIndex()
|
|
{
|
|
SearchIndex.Reset();
|
|
#if ENABLE_ANIM_DEBUG
|
|
SearchIndexCompare.Reset();
|
|
#endif //ENABLE_ANIM_DEBUG
|
|
}
|
|
|
|
#if ENABLE_ANIM_DEBUG
|
|
void FPoseSearchDatabaseAsyncCacheTask::TestSynchronizeWithExternalDependencies()
|
|
{
|
|
if (GetState() == EState::Ended && Database != nullptr)
|
|
{
|
|
Database->TestSynchronizeWithExternalDependencies();
|
|
}
|
|
}
|
|
#endif //ENABLE_ANIM_DEBUG
|
|
|
|
void FPoseSearchDatabaseAsyncCacheTask::StartNewRequestIfNeeded(bool bPerformConditionalPostLoadIfRequired, FPartialKeyHashes& PartialKeyHashes)
|
|
{
|
|
using namespace UE::DerivedData;
|
|
|
|
check(IsInGameThread());
|
|
|
|
// making sure there are no active requests
|
|
// Owner.Cancel must be performed before SearchIndex.Reset() in case any task is flying (launched by Owner.LaunchTask)
|
|
Owner.Cancel();
|
|
|
|
FKeyBuilder::EDebugPartialKeyHashesMode DebugPartialKeyHashesMode;
|
|
switch (GVarMotionMatchPartialKeyHashesMode)
|
|
{
|
|
case 0:
|
|
DebugPartialKeyHashesMode = FKeyBuilder::EDebugPartialKeyHashesMode::Use;
|
|
break;
|
|
case 1:
|
|
DebugPartialKeyHashesMode = FKeyBuilder::EDebugPartialKeyHashesMode::DoNotUse;
|
|
break;
|
|
default:
|
|
DebugPartialKeyHashesMode = FKeyBuilder::EDebugPartialKeyHashesMode::Validate;
|
|
break;
|
|
}
|
|
|
|
// composing the key
|
|
const FKeyBuilder KeyBuilder(Database.Get(), true, bPerformConditionalPostLoadIfRequired, &PartialKeyHashes, DebugPartialKeyHashesMode);
|
|
if (KeyBuilder.AnyAssetNotFullyLoaded())
|
|
{
|
|
DerivedDataKey = FIoHash::Zero;
|
|
SetState(EState::Notstarted);
|
|
|
|
UE_LOG(LogPoseSearch, Log, TEXT("Delaying DDC until dependents are fully loaded - %s"), *Database->GetName());
|
|
}
|
|
else
|
|
{
|
|
const FIoHash NewDerivedDataKey(KeyBuilder.Finalize());
|
|
const bool bHasKeyChanged = NewDerivedDataKey != DerivedDataKey;
|
|
if (bHasKeyChanged)
|
|
{
|
|
if (!KeyBuilder.AnyAssetNotReady())
|
|
{
|
|
DerivedDataKey = NewDerivedDataKey;
|
|
|
|
DatabaseDependencies.Reset();
|
|
DatabaseDependencies.Reserve(KeyBuilder.GetDependencies().Num());
|
|
for (const UObject* Dependency : KeyBuilder.GetDependencies())
|
|
{
|
|
DatabaseDependencies.Add(Dependency);
|
|
}
|
|
|
|
SetState(EState::Prestarted);
|
|
|
|
UE_LOG(LogPoseSearch, Log, TEXT("%s - %s BeginCache"), *LexToString(DerivedDataKey), *Database->GetName());
|
|
|
|
const FCacheKey CacheKey{ Bucket, DerivedDataKey };
|
|
const FCacheGetRequest CacheRequest = { { Database->GetPathName() }, CacheKey, ECachePolicy::Default };
|
|
|
|
Owner = FRequestOwner(EPriority::Normal);
|
|
GetCache().Get(MakeArrayView(&CacheRequest, 1), Owner, [this](FCacheGetResponse&& Response)
|
|
{
|
|
OnGetComplete(MoveTemp(Response));
|
|
});
|
|
}
|
|
else
|
|
{
|
|
DerivedDataKey = FIoHash::Zero;
|
|
SetState(EState::Notstarted);
|
|
UE_LOG(LogPoseSearch, Log, TEXT("Delaying DDC until dependents are ready - %s"), *Database->GetName());
|
|
}
|
|
|
|
bResidencyCleared = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// it cancels and waits for the task to be done and set the state to PreCancelled, so no other new requests can start until the task gets cancelled
|
|
void FPoseSearchDatabaseAsyncCacheTask::PreCancelIfDependsOn(const UObject* Object)
|
|
{
|
|
check(IsInGameThread());
|
|
|
|
if (Object)
|
|
{
|
|
// DatabaseDependencies is updated only in StartNewRequestIfNeeded when there are no active requests, so it's thread safe to access it
|
|
if (DatabaseDependencies.Contains(Object))
|
|
{
|
|
// Database can be null if the task was Ended/Failed and Database was already garbage collected, but Tick hasn't been called yet
|
|
FString DatabaseName = IsValid() ? *Database->GetName() : TEXT("Garbage Collected Database");
|
|
UE_LOG(LogPoseSearch, Log, TEXT("%s - %s PreCancelled because of %s"), *LexToString(DerivedDataKey), *DatabaseName, *Object->GetName());
|
|
|
|
// Owner.Cancel must be performed before SearchIndex.Reset() in case any task is flying (launched by Owner.LaunchTask)
|
|
Owner.Cancel();
|
|
|
|
SetState(EState::PreCancelled);
|
|
}
|
|
}
|
|
}
|
|
|
|
// it cancels and waits for the task to be done and reset the local SearchIndex. SetState to Cancelled
|
|
void FPoseSearchDatabaseAsyncCacheTask::Cancel()
|
|
{
|
|
check(IsInGameThread());
|
|
|
|
FString DatabaseName = IsValid() ? *Database->GetName() : TEXT("Garbage Collected Database");
|
|
UE_LOG(LogPoseSearch, Log, TEXT("%s - %s Cancelled"), *LexToString(DerivedDataKey), *DatabaseName);
|
|
|
|
// Owner.Cancel must be performed before SearchIndex.Reset() in case any task is flying (launched by Owner.LaunchTask)
|
|
Owner.Cancel();
|
|
|
|
ResetSearchIndex();
|
|
|
|
DerivedDataKey = FIoHash::Zero;
|
|
SetState(EState::Cancelled);
|
|
ClearAnimSequenceResidency();
|
|
}
|
|
|
|
void FPoseSearchDatabaseAsyncCacheTask::Update(FCriticalSection& OuterMutex, FPartialKeyHashes& PartialKeyHashes)
|
|
{
|
|
check(IsInGameThread());
|
|
|
|
check(GetState() != EState::Cancelled); // otherwise FPoseSearchDatabaseAsyncCacheTask should have been already removed
|
|
|
|
if (GetState() == EState::Notstarted)
|
|
{
|
|
StartNewRequestIfNeeded(false, PartialKeyHashes);
|
|
}
|
|
|
|
if (GetState() == EState::Prestarted && Poll())
|
|
{
|
|
// task is done: we need to update the state form Prestarted to Ended/Failed
|
|
Wait(OuterMutex);
|
|
}
|
|
|
|
if (GetState() != EState::PreCancelled)
|
|
{
|
|
if (bBroadcastOnDerivedDataRebuild)
|
|
{
|
|
Database->NotifyDerivedDataRebuild();
|
|
bBroadcastOnDerivedDataRebuild = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// it waits for the task to be done and SetSearchIndex on the database. SetState to Ended/Failed
|
|
void FPoseSearchDatabaseAsyncCacheTask::Wait(FCriticalSection& OuterMutex)
|
|
{
|
|
check(GetState() == EState::Prestarted);
|
|
|
|
FScopeLock Lock(&OuterMutex);
|
|
Owner.Wait();
|
|
|
|
const bool bFailedIndexing = SearchIndex.IsEmpty();
|
|
if (!bFailedIndexing)
|
|
{
|
|
Database->SetSearchIndex(SearchIndex);
|
|
|
|
check(Database->Schema && !SearchIndex.IsEmpty() && SearchIndex.GetNumDimensions() == Database->Schema->SchemaCardinality);
|
|
|
|
SetState(EState::Ended);
|
|
ClearAnimSequenceResidency();
|
|
bBroadcastOnDerivedDataRebuild = true;
|
|
}
|
|
else
|
|
{
|
|
check(!bBroadcastOnDerivedDataRebuild);
|
|
ClearAnimSequenceResidency();
|
|
SetState(EState::Failed);
|
|
}
|
|
ResetSearchIndex();
|
|
}
|
|
|
|
// true is the task is done executing
|
|
bool FPoseSearchDatabaseAsyncCacheTask::Poll() const
|
|
{
|
|
return Owner.Poll();
|
|
}
|
|
|
|
bool FPoseSearchDatabaseAsyncCacheTask::IsValid() const
|
|
{
|
|
return Database.IsValid();
|
|
}
|
|
|
|
// called once the task is done:
|
|
// if EStatus::Ok (data has been retrieved from DDC) we deserialize the payload into the local SearchIndex
|
|
// if EStatus::Error we BuildIndex and if that's successful we 'Put' it on DDC
|
|
void FPoseSearchDatabaseAsyncCacheTask::OnGetComplete(UE::DerivedData::FCacheGetResponse&& Response)
|
|
{
|
|
using namespace UE::DerivedData;
|
|
|
|
const FCacheKey& FullIndexKey = Response.Record.GetKey();
|
|
|
|
// The database is part of the derived data cache and up to date, skip re-building it.
|
|
bool bCacheCorrupted = false;
|
|
if (Response.Status == EStatus::Ok)
|
|
{
|
|
COOK_STAT(auto Timer = UsageStats.TimeAsyncWait());
|
|
|
|
// we found the cached data associated to the PendingDerivedDataKey: we'll deserialized into SearchIndex
|
|
ResetSearchIndex();
|
|
|
|
FSharedBuffer RawData = Response.Record.GetValue(Id).GetData().Decompress();
|
|
FMemoryReaderView Reader(RawData);
|
|
Reader << SearchIndex;
|
|
|
|
check(Database != nullptr && Database->Schema);
|
|
// cache can be corrupted in case the version of the derived data cache has not being updated while
|
|
// developing channels that changes their cardinality without impacting any asset properties
|
|
// so to account for this, we just reindex the database and update the associated DDC
|
|
if (ValidateSearchInfoAgainstDatabase(SearchIndex, Database.Get(), FullIndexKey))
|
|
{
|
|
UE_LOG(LogPoseSearch, Log, TEXT("%s - %s BuildIndex From Cache"), *LexToString(FullIndexKey.Hash), *Database->GetName());
|
|
}
|
|
else
|
|
{
|
|
bCacheCorrupted = true;
|
|
}
|
|
|
|
COOK_STAT(Timer.AddHit(RawData.GetSize()));
|
|
}
|
|
|
|
if (Response.Status == EStatus::Canceled)
|
|
{
|
|
ResetSearchIndex();
|
|
UE_LOG(LogPoseSearch, Log, TEXT("%s - %s BuildIndex Cancelled"), *LexToString(FullIndexKey.Hash), *Database->GetName());
|
|
}
|
|
|
|
bool bForceBuildIndex = false;
|
|
#if ENABLE_ANIM_DEBUG
|
|
bForceBuildIndex = AnyTestFlags(EMotionMatchTestFlags::ForceIndexing);
|
|
#endif // ENABLE_ANIM_DEBUG
|
|
|
|
if (Response.Status == EStatus::Error || bCacheCorrupted || bForceBuildIndex)
|
|
{
|
|
bool bCompareSearchIndex = false;
|
|
#if ENABLE_ANIM_DEBUG
|
|
bCompareSearchIndex = Response.Status != EStatus::Error && !bCacheCorrupted && bForceBuildIndex;
|
|
if (bCompareSearchIndex)
|
|
{
|
|
SearchIndexCompare = SearchIndex;
|
|
}
|
|
#endif // ENABLE_ANIM_DEBUG
|
|
|
|
// we didn't find the cached data associated to the PendingDerivedDataKey: we'll BuildIndex to update SearchIndex and "Put" the data over the DDC
|
|
Owner.LaunchTask(TEXT("PoseSearchDatabaseBuild"), [this, FullIndexKey, bCompareSearchIndex]
|
|
{
|
|
COOK_STAT(auto Timer = UsageStats.TimeSyncWork());
|
|
|
|
const UPoseSearchDatabase* MainDatabase = Database.Get();
|
|
|
|
// collecting all the databases that need to be built to gather their FSearchIndexBase
|
|
// the first one is always the main database (the one we're calculating the index on)
|
|
TArray<const UPoseSearchDatabase*> IndexBaseDatabases;
|
|
IndexBaseDatabases.Reserve(64);
|
|
IndexBaseDatabases.Add(MainDatabase);
|
|
if (!MainDatabase)
|
|
{
|
|
UE_LOG(LogPoseSearch, Log, TEXT("%s - BuildIndex Cancelled because associated Database weak pointer has been released."), *LexToString(FullIndexKey.Hash));
|
|
ResetSearchIndex();
|
|
return;
|
|
}
|
|
|
|
if (MainDatabase->NormalizationSet)
|
|
{
|
|
MainDatabase->NormalizationSet->AddUniqueDatabases(IndexBaseDatabases);
|
|
}
|
|
|
|
const FString MainDatabaseName = MainDatabase->GetName();
|
|
const UPoseSearchSchema* MainDatabaseSchema = MainDatabase->Schema;
|
|
if (!MainDatabaseSchema || MainDatabaseSchema->SchemaCardinality <= 0)
|
|
{
|
|
UE_LOG(LogPoseSearch, Error, TEXT("%s - %s BuildIndex Failed because of invalid Schema"), *LexToString(FullIndexKey.Hash), *MainDatabaseName);
|
|
ResetSearchIndex();
|
|
return;
|
|
}
|
|
|
|
const bool bNormalizeWithCommonSchema = MainDatabaseSchema->DataPreprocessor == EPoseSearchDataPreprocessor::NormalizeWithCommonSchema;
|
|
|
|
// @todo: DDC or parallelize this code
|
|
TArray<FSearchIndexBase> SearchIndexBases;
|
|
TArray<const UPoseSearchSchema*> Schemas;
|
|
SearchIndexBases.AddDefaulted(IndexBaseDatabases.Num());
|
|
Schemas.AddDefaulted(IndexBaseDatabases.Num());
|
|
for (int32 IndexBaseIdx = 0; IndexBaseIdx < IndexBaseDatabases.Num(); ++IndexBaseIdx)
|
|
{
|
|
const UPoseSearchDatabase* DependentDatabase = IndexBaseDatabases[IndexBaseIdx];
|
|
check(DependentDatabase);
|
|
|
|
const UPoseSearchSchema* DependentDatabaseSchema = bNormalizeWithCommonSchema ? MainDatabaseSchema : DependentDatabase->Schema.Get();
|
|
const FString DependentDatabaseName = DependentDatabase->GetName();
|
|
|
|
FSearchIndexBase& SearchIndexBase = SearchIndexBases[IndexBaseIdx];
|
|
|
|
Schemas[IndexBaseIdx] = DependentDatabaseSchema;
|
|
const FAssetSamplingContext DependentSamplingContext(*DependentDatabase);
|
|
const FFloatInterval& DependentExcludeFromDatabaseParameters = DependentDatabase->ExcludeFromDatabaseParameters;
|
|
const FFloatInterval& DependentAdditionalExtrapolationTime = DependentDatabase->AdditionalExtrapolationTime;
|
|
|
|
// early out for invalid indexing conditions
|
|
if (!DependentDatabaseSchema || DependentDatabaseSchema->SchemaCardinality <= 0)
|
|
{
|
|
if (IndexBaseIdx == 0)
|
|
{
|
|
UE_LOG(LogPoseSearch, Error, TEXT("%s - %s BuildIndex Failed because of invalid Schema"), *LexToString(FullIndexKey.Hash), *MainDatabaseName);
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogPoseSearch, Error, TEXT("%s - %s BuildIndex Failed because dependent database '%s' has an invalid Schema"), *LexToString(FullIndexKey.Hash), *MainDatabaseName, *DependentDatabaseName);
|
|
}
|
|
ResetSearchIndex();
|
|
return;
|
|
}
|
|
|
|
// validating that the missing MirrorDataTable(s) are not necessary
|
|
bool bAllRoledSkeletonHaveMirrorDataTable = true;
|
|
for (const FPoseSearchRoledSkeleton& RoledSkeleton : DependentDatabaseSchema->GetRoledSkeletons())
|
|
{
|
|
if (RoledSkeleton.MirrorDataTable)
|
|
{
|
|
if (!RoledSkeleton.MirrorDataTable->Skeleton)
|
|
{
|
|
UE_LOG(LogPoseSearch, Error, TEXT("%s - %s BuildIndex Failed because '%s' schema MirrorDataTable Skeleton is not set for Role '%s' "), *LexToString(FullIndexKey.Hash), *MainDatabaseName, *DependentDatabaseName, *RoledSkeleton.Role.ToString());
|
|
ResetSearchIndex();
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
bAllRoledSkeletonHaveMirrorDataTable = false;
|
|
}
|
|
}
|
|
|
|
if (!bAllRoledSkeletonHaveMirrorDataTable)
|
|
{
|
|
for (int32 AnimationAssetIndex = 0; AnimationAssetIndex < DependentDatabase->GetNumAnimationAssets(); ++AnimationAssetIndex)
|
|
{
|
|
if (const FPoseSearchDatabaseAnimationAssetBase* DatabaseAsset = DependentDatabase->GetDatabaseAnimationAsset<FPoseSearchDatabaseAnimationAssetBase>(AnimationAssetIndex))
|
|
{
|
|
if (DatabaseAsset->GetMirrorOption() == EPoseSearchMirrorOption::MirroredOnly || DatabaseAsset->GetMirrorOption() == EPoseSearchMirrorOption::UnmirroredAndMirrored)
|
|
{
|
|
// want to sample a mirrored asset
|
|
UE_LOG(LogPoseSearch, Error, TEXT("%s - %s BuildIndex Failed because '%s' schema requires MirrorDataTable(s) to sample mirrored animation assets"), *LexToString(FullIndexKey.Hash), *MainDatabaseName, *DependentDatabaseName);
|
|
ResetSearchIndex();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (int32 AnimationAssetIndex = 0; AnimationAssetIndex < DependentDatabase->GetNumAnimationAssets(); ++AnimationAssetIndex)
|
|
{
|
|
if (const FPoseSearchDatabaseAnimationAssetBase* DatabaseAsset = DependentDatabase->GetDatabaseAnimationAsset<FPoseSearchDatabaseAnimationAssetBase>(AnimationAssetIndex))
|
|
{
|
|
if (DatabaseAsset->GetAnimationAsset() == nullptr)
|
|
{
|
|
UE_LOG(LogPoseSearch, Warning, TEXT("OnGetComplete - - No asset has been selected."));
|
|
}
|
|
// @todo: Find a way to prevent accessing UObjects while GC since this func is ran async. Commenting out for now to prevent crashes.
|
|
/*else if (!DatabaseAsset->IsSkeletonCompatible(Database->Schema))
|
|
{
|
|
UE_LOG(LogPoseSearch, Warning, TEXT("OnGetComplete - %s's skeleton is not compatible with the schema's skeleton(s)."), *DatabaseAsset->GetName());
|
|
}*/
|
|
else if (const UBlendSpace* BlendSpace = Cast<UBlendSpace>(DatabaseAsset->GetAnimationAsset()))
|
|
{
|
|
if (!BlendSpace->bShouldMatchSyncPhases)
|
|
{
|
|
UE_LOG(LogPoseSearch, Warning, TEXT("OnGetComplete - %s's bShouldMatchSyncPhases flag is not enabled. This is required for properly pose matching blendspaces."), *DatabaseAsset->GetName());
|
|
}
|
|
else
|
|
{
|
|
const TArray<FBlendSample>& BlendSamples = BlendSpace->GetBlendSamples();
|
|
for (int i = 0; i < BlendSamples.Num() - 1; ++i)
|
|
{
|
|
const FBlendSample& CurrSample = BlendSamples[i];
|
|
const FBlendSample& NextSample = BlendSamples[i + 1];
|
|
|
|
if (CurrSample.Animation && NextSample.Animation)
|
|
{
|
|
bool bWarning = false;
|
|
|
|
if (CurrSample.Animation->AuthoredSyncMarkers.Num() == NextSample.Animation->AuthoredSyncMarkers.Num())
|
|
{
|
|
for (int j = 0; j < CurrSample.Animation->AuthoredSyncMarkers.Num(); ++j)
|
|
{
|
|
if (CurrSample.Animation->AuthoredSyncMarkers[j].MarkerName != NextSample.Animation->AuthoredSyncMarkers[j].MarkerName)
|
|
{
|
|
bWarning = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
bWarning = true;
|
|
}
|
|
|
|
if (bWarning)
|
|
{
|
|
UE_LOG(LogPoseSearch, Warning, TEXT("OnGetComplete - %s's samples don't share the same layout of sync markers. This is required for properly pose matching blendspaces."), *DatabaseAsset->GetName());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (Owner.IsCanceled())
|
|
{
|
|
UE_LOG(LogPoseSearch, Log, TEXT("%s - %s BuildIndex Cancelled"), *LexToString(FullIndexKey.Hash), *MainDatabaseName);
|
|
ResetSearchIndex();
|
|
return;
|
|
}
|
|
|
|
// Building all the related FPoseSearchBaseIndex first
|
|
if (!InitSearchIndexAssets(SearchIndexBase, DependentDatabase, DependentDatabaseSchema, DependentExcludeFromDatabaseParameters))
|
|
{
|
|
UE_LOG(LogPoseSearch, Error, TEXT("%s - %s BuildIndex Failed becasue of invalid assets"), *LexToString(FullIndexKey.Hash), *MainDatabaseName);
|
|
ResetSearchIndex();
|
|
return;
|
|
}
|
|
|
|
if (Owner.IsCanceled())
|
|
{
|
|
UE_LOG(LogPoseSearch, Log, TEXT("%s - %s BuildIndex Cancelled"), *LexToString(FullIndexKey.Hash), *MainDatabaseName);
|
|
ResetSearchIndex();
|
|
return;
|
|
}
|
|
|
|
if (!IndexDatabase(SearchIndexBase, DependentDatabase, DependentDatabaseSchema, DependentSamplingContext, DependentAdditionalExtrapolationTime, Owner))
|
|
{
|
|
UE_LOG(LogPoseSearch, Log, TEXT("%s - %s BuildIndex Cancelled"), *LexToString(FullIndexKey.Hash), *MainDatabaseName);
|
|
ResetSearchIndex();
|
|
return;
|
|
}
|
|
|
|
#if ENABLE_ANIM_DEBUG
|
|
if (AnyTestFlags(EMotionMatchTestFlags::TestIndexDatabaseDeterminism))
|
|
{
|
|
const int32 NumIterations = GVarMotionMatchTestNumIterations;
|
|
for (int32 Iteration = 0; Iteration < NumIterations; ++Iteration)
|
|
{
|
|
FSearchIndexBase TestSearchIndexBase = SearchIndexBase;
|
|
if (IndexDatabase(TestSearchIndexBase, DependentDatabase, DependentDatabaseSchema, DependentSamplingContext, DependentAdditionalExtrapolationTime, Owner))
|
|
{
|
|
if (TestSearchIndexBase != SearchIndexBase)
|
|
{
|
|
FStringBuilderBase Message;
|
|
CompareSearchIndexBase(TestSearchIndexBase, SearchIndexBase, DependentDatabaseSchema, Message);
|
|
UE_LOG(LogPoseSearch, Warning, TEXT("OnGetComplete - IndexDatabase is not deterministic\n%s"), *Message);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif //ENABLE_ANIM_DEBUG
|
|
}
|
|
|
|
static_cast<FSearchIndexBase&>(SearchIndex) = SearchIndexBases[0];
|
|
|
|
#if ENABLE_ANIM_DEBUG
|
|
// testing PruneDuplicateValues determinism
|
|
if (AnyTestFlags(EMotionMatchTestFlags::TestPruneDuplicateValuesDeterminism))
|
|
{
|
|
const int32 NumIterations = GVarMotionMatchTestNumIterations;
|
|
|
|
FSearchIndex TestSearchIndexA = SearchIndex;
|
|
TestSearchIndexA.PruneDuplicateValues(MainDatabase->PosePruningSimilarityThreshold, MainDatabaseSchema->SchemaCardinality, false);
|
|
for (int32 Iteration = 0; Iteration < NumIterations; ++Iteration)
|
|
{
|
|
FSearchIndex TestSearchIndexB = SearchIndex;
|
|
TestSearchIndexB.PruneDuplicateValues(MainDatabase->PosePruningSimilarityThreshold, MainDatabaseSchema->SchemaCardinality, false);
|
|
|
|
if (TestSearchIndexA.Values != TestSearchIndexB.Values)
|
|
{
|
|
UE_LOG(LogPoseSearch, Warning, TEXT("OnGetComplete - PruneDuplicateValues is not deterministic"));
|
|
}
|
|
|
|
if (TestSearchIndexA.ValuesVectorToPoseIndexes != TestSearchIndexB.ValuesVectorToPoseIndexes)
|
|
{
|
|
UE_LOG(LogPoseSearch, Warning, TEXT("OnGetComplete - PruneDuplicateValues ValuesVectorToPoseIndexes generation is not deterministic"));
|
|
}
|
|
}
|
|
}
|
|
#endif // ENABLE_ANIM_DEBUG
|
|
|
|
// VPTree requires ValuesVectorToPoseIndexes if there's any Values pruning
|
|
const bool bDoNotGenerateValuesVectorToPoseIndexes = MainDatabase->PoseSearchMode != EPoseSearchMode::VPTree;
|
|
SearchIndex.PruneDuplicateValues(MainDatabase->PosePruningSimilarityThreshold, MainDatabaseSchema->SchemaCardinality, bDoNotGenerateValuesVectorToPoseIndexes);
|
|
|
|
const TArray<float> Deviation = FMeanDeviationCalculator::Calculate(SearchIndexBases, Schemas);
|
|
|
|
// Building FSearchIndex
|
|
PreprocessSearchIndexWeights(SearchIndex, MainDatabaseSchema, Deviation);
|
|
if (Owner.IsCanceled())
|
|
{
|
|
UE_LOG(LogPoseSearch, Log, TEXT("%s - %s BuildIndex Cancelled"), *LexToString(FullIndexKey.Hash), *MainDatabaseName);
|
|
ResetSearchIndex();
|
|
return;
|
|
}
|
|
|
|
const Eigen::ComputationInfo ComputationInfo = PreprocessSearchIndexPCAData(SearchIndex, MainDatabaseSchema->SchemaCardinality, MainDatabase->GetNumberOfPrincipalComponents(), MainDatabase->PoseSearchMode);
|
|
if (ComputationInfo != Eigen::Success)
|
|
{
|
|
FString Reason;
|
|
switch (ComputationInfo)
|
|
{
|
|
case Eigen::NumericalIssue: Reason = "Numerical Issues"; break;
|
|
case Eigen::NoConvergence: Reason = "No Convergence"; break;
|
|
case Eigen::InvalidInput: Reason = "Invalid Input"; break;
|
|
default: Reason = "Unknown Reasons"; break;
|
|
}
|
|
UE_LOG(LogPoseSearch, Error, TEXT("%s - %s BuildIndex Failed because of '%s' while calculating PCA data. Try with a different dataset or change the database 'Pose Search Mode'"), *LexToString(FullIndexKey.Hash), *MainDatabaseName, *Reason);
|
|
|
|
ResetSearchIndex();
|
|
return;
|
|
}
|
|
|
|
if (Owner.IsCanceled())
|
|
{
|
|
UE_LOG(LogPoseSearch, Log, TEXT("%s - %s BuildIndex Cancelled"), *LexToString(FullIndexKey.Hash), *MainDatabaseName);
|
|
ResetSearchIndex();
|
|
return;
|
|
}
|
|
|
|
#if ENABLE_ANIM_DEBUG
|
|
// testing PruneDuplicatePCAValues determinism
|
|
if (AnyTestFlags(EMotionMatchTestFlags::TestPruneDuplicatePCAValuesDeterminism))
|
|
{
|
|
const int32 NumIterations = GVarMotionMatchTestNumIterations;
|
|
|
|
FSearchIndex TestSearchIndexA = SearchIndex;
|
|
TestSearchIndexA.PruneDuplicatePCAValues(MainDatabase->PCAValuesPruningSimilarityThreshold, MainDatabase->GetNumberOfPrincipalComponents());
|
|
for (int32 Iteration = 0; Iteration < NumIterations; ++Iteration)
|
|
{
|
|
FSearchIndex TestSearchIndexB = SearchIndex;
|
|
TestSearchIndexB.PruneDuplicatePCAValues(MainDatabase->PCAValuesPruningSimilarityThreshold, MainDatabase->GetNumberOfPrincipalComponents());
|
|
|
|
if (TestSearchIndexA.PCAValues != TestSearchIndexB.PCAValues)
|
|
{
|
|
UE_LOG(LogPoseSearch, Warning, TEXT("OnGetComplete - PruneDuplicatePCAValues is not deterministic"));
|
|
}
|
|
|
|
if (TestSearchIndexA.PCAValuesVectorToPoseIndexes != TestSearchIndexB.PCAValuesVectorToPoseIndexes)
|
|
{
|
|
UE_LOG(LogPoseSearch, Warning, TEXT("OnGetComplete - PruneDuplicatePCAValues PCAValuesVectorToPoseIndexes generation is not deterministic"));
|
|
}
|
|
}
|
|
}
|
|
#endif // ENABLE_ANIM_DEBUG
|
|
|
|
SearchIndex.PruneDuplicatePCAValues(MainDatabase->PCAValuesPruningSimilarityThreshold, MainDatabase->GetNumberOfPrincipalComponents());
|
|
if (Owner.IsCanceled())
|
|
{
|
|
UE_LOG(LogPoseSearch, Log, TEXT("%s - %s BuildIndex Cancelled"), *LexToString(FullIndexKey.Hash), *MainDatabaseName);
|
|
ResetSearchIndex();
|
|
return;
|
|
}
|
|
|
|
SearchIndex.PrunePCAValuesFromBlockTransitionPoses(MainDatabase->GetNumberOfPrincipalComponents());
|
|
|
|
PreprocessSearchIndexKDTree(SearchIndex, MainDatabase);
|
|
if (Owner.IsCanceled())
|
|
{
|
|
UE_LOG(LogPoseSearch, Log, TEXT("%s - %s BuildIndex Cancelled"), *LexToString(FullIndexKey.Hash), *MainDatabaseName);
|
|
ResetSearchIndex();
|
|
return;
|
|
}
|
|
|
|
// removing SearchIndex.Values and relying on FSearchIndex::GetReconstructedPoseValues to reconstruct the Values data from the PCAValues
|
|
if (MainDatabase->PoseSearchMode == EPoseSearchMode::PCAKDTree && MainDatabase->KDTreeQueryNumNeighbors <= 1)
|
|
{
|
|
SearchIndex.ResetValues();
|
|
}
|
|
|
|
const int32 RandomSeed = GetTypeHash(FullIndexKey.Hash);
|
|
PreprocessSearchIndexVPTree(SearchIndex, MainDatabase, RandomSeed);
|
|
if (Owner.IsCanceled())
|
|
{
|
|
UE_LOG(LogPoseSearch, Log, TEXT("%s - %s BuildIndex Cancelled"), *LexToString(FullIndexKey.Hash), *MainDatabaseName);
|
|
ResetSearchIndex();
|
|
return;
|
|
}
|
|
|
|
UE_LOG(LogPoseSearch, Log, TEXT("%s - %s BuildIndex Succeeded"), *LexToString(FullIndexKey.Hash), *MainDatabaseName);
|
|
|
|
#if ENABLE_ANIM_DEBUG
|
|
if (bCompareSearchIndex && SearchIndexCompare != SearchIndex)
|
|
{
|
|
FStringBuilderBase Message;
|
|
CompareSearchIndex(SearchIndexCompare, SearchIndex, MainDatabaseSchema, Message);
|
|
UE_LOG(LogPoseSearch, Warning, TEXT("%s - %s BuildIndex mismatch with DDC Index\n%s"), *LexToString(FullIndexKey.Hash), *MainDatabaseName, *Message);
|
|
}
|
|
#endif // ENABLE_ANIM_DEBUG
|
|
|
|
// putting SearchIndex to DDC
|
|
TArray<uint8> RawBytes;
|
|
// reserving 20k as initial buffer to serialize the SearchIndex to avoid multiple reallocations
|
|
RawBytes.Reserve(20 * 1024);
|
|
FMemoryWriter Writer(RawBytes);
|
|
Writer << SearchIndex;
|
|
FSharedBuffer RawData = MakeSharedBufferFromArray(MoveTemp(RawBytes));
|
|
const int32 BytesProcessed = RawData.GetSize();
|
|
|
|
FCacheRecordBuilder Builder(FullIndexKey);
|
|
Builder.AddValue(Id, RawData);
|
|
|
|
GetCache().Put({ { { MainDatabase->GetPathName() }, Builder.Build() } }, Owner, [this, MainDatabaseSchema, MainDatabaseName, FullIndexKey](FCachePutResponse&& Response)
|
|
{
|
|
switch (Response.Status)
|
|
{
|
|
case EStatus::Error:
|
|
UE_LOG(LogPoseSearch, Log, TEXT("%s - %s Failed to store DDC"), *LexToString(FullIndexKey.Hash), *MainDatabaseName);
|
|
break;
|
|
case EStatus::Canceled:
|
|
UE_LOG(LogPoseSearch, Log, TEXT("%s - %s Canceled to store DDC"), *LexToString(FullIndexKey.Hash), *MainDatabaseName);
|
|
break;
|
|
case EStatus::Ok:
|
|
UE_LOG(LogPoseSearch, Log, TEXT("%s - %s BuildIndex stored to DDC"), *LexToString(FullIndexKey.Hash), *MainDatabaseName);
|
|
|
|
#if ENABLE_ANIM_DEBUG
|
|
if (AnyTestFlags(EMotionMatchTestFlags::ValidateDDC))
|
|
{
|
|
const FCacheKey CacheKey{ Bucket, DerivedDataKey };
|
|
const FCacheGetRequest CacheRequest = { { Database->GetPathName() }, CacheKey, ECachePolicy::Default };
|
|
GetCache().Get(MakeArrayView(&CacheRequest, 1), Owner, [this, MainDatabaseSchema, MainDatabaseName](FCacheGetResponse&& Response)
|
|
{
|
|
const FCacheKey& FullIndexKey = Response.Record.GetKey();
|
|
check(FullIndexKey.Hash == DerivedDataKey);
|
|
|
|
if (Response.Status == EStatus::Ok)
|
|
{
|
|
FSharedBuffer RawData = Response.Record.GetValue(Id).GetData().Decompress();
|
|
FMemoryReaderView Reader(RawData);
|
|
|
|
FSearchIndex TestSearchIndex;
|
|
Reader << TestSearchIndex;
|
|
|
|
if (TestSearchIndex != SearchIndex)
|
|
{
|
|
FStringBuilderBase Message;
|
|
CompareSearchIndex(TestSearchIndex, SearchIndex, MainDatabaseSchema, Message);
|
|
UE_LOG(LogPoseSearch, Warning, TEXT("%s - %s DDC Index mismatch with BuildIndex\n%s"), *LexToString(FullIndexKey.Hash), *MainDatabaseName, *Message);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
#endif // ENABLE_ANIM_DEBUG
|
|
}
|
|
});
|
|
|
|
COOK_STAT(Timer.AddMiss(BytesProcessed));
|
|
});
|
|
}
|
|
}
|
|
|
|
void FPoseSearchDatabaseAsyncCacheTask::AddReferencedObjects(FReferenceCollector& Collector)
|
|
{
|
|
const EState State = GetState();
|
|
if (State != EState::Ended && State != EState::Failed)
|
|
{
|
|
if (Database.IsValid())
|
|
{
|
|
// keeping around the assets for starting or in progress tasks
|
|
Collector.AddReferencedObject(Database);
|
|
}
|
|
|
|
for (TWeakObjectPtr<const UObject>& Dependency : DatabaseDependencies)
|
|
{
|
|
if (Dependency.IsValid())
|
|
{
|
|
Collector.AddReferencedObject(Dependency);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// FAsyncPoseSearchDatabasesManagement
|
|
FCriticalSection FAsyncPoseSearchDatabasesManagement::Mutex;
|
|
|
|
FAsyncPoseSearchDatabasesManagement& FAsyncPoseSearchDatabasesManagement::Get()
|
|
{
|
|
FScopeLock Lock(&Mutex);
|
|
|
|
static FAsyncPoseSearchDatabasesManagement SingletonInstance;
|
|
return SingletonInstance;
|
|
}
|
|
|
|
FAsyncPoseSearchDatabasesManagement::FAsyncPoseSearchDatabasesManagement()
|
|
: Tasks(*(new FPoseSearchDatabaseAsyncCacheTasks()))
|
|
{
|
|
FScopeLock Lock(&Mutex);
|
|
|
|
OnObjectModifiedHandle = FCoreUObjectDelegates::OnObjectModified.AddRaw(this, &FAsyncPoseSearchDatabasesManagement::OnObjectModified);
|
|
OnObjectTransactedHandle = FCoreUObjectDelegates::OnObjectTransacted.AddRaw(this, &FAsyncPoseSearchDatabasesManagement::OnObjectTransacted);
|
|
OnPackageReloadedHandle = FCoreUObjectDelegates::OnPackageReloaded.AddRaw(this, &FAsyncPoseSearchDatabasesManagement::OnPackageReloaded);
|
|
OnPreObjectPropertyChangedHandle = FCoreUObjectDelegates::OnPreObjectPropertyChanged.AddRaw(this, &FAsyncPoseSearchDatabasesManagement::OnPreObjectPropertyChanged);
|
|
OnObjectPropertyChangedHandle = FCoreUObjectDelegates::OnObjectPropertyChanged.AddRaw(this, &FAsyncPoseSearchDatabasesManagement::OnObjectPropertyChanged);
|
|
|
|
FCoreDelegates::OnPreExit.AddRaw(this, &FAsyncPoseSearchDatabasesManagement::Shutdown);
|
|
}
|
|
|
|
FAsyncPoseSearchDatabasesManagement::~FAsyncPoseSearchDatabasesManagement()
|
|
{
|
|
FScopeLock Lock(&Mutex);
|
|
|
|
FCoreDelegates::OnPreExit.RemoveAll(this);
|
|
Shutdown();
|
|
|
|
delete &Tasks;
|
|
}
|
|
|
|
// given Object it figures out a map of databases to UAnimSequenceBase(s) containing UAnimNotifyState_PoseSearchBranchIn
|
|
void FAsyncPoseSearchDatabasesManagement::CollectDatabasesToSynchronize(UObject* Object)
|
|
{
|
|
FScopeLock Lock(&Mutex);
|
|
|
|
if (UAnimSequenceBase* SequenceBase = Cast<UAnimSequenceBase>(Object))
|
|
{
|
|
for (const FAnimNotifyEvent& NotifyEvent : SequenceBase->Notifies)
|
|
{
|
|
if (const UAnimNotifyState_PoseSearchBranchIn* BranchIn = Cast<UAnimNotifyState_PoseSearchBranchIn>(NotifyEvent.NotifyStateClass))
|
|
{
|
|
if (BranchIn->Database)
|
|
{
|
|
DatabasesToSynchronize.FindOrAdd(BranchIn->Database).AddUnique(SequenceBase);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (UAnimNotifyState_PoseSearchBranchIn* BranchIn = Cast<UAnimNotifyState_PoseSearchBranchIn>(Object))
|
|
{
|
|
if (BranchIn->Database)
|
|
{
|
|
if (UAnimSequenceBase* OuterSequenceBase = Cast<UAnimSequenceBase>(BranchIn->GetOuter()))
|
|
{
|
|
DatabasesToSynchronize.FindOrAdd(BranchIn->Database).AddUnique(OuterSequenceBase);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void FAsyncPoseSearchDatabasesManagement::SynchronizeDatabases()
|
|
{
|
|
FScopeLock Lock(&Mutex);
|
|
|
|
if (!DatabasesToSynchronize.IsEmpty())
|
|
{
|
|
// copying DatabasesToSynchronize because modifying the database will call OnObjectModified that could populate DatabasesToSynchronize again
|
|
const TDatabasesToSynchronize DatabasesToSynchronizeCopy = DatabasesToSynchronize;
|
|
DatabasesToSynchronize.Reset();
|
|
|
|
TArray<UAnimSequenceBase*, TInlineAllocator<256>> SequencesBase;
|
|
for (const TDatabasesToSynchronizePair& Pair : DatabasesToSynchronizeCopy)
|
|
{
|
|
if (Pair.Key.IsValid())
|
|
{
|
|
SequencesBase.Reset();
|
|
for (const TWeakObjectPtr<UAnimSequenceBase>& SequenceBase : Pair.Value)
|
|
{
|
|
if (SequenceBase.IsValid())
|
|
{
|
|
SequencesBase.Add(SequenceBase.Get());
|
|
}
|
|
}
|
|
|
|
Pair.Key->SynchronizeWithExternalDependencies(SequencesBase);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// we're listening to OnObjectModified to cancel any pending Task indexing databases depending from Object to avoid multi threading issues
|
|
void FAsyncPoseSearchDatabasesManagement::OnObjectModified(UObject* Object)
|
|
{
|
|
PreModified(Object);
|
|
}
|
|
|
|
void FAsyncPoseSearchDatabasesManagement::ClearPreCancelled()
|
|
{
|
|
// iterating backwards because of the possible RemoveAtSwap
|
|
for (int32 TaskIndex = Tasks.Num() - 1; TaskIndex >= 0; --TaskIndex)
|
|
{
|
|
if (Tasks[TaskIndex]->GetState() == FPoseSearchDatabaseAsyncCacheTask::EState::PreCancelled)
|
|
{
|
|
Tasks[TaskIndex]->Cancel();
|
|
}
|
|
}
|
|
}
|
|
|
|
void FAsyncPoseSearchDatabasesManagement::PreModified(UObject* Object)
|
|
{
|
|
check(IsInGameThread());
|
|
|
|
FScopeLock Lock(&Mutex);
|
|
|
|
PartialKeyHashes.Remove(Object);
|
|
|
|
// iterating backwards because of the possible RemoveAtSwap
|
|
for (int32 TaskIndex = Tasks.Num() - 1; TaskIndex >= 0; --TaskIndex)
|
|
{
|
|
Tasks[TaskIndex]->PreCancelIfDependsOn(Object);
|
|
}
|
|
|
|
// collecting databases to synchronize prior modifying the Object
|
|
CollectDatabasesToSynchronize(Object);
|
|
}
|
|
|
|
void FAsyncPoseSearchDatabasesManagement::PostModified(UObject* Object)
|
|
{
|
|
check(IsInGameThread());
|
|
|
|
FScopeLock Lock(&Mutex);
|
|
|
|
// collecting databases to synchronize, and merging the results with the PreModified collection
|
|
CollectDatabasesToSynchronize(Object);
|
|
|
|
SynchronizeDatabases();
|
|
|
|
SynchronizeDatabaseChooser(Object);
|
|
|
|
ClearPreCancelled();
|
|
}
|
|
|
|
void FAsyncPoseSearchDatabasesManagement::OnObjectTransacted(UObject* Object, const FTransactionObjectEvent& TransactionObjectEvent)
|
|
{
|
|
PostModified(Object);
|
|
}
|
|
|
|
void FAsyncPoseSearchDatabasesManagement::OnPackageReloaded(const EPackageReloadPhase InPackageReloadPhase, FPackageReloadedEvent* InPackageReloadedEvent)
|
|
{
|
|
check(IsInGameThread());
|
|
|
|
if (InPackageReloadPhase == EPackageReloadPhase::PostPackageFixup && InPackageReloadedEvent)
|
|
{
|
|
FScopeLock Lock(&Mutex);
|
|
|
|
// @todo: figure out why we don't find the correct dependency into InPackageReloadedEvent->GetRepointedObjects()
|
|
// for now we invalidate all the DDC cache to be on the safe side
|
|
//for (const TPair<UObject*, UObject*>& Pair : InPackageReloadedEvent->GetRepointedObjects())
|
|
//{
|
|
// OnObjectModified(Pair.Key);
|
|
//}
|
|
|
|
for (TUniquePtr<FPoseSearchDatabaseAsyncCacheTask>& TaskPtr : Tasks)
|
|
{
|
|
UE_LOG(LogPoseSearch, Log, TEXT("%s - %s Cancelled because of OnPackageReloaded"), *LexToString(TaskPtr->GetDerivedDataKey()), *TaskPtr->GetDatabase()->GetName());
|
|
}
|
|
Tasks.Reset();
|
|
}
|
|
}
|
|
|
|
void FAsyncPoseSearchDatabasesManagement::OnPreObjectPropertyChanged(UObject* InObject, const FEditPropertyChain& InPropertyChain)
|
|
{
|
|
PreModified(InObject);
|
|
}
|
|
|
|
void FAsyncPoseSearchDatabasesManagement::OnObjectPropertyChanged(UObject* InObject, FPropertyChangedEvent& InPropertyChangedEvent)
|
|
{
|
|
PostModified(InObject);
|
|
}
|
|
|
|
void FAsyncPoseSearchDatabasesManagement::Shutdown()
|
|
{
|
|
FScopeLock Lock(&Mutex);
|
|
|
|
Tasks.Reset();
|
|
|
|
FCoreUObjectDelegates::OnObjectModified.Remove(OnObjectModifiedHandle);
|
|
OnObjectModifiedHandle.Reset();
|
|
|
|
FCoreUObjectDelegates::OnObjectTransacted.Remove(OnObjectTransactedHandle);
|
|
OnObjectTransactedHandle.Reset();
|
|
|
|
FCoreUObjectDelegates::OnPackageReloaded.Remove(OnPackageReloadedHandle);
|
|
OnPackageReloadedHandle.Reset();
|
|
|
|
FCoreUObjectDelegates::OnPreObjectPropertyChanged.Remove(OnPreObjectPropertyChangedHandle);
|
|
OnPreObjectPropertyChangedHandle.Reset();
|
|
|
|
FCoreUObjectDelegates::OnObjectPropertyChanged.Remove(OnObjectPropertyChangedHandle);
|
|
OnObjectPropertyChangedHandle.Reset();
|
|
}
|
|
|
|
void FAsyncPoseSearchDatabasesManagement::Tick(float DeltaTime)
|
|
{
|
|
FScopeLock Lock(&Mutex);
|
|
|
|
check(IsInGameThread());
|
|
|
|
#if ENABLE_ANIM_DEBUG
|
|
// testing sampler determinism
|
|
const bool bTestAssetSamplerDeterminism = AnyTestFlags(EMotionMatchTestFlags::TestAssetSamplerDeterminism);
|
|
const bool bTestAssetSamplerDeterminismFromPreviousExecution = AnyTestFlags(EMotionMatchTestFlags::TestAssetSamplerDeterminismFromPreviousExecution);
|
|
if (bTestAssetSamplerDeterminism || bTestAssetSamplerDeterminismFromPreviousExecution)
|
|
{
|
|
const int32 NumIterations = GVarMotionMatchTestNumIterations;
|
|
|
|
struct FTestSample
|
|
{
|
|
FBoneContainer BoneContainer;
|
|
const UPoseSearchDatabase* Database = nullptr;
|
|
int32 AnimationAssetIndex = INDEX_NONE;
|
|
const UAnimationAsset* AnimationAsset = nullptr;
|
|
FTransform RootTransformOrigin = FTransform::Identity;
|
|
FVector BlendParameters = FVector::ZeroVector;
|
|
int32 SampleIndex = INDEX_NONE;
|
|
float SampleNormalizedTime = 0.f;
|
|
|
|
TAlignedArray<uint8> SerializedData;
|
|
|
|
FString GetFileName() const
|
|
{
|
|
return FString::Printf(TEXT("%s//TestAssetSamplerDeterminism//%s_%d_%s_%d.bin"), *FPaths::EngineDir(), *GetNameSafe(Database), AnimationAssetIndex, *GetNameSafe(AnimationAsset), SampleIndex);
|
|
}
|
|
};
|
|
|
|
TArray<FTestSample> TestSamples;
|
|
TestSamples.Reserve(256);
|
|
for (const TUniquePtr<FPoseSearchDatabaseAsyncCacheTask>& TaskPtr : Tasks)
|
|
{
|
|
if (const UPoseSearchDatabase* Database = TaskPtr->GetDatabase())
|
|
{
|
|
if (Database->Schema)
|
|
{
|
|
TMap<FRole, FBoneContainer> RoledBoneContainers;
|
|
InitRoledBoneContainers(RoledBoneContainers, Database, Database->Schema);
|
|
|
|
for (int32 AnimationAssetIndex = 0; AnimationAssetIndex < Database->GetNumAnimationAssets(); ++AnimationAssetIndex)
|
|
{
|
|
if (const FPoseSearchDatabaseAnimationAssetBase* DatabaseAsset = Database->GetDatabaseAnimationAsset<FPoseSearchDatabaseAnimationAssetBase>(AnimationAssetIndex))
|
|
{
|
|
if (DatabaseAsset->IsEnabled())
|
|
{
|
|
DatabaseAsset->IterateOverSamplingParameter([Database, DatabaseAsset, AnimationAssetIndex, &TestSamples, &RoledBoneContainers](const FVector& BlendParameters)
|
|
{
|
|
const int32 NumRoles = DatabaseAsset->GetNumRoles();
|
|
for (int32 RoleIndex = 0; RoleIndex < NumRoles; ++RoleIndex)
|
|
{
|
|
const FRole& Role = DatabaseAsset->GetRole(RoleIndex);
|
|
if (const FBoneContainer* BoneContainer = RoledBoneContainers.Find(Role))
|
|
{
|
|
const UAnimationAsset* AnimationAsset = DatabaseAsset->GetAnimationAssetForRole(Role);
|
|
const FTransform RootTransformOrigin = DatabaseAsset->GetRootTransformOriginForRole(Role);
|
|
|
|
static int32 NUM_TESTING_SAMPLES_PER_DATABASE_ASSET = 100; // make sure it's greater than 1
|
|
|
|
for (int32 SampleIndex = 0; SampleIndex < NUM_TESTING_SAMPLES_PER_DATABASE_ASSET; SampleIndex++)
|
|
{
|
|
FTestSample& TestSample = TestSamples.AddDefaulted_GetRef();
|
|
TestSample.BoneContainer = *BoneContainer;
|
|
TestSample.Database = Database;
|
|
TestSample.AnimationAssetIndex = AnimationAssetIndex;
|
|
TestSample.AnimationAsset = AnimationAsset;
|
|
TestSample.RootTransformOrigin = RootTransformOrigin;
|
|
TestSample.BlendParameters = BlendParameters;
|
|
TestSample.SampleIndex = SampleIndex;
|
|
TestSample.SampleNormalizedTime = SampleIndex / (NUM_TESTING_SAMPLES_PER_DATABASE_ASSET - 1);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ParallelFor(TestSamples.Num(), [&TestSamples, NumIterations, bTestAssetSamplerDeterminism, bTestAssetSamplerDeterminismFromPreviousExecution](int32 TestSampleIndex)
|
|
{
|
|
FMemMark Mark(FMemStack::Get());
|
|
|
|
FTestSample& TestSample = TestSamples[TestSampleIndex];
|
|
const FAnimationAssetSampler AssetSampler(TestSample.AnimationAsset);
|
|
const float SampleTime = AssetSampler.GetPlayLength() * TestSample.SampleNormalizedTime;
|
|
|
|
FCompactPose Pose;
|
|
Pose.SetBoneContainer(&TestSample.BoneContainer);
|
|
AssetSampler.ExtractPose(SampleTime, Pose);
|
|
const FTransform RootTransform = AssetSampler.ExtractRootTransform(SampleTime);
|
|
|
|
const TConstArrayView<FTransform> Bones = Pose.GetBones();
|
|
|
|
if (bTestAssetSamplerDeterminism)
|
|
{
|
|
for (int32 IterationIndex = 0; IterationIndex < NumIterations; ++IterationIndex)
|
|
{
|
|
FCompactPose TestPose;
|
|
TestPose.SetBoneContainer(&TestSample.BoneContainer);
|
|
AssetSampler.ExtractPose(SampleTime, TestPose);
|
|
const FTransform TestRootTransform = AssetSampler.ExtractRootTransform(SampleTime);
|
|
|
|
if (FMemory::Memcmp(&RootTransform, &TestRootTransform, sizeof(FTransform)) != 0)
|
|
{
|
|
UE_LOG(LogPoseSearch, Error, TEXT("FAnimationAssetSampler - ExtractRootTransform is not deterministic"));
|
|
}
|
|
|
|
const TConstArrayView<FTransform> TestBones = TestPose.GetBones();
|
|
if (Bones.Num() != TestBones.Num())
|
|
{
|
|
UE_LOG(LogPoseSearch, Error, TEXT("FAnimationAssetSampler - ExtractPose is not deterministic"));
|
|
}
|
|
else
|
|
{
|
|
for (int32 BoneIndex = 0; BoneIndex < Bones.Num(); ++BoneIndex)
|
|
{
|
|
if (FMemory::Memcmp(&Bones[BoneIndex], &TestBones[BoneIndex], sizeof(FTransform)) != 0)
|
|
{
|
|
UE_LOG(LogPoseSearch, Error, TEXT("FAnimationAssetSampler - ExtractPose is not deterministic"));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bTestAssetSamplerDeterminismFromPreviousExecution)
|
|
{
|
|
const int32 RootTransformSize = sizeof(FTransform);
|
|
const int32 BonesSize = sizeof(FTransform) * Bones.Num();
|
|
|
|
TestSample.SerializedData.Reserve(RootTransformSize + BonesSize);
|
|
TestSample.SerializedData.Append(reinterpret_cast<const uint8*>(&RootTransform), RootTransformSize);
|
|
TestSample.SerializedData.Append(reinterpret_cast<const uint8*>(Bones.GetData()), BonesSize);
|
|
}
|
|
}, ParallelForFlags);
|
|
|
|
if (bTestAssetSamplerDeterminismFromPreviousExecution)
|
|
{
|
|
for (const FTestSample& TestSample : TestSamples)
|
|
{
|
|
FString FileName = TestSample.GetFileName();
|
|
TArray<uint8> LoadedData;
|
|
const bool bLoadedFile = FFileHelper::LoadFileToArray(LoadedData, *FileName, FILEREAD_Silent);
|
|
if (bLoadedFile)
|
|
{
|
|
if (LoadedData.Num() <= 0 || LoadedData.Num() % sizeof(FTransform) != 0)
|
|
{
|
|
UE_LOG(LogPoseSearch, Error, TEXT("FAnimationAssetSampler - Loaded the wrong amount of data!"));
|
|
}
|
|
else if (TestSample.SerializedData.Num() != LoadedData.Num())
|
|
{
|
|
UE_LOG(LogPoseSearch, Error, TEXT("FAnimationAssetSampler - Loaded data mismatch expected amount of data!"));
|
|
}
|
|
else if (FMemory::Memcmp(TestSample.SerializedData.GetData(), LoadedData.GetData(), TestSample.SerializedData.Num()) != 0)
|
|
{
|
|
const int32 NumTransforms = LoadedData.Num() / sizeof(FTransform);
|
|
|
|
// copying the data into an aligned buffer, so we can cast it to FTransform
|
|
TAlignedArray<uint8> LoadedDataAligned(LoadedData.GetData(), LoadedData.Num());
|
|
TArrayView<const FTransform> LoadedTransforms(reinterpret_cast<const FTransform*>(LoadedDataAligned.GetData()), NumTransforms);
|
|
TArrayView<const FTransform> SerializedTransforms(reinterpret_cast<const FTransform*>(TestSample.SerializedData.GetData()), NumTransforms);
|
|
|
|
for (int32 TransformIndex = 0; TransformIndex < NumTransforms; ++TransformIndex)
|
|
{
|
|
if (FMemory::Memcmp(&LoadedTransforms[TransformIndex], &SerializedTransforms[TransformIndex], sizeof(FTransform)) != 0)
|
|
{
|
|
// NoTe: TransformIndex == 0 for the root
|
|
// TransformIndex > 0 are the bones, where BoneIndex = TransformIndex - 1
|
|
const int32 BoneIndex = TransformIndex - 1;
|
|
UE_LOG(LogPoseSearch, Error, TEXT("FAnimationAssetSampler - ExtractPose is not deterministic for %s for Bone %d"), *FileName, BoneIndex);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
bool bSavedFile = FFileHelper::SaveArrayToFile(TestSample.SerializedData, *FileName);
|
|
if (!bSavedFile)
|
|
{
|
|
UE_LOG(LogPoseSearch, Error, TEXT("FAnimationAssetSampler - Failed to save comparison file!"));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (AnyTestFlags(EMotionMatchTestFlags::InvalidateCache))
|
|
{
|
|
if (AnyTestFlags(EMotionMatchTestFlags::WaitForTaskCompletion))
|
|
{
|
|
// iterating backwards because of the possible RemoveAtSwap
|
|
for (int32 TaskIndex = Tasks.Num() - 1; TaskIndex >= 0; --TaskIndex)
|
|
{
|
|
if (Tasks[TaskIndex]->GetState() == FPoseSearchDatabaseAsyncCacheTask::EState::Ended ||
|
|
Tasks[TaskIndex]->GetState() == FPoseSearchDatabaseAsyncCacheTask::EState::Failed)
|
|
{
|
|
UE_LOG(LogPoseSearch, Log, TEXT("%s - %s Removed because of InvalidateCache with WaitForTaskCompletion"), *LexToString(Tasks[TaskIndex]->GetDerivedDataKey()), *Tasks[TaskIndex]->GetDatabase()->GetName());
|
|
Tasks.RemoveAtSwap(TaskIndex, EAllowShrinking::No);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (TUniquePtr<FPoseSearchDatabaseAsyncCacheTask>& TaskPtr : Tasks)
|
|
{
|
|
UE_LOG(LogPoseSearch, Log, TEXT("%s - %s Cancelled because of InvalidateCache"), *LexToString(TaskPtr->GetDerivedDataKey()), *TaskPtr->GetDatabase()->GetName());
|
|
}
|
|
Tasks.Reset();
|
|
}
|
|
}
|
|
|
|
if (AnyTestFlags(EMotionMatchTestFlags::ValidateSynchronizeWithExternalDependenciesDeterminism))
|
|
{
|
|
for (int32 TaskIndex = 0; TaskIndex < Tasks.Num(); ++TaskIndex)
|
|
{
|
|
Tasks[TaskIndex]->TestSynchronizeWithExternalDependencies();
|
|
}
|
|
}
|
|
|
|
#endif // ENABLE_ANIM_DEBUG
|
|
|
|
const bool bReindexCancelledDatabases = GVarMotionMatchReindexCancelledDatabases;
|
|
|
|
// iterating backwards because of the possible RemoveAtSwap
|
|
for (int32 TaskIndex = Tasks.Num() - 1; TaskIndex >= 0; --TaskIndex)
|
|
{
|
|
if (!Tasks[TaskIndex]->IsValid())
|
|
{
|
|
Tasks.RemoveAtSwap(TaskIndex, EAllowShrinking::No);
|
|
}
|
|
else if (Tasks[TaskIndex]->GetState() == FPoseSearchDatabaseAsyncCacheTask::EState::Cancelled)
|
|
{
|
|
if (bReindexCancelledDatabases)
|
|
{
|
|
RequestAsyncBuildIndex(Tasks[TaskIndex]->GetDatabase(), ERequestAsyncBuildFlag::NewRequest);
|
|
Tasks.RemoveAtSwap(TaskIndex, EAllowShrinking::No);
|
|
}
|
|
else
|
|
{
|
|
Tasks.RemoveAtSwap(TaskIndex, EAllowShrinking::No);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Tasks[TaskIndex]->Update(Mutex, PartialKeyHashes);
|
|
}
|
|
}
|
|
|
|
#if ENABLE_ANIM_DEBUG
|
|
if (AnyTestFlags(EMotionMatchTestFlags::TestDDCKeyDeterminism))
|
|
{
|
|
const int32 NumIterations = GVarMotionMatchTestNumIterations;
|
|
for (int32 TaskIndex = 0; TaskIndex < Tasks.Num(); ++TaskIndex)
|
|
{
|
|
if (const UPoseSearchDatabase* Database = Tasks[TaskIndex]->GetDatabase())
|
|
{
|
|
const FKeyBuilder KeyBuilder(Database, false, false);
|
|
const FIoHash IoHash = KeyBuilder.Finalize();
|
|
|
|
for (int32 IterationIndex = 0; IterationIndex < NumIterations; ++IterationIndex)
|
|
{
|
|
const FKeyBuilder TestKeyBuilder(Database, false, false, &PartialKeyHashes, FKeyBuilder::EDebugPartialKeyHashesMode::Use);
|
|
const FIoHash TestIoHash = TestKeyBuilder.Finalize();
|
|
|
|
if (!KeyBuilder.ValidateAgainst(TestKeyBuilder))
|
|
{
|
|
UE_LOG(LogPoseSearch, Error, TEXT("FKeyBuilder - key generation is not deterministic: %s / %s for asset %s"), *LexToString(IoHash), *LexToString(TestIoHash), *Database->GetName());
|
|
}
|
|
|
|
if (IoHash != TestIoHash)
|
|
{
|
|
UE_LOG(LogPoseSearch, Error, TEXT("FKeyBuilder - key generation is not deterministic: %s / %s for asset %s"), *LexToString(IoHash), *LexToString(TestIoHash), *Database->GetName());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif // ENABLE_ANIM_DEBUG
|
|
}
|
|
|
|
void FAsyncPoseSearchDatabasesManagement::TickCook(float DeltaTime, bool bCookCompete)
|
|
{
|
|
Tick(DeltaTime);
|
|
}
|
|
|
|
TStatId FAsyncPoseSearchDatabasesManagement::GetStatId() const
|
|
{
|
|
RETURN_QUICK_DECLARE_CYCLE_STAT(FAsyncPoseSearchDatabasesManagement, STATGROUP_Tickables);
|
|
}
|
|
|
|
void FAsyncPoseSearchDatabasesManagement::AddReferencedObjects(FReferenceCollector& Collector)
|
|
{
|
|
FScopeLock Lock(&Mutex);
|
|
|
|
for (TUniquePtr<FPoseSearchDatabaseAsyncCacheTask>& TaskPtr : Tasks)
|
|
{
|
|
TaskPtr->AddReferencedObjects(Collector);
|
|
}
|
|
}
|
|
|
|
EAsyncBuildIndexResult FAsyncPoseSearchDatabasesManagement::RequestAsyncBuildIndexInternal(const UPoseSearchDatabase* Database, ERequestAsyncBuildFlag Flag)
|
|
{
|
|
if (!IsValid(Database))
|
|
{
|
|
return EAsyncBuildIndexResult::Failed;
|
|
}
|
|
|
|
if (Database->GetPackage()->HasAnyPackageFlags(PKG_Cooked))
|
|
{
|
|
// Don't cache for cooked packages
|
|
return EAsyncBuildIndexResult::Success;
|
|
}
|
|
|
|
FScopeLock Lock(&Mutex);
|
|
|
|
check(EnumHasAnyFlags(Flag, ERequestAsyncBuildFlag::NewRequest | ERequestAsyncBuildFlag::ContinueRequest));
|
|
|
|
FAsyncPoseSearchDatabasesManagement& This = FAsyncPoseSearchDatabasesManagement::Get();
|
|
|
|
const bool bWaitForCompletion = EnumHasAnyFlags(Flag, ERequestAsyncBuildFlag::WaitForCompletion);
|
|
|
|
FPoseSearchDatabaseAsyncCacheTask* Task = nullptr;
|
|
for (TUniquePtr<FPoseSearchDatabaseAsyncCacheTask>& TaskPtr : This.Tasks)
|
|
{
|
|
if (TaskPtr->GetDatabase() == Database)
|
|
{
|
|
Task = TaskPtr.Get();
|
|
|
|
if (EnumHasAnyFlags(Flag, ERequestAsyncBuildFlag::NewRequest))
|
|
{
|
|
if (Task->GetState() == FPoseSearchDatabaseAsyncCacheTask::EState::Prestarted)
|
|
{
|
|
Task->Cancel();
|
|
}
|
|
Task->StartNewRequestIfNeeded(bWaitForCompletion, This.PartialKeyHashes);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!Task)
|
|
{
|
|
// we didn't find the Task, so we Emplace a new one
|
|
This.Tasks.Emplace(MakeUnique<FPoseSearchDatabaseAsyncCacheTask>(const_cast<UPoseSearchDatabase*>(Database), bWaitForCompletion, This.PartialKeyHashes));
|
|
Task = This.Tasks.Last().Get();
|
|
}
|
|
|
|
if (bWaitForCompletion)
|
|
{
|
|
check(Task->GetState() != FPoseSearchDatabaseAsyncCacheTask::EState::Notstarted);
|
|
if (Task->GetState() == FPoseSearchDatabaseAsyncCacheTask::EState::Prestarted)
|
|
{
|
|
Task->Wait(Mutex);
|
|
}
|
|
}
|
|
|
|
if (Task->GetState() == FPoseSearchDatabaseAsyncCacheTask::EState::Ended)
|
|
{
|
|
return EAsyncBuildIndexResult::Success;
|
|
}
|
|
|
|
if (Task->GetState() == FPoseSearchDatabaseAsyncCacheTask::EState::Failed)
|
|
{
|
|
return EAsyncBuildIndexResult::Failed;
|
|
}
|
|
|
|
return EAsyncBuildIndexResult::InProgress;
|
|
}
|
|
|
|
EAsyncBuildIndexResult FAsyncPoseSearchDatabasesManagement::RequestAsyncBuildIndex(const UPoseSearchDatabase* Database, ERequestAsyncBuildFlag Flag)
|
|
{
|
|
if (GVarMotionMatchReindexAllReferencedDatabases)
|
|
{
|
|
FDatabaseSet DatabaseSet;
|
|
RecursivePopulateDependentDatabases(Database, DatabaseSet);
|
|
|
|
for (FDatabaseSet::TConstIterator Iter = DatabaseSet.CreateConstIterator(); Iter; ++Iter)
|
|
{
|
|
const UPoseSearchDatabase* DependentDatabase = *Iter;
|
|
if (DependentDatabase != Database)
|
|
{
|
|
RequestAsyncBuildIndexInternal(DependentDatabase, ERequestAsyncBuildFlag::ContinueRequest);
|
|
}
|
|
}
|
|
}
|
|
|
|
return RequestAsyncBuildIndexInternal(Database, Flag);
|
|
}
|
|
|
|
} // namespace UE::PoseSearch
|
|
|
|
#undef LOCTEXT_NAMESPACE
|
|
|
|
#endif // WITH_EDITOR
|