Logic error in FCompressedAnimSequence::IsBoneDataValid

Hi,

After updating to Unreal 5.6, we had problems with root motion extraction at cook time. I found Jack Potter’s CL 44083221 but I was still facing an issue. My investigation led me to FCompressedAnimSequence::IsBoneDataValid. Even though the compressed data is invalid (default value), FCompressedAnimSequence::IsBoneDataValid returns true due to the condition “CompressedTrackToSkeletonMapTable.Num() == 0”.

My animation has bone data and no curve data. This combo led Jack Potter’s fix to extract the root motion from the invalid compressed data. Changing the condition to “CompressedTrackToSkeletonMapTable.Num() != 0” fixed it.

bool FCompressedAnimSequence::IsBoneDataValid(const UAnimSequence* AnimSequence, bool bLogInformation) const { #if WITH_EDITOR const bool bHasBoneTracks = !AnimSequence->GetOutermost()->HasAnyPackageFlags(PKG_Cooked) && AnimSequence->GetDataModelInterface()->GetNumBoneTracks() != 0; const bool bHasValidCompressedBoneData = CompressedDataStructure != nullptr || CompressedTrackToSkeletonMapTable.Num() != 0; const bool bValidCompressedData = !bHasBoneTracks || bHasValidCompressedBoneData; if(bLogInformation && !bValidCompressedData) { UE_LOG(LogAnimation, Warning, TEXT("%s: Num Bone Tracks: %i\nNum Curves: %i\nCompressed bone data: %i (%i)\nCompressed curve data %i\nValid additive: %i"), *AnimSequence->GetName(), AnimSequence->GetDataModelInterface()->GetNumBoneTracks(), AnimSequence->GetDataModelInterface()->GetNumberOfFloatCurves(), CompressedDataStructure != nullptr ? 1 : 0, CompressedTrackToSkeletonMapTable.Num(), CompressedCurveByteStream.Num() != 0 ? 1 : 0, AnimSequence->IsValidAdditive() ? 1 : 0); } return bValidCompressedData; #else return true; #endif }

Hey there,

This function’s name might be a little poorly named, but it is valid to have the compressed track to skeleton map table to be empty or not. This is intended to infer whether the compressed data is usable yet. I think to debug the issue thoroughly, we would need to see a call stack of what you are seeing at cook time when extracting root motion data to figure out the source of the issue.

Dustin

Hi,

The more I think about it, the more I’m wondering if the check is still required. It was added in CL 24007124.

In the meantime, we will be running the engine without it.

const bool bHasValidCompressedBoneData = CompressedDataStructure != nullptr;

edit: I missed your message when I wrote mine. I’ll get you more info.

Hi Dustin,

Here is the callstack.

`UnrealEditor-Engine.dll!UAnimSequence::ExtractRootTrackTransform_Lockless::__l2::<lambda_1>::operator()() Line 2218 C++
UnrealEditor-Engine.dll!UAnimSequence::ExtractRootTrackTransform_Lockless(const FAnimExtractContext & ExtractionContext, const FBoneContainer * RequiredBones) Line 2275 C++
UnrealEditor-Engine.dll!UAnimSequence::ExtractRootMotionFromRange(double StartTime, double EndTime, const FAnimExtractContext & ExtractionContext) Line 1608 C++
UnrealEditor-Engine.dll!UAnimSequence::ExtractRootMotion(const FAnimExtractContext & ExtractionContext) Line 1569 C++

*** Custom code calling eventually UAnimSequence::ExtractRootMotion ***

UnrealEditor-EDITORCUSTOMMODULE-Win64-DebugGame.dll!EDITORCUSTOMMODULE::OnObjectSaved(UObject * ObjectSaved, FObjectPreSaveContext Context) Line 528 C++
[External Code]
[Inline Frame] UnrealEditor-CoreUObject.dll!TMulticastDelegateBase::Broadcast(UObject *) Line 258 C++
[Inline Frame] UnrealEditor-CoreUObject.dll!TMulticastDelegate<void __cdecl(UObject *,FObjectPreSaveContext),FDefaultDelegateUserPolicy>::Broadcast(UObject *) Line 1080 C++
UnrealEditor-CoreUObject.dll!UObject::PreSave(FObjectPreSaveContext SaveContext) Line 1488 C++
UnrealEditor-CUSTOMMODULE-Win64-DebugGame.dll!CUSTOMDATAASSET::PreSave(FObjectPreSaveContext ObjectSaveContext) Line 301 C++
UnrealEditor-CoreUObject.dll!UE::SavePackageUtilities::CallPreSave(UObject * Object, FObjectSaveContextData & ObjectSaveContext) Line 682 C++
UnrealEditor-CoreUObject.dll!anonymous namespace'::RoutePresave(FSaveContext & SaveContext) Line 340 C++ UnrealEditor-CoreUObject.dll!UPackage::Save2(UPackage * InPackage, UObject * InAsset, const wchar_t * InFilename, const FSavePackageArgs & SaveArgs) Line 3791 C++ UnrealEditor-CoreUObject.dll!UPackage::Save(UPackage * InOuter, UObject * InAsset, const wchar_t * Filename, const FSavePackageArgs & SaveArgs) Line 20 C++ UnrealEditor-UnrealEd.dll!UEditorEngine::Save(UPackage * InOuter, UObject * InAsset, const wchar_t * Filename, const FSavePackageArgs & InSaveArgs) Line 4525 C++ UnrealEditor-UnrealEd.dll!UCookOnTheFlyServer::SaveCookedPackage(UE::Cook::FSaveCookedPackageContext & Context) Line 126 C++ UnrealEditor-UnrealEd.dll!UCookOnTheFlyServer::PumpRuntimeSaves(UE::Cook::FTickStackData & StackData, unsigned int DesiredQueueLength, int & OutNumPushed, bool & bOutBusy) Line 5163 C++ [Inline Frame] UnrealEditor-UnrealEd.dll!IsEngineExitRequested() Line 404 C++ UnrealEditor-UnrealEd.dll!UCookOnTheFlyServer::TickMainCookLoop(UE::Cook::FTickStackData & StackData) Line 1632 C++ UnrealEditor-UnrealEd.dll!UCookOnTheFlyServer::TickCookByTheBook(const float TimeSlice, ECookTickFlags TickFlags) Line 1512 C++ UnrealEditor-UnrealEd.dll!UCookCommandlet::RunCookByTheBookCook(UCookOnTheFlyServer * CookOnTheFlyServer, void * StartupOptionsAsVoid, ECookByTheBookOptions CookOptions) Line 598 C++ UnrealEditor-UnrealEd.dll!UCookCommandlet::CookByTheBook(const TArray<ITargetPlatform *,TSizedDefaultAllocator<32>> & Platforms) Line 550 C++ UnrealEditor-UnrealEd.dll!UCookCommandlet::Main(const FString & CmdLineParams) Line 269 C++ UnrealEditor-Win64-DebugGame.exe!FEngineLoop::PreInitPostStartupScreen(const wchar_t * CmdLine) Line 3904 C++ [Inline Frame] UnrealEditor-Win64-DebugGame.exe!FEngineLoop::PreInit(const wchar_t *) Line 4203 C++ [Inline Frame] UnrealEditor-Win64-DebugGame.exe!EnginePreInit(const wchar_t *) Line 40 C++ UnrealEditor-Win64-DebugGame.exe!GuardedMain(const wchar_t * CmdLine) Line 143 C++ UnrealEditor-Win64-DebugGame.exe!GuardedMainWrapper(const wchar_t * CmdLine) Line 128 C++ UnrealEditor-Win64-DebugGame.exe!LaunchWindowsStartup(HINSTANCE__ * hInInstance, HINSTANCE__ * hPrevInstance, char * __formal, int nCmdShow, const wchar_t * CmdLine) Line 282 C++ UnrealEditor-Win64-DebugGame.exe!WinMain(HINSTANCE__ * hInInstance, HINSTANCE__ * hPrevInstance, char * pCmdLine, int nCmdShow) Line 339 C++ [External Code] We have a custom DataAsset that contains AnimSequences on which we are calling ExtractRootMotion.

Since 5.6, I noticed PlatformCompressedData in UAnimSequence::ExtractRootTrackTransform_Lockless is empty but bValidCompressedData is set to true because of the check “CompressedTrackToSkeletonMapTable.Num() == 0” in FCompressedAnimSequence::IsBoneDataValid. This happens because my animation has bone tracks but no curve data. If my animation had curve data, bValidCompressedData would be set to false because the logic in FCompressedAnimSequence::IsCurveDataValid is correct.

UAnimSequence::ExtractRootTrackTransform_Lockless ends up returning the root bone from the default skeleton instead of the root bone from the raw data.

[Image Removed]

We have currently removed “CompressedTrackToSkeletonMapTable.Num() == 0” in FCompressedAnimSequence::IsBoneDataValid to temporarily fix this issue.

Let me know what do you think about it.

Thank you

Stéphane

Apologies for the delay, when you debug this what is the return value of UAnimSequence::GetPlatformCompressedData inside UAnimSequence::ExtractRootTrackTransform_Lockless? Depending on the result of that, we’ll have better info on the best course of action.

Dustin

You can see what UAnimSequence::GetPlatformCompressedData returns in my VisualStudio screenshot. It is the variable PlatformCompressedData.

So there are two potential next steps here.

You mentioned getting Jack’s change, if you implemented that changlist, would you be able to send along which section and line you are running into issues on? The reason we ask is that Jacks change would take you into evaluating raw data, where access compressed data doesn’t happen at all and there could be more work to do there.

If you didn’t implement Jack’s change or do want to operate on condenced data then you would need to implement:

UAnimSequence::WaitOnExistingCompression() //use the deffault true.in your presave code, before you attempt to do the extraction. The primary concern here is that it could be a time sync, but assuming you’re using a DDC, it could be reasonable for a build process.

Dustin

Hi Dustin,

We have implemented Jack’s change. Here is how it goes.

1) GetPlatformCompressedData returns an empty struct

[Image Removed]

2) Jack’s CL checks if the compressed data is valid. Both IsBoneDataValid and IsCurveDataValid returns true. IsBoneDataValid is because of the check CompressedTrackToSkeletonMapTable.Num() == 0 and IsCurveDataValid is for the check !bHasCurveData.

[Image Removed] [Image Removed]

3) This set bValidCompressedData to true and bUseRawDataForPoseExtraction to false

[Image Removed]

4) With bUseRawDataForPoseExtraction to false and PlatformCompressedData.CompressedTrackToSkeletonMapTable empty, the lambda returns false

[Image Removed]

5) The algo skips the raw data fetching

[Image Removed]

6) The root bone from default skeleton gets returned

[Image Removed]

I hope this shed some light

Thank you, this is very helpful. The dev team agrees that your fix is a good one, and we’ll integrate it and further test it.

Thanks again for spending the time and effort going through that with us.

Dustin

Glad I could help.

For info, we have been running the engine without the OR statement for a week. No issue to report.

const bool bHasValidCompressedBoneData = CompressedDataStructure != nullptr;