Files
UnrealEngine/Engine/Source/Runtime/MassEntity/Public/MassProcessorDependencySolver.h
2025-05-18 13:04:45 +08:00

244 lines
9.0 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "MassProcessingTypes.h"
#include "MassEntityTypes.h"
#include "MassArchetypeTypes.h"
#include "Containers/StaticArray.h"
#define UE_API MASSENTITY_API
class UMassProcessor;
namespace UE::Mass
{
struct FTypeManager;
}
namespace EMassAccessOperation
{
constexpr uint32 Read = 0;
constexpr uint32 Write = 1;
constexpr uint32 MAX = 2;
};
template<typename T>
struct TMassExecutionAccess
{
T Read;
T Write;
T& operator[](const uint32 OpIndex)
{
check(OpIndex <= EMassAccessOperation::MAX);
return OpIndex == EMassAccessOperation::Read ? Read : Write;
}
const T& operator[](const uint32 OpIndex) const
{
check(OpIndex <= EMassAccessOperation::MAX);
return OpIndex == EMassAccessOperation::Read ? Read : Write;
}
TConstArrayView<T> AsArrayView() const { return MakeArrayView(&Read, 2); }
bool IsEmpty() const { return Read.IsEmpty() && Write.IsEmpty(); }
};
/**
* TMassExecutionAccess specialization for FMassConstSharedFragmentBitSet to enforce lack of access (not needed) and
* no "Write" component (conceptually doesn't make sense).
*/
template<>
struct TMassExecutionAccess<FMassConstSharedFragmentBitSet>
{
FMassConstSharedFragmentBitSet Read;
TConstArrayView<FMassConstSharedFragmentBitSet> AsArrayView() const
{
return MakeArrayView(&Read, 1);
}
bool IsEmpty() const
{
return Read.IsEmpty();
}
};
struct FMassExecutionRequirements
{
UE_API void Append(const FMassExecutionRequirements& Other);
UE_API void CountResourcesUsed();
UE_API int32 GetTotalBitsUsedCount();
UE_API bool IsEmpty() const;
UE_API FMassArchetypeCompositionDescriptor AsCompositionDescriptor() const;
TMassExecutionAccess<FMassFragmentBitSet> Fragments;
TMassExecutionAccess<FMassChunkFragmentBitSet> ChunkFragments;
TMassExecutionAccess<FMassSharedFragmentBitSet> SharedFragments;
TMassExecutionAccess<FMassConstSharedFragmentBitSet> ConstSharedFragments;
TMassExecutionAccess<FMassExternalSubsystemBitSet> RequiredSubsystems;
FMassTagBitSet RequiredAllTags;
FMassTagBitSet RequiredAnyTags;
FMassTagBitSet RequiredNoneTags;
int32 ResourcesUsedCount = INDEX_NONE;
};
struct FMassProcessorDependencySolver
{
struct FNode
{
FNode(const FName InName, UMassProcessor* InProcessor, const int32 InNodeIndex = INDEX_NONE)
: Name(InName), Processor(InProcessor), NodeIndex(InNodeIndex)
{}
bool IsGroup() const { return Processor == nullptr; }
/**
* @return `true` when everything's fine, `false` when cycles have been encountered. If that
* happens `OutCycleIndices` gets filled with the relevant node indices.
*/
bool IncreaseWaitingNodesCount(TArrayView<FNode> InAllNodes, const int32 IterationsLimit, TArray<int32>& OutCycleIndices);
bool IncreaseWaitingNodesCountAndPriority(TArrayView<FNode> InAllNodes, const int32 IterationsLimit, TArray<int32>& OutCycleIndices
, const int32 InChildPriority = TNumericLimits<int32>::Min());
void UpdateExecutionPriority(const int32 ChildExecutionPriority)
{
// picking the max execution priority - note that we're increasing child
// priority to ensure dependencies always have a higher stored priority
// than the nodes that depend on them
MaxExecutionPriority = FMath::Max(int32(ChildExecutionPriority) + 1, MaxExecutionPriority);
}
FName Name = TEXT("");
UMassProcessor* Processor = nullptr;
TArray<int32> OriginalDependencies;
TArray<int32> TransientDependencies;
TArray<FName> ExecuteBefore;
TArray<FName> ExecuteAfter;
FMassExecutionRequirements Requirements;
int32 NodeIndex = INDEX_NONE;
/** indicates how often given node can be found in dependencies sequence for other nodes */
int32 TotalWaitingNodes = 0;
/**
* Indicates the maximum execution priority represented by this node or any od the nodes
* that depend on it in a logical sense - i.e. it does not include the nodes that are dependencies just by blocing required resources
* @todo reword this comment
* Note that we're using a larger type than UMassProcessor.ExecutionPriority (int32 vs int16)
* to not have to handle overflow in UpdateExecutionPriority
*/
int32 MaxExecutionPriority = 0;
/**
* indicates how deep within dependencies graph this give node is, or in other words, what's the longest sequence
* from this node to a dependency-less "parent" node
*/
int32 SequencePositionIndex = 0;
TArray<int32> SubNodeIndices;
TArray<FMassArchetypeHandle> ValidArchetypes;
};
private:
struct FResourceUsage
{
FResourceUsage(const TArray<FNode>& InAllNodes);
bool CanAccessRequirements(const FMassExecutionRequirements& TestedRequirements, const TArray<FMassArchetypeHandle>& InArchetypes) const;
void SubmitNode(const int32 NodeIndex, FNode& InOutNode);
private:
struct FResourceUsers
{
TArray<int32> Users;
};
struct FResourceAccess
{
TArray<FResourceUsers> Access;
};
FMassExecutionRequirements Requirements;
TMassExecutionAccess<FResourceAccess> FragmentsAccess;
TMassExecutionAccess<FResourceAccess> ChunkFragmentsAccess;
TMassExecutionAccess<FResourceAccess> SharedFragmentsAccess;
TMassExecutionAccess<FResourceAccess> RequiredSubsystemsAccess;
TConstArrayView<FNode> AllNodesView;
template<typename TBitSet>
void HandleElementType(TMassExecutionAccess<FResourceAccess>& ElementAccess
, const TMassExecutionAccess<TBitSet>& TestedRequirements, FMassProcessorDependencySolver::FNode& InOutNode, const int32 NodeIndex);
template<typename TBitSet>
static bool CanAccess(const TMassExecutionAccess<TBitSet>& StoredElements, const TMassExecutionAccess<TBitSet>& TestedElements);
/** Determines whether any of the Elements' (i.e. Fragment, Tag,...) users operate on any of the archetypes given via InArchetypes */
bool HasArchetypeConflict(TMassExecutionAccess<FResourceAccess> ElementAccess, const TArray<FMassArchetypeHandle>& InArchetypes) const;
};
public:
/** Optionally returned by ResolveDependencies and contains information about processors that have been pruned and
* other potentially useful bits. To be used in a transient fashion. */
struct FResult
{
FString DependencyGraphFileName;
TArray<TObjectPtr<UMassProcessor>> PrunedProcessors;
int32 MaxSequenceLength = 0;
uint32 ArchetypeDataVersion = 0;
UE_DEPRECATED(5.6, "This property is deprecated, replaced by PrunedProcessors")
TArray<TSubclassOf<UMassProcessor>> PrunedProcessorClasses;
void Reset()
{
PrunedProcessors.Reset();
MaxSequenceLength = 0;
ArchetypeDataVersion = 0;
}
};
MASSENTITY_API FMassProcessorDependencySolver(TArrayView<UMassProcessor* const> InProcessors, const bool bIsGameRuntime = true);
MASSENTITY_API void ResolveDependencies(TArray<FMassProcessorOrderInfo>& OutResult, TSharedPtr<FMassEntityManager> EntityManager = nullptr, FResult* InOutOptionalResult = nullptr);
MASSENTITY_API static void CreateSubGroupNames(FName InGroupName, TArray<FString>& SubGroupNames);
/** Determines whether the dependency solving that produced InResult will produce different results if run with a given EntityManager */
static bool IsResultUpToDate(const FResult& InResult, TSharedPtr<FMassEntityManager> EntityManager);
bool IsSolvingForSingleThread() const { return bSingleThreadTarget; }
protected:
// note that internals are protected rather than private to support unit testing
/**
* Traverses InOutIndicesRemaining in search of the first RootNode's node that has no dependencies left. Once found
* the node's index gets added to OutNodeIndices, removed from dependency lists from all other nodes and the function
* quits.
* @return 'true' if a dependency-less node has been found and added to OutNodeIndices; 'false' otherwise.
*/
bool PerformSolverStep(FResourceUsage& ResourceUsage, TArray<int32>& InOutIndicesRemaining, TArray<int32>& OutNodeIndices);
int32 CreateNodes(UMassProcessor& Processor);
void BuildDependencies();
void Solve(TArray<FMassProcessorOrderInfo>& OutResult);
void LogNode(const FNode& Node, int Indent = 0);
/** Finds out which subsystems handle multithreaded RW operations, and caches the result in MultiThreadedSystemsBitSet */
void GatherSubsystemInformation(const UE::Mass::FTypeManager& TypeManager);
TArrayView<UMassProcessor* const> Processors;
/**
* indicates whether we're generating processor order to be run in single-threaded or multithreaded environment (usually
* this means Dedicated Server vs Any other configuration). In Single-Threaded mode we can skip a bunch of expensive,
* fine-tuning tests.
* @Note currently the value depends on MASS_DO_PARALLEL and there's no way to configure it otherwise, but there's
* nothing inherently stopping us from letting users configure it.
*/
const bool bSingleThreadTarget = bool(!MASS_DO_PARALLEL);
const bool bGameRuntime = true;
FString DependencyGraphFileName;
TArray<FNode> AllNodes;
TMap<FName, int32> NodeIndexMap;
/** Stores the subsystems we know of that handle multithreaded access well - we filter those out, we don't need to consider them. */
FMassExternalSubsystemBitSet MultiThreadedSystemsBitSet;
};
#undef UE_API