convert atom to task graph (#4230)

* Intial attempt to convert the Atom/RHI/FrameScheduler to use the new TaskGraph api

Signed-off-by: rgba16f <82187279+rgba16f@users.noreply.github.com>

* Avoid enqueuing work on the active task thread if the submitted task
graph is waitable

When submitting a task graph, supplying a wait event implies that
dependent jobs must occur on threads that do not wait on the event (in
the absence of work stealing). This change prevents this by adding a
notion of a task thread enable/disable state, and prohibiting dependent
jobs from being enqueued on waiting threads.

Signed-off-by: Jeremy Ong <jcong@amazon.com>

* Convert RPI/Scene to use TaskGraph pass 1, Culling jobs remain on the old system

Signed-off-by: rgba16f <82187279+rgba16f@users.noreply.github.com>

* RemoveTask Graph changes from the FrameScheduler::ExecuteGroups, use old job system instead

Signed-off-by: rgba16f <82187279+rgba16f@users.noreply.github.com>

* Per review, removing commented out code

Signed-off-by: rgba16f <82187279+rgba16f@users.noreply.github.com>

* Cleanup debug code, & build fix

Signed-off-by: rgba16f <82187279+rgba16f@users.noreply.github.com>

* Add a cvar & interface to query whether to use jobs or task graph

Signed-off-by: rgba16f <82187279+rgba16f@users.noreply.github.com>

* Make TaskGraph assert if you try to wait inside a job

Signed-off-by: rgba16f <82187279+rgba16f@users.noreply.github.com>

* Fix TaskTest SpawnSubgraph to account for the new TaskGraphEvent assert on wait in a running task

Signed-off-by: rgba16f <82187279+rgba16f@users.noreply.github.com>

* 3 minor cleanups. 1) Events always store a ptr to their executor 2) Fix clang compile error 3) remove an early out.

Signed-off-by: rgba16f <82187279+rgba16f@users.noreply.github.com>

* Fix double group end that was causing assert/crash plus misc minor diff's with development

Signed-off-by: rgba16f <82187279+rgba16f@users.noreply.github.com>

* Fix deallocation failure on deactivation of the TaskGraphSystemComponent. Also make the system component account for multiple creation in Unit Tests.

Signed-off-by: rgba16f <82187279+rgba16f@users.noreply.github.com>

* Update with PR feedback
1) Rename UseTaskGraph to IsTaskGraphActive & update related code
2) prefer TaskExecutor::SetInstance
3) add comments and remove commented out code

Signed-off-by: rgba16f <82187279+rgba16f@users.noreply.github.com>

* Fix incorrect RTTI name for TaskGraphActiveInterface

Signed-off-by: rgba16f <82187279+rgba16f@users.noreply.github.com>

* Move TaskGraphSystemComponent CRC calculation to a shared variable

Signed-off-by: rgba16f <82187279+rgba16f@users.noreply.github.com>

Co-authored-by: Jeremy Ong <jcong@amazon.com>
This commit is contained in:
rgba16f [Amazon]
2021-09-30 17:45:33 -05:00
committed by GitHub
parent 60a0d2ba01
commit e1c49e436d
18 changed files with 652 additions and 130 deletions
@@ -22,6 +22,7 @@
#include <AzCore/Time/TimeSystemComponent.h>
#include <AzCore/Console/LoggerSystemComponent.h>
#include <AzCore/EBus/EventSchedulerSystemComponent.h>
#include <AzCore/Task/TaskGraphSystemComponent.h>
namespace AZ
{
@@ -41,6 +42,7 @@ namespace AZ
TimeSystemComponent::CreateDescriptor(),
LoggerSystemComponent::CreateDescriptor(),
EventSchedulerSystemComponent::CreateDescriptor(),
TaskGraphSystemComponent::CreateDescriptor(),
#if !defined(AZCORE_EXCLUDE_LUA)
ScriptSystemComponent::CreateDescriptor(),
@@ -55,6 +57,7 @@ namespace AZ
azrtti_typeid<TimeSystemComponent>(),
azrtti_typeid<LoggerSystemComponent>(),
azrtti_typeid<EventSchedulerSystemComponent>(),
azrtti_typeid<TaskGraphSystemComponent>(),
};
}
}
@@ -190,11 +190,13 @@ namespace AZ
class TaskWorker
{
public:
void Spawn(::AZ::TaskExecutor& executor, size_t id, AZStd::semaphore& initSemaphore, bool affinitize)
static thread_local TaskWorker* t_worker;
void Spawn(::AZ::TaskExecutor& executor, uint32_t id, AZStd::semaphore& initSemaphore, bool affinitize)
{
m_executor = &executor;
AZStd::string threadName = AZStd::string::format("TaskWorker %zu", id);
AZStd::string threadName = AZStd::string::format("TaskWorker %u", id);
AZStd::thread_desc desc = {};
desc.m_name = threadName.c_str();
if (affinitize)
@@ -205,12 +207,29 @@ namespace AZ
m_thread = AZStd::thread{ [this, &initSemaphore]
{
t_worker = this;
initSemaphore.release();
Run();
},
&desc };
}
// Threads that wait on a graph to complete are disqualified from receiving tasks until the wait finishes
void Disable()
{
m_enabled = false;
}
void Enable()
{
m_enabled = true;
}
bool Enabled() const
{
return m_enabled;
}
void Join()
{
m_active.store(false, AZStd::memory_order_release);
@@ -222,11 +241,7 @@ namespace AZ
{
m_queue.Enqueue(task);
if (!m_busy.exchange(true))
{
// The worker was idle prior to enqueueing the task, release the semaphore
m_semaphore.release();
}
m_semaphore.release();
}
private:
@@ -234,7 +249,6 @@ namespace AZ
{
while (m_active)
{
m_busy = false;
m_semaphore.acquire();
if (!m_active)
@@ -242,8 +256,6 @@ namespace AZ
return;
}
m_busy = true;
Task* task = m_queue.TryDequeue();
while (task)
{
@@ -271,12 +283,15 @@ namespace AZ
AZStd::thread m_thread;
AZStd::atomic<bool> m_active;
AZStd::atomic<bool> m_busy;
AZStd::atomic<bool> m_enabled = true;
AZStd::binary_semaphore m_semaphore;
::AZ::TaskExecutor* m_executor;
TaskQueue m_queue;
friend class ::AZ::TaskExecutor;
};
thread_local TaskWorker* TaskWorker::t_worker = nullptr;
} // namespace Internal
static EnvironmentVariable<TaskExecutor*> s_executor;
@@ -291,13 +306,16 @@ namespace AZ
return **s_executor;
}
// TODO: Create the default executor as part of a component (as in TaskManagerComponent)
void TaskExecutor::SetInstance(TaskExecutor* executor)
{
AZ_Assert(!s_executor, "Attempting to set the global task executor more than once");
s_executor = AZ::Environment::CreateVariable<TaskExecutor*>("GlobalTaskExecutor");
s_executor.Set(executor);
if (!executor)
{
s_executor.Reset();
}
else if (!s_executor) // ignore any calls to set after the first (this happens in unit tests that create new system entities)
{
s_executor = AZ::Environment::CreateVariable<TaskExecutor*>(s_executorName, executor);
}
}
TaskExecutor::TaskExecutor(uint32_t threadCount)
@@ -307,14 +325,12 @@ namespace AZ
m_workers = reinterpret_cast<Internal::TaskWorker*>(azmalloc(m_threadCount * sizeof(Internal::TaskWorker)));
bool affinitize = m_threadCount == AZStd::thread::hardware_concurrency();
AZStd::semaphore initSemaphore;
for (size_t i = 0; i != m_threadCount; ++i)
for (uint32_t i = 0; i != m_threadCount; ++i)
{
new (m_workers + i) Internal::TaskWorker{};
m_workers[i].Spawn(*this, i, initSemaphore, affinitize);
m_workers[i].Spawn(*this, i, initSemaphore, false);
}
for (size_t i = 0; i != m_threadCount; ++i)
@@ -334,9 +350,21 @@ namespace AZ
azfree(m_workers);
}
void TaskExecutor::Submit(Internal::CompiledTaskGraph& graph)
Internal::TaskWorker* TaskExecutor::GetTaskWorker()
{
if (Internal::TaskWorker::t_worker && Internal::TaskWorker::t_worker->m_executor == this)
{
return Internal::TaskWorker::t_worker;
}
return nullptr;
}
void TaskExecutor::Submit(Internal::CompiledTaskGraph& graph, TaskGraphEvent* event)
{
++m_graphsRemaining;
event->m_executor = this; // Used to validate event is not waited for inside a job
// Submit all tasks that have no inbound edges
for (Internal::Task& task : graph.Tasks())
{
@@ -352,11 +380,24 @@ namespace AZ
// TODO: Something more sophisticated is likely needed here.
// First, we are completely ignoring affinity.
// Second, some heuristics on core availability will help distribute work more effectively
m_workers[++m_lastSubmission % m_threadCount].Enqueue(&task);
uint32_t nextWorker = ++m_lastSubmission % m_threadCount;
while (!m_workers[nextWorker].Enabled())
{
// Graphs that are waiting for the completion of a task graph cannot enqueue tasks onto
// the thread issuing the wait.
nextWorker = ++m_lastSubmission % m_threadCount;
}
m_workers[nextWorker].Enqueue(&task);
}
void TaskExecutor::ReleaseGraph()
{
--m_graphsRemaining;
}
void TaskExecutor::ReactivateTaskWorker()
{
GetTaskWorker()->Enable();
}
} // namespace AZ
@@ -72,14 +72,19 @@ namespace AZ
explicit TaskExecutor(uint32_t threadCount = 0);
~TaskExecutor();
void Submit(Internal::CompiledTaskGraph& graph);
// Submit a task graph for execution. Waitable task graphs cannot enqueue work on the task thread
// that is currently active
void Submit(Internal::CompiledTaskGraph& graph, TaskGraphEvent* event);
void Submit(Internal::Task& task);
private:
friend class Internal::TaskWorker;
friend class TaskGraphEvent;
Internal::TaskWorker* GetTaskWorker();
void ReleaseGraph();
void ReactivateTaskWorker();
Internal::TaskWorker* m_workers;
uint32_t m_threadCount = 0;
@@ -14,6 +14,12 @@ namespace AZ
{
using Internal::CompiledTaskGraph;
void TaskGraphEvent::Wait()
{
AZ_Assert(m_executor->GetTaskWorker() == nullptr, "Waiting in a task is unsupported");
m_semaphore.acquire();
}
void TaskToken::PrecedesInternal(TaskToken& comesAfter)
{
AZ_Assert(!m_parent.m_submitted, "Cannot mutate a TaskGraph that was previously submitted.");
@@ -71,7 +77,7 @@ namespace AZ
m_compiledTaskGraph->m_tasks[i].Init();
}
executor.Submit(*m_compiledTaskGraph);
executor.Submit(*m_compiledTaskGraph, waitEvent);
if (m_retained)
{
@@ -22,10 +22,19 @@ namespace AZ
namespace Internal
{
class CompiledTaskGraph;
class TaskWorker;
}
class TaskExecutor;
class TaskGraph;
class TaskGraphActiveInterface
{
public:
AZ_RTTI(TaskGraphActiveInterface, "{08118074-B139-4EF9-B8FD-29F1D6DC9233}");
virtual bool IsTaskGraphActive() const = 0;
};
// A TaskToken is returned each time a Task is added to the TaskGraph. TaskTokens are used to
// express dependencies between tasks within the graph, and have no purpose after the graph
// is submitted (simply let them go out of scope)
@@ -70,9 +79,12 @@ namespace AZ
private:
friend class ::AZ::Internal::CompiledTaskGraph;
friend class TaskGraph;
friend class TaskExecutor;
void Signal();
AZStd::binary_semaphore m_semaphore;
TaskExecutor* m_executor = nullptr;
};
// The TaskGraph encapsulates a set of tasks and their interdependencies. After adding
@@ -89,6 +101,9 @@ namespace AZ
// Reset the state of the task graph to begin recording tasks and edges again
// NOTE: Graph must be in a "settled" state (cannot be in-flight)
void Reset();
// Returns false if 1 or more tasks have been added to the graph
bool IsEmpty();
// Add a task to the graph, retrieiving a token that can be used to express dependencies
// between tasks. The first argument specifies the TaskKind, used for tracking the task.
@@ -33,11 +33,6 @@ namespace AZ
return m_semaphore.try_acquire_for(AZStd::chrono::milliseconds{ 0 });
}
inline void TaskGraphEvent::Wait()
{
m_semaphore.acquire();
}
inline void TaskGraphEvent::Signal()
{
m_semaphore.release();
@@ -59,6 +54,11 @@ namespace AZ
return { AddTask(descriptor, AZStd::forward<Lambdas>(lambdas))... };
}
inline bool TaskGraph::IsEmpty()
{
return m_tasks.empty();
}
inline void TaskGraph::Detach()
{
m_retained = false;
@@ -0,0 +1,88 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <AzCore/Console/IConsole.h>
#include <AzCore/Interface/Interface.h>
#include <AzCore/Task/TaskGraphSystemComponent.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/Serialization/EditContext.h>
// Create a cvar as a central location for experimentation with switching from the Job system to TaskGraph system.
AZ_CVAR(bool, cl_activateTaskGraph, false, nullptr, AZ::ConsoleFunctorFlags::Null, "Flag clients of TaskGraph to switch between jobs/taskgraph (Note does not disable task graph system)");
static constexpr uint32_t TaskExecutorServiceCrc = AZ_CRC_CE("TaskExecutorService");
namespace AZ
{
void TaskGraphSystemComponent::Activate()
{
AZ_Assert(m_taskExecutor == nullptr, "Error multiple activation of the TaskGraphSystemComponent");
if (Interface<TaskGraphActiveInterface>::Get() == nullptr)
{
Interface<TaskGraphActiveInterface>::Register(this);
m_taskExecutor = aznew TaskExecutor();
TaskExecutor::SetInstance(m_taskExecutor);
}
}
void TaskGraphSystemComponent::Deactivate()
{
if (&TaskExecutor::Instance() == m_taskExecutor) // check that our instance is the global instance (not always true in unit tests)
{
m_taskExecutor->SetInstance(nullptr);
}
if (m_taskExecutor)
{
azdestroy(m_taskExecutor);
m_taskExecutor = nullptr;
}
if (Interface<TaskGraphActiveInterface>::Get() == this)
{
Interface<TaskGraphActiveInterface>::Unregister(this);
}
}
void TaskGraphSystemComponent::GetProvidedServices(ComponentDescriptor::DependencyArrayType& provided)
{
provided.push_back(TaskExecutorServiceCrc);
}
void TaskGraphSystemComponent::GetIncompatibleServices(ComponentDescriptor::DependencyArrayType& incompatible)
{
incompatible.push_back(TaskExecutorServiceCrc);
}
void TaskGraphSystemComponent::GetDependentServices([[maybe_unused]] ComponentDescriptor::DependencyArrayType& dependent)
{
}
void TaskGraphSystemComponent::Reflect(ReflectContext* context)
{
if (SerializeContext* serializeContext = azrtti_cast<SerializeContext*>(context))
{
serializeContext->Class<TaskGraphSystemComponent, AZ::Component>()
->Version(1)
;
if (AZ::EditContext* ec = serializeContext->GetEditContext())
{
ec->Class<TaskGraphSystemComponent>
("TaskGraph", "System component to create the default executor")
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
->Attribute(AZ::Edit::Attributes::Category, "Engine")
->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("System"))
;
}
}
}
bool TaskGraphSystemComponent::IsTaskGraphActive() const
{
return cl_activateTaskGraph;
}
} // namespace AZ
@@ -0,0 +1,47 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <AzCore/Component/Component.h>
#include <AzCore/Math/Crc.h>
#include <AzCore/Task/TaskExecutor.h>
#include <AzCore/Task/TaskGraph.h>
namespace AZ
{
class TaskGraphSystemComponent
: public Component
, public TaskGraphActiveInterface
{
public:
AZ_COMPONENT(AZ::TaskGraphSystemComponent, "{5D56B829-1FEB-43D5-A0BD-E33C0497EFE2}")
TaskGraphSystemComponent() = default;
// Implement TaskGraphActiveInterface
bool IsTaskGraphActive() const override;
private:
//////////////////////////////////////////////////////////////////////////
// Component base
void Activate() override;
void Deactivate() override;
//////////////////////////////////////////////////////////////////////////
/// \ref ComponentDescriptor::GetProvidedServices
static void GetProvidedServices(ComponentDescriptor::DependencyArrayType& provided);
/// \ref ComponentDescriptor::GetIncompatibleServices
static void GetIncompatibleServices(ComponentDescriptor::DependencyArrayType& incompatible);
/// \ref ComponentDescriptor::GetDependentServices
static void GetDependentServices(ComponentDescriptor::DependencyArrayType& dependent);
/// \red ComponentDescriptor::Reflect
static void Reflect(ReflectContext* reflection);
AZ::TaskExecutor* m_taskExecutor = nullptr;
};
}
@@ -633,6 +633,8 @@ set(FILES
Task/TaskGraph.cpp
Task/TaskGraph.h
Task/TaskGraph.inl
Task/TaskGraphSystemComponent.h
Task/TaskGraphSystemComponent.cpp
Threading/ThreadSafeDeque.h
Threading/ThreadSafeDeque.inl
Threading/ThreadSafeObject.h
+81 -3
View File
@@ -34,7 +34,7 @@ namespace UnitTest
AZ::AllocatorInstance<AZ::PoolAllocator>::Create();
AZ::AllocatorInstance<AZ::ThreadPoolAllocator>::Create();
m_executor = aznew TaskExecutor(4);
m_executor = aznew TaskExecutor();
}
void TearDown() override
@@ -236,6 +236,82 @@ namespace UnitTest
EXPECT_EQ(x, 1);
}
TEST_F(TaskGraphTestFixture, SingleTask)
{
AZStd::atomic_int32_t x = 0;
TaskGraph graph;
graph.AddTask(
defaultTD,
[&x]
{
x = 1;
});
TaskGraphEvent ev;
graph.SubmitOnExecutor(*m_executor, &ev);
ev.Wait();
EXPECT_EQ(1, x);
}
TEST_F(TaskGraphTestFixture, SingleTaskChain)
{
AZStd::atomic_int32_t x = 0;
TaskGraph graph;
auto a = graph.AddTask(
defaultTD,
[&x]
{
x += 1;
});
auto b = graph.AddTask(
defaultTD,
[&x]
{
x += 1;
});
b.Precedes(a);
TaskGraphEvent ev;
graph.SubmitOnExecutor(*m_executor, &ev);
ev.Wait();
EXPECT_EQ(2, x);
}
TEST_F(TaskGraphTestFixture, MultipleIndependentTaskChains)
{
AZStd::atomic_int32_t x = 0;
constexpr int numChains = 5;
TaskGraph graph;
for( int i = 0; i < numChains; ++i)
{
auto a = graph.AddTask(
defaultTD,
[&x]
{
x += 1;
});
auto b = graph.AddTask(
defaultTD,
[&x]
{
x += 1;
});
b.Precedes(a);
}
TaskGraphEvent ev;
graph.SubmitOnExecutor(*m_executor, &ev);
ev.Wait();
EXPECT_EQ(2*numChains, x);
}
TEST_F(TaskGraphTestFixture, VariadicInterface)
{
int x = 0;
@@ -388,6 +464,7 @@ namespace UnitTest
EXPECT_EQ(3, x);
}
// Waiting inside a task is disallowed , test that it fails correctly
TEST_F(TaskGraphTestFixture, SpawnSubgraph)
{
AZStd::atomic<int> x = 0;
@@ -434,7 +511,10 @@ namespace UnitTest
f.Precedes(g);
TaskGraphEvent ev;
subgraph.SubmitOnExecutor(*m_executor, &ev);
// TaskGraphEvent::Wait asserts if called on a worker thread, suppress & validate assert
AZ_TEST_START_TRACE_SUPPRESSION;
ev.Wait();
AZ_TEST_STOP_TRACE_SUPPRESSION(1);
});
auto d = graph.AddTask(
defaultTD,
@@ -464,8 +544,6 @@ namespace UnitTest
TaskGraphEvent ev;
graph.SubmitOnExecutor(*m_executor, &ev);
ev.Wait();
EXPECT_EQ(3 | 0b100000, x);
}
TEST_F(TaskGraphTestFixture, RetainedGraph)
@@ -28,6 +28,7 @@
#include <AzCore/NativeUI/NativeUISystemComponent.h>
#include <AzCore/Module/ModuleManagerBus.h>
#include <AzCore/Interface/Interface.h>
#include <AzCore/Task/TaskGraphSystemComponent.h>
#include <AzFramework/Asset/SimpleAsset.h>
#include <AzFramework/Asset/AssetBundleManifest.h>
@@ -295,6 +296,7 @@ namespace AzFramework
azrtti_typeid<AZ::ScriptSystemComponent>(),
azrtti_typeid<AZ::JobManagerComponent>(),
azrtti_typeid<AZ::SliceSystemComponent>(),
azrtti_typeid<AZ::TaskGraphSystemComponent>(),
azrtti_typeid<AzFramework::AssetCatalogComponent>(),
azrtti_typeid<AzFramework::CustomAssetTypeComponent>(),
@@ -23,6 +23,7 @@
namespace AZ
{
class Job;
class TaskGraphActiveInterface;
namespace RHI
{
@@ -228,6 +229,8 @@ namespace AZ
// list of RayTracingShaderTables that should be built this frame
AZStd::vector<RHI::Ptr<RayTracingShaderTable>> m_rayTracingShaderTablesToBuild;
AZ::TaskGraphActiveInterface* m_taskGraphActive = nullptr;
};
}
}
@@ -25,9 +25,11 @@
#include <Atom/RHI/RayTracingShaderTable.h>
#include <AzCore/Debug/EventTrace.h>
#include <AzCore/Interface/Interface.h>
#include <AzCore/Jobs/Algorithms.h>
#include <AzCore/Jobs/JobCompletion.h>
#include <AzCore/Jobs/JobFunction.h>
#include <AzCore/Task/TaskGraph.h>
namespace AZ
{
@@ -77,6 +79,8 @@ namespace AZ
m_rootScope = m_rootScopeProducer->GetScope();
m_device = &device;
m_taskGraphActive = AZ::Interface<AZ::TaskGraphActiveInterface>::Get();
m_lastFrameEndTime = AZStd::GetTimeNowTicks();
return ResultCode::Success;
@@ -85,6 +89,7 @@ namespace AZ
void FrameScheduler::Shutdown()
{
m_device = nullptr;
m_taskGraphActive = nullptr;
m_rootScopeProducer = nullptr;
m_rootScope = nullptr;
m_frameGraphExecuter = nullptr;
@@ -258,50 +263,98 @@ namespace AZ
if (m_compileRequest.m_jobPolicy == JobPolicy::Parallel)
{
const auto compileGroupsBeginFunction = [](ShaderResourceGroupPool* srgPool)
{
srgPool->CompileGroupsBegin();
};
resourcePoolDatabase.ForEachShaderResourceGroupPool<decltype(compileGroupsBeginFunction)>(compileGroupsBeginFunction);
// Iterate over each SRG pool and fork jobs to compile SRGs.
const uint32_t compilesPerJob = m_compileRequest.m_shaderResourceGroupCompilesPerJob;
AZ::JobCompletion jobCompletion;
const auto compileIntervalsFunction = [compilesPerJob, &jobCompletion](ShaderResourceGroupPool* srgPool)
if (m_taskGraphActive && m_taskGraphActive->IsTaskGraphActive())
{
const uint32_t compilesInPool = srgPool->GetGroupsToCompileCount();
const uint32_t jobCount = DivideByMultiple(compilesInPool, compilesPerJob);
AZ::TaskGraph taskGraph;
for (uint32_t i = 0; i < jobCount; ++i)
const auto compileIntervalsFunction = [compilesPerJob, &taskGraph](ShaderResourceGroupPool* srgPool)
{
Interval interval;
interval.m_min = i * compilesPerJob;
interval.m_max = AZStd::min(interval.m_min + compilesPerJob, compilesInPool);
srgPool->CompileGroupsBegin();
const uint32_t compilesInPool = srgPool->GetGroupsToCompileCount();
const uint32_t jobCount = DivideByMultiple(compilesInPool, compilesPerJob);
AZ::TaskDescriptor srgCompileDesc{"SrgCompile", "Graphics"};
AZ::TaskDescriptor srgCompileEndDesc{"SrgCompileEnd", "Graphics"};
const auto compileGroupsForIntervalLambda = [srgPool, interval]()
auto srgCompileEndTask = taskGraph.AddTask(
srgCompileEndDesc,
[srgPool]()
{
srgPool->CompileGroupsEnd();
});
for (uint32_t i = 0; i < jobCount; ++i)
{
AZ_PROFILE_SCOPE(RHI, "FrameScheduler : compileGroupsForIntervalLambda");
srgPool->CompileGroupsForInterval(interval);
};
Interval interval;
interval.m_min = i * compilesPerJob;
interval.m_max = AZStd::min(interval.m_min + compilesPerJob, compilesInPool);
AZ::Job* executeGroupJob = AZ::CreateJobFunction(AZStd::move(compileGroupsForIntervalLambda), true, nullptr);
executeGroupJob->SetDependent(&jobCompletion);
executeGroupJob->Start();
auto compileTask = taskGraph.AddTask(
srgCompileDesc,
[srgPool, interval]()
{
AZ_PROFILE_SCOPE(RHI, "FrameScheduler : compileGroupsForIntervalLambda");
srgPool->CompileGroupsForInterval(interval);
});
compileTask.Precedes(srgCompileEndTask);
}
};
resourcePoolDatabase.ForEachShaderResourceGroupPool<decltype(compileIntervalsFunction)>(AZStd::move(compileIntervalsFunction));
if (!taskGraph.IsEmpty())
{
AZ::TaskGraphEvent finishedEvent;
taskGraph.Submit(&finishedEvent);
finishedEvent.Wait();
}
};
resourcePoolDatabase.ForEachShaderResourceGroupPool<decltype(compileIntervalsFunction)>(AZStd::move(compileIntervalsFunction));
jobCompletion.StartAndWaitForCompletion();
const auto compileGroupsEndFunction = [](ShaderResourceGroupPool* srgPool)
}
else // use Job system
{
srgPool->CompileGroupsEnd();
};
const auto compileGroupsBeginFunction = [](ShaderResourceGroupPool* srgPool)
{
srgPool->CompileGroupsBegin();
};
resourcePoolDatabase.ForEachShaderResourceGroupPool<decltype(compileGroupsEndFunction)>(compileGroupsEndFunction);
resourcePoolDatabase.ForEachShaderResourceGroupPool<decltype(compileGroupsBeginFunction)>(compileGroupsBeginFunction);
// Iterate over each SRG pool and fork jobs to compile SRGs.
AZ::JobCompletion jobCompletion;
const auto compileIntervalsFunction = [compilesPerJob, &jobCompletion](ShaderResourceGroupPool* srgPool)
{
const uint32_t compilesInPool = srgPool->GetGroupsToCompileCount();
const uint32_t jobCount = DivideByMultiple(compilesInPool, compilesPerJob);
for (uint32_t i = 0; i < jobCount; ++i)
{
Interval interval;
interval.m_min = i * compilesPerJob;
interval.m_max = AZStd::min(interval.m_min + compilesPerJob, compilesInPool);
const auto compileGroupsForIntervalLambda = [srgPool, interval]()
{
AZ_PROFILE_SCOPE(RHI, "FrameScheduler : compileGroupsForIntervalLambda");
srgPool->CompileGroupsForInterval(interval);
};
AZ::Job* executeGroupJob = AZ::CreateJobFunction(AZStd::move(compileGroupsForIntervalLambda), true, nullptr);
executeGroupJob->SetDependent(&jobCompletion);
executeGroupJob->Start();
}
};
resourcePoolDatabase.ForEachShaderResourceGroupPool<decltype(compileIntervalsFunction)>(AZStd::move(compileIntervalsFunction));
jobCompletion.StartAndWaitForCompletion();
const auto compileGroupsEndFunction = [](ShaderResourceGroupPool* srgPool)
{
srgPool->CompileGroupsEnd();
};
resourcePoolDatabase.ForEachShaderResourceGroupPool<decltype(compileGroupsEndFunction)>(compileGroupsEndFunction);
}
}
else
{
@@ -33,6 +33,7 @@ namespace AZ
// Use separate work submission queue from the hw copy queue to avoid the per frame sync.
m_copyQueue = CommandQueue::Create();
m_copyQueue->SetName(AZ::Name("AsyncUpload Queue"));
RHI::CommandQueueDescriptor commandQueueDescriptor;
commandQueueDescriptor.m_hardwareQueueClass = RHI::HardwareQueueClass::Copy;
@@ -29,6 +29,7 @@
#include <AzCore/Memory/SystemAllocator.h>
#include <AzCore/RTTI/RTTI.h>
#include <AzCore/Script/ScriptTimePoint.h>
#include <AzCore/Task/TaskGraph.h>
#include <AzFramework/Scene/Scene.h>
#include <AzFramework/Scene/SceneSystemInterface.h>
@@ -194,6 +195,9 @@ namespace AZ
// This function is called every time scene's render pipelines change.
void RebuildPipelineStatesLookup();
// Helper function to wait for end of TaskGraph
void WaitTGEvent(AZ::TaskGraphEvent& completionTGEvent, AZStd::atomic_bool* workToWaitOn = nullptr);
// Helper function for wait and clean up a completion job
void WaitAndCleanCompletionJob(AZ::JobCompletion*& completionJob);
@@ -204,12 +208,26 @@ namespace AZ
// This happens in UpdateSrgs()
void PrepareSceneSrg();
// Implementation functions that allow scene to switch between using Jobs or TaskGraphs
void SimulateTaskGraph();
void SimulateJobs();
void CollectDrawPacketsTaskGraph();
void CollectDrawPacketsJobs();
void FinalizeDrawListsTaskGraph();
void FinalizeDrawListsJobs();
// List of feature processors that are active for this scene
AZStd::vector<FeatureProcessorPtr> m_featureProcessors;
// List of pipelines of this scene. Each pipeline has an unique pipeline Id.
AZStd::vector<RenderPipelinePtr> m_pipelines;
// CPU simulation TaskGraphEvent to wait for completion of all the simulation tasks
AZ::TaskGraphEvent m_simulationFinishedTGEvent;
AZStd::atomic_bool m_simulationFinishedWorkActive = false;
// CPU simulation job completion for track all feature processors' simulation jobs
AZ::JobCompletion* m_simulationCompletion = nullptr;
@@ -228,6 +246,7 @@ namespace AZ
SceneId m_id;
bool m_activated = false;
bool m_taskGraphActive = false; // update during tick, to ensure it only changes on frame boundaries
RenderPipelinePtr m_defaultPipeline;
+220 -61
View File
@@ -23,6 +23,8 @@
#include <AzCore/Jobs/JobFunction.h>
#include <AzCore/Jobs/JobEmpty.h>
#include <AzCore/Task/TaskGraph.h>
#include <AzFramework/Entity/EntityContext.h>
namespace AZ
@@ -92,7 +94,14 @@ namespace AZ
Scene::~Scene()
{
WaitAndCleanCompletionJob(m_simulationCompletion);
if (m_taskGraphActive)
{
WaitTGEvent(m_simulationFinishedTGEvent, &m_simulationFinishedWorkActive);
}
else
{
WaitAndCleanCompletionJob(m_simulationCompletion);
}
SceneRequestBus::Handler::BusDisconnect();
// Remove all the render pipelines. Need to process queued changes with pass system before and after remove render pipelines
@@ -346,6 +355,47 @@ namespace AZ
return nullptr;
}
void Scene::SimulateTaskGraph()
{
static const AZ::TaskDescriptor simulationTGDesc{"RPI::Scene::Simulate", "Graphics"};
AZ::TaskGraph simulationTG;
for (FeatureProcessorPtr& fp : m_featureProcessors)
{
FeatureProcessor* featureProcessor = fp.get();
simulationTG.AddTask(
simulationTGDesc,
[this, featureProcessor]()
{
featureProcessor->Simulate(m_simulatePacket);
});
}
simulationTG.Detach();
m_simulationFinishedWorkActive = true;
simulationTG.Submit(&m_simulationFinishedTGEvent);
}
void Scene::SimulateJobs()
{
// Create a new job to track completion.
m_simulationCompletion = aznew AZ::JobCompletion();
for (FeatureProcessorPtr& fp : m_featureProcessors)
{
FeatureProcessor* featureProcessor = fp.get();
const auto jobLambda = [this, featureProcessor]()
{
featureProcessor->Simulate(m_simulatePacket);
};
AZ::Job* simulationJob = AZ::CreateJobFunction(AZStd::move(jobLambda), true, nullptr); //auto-deletes
simulationJob->SetDependent(m_simulationCompletion);
simulationJob->Start();
}
//[GFX TODO]: the completion job should start here
}
void Scene::Simulate([[maybe_unused]] const TickTimeInfo& tickInfo, RHI::JobPolicy jobPolicy)
{
AZ_PROFILE_SCOPE(RPI, "Scene: Simulate");
@@ -353,7 +403,17 @@ namespace AZ
m_simulationTime = tickInfo.m_currentGameTime;
// If previous simulation job wasn't done, wait for it to finish.
WaitAndCleanCompletionJob(m_simulationCompletion);
if (m_taskGraphActive)
{
WaitTGEvent(m_simulationFinishedTGEvent, &m_simulationFinishedWorkActive);
}
else
{
WaitAndCleanCompletionJob(m_simulationCompletion);
}
auto taskGraphActiveInterface = AZ::Interface<AZ::TaskGraphActiveInterface>::Get();
m_taskGraphActive = taskGraphActiveInterface && taskGraphActiveInterface->IsTaskGraphActive();
if (jobPolicy == RHI::JobPolicy::Serial)
{
@@ -364,22 +424,27 @@ namespace AZ
}
else
{
// Create a new job to track completion.
m_simulationCompletion = aznew AZ::JobCompletion();
for (FeatureProcessorPtr& fp : m_featureProcessors)
if (m_taskGraphActive)
{
FeatureProcessor* featureProcessor = fp.get();
const auto jobLambda = [this, featureProcessor]()
{
featureProcessor->Simulate(m_simulatePacket);
};
AZ::Job* simulationJob = AZ::CreateJobFunction(AZStd::move(jobLambda), true, nullptr); //auto-deletes
simulationJob->SetDependent(m_simulationCompletion);
simulationJob->Start();
SimulateTaskGraph();
}
//[GFX TODO]: the completion job should start here
else
{
SimulateJobs();
}
}
}
void Scene::WaitTGEvent(AZ::TaskGraphEvent& completionTGEvent, AZStd::atomic_bool* workToWaitOn )
{
AZ_PROFILE_SCOPE(RPI, "Scene: WaitAndCleanCompletionJob");
if (!workToWaitOn || workToWaitOn->load())
{
completionTGEvent.Wait();
}
if (workToWaitOn)
{
workToWaitOn->store(false);
}
}
@@ -394,7 +459,7 @@ namespace AZ
completionJob = nullptr;
}
}
void Scene::ConnectEvent(PrepareSceneSrgEvent::Handler& handler)
{
handler.Connect(m_prepareSrgEvent);
@@ -418,12 +483,139 @@ namespace AZ
}
}
void Scene::CollectDrawPacketsTaskGraph()
{
AZ_PROFILE_SCOPE(RPI, "CollectDrawPackets");
AZ::TaskGraphEvent collectDrawPacketsTGEvent;
static const AZ::TaskDescriptor collectDrawPacketsTGDesc{"RPI_Scene_PrepareRender_CollectDrawPackets", "Graphics"};
AZ::TaskGraph collectDrawPacketsTG;
// Launch FeatureProcessor::Render() jobs
for (auto& fp : m_featureProcessors)
{
collectDrawPacketsTG.AddTask(
collectDrawPacketsTGDesc,
[this, &fp]()
{
fp->Render(m_renderPacket);
});
}
collectDrawPacketsTG.Submit(&collectDrawPacketsTGEvent);
// Launch CullingSystem::ProcessCullables() jobs (will run concurrently with FeatureProcessor::Render() jobs if m_parallelOctreeTraversal)
bool parallelOctreeTraversal = m_cullingScene->GetDebugContext().m_parallelOctreeTraversal;
m_cullingScene->BeginCulling(m_renderPacket.m_views);
AZ::JobCompletion processCullablesCompletion;
for (ViewPtr& viewPtr : m_renderPacket.m_views)
{
AZ::Job* processCullablesJob = AZ::CreateJobFunction([this, &viewPtr](AZ::Job& thisJob)
{
m_cullingScene->ProcessCullables(*this, *viewPtr, thisJob); // can't call directly because ProcessCullables needs a parent job
},
true, nullptr); //auto-deletes
if (parallelOctreeTraversal)
{
processCullablesJob->SetDependent(&processCullablesCompletion);
processCullablesJob->Start();
}
else
{
processCullablesJob->StartAndWaitForCompletion();
}
}
WaitTGEvent(collectDrawPacketsTGEvent);
processCullablesCompletion.StartAndWaitForCompletion();
}
void Scene::CollectDrawPacketsJobs()
{
AZ_PROFILE_SCOPE(RPI, "CollectDrawPackets");
AZ::JobCompletion* collectDrawPacketsCompletion = aznew AZ::JobCompletion();
// Launch FeatureProcessor::Render() jobs
for (auto& fp : m_featureProcessors)
{
const auto renderLambda = [this, &fp]()
{
fp->Render(m_renderPacket);
};
AZ::Job* renderJob = AZ::CreateJobFunction(AZStd::move(renderLambda), true, nullptr); //auto-deletes
renderJob->SetDependent(collectDrawPacketsCompletion);
renderJob->Start();
}
// Launch CullingSystem::ProcessCullables() jobs (will run concurrently with FeatureProcessor::Render() jobs)
m_cullingScene->BeginCulling(m_renderPacket.m_views);
for (ViewPtr& viewPtr : m_renderPacket.m_views)
{
AZ::Job* processCullablesJob = AZ::CreateJobFunction([this, &viewPtr](AZ::Job& thisJob)
{
m_cullingScene->ProcessCullables(*this, *viewPtr, thisJob); // can't call directly because ProcessCullables needs a parent job
},
true, nullptr); //auto-deletes
if (m_cullingScene->GetDebugContext().m_parallelOctreeTraversal)
{
processCullablesJob->SetDependent(collectDrawPacketsCompletion);
processCullablesJob->Start();
}
else
{
processCullablesJob->StartAndWaitForCompletion();
}
}
WaitAndCleanCompletionJob(collectDrawPacketsCompletion);
}
void Scene::FinalizeDrawListsTaskGraph()
{
AZ::TaskGraphEvent finalizeDrawListsTGEvent;
static const AZ::TaskDescriptor finalizeDrawListsTGDesc{"RPI_Scene_PrepareRender_FinalizeDrawLists", "Graphics"};
AZ::TaskGraph finalizeDrawListsTG;
for (auto& view : m_renderPacket.m_views)
{
finalizeDrawListsTG.AddTask(
finalizeDrawListsTGDesc,
[view]()
{
view->FinalizeDrawLists();
});
}
finalizeDrawListsTG.Submit(&finalizeDrawListsTGEvent);
WaitTGEvent(finalizeDrawListsTGEvent);
}
void Scene::FinalizeDrawListsJobs()
{
AZ::JobCompletion* finalizeDrawListsCompletion = aznew AZ::JobCompletion();
for (auto& view : m_renderPacket.m_views)
{
const auto finalizeDrawListsLambda = [view]()
{
view->FinalizeDrawLists();
};
AZ::Job* finalizeDrawListsJob = AZ::CreateJobFunction(AZStd::move(finalizeDrawListsLambda), true, nullptr); //auto-deletes
finalizeDrawListsJob->SetDependent(finalizeDrawListsCompletion);
finalizeDrawListsJob->Start();
}
WaitAndCleanCompletionJob(finalizeDrawListsCompletion);
}
void Scene::PrepareRender(const TickTimeInfo& tickInfo, RHI::JobPolicy jobPolicy)
{
AZ_PROFILE_SCOPE(RPI, "Scene: PrepareRender");
if (m_taskGraphActive)
{
WaitTGEvent(m_simulationFinishedTGEvent, &m_simulationFinishedWorkActive);
}
else
{
AZ_PROFILE_SCOPE(RPI, "WaitForSimulationCompletion");
WaitAndCleanCompletionJob(m_simulationCompletion);
}
@@ -496,44 +688,16 @@ namespace AZ
}
{
AZ_PROFILE_SCOPE(RPI, "CollectDrawPackets");
AZ::JobCompletion* collectDrawPacketsCompletion = aznew AZ::JobCompletion();
// Launch FeatureProcessor::Render() jobs
for (auto& fp : m_featureProcessors)
if (m_taskGraphActive)
{
const auto renderLambda = [this, &fp]()
{
fp->Render(m_renderPacket);
};
AZ::Job* renderJob = AZ::CreateJobFunction(AZStd::move(renderLambda), true, nullptr); //auto-deletes
renderJob->SetDependent(collectDrawPacketsCompletion);
renderJob->Start();
CollectDrawPacketsTaskGraph();
}
// Launch CullingSystem::ProcessCullables() jobs (will run concurrently with FeatureProcessor::Render() jobs)
m_cullingScene->BeginCulling(m_renderPacket.m_views);
for (ViewPtr& viewPtr : m_renderPacket.m_views)
else
{
AZ::Job* processCullablesJob = AZ::CreateJobFunction([this, &viewPtr](AZ::Job& thisJob)
{
m_cullingScene->ProcessCullables(*this, *viewPtr, thisJob);
},
true, nullptr); //auto-deletes
if (m_cullingScene->GetDebugContext().m_parallelOctreeTraversal)
{
processCullablesJob->SetDependent(collectDrawPacketsCompletion);
processCullablesJob->Start();
}
else
{
processCullablesJob->StartAndWaitForCompletion();
}
CollectDrawPacketsJobs();
}
WaitAndCleanCompletionJob(collectDrawPacketsCompletion);
m_cullingScene->EndCulling();
// Add dynamic draw data for all the views
@@ -556,20 +720,15 @@ namespace AZ
}
else
{
AZ::JobCompletion* finalizeDrawListsCompletion = aznew AZ::JobCompletion();
for (auto& view : m_renderPacket.m_views)
if (m_taskGraphActive)
{
const auto finalizeDrawListsLambda = [view]()
{
view->FinalizeDrawLists();
};
AZ::Job* finalizeDrawListsJob = AZ::CreateJobFunction(AZStd::move(finalizeDrawListsLambda), true, nullptr); //auto-deletes
finalizeDrawListsJob->SetDependent(finalizeDrawListsCompletion);
finalizeDrawListsJob->Start();
FinalizeDrawListsTaskGraph();
}
else
{
FinalizeDrawListsJobs();
}
AZ_PROFILE_END(RPI);
WaitAndCleanCompletionJob(finalizeDrawListsCompletion);
}
}
@@ -1,6 +1,6 @@
{
"description": "",
"materialType": "Materials\\Types\\StandardPBR.materialtype",
"materialType": "Materials/Types/StandardPBR.materialtype",
"parentMaterial": "",
"propertyLayoutVersion": 3
}
}
@@ -1,6 +1,6 @@
{
"description": "",
"materialType": "Materials\\Types\\StandardPBR.materialtype",
"materialType": "Materials/Types/StandardPBR.materialtype",
"parentMaterial": "",
"propertyLayoutVersion": 3,
"properties": {
@@ -16,4 +16,4 @@
"textureMap": "TestData/Textures/TextureHaven/4k_castle_brick_02_red/4k_castle_brick_02_red_hp_bc.png"
}
}
}
}