Workaround for ensure(PinHelpers::UnresolvedPins.Num() == 0)

Regarding the ensure(PinHelpers::UnresolvedPins.Num() == 0) failure referenced here:

[Content removed]

[Content removed]

I found setting bUseSimpleWildcardInference to True (from UK2Node_Tunnel) seems to stop this ensure from failing.

Is this a safe workaround? Does it give any clues about how to fix it properly?

(this change in behavior was introduced in 35384693 by dan.oconnor)

Thanks for the report, Ben.

By any chance, did you have any kind of repro for this issue? How did you manage to track it down to this setting? That might help us devise a fix for this, but I can also get the author of the change to weigh in.

This was originally added to fix an issue where wildcard maps in macros weren’t correctly inferring separate types for the key/values. If that isn’t something that your project really uses, then it’s fine to use the simple inference.

-Dave

Hi Dave,

We’ve been hitting this ensure fairly often in our project. I tracked it down by looking at changes made to UK2Node_Tunnel recently and I haven’t had it since setting that flag.

I’m on the same project as the original reporter of this issue. We haven’t been able to isolate a repro outside of our project. But, at least since updating to UE5.6, it seems to happen regardless of whether ZenLoader is enabled or not.

Ben

35384693 is adding a ‘smarter’ (and admittedly more complicated) wildcard inference mechanism for macros. The original implementation of blueprint macros just found the first non wildcard pin and applied it to all wildcards. This made it impossible to have a macro graph that worked with TMaps, for example.

The ensure is a low level problem with pin resolution. Because UEdGraphPin is so common it’s not a UObject and references to it are managed manually. I’m very curious about what’s going wrong here. It’s likely there is a specific macro with wildcards that is failing to compile correctly with the ‘smarter’ logic, but I’m unsure how that leads to a pin leak.

I just hit the ensure again, even with bUseSimpleWildcardInference=True, so it turns out that isn’t a complete fix.

I looked at the PinHelpers::UnresolvedPins and found they were all UK2Node_Tunnels generated as part of expanding UK2Node_MathExpressions.

This was the simplest case, subtracting a local variable from another local variable:

[Image Removed]Interestingly, I found this node had 8 internal generated nodes:

   Begin Object Class=/Script/Engine.EdGraph Name="MathExpression" ExportPath="/Script/Engine.EdGraph'/Game/Blueprints/Characters/Attributes/BP_AttributeSet_Health.BP_AttributeSet_Health:PostGameplayEffectPressureDamage.K2Node_MathExpression_1.MathExpression'"
      Begin Object Class=/Script/BlueprintGraph.K2Node_Tunnel Name="K2Node_Tunnel_0" ExportPath="/Script/BlueprintGraph.K2Node_Tunnel'/Game/Blueprints/Characters/Attributes/BP_AttributeSet_Health.BP_AttributeSet_Health:PostGameplayEffectPressureDamage.K2Node_MathExpression_1.MathExpression.K2Node_Tunnel_0'"
      End Object
      Begin Object Class=/Script/BlueprintGraph.K2Node_Tunnel Name="K2Node_Tunnel_1" ExportPath="/Script/BlueprintGraph.K2Node_Tunnel'/Game/Blueprints/Characters/Attributes/BP_AttributeSet_Health.BP_AttributeSet_Health:PostGameplayEffectPressureDamage.K2Node_MathExpression_1.MathExpression.K2Node_Tunnel_1'"
      End Object
      Begin Object Class=/Script/BlueprintGraph.K2Node_VariableGet Name="K2Node_VariableGet_12" ExportPath="/Script/BlueprintGraph.K2Node_VariableGet'/Game/Blueprints/Characters/Attributes/BP_AttributeSet_Health.BP_AttributeSet_Health:PostGameplayEffectPressureDamage.K2Node_MathExpression_1.MathExpression.K2Node_VariableGet_12'"
      End Object
      Begin Object Class=/Script/BlueprintGraph.K2Node_VariableGet Name="K2Node_VariableGet_13" ExportPath="/Script/BlueprintGraph.K2Node_VariableGet'/Game/Blueprints/Characters/Attributes/BP_AttributeSet_Health.BP_AttributeSet_Health:PostGameplayEffectPressureDamage.K2Node_MathExpression_1.MathExpression.K2Node_VariableGet_13'"
      End Object
      Begin Object Class=/Script/BlueprintGraph.K2Node_CallFunction Name="K2Node_CallFunction_1" ExportPath="/Script/BlueprintGraph.K2Node_CallFunction'/Game/Blueprints/Characters/Attributes/BP_AttributeSet_Health.BP_AttributeSet_Health:PostGameplayEffectPressureDamage.K2Node_MathExpression_1.MathExpression.K2Node_CallFunction_1'"
      End Object
      Begin Object Class=/Script/BlueprintGraph.K2Node_CallFunction Name="K2Node_CallFunction_0" ExportPath="/Script/BlueprintGraph.K2Node_CallFunction'/Game/Blueprints/Characters/Attributes/BP_AttributeSet_Health.BP_AttributeSet_Health:PostGameplayEffectPressureDamage.K2Node_MathExpression_1.MathExpression.K2Node_CallFunction_0'"
      End Object
      Begin Object Class=/Script/BlueprintGraph.K2Node_VariableGet Name="K2Node_VariableGet_10" ExportPath="/Script/BlueprintGraph.K2Node_VariableGet'/Game/Blueprints/Characters/Attributes/BP_AttributeSet_Health.BP_AttributeSet_Health:PostGameplayEffectPressureDamage.K2Node_MathExpression_1.MathExpression.K2Node_VariableGet_10'"
      End Object
      Begin Object Class=/Script/BlueprintGraph.K2Node_VariableGet Name="K2Node_VariableGet_11" ExportPath="/Script/BlueprintGraph.K2Node_VariableGet'/Game/Blueprints/Characters/Attributes/BP_AttributeSet_Health.BP_AttributeSet_Health:PostGameplayEffectPressureDamage.K2Node_MathExpression_1.MathExpression.K2Node_VariableGet_11'"
      End Object
   End Object

If I remake the node, it only has 5:

   Begin Object Class=/Script/Engine.EdGraph Name="MathExpression" ExportPath="/Script/Engine.EdGraph'/Game/Blueprints/Characters/Attributes/BP_AttributeSet_Health.BP_AttributeSet_Health:PostGameplayEffectPressureDamage.K2Node_MathExpression_2.MathExpression'"
      Begin Object Class=/Script/BlueprintGraph.K2Node_CallFunction Name="K2Node_CallFunction_9" ExportPath="/Script/BlueprintGraph.K2Node_CallFunction'/Game/Blueprints/Characters/Attributes/BP_AttributeSet_Health.BP_AttributeSet_Health:PostGameplayEffectPressureDamage.K2Node_MathExpression_2.MathExpression.K2Node_CallFunction_9'"
      End Object
      Begin Object Class=/Script/BlueprintGraph.K2Node_VariableGet Name="K2Node_VariableGet_22" ExportPath="/Script/BlueprintGraph.K2Node_VariableGet'/Game/Blueprints/Characters/Attributes/BP_AttributeSet_Health.BP_AttributeSet_Health:PostGameplayEffectPressureDamage.K2Node_MathExpression_2.MathExpression.K2Node_VariableGet_22'"
      End Object
      Begin Object Class=/Script/BlueprintGraph.K2Node_VariableGet Name="K2Node_VariableGet_21" ExportPath="/Script/BlueprintGraph.K2Node_VariableGet'/Game/Blueprints/Characters/Attributes/BP_AttributeSet_Health.BP_AttributeSet_Health:PostGameplayEffectPressureDamage.K2Node_MathExpression_2.MathExpression.K2Node_VariableGet_21'"
      End Object
      Begin Object Class=/Script/BlueprintGraph.K2Node_Tunnel Name="K2Node_Tunnel_1" ExportPath="/Script/BlueprintGraph.K2Node_Tunnel'/Game/Blueprints/Characters/Attributes/BP_AttributeSet_Health.BP_AttributeSet_Health:PostGameplayEffectPressureDamage.K2Node_MathExpression_2.MathExpression.K2Node_Tunnel_1'"
      End Object
      Begin Object Class=/Script/BlueprintGraph.K2Node_Tunnel Name="K2Node_Tunnel_0" ExportPath="/Script/BlueprintGraph.K2Node_Tunnel'/Game/Blueprints/Characters/Attributes/BP_AttributeSet_Health.BP_AttributeSet_Health:PostGameplayEffectPressureDamage.K2Node_MathExpression_2.MathExpression.K2Node_Tunnel_0'"
      End Object
   End Object

Both the old and new UK2Node_MathExpressions generate the same looking graph:

[Image Removed]

But it looks like old UK2Node_MathExpressions somehow has some stale internal nodes and I assume it’s those that’s causing the PinHelpers::UnresolvedPins issue.

Now that we’ve tracked it down, we can remake the UK2Node_MathExpressions to work around it, but maybe this gives a clue about what the underlying issue is?

Ben

That’s very interesting and strange.. Can you show me how the node is used in a macro?

It’s not used in a macro. The failure is in the UK2Node_MathExpression expansion.

And it turns out that recreating the UK2Node_MathExpressions doesn’t fix it. On a fresh load, the same problem occurs.

Ben

Hi [mention removed]​ & [mention removed]​,

Ben was able to provide me a 100% repro case within our project and using that I was able to figure out the underlying issue and produce a 100% repro scenario project for the vanilla engine (tested with 5.6.1), which is attached to this message. To reproduce the issue;

  • Extract the project to a new project folder and open it with the latest Unreal Engine, which should launch with TB_Bug loaded, if not load into this map and do not touch other assets.
  • Start a PIE session and stop it after a second or two.
  • Make any random change on an actor and just undo. You will see the debugger stopping at this ensure at this point.

Why is this happening?

Math expression blueprint nodes behaves really differently compared to most of the other nodes, they provide an internal graph that contains the extracted expression. When saved, all of this internal graph and its nodes are also serialized into the package as well. During initial loading, math expression is loaded and right away rebuilt, which means the existing data is deleted and replaced with new nodes, however this deletion also means that the package is now half-loaded, if any other asset that references the owner of the math expression node is loaded beyond this point, the AsyncLoading system will consider this package to be half loaded and will load the missing chunks from the disk again, which are the deleted nodes & pins. But if we’re loading everything again, why are they unresolved? It is because the `End` node is not reloaded, that node was kept in memory but its pin was thrashed and recreated. I believe this was done because at some point this underlying issue caused some math expressions to become invalid as the second load would be moving the link of the end node to newly loaded node, but this fix caused a silent error which is the loaded pin being left in the UnresolvedPins list.

In my opinion, looking at how the system works, the best possible solution would be to change how MathExpression nodes are saved, and not to serialize their internal graph into the package at least for the editor, as they are already going to be rebuilt on load but I’m not sure if that is going to cause other problems and I would love to see the solution you’re going to come up with.

Thanks for your help on the matter so far and have a great day & weekend.

Great bug report, I’ve reproduced the problem and logged it as UE-318827. My first instinct is that the pin resolution logic can be hardened to accommodate math expression, but the exact solution will be determined after deeper investigation.

It seems this case doesn’t involve any ‘smart’ macro inference, which may mean another problem with the more advanced macro inference routine is lurking - or that this math expression node is just problematic and is also tripping up macro expansion.

Public link for tracking: Unreal Engine Issues and Bug Tracker (UE\-318827)