// 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> 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& Names = CommandBatchedFNames.FindOrAdd(CommandFName); OutName = &Names.Get(); OutANSIName = &Names.Get(); if (OutName->IsEmpty()) { *OutName = CommandFName.ToString(); *OutANSIName = **OutName; } } #endif } // UE::Mass::Command //----------------------------------------------------------------------------- // FMassBatchedCommand //----------------------------------------------------------------------------- std::atomic 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(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(EMassCommandOperationType::Remove)] , CommandTypeOrder[static_cast(EMassCommandOperationType::ChangeComposition)] , CommandTypeOrder[static_cast(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 CommandsOrder; const int32 OwnedCommandsCount = CommandInstances.Num(); CommandsOrder.Reserve(OwnedCommandsCount); for (int32 i = 0; i < OwnedCommandsCount; ++i) { const TUniquePtr& 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& Command = AppendedCommandInstances[i]; CommandsOrder.Add(FBatchedCommandsSortedIndex(i + OwnedCommandsCount, (Command && Command->HasWork()) ? CommandTypeOrder[(int)Command->GetOperationType()] : MAX_int32)); } CommandsOrder.StableSort(); TSharedPtr ObserverLock; TSharedPtr 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(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& 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& Command : CommandInstances) { TotalSize += Command ? Command->GetAllocatedSize() : 0; } for (const TUniquePtr& Command : AppendedCommandInstances) { TotalSize += Command ? Command->GetAllocatedSize() : 0; } TotalSize += CommandInstances.GetAllocatedSize(); return TotalSize; }