// Copyright Epic Games, Inc. All Rights Reserved. #include "ConstraintsEvaluationGraph.h" #include "ConstraintsManager.h" #include "ConstraintSubsystem.h" #include "Algo/TopologicalSort.h" namespace ConstraintsEvaluationGraph { static bool bUseEvaluationGraph = true; static FAutoConsoleVariableRef CVarUseEvaluationGraph( TEXT("Constraints.UseEvaluationGraph"), bUseEvaluationGraph, TEXT("Use Evaluation Graph to update constraints when manipulating.") ); static bool bDebugGraph = false; static FAutoConsoleVariableRef CVarDebugEvaluationGraph( TEXT("Constraints.DebugEvaluationGraph"), bDebugGraph, TEXT("Print debug info about constraints evaluation graph.") ); } bool FConstraintsEvaluationGraph::UseEvaluationGraph() { return ConstraintsEvaluationGraph::bUseEvaluationGraph; } FConstraintNode& FConstraintsEvaluationGraph::GetNode(const TWeakObjectPtr& InConstraint) { const int32 Found = Nodes.IndexOfByPredicate([InConstraint](const FConstraintNode& Node) { return InConstraint.IsValid() && Node.ConstraintID == InConstraint->ConstraintID; }); if (Found != INDEX_NONE) { return Nodes[Found]; } FConstraintNode Node; Node.ConstraintID = InConstraint->ConstraintID; Node.ConstraintTick = &InConstraint->GetTickFunction(ConstraintsInWorld.World.Get()); return Nodes.Emplace_GetRef(MoveTemp(Node)); } FConstraintNode* FConstraintsEvaluationGraph::FindNode(const TWeakObjectPtr& InConstraint) { if (!InConstraint.IsValid()) { return nullptr; } return Nodes.FindByPredicate([InConstraint](const FConstraintNode& Node) { return InConstraint.IsValid() && Node.ConstraintID == InConstraint->ConstraintID; }); } void FConstraintsEvaluationGraph::FlushPendingEvaluations() { if (State == InvalidData || State == Flushing) { return; } if (Nodes.IsEmpty()) { return; } if (ConstraintsEvaluationGraph::bDebugGraph) { UE_LOG(LogTemp, Warning, TEXT("Flush Constraints Evaluation Graph")); } State = Flushing; for (FConstraintNode& Node: Nodes) { if (Node.bMarkedForEvaluation) { Evaluate(&Node); } } const bool bHasNodesToEvaluate = Nodes.ContainsByPredicate([](const FConstraintNode& Node) { return Node.bMarkedForEvaluation; }); ensure(!bHasNodesToEvaluate); State = ReadyForEvaluation; } void FConstraintsEvaluationGraph::Rebuild() { Nodes.Empty(); if (!ensure(ConstraintsInWorld.World.Get())) { return; } const TArray>& Constraints = ConstraintsInWorld.Constraints; if (Constraints.IsEmpty()) { return; } UE::Constraints::Graph::BuildGraph(ConstraintsInWorld.World.Get(), Constraints, Nodes); State = ReadyForEvaluation; Dump(); } bool FConstraintsEvaluationGraph::GetSortedConstraints(TArray>& OutConstraints) { OutConstraints.Reset(); if (!ensure(ConstraintsInWorld.World.Get())) { return false; } if (State == InvalidData) { Rebuild(); } if (Nodes.IsEmpty()) { return false; } using ConstraintPtr = TWeakObjectPtr; const TArray& Constraints = ConstraintsInWorld.Constraints; OutConstraints.Reserve(Nodes.Num()); for (const FConstraintNode& Node: Nodes) { const ConstraintPtr Constraint = Constraints[Node.ConstraintIndex]; if (Constraint.IsValid()) { OutConstraints.Add(Constraint); } } ensure(OutConstraints.Num() == Constraints.Num()); return true; } bool FConstraintsEvaluationGraph::IsPendingEvaluation() const { return State == PendingEvaluation; } void FConstraintsEvaluationGraph::Evaluate(const TWeakObjectPtr& InConstraint) { if (State == InvalidData) { Rebuild(); } if (Nodes.IsEmpty()) { return; } if (FConstraintNode* Node = FindNode(InConstraint)) { Evaluate(Node); } } void FConstraintsEvaluationGraph::Evaluate(FConstraintNode* InNode) { const TArray>& Constraints = ConstraintsInWorld.Constraints; if (!Constraints.IsValidIndex(InNode->ConstraintIndex)) { return; } const TWeakObjectPtr Constraint = Constraints[InNode->ConstraintIndex]; if (!Constraint.IsValid()) { return; } if (InNode->bEvaluating) { return; } // evaluate TGuardValue ReentrantGuard(InNode->bEvaluating, true); if (Constraint->IsFullyActive() && InNode->ConstraintTick->IsTickFunctionRegistered() && InNode->ConstraintTick->IsTickFunctionEnabled()) { Constraint->Evaluate(); } InNode->bMarkedForEvaluation = false; // evaluate dependencies for (const uint32 ChildIndex: InNode->Children) { if (ensure(Nodes.IsValidIndex(ChildIndex))) { Evaluate(&Nodes[ChildIndex]); } } } void FConstraintsEvaluationGraph::InvalidateData() { State = InvalidData; Nodes.Empty(); } void FConstraintsEvaluationGraph::MarkForEvaluation(const TWeakObjectPtr& InConstraint) { if (State == InvalidData) { Rebuild(); } if (State == Flushing) { // do not mark this constraint for evaluation while flushing. // this can happen with UControlRig::OnControlModified being called while evaluating additive rigs return; } if (FConstraintNode* Node = FindNode(InConstraint)) { auto GetConstraintLabel = [](const TWeakObjectPtr& InConstraint) { #if WITH_EDITOR return InConstraint->GetFullLabel(); #else return InConstraint->GetName(); #endif }; if (ConstraintsEvaluationGraph::bDebugGraph) { UE_LOG(LogTemp, Warning, TEXT("Mark %s For Evaluation"), *GetConstraintLabel(InConstraint)); } Node->bMarkedForEvaluation = true; if (State == ReadyForEvaluation) { State = PendingEvaluation; } } } void FConstraintsEvaluationGraph::Dump() const { if (!ConstraintsEvaluationGraph::bDebugGraph) { return; } auto GetConstraintLabel = [](const TWeakObjectPtr& InConstraint) { #if WITH_EDITOR return InConstraint->GetFullLabel(); #else return InConstraint->GetName(); #endif }; const TArray>& Constraints = ConstraintsInWorld.Constraints; UE_LOG(LogTemp, Warning, TEXT("Nb Constraints = %d"), Constraints.Num()); for (int32 ConstraintIndex = 0; ConstraintIndex < Constraints.Num(); ConstraintIndex++) { if (Constraints[ConstraintIndex].IsValid()) { UE_LOG(LogTemp, Warning, TEXT("\tConstraint[%d] = %s"), ConstraintIndex, *GetConstraintLabel(Constraints[ConstraintIndex])); } } UE_LOG(LogTemp, Warning, TEXT("Nb Nodes = %d"), Nodes.Num()); for (int32 NodeIndex = 0; NodeIndex < Nodes.Num(); NodeIndex++) { const FConstraintNode& Node = Nodes[NodeIndex]; ensure(Constraints.IsValidIndex(Node.ConstraintIndex)); ensure(Node.NodeIndex == NodeIndex); UE_LOG(LogTemp, Warning, TEXT("\tNode[%d] = %s [%d]"), NodeIndex, *GetConstraintLabel(Constraints[Node.ConstraintIndex]), Node.ConstraintIndex); for (const uint32 ChildIndex: Node.Children) { if (ensure(Nodes.IsValidIndex(ChildIndex))) { const FConstraintNode& ChildNode = Nodes[ChildIndex]; UE_LOG(LogTemp, Warning, TEXT("\t\tChild[%d] = %s [%d]"), ChildIndex, *GetConstraintLabel(Constraints[ChildNode.ConstraintIndex]), ChildNode.ConstraintIndex); } } } } namespace UE::Constraints::Graph::Private { struct FGraphBuildHelper { FGraphBuildHelper(UWorld* InWorld, const TArrayView& InConstraints, TArray& OutNodes) : World(InWorld) , Constraints(InConstraints) , Nodes(OutNodes) {} void Build() { Nodes.Reset(); if (!World || Constraints.IsEmpty()) { return; } BuildNodes(); BuildNodeDependencies(); SortNodes(); } private: UWorld* World = nullptr; const TArrayView& Constraints; TArray& Nodes; FConstraintNode& GetNode(const ConstraintPtr& InConstraint) const { const int32 Found = Nodes.IndexOfByPredicate([InConstraint](const FConstraintNode& Node) { return InConstraint.IsValid() && Node.ConstraintID == InConstraint->ConstraintID; }); if (Found != INDEX_NONE) { return Nodes[Found]; } FConstraintNode Node; Node.ConstraintID = InConstraint->ConstraintID; Node.ConstraintTick = &InConstraint->GetTickFunction(World); return Nodes.Emplace_GetRef(MoveTemp(Node)); } // build vertices void BuildNodes() const { for (int32 ConstraintIndex = 0, NumConstraints = Constraints.Num(), NodeIndex = 0; ConstraintIndex < NumConstraints; ConstraintIndex++) { if (Constraints[ConstraintIndex].IsValid()) { FConstraintNode& Node = GetNode(Constraints[ConstraintIndex]); Node.NodeIndex = NodeIndex++; Node.ConstraintIndex = ConstraintIndex; } } } // build edges void BuildNodeDependencies() const { for (FConstraintNode& Node: Nodes) { const TArray& Prerequisites = Node.ConstraintTick->GetPrerequisites(); for (const FTickPrerequisite& Prerex: Prerequisites) { if (const FTickFunction* PrerexFunction = Prerex.Get()) { FConstraintNode* PrerexNode = Nodes.FindByPredicate([PrerexFunction, Node](const FConstraintNode& OtherNode) { if (OtherNode.NodeIndex != Node.NodeIndex) { if (OtherNode.ConstraintTick == PrerexFunction) { return true; } } return false; }); if (PrerexNode) { if (!PrerexNode->Parents.Contains(Node.NodeIndex)) { Node.Parents.Add(PrerexNode->NodeIndex); } else { // we may have create a cycle ensure(false); } if (!Node.Children.Contains(PrerexNode->NodeIndex)) { PrerexNode->Children.Add(Node.NodeIndex); } else { // we may have create a cycle ensure(false); } } } } } } // sort using dependencies void SortNodes() const { const int32 NumNodes = Nodes.Num(); // switch to Indices TArray Indices; Indices.Reserve(NumNodes); Algo::Transform(Nodes, Indices, [](const FConstraintNode& Node) { return Node.NodeIndex; }); // store copy const TArray IndicesBeforeSort(Indices); const TArray NodesBeforeSort(Nodes); // try topological sort auto GetDependencies = [this](const int32 Index) { return Nodes[Index].Parents.Array(); }; const bool bSucceeded = Algo::TopologicalSort(Indices, GetDependencies); TMap OldToNewIndex; if (ensure(bSucceeded)) { // store a old to new index map to re-index things out bool bHasBeenReordered = false; for (int32 Index = 0; Index < NumNodes; ++Index) { const int32 OldIndex = IndicesBeforeSort.IndexOfByKey(Indices[Index]); OldToNewIndex.Emplace(OldIndex, Index); bHasBeenReordered |= OldIndex != Index; } // early exits if it hasn't been reordered if (!bHasBeenReordered) { if (ConstraintsEvaluationGraph::bDebugGraph) { UE_LOG(LogTemp, Warning, TEXT("No need to re-index constraints.")); } return; } // switch back to Nodes for (int32 Index = 0; Index < NumNodes; ++Index) { const int32 NewIndex = Indices[Index]; Nodes[Index] = NodesBeforeSort[NewIndex]; } } else { // if it failed (mostly due to cycles) return to the basic Algo::Sort to still trying to sort things out. auto Predicate = [](const FConstraintNode& LHS, const FConstraintNode& RHS) { const FConstraintTickFunction* LHSTickFunction = LHS.ConstraintTick; const FConstraintTickFunction* RHSTickFunction = RHS.ConstraintTick; if (!LHSTickFunction || !RHSTickFunction) { return LHS.ConstraintIndex < RHS.ConstraintIndex; } const TArray& RHSPrerex = RHSTickFunction->GetPrerequisites(); const bool bIsLHSAPrerexOfRHS = RHSPrerex.ContainsByPredicate([LHSTickFunction](const FTickPrerequisite& Prerex) { return Prerex.PrerequisiteTickFunction == LHSTickFunction; }); if (bIsLHSAPrerexOfRHS) { return true; } const TArray& LHSPrerex = LHSTickFunction->GetPrerequisites(); const bool bIsRHSAPrerexOfLHS = LHSPrerex.ContainsByPredicate([RHSTickFunction](const FTickPrerequisite& Prerex) { return Prerex.PrerequisiteTickFunction == RHSTickFunction; }); if (bIsRHSAPrerexOfLHS) { return false; } // if not a prerequisite then compare constraints indices return LHS.ConstraintIndex < RHS.ConstraintIndex; }; Algo::Sort(Nodes, Predicate); // store old to new index map to re-index things out bool bHasBeenReordered = false; for (int32 Index = 0; Index < NumNodes; ++Index) { const int32 OldIndex = IndicesBeforeSort.IndexOfByKey(Nodes[Index].NodeIndex); OldToNewIndex.Emplace(OldIndex, Index); bHasBeenReordered |= OldIndex != Index; } // early exits if it hasn't been reordered if (!bHasBeenReordered) { if (ConstraintsEvaluationGraph::bDebugGraph) { UE_LOG(LogTemp, Warning, TEXT("No need to re-index constraints.")); } return; } } if (ensure(OldToNewIndex.Num() == NumNodes)) { // reindex node + parents + children for (int32 Index = 0; Index < NumNodes; ++Index) { FConstraintNode& Node = Nodes[Index]; ReIndexNode(Node, OldToNewIndex); } } } static void ReIndexNode(FConstraintNode& InOutNode, const TMap& InOldToNewIndex) { // update node + NodeIndex InOutNode.NodeIndex = InOldToNewIndex[InOutNode.NodeIndex]; // update parents indices if (!InOutNode.Parents.IsEmpty()) { TSet NewParents; NewParents.Reserve(InOutNode.Parents.Num()); for (const int32 OldParentIndex: InOutNode.Parents) { NewParents.Add(InOldToNewIndex[OldParentIndex]); } InOutNode.Parents = MoveTemp(NewParents); } // update children indices if (!InOutNode.Children.IsEmpty()) { TSet NewChildren; NewChildren.Reserve(InOutNode.Children.Num()); for (const int32 OldChildIndex: InOutNode.Children) { NewChildren.Add(InOldToNewIndex[OldChildIndex]); } InOutNode.Children = MoveTemp(NewChildren); } } }; } namespace UE::Constraints::Graph { void BuildGraph(UWorld* InWorld, const TArrayView& InConstraints, TArray& OutNodes) { OutNodes.Empty(); if (!ensure(InWorld)) { return; } if (InConstraints.IsEmpty()) { return; } Private::FGraphBuildHelper BuildHelper(InWorld, InConstraints, OutNodes); BuildHelper.Build(); } void SortConstraints(UWorld* InWorld, TArray& InOutConstraints) { // build graph TArray Nodes; BuildGraph(InWorld, InOutConstraints, Nodes); if (!Nodes.IsEmpty()) { // re-order constraints const TArray ConstraintsCopy(InOutConstraints); InOutConstraints.Reset(Nodes.Num()); for (int32 Index = 0, NumNodes = Nodes.Num(); Index < NumNodes; ++Index) { const int32 ConstraintIndex = Nodes[Index].ConstraintIndex; if (ConstraintsCopy.IsValidIndex(ConstraintIndex)) { InOutConstraints.Add(ConstraintsCopy[ConstraintIndex]); } } } } }