Files
UnrealEngine/Engine/Source/Runtime/MassEntity/Private/MassCommandBuffer.cpp
2025-05-18 13:04:45 +08:00

263 lines
8.9 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "MassCommandBuffer.h"
#include "Containers/AnsiString.h"
#include "MassEntityManager.h"
#include "MassObserverManager.h"
#include "HAL/IConsoleManager.h"
#include "ProfilingDebugging/CsvProfiler.h"
#include "VisualLogger/VisualLogger.h"
CSV_DEFINE_CATEGORY(MassEntities, true);
CSV_DEFINE_CATEGORY(MassEntitiesCounters, true);
DECLARE_CYCLE_STAT(TEXT("Mass Flush Commands"), STAT_Mass_FlushCommands, STATGROUP_Mass);
namespace UE::Mass::Command
{
/**
* Note that we default to `false` because the correctness of the feature's behavior depends on use cases.
* If there are no observers watching fragment removal, everything will be great. If not, enabling the feature
* will result in the data removed no longer being available when the removal-observers get triggered upon lock's release
*/
bool bLockObserversDuringFlushing = false;
FAutoConsoleVariableRef CVarLockObserversDuringFlushing(TEXT("mass.commands.LockObserversDuringFlushing"), bLockObserversDuringFlushing
, TEXT("Controls whether observers will get locked during commands flushing."), ECVF_Default);
#if CSV_PROFILER_STATS
bool bEnableDetailedStats = false;
FAutoConsoleVariableRef CVarEnableDetailedCommandStats(TEXT("massentities.EnableCommandDetailedStats"), bEnableDetailedStats,
TEXT("Set to true create a dedicated stat per type of command."), ECVF_Default);
/** CSV stat names */
static FString DefaultBatchedName = TEXT("BatchedCommand");
static TMap<FName, TPair<FString, FAnsiString>> CommandBatchedFNames;
/** CSV custom stat names (ANSI) */
static FAnsiString DefaultANSIBatchedName = "BatchedCommand";
/**
* Provides valid names for CSV profiling.
* @param Command is the command instance
* @param OutName is the name to use for csv custom stats
* @param OutANSIName is the name to use for csv stats
*/
void GetCommandStatNames(FMassBatchedCommand& Command, FString*& OutName, FAnsiString*& OutANSIName)
{
OutANSIName = &DefaultANSIBatchedName;
OutName = &DefaultBatchedName;
if (!bEnableDetailedStats)
{
return;
}
const FName CommandFName = Command.GetFName();
TPair<FString, FAnsiString>& Names = CommandBatchedFNames.FindOrAdd(CommandFName);
OutName = &Names.Get<FString>();
OutANSIName = &Names.Get<FAnsiString>();
if (OutName->IsEmpty())
{
*OutName = CommandFName.ToString();
*OutANSIName = **OutName;
}
}
#endif
} // UE::Mass::Command
//-----------------------------------------------------------------------------
// FMassBatchedCommand
//-----------------------------------------------------------------------------
std::atomic<uint32> FMassBatchedCommand::CommandsCounter;
//-----------------------------------------------------------------------------
// FMassCommandBuffer
//-----------------------------------------------------------------------------
FMassCommandBuffer::FMassCommandBuffer()
: OwnerThreadId(FPlatformTLS::GetCurrentThreadId())
{
}
FMassCommandBuffer::~FMassCommandBuffer()
{
ensureMsgf(HasPendingCommands() == false, TEXT("Destroying FMassCommandBuffer while there are still unprocessed commands. These operations will never be performed now."));
CleanUp();
}
void FMassCommandBuffer::ForceUpdateCurrentThreadID()
{
OwnerThreadId = FPlatformTLS::GetCurrentThreadId();
}
bool FMassCommandBuffer::Flush(FMassEntityManager& EntityManager)
{
check(!bIsFlushing);
TGuardValue FlushingGuard(bIsFlushing, true);
// short-circuit exit
if (HasPendingCommands() == false)
{
return false;
}
UE_MT_SCOPED_WRITE_ACCESS(PendingBatchCommandsDetector);
LLM_SCOPE_BYNAME(TEXT("Mass/FlushCommands"));
SCOPE_CYCLE_COUNTER(STAT_Mass_FlushCommands);
// array used to group commands depending on their operations. Based on EMassCommandOperationType
constexpr int32 CommandTypeOrder[static_cast<uint8>(EMassCommandOperationType::MAX)] =
{
MAX_int32 - 1, // None
0, // Create
2, // Add
6, // Remove
3, // ChangeComposition
4, // Set
6, // Destroy
};
/**
* The following three types of commands are the ones where we cannot guarantee the new behavior
* will be consistent with the pre-change behavior.
* Before the change, every removal-observer gets notified before the data is actually removed,
* which means the observer can access the data-about-to-be-removed. Now, if removal happens while
* an observer lock is active, then the removal-observers will get notified after the fact.
* For now we're going to support the old behavior.
*/
constexpr int32 CommandTypeGroupToReleaseObserverLock = FMath::Min3(
CommandTypeOrder[static_cast<uint8>(EMassCommandOperationType::Remove)]
, CommandTypeOrder[static_cast<uint8>(EMassCommandOperationType::ChangeComposition)]
, CommandTypeOrder[static_cast<uint8>(EMassCommandOperationType::Destroy)]
);
struct FBatchedCommandsSortedIndex
{
FBatchedCommandsSortedIndex(const int32 InIndex, const int32 InGroupOrder)
: Index(InIndex), GroupOrder(InGroupOrder)
{}
const int32 Index = -1;
const int32 GroupOrder = MAX_int32;
bool IsValid() const { return GroupOrder < MAX_int32; }
bool operator<(const FBatchedCommandsSortedIndex& Other) const { return GroupOrder < Other.GroupOrder; }
};
TArray<FBatchedCommandsSortedIndex> CommandsOrder;
const int32 OwnedCommandsCount = CommandInstances.Num();
CommandsOrder.Reserve(OwnedCommandsCount);
for (int32 i = 0; i < OwnedCommandsCount; ++i)
{
const TUniquePtr<FMassBatchedCommand>& Command = CommandInstances[i];
CommandsOrder.Add(FBatchedCommandsSortedIndex(i, (Command && Command->HasWork())? CommandTypeOrder[(int)Command->GetOperationType()] : MAX_int32));
}
for (int32 i = 0; i < AppendedCommandInstances.Num(); ++i)
{
const TUniquePtr<FMassBatchedCommand>& Command = AppendedCommandInstances[i];
CommandsOrder.Add(FBatchedCommandsSortedIndex(i + OwnedCommandsCount, (Command && Command->HasWork()) ? CommandTypeOrder[(int)Command->GetOperationType()] : MAX_int32));
}
CommandsOrder.StableSort();
TSharedPtr<FMassObserverManager::FObserverLock> ObserverLock;
TSharedPtr<FMassObserverManager::FCreationContext> CreationLock;
if (UE::Mass::Command::bLockObserversDuringFlushing
&& CommandsOrder[0].GroupOrder < CommandTypeGroupToReleaseObserverLock)
{
ObserverLock = EntityManager.GetOrMakeObserversLock();
// we only want to create CreationLock if the very first command is of `Create` type.
if (CommandsOrder[0].GroupOrder == CommandTypeOrder[static_cast<uint8>(EMassCommandOperationType::Create)])
{
CreationLock = EntityManager.GetOrMakeCreationContext();
}
}
bool bObserversLock = ObserverLock.IsValid();
bool bCreationLock = CreationLock.IsValid();
for (int32 k = 0; k < CommandsOrder.Num() && CommandsOrder[k].IsValid(); ++k)
{
if (bCreationLock && CommandsOrder[k].GroupOrder > 0)
{
bCreationLock = false;
CreationLock.Reset();
}
if (bObserversLock && CommandsOrder[k].GroupOrder >= CommandTypeGroupToReleaseObserverLock)
{
bObserversLock = false;
ObserverLock.Reset();
}
const int32 CommandIndex = CommandsOrder[k].Index;
TUniquePtr<FMassBatchedCommand>& Command = CommandIndex < OwnedCommandsCount
? CommandInstances[CommandIndex]
: AppendedCommandInstances[CommandIndex - OwnedCommandsCount];
check(Command)
#if CSV_PROFILER_STATS
using namespace UE::Mass::Command;
// Extract name (default or detailed)
FAnsiString* ANSIName = nullptr;
FString* Name = nullptr;
GetCommandStatNames(*Command, Name, ANSIName);
// Push stats
FScopedCsvStat ScopedCsvStat(**ANSIName, CSV_CATEGORY_INDEX(MassEntities));
FCsvProfiler::RecordCustomStat(**Name, CSV_CATEGORY_INDEX(MassEntitiesCounters), Command->GetNumOperationsStat(), ECsvCustomStatOp::Accumulate);
#endif // CSV_PROFILER_STATS
Command->Execute(EntityManager);
Command->Reset();
}
AppendedCommandInstances.Reset();
ActiveCommandsCounter = 0;
return true;
}
void FMassCommandBuffer::CleanUp()
{
CommandInstances.Reset();
AppendedCommandInstances.Reset();
ActiveCommandsCounter = 0;
}
void FMassCommandBuffer::MoveAppend(FMassCommandBuffer& Other)
{
TRACE_CPUPROFILER_EVENT_SCOPE(MassCommandBuffer_MoveAppend);
// @todo optimize, there surely a way to do faster then this.
UE_MT_SCOPED_READ_ACCESS(Other.PendingBatchCommandsDetector);
if (Other.HasPendingCommands())
{
FScopeLock Lock(&AppendingCommandsCS);
UE_MT_SCOPED_WRITE_ACCESS(PendingBatchCommandsDetector);
AppendedCommandInstances.Append(MoveTemp(Other.CommandInstances));
AppendedCommandInstances.Append(MoveTemp(Other.AppendedCommandInstances));
ActiveCommandsCounter += Other.ActiveCommandsCounter;
Other.ActiveCommandsCounter = 0;
}
}
SIZE_T FMassCommandBuffer::GetAllocatedSize() const
{
SIZE_T TotalSize = 0;
for (const TUniquePtr<FMassBatchedCommand>& Command : CommandInstances)
{
TotalSize += Command ? Command->GetAllocatedSize() : 0;
}
for (const TUniquePtr<FMassBatchedCommand>& Command : AppendedCommandInstances)
{
TotalSize += Command ? Command->GetAllocatedSize() : 0;
}
TotalSize += CommandInstances.GetAllocatedSize();
return TotalSize;
}