Delegate bindings for blueprints made in 4.15 are always out of date

Hello,

We’re experiencing an issue trying to migrate our project from 4.15 to 4.18.1. We’ve got some widget-based blueprints that expose variables that are instances of (more) widget-based blueprints. A few of these blueprints provide delegates that other widgets can bind to. When we migrated these blueprints to 4.18.1, we noticed we were constantly getting ‘Event node XYZ is out-of-date.’ errors. Recompiling makes these errors disappear but they come right back if we reload the editor.

I have a sample project which I can upload that exhibits this issue. I don’t have any web storage available unfortunately but I can PM the project to whoever troubleshoots the issue (the attachment system here doesn’t like .7z files).

The sample project has the following blueprints within it:

The hierarchy is as follows (in increasing order):

  1. UserWidget
  2. GraphicSetting
  3. GraphicSettingSlider / GraphicSettingCheckbox

ControlsBinding contains instances of all the other blueprints in its widget tree. Some of these instances are exposed as variables to the ControlsBinding graph. Two variables in particular (“MouseSensitivity” of type GraphicSettingSlider) and (“MouseInvert” of type “GraphicSettingCheckbox”) have delegate bindings associated with them within ControlsBinding’s graph:

In the image above, both of these event nodes are showing as out of date. However, there have been no changes made to either GraphicSettingCheckbox or GraphicSettingSlider.

I have spent about three weeks attempting to debug this issue and here is what I’ve found:

I apologize in advance for the massive wall of text. I am trying to provide as much useful information as possible :).

At the top level, the error message is appearing because FKismetCompilerContext::CreateFunctionStubForEvent calls attempts to reconcile the pins in the delegate binding event node with a runtime generated instance of UK2Node_FunctionEntry. The IsFunctionEntryCompatible call fails because the generated UK2Node_FunctionEntry does not have the same number (and type) of pins that the delegate binding event node has.

The reason why the EntryNode has incorrect pins is because UK2Node_FunctionEntry::AllocateDefaultPins (which is called by FKismetCompilerContext::CreateFunctionStubForEvent) fails to allocate pins for a function entry and exit.

The reason why this occurs is because of this code:

The call to FindField is failing. If you debug the project, you will see that SignatureClass is an instance of ‘GraphicSettingSlider_C’ and that SignatureName is “OnDraggingFinished__DelegateSignature”. The GraphicSettingSlider blueprint provides this delegate for binding.

Under the hood, FindField uses a TFieldIterator template class to search through properties for a UObject. Here is a picture of the constructor for this class:

InStruct is an instance of ‘GraphicSettingSlider_C’. You will note that ‘Field’ is set to ‘InStruct->Children’. FindField’s failure in this situation manifests because ‘InStruct->Children’ is NULL. IterateToNext() attempts to traverse the child’s fields first, then it moves on to the parent’s. The delegate signature function (i.e. OnDraggingFinished__DelegateSignature) by definition is supposed to be a field in the child class. Since ‘InStruct->Children’ is NULL, it never finds the UFunction and returns NULL.

This NULL value means CreatePinsForFunctionEntryExit() is never called on the UK2Node_FunctionEntry object. This is what leads to the warning.

This is also why saving the blueprint after a recompile doesn’t make the error go away. UK2Node_FunctionEntry is being added on a stub graph marked as RF_Transient (which propagates that flag to all nodes underneath it):

This however, doesn’t explain why SignatureClass->Children is set to NULL which is the true root cause.

That problem is caused by FBlueprintCompilationManagerImpl::FlushCompilationQueueImpl. This method is called when ControlsBinding is unserialized from disk into memory. The FLinkerLoad class calls RegenerateClass on each blueprint object ControlsBinding depends on. Each dependent object is submitted for compilation via a call to FBlueprintCompilationManagerImpl::QueueForCompilation. The requests appear to be batched up and stored in FBlueprintCompilationManagerImpl::QueuedRequests.

The weird problem with the ControlsBinding blueprint is, QueuedRequests contains five blueprints queued for compilation in the following order:

[0] {BPToCompile=0x000001bc959df900 (Name=0x000001bc9262c788 "GraphicSetting") 
[1] {BPToCompile=0x000001bc959deb00 (Name=0x000001bc9262c7f8 "GraphicSettingCheckbox") 
[2] {BPToCompile=0x000001bc959dd600 (Name=0x000001bc9262c968 "GraphicSettingSlider") 
[3] {BPToCompile=0x000001bcca281600 (Name=0x000001bc9262ca68 "KeyBindDynamic") 
[4] {BPToCompile=0x000001bcca282400 (Name=0x000001bc9262c650 "ControlsBinding") 

This would be the correct ordering to compile these blueprints in. This array is later pushed into CurrentlyCompilingBPs (yet another array). Later on, this array is sorted via this line:

CurrentlyCompilingBPs.Sort( HierarchyDepthSortFn );

HierarchyDepthSortFn sorts the elements in the array by how deep their dependency hierarchy is. This is where the real problem begins. ControlsBinding is not a parent or child class of any of the other blueprints. It only depends on UserWidget. Because it only has a dependency hierarchy of 2, it bubbles up to the top of CurrentlyCompilingBPs:

[0] {BP=0x000001bcca282400 (Name=0x000001bc9262c650 "ControlsBinding") 
[1] {BP=0x000001bc959df900 (Name=0x000001bc9262c788 "GraphicSetting") 
[2] {BP=0x000001bcca281600 (Name=0x000001bc9262ca68 "KeyBindDynamic") 
[3] {BP=0x000001bc959deb00 (Name=0x000001bc9262c7f8 "GraphicSettingCheckbox")
[4] {BP=0x000001bc959dd600 (Name=0x000001bc9262c968 "GraphicSettingSlider") 

During “STAGE X” of blueprint compilation, these blueprint instances’ generatedclasses are copied off and reinstanced. During this process, FBlueprintCompileReinstancer::MoveCDOToNewClass is called. The purpose of this function appears to be to copy off the class default object data to the reinstanced version of the blueprint class:

GIsDuplicatingClassForReinstancing = true;
OwnerClass->ClassFlags |= CLASS_NewerVersionExists;

UObject* OldCDO = OwnerClass->ClassDefaultObject;
const FName ReinstanceName = MakeUniqueObjectName(GetTransientPackage(), OwnerClass->GetClass(), *(FString(TEXT("REINST_")) + *OwnerClass->GetName()));
UClass* CopyOfOwnerClass = CastChecked<UClass>(StaticDuplicateObject(OwnerClass, GetTransientPackage(), ReinstanceName, ~RF_Transactional));

CopyOfOwnerClass->RemoveFromRoot();
OwnerClass->ClassFlags &= ~CLASS_NewerVersionExists;
GIsDuplicatingClassForReinstancing = false;

Because the class flag CLASS_NewerVersionExists was added to OwnerClass, it gets transferred to the new copy (which is the REINST_ version of the blueprint class). This flag is later removed from OwnerClass but not CopyOfOwnerClass. This becomes important later on during the blueprint compilation process. CopyOfOwnerClass is returned to the caller.

Compilation moves on to “STAGE XI”. Each currently compiled blueprint is looped over and checked to see if there is a reinstanced version of it (by checking for the CLASS_NewerVersionExists flag on the Blueprint’s GeneratedClass field). This flag was set on GraphicSettingSlider and GraphicSettingCheckbox (which are the two blueprints whose delegate signatures are not being found by FindField) as described above. This loop finds this flag and promptly does the following:

if(CompilerData.ShouldResetClassMembers())
{
    BP->GeneratedClass->Children = NULL;
    BP->GeneratedClass->Script.Empty();
    BP->GeneratedClass->MinAlignment = 0;
    BP->GeneratedClass->RefLink = NULL;
    BP->GeneratedClass->PropertyLink = NULL;
    BP->GeneratedClass->DestructorLink = NULL;
    BP->GeneratedClass->ScriptObjectReferences.Empty();
    BP->GeneratedClass->PropertyLink = NULL;
}

This is the reason why the ‘Children’ field is NULL.

Finally, in “STAGE XII” of the blueprint compilation process, CurrentlyCompilingBPs is looped over. The root cause of the issue occurs here. ControlsBinding is the first blueprint to be compiled, but it depends on GraphicSettingSlider’s GeneratedClass having all of its delegate signatures stored in its Children field. It is this stage of the compilation process that ends up calling FKismetCompilerContext::CreateFunctionStubForEvent and ultimately causing the incorrect pins to be generated.

My hypothesis is, there is some edge case not being handled properly by the unserialization process. It really seems as though FBlueprintCompilationManagerImpl::FlushCompilationQueueImpl is only meant to batch together related blueprints that have a parent-child relationship. I make this assumption based on HierarchyDepthSortFn sorting by how deep the inheritance hierarchy goes. The sort function never compares types. ControlsBinding should not be batched together with any of the GraphicSetting blueprints. Alternatively, HierarchyDepthSortFn needs to take the type into consideration when sorting.

That’s just my guess though. I’m in no way shape or form familiar with the reasons behind the blueprint compile code. I merely tried to debug it as far as I could :slight_smile:

I got reports of this error occuring in my Menu Starter Kit marketplace asset. Sometimes the error only appeared during packaging causing the build to fail. I also created the plugin in 4.15 and updated it for 4.16, 4.17 and 4.18. The reporters were all using 4.17 or newer. However I have not been able to reproduce the issue myself so far, and thus couldn’t do any debugging myself. Maybe the issue you are describing and the reports I got are related.
Could you message me your example project, so I can test this issue on my side too? I can also upload it to a shared cloud folder for you, if you want.

Project sent. I would definitely appreciate the hosting :).

Hi,

I did more digging and figured out the actual cause:

ControlsBinding depends on KeyBindDynamic…
…and KeyBindDynamic depends on ControlsBinding…

This is why ControlsBinding was getting batched together in the fashion I described in my original post. I was double clicking on the ControlsBinding blueprint to load it, and it would load all its dependencies dependencies…which turns out would cause it to load itself when it loaded KeyBindDynamic.

This leads to the failure conditions described above.

This bug can be closed. It would be super awesome to have a circular dependency warning if possible. It would’ve saved me a ton of time. :slight_smile: