Files
UnrealEngine/Engine/Plugins/Mutable/Source/CustomizableObject/Private/MuCO/FMutableTaskGraph.cpp
2025-05-18 13:04:45 +08:00

440 lines
10 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "MuCO/FMutableTaskGraph.h"
#include "MuCO/CustomizableObject.h"
#include "MuCO/CustomizableObjectSystem.h"
#include "MuR/MutableTrace.h"
#define UE_MUTABLE_THREAD_REQUEST_LOCK TEXT("Mutable Thread Request Lock")
#define UE_MUTABLE_THREAD_LOCK TEXT("Mutable Thread Lock")
constexpr LowLevelTasks::ETaskPriority TASKGRAPH_PRIORITY = LowLevelTasks::ETaskPriority::BackgroundHigh;
static TAutoConsoleVariable<float> CVarMutableTaskLowPriorityMaxWaitTime(
TEXT("mutable.MutableTaskLowPriorityMaxWaitTime"),
3.0f,
TEXT("Max time a Mutable Task with Low priority can wait. Once this time has passed it will be launched unconditionally."),
ECVF_Scalability);
static TAutoConsoleVariable<float> CVarGameThreadTaskMaxTime(
TEXT("mutable.GameThreadTaskMaxTime"),
0.5 / 1000.0, // 0.5ms
TEXT("Max time Mutable can execute Game Thread Task each tick. A single task may take longer than the specified time."),
ECVF_Scalability);
static TAutoConsoleVariable<bool> CVarEnableMutableTaskLowPriority(
TEXT("mutable.EnableMutableTaskLowPriority"),
true,
TEXT("Enable or disable Mutable Tasks with Low priority. If disabled, all task will have the same priority. "),
ECVF_Scalability);
/** Status of the Mutable Thread lock. Only used for Insights Regions. */
enum class ERegionLockStatus
{
Requested,
Locked,
Unlocked // No region.
};
/** What Insights Region we are currently showing. Used only for debug. */
static ERegionLockStatus RegionLockStatus;
bool FMutableTaskGraph::FGameThreadTask::AreDependenciesComplete() const
{
for (const UE::Tasks::FTask& Dependency : Prerequisites)
{
if (!Dependency.IsCompleted())
{
return false;
}
}
return true;
}
FMutableTaskGraph::FMutableTaskGraph()
{
MutableThreadUnlockEvent.Trigger();
}
FMutableTaskGraph::~FMutableTaskGraph()
{
GameThreadTasks.Empty();
}
void FMutableTaskGraph::AddGameThreadTask(const TCHAR* DebugName, TUniqueFunction<void()>&& TaskBody, bool bLockMutableThread, const TArray<UE::Tasks::FTask>& Prerequisites)
{
GameThreadTasks.Enqueue({ DebugName, Forward<TUniqueFunction<void()>>(TaskBody), Prerequisites, bLockMutableThread });
}
UE::Tasks::FTask FMutableTaskGraph::AddMutableThreadTask(const TCHAR* DebugName, TUniqueFunction<void()>&& TaskBody, const TArray<UE::Tasks::FTask>& InPrerequisites)
{
FScopeLock Lock(&MutableTaskLock);
TArray<UE::Tasks::FTask, TInlineAllocator<2>> Prerequisites = LastMutableTask.IsValid()
? TArray<UE::Tasks::FTask, TInlineAllocator<2>>{LastMutableTask, MutableThreadUnlockEvent }
: TArray<UE::Tasks::FTask, TInlineAllocator<2>>{};
Prerequisites.Append(InPrerequisites);
LastMutableTask = UE::Tasks::Launch(DebugName, Forward<TUniqueFunction<void()>>(TaskBody), Prerequisites, TASKGRAPH_PRIORITY);
return LastMutableTask;
}
uint32 FMutableTaskGraph::AddMutableThreadTaskLowPriority(const TCHAR* DebugName, TFunction<void()>&& TaskBody)
{
FScopeLock Lock(&MutableTaskLock);
if (CVarEnableMutableTaskLowPriority.GetValueOnAnyThread())
{
const uint32 Id = ++TaskIdGenerator;
const FMutableThreadLowPriorityTask Task { Id, DebugName, MoveTemp(TaskBody)};
QueueMutableTasksLowPriority.Add(Task);
TryLaunchMutableTaskLowPriority(false);
return Id;
}
else
{
AddMutableThreadTask(DebugName, TaskBody);
return INVALID_ID;
}
}
bool FMutableTaskGraph::CancelMutableThreadTaskLowPriority(uint32 Id)
{
FScopeLock Lock(&MutableTaskLock);
for (int32 Index = 0; Index < QueueMutableTasksLowPriority.Num(); ++Index)
{
if (QueueMutableTasksLowPriority[Index].Id == Id)
{
QueueMutableTasksLowPriority.RemoveAt(Index);
return true;
}
}
return false;
}
void FMutableTaskGraph::AddAnyThreadTask(const TCHAR* DebugName, TUniqueFunction<void()>&& TaskBody) const
{
UE::Tasks::Launch(DebugName, MoveTemp(TaskBody));
}
void FMutableTaskGraph::WaitForMutableTasks()
{
FScopeLock Lock(&MutableTaskLock);
if (LastMutableTask.IsValid())
{
LastMutableTask.Wait();
LastMutableTask = {};
}
}
void FMutableTaskGraph::WaitForLaunchedLowPriorityTask(uint32 TaskID)
{
UE::Tasks::FTask Task;
{
FScopeLock Lock(&MutableTaskLock);
if (LastMutableTaskLowPriorityID == TaskID)
{
Task = LastMutableTaskLowPriority;
}
}
if (Task.IsValid())
{
Task.Wait();
}
}
void FMutableTaskGraph::TryLaunchMutableTaskLowPriority(bool bFromMutableTask)
{
MUTABLE_CPUPROFILER_SCOPE(TryLaunchMutableTaskLowPriority)
if (QueueMutableTasksLowPriority.IsEmpty())
{
return;
}
if (!IsTaskCompleted(LastMutableTaskLowPriority)) // At any time only a single Low Priority task can be launched.
{
return;
}
double TimeLimit;
double TimeElapsed;
bool bTimeLimit;
{
FScopeLock Lock(&MutableTaskLock);
if (QueueMutableTasksLowPriority.IsEmpty()) // Also checked inside the lock since we will be writing it
{
return;
}
if (!IsTaskCompleted(LastMutableTaskLowPriority)) // Check #1 // Also check inside the lock since we will be writing it
{
return;
}
FMutableThreadLowPriorityTask& NextTask = QueueMutableTasksLowPriority[0];
TimeLimit = CVarMutableTaskLowPriorityMaxWaitTime.GetValueOnAnyThread();
TimeElapsed = FPlatformTime::Seconds() - NextTask.CreationTime;
bTimeLimit = TimeElapsed >= TimeLimit;
if (!bAllowLaunchMutableTaskLowPriority || // Check #2
(bFromMutableTask && IsTaskCompleted(LastMutableTask) && !bTimeLimit)) // Check #3
{
return;
}
if (CVarFixLowPriorityTasksOverlap.GetValueOnAnyThread())
{
LastMutableTaskLowPriorityID = NextTask.Id;
LastMutableTaskLowPriority = AddMutableThreadTask(*NextTask.DebugName, [this, Task = MoveTemp(NextTask)]() // Moves the task, not the pointer.
{
MUTABLE_CPUPROFILER_SCOPE(LowPriorityTaskBody)
Task.Body();
});
const FString TaskName = NextTask.DebugName + TEXT(" End");
AddMutableThreadTask(*TaskName, [this]()
{
MUTABLE_CPUPROFILER_SCOPE(LowPriorityTaskBodyEnd)
FScopeLock Lock(&MutableTaskLock);
LastMutableTaskLowPriorityID = INVALID_ID;
LastMutableTaskLowPriority = {};
TryLaunchMutableTaskLowPriority(true);
},
{ LastMutableTaskLowPriority });
}
else
{
LastMutableTaskLowPriorityID = NextTask.Id;
LastMutableTaskLowPriority = AddMutableThreadTask(*NextTask.DebugName, [this, Task = MoveTemp(NextTask)]() // Moves the task, not the pointer.
{
MUTABLE_CPUPROFILER_SCOPE(LowPriorityTaskBody)
Task.Body();
{
FScopeLock Lock(&MutableTaskLock);
LastMutableTaskLowPriorityID = INVALID_ID;
LastMutableTaskLowPriority = {};
TryLaunchMutableTaskLowPriority(true);
}
});
}
QueueMutableTasksLowPriority.RemoveAt(0);
}
if (bTimeLimit)
{
UE_LOG(LogMutable, Verbose, TEXT("Low Priority Mutable Task launched due to time limit (%f)! Waited for: %f"), TimeLimit, TimeElapsed)
}
}
bool FMutableTaskGraph::IsTaskCompleted(const UE::Tasks::FTask& Task) const
{
return Task.IsCompleted();
}
void FMutableTaskGraph::AllowLaunchingMutableTaskLowPriority(bool bAllow, bool bFromMutableTask)
{
FScopeLock Lock(&MutableTaskLock);
bAllowLaunchMutableTaskLowPriority = bAllow;
if (bAllowLaunchMutableTaskLowPriority)
{
TryLaunchMutableTaskLowPriority(bFromMutableTask);
}
}
void FMutableTaskGraph::TryLaunchGameThreadTask()
{
MUTABLE_CPUPROFILER_SCOPE(AdvanceCurrentOperation);
double CurrentTime = FPlatformTime::Seconds();
const double EndTime = CurrentTime + CVarGameThreadTaskMaxTime.GetValueOnGameThread();
while (CurrentTime < EndTime)
{
const FGameThreadTask* PendingTask = GameThreadTasks.Peek();
if (!PendingTask)
{
break;
}
if (!PendingTask->AreDependenciesComplete())
{
break;
}
if (PendingTask->bLockMutableThread)
{
AsyncLockMutableThread();
if (!IsMutableThreadLocked())
{
break;
}
}
FGameThreadTask CurrentTask;
GameThreadTasks.Dequeue(CurrentTask); // Dequeue now since current task can enqueue new tasks.
// Careful! From this point, PendingTask is no longer valid.
CurrentTask.Body();
CurrentTime = FPlatformTime::Seconds();
}
UnlockMutableThread();
}
int32 FMutableTaskGraph::Tick()
{
check(IsInGameThread())
TryLaunchGameThreadTask();
TryLaunchMutableTaskLowPriority(false);
return !IsTaskCompleted(LastMutableTask) + !IsTaskCompleted(LastMutableTaskLowPriority) + QueueMutableTasksLowPriority.Num() + !GameThreadTasks.IsEmpty();
}
void UpdateRegions(ERegionLockStatus NewRegionLockStatus)
{
if (RegionLockStatus == NewRegionLockStatus)
{
return;
}
// End current region.
switch (RegionLockStatus)
{
case ERegionLockStatus::Requested:
TRACE_END_REGION(UE_MUTABLE_THREAD_REQUEST_LOCK);
break;
case ERegionLockStatus::Locked:
TRACE_END_REGION(UE_MUTABLE_THREAD_LOCK);
break;
case ERegionLockStatus::Unlocked:
break;
default:
unimplemented();
}
// Start new region.
switch (NewRegionLockStatus)
{
case ERegionLockStatus::Requested:
TRACE_BEGIN_REGION(UE_MUTABLE_THREAD_REQUEST_LOCK);
break;
case ERegionLockStatus::Locked:
TRACE_BEGIN_REGION(UE_MUTABLE_THREAD_LOCK);
break;
case ERegionLockStatus::Unlocked:
break;
default:
unimplemented();
}
RegionLockStatus = NewRegionLockStatus;
}
void FMutableTaskGraph::AsyncLockMutableThread()
{
MUTABLE_CPUPROFILER_SCOPE(FMutableTaskGraph::AsyncLockMutableThread)
FScopeLock Lock(&MutableTaskLock);
if (!MutableThreadUnlockEvent.IsCompleted()) // Locking already in progress.
{
return;
}
UpdateRegions(ERegionLockStatus::Requested);
LastMutableTaskBeforeLock = LastMutableTask;
MutableThreadUnlockEvent = UE::Tasks::FTaskEvent(UE_SOURCE_LOCATION);
}
bool FMutableTaskGraph::IsMutableThreadLockedNoLock() const
{
return !MutableThreadUnlockEvent.IsCompleted() && LastMutableTaskBeforeLock.IsCompleted();
}
bool FMutableTaskGraph::IsMutableThreadLocked() const
{
MUTABLE_CPUPROFILER_SCOPE(FMutableTaskGraph::IsMutableThreadLocked)
FScopeLock Lock(&MutableTaskLock);
const bool bLocked = IsMutableThreadLockedNoLock();
UpdateRegions(bLocked ? ERegionLockStatus::Locked : RegionLockStatus);
return bLocked;
}
void FMutableTaskGraph::UnlockMutableThread()
{
MUTABLE_CPUPROFILER_SCOPE(FMutableTaskGraph::UnlockMutableThread)
FScopeLock Lock(&MutableTaskLock);
if (!IsMutableThreadLockedNoLock())
{
return;
}
UpdateRegions(ERegionLockStatus::Unlocked);
LastMutableTaskBeforeLock = {};
MutableThreadUnlockEvent.Trigger();
}