// Copyright Epic Games, Inc. All Rights Reserved. #include "BlueprintNodeTemplateCache.h" #include "Animation/AnimBlueprint.h" #include "Animation/AnimBlueprintGeneratedClass.h" #include "Animation/AnimInstance.h" #include "BlueprintEditorSettings.h" #include "BlueprintNodeBinder.h" #include "BlueprintNodeSpawner.h" #include "EdGraph/EdGraph.h" #include "EdGraph/EdGraphNode.h" #include "Engine/Blueprint.h" #include "Engine/BlueprintGeneratedClass.h" #include "GameFramework/Actor.h" #include "HAL/PlatformCrt.h" #include "Kismet2/BlueprintEditorUtils.h" #include "Kismet2/KismetEditorUtilities.h" #include "Logging/LogCategory.h" #include "Logging/LogMacros.h" #include "Math/Vector2D.h" #include "Misc/AssertionMacros.h" #include "ProfilingDebugging/CpuProfilerTrace.h" #include "Templates/Casts.h" #include "Templates/SubclassOf.h" #include "Templates/Tuple.h" #include "Trace/Detail/Channel.h" #include "UObject/Class.h" #include "UObject/MetaData.h" #include "UObject/NameTypes.h" #include "UObject/Object.h" #include "UObject/ObjectMacros.h" #include "UObject/Package.h" #include "UObject/UObjectGlobals.h" #include "UObject/UObjectHash.h" class UEdGraphSchema; DEFINE_LOG_CATEGORY_STATIC(LogBlueprintNodeCache, Log, All); /******************************************************************************* * Static FBlueprintNodeTemplateCache Helpers ******************************************************************************/ namespace BlueprintNodeTemplateCacheImpl { /** */ static int64 ActiveMemFootprint = 0; /** Used to track the average blueprint size (so that we can try to predict when a blueprint would fail to be cached) */ static int32 MadeBlueprintCount = 0; static int32 AverageBlueprintSize = 0; /** Metadata tag used to identify graphs created by this system. */ static const FName TemplateGraphMetaTag(TEXT("NodeTemplateCache_Graph")); /** * Checks to see if this node is compatible with the given graph (to know if * a node template can be spawned within it). * * @param NodeObj The CDO of the node you want to spawn. * @param Graph The graph you want to check compatibility for. * @return True if the node and graph are compatible (for templating purposes). */ static bool IsCompatible(UEdGraphNode* NodeObj, UEdGraph* Graph); /** * Looks through a blueprint for compatible graphs (ones that the specified * node can spawn into). * * @param BlueprintOuter The blueprint to search through. * @param NodeObj The CDO of the node you want to spawn. * @param IsCompatibleFunc An option callback function to further filter out incompatible nodes. * @return The first compatible graph found (null if no graph was found). */ static UEdGraph* FindCompatibleGraph(UBlueprint* BlueprintOuter, UEdGraphNode* NodeObj, bool(*IsCompatibleFunc)(UEdGraph*) = nullptr); /** * Creates a transient, temporary blueprint. Intended to be used as a * template-node's outer (grandparent). * * @param BlueprintClass The class of blueprint to make. * @param ParentClass The class type of blueprint to make (actor, object, etc.). * @param GeneratedClassType The type of class that the blueprint should generate. * @return A newly spawned (transient) blueprint. */ static UBlueprint* MakeCompatibleBlueprint(TSubclassOf BlueprintClass, UClass* ParentClass, TSubclassOf GeneratedClassType); /** * Creates a new transient graph, for template node use (meant to be used as * a template node's outer). * * @param BlueprintOuter The blueprint to nest the new graph under. * @param SchemaClass The schema to assign the new graph. * @return A newly created graph. */ static UEdGraph* AddGraph(UBlueprint* BlueprintOuter, TSubclassOf SchemaClass); /** * Adds metadata to the supplied graph, flagging it as a graph belonging to * the BlueprintNodeTemplateCache (so we can easily identify it later on). * * @param NewGraph The graph to flag. */ static void MarkGraphForTemplateUse(UEdGraph* NewGraph); /** * Determines if the specified graph is one that was allocated by * BlueprintNodeTemplateCache (to house template nodes). * * @param ParentGraph The graph you want checked. * @return True if this graph belongs to a BlueprintNodeTemplateCache, false if not. */ static bool IsTemplateOuter(UEdGraph* ParentGraph); /** * Converts the cache memory cap into bytes (form user settings). * * @return The user defined cache cap (in bytes). */ static int32 GetCacheCapSize(); /** * Totals the size of the specified object, along with any other objects * that have it in their outer chain. Does not account for any allocated * memory that belongs to the object(s). * * @param Object The object you want an estimated byte size for. * @return An estimated size (in bytes)... currently does not account for any allocated memory that the object may be responsible for. */ static int32 ApproximateMemFootprint(UObject const* Object); } //------------------------------------------------------------------------------ static bool BlueprintNodeTemplateCacheImpl::IsCompatible(UEdGraphNode* NodeObj, UEdGraph* Graph) { const UEdGraphSchema* Schema = Graph->GetSchema(); return ensureMsgf(Schema != nullptr, TEXT("PROTO_BP graph with invalid schema: %s "), *Graph->GetName()) && NodeObj->CanCreateUnderSpecifiedSchema(Schema); } //------------------------------------------------------------------------------ static UEdGraph* BlueprintNodeTemplateCacheImpl::FindCompatibleGraph(UBlueprint* BlueprintOuter, UEdGraphNode* NodeObj, bool(*IsCompatibleFunc)(UEdGraph*)) { UEdGraph* FoundGraph = nullptr; TArray BlueprintChildObjs; GetObjectsWithOuter(BlueprintOuter, BlueprintChildObjs, /*bIncludeNestedObjects =*/false, /*ExclusionFlags =*/ RF_NoFlags, /** InternalExcludeFlags */ EInternalObjectFlags::Garbage); for (UObject* Child : BlueprintChildObjs) { UEdGraph* ChildGraph = Cast(Child); bool bIsCompatible = (ChildGraph != nullptr) && IsCompatible(NodeObj, ChildGraph); if (bIsCompatible && (IsCompatibleFunc != nullptr)) { bIsCompatible = IsCompatibleFunc(ChildGraph); } if (bIsCompatible) { FoundGraph = ChildGraph; break; } } return FoundGraph; } //------------------------------------------------------------------------------ static UBlueprint* BlueprintNodeTemplateCacheImpl::MakeCompatibleBlueprint(TSubclassOf BlueprintClass, UClass* ParentClass, TSubclassOf GeneratedClassType) { EBlueprintType BlueprintType = BPTYPE_Normal; if (GeneratedClassType == nullptr) { GeneratedClassType = UBlueprintGeneratedClass::StaticClass(); } UPackage* BlueprintOuter = GetTransientPackage(); const FString DesiredName = FString::Printf(TEXT("PROTO_BP_%s"), *BlueprintClass->GetName()); const FName BlueprintName = MakeUniqueObjectName(BlueprintOuter, BlueprintClass, FName(*DesiredName)); BlueprintClass = FBlueprintEditorUtils::FindFirstNativeClass(BlueprintClass); UBlueprint* NewBlueprint = FKismetEditorUtilities::CreateBlueprint(ParentClass, BlueprintOuter, BlueprintName, BlueprintType, BlueprintClass, GeneratedClassType); NewBlueprint->SetFlags(RF_Transient); ++MadeBlueprintCount; const float AproxBlueprintSize = static_cast(ApproximateMemFootprint(NewBlueprint)); const float MadeBlueprintCountFloat = static_cast(MadeBlueprintCount); // track the average blueprint size, so that we can attempt to predict // whether a blueprint will fail to be cached (when the cache is near full) AverageBlueprintSize = static_cast( (AverageBlueprintSize * ((MadeBlueprintCountFloat - 1.0f) / MadeBlueprintCountFloat)) + (AproxBlueprintSize / MadeBlueprintCountFloat) + 0.5f ); return NewBlueprint; } //------------------------------------------------------------------------------ static UEdGraph* BlueprintNodeTemplateCacheImpl::AddGraph(UBlueprint* BlueprintOuter, TSubclassOf SchemaClass) { UClass* GraphClass = UEdGraph::StaticClass(); FName const GraphName = MakeUniqueObjectName(BlueprintOuter, GraphClass, FName(TEXT("TEMPLATE_NODE_OUTER"))); UEdGraph* NewGraph = NewObject(BlueprintOuter, GraphClass, GraphName, RF_Transient); NewGraph->Schema = SchemaClass; MarkGraphForTemplateUse(NewGraph); return NewGraph; } //------------------------------------------------------------------------------ static void BlueprintNodeTemplateCacheImpl::MarkGraphForTemplateUse(UEdGraph* NewGraph) { UPackage* TemplatePackage = NewGraph->GetPackage(); FMetaData& PackageMetadata = TemplatePackage->GetMetaData(); PackageMetadata.SetValue(NewGraph, TemplateGraphMetaTag, TEXT("true")); } //------------------------------------------------------------------------------ bool BlueprintNodeTemplateCacheImpl::IsTemplateOuter(UEdGraph* ParentGraph) { if (ParentGraph->HasAnyFlags(RF_Transactional)) { UPackage* GraphPackage = ParentGraph->GetPackage(); FMetaData& PackageMetadata = GraphPackage->GetMetaData(); return PackageMetadata.HasValue(ParentGraph, TemplateGraphMetaTag); } return false; } //------------------------------------------------------------------------------ static int32 BlueprintNodeTemplateCacheImpl::GetCacheCapSize() { const UBlueprintEditorSettings* BpSettings = GetDefault(); // have to convert from MB to bytes return (static_cast(BpSettings->NodeTemplateCacheCapMB) << 20); } //------------------------------------------------------------------------------ static int32 BlueprintNodeTemplateCacheImpl::ApproximateMemFootprint(UObject const* Object) { TArray ChildObjs; GetObjectsWithOuter(Object, ChildObjs, /*bIncludeNestedObjects =*/true); int32 ApproimateDataSize = sizeof(*Object); for (UObject* ChildObj : ChildObjs) { // @TODO: doesn't account for any internal allocated memory (for member TArrays, etc.) ApproimateDataSize += sizeof(*ChildObj); } return ApproimateDataSize; } /******************************************************************************* * FBlueprintNodeTemplateCache ******************************************************************************/ //------------------------------------------------------------------------------ FBlueprintNodeTemplateCache::FBlueprintNodeTemplateCache() : ApproximateObjectMem(0) { using namespace BlueprintNodeTemplateCacheImpl; // for MakeCompatibleBlueprint() UBlueprint* StandardBlueprint = MakeCompatibleBlueprint(UBlueprint::StaticClass(), AActor::StaticClass(), UBlueprintGeneratedClass::StaticClass()); CacheBlueprintOuter(StandardBlueprint); UBlueprint* AnimBlueprint = MakeCompatibleBlueprint(UAnimBlueprint::StaticClass(), UAnimInstance::StaticClass(), UAnimBlueprintGeneratedClass::StaticClass()); CacheBlueprintOuter(AnimBlueprint); } //------------------------------------------------------------------------------ UEdGraphNode* FBlueprintNodeTemplateCache::GetNodeTemplate(UBlueprintNodeSpawner const* NodeSpawner, UEdGraph* TargetGraph) { TRACE_CPUPROFILER_EVENT_SCOPE(FBlueprintNodeTemplateCache::GetNodeTemplate); using namespace BlueprintNodeTemplateCacheImpl; bool bIsOverMemCap = false; auto LogCacheFullMsg = [&bIsOverMemCap]() { if (!bIsOverMemCap) { static int32 LoggedCapSize = -1; int32 const CurrentCacheSize = GetCacheCapSize(); // log only once for each cap size change if (LoggedCapSize != CurrentCacheSize) { UE_LOG(LogBlueprintNodeCache, Display, TEXT("The blueprint template-node cache is full. As a result, you may experience interactions which are slower than normal. To avoid this, increase the cache's cap in the blueprint editor prefences.")); LoggedCapSize = CurrentCacheSize; } bIsOverMemCap = true; } }; UEdGraphNode* TemplateNode = nullptr; if (auto* FoundNode = NodeTemplateCache.Find(NodeSpawner)) { TemplateNode = *FoundNode; } else if (NodeSpawner->NodeClass != nullptr) { UEdGraphNode* NodeCDO = NodeSpawner->NodeClass->GetDefaultObject(); check(NodeCDO != nullptr); UBlueprint* TargetBlueprint = nullptr; TSubclassOf BlueprintClass; bool const bHasTargetGraph = (TargetGraph != nullptr); if (bHasTargetGraph) { // by the time we're asking for a prototype for this spawner, we should // be sure that it is compatible with the TargetGraph //check(IsCompatible(NodeCDO, TargetGraph)); TargetBlueprint = FBlueprintEditorUtils::FindBlueprintForGraph(TargetGraph); check(TargetBlueprint != nullptr); BlueprintClass = TargetBlueprint->GetClass(); // check used to help identify user interactable graphs (as opposed to // intermediate/transient graphs). auto IsCompatibleUserGraph = [](UEdGraph* Graph)->bool { return !Graph->HasAnyFlags(RF_Transient); }; TargetGraph = FindCompatibleGraph(TargetBlueprint, NodeCDO, IsCompatibleUserGraph); check(TargetGraph != nullptr); } UBlueprint* CompatibleBlueprint = nullptr; UEdGraph* CompatibleOuter = nullptr; // find a compatible outer (don't want to have to create a new one if we don't have to) for (UBlueprint* Blueprint : TemplateOuters) { CompatibleOuter = FindCompatibleGraph(Blueprint, NodeCDO); if (CompatibleOuter != nullptr) { MarkGraphForTemplateUse(CompatibleOuter); } if (CompatibleOuter != nullptr) { CompatibleBlueprint = Blueprint; break; } else if ((BlueprintClass != nullptr) && Blueprint->GetClass()->IsChildOf(BlueprintClass)) { CompatibleBlueprint = Blueprint; } } // reset ActiveMemFootprint, so calls to CacheBlueprintOuter()/CacheTemplateNode() // use the most up-to-date value (users could have since modified the // nodes, so they could have grown in size... like with AllocateDefaultPins) // // @TODO: GetEstimateCacheSize() is (most likely) inaccurate, seeing as // external systems mutate template-nodes (such as calling // AllocateDefaultPins), and this returns a size estimate from // when the node was first spawned (it is too slow to recalculate // the size of the object hierarchy here) ActiveMemFootprint = GetEstimateCacheSize(); int32 const CacheCapSize = GetCacheCapSize(); if (ActiveMemFootprint > CacheCapSize) { LogCacheFullMsg(); // @TODO: evict nodes until we're back under the cap (in case the cap // was changed at runtime, or external user modified node sizes) } // if a TargetGraph was supplied, and we couldn't find a suitable outer // for this template-node, then attempt to emulate that graph if (bHasTargetGraph) { if (CompatibleBlueprint == nullptr) { // if the cache is near full, attempt to predict if this // impending cache will fail (if so, we don't want to waste the // cycles on allocating a temp blueprint) if (!bIsOverMemCap && ((AverageBlueprintSize == 0) || (ActiveMemFootprint + AverageBlueprintSize <= CacheCapSize))) { TSubclassOf GeneratedClassType = UBlueprintGeneratedClass::StaticClass(); if (TargetBlueprint->GeneratedClass != nullptr) { GeneratedClassType = TargetBlueprint->GeneratedClass->GetClass(); } UClass* ParentClass = TargetBlueprint->ParentClass != nullptr ? TargetBlueprint->ParentClass.Get() : UObject::StaticClass(); CompatibleBlueprint = MakeCompatibleBlueprint(BlueprintClass, ParentClass, GeneratedClassType); if (!CacheBlueprintOuter(CompatibleBlueprint)) { LogCacheFullMsg(); } // this graph may come default with a compatible graph CompatibleOuter = FindCompatibleGraph(CompatibleBlueprint, NodeCDO); if (CompatibleOuter != nullptr) { MarkGraphForTemplateUse(CompatibleOuter); } } else { CompatibleBlueprint = TargetBlueprint; CompatibleOuter = FindCompatibleGraph(TargetBlueprint, NodeCDO, &BlueprintNodeTemplateCacheImpl::IsTemplateOuter); LogCacheFullMsg(); } } if (CompatibleOuter == nullptr) { CompatibleOuter = AddGraph(CompatibleBlueprint, TargetGraph->Schema); ensureMsgf( CompatibleOuter->Schema != nullptr, TEXT("Invalid schema for template graph (from '%s :: %s')."), *TargetBlueprint->GetName(), *TargetGraph->GetName() ); if (CompatibleBlueprint != TargetBlueprint) { int32 const ApproxGraphSize = ApproximateMemFootprint(CompatibleOuter); ActiveMemFootprint += ApproxGraphSize; ApproximateObjectMem += ApproxGraphSize; } } } if (CompatibleOuter != nullptr) { TemplateNode = NodeSpawner->Invoke(CompatibleOuter, IBlueprintNodeBinder::FBindingSet(), FVector2D::ZeroVector); if (!bIsOverMemCap && !CacheTemplateNode(NodeSpawner, TemplateNode)) { LogCacheFullMsg(); } } } return TemplateNode; } //------------------------------------------------------------------------------ UEdGraphNode* FBlueprintNodeTemplateCache::GetNodeTemplate(UBlueprintNodeSpawner const* NodeSpawner, ENoInit) const { UEdGraphNode* TemplateNode = nullptr; if (auto* FoundNode = NodeTemplateCache.Find(NodeSpawner)) { return *FoundNode; } return nullptr; } //------------------------------------------------------------------------------ void FBlueprintNodeTemplateCache::ClearCachedTemplate(UBlueprintNodeSpawner const* NodeSpawner) { NodeTemplateCache.Remove(NodeSpawner); // GC should take care of the rest } //------------------------------------------------------------------------------ int64 FBlueprintNodeTemplateCache::GetEstimateCacheSize() const { int64 TotalEstimatedSize = ApproximateObjectMem; TotalEstimatedSize += TemplateOuters.GetAllocatedSize(); TotalEstimatedSize += NodeTemplateCache.GetAllocatedSize(); TotalEstimatedSize += sizeof(*this); return TotalEstimatedSize; } //------------------------------------------------------------------------------ int64 FBlueprintNodeTemplateCache::RecalculateCacheSize() { ApproximateObjectMem = 0; for (UBlueprint* Blueprint : TemplateOuters) { // if we didn't run garbage collection at the top, then this could also // account for nodes that were never stored (because the cache was too full) ApproximateObjectMem += BlueprintNodeTemplateCacheImpl::ApproximateMemFootprint(Blueprint); } return ApproximateObjectMem; } //------------------------------------------------------------------------------ bool FBlueprintNodeTemplateCache::IsTemplateOuter(UEdGraph* ParentGraph) { return BlueprintNodeTemplateCacheImpl::IsTemplateOuter(ParentGraph); } //------------------------------------------------------------------------------ void FBlueprintNodeTemplateCache::AddReferencedObjects(FReferenceCollector& Collector) { for (auto& TemplateEntry : NodeTemplateCache) { Collector.AddReferencedObject(TemplateEntry.Value); } Collector.AddReferencedObjects(TemplateOuters); } FString FBlueprintNodeTemplateCache::GetReferencerName() const { return TEXT("FBlueprintNodeTemplateCache"); } //------------------------------------------------------------------------------ bool FBlueprintNodeTemplateCache::CacheBlueprintOuter(UBlueprint* Blueprint) { using namespace BlueprintNodeTemplateCacheImpl; int32 const ApproxBlueprintSize = ApproximateMemFootprint(Blueprint); if (ActiveMemFootprint + ApproxBlueprintSize > GetCacheCapSize()) { return false; } else { ApproximateObjectMem += ApproxBlueprintSize; TemplateOuters.Add(Blueprint); return true; } } //------------------------------------------------------------------------------ bool FBlueprintNodeTemplateCache::CacheTemplateNode(UBlueprintNodeSpawner const* NodeSpawner, UEdGraphNode* NewNode) { using namespace BlueprintNodeTemplateCacheImpl; if (NewNode == nullptr) { return true; } int32 const ApproxNodeSize = BlueprintNodeTemplateCacheImpl::ApproximateMemFootprint(NewNode); if (ActiveMemFootprint + ApproxNodeSize > GetCacheCapSize()) { return false; } else { ApproximateObjectMem += ApproxNodeSize; NodeTemplateCache.Add(NodeSpawner, NewNode); return true; } }