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

907 lines
30 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "EntitySystem/MovieSceneTaskScheduler.h"
#include "Tasks/Task.h"
#include "Algo/RandomShuffle.h"
#include "ProfilingDebugging/CpuProfilerTrace.h"
#include "Stats/Stats.h"
#include "Misc/AutomationTest.h"
namespace UE::MovieScene
{
DECLARE_CYCLE_STAT(TEXT("Anonymous MovieScene Task"), MovieSceneEval_AnonymousTask, STATGROUP_MovieSceneECS);
/** CVar that disables our task scheduler. When disabled, all systems that are normally in the Scheduling phase will be executed in the Evaluation phase with their OnRun function */
bool GSequencerCustomTaskScheduling = true;
static FAutoConsoleVariableRef CVarSequencerCustomTaskScheduling(
TEXT("Sequencer.CustomTaskScheduling"),
GSequencerCustomTaskScheduling,
TEXT("(Default: true. Enables more efficient custom task scheduling of asynchronous Sequencer evaluation.")
);
/** Flag structure to pass when executing a task. */
struct FTaskExecutionFlags
{
explicit FTaskExecutionFlags()
{
bCanInlineSubsequents = true;
}
/**
* When false, prevents any other tasks from being executed inline on completion of this task.
* This should be used when a task is being forced inline to prevent a cascade of inlined tasks from blocking the scheduling of other async work
*/
uint8 bCanInlineSubsequents : 1;
};
FScheduledTask::FScheduledTask(FEntityAllocationWriteContext InWriteContextOffset)
: StatId(GET_STATID(MovieSceneEval_AnonymousTask))
, WriteContextOffset(InWriteContextOffset)
, TaskFunctionType(FScheduledTaskFuncionPtr::EType::None)
{
bForceGameThread = false;
bForceInline = false;
}
FScheduledTask::FScheduledTask(FScheduledTask&& InTask)
: ComputedSubsequents(MoveTemp(InTask.ComputedSubsequents))
, TaskFunction(InTask.TaskFunction)
, TaskContext(InTask.TaskContext)
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
, DebugName(InTask.DebugName)
#endif
, StatId(InTask.StatId)
, WriteContextOffset(InTask.WriteContextOffset)
, LockedComponentData(MoveTemp(InTask.LockedComponentData))
, NumPrerequisites(InTask.NumPrerequisites)
, WaitCount(InTask.WaitCount.Load(EEntityThreadingModel::NoThreading))
, ChildCompleteCount(InTask.ChildCompleteCount.Load(EEntityThreadingModel::NoThreading))
, Parent(InTask.Parent)
, NumChildren(InTask.NumChildren)
, TaskFunctionType(InTask.TaskFunctionType)
{
bForceGameThread = InTask.bForceGameThread;
bForceInline = InTask.bForceInline;
}
FScheduledTask::~FScheduledTask()
{}
void FScheduledTask::SetFunction(TaskFunctionPtr InFunction)
{
switch (InFunction.GetIndex())
{
case 0: TaskFunctionType = TaskFunction.Assign(InFunction.Get<UnboundTaskFunctionPtr>()); break;
case 1: TaskFunctionType = TaskFunction.Assign(InFunction.Get<AllocationFunctionPtr>()); break;
case 2: TaskFunctionType = TaskFunction.Assign(InFunction.Get<AllocationItemFunctionPtr>()); break;
case 3: TaskFunctionType = TaskFunction.Assign(InFunction.Get<PreLockedAllocationItemFunctionPtr>()); break;
}
}
void FScheduledTask::Run(const FEntitySystemScheduler* Scheduler, FTaskExecutionFlags InFlags) const
{
if (TaskFunctionType != FScheduledTaskFuncionPtr::EType::None)
{
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
UE_LOG(LogMovieSceneECS, VeryVerbose, TEXT("Running task \"%s\""), *DebugName);
TRACE_CPUPROFILER_EVENT_SCOPE_TEXT(*DebugName);
#endif
#if STATS || ENABLE_STATNAMEDEVENTS
FScopeCycleCounter Scope(StatId);
#endif
FEntityAllocationWriteContext ThisWriteContext = Scheduler->GetWriteContextOffset().Add(WriteContextOffset);
switch(TaskFunctionType)
{
case FScheduledTaskFuncionPtr::EType::Unbound:
(TaskFunction.UnboundTask)(TaskContext.Get(), ThisWriteContext);
break;
case FScheduledTaskFuncionPtr::EType::AllocationPtr:
{
check(LockedComponentData.AllocationIndex != MAX_uint16);
FEntityAllocationProxy Allocation = FEntityAllocationProxy::MakeInstance(Scheduler->GetEntityManager(), LockedComponentData.AllocationIndex);
(TaskFunction.Allocation)(Allocation, TaskContext.Get(), ThisWriteContext);
}
break;
case FScheduledTaskFuncionPtr::EType::AllocationItem:
{
check(LockedComponentData.AllocationIndex != MAX_uint16);
FEntityAllocationProxy Allocation = FEntityAllocationProxy::MakeInstance(Scheduler->GetEntityManager(), LockedComponentData.AllocationIndex);
(TaskFunction.AllocationItem)(Allocation, TaskContext.Get(), ThisWriteContext);
}
break;
case FScheduledTaskFuncionPtr::EType::PreLockedAllocationItem:
{
check(LockedComponentData.AllocationIndex != MAX_uint16);
FEntityAllocationProxy Allocation = FEntityAllocationProxy::MakeInstance(Scheduler->GetEntityManager(), LockedComponentData.AllocationIndex);
TArrayView<const FPreLockedDataPtr> PreLockedData(LockedComponentData.PreLockedComponentData);
(TaskFunction.PreLockedAllocationItem)(Allocation, PreLockedData, TaskContext.Get(), ThisWriteContext);
}
break;
}
}
// Now the task is finished, schedule any children to run, or any subsequents
// If we are a parent we do not call CompleteTask until _all_ our children have finished:
// this happens in FEntitySystemScheduler::CompleteTask if Parent is valid
if (NumChildren > 0)
{
// Increment the child completion count to protect CompleteTask being called for _this_ parent task
// while the loop over ChildTasks is running. This prevents a race condition where the final child can end up being
// the last task altogether, which triggers OnAllTasksFinished, potentially allowing the waiting thread to continue
// and destroy or otherwise mutate the contents of FEntitySystemScheduler resulting in a crash.
//
// Once our loop has finished we check the child complete count to see if this was the last one
ChildCompleteCount.Add(Scheduler->GetEntityManager()->GetThreadingModel(), 1);
const FScheduledTask* Children = this + 1;
for (uint16 ChildIndex = 0; ChildIndex < NumChildren; ++ChildIndex)
{
Scheduler->PrerequisiteCompleted(&Children[ChildIndex], nullptr);
}
// Subtract our count added on ln 205. If this is the last count, complete this task (all children have completed)
const int32 PreviousCompleteCount = ChildCompleteCount.Sub(Scheduler->GetEntityManager()->GetThreadingModel(), 1);
if (PreviousCompleteCount == 1)
{
Scheduler->CompleteTask(this, InFlags);
}
}
else
{
checkSlow(ChildCompleteCount.Load(Scheduler->GetEntityManager()->GetThreadingModel()) == 0);
Scheduler->CompleteTask(this, InFlags);
}
}
FEntitySystemScheduler::FEntitySystemScheduler(FEntityManager* InEntityManager)
: EntityManager(InEntityManager)
{
}
FEntitySystemScheduler::~FEntitySystemScheduler()
{
check(!GameThreadSignal);
}
bool FEntitySystemScheduler::IsCustomSchedulingEnabled()
{
return GSequencerCustomTaskScheduling;
}
FTaskID FEntitySystemScheduler::CreateForkedAllocationTask(const FTaskParams& InParams, TSharedPtr<ITaskContext> InTaskContext, TaskFunctionPtr InTaskFunction, TFunctionRef<void(FEntityAllocationIteratorItem,TArray<FPreLockedDataPtr>&)> InPreLockFunc, const FEntityComponentFilter& Filter, const FComponentMask& ReadDeps, const FComponentMask& WriteDeps)
{
FEntityAllocationWriteContext WriteContext(*EntityManager);
WriteContext.Subtract(WriteContextBase);
const int32 StartingNumTasks = Tasks.Num();
// We should never encounter both read and write dependencies for the same component
ensure(FComponentMask::BitwiseAND(ReadDeps, WriteDeps, EBitwiseOperatorFlags::MinSize).NumComponents() == 0);
FTaskID LastTaskID, ParentTaskID;
for (FEntityAllocationIteratorItem Allocation : EntityManager->Iterate(&Filter))
{
// If we haven't created a parent yet, create that now
if (!ParentTaskID)
{
ParentTaskID = FTaskID(Tasks.Emplace(WriteContext));
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
TStringBuilder<128> ParentTaskName;
ParentTaskName += TEXT("Parent task for ");
ParentTaskName += InParams.DebugName;
if (InParams.DebugName)
{
ParentTaskName += InParams.DebugName;
}
#if STATS
else
{
InParams.StatId.GetName().ToString(ParentTaskName);
}
#endif
#endif
Tasks[ParentTaskID.Index].bForceInline = true;
Tasks[ParentTaskID.Index].bForceGameThread = false;
// We're dependent upon the output of any explicit upstream dependency (not bound to a specific allocation)
if (CurrentPrerequisites)
{
if (InParams.bForceConsumeUpstream)
{
for (int32 SystemPrereq : CurrentPrerequisites->SystemWidePrerequisites)
{
AddPrerequisite(FTaskID(SystemPrereq), ParentTaskID);
}
}
for (int32 SystemPrereq : CurrentPrerequisites->ForcedSystemWidePrerequisites)
{
AddPrerequisite(FTaskID(SystemPrereq), ParentTaskID);
}
}
CurrentSubsequents.SystemWidePrerequisites.SetBit(ParentTaskID.Index);
if (InParams.bForcePropagateDownstream)
{
CurrentSubsequents.ForcedSystemWidePrerequisites.SetBit(ParentTaskID.Index);
}
}
const int32 AllocationIndex = Allocation.GetAllocationIndex();
// Set up a new task for this allocation
const int32 AllocationTask = Tasks.Num();
const FTaskID ThisTask(AllocationTask);
FScheduledTask::FLockedComponentData LockedComponentData;
LockedComponentData.AllocationIndex = static_cast<uint16>(AllocationIndex);
InPreLockFunc(Allocation, LockedComponentData.PreLockedComponentData);
// Create the task
const int32 ChildTaskIndex = Tasks.Num();
FScheduledTask& NewTask = Tasks.Emplace_GetRef(WriteContext);
NewTask.SetFunction(InTaskFunction);
NewTask.StatId = InParams.StatId;
NewTask.Parent = FTaskID(ParentTaskID.Index);
NewTask.TaskContext = InTaskContext;
NewTask.bForceGameThread = InParams.bForceGameThread;
NewTask.NumPrerequisites = 1; // +1 Because the parent triggers us as well when it starts
NewTask.LockedComponentData = MoveTemp(LockedComponentData);
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
if (InParams.DebugName)
{
NewTask.DebugName = InParams.DebugName;
}
#if STATS
else
{
NewTask.DebugName = InParams.StatId.GetName().ToString();
}
#endif
#endif
++Tasks[ParentTaskID.Index].NumChildren;
// If we're forking these tasks, the parent schedules this allocation task as soon as possible
// In this case that means as soon as everything upstream that writes to the same components on the same allocation is finished
// This guarantees we can never read/write from/to component data that is being written to on another thread
// Component reads only depend up upstream writes
for (FComponentMaskIterator It = ReadDeps.Iterate(); It; ++It)
{
FComponentTypeID Type = FComponentTypeID::FromBitIndex(It.GetIndex());
// Otherwise we can be scheduled as soon as the last write task to this allocation is done
FComponentDependencies& Dependencies = ComponentDepedenciesByAllocation.FindOrAdd(MakeTuple(AllocationIndex, Type));
if (Dependencies.WriteTask)
{
AddPrerequisite(Dependencies.WriteTask, ThisTask);
}
Dependencies.ReadTasks.SetBit(ThisTask.Index);
}
// Component writes depend up upstream reads and writes, and become the new write dependency for anything downstream
for (FComponentMaskIterator It = WriteDeps.Iterate(); It; ++It)
{
FComponentTypeID Type = FComponentTypeID::FromBitIndex(It.GetIndex());
FComponentDependencies& Dependencies = ComponentDepedenciesByAllocation.FindOrAdd(MakeTuple(AllocationIndex, Type));
for (int32 Dep : Dependencies.ReadTasks)
{
AddPrerequisite(FTaskID(Dep), ThisTask);
}
if (Dependencies.WriteTask)
{
AddPrerequisite(Dependencies.WriteTask, ThisTask);
}
Dependencies.ReadTasks = FTaskBitSet();
Dependencies.WriteTask = ThisTask;
}
// If the tasks are serial, we depend on the last child we made
if (InParams.bSerialTasks && LastTaskID)
{
AddPrerequisite(LastTaskID, ThisTask);
}
LastTaskID = ThisTask;
}
check(!ParentTaskID || ParentTaskID.Index + Tasks[ParentTaskID.Index].NumChildren == Tasks.Num()-1);
return ParentTaskID;
}
void FEntitySystemScheduler::AddPrerequisite(FTaskID Prerequisite, FTaskID Subsequent)
{
// Take care not to add the same dependency multiple times.
// This will happen if the same upstream task writes to many components
// that this task needs.
if (Prerequisite && Subsequent && Prerequisite.Index != Subsequent.Index && !Tasks[Prerequisite.Index].ComputedSubsequents.IsBitSet(Subsequent.Index))
{
Tasks[Prerequisite.Index].ComputedSubsequents.SetBit(Subsequent.Index);
Tasks[Subsequent.Index].NumPrerequisites += 1;
}
}
void FEntitySystemScheduler::AddChildFront(FTaskID Parent, FTaskID Child)
{
if (Parent && Child && ensure(Child.Index > Parent.Index && Child.Index == Parent.Index + Tasks[Parent.Index].NumChildren + 1))
{
// Children can't be prerequisites of their parent or visa-versa
check(!Tasks[Parent.Index].ComputedSubsequents.IsBitSet(Child.Index) && !Tasks[Child.Index].ComputedSubsequents.IsBitSet(Parent.Index));
// All existing children must come after this
const int32 FirstChild = Parent.Index+1;
const int32 ChildrenEnd = FirstChild + Tasks[Parent.Index].NumChildren;
for (int32 ChildIndex = FirstChild; ChildIndex < ChildrenEnd; ++ChildIndex)
{
AddPrerequisite(Child, FTaskID(ChildIndex));
}
++Tasks[Parent.Index].NumChildren;
// The parent will signal this task when it is run
Tasks[Child.Index].NumPrerequisites += 1;
Tasks[Child.Index].Parent = Parent;
}
}
void FEntitySystemScheduler::AddChildBack(FTaskID Parent, FTaskID Child)
{
// Child tasks must be immediately reported
if (Parent && Child && ensure(Child.Index > Parent.Index && Child.Index == Parent.Index + Tasks[Parent.Index].NumChildren + 1))
{
// Children can't be prerequisites of their parent or visa-versa
check(!Tasks[Parent.Index].ComputedSubsequents.IsBitSet(Child.Index) && !Tasks[Child.Index].ComputedSubsequents.IsBitSet(Parent.Index));
// All existing children must come before this
const int32 FirstChild = Parent.Index+1;
const int32 ChildrenEnd = FirstChild + Tasks[Parent.Index].NumChildren;
for (int32 ChildIndex = FirstChild; ChildIndex < ChildrenEnd; ++ChildIndex)
{
AddPrerequisite(FTaskID(ChildIndex), Child);
}
++Tasks[Parent.Index].NumChildren;
// The parent will signal this task when it is run
Tasks[Child.Index].NumPrerequisites += 1;
Tasks[Child.Index].Parent = Parent;
}
}
FTaskID FEntitySystemScheduler::AddTask(const FTaskParams& InParams, TSharedPtr<ITaskContext> InTaskContext, TaskFunctionPtr InTaskFunction)
{
FEntityAllocationWriteContext WriteContext(*EntityManager);
WriteContext.Subtract(WriteContextBase);
const int32 TaskIndex = Tasks.Num();
FScheduledTask& NewTask = Tasks.Emplace_GetRef(WriteContext);
NewTask.SetFunction(InTaskFunction);
NewTask.TaskContext = InTaskContext;
NewTask.StatId = InParams.StatId;
NewTask.bForceGameThread = InParams.bForceGameThread;
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
NewTask.DebugName = InParams.DebugName;
#endif
FTaskID ThisTask(TaskIndex);
// We're dependent upon the output of any explicit upstream dependency (not bound to a specific allocation)
if (CurrentPrerequisites)
{
if (InParams.bForceConsumeUpstream)
{
for (int32 SystemPrereq : CurrentPrerequisites->SystemWidePrerequisites)
{
AddPrerequisite(FTaskID(SystemPrereq), ThisTask);
}
}
for (int32 SystemPrereq : CurrentPrerequisites->ForcedSystemWidePrerequisites)
{
AddPrerequisite(FTaskID(SystemPrereq), ThisTask);
}
}
CurrentSubsequents.SystemWidePrerequisites.SetBit(TaskIndex);
if (InParams.bForcePropagateDownstream)
{
CurrentSubsequents.ForcedSystemWidePrerequisites.SetBit(TaskIndex);
}
return ThisTask;
}
FTaskID FEntitySystemScheduler::AddNullTask()
{
FEntityAllocationWriteContext WriteContext(*EntityManager);
WriteContext.Subtract(WriteContextBase);
const int32 TaskIndex = Tasks.Num();
FScheduledTask& NewTask = Tasks.Emplace_GetRef(WriteContext);
return FTaskID(TaskIndex);
}
void FEntitySystemScheduler::ShuffleTasks()
{
TArray<int32> RemainingIndices;
// We have to keep children contiguous in memory with their parents so only shuffle parents
for (int32 Index = 0; Index < Tasks.Num(); ++Index)
{
RemainingIndices.Emplace(Index);
Index += Tasks[Index].NumChildren;
}
Algo::RandomShuffle(RemainingIndices);
TArray<int32> ReverseShuffledIndices;
ReverseShuffledIndices.SetNum(Tasks.Num());
for (int32 Index = 0, ShuffledIndex = 0; Index < RemainingIndices.Num(); ++Index)
{
int32 TaskIndex = RemainingIndices[Index];
ReverseShuffledIndices[ShuffledIndex] = TaskIndex;
const int32 ShuffeldChildStart = ShuffledIndex + 1;
const int32 UnshuffeldChildStart = TaskIndex + 1;
const int32 NumChildren = Tasks[TaskIndex].NumChildren;
ShuffledIndex += 1 + NumChildren;
for (int32 ChildIndex = 0; ChildIndex < NumChildren; ++ChildIndex)
{
ReverseShuffledIndices[ShuffeldChildStart + ChildIndex] = UnshuffeldChildStart + ChildIndex;
}
}
TArray<int32> ShuffledIndices;
ShuffledIndices.SetNum(Tasks.Num());
for (int32 Index = 0; Index < ReverseShuffledIndices.Num(); ++Index)
{
const int32 OriginalTaskIndex = ReverseShuffledIndices[Index];
ShuffledIndices[OriginalTaskIndex] = Index;
}
auto RedirectMask = [&ShuffledIndices](FTaskBitSet& InOutBitSet)
{
FTaskBitSet NewBits;
for (int32 Index : InOutBitSet)
{
NewBits.SetBit(ShuffledIndices[Index]);
}
Swap(InOutBitSet, NewBits);
};
for (int32 Index = 0; Index < Tasks.Num(); ++Index)
{
FScheduledTask& Task = Tasks[Index];
if (Task.Parent)
{
Task.Parent = FTaskID(ShuffledIndices[Task.Parent.Index]);
}
RedirectMask(Task.ComputedSubsequents);
}
RedirectMask(InitialTasks);
TArray<FScheduledTask> OldTasks;
Swap(Tasks, OldTasks);
for (int32 Index = 0; Index < OldTasks.Num(); ++Index)
{
Tasks.Emplace(MoveTemp(OldTasks[ReverseShuffledIndices[Index]]));
}
}
void FEntitySystemScheduler::ExecuteTasks()
{
if (!Tasks.Num())
{
return;
}
const EEntityThreadingModel ThreadingModelToUse = EntityManager->GetThreadingModel();
const int32 PreviousNumRemaining = NumTasksRemaining.Exchange(ThreadingModelToUse, Tasks.Num());
check(PreviousNumRemaining == 0);
ThreadingModel = ThreadingModelToUse;
// In a transaction we can only support the no-threading mode.
check(!AutoRTFM::IsTransactional() || (EEntityThreadingModel::NoThreading == ThreadingModel));
WriteContextBase = FEntityAllocationWriteContext(*EntityManager);
// Condition 1: No threading
// Initiate all tasks immediately. Their subsequents will be triggered inline
if (ThreadingModel == EEntityThreadingModel::NoThreading)
{
TRACE_CPUPROFILER_EVENT_SCOPE_STR("Run Scheduled Tasks");
for (int32 Index : InitialTasks)
{
Tasks[Index].Run(this, FTaskExecutionFlags());
}
check(NumTasksRemaining.Load(ThreadingModel) == 0);
EntityManager->IncrementSystemSerial(SystemSerialIncrement);
return;
}
// Condition 2: Task graph threading
// Schedule initial tasks tasks immediately. Gamethread tasks will be added to the GT queue to ensure that threaded work can be scheduled asap.
// We need to get a game thread signal from the event pool for the execution
// which we'll return to the pool after we've completed execution.
check(!GameThreadSignal);
GameThreadSignal = FPlatformProcess::GetSynchEventFromPool();
{
TRACE_CPUPROFILER_EVENT_SCOPE_STR("Dispatch Scheduled Tasks");
int32 NumInitialTasks = 0;
for (int32 Index : InitialTasks)
{
// If it has to run on the game thread, add it to the task list.
// This allows us to schedule threaded tasks first, then run the game
// thread ones while they are in flight
if (Tasks[Index].bForceGameThread)
{
GameThreadTaskList.Push(const_cast<FScheduledTask*>(&Tasks[Index]));
}
else if (GameThreadTaskList.IsEmpty())
{
GameThreadTaskList.Push(const_cast<FScheduledTask*>(&Tasks[Index]));
}
else
{
UE::Tasks::Launch(TEXT("MovieSceneTask"), [this, Index](){
this->Tasks[Index].Run(this, FTaskExecutionFlags());
}, UE::Tasks::ETaskPriority::High);
}
++NumInitialTasks;
}
ensure(NumInitialTasks != 0);
}
TRACE_CPUPROFILER_EVENT_SCOPE_STR("Wait For Scheduled Tasks");
for(;;)
{
while (const FScheduledTask* Task = GameThreadTaskList.Pop())
{
Task->Run(this, FTaskExecutionFlags());
}
// Process exit conditions for this loop, where we have one of two situations:
//
// 1. The final task was just processed on this thread.
// In this case OnAllTasksFinished will have decremented
// NumTasksRemaining to -1 and we can break immediately.
//
// 2. The final task was completed on a thread.
// In this case, this thread may see NumTasksRemaining as either 0 or -1
// depending on how far through OnAllTasksFinished the bg thread is.
// In this case, -1 means OnAllTasksFinished has finished, 0 means it is
// part way through, so we just spin on the atomic until it hits -1
const int32 RemainingCount = NumTasksRemaining.Load(ThreadingModel);
if (RemainingCount == -1)
{
break;
}
else if (RemainingCount == 0)
{
// Handle the race condition where this code runs in-between
// NumTasksRemaining reaching 0 and the GameThreadSignal being triggered.
// If the Trigger is called before the Wait below, some platforms may deadlock
while (NumTasksRemaining.Load(ThreadingModel) != -1);
break;
}
GameThreadSignal->Wait();
}
check(NumTasksRemaining.Load(ThreadingModel) == -1);
EntityManager->IncrementSystemSerial(SystemSerialIncrement);
NumTasksRemaining.Exchange(ThreadingModel, 0);
// Lastly return the game thread signal event to the pool as we are
// done executing.
check(GameThreadSignal);
FPlatformProcess::ReturnSynchEventToPool(GameThreadSignal);
GameThreadSignal = nullptr;
}
void FEntitySystemScheduler::CompleteTask(const FScheduledTask* Task, FTaskExecutionFlags InFlags) const
{
// Reset the WaitCount ready for the next run
const int32 PreviousWaitCount = Task->WaitCount.Exchange(ThreadingModel, Task->NumPrerequisites);
const int32 PreviousChildCount = Task->ChildCompleteCount.Exchange(ThreadingModel, Task->NumChildren);
checkSlow(PreviousWaitCount == 0 && PreviousChildCount == 0);
int32 FirstInlineIndex = INDEX_NONE;
for (int32 Index : Task->ComputedSubsequents)
{
PrerequisiteCompleted(FTaskID(Index), InFlags.bCanInlineSubsequents ? &FirstInlineIndex : nullptr);
}
// Complete our parent if this is the last child
if (Task->Parent)
{
const FScheduledTask* Parent = &Tasks[Task->Parent.Index];
const int32 PreviousCompleteCount = Parent->ChildCompleteCount.Sub(ThreadingModel, 1);
if (PreviousCompleteCount == 1)
{
CompleteTask(Parent, InFlags);
}
}
if (FirstInlineIndex != INDEX_NONE)
{
Tasks[FirstInlineIndex].Run(this, FTaskExecutionFlags());
}
const int32 PreviousNumRemaining = NumTasksRemaining.Sub(ThreadingModel, 1);
if (PreviousNumRemaining == 1)
{
OnAllTasksFinished();
}
}
void FEntitySystemScheduler::PrerequisiteCompleted(FTaskID TaskID, int32* OptRunInlineIndex) const
{
PrerequisiteCompleted(&Tasks[TaskID.Index], OptRunInlineIndex);
}
void FEntitySystemScheduler::PrerequisiteCompleted(const FScheduledTask* Task, int32* OptRunInlineIndex) const
{
// We either need to not be using threading, or have a valid game thread signal event!
check((ThreadingModel == EEntityThreadingModel::NoThreading) || (GameThreadSignal != nullptr));
int32 PreviousWaitCount = Task->WaitCount.Sub(ThreadingModel, 1);
if (PreviousWaitCount <= 1)
{
if (PreviousWaitCount <= 0)
{
// This is an error
ensureMsgf(false, TEXT("Sequencer Task Prerequisite Count underflow!"));
if (GameThreadSignal)
{
// Trigger the game thread to wake up if necessary
GameThreadSignal->Trigger();
}
}
else if (ThreadingModel == EEntityThreadingModel::NoThreading)
{
Task->Run(this, FTaskExecutionFlags());
}
else if (Task->bForceInline)
{
FTaskExecutionFlags Flags;
// Don't let the completion of this task inline any others
// to prevent cascades of inlined tasks suffocating the dispatch of others
Flags.bCanInlineSubsequents = false;
Task->Run(this, Flags);
}
else if (Task->bForceGameThread)
{
// Push this onto the GT list even if we are already on the game thread.
// This ensures other subsequent tasks being processed in the same loop
// have a chance to be scheduled before we do any potentially time-consuming task work.
GameThreadTaskList.Push(const_cast<FScheduledTask*>(Task));
GameThreadSignal->Trigger();
}
else if (OptRunInlineIndex && *OptRunInlineIndex == INDEX_NONE)
{
const int32 TaskIndex = (Task - Tasks.GetData());
*OptRunInlineIndex = TaskIndex;
}
else if (GameThreadTaskList.IsEmpty())
{
GameThreadTaskList.Push(const_cast<FScheduledTask*>(Task));
GameThreadSignal->Trigger();
}
else
{
UE::Tasks::Launch(TEXT("MovieSceneTask"), [this, Task](){
Task->Run(this, FTaskExecutionFlags());
}, UE::Tasks::ETaskPriority::High);
}
}
}
void FEntitySystemScheduler::OnAllTasksFinished() const
{
if (ThreadingModel != EEntityThreadingModel::NoThreading)
{
GameThreadSignal->Trigger();
NumTasksRemaining.Sub(ThreadingModel, 1);
}
}
bool FEntitySystemScheduler::HasAnyTasksToPropagateDownstream() const
{
return CurrentSubsequents.SystemWidePrerequisites.CountSetBits() != 0
|| CurrentSubsequents.ForcedSystemWidePrerequisites.CountSetBits() != 0;
}
void FEntitySystemScheduler::BeginConstruction()
{
WriteContextBase = FEntityAllocationWriteContext(*EntityManager);
SystemSerialIncrement = EntityManager->GetSystemSerial();
Tasks.Reset();
InitialTasks = FTaskBitSet();
}
void FEntitySystemScheduler::BeginSystem(uint16 NodeID)
{
CurrentSubsequents.Reset();
CurrentPrerequisites = AllPrerequisites.Find(NodeID);
}
void FEntitySystemScheduler::PropagatePrerequisite(uint16 ToNodeID)
{
FTaskPrerequisiteCache& DownstreamPrerequisites = AllPrerequisites.FindOrAdd(ToNodeID);
DownstreamPrerequisites.SystemWidePrerequisites |= CurrentSubsequents.SystemWidePrerequisites;
DownstreamPrerequisites.ForcedSystemWidePrerequisites |= CurrentSubsequents.ForcedSystemWidePrerequisites;
}
void FEntitySystemScheduler::EndSystem(uint16 NodeID)
{
}
void FEntitySystemScheduler::EndConstruction()
{
// SystemSerialIncrement is currently the system serial number from BeginConstruction
// Make this a diff by subtracting the current system serial so we can increment by it each time we run our tasks
const uint64 CurrentSerial = EntityManager->GetSystemSerial();
if (SystemSerialIncrement <= CurrentSerial)
{
SystemSerialIncrement = CurrentSerial - SystemSerialIncrement;
}
else
{
SystemSerialIncrement = 0;
}
Tasks.Shrink();
AllPrerequisites.Empty();
ComponentDepedenciesByAllocation.Empty();
CurrentSubsequents.Reset();
CurrentPrerequisites = nullptr;
for (int32 Index = 0; Index < Tasks.Num(); ++Index)
{
FScheduledTask& Task = Tasks[Index];
Task.WaitCount.Exchange(ThreadingModel, Task.NumPrerequisites);
Task.ChildCompleteCount.Exchange(ThreadingModel, Task.NumChildren);
if (Task.NumPrerequisites == 0)
{
InitialTasks.SetBit(Index);
}
}
#if WITH_AUTOMATION_TESTS && (!UE_BUILD_SHIPPING && !UE_BUILD_TEST)
if (GIsAutomationTesting)
{
ShuffleTasks();
}
#endif
UE_LOG(LogMovieSceneECS, VeryVerbose, TEXT("Finalized building task graph:\n %s"), *ToString());
}
FString FEntitySystemScheduler::ToString() const
{
TStringBuilder<1024> GraphString;
GraphString += TEXT("\ndigraph FEntitySystemScheduler {\n");
GraphString += TEXT("\trankdir=\"LR\"\n");
GraphString += TEXT("\tcompound=true\n");
GraphString += TEXT("\tnode [shape=record,height=.1];\n");
TBitArray<> ParentTasks(false, Tasks.Num());
TBitArray<> ChildTasks(false, Tasks.Num());
for (int32 Index = 0; Index < Tasks.Num(); ++Index)
{
const FScheduledTask& Task = Tasks[Index];
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
const TCHAR* DebugName = *Task.DebugName;
#else
const TCHAR* DebugName = TEXT("");
#endif
FString SanitizedName(DebugName);
SanitizedName.ReplaceCharInline('<', ' ');
SanitizedName.ReplaceCharInline('>', ' ');
if (Task.NumChildren != 0)
{
ParentTasks[Index] = true;
GraphString.Appendf(TEXT("\tsubgraph cluster_%d{ \n\t label=\"Parent Task %d: %s\";\n"), Index, Index, *SanitizedName);
GraphString.Appendf(TEXT("\t\ttask_%d[label=\"[All Children]\" style=invis];\n"), Index);
const int32 FirstChild = Index+1;
for (int32 ChildIndex = FirstChild; ChildIndex < FirstChild + Task.NumChildren; ++ChildIndex)
{
ChildTasks[ChildIndex] = true;
GraphString.Appendf(TEXT("\t\ttask_%d;\n"), ChildIndex);
}
GraphString.Append(TEXT("\t}\n"));
}
else
{
GraphString.Appendf(TEXT("\ttask_%d[label=\"Task %d: %s (%d prerequisites)\"];\n"),
Index, Index, *SanitizedName, Task.NumPrerequisites);
}
}
GraphString += TEXT("\n\n");
// Draw edges
for (int32 Index = 0; Index < Tasks.Num(); ++Index)
{
const FScheduledTask& Task = Tasks[Index];
FString Color = FLinearColor::MakeRandomColor().ToFColorSRGB().ToHex();
if (ParentTasks[Index])
{
for (int32 SubsequentIndex : Task.ComputedSubsequents)
{
if (ParentTasks[SubsequentIndex])
{
GraphString.Appendf(TEXT("\ttask_%d -> task_%d [ltail=cluster_%d, lhead=cluster_%d, color=\"#%s\"];\n"), Index, SubsequentIndex, Index, SubsequentIndex, *Color);
}
else
{
GraphString.Appendf(TEXT("\ttask_%d -> task_%d [ltail=cluster_%d, color=\"#%s\"];\n"), Index, SubsequentIndex, Index, *Color);
}
}
}
else
{
for (int32 SubsequentIndex : Task.ComputedSubsequents)
{
if (ParentTasks[SubsequentIndex])
{
GraphString.Appendf(TEXT("\ttask_%d -> task_%d [lhead=cluster_%d, color=\"#%s\"];\n"), Index, SubsequentIndex, SubsequentIndex, *Color);
}
else
{
GraphString.Appendf(TEXT("\ttask_%d -> task_%d [color=\"#%s\"];\n"), Index, SubsequentIndex, *Color);
}
}
}
}
GraphString += TEXT("}\n");
return GraphString.ToString();
}
} // namespace UE::MovieScene