// Copyright Epic Games, Inc. All Rights Reserved. #include "GraphReachability.h" #include "Algo/BinarySearch.h" #include "Algo/IsSorted.h" #include "Algo/Sort.h" #include "Algo/Unique.h" #include "Containers/BinaryHeap.h" #include "Math/NumericLimits.h" #include "Misc/AssertionMacros.h" #include "Misc/AutomationTest.h" #include "Misc/EnumClassFlags.h" #include "SortSpecialCases.h" #if WITH_DEV_AUTOMATION_TESTS #include "Containers/UnrealString.h" #include "Misc/StringBuilder.h" #endif template void ReserveGrowth(ArrayType& Array, NumToAddSizeType NumToAdd) { typename ArrayType::SizeType OriginalCount = Array.Num(); Array.AddUninitialized(NumToAdd); // this will grow the array geometrically using CalculateSlackGrow Array.SetNum(OriginalCount, EAllowShrinking::No); } namespace Algo::Graph { /** Implements ConstructReachabilityGraph. */ class FReachabilityBuilder { public: FReachabilityBuilder(TConstArrayView> InGraph, TArray64& OutReachabilityGraphBuffer, TArray>& OutGraph); /** Calculate ReachabilityGraph for the current Graph, which might be cyclic or acyclic. */ void Build(); private: /** Status for vertices in the graphsearch. InProgress should never be encountered since our graph is acyclic. */ enum class EVertexStatus : uint8 { NotStarted, InProgress, Done, }; /** * A view into an array. Similar to an arrayview, but it is based on a pointer to the array and an index rather * than a pointer to the allocation. This prevents us from having to update it when the array reallocates. */ struct FSliceOfArray { TArray64* Buffer = nullptr; int64 Offset = INDEX_NONE; int32 Num = 0; TConstArrayView GetView() const { return TConstArrayView(Buffer->GetData() + Offset, Num); } void Set(TArray64* InBuffer, int64 InOffset, int32 InNum) { Buffer = InBuffer; Offset = InOffset; Num = InNum; } }; /** * Data about each vertex that lasts beyond the current graph search stack. ReachablesSlice will be volatile during the * search that first finds the vertex but will end up as a slice of the ReachabilityBuffer. */ struct FVertexData { FSliceOfArray ReachablesSlice; EVertexStatus Status = EVertexStatus::NotStarted; }; /** * A buffer that holds the ReachablesSlice for one or more vertices encountered during a graph search stack. It is * formed from a vertex that cannot be embedded into its referencer's reachability slice. When the stack is * emptied, the Buffer is copied onto the reachabilitybuffer after the slices embedded into the reachabilitybuffer * during the search, and the EmbeddedSlices are updated to point to the copy on the reachabilitybuffer. */ struct FSeparateBuffer { TArray64 Buffer; TArray EmbeddedSlices; }; /** * Information about the reachables of direct edges of a vertex on the stack. The reachables of these edges are * embedded into the vertex's reachables during FinishVertex, if possible. */ struct FStackEdgeData { FVertex EdgeVertex = InvalidVertex; FSeparateBuffer EdgeBuffer; bool bEmbeddable = false; }; /** * Information about the reachables of a vertex on the stack, including its direct edge's reachables that need * to be embedded into it. Buffer and EmbeddedSlices are provided by the referencer of the vertex, for it to * store its reachables into so that it can be embedded into its referencer when its turn to be embedded comes. */ struct FStackData { TArray64* Buffer; TArray* EmbeddedSlices; TArray StackEdgeDatas; FVertex Vertex = InvalidVertex; int32 NextEdge = 0; }; private: /** Calculate ReachabilityGraph for the current Graph. Assumes Graph is acyclic. */ void BuildAcyclic(bool bShrink); /** * Push a vertex onto the graph search stack and initialize the data used to calculate its reachables. * Its reachables (and the list of ReachabilitySlices for all vertices using the buffer) will be stored in * Buffer and EmbeddedSlices. */ void StartVertex(FVertex Vertex, TArray64* Buffer, TArray* EmbeddedSlices); /** * Called after all directedges have recursively calculated their reachables (or were previously calculated. * Creates the list of reachables for the vertex, embeds the reachables of all possible direct edges into it, * stores the list in the Buffer that was passed into StartVertex, and records its VertexData's ReachabilitySlice * as (currently) pointing to that Buffer. The ReachabilitySlice will be updated as the Buffer moves around. * Also pops the vertex from the graph search stack. */ void FinishVertex(); /** Called when the input graph is already acyclic. Calculate RootToLeafOrder required by BuildAcyclic. */ void ReadRootToLeafOrderFromInputVertexToGraphVertex(); /** * Called when the input graph has cycles and so BuildAcyclic was called on the CondensationGraph. * Transform the results from the CondensationGraph back into the results for the input graph. */ void TransformCondensationReachabilityToInputReachability(); /** * Append the given SourceBuffer onto the TargetBuffer, and update the EmbeddedSlices that were pointing to the * SourceBuffer to point to their new copy in the TargetBuffer. */ static void RelocateViews(TArray64& InOutTargetBuffer, TArray64& SourceBuffer, TArrayView EmbeddedSlices); private: /** * The graph for which we calculate reachability. Possibly used during the graph search, but we will instead * operate on its CondensationGraph if it has cycles */ TConstArrayView> InputGraph; /** Output buffer for the edges of the reachability graph. We write to it during the graph search. */ TArray64& ReachabilityBuffer; /** * Output list of arrayviews for the edges of the reachabilitygraph. To save memory, these arrayviews will * overlap each other. We calculate it at the end from intermediate data */ TArray>& ReachabilityGraph; /** The graph of edges used in the graph search. Either the InputGraph or its CondensationGraph. */ TConstArrayView> Graph; /** Status and reachables Data about each vertex. Reachables are recorded as a slice into a changeable buffer. */ TArray VertexDatas; /** Information to keep place and calculate reachables for vertices encountered during a search from a root. */ TArray Stack; /** * List of reachables buffers for vertices that were not embeddable during the graph search and need to be added * to reachabilitybuffer afterwards. */ TArray SeparateBuffers; /** RootToLeafOrder of the input vertices. Calculated at the same time as we check for cycles and create the * CondensationGraph. Our algorithm for embedding reachables relies on acyclic graph and RootToLleaf order. */ TArray RootToLeafOrder; /** Buffer for edges for the CondensationGraph. Empty and unused if InputGraph is acyclic. */ TArray64 CondensationGraphBuffer; /** ArrayViews of edges of the CondensationGraph. Empty and unused if InputGraph is acyclic. */ TArray> GraphEdges; /** Buffer for input graph vertices for each condensationgraph vertex. unused if InputGraph is acyclic. */ TArray64 CondensationVertexToInputVerticesBuffer; /** List of input graph vertices for each condensationgraph vertex unused if InputGraph is acyclic. */ TArray> GraphVertexToInputVertices; /** * Map from input vertex to condensationgraph vertex; multiple input vertices can map to the same condensation * vertex. Unused (after creating RootToLeafOrder) if InputGraph is acyclic */ TArray InputVertexToGraphVertex; /** Scratch-space buffer for unioning sets of vertices during FinishVertex. NumVertices array of bools. */ TBitArray<> IsReachable; }; void ConstructReachabilityGraph(TConstArrayView> InGraph, TArray64& OutReachabilityGraphBuffer, TArray>& OutGraph) { FReachabilityBuilder Builder(InGraph, OutReachabilityGraphBuffer, OutGraph); Builder.Build(); } FReachabilityBuilder::FReachabilityBuilder(TConstArrayView> InGraph, TArray64& OutReachabilityGraphBuffer, TArray>& OutGraph) : InputGraph(InGraph) , ReachabilityBuffer(OutReachabilityGraphBuffer) , ReachabilityGraph(OutGraph) { } void FReachabilityBuilder::Build() { // Our compaction algorithm requires that the graph is acyclic, so create the condensation graph, // run the reachability graph on the condensation graph, and then transform the reachability of the // condensation graph into reachability for the input graph if (TryConstructCondensationGraph(InputGraph, CondensationGraphBuffer, GraphEdges, &CondensationVertexToInputVerticesBuffer, &GraphVertexToInputVertices, &InputVertexToGraphVertex)) { Graph = GraphEdges; RootToLeafOrder = Algo::RangeArray>(0, Graph.Num()); BuildAcyclic(false /* bShrink */); TransformCondensationReachabilityToInputReachability(); } else { // If the input graph is acyclic, no transforming is needed, but we do still need a root to leaf // order of the input graph. TryConstructCondensationGraph left it for us in InputVertexToGraphVertex Graph = InputGraph; ReadRootToLeafOrderFromInputVertexToGraphVertex(); BuildAcyclic(true /* bShrink */); } } void FReachabilityBuilder::BuildAcyclic(bool bShrink) { // About compaction: // The reachability graph for N vertices could be as large as NxN, and in practice will often // have size N*N/2. When N is 1 million this is too big to fit into memory in our current machines // which have on the order of 100GB. (Half of million*million = 1TB*BytesPerElement/2). // But reachability graphs have a lot of redundancy and can be compacted; multiple vertices will overlap // each other in the buffer of reachable vertices. // As we search through the graph, we embed the reachables of direct edges into the reachables of the current // vertex when possible. int32 NumVertices = Graph.Num(); Stack.Reset(NumVertices); SeparateBuffers.Reset(NumVertices); VertexDatas.Reset(NumVertices); VertexDatas.SetNum(NumVertices, EAllowShrinking::No); ReachabilityBuffer.Reset(); IsReachable.Init(false, NumVertices); // Start searching at root vertices so we can maximize compaction for (FVertex RootVertex : RootToLeafOrder) { EVertexStatus RootStatus = VertexDatas[RootVertex].Status; check(RootStatus == EVertexStatus::NotStarted || RootStatus == EVertexStatus::Done); if (RootStatus == EVertexStatus::Done) { continue; } // For each new root vertex, graph search from it to calculate its direct edges' reachables, union those // reachables into its own complete list of reachables, and embed the direct edges' reachables into its // bigger list. check(Stack.IsEmpty()); StartVertex(RootVertex, &ReachabilityBuffer, nullptr); while (!Stack.IsEmpty()) { FStackData& StackData = Stack.Last(); FVertex Vertex = StackData.Vertex; TConstArrayView Edges = Graph[Vertex]; bool bPushed = false; while (!bPushed && StackData.NextEdge < Edges.Num()) { int32 NextEdge = StackData.NextEdge++; FVertex EdgeVertex = Edges[NextEdge]; if (EdgeVertex == Vertex) { continue; // Ignore edges to self } FVertexData& EdgeData = VertexDatas[EdgeVertex]; if (EdgeData.Status == EVertexStatus::NotStarted) { FStackEdgeData& StackEdgeData = StackData.StackEdgeDatas[NextEdge]; StartVertex(EdgeVertex, &StackEdgeData.EdgeBuffer.Buffer, &StackEdgeData.EdgeBuffer.EmbeddedSlices); bPushed = true; break; } else { // Our graph is acyclic so we should never encounter an in-progress vertex check(EdgeData.Status == EVertexStatus::Done); } } if (!bPushed) { FinishVertex(); } } // Whenever we are unable to embed a vertex into its parent, we add it as a SeparateBuffer. // Now that the graph search from the root is complete, add all these SeparateBuffers onto // the reachabilitybuffer after the root's reachables. for (FSeparateBuffer& Buffer : SeparateBuffers) { RelocateViews(ReachabilityBuffer, Buffer.Buffer, Buffer.EmbeddedSlices); } SeparateBuffers.Reset(); } check(SeparateBuffers.IsEmpty()); // We're done creating ReachabilityBuffer. All of the vertices have FSliceOfArrays into it to record // their reachables. Shrink it now to save memory, unless we're going to reallocate it anyway later. if (bShrink) { ReachabilityBuffer.Shrink(); } // Convert the FSliceOfArrays into TArrayViews for the output ReachabilityGraph FVertex* ReachabilityData = ReachabilityBuffer.GetData(); ReachabilityGraph.Reset(NumVertices); for (FVertex Vertex = 0; Vertex < NumVertices; ++Vertex) { FVertexData& VertexData = VertexDatas[Vertex]; check(VertexData.Status == EVertexStatus::Done); FSliceOfArray& ReachablesSlice = VertexData.ReachablesSlice; check(ReachablesSlice.Buffer == &ReachabilityBuffer); ReachabilityGraph.Emplace(ReachablesSlice.GetView()); } // Clear memory for the structures we used during the search Stack.Empty(); VertexDatas.Empty(); SeparateBuffers.Empty(); IsReachable.Empty(); } void FReachabilityBuilder::StartVertex(FVertex Vertex, TArray64* Buffer, TArray* EmbeddedSlices) { FStackData* StackDataPointer = Stack.GetData(); FStackData& StackData = Stack.Emplace_GetRef(); check(Stack.GetData() == StackDataPointer); // StackDatas have pointers to TArrays higher on the stack so we cannot let it reallocate StackData.Buffer = Buffer; StackData.EmbeddedSlices = EmbeddedSlices; StackData.Vertex = Vertex; StackData.NextEdge = 0; TConstArrayView Edges = Graph[Vertex]; int32 NumEdges = Edges.Num(); StackData.StackEdgeDatas.Reset(NumEdges); for (int32 EdgeIndex = 0; EdgeIndex < NumEdges; ++EdgeIndex) { FStackEdgeData& StackEdgeData = StackData.StackEdgeDatas.Emplace_GetRef(); StackEdgeData.EdgeVertex = Edges[EdgeIndex]; } FVertexData& VertexData = VertexDatas[Vertex]; VertexData.Status = EVertexStatus::InProgress; }; void FReachabilityBuilder::FinishVertex() { FStackData& StackData = Stack.Last(); FVertex StackVertex = StackData.Vertex; int32 NumVertices = Graph.Num(); int32 NumEdges = StackData.StackEdgeDatas.Num(); // Sort the edges from largest to smallest reachables count; push already allocated to the end // DO NOT move the FStackEdgeDatas around. Some vertices have FSliceOfArrays pointing at TArray // fields on the FStackEdgeData, and these pointers will be left pointing to an array with the // wrong allocation if we move-assign the arrays' allocations. TArray StackEdgeDatas; StackEdgeDatas.Reset(NumEdges); for (FStackEdgeData& StackEdgeData : StackData.StackEdgeDatas) { StackEdgeDatas.Add(&StackEdgeData); } Algo::Sort(StackEdgeDatas, [this, StackVertex](const FStackEdgeData* A, const FStackEdgeData* B) { bool bAIsEmpty = A->EdgeBuffer.Buffer.IsEmpty(); if (bAIsEmpty != B->EdgeBuffer.Buffer.IsEmpty()) { return !bAIsEmpty; } int32 NumA = VertexDatas[A->EdgeVertex].ReachablesSlice.Num; int32 NumB = VertexDatas[B->EdgeVertex].ReachablesSlice.Num; if (NumA != NumB) { return NumA > NumB; } return A->EdgeVertex < B->EdgeVertex; }); // Ignoring edges that were already complete before we entered this vertex, try to embed all of the direct // edges' reachables into this vertex's reachables. We can only embed a set of reachables if it does not // overlap with reachables we have already collected. (So in a diamond graph A->B->D, A->C->D, we will only // be able to embed one of B or C into A). Iterate through direct edges from largest to smallest and greedily // embed the largest direct edge possible until no more are possible. // This is a heavy set of set operations for large vertices; we use the IsReachable array of bools to execute // those operations. An array of bools for all vertices has the down-side of being expensive to clear. To mitigate // that we initialze IsReachable to false for all vertices at the start of BuildAcyclic, and we incrementally clear // it back to false when we're done with it here. // List of leftover vertices to add on to this vertex's reachables from direct edges that we could not embed. TArray NonEmbeddedReachables; int32 NumReachables = 1; // Start at 1 to count this vertex as part of its own reachables for (FStackEdgeData* StackEdgeData : StackEdgeDatas) { FVertex EdgeVertex = StackEdgeData->EdgeVertex; FVertexData& EdgeData = VertexDatas[EdgeVertex]; if (StackEdgeData->EdgeBuffer.Buffer.IsEmpty()) { // The edgevertex was already complete before we entered this vertex, so we can't embed it StackEdgeData->bEmbeddable = false; } else { StackEdgeData->bEmbeddable = true; for (FVertex Vertex : EdgeData.ReachablesSlice.GetView()) { if (IsReachable[Vertex]) { // The edgevertex's reachables overlap with a reachable we already added; we can't embed it StackEdgeData->bEmbeddable = false; break; } } if (StackEdgeData->bEmbeddable) { // The edgevertex is embeddable, and its the biggest, so commit to the embedding now for (FVertex Vertex : EdgeData.ReachablesSlice.GetView()) { IsReachable[Vertex] = true; } NumReachables += EdgeData.ReachablesSlice.Num; } } } // Gather all the unique vertices we don't already have from the non-embeddable edgevertices TArray OverlappedReachables; for (FStackEdgeData* StackEdgeData : StackEdgeDatas) { if (!StackEdgeData->bEmbeddable) { if (StackEdgeData->EdgeVertex == StackVertex) { continue; // Ignore edges to self } FVertex EdgeVertex = StackEdgeData->EdgeVertex; FVertexData& EdgeData = VertexDatas[EdgeVertex]; for (FVertex Vertex : EdgeData.ReachablesSlice.GetView()) { if (!IsReachable[Vertex]) { ++NumReachables; IsReachable[Vertex] = true; NonEmbeddedReachables.Add(Vertex); } } } } // Create our ReachablesSlice to point to the buffer we're about to populate int64 InitialBufferNum = StackData.Buffer->Num(); FVertexData& VertexData = VertexDatas[StackVertex]; VertexData.ReachablesSlice.Set(StackData.Buffer, InitialBufferNum, NumReachables); if (StackData.EmbeddedSlices) { StackData.EmbeddedSlices->Add(&VertexData.ReachablesSlice); } // Add all of our reachables into the buffer, copying the reachables for each embedded // edgevertex into a contiguous range so its ReachablesSlice can overlap our own ReserveGrowth(*StackData.Buffer, NumReachables); StackData.Buffer->Add(StackVertex); for (FStackEdgeData* StackEdgeData : StackEdgeDatas) { FVertex EdgeVertex = StackEdgeData->EdgeVertex; FVertexData& EdgeData = VertexDatas[EdgeVertex]; if (StackEdgeData->bEmbeddable) { // Copies from the edgevertex's buffer into ours RelocateViews(*StackData.Buffer, StackEdgeData->EdgeBuffer.Buffer, StackEdgeData->EdgeBuffer.EmbeddedSlices); if (StackData.EmbeddedSlices) { StackData.EmbeddedSlices->Append(StackEdgeData->EdgeBuffer.EmbeddedSlices); } } else if (!StackEdgeData->EdgeBuffer.Buffer.IsEmpty()) { // For all of the edgevertices we recursively calculated but cannot embed, add them as separatebuffers to // copy onto reachabilitybuffer after the graph search stack empties. FSeparateBuffer* SeparateBuffersData = SeparateBuffers.GetData(); SeparateBuffers.Add(MoveTemp(StackEdgeData->EdgeBuffer)); check(SeparateBuffers.GetData() == SeparateBuffersData); FSeparateBuffer& SeparateBuffer = SeparateBuffers.Last(); for (FSliceOfArray* EmbeddedSlice : SeparateBuffer.EmbeddedSlices) { EmbeddedSlice->Buffer = &SeparateBuffer.Buffer; } } } StackData.Buffer->Append(NonEmbeddedReachables); check(StackData.Buffer->Num() == InitialBufferNum + NumReachables); // Fast-clear IsReachable, by individually clearing all of the values we set to true constexpr int32 FastClearFraction = 10; if (((int64)NumReachables) * FastClearFraction > NumVertices) { // In this case we touched so much of IsReachable that it's faster to bulk-clear all of it IsReachable.Init(false, NumVertices); } else { for (FVertex Vertex : VertexData.ReachablesSlice.GetView()) { IsReachable[Vertex] = false; } } // Mark our status complete and pop the stack VertexData.Status = EVertexStatus::Done; FStackData* StackDataPointer = Stack.GetData(); Stack.Pop(EAllowShrinking::No); // StackDatas have pointers to TArrays higher on the stack so we cannot let it reallocate check(Stack.GetData() == StackDataPointer); } void FReachabilityBuilder::TransformCondensationReachabilityToInputReachability() { // We walk through all CondensationGraph vertices in the ReachabilityBuffer, and replace them with their single or // multiple InputGraph vertices from GraphVertexToInputVertices. // Whenever we insert vertices in the ReachabilityGraph instead of merely replacing them, we have to update all of // the affected TArrayViews: any TArrayViews that started after that spot have to be shifted higher up the buffer, // and any TArrayViews that overlapped the insertion spot have to have their Num increased. // We handle the shifting by sorting the arrayviews by start point and keeping track of the cumulative increase // as we reach the startpoint of each arrayview. We handle the num growing by keeping a heap of arrayviews that // have started but not yet finished at our current position (heap-sorted by their ending position so we can pop // them off as we move along the buffer). // Sort the ArrayViews into ViewsSortedByStart int32 NumVertices = ReachabilityGraph.Num(); TArray*> ViewsSortedByStart; ViewsSortedByStart.Reserve(NumVertices); for (FVertex Vertex = 0; Vertex < NumVertices; ++Vertex) { ViewsSortedByStart.Add(&ReachabilityGraph[Vertex]); } Algo::Sort(ViewsSortedByStart, [](TConstArrayView* A, TConstArrayView* B) { if (A->GetData() != B->GetData()) { return A->GetData() < B->GetData(); } return A->Num() > B->Num(); }); int64 AddedSize = 0; for (FVertex Vertex : ReachabilityBuffer) { AddedSize += GraphVertexToInputVertices[Vertex].Num() - 1; } // The new version of ReachabilityBuffer with all of the CondensationGraph verts converted to InputGraph verts TArray64 CopyBuffer; CopyBuffer.Reserve(AddedSize + ReachabilityBuffer.Num()); // Start and end of the source and copy buffers' allocations FVertex* SourceData = ReachabilityBuffer.GetData(); FVertex* SourceDataEnd = SourceData + ReachabilityBuffer.Num(); FVertex* CopyData = CopyBuffer.GetData(); // The next ArrayView from ViewsSortedByStart that we need to add when reaching its start position int32 NextIndexInSortedViews = 0; // The heap of ArrayViews that overlap the current position. The arrayview that ends next is at top of heap FBinaryHeap ActiveViews; // An array NumVertices in size that stores the CumulativeGrowth that was present when we reached the start of the // vertex's ArrayView. Element n corresponds to the vertex in ViewsSortedByStat[n]. We use the delta between // CumulativeGrowth from start to end of the ArrayView into the CondensationGraph's buffer to to detect how much // the ArrayView's Num needs to grow for the InputGraph's version of the ArrayView. TArray CumulativeGrowthWhenAdded; CumulativeGrowthWhenAdded.SetNumUninitialized(NumVertices); int64 CumulativeGrowth = 0; for (FVertex* NextVertex = SourceData; ; ++NextVertex) { // Remove any active ArrayViews that reached the end of their view in the source data, and update their Num while (!ActiveViews.IsEmpty()) { uint32 IndexInSortedViewsUnsigned = ActiveViews.Top(); const FVertex* SourceViewEnd = ActiveViews.GetKey(IndexInSortedViewsUnsigned); if (SourceViewEnd != NextVertex) { check(SourceViewEnd > NextVertex); break; } int32 IndexInSortedViews = static_cast(IndexInSortedViewsUnsigned); TConstArrayView& View = *ViewsSortedByStart[IndexInSortedViews]; View = TConstArrayView(View.GetData(), View.Num() + CumulativeGrowth - CumulativeGrowthWhenAdded[IndexInSortedViews]); ActiveViews.Pop(); } // write the for loop termination condition here so we can break after finalizing arrayviews at end of buffer if (NextVertex == SourceDataEnd) { break; } // Add any new ArrayViews from ViewsSortedByStart that start at the current position in SourceData while (NextIndexInSortedViews < ViewsSortedByStart.Num() && ViewsSortedByStart[NextIndexInSortedViews]->GetData() == NextVertex) { TConstArrayView& View = *ViewsSortedByStart[NextIndexInSortedViews]; CumulativeGrowthWhenAdded[NextIndexInSortedViews] = CumulativeGrowth; const FVertex* ViewSourceData = View.GetData(); int32 ViewNum = View.Num(); const FVertex* ViewEndData = ViewSourceData + ViewNum; View = TConstArrayView(CopyData + (ViewSourceData - SourceData) + CumulativeGrowth, ViewNum); ActiveViews.Add(ViewEndData, static_cast(NextIndexInSortedViews)); ++NextIndexInSortedViews; } check(NextIndexInSortedViews == ViewsSortedByStart.Num() || ViewsSortedByStart[NextIndexInSortedViews]->GetData() > SourceData); // Convert the CondensationGraph vertex to 1 or more Input vertices and copy them onto the CopyBuffer TConstArrayView& OutVertices = GraphVertexToInputVertices[*NextVertex]; CopyBuffer.Append(OutVertices); CumulativeGrowth += OutVertices.Num() - 1; } // Swap the new buffer into our output variable ReachabilityBuffer = MoveTemp(CopyBuffer); check(ReachabilityBuffer.GetData() == CopyData); // Phase two: we've updated the data in the buffer, but now we need to update which vertices point to which // ranges of it. This phase is much easier since we just need to copy ArrayViews without modifying them. TArray> InputReachabilityGraph; int32 NumInputVertices = InputGraph.Num(); InputReachabilityGraph.Reserve(NumInputVertices); for (FVertex InputVertex = 0; InputVertex < NumInputVertices; ++InputVertex) { FVertex GraphVertex = InputVertexToGraphVertex[InputVertex]; InputReachabilityGraph.Add(ReachabilityGraph[GraphVertex]); } // Swap the new graph into our output variable ReachabilityGraph = MoveTemp(InputReachabilityGraph); } void FReachabilityBuilder::ReadRootToLeafOrderFromInputVertexToGraphVertex() { int32 NumVertices = Graph.Num(); RootToLeafOrder.SetNumUninitialized(NumVertices); for (FVertex& Vertex : RootToLeafOrder) { Vertex = InvalidVertex; } check(InputVertexToGraphVertex.Num() == NumVertices); for (FVertex UnsortedVertex = 0; UnsortedVertex < NumVertices; ++UnsortedVertex) { int32 SortedIndex = (int32)InputVertexToGraphVertex[UnsortedVertex]; check(SortedIndex < NumVertices && RootToLeafOrder[SortedIndex] == InvalidVertex); RootToLeafOrder[SortedIndex] = UnsortedVertex; } } void FReachabilityBuilder::RelocateViews(TArray64& InOutTargetBuffer, TArray64& SourceBuffer, TArrayView EmbeddedSlices) { int64 InitialOffset = InOutTargetBuffer.Num(); InOutTargetBuffer.Append(SourceBuffer); for (FSliceOfArray* EmbeddedSlize : EmbeddedSlices) { check(EmbeddedSlize->Buffer == &SourceBuffer); EmbeddedSlize->Buffer = &InOutTargetBuffer; EmbeddedSlize->Offset += InitialOffset; } } } // namespace Algo::Graph #if WITH_DEV_AUTOMATION_TESTS IMPLEMENT_SIMPLE_AUTOMATION_TEST(FReachabilityTest, "System.Core.Algo.ReachabilityGraph", EAutomationTestFlags_ApplicationContextMask | EAutomationTestFlags::EngineFilter); bool FReachabilityTest::RunTest(const FString& Parameters) { using namespace Algo::Graph; TArray> Buffer; TArray> Graph; TArray64 ReachabilityBuffer; TArray> Reachability; TArray> ExpectedReachability; auto AddVertex = [&Buffer, &Graph, &ExpectedReachability](FVertex Vertex, TConstArrayView Edges, TConstArrayView ExpectedVertexReachability) { if (Graph.Num() <= Vertex) { Graph.SetNum(Vertex + 1); Buffer.SetNum(Vertex + 1); ExpectedReachability.SetNum(Vertex + 1); } Buffer[Vertex] = Edges; Graph[Vertex] = Buffer[Vertex]; ExpectedReachability[Vertex] = ExpectedVertexReachability; }; auto Clear = [&Buffer, &Graph, &ExpectedReachability]() { Buffer.Reset(); Graph.Reset(); ExpectedReachability.Reset(); }; auto WriteArrayToString = [](TConstArrayView A) { TStringBuilder<256> Writer; Writer << TEXT("["); Writer.Join(A, TEXT(',')); Writer << TEXT("]"); return FString(Writer); }; auto ConfirmResults = [&Reachability, &ExpectedReachability, &WriteArrayToString](FString& OutError) { int32 NumVertices = Reachability.Num(); if (NumVertices != ExpectedReachability.Num()) { OutError = FString::Printf(TEXT("reachability graph has incorrect number of vertices, Expected=%d, Actual=%d"), ExpectedReachability.Num(), NumVertices); return false; } for (int32 Vertex = 0; Vertex < NumVertices; ++Vertex) { TArray Expected = ExpectedReachability[Vertex]; TArray Actual(Reachability[Vertex]); Algo::Sort(Expected); Algo::Sort(Actual); if (Expected != Actual) { OutError = FString::Printf(TEXT("reachability for vertex %d does not match. Expected=%s, Actual=%s"), Vertex, *WriteArrayToString(Expected), *WriteArrayToString(Actual)); return false; } } return true; }; auto RunTrialsForCase = [&ConfirmResults, &Graph, &ReachabilityBuffer, &Reachability, this](const TCHAR* TestCaseName) { FString Error; { FReachabilityBuilder Builder(Graph, ReachabilityBuffer, Reachability); Builder.Build(); } if (!ConfirmResults(Error)) { AddError(FString::Printf(TEXT("Search failed for case \"%s\": %s."), TestCaseName, *Error)); } }; { Clear(); AddVertex(0, { }, { 0 }); AddVertex(1, { 0 }, { 0,1 }); AddVertex(2, { 1 }, { 0,1,2 }); RunTrialsForCase(TEXT("Each node depends on the previous one")); } { Clear(); AddVertex(0, { 1 }, {0,1,2}); AddVertex(1, { 2 }, { 1,2 }); AddVertex(2, { }, { 2 }); RunTrialsForCase(TEXT("Each node depends on the next one")); } { Clear(); AddVertex(0, { 0 }, { 0 }); AddVertex(1, { 0,1 }, { 0,1 }); AddVertex(2, { 1,2 }, { 0,1,2 }); RunTrialsForCase(TEXT("SelfReferences")); } { Clear(); AddVertex(0, { 1 }, { 0,1 }); AddVertex(1, { 0 }, { 0,1 }); RunTrialsForCase(TEXT("Simple cycle")); } { Clear(); for (FVertex Vertex = 0; Vertex < 10; ++Vertex) { if (Vertex != 5) { AddVertex(Vertex, { 5 }, { Vertex, 5 }); } } AddVertex(5, { }, { 5 }); RunTrialsForCase(TEXT("Every node has a dependency on a single node")); } { // 6 // / \ // 5 7 // / \ \ // 0 1 8 // \ / \ \ // 3 4 | // \ \ / // \ 9 // \ / // 2 Clear(); AddVertex(6, { 5,7 }, { 0,1,2,3,4,5,6,7,8,9 }); AddVertex(5, { 0,1 }, { 5,0,1,3,4,9,2 }); AddVertex(7, { 8 }, { 7,8,9,2 }); AddVertex(0, { 3 }, { 0,3,2 }); AddVertex(1, { 3,4 }, { 1,3,4,9,2 }); AddVertex(8, { 9 }, { 8,9,2 }); AddVertex(3, { 2 }, { 3,2 }); AddVertex(4, { 9 }, { 4,9,2 }); AddVertex(9, { 2 }, { 2,9 }); AddVertex(2, { }, { 2 }); RunTrialsForCase(TEXT("SketchedOutExample1")); } { // 0 // 1 2 // 3 4 5 6 // 7 8 9 10 11 12 13 14 Clear(); AddVertex(0, { 1,2 }, { 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14 }); AddVertex(1, { 3,4 }, { 1,3,4,7,8,9,10 }); AddVertex(2, { 5,6 }, { 2,5,6,11,12,13,14 }); AddVertex(3, { 7,8 }, { 3,7,8 }); AddVertex(4, { 9,10 }, { 4,9,10 }); AddVertex(5, { 11,12 }, { 5,11,12 }); AddVertex(6, { 13,14 }, { 6,13,14 }); for (FVertex Vertex = 7; Vertex <= 14; ++Vertex) { AddVertex(Vertex, { }, { Vertex }); } RunTrialsForCase(TEXT("BinaryTree")); } { // 0 // 1 2 // 3 4 5 6 // 3 through 6 have edges to every vertex in 7 through 14 // 7 8 9 10 11 12 13 14 Clear(); AddVertex(0, { 1,2 }, { 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14 }); AddVertex(1, { 3,4 }, { 1,3,4,7,8,9,10,11,12,13,14 }); AddVertex(2, { 5,6 }, { 2,5,6,7,8,9,10,11,12,13,14 }); AddVertex(3, { 7,8,9,10,11,12,13,14 }, { 3,7,8,9,10,11,12,13,14 }); AddVertex(4, { 7,8,9,10,11,12,13,14 }, { 4,7,8,9,10,11,12,13,14 }); AddVertex(5, { 7,8,9,10,11,12,13,14 }, { 5,7,8,9,10,11,12,13,14 }); AddVertex(6, { 7,8,9,10,11,12,13,14 }, { 6,7,8,9,10,11,12,13,14 }); for (FVertex Vertex = 7; Vertex <= 14; ++Vertex) { AddVertex(Vertex, { }, { Vertex }); } RunTrialsForCase(TEXT("BinaryTreeWithEverythingFromRow2ToRow3")); } { Clear(); AddVertex(0, { 1 }, { 0,1,2,3 }); AddVertex(1, { 0,2 }, { 0,1,2,3 }); AddVertex(2, { 3 }, { 2,3 }); AddVertex(3, { }, { 3 }); RunTrialsForCase(TEXT("Cycle in the root and with the root cycle depending on a chain of non-cycle verts")); } { Clear(); AddVertex(0, { }, { 0 }); AddVertex(1, { 0 }, { 0,1 }); AddVertex(2, { 1,3 }, { 0,1,2,3 }); AddVertex(3, { 2 }, { 0,1,2,3 }); RunTrialsForCase(TEXT("Cycle in the root and with the root cycle depending on a chain of non-cycle verts, submitted in reverse")); } { Clear(); AddVertex(0, { 1 }, { 0,1,2,3 }); AddVertex(1, { 2 }, { 1,2,3 }); AddVertex(2, { 3 }, { 2,3 }); AddVertex(3, { 2 }, { 2,3 }); RunTrialsForCase(TEXT("Cycle at a leaf and a chain from the root depending on that cycle")); } { Clear(); AddVertex(0, { 1 }, { 0,1 }); AddVertex(1, { 0 }, { 0,1 }); AddVertex(2, { 1 }, { 2,1,0 }); AddVertex(3, { 2 }, { 3,2,1,0 }); RunTrialsForCase(TEXT("Cycle in the root and with the root cycle depending on a chain of non-cycle verts")); } { Clear(); AddVertex(0, { 1 }, { 0,1,2,3 }); AddVertex(1, { 2,3 }, { 1,2,3 }); AddVertex(2, { 1,3 }, { 1,2,3 }); AddVertex(3, { 1,2 }, { 1,2,3 }); RunTrialsForCase(TEXT("Vertex dependent upon a cycle")); } { // 0 -> 1 -> 2 -> 3 -> 1 // | // v // 4 -> 5 -> 6 -> 4 Clear(); AddVertex(0, { 1 }, { 0,1,2,3,4,5,6 }); AddVertex(1, { 2 }, { 1,2,3,4,5,6 }); AddVertex(2, { 3,5 }, { 1,2,3,4,5,6 }); AddVertex(3, { 1 }, { 1,2,3,4,5,6 }); AddVertex(4, { 5 }, { 4,5,6 }); AddVertex(5, { 6 }, { 4,5,6 }); AddVertex(6, { 4 }, { 4,5,6 }); RunTrialsForCase(TEXT("One cycle dependent upon another")); } { // 5 -> 0 -> (1 -> 2 -> 1) // | | // | v // | 5 // v // (3 -> 4 -> 3) Clear(); AddVertex(0, { 1,3 }, { 0,1,2,3,4,5 }); AddVertex(1, { 2 }, { 0,1,2,3,4,5 }); AddVertex(2, { 1,5 }, { 0,1,2,3,4,5 }); AddVertex(3, { 4 }, { 3,4 }); AddVertex(4, { 3 }, { 3,4 }); AddVertex(5, { 0 }, { 0,1,2,3,4,5 }); RunTrialsForCase(TEXT("MutuallyReachableSet Problem1")); } { Clear(); TArray All; TArray AllButItself; for (FVertex Vertex = 0; Vertex < 6; ++Vertex) { All.Add(Vertex); AllButItself.Add(Vertex); } for (FVertex Vertex = 0; Vertex < 6; ++Vertex) { AllButItself.Remove(Vertex); AddVertex(Vertex, AllButItself, All); AllButItself.Add(Vertex); } RunTrialsForCase(TEXT("FullyConnectedGraph")); } return true; } #endif