Hello Euan,
We have resolved this by now, and it is indeed the `InitializationCounter` that fails to prevent AnimNodes from initializing over and over again.
The reason why it looks so “infinite loop”-like is due to the SaveCachedPose AnimNode being used very early in the AnimGraph.
Combined with AnimNodes like “LayeredBlendPerBone”, which use the same cached pose a second time, with, e.g., just a MontageSlot in front of it, will cause the SavedCachedPose AnimNode to initialize multiple times, with just a slight offset in the trace (creating the visual pattern).
So now the question of “Why does the Counter not stop this?” comes up.
First of all, I have to mention that the trace posted by Chris is of an AnimGraph (or AnimInstance) that is only used as a LinkedLayer, and a LinkedLayer will use the InitializationCounter of its outer AnimGraph to sync itself with.
So why is the outer InitializationCounter invalid? Because, in our case, the outer AnimGraph hasn’t actually initialized yet when the LinkedLayer gets applied.
I can’t say if these are the correct repro steps, but this is roughly how our setup works:
- We have a main ThirdPerson Character AnimInstance with a LinkedLayerSlot for a ThirdPerson Weapon AnimInstance.
- The ThirdPerson Weapon AnimInstance has lots of SaveCachedPose AnimNodes that are used with AnimNodes that initialize multiple “paths” (like the LayeredBlendPerBone one).
- We set `bTickAnimationOnSkeletalMeshInit` to false within our DefaultEngine.ini, to allow our AnimGraphs from initializing deferred.
- We apply the ThirdPerson Weapon AnimInstance as a linked layer through an OnRep.
Due to bTickAnimationOnSkeletalMeshInit being false, the init of the main AnimGraph will be deferred and will only happen once the AnimationProxy calls Update on a worker thread.
We call `USkeletalMeshComponent::LinkAnimClassLayers` when the weapon item replicates and triggers an OnRep, which will lead to `UAnimInstance::LinkAnimClassLayers` and finally to `UAnimInstance::PerformLinkedLayerOverlayOperation`. And while `PerformLinkedLayerOverlayOperation` does have a parameter called `bInDeferSubGraphInitialization`, it defaults to false in this case.
So now we have the linked AnimGraph going through the initialization flow without the outer/main AnimGraph having done the same.
Since there is no call to increment the counter in that code-path, it will result in the expensive initialize call where the SavedCachePose node will trigger over and over again, for every single time it is used in the graph (or at least every time it is encountered when walking through the AnimGraph on initialization).
Since circular dependencies between SavedCachedPose AnimNodes are blocked (so you can’t have cached pose A reference cached pose B and the other way round), this will still eventually finish, but depending on how many AnimNodes sit behind a given SavedCachedPose AnimNode and depending on how often that SavedCachedPose AnimNode is used, this can take ages to initialize.
How did we resolve this?
So, first things first, I’m not that used to AnimGraph code, so this could be the completely wrong approach, just keep that in mind.
The first thing I did was copy the `bTickAnimationNow` boolean, which can be found in `USkeletalMeshComponent::InitAnim`, to two call places of `PerformLinkedLayerOverlayOperation` within `UAnimInstance` and use the boolean for the otherwise defaulted param.
Those two places are `UAnimInstance::LinkAnimClassLayers` and `UAnimInstance::UnlinkAnimClassLayers`. The third one is the standard call coming from `UAnimInstance::InitializeGroupedLayers`, which already uses the boolean.
`const bool bTickAnimationNow = (((GetWorld()->WorldType == EWorldType::Editor) && !SkelMeshComp->bForceRefpose)
|| UAnimationSettings::Get()->bTickAnimationOnSkeletalMeshInit)
&& !SkelMeshComp->bUseRefPoseOnInitAnim;
PerformLinkedLayerOverlayOperation(InClass, SelectResolvedClassIfValid, !bTickAnimationNow);`I did adjust this a bit more because our Dedicated Server doesn’t tick AnimInstances, which means I always set that boolean to true for it, but that’s irrelevant to the problem.
This already solves the majority of the issue, because now the linked AnimGraph is also initializing deferred and will be taken care of alongside the main AnimGraph in `FAnimInstanceProxy::UpdateAnimation_WithRoot`.
This does, however, cause a problem if the main AnimGraph is already initialized, so I also added a boolean called `bDeferSubGraphInitialization` to the `UAnimInstance`, which I set to true inside `PerformLinkedLayerOverlayOperation` when `bInDeferSubGraphInitialization` is also true.
I then use this inside `FAnimInstanceProxy::UpdateAnimation_WithRoot`, to initialize just the linked layers. A small improvement was made by adding the same boolean to `FAnimInstanceProxy` and also setting it to true, so that we aren’t looping over the linked AnimInstances every time for no reason.
That now allows linked AnimGraphs to initialize deferred “later”, such as when changing weapons during gameplay.
Anything else?
Yes, we noticed that the order in which `FAnimInstanceProxy::InitializeRootNode_WithRoot`, `FAnimInstanceProxy::CacheBones`, and the initialization and caching of bones of sub graphs is handled causes a similar expensive call to `FAnimNode_LinkedAnimGraph::CacheBonesSubGraph_AnyThread`.
`CacheBones` is called after the linked AnimGraphs are looped, and `CacheBones` is the function that increments the `CachedBonesCounter`, so the linked AnimGraphs can still end up caching bones with an invalid outer/main AnimGraph `CachedBonesCounter`. We ended up moving the `CacheBones` call upwards, between `UpdateAnimation_WithRoot` and the linked AnimGraph loop. This doesn’t seem to have broken anything and resolves the additional expensive call to `CacheBonesSubGraph_AnyThread`.
There is one additional change that I added. Inside `PerformLinkedLayerOverlayOperation`, the following code can be found:
UAnimInstance* LinkedInstance = SharedLinkedAnimLayers->AddLinkedFunction(this, ClassToSet, FunctionToLink, bIsNewInstance);
And while a bit further down, I handle setting the `bDeferSubGraphInitialization` boolean to true for the `LinkedInstance`, the `FAnimSubsystem_SharedLinkedAnimLayers::AddLinkedFunction` results in the Initialize flow being called instantly.
Inside `FLinkedAnimLayerClassData::FindOrAddInstanceForLinking`, it calls `NewAnimInstance->InitializeAnimation()`, which doesn’t pass any bDefer boolean in.
So my change added another parameter to `AddLinkedFunction` and `FindOrAddInstanceForLinking` called `bInDeferSubGraphInitialization`, which I use to pass the same parameter of `PerformLinkedLayerOverlayOperation` along. And inside `FindOrAddInstanceForLinking`, I then pass this into `InitializeAnimation` and set the `bDeferSubGraphInitialization` boolean of the `NewAnimInstance` to that same param. This then handles deferring that last puzzle piece.
--
And that’s kinda it. Feel free to provide a “proper” solution in case there are hidden problems with ours.
Cheers
Cedric