diff --git a/Code/Framework/AzCore/AzCore/Task/TaskExecutor.cpp b/Code/Framework/AzCore/AzCore/Task/TaskExecutor.cpp index 4097348798..08cfb11d34 100644 --- a/Code/Framework/AzCore/AzCore/Task/TaskExecutor.cpp +++ b/Code/Framework/AzCore/AzCore/Task/TaskExecutor.cpp @@ -363,7 +363,11 @@ namespace AZ { ++m_graphsRemaining; - event->m_executor = this; // Used to validate event is not waited for inside a job + if (event) + { + event->IncWaitCount(); + 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()) diff --git a/Code/Framework/AzCore/AzCore/Task/TaskGraph.cpp b/Code/Framework/AzCore/AzCore/Task/TaskGraph.cpp index f57b06890a..eeb46d6887 100644 --- a/Code/Framework/AzCore/AzCore/Task/TaskGraph.cpp +++ b/Code/Framework/AzCore/AzCore/Task/TaskGraph.cpp @@ -20,6 +20,46 @@ namespace AZ m_semaphore.acquire(); } + void TaskGraphEvent::IncWaitCount() + { + // guess zero to optimize for single task graph using an event, if multiple are using it then this will take 2+ comp_exch calls + int expectedValue = 0; + while(!m_waitCount.compare_exchange_weak(expectedValue, expectedValue + 1)) + { + // value will be negative once event is ready to signal or has been signaled. Shouldn't happen. + AZ_Assert(expectedValue >= 0, "Called TaskGraphEvent::IncWaitCount on a signalled event"); + if (expectedValue < 0) // event already signaled, skip + { + return; + } + }; + } + + void TaskGraphEvent::Signal() + { + // guess one to optimize for single task graph using an event, if multiple are using it then this will take 2+ comp_exch calls + int expectedValue = 1; + while(!m_waitCount.compare_exchange_weak(expectedValue, expectedValue - 1)) + { + // It's an error for Signal to be called if no one is waiting, or the event has already been signaled + AZ_Assert(expectedValue > 0, "Called TaskGraphEvent::Signal when event is either signaled or unused"); + if (expectedValue < 0) // return if already signaled + { + return; + } + }; + + if (expectedValue == 1) // This call to Signal decremented the value to 0. + { + expectedValue = 0; + // validate no one incremented the wait count and mark signalling state + if (m_waitCount.compare_exchange_strong(expectedValue, -1)) + { + m_semaphore.release(); + } + } + } + void TaskToken::PrecedesInternal(TaskToken& comesAfter) { AZ_Assert(!m_parent.m_submitted, "Cannot mutate a TaskGraph that was previously submitted."); diff --git a/Code/Framework/AzCore/AzCore/Task/TaskGraph.h b/Code/Framework/AzCore/AzCore/Task/TaskGraph.h index 9553013a4b..fa6f5dbe94 100644 --- a/Code/Framework/AzCore/AzCore/Task/TaskGraph.h +++ b/Code/Framework/AzCore/AzCore/Task/TaskGraph.h @@ -61,14 +61,14 @@ namespace AZ uint32_t m_index; }; - // A TaskGraphEvent may be used to block until a task graph has finished executing. Usage + // A TaskGraphEvent may be used to block until one or more task graphs has finished executing. Usage // is NOT recommended for the majority of tasks (prefer to simply containing expanding/contracting // the graph without synchronization over the course of the frame). However, the event // is useful for the edges of the computation graph. // // You are responsible for ensuring the event object lifetime exceeds the task graph lifetime. // - // After the TaskGraphEvent is signaled, you are allowed to reuse the same TaskGraphEvent + // After the TaskGraphEvent is signaled, you are NOT allowed to reuse the same TaskGraphEvent // for a future submission. class TaskGraphEvent { @@ -81,10 +81,12 @@ namespace AZ friend class TaskGraph; friend class TaskExecutor; + void IncWaitCount(); void Signal(); AZStd::binary_semaphore m_semaphore; - TaskExecutor* m_executor = nullptr; + AZStd::atomic_int m_waitCount = 0; + TaskExecutor* m_executor = nullptr; }; // The TaskGraph encapsulates a set of tasks and their interdependencies. After adding diff --git a/Code/Framework/AzCore/AzCore/Task/TaskGraph.inl b/Code/Framework/AzCore/AzCore/Task/TaskGraph.inl index 7b2f0cefdc..9a5289eb82 100644 --- a/Code/Framework/AzCore/AzCore/Task/TaskGraph.inl +++ b/Code/Framework/AzCore/AzCore/Task/TaskGraph.inl @@ -33,11 +33,6 @@ namespace AZ return m_semaphore.try_acquire_for(AZStd::chrono::milliseconds{ 0 }); } - inline void TaskGraphEvent::Signal() - { - m_semaphore.release(); - } - template TaskToken TaskGraph::AddTask(TaskDescriptor const& desc, Lambda&& lambda) { diff --git a/Code/Framework/AzCore/Tests/TaskTests.cpp b/Code/Framework/AzCore/Tests/TaskTests.cpp index 9e839f60ee..4f53772d51 100644 --- a/Code/Framework/AzCore/Tests/TaskTests.cpp +++ b/Code/Framework/AzCore/Tests/TaskTests.cpp @@ -610,15 +610,16 @@ namespace UnitTest g.Follows(e, f); g.Precedes(d); - TaskGraphEvent ev; - graph.SubmitOnExecutor(*m_executor, &ev); - ev.Wait(); + TaskGraphEvent ev1; + graph.SubmitOnExecutor(*m_executor, &ev1); + ev1.Wait(); EXPECT_EQ(3 | 0b100000, x); x = 0; - graph.SubmitOnExecutor(*m_executor, &ev); - ev.Wait(); + TaskGraphEvent ev2; + graph.SubmitOnExecutor(*m_executor, &ev2); + ev2.Wait(); EXPECT_EQ(3 | 0b100000, x); } diff --git a/Gems/Atom/Feature/Common/Code/Source/FrameCaptureSystemComponent.cpp b/Gems/Atom/Feature/Common/Code/Source/FrameCaptureSystemComponent.cpp index a9bb7271ab..8b016410aa 100644 --- a/Gems/Atom/Feature/Common/Code/Source/FrameCaptureSystemComponent.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/FrameCaptureSystemComponent.cpp @@ -29,6 +29,8 @@ #include #include +#include + #include #include @@ -50,6 +52,15 @@ namespace AZ "Sets the compression level for saving png screenshots. Valid values are from 0 to 8" ); + AZ_CVAR(int, + r_pngCompressionNumThreads, + 8, // Number of threads to use for the png r<->b channel data swap + nullptr, + ConsoleFunctorFlags::Null, + "Sets the number of threads for saving png screenshots. Valid values are from 1 to 128, although less than or equal the number of hw threads is recommended" + ); + + FrameCaptureOutputResult PngFrameCaptureOutput( const AZStd::string& outputFilePath, const AZ::RPI::AttachmentReadback::ReadbackResult& readbackResult) { @@ -65,33 +76,67 @@ namespace AZ buffer = AZStd::make_shared>(readbackResult.m_dataBuffer->size()); AZStd::copy(readbackResult.m_dataBuffer->begin(), readbackResult.m_dataBuffer->end(), buffer->begin()); - - AZ::JobCompletion jobCompletion; - const int numThreads = 8; + const int numThreads = r_pngCompressionNumThreads; const int numPixelsPerThread = static_cast(buffer->size() / numChannels / numThreads); - for (int i = 0; i < numThreads; ++i) + + AZ::TaskGraphActiveInterface* taskGraphActiveInterface = AZ::Interface::Get(); + bool taskGraphActive = taskGraphActiveInterface && taskGraphActiveInterface->IsTaskGraphActive(); + + if (taskGraphActive) { - int startPixel = i * numPixelsPerThread; + static const AZ::TaskDescriptor pngTaskDescriptor{"PngWriteOutChannelSwap", "Graphics"}; + AZ::TaskGraph taskGraph; + for (int i = 0; i < numThreads; ++i) + { + int startPixel = i * numPixelsPerThread; - AZ::Job* job = AZ::CreateJobFunction( - [&, startPixel, numPixelsPerThread]() - { - for (int pixelOffset = 0; pixelOffset < numPixelsPerThread; ++pixelOffset) + taskGraph.AddTask( + pngTaskDescriptor, + [&, startPixel]() { - if (startPixel * numChannels + numChannels < buffer->size()) + for (int pixelOffset = 0; pixelOffset < numPixelsPerThread; ++pixelOffset) { - AZStd::swap( - buffer->data()[(startPixel + pixelOffset) * numChannels], - buffer->data()[(startPixel + pixelOffset) * numChannels + 2] - ); + if (startPixel * numChannels + numChannels < buffer->size()) + { + AZStd::swap( + buffer->data()[(startPixel + pixelOffset) * numChannels], + buffer->data()[(startPixel + pixelOffset) * numChannels + 2] + ); + } } - } - }, true, nullptr); - - job->SetDependent(&jobCompletion); - job->Start(); + }); + } + AZ::TaskGraphEvent taskGraphFinishedEvent; + taskGraph.Submit(&taskGraphFinishedEvent); + taskGraphFinishedEvent.Wait(); + } + else + { + AZ::JobCompletion jobCompletion; + for (int i = 0; i < numThreads; ++i) + { + int startPixel = i * numPixelsPerThread; + + AZ::Job* job = AZ::CreateJobFunction( + [&, startPixel]() + { + for (int pixelOffset = 0; pixelOffset < numPixelsPerThread; ++pixelOffset) + { + if (startPixel * numChannels + numChannels < buffer->size()) + { + AZStd::swap( + buffer->data()[(startPixel + pixelOffset) * numChannels], + buffer->data()[(startPixel + pixelOffset) * numChannels + 2] + ); + } + } + }, true, nullptr); + + job->SetDependent(&jobCompletion); + job->Start(); + } + jobCompletion.StartAndWaitForCompletion(); } - jobCompletion.StartAndWaitForCompletion(); } Utils::PngFile image = Utils::PngFile::Create(readbackResult.m_imageDescriptor.m_size, format, *buffer); diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Culling.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Culling.h index 82e9c733c8..44b1425c7a 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Culling.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Culling.h @@ -38,6 +38,8 @@ namespace AZ { class Job; + class TaskGraphActiveInterface; + class TaskGraph; namespace RHI { @@ -256,7 +258,13 @@ namespace AZ //! Must be called between BeginCulling() and EndCulling(), once for each active scene/view pair. //! Will create child jobs under the parentJob to do the processing in parallel. //! Can be called in parallel (i.e. to perform culling on multiple views at the same time). - void ProcessCullables(const Scene& scene, View& view, AZ::Job& parentJob); + void ProcessCullablesJobs(const Scene& scene, View& view, AZ::Job& parentJob); + + //! Performs render culling and lod selection for a View, then adds the visible renderpackets to that View. + //! Must be called between BeginCulling() and EndCulling(), once for each active scene/view pair. + //! Will create child task graphs that signal the TaskGraphEvent to do the processing in parallel. + //! Can be called in parallel (i.e. to perform culling on multiple views at the same time). + void ProcessCullablesTG(const Scene& scene, View& view, AZ::TaskGraph& taskGraph); //! Adds a Cullable to the underlying visibility system(s). //! Must be called at least once on initialization and whenever a Cullable's position or bounds is changed. @@ -276,17 +284,20 @@ namespace AZ return m_debugCtx; } - static const size_t WorkListCapacity = 5; - using WorkListType = AZStd::fixed_vector; - protected: size_t CountObjectsInScene(); + private: + void BeginCullingTaskGraph(const AZStd::vector& views); + void BeginCullingJobs(const AZStd::vector& views); + void ProcessCullablesCommon(const Scene& scene, View& view, AZ::Frustum& frustum, void*& maskedOcclusionCulling); + const Scene* m_parentScene = nullptr; AzFramework::IVisibilityScene* m_visScene = nullptr; CullingDebugContext m_debugCtx; AZStd::concurrency_checker m_cullDataConcurrencyCheck; OcclusionPlaneVector m_occlusionPlanes; + AZ::TaskGraphActiveInterface* m_taskGraphActive = nullptr; }; diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Scene.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Scene.h index f86383101a..21e446feda 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Scene.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Scene.h @@ -200,8 +200,8 @@ 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 to wait for end of TaskGraph and then delete the TaskGraphEvent + void WaitAndCleanTGEvent(AZStd::unique_ptr&& completionTGEvent); // Helper function for wait and clean up a completion job void WaitAndCleanCompletionJob(AZ::JobCompletion*& completionJob); @@ -230,8 +230,7 @@ namespace AZ AZStd::vector m_pipelines; // CPU simulation TaskGraphEvent to wait for completion of all the simulation tasks - AZ::TaskGraphEvent m_simulationFinishedTGEvent; - AZStd::atomic_bool m_simulationFinishedWorkActive = false; + AZStd::unique_ptr m_simulationFinishedTGEvent; // CPU simulation job completion for track all feature processors' simulation jobs AZ::JobCompletion* m_simulationCompletion = nullptr; diff --git a/Gems/Atom/RPI/Code/Source/RPI.Public/Culling.cpp b/Gems/Atom/RPI/Code/Source/RPI.Public/Culling.cpp index ac6b10694e..65508b6371 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Public/Culling.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Public/Culling.cpp @@ -23,6 +23,8 @@ #include #include #include +#include +#include #include #if AZ_TRAIT_MASKED_OCCLUSION_CULLING_SUPPORTED @@ -265,89 +267,71 @@ namespace AZ return m_visScene->GetEntryCount(); } - class AddObjectsToViewJob final - : public Job + + struct WorklistData { - public: - AZ_CLASS_ALLOCATOR(AddObjectsToViewJob, ThreadPoolAllocator, 0); - - struct JobData - { - CullingDebugContext* m_debugCtx = nullptr; - const Scene* m_scene = nullptr; - View* m_view = nullptr; - Frustum m_frustum; + CullingDebugContext* m_debugCtx = nullptr; + const Scene* m_scene = nullptr; + View* m_view = nullptr; + Frustum m_frustum; #if AZ_TRAIT_MASKED_OCCLUSION_CULLING_SUPPORTED - MaskedOcclusionCulling* m_maskedOcclusionCulling = nullptr; + MaskedOcclusionCulling* m_maskedOcclusionCulling = nullptr; #endif - }; + }; - private: - const AZStd::shared_ptr m_jobData; - CullingScene::WorkListType m_worklist; + static AZStd::shared_ptr MakeWorklistData( + CullingDebugContext& debugCtx, + const Scene& scene, + View& view, + Frustum& frustum, + void* maskedOcclusionCulling) + { + AZStd::shared_ptr worklistData = AZStd::make_shared(); + worklistData->m_debugCtx = &debugCtx; + worklistData->m_scene = &scene; + worklistData->m_view = &view; + worklistData->m_frustum = frustum; +#if AZ_TRAIT_MASKED_OCCLUSION_CULLING_SUPPORTED + worklistData->m_maskedOcclusionCulling = static_cast(maskedOcclusionCulling); +#endif + return worklistData; + } + + constexpr size_t WorkListCapacity = 5; + using WorkListType = AZStd::fixed_vector; - public: - AddObjectsToViewJob(const AZStd::shared_ptr& jobData, CullingScene::WorkListType& worklist) - : Job(true, nullptr) //auto-deletes, no JobContext - , m_jobData(jobData) - , m_worklist(worklist) +#if AZ_TRAIT_MASKED_OCCLUSION_CULLING_SUPPORTED + static MaskedOcclusionCulling::CullingResult TestOcclusionCulling( + const AZStd::shared_ptr& worklistData, + AzFramework::VisibilityEntry* visibleEntry); +#endif + + static void ProcessWorklist(const AZStd::shared_ptr& worklistData, const WorkListType& worklist) + { + AZ_PROFILE_SCOPE(RPI, "AddObjectsToViewJob: Process"); + + const View::UsageFlags viewFlags = worklistData->m_view->GetUsageFlags(); + const RHI::DrawListMask drawListMask = worklistData->m_view->GetDrawListMask(); + uint32_t numDrawPackets = 0; + uint32_t numVisibleCullables = 0; + + AZ_Assert(worklist.size() > 0, "Received empty worklist in ProcessWorklist"); + + for (const AzFramework::IVisibilityScene::NodeData& nodeData : worklist) { - } - - //work function - void Process() override - { - AZ_PROFILE_SCOPE(RPI, "AddObjectsToViewJob: Process"); - - const View::UsageFlags viewFlags = m_jobData->m_view->GetUsageFlags(); - const RHI::DrawListMask drawListMask = m_jobData->m_view->GetDrawListMask(); - uint32_t numDrawPackets = 0; - uint32_t numVisibleCullables = 0; - - for (const AzFramework::IVisibilityScene::NodeData& nodeData : m_worklist) - { - //If a node is entirely contained within the frustum, then we can skip the fine grained culling. - bool nodeIsContainedInFrustum = ShapeIntersection::Contains(m_jobData->m_frustum, nodeData.m_bounds); + //If a node is entirely contained within the frustum, then we can skip the fine grained culling. + bool nodeIsContainedInFrustum = ShapeIntersection::Contains(worklistData->m_frustum, nodeData.m_bounds); #ifdef AZ_CULL_PROFILE_VERBOSE - AZ_PROFILE_SCOPE(RPI, "process node (view: %s, skip fine cull: %d", - m_view->GetName().GetCStr(), nodeIsContainedInFrustum ? 1 : 0); + AZ_PROFILE_SCOPE(RPI, "process node (view: %s, skip fine cull: %d", + m_view->GetName().GetCStr(), nodeIsContainedInFrustum ? 1 : 0); #endif - if (nodeIsContainedInFrustum || !m_jobData->m_debugCtx->m_enableFrustumCulling) + if (nodeIsContainedInFrustum || !worklistData->m_debugCtx->m_enableFrustumCulling) + { + //Add all objects within this node to the view, without any extra culling + for (AzFramework::VisibilityEntry* visibleEntry : nodeData.m_entries) { - //Add all objects within this node to the view, without any extra culling - for (AzFramework::VisibilityEntry* visibleEntry : nodeData.m_entries) - { - { - if (visibleEntry->m_typeFlags & AzFramework::VisibilityEntry::TYPE_RPI_Cullable) - { - Cullable* c = static_cast(visibleEntry->m_userData); - - if ((c->m_cullData.m_drawListMask & drawListMask).none() || - c->m_cullData.m_hideFlags & viewFlags || - c->m_cullData.m_scene != m_jobData->m_scene || //[GFX_TODO][ATOM-13796] once the IVisibilitySystem supports multiple octree scenes, remove this - c->m_isHidden) - { - continue; - } - -#if AZ_TRAIT_MASKED_OCCLUSION_CULLING_SUPPORTED - if (TestOcclusionCulling(visibleEntry) == MaskedOcclusionCulling::CullingResult::VISIBLE) -#endif - { - numDrawPackets += AddLodDataToView(c->m_cullData.m_boundingSphere.GetCenter(), c->m_lodData, *m_jobData->m_view); - ++numVisibleCullables; - c->m_isVisible = true; - } - } - } - } - } - else - { - //Do fine-grained culling before adding objects to the view - for (AzFramework::VisibilityEntry* visibleEntry : nodeData.m_entries) { if (visibleEntry->m_typeFlags & AzFramework::VisibilityEntry::TYPE_RPI_Cullable) { @@ -355,160 +339,188 @@ namespace AZ if ((c->m_cullData.m_drawListMask & drawListMask).none() || c->m_cullData.m_hideFlags & viewFlags || - c->m_cullData.m_scene != m_jobData->m_scene || //[GFX_TODO][ATOM-13796] once the IVisibilitySystem supports multiple octree scenes, remove this + c->m_cullData.m_scene != worklistData->m_scene || //[GFX_TODO][ATOM-13796] once the IVisibilitySystem supports multiple octree scenes, remove this c->m_isHidden) { continue; } - IntersectResult res = ShapeIntersection::Classify(m_jobData->m_frustum, c->m_cullData.m_boundingSphere); - if (res == IntersectResult::Exterior) - { - continue; - } - else if (res == IntersectResult::Interior || ShapeIntersection::Overlaps(m_jobData->m_frustum, c->m_cullData.m_boundingObb)) - { #if AZ_TRAIT_MASKED_OCCLUSION_CULLING_SUPPORTED - if (TestOcclusionCulling(visibleEntry) == MaskedOcclusionCulling::CullingResult::VISIBLE) + if (TestOcclusionCulling(worklistData, visibleEntry) == MaskedOcclusionCulling::CullingResult::VISIBLE) #endif - { - numDrawPackets += AddLodDataToView(c->m_cullData.m_boundingSphere.GetCenter(), c->m_lodData, *m_jobData->m_view); - ++numVisibleCullables; - c->m_isVisible = true; - } + { + numDrawPackets += AddLodDataToView(c->m_cullData.m_boundingSphere.GetCenter(), c->m_lodData, *worklistData->m_view); + ++numVisibleCullables; + c->m_isVisible = true; } } } } - - if (m_jobData->m_debugCtx->m_debugDraw && (m_jobData->m_view->GetName() == m_jobData->m_debugCtx->m_currentViewSelectionName)) + } + else + { + //Do fine-grained culling before adding objects to the view + for (AzFramework::VisibilityEntry* visibleEntry : nodeData.m_entries) { - AZ_PROFILE_SCOPE(RPI, "debug draw culling"); - - AuxGeomDrawPtr auxGeomPtr = AuxGeomFeatureProcessorInterface::GetDrawQueueForScene(m_jobData->m_scene); - if (auxGeomPtr) + if (visibleEntry->m_typeFlags & AzFramework::VisibilityEntry::TYPE_RPI_Cullable) { - //Draw the node bounds - // "Fully visible" nodes are nodes that are fully inside the frustum. "Partially visible" nodes intersect the edges of the frustum. - // Since the nodes of an octree have lots of overlapping boxes with coplanar edges, it's easier to view these separately, so - // we have a few debug booleans to toggle which ones to draw. - if (nodeIsContainedInFrustum && m_jobData->m_debugCtx->m_drawFullyVisibleNodes) + Cullable* c = static_cast(visibleEntry->m_userData); + + if ((c->m_cullData.m_drawListMask & drawListMask).none() || + c->m_cullData.m_hideFlags & viewFlags || + c->m_cullData.m_scene != worklistData->m_scene || //[GFX_TODO][ATOM-13796] once the IVisibilitySystem supports multiple octree scenes, remove this + c->m_isHidden) { - auxGeomPtr->DrawAabb(nodeData.m_bounds, Colors::Lime, RPI::AuxGeomDraw::DrawStyle::Line, RPI::AuxGeomDraw::DepthTest::Off); - } - else if (!nodeIsContainedInFrustum && m_jobData->m_debugCtx->m_drawPartiallyVisibleNodes) - { - auxGeomPtr->DrawAabb(nodeData.m_bounds, Colors::Yellow, RPI::AuxGeomDraw::DrawStyle::Line, RPI::AuxGeomDraw::DepthTest::Off); + continue; } - //Draw bounds on individual objects - if (m_jobData->m_debugCtx->m_drawBoundingBoxes || m_jobData->m_debugCtx->m_drawBoundingSpheres || m_jobData->m_debugCtx->m_drawLodRadii) + IntersectResult res = ShapeIntersection::Classify(worklistData->m_frustum, c->m_cullData.m_boundingSphere); + if (res == IntersectResult::Exterior) { - for (AzFramework::VisibilityEntry* visibleEntry : nodeData.m_entries) + continue; + } + else if (res == IntersectResult::Interior || ShapeIntersection::Overlaps(worklistData->m_frustum, c->m_cullData.m_boundingObb)) + { +#if AZ_TRAIT_MASKED_OCCLUSION_CULLING_SUPPORTED + if (TestOcclusionCulling(worklistData, visibleEntry) == MaskedOcclusionCulling::CullingResult::VISIBLE) +#endif { - if (visibleEntry->m_typeFlags & AzFramework::VisibilityEntry::TYPE_RPI_Cullable) - { - Cullable* c = static_cast(visibleEntry->m_userData); - if (m_jobData->m_debugCtx->m_drawBoundingBoxes) - { - auxGeomPtr->DrawObb(c->m_cullData.m_boundingObb, Matrix3x4::Identity(), - nodeIsContainedInFrustum ? Colors::Lime : Colors::Yellow, AuxGeomDraw::DrawStyle::Line); - } - - if (m_jobData->m_debugCtx->m_drawBoundingSpheres) - { - auxGeomPtr->DrawSphere(c->m_cullData.m_boundingSphere.GetCenter(), c->m_cullData.m_boundingSphere.GetRadius(), - Color(0.5f, 0.5f, 0.5f, 0.3f), AuxGeomDraw::DrawStyle::Shaded); - } - - if (m_jobData->m_debugCtx->m_drawLodRadii) - { - auxGeomPtr->DrawSphere(c->m_cullData.m_boundingSphere.GetCenter(), - c->m_lodData.m_lodSelectionRadius, - Color(1.0f, 0.5f, 0.0f, 0.3f), RPI::AuxGeomDraw::DrawStyle::Shaded); - } - } + numDrawPackets += AddLodDataToView(c->m_cullData.m_boundingSphere.GetCenter(), c->m_lodData, *worklistData->m_view); + ++numVisibleCullables; + c->m_isVisible = true; } } } } } - if (m_jobData->m_debugCtx->m_enableStats) + if (worklistData->m_debugCtx->m_debugDraw && (worklistData->m_view->GetName() == worklistData->m_debugCtx->m_currentViewSelectionName)) { - CullingDebugContext::CullStats& cullStats = m_jobData->m_debugCtx->GetCullStatsForView(m_jobData->m_view); + AZ_PROFILE_SCOPE(RPI, "debug draw culling"); - //no need for mutex here since these are all atomics - cullStats.m_numVisibleDrawPackets += numDrawPackets; - cullStats.m_numVisibleCullables += numVisibleCullables; - ++cullStats.m_numJobs; + AuxGeomDrawPtr auxGeomPtr = AuxGeomFeatureProcessorInterface::GetDrawQueueForScene(worklistData->m_scene); + if (auxGeomPtr) + { + //Draw the node bounds + // "Fully visible" nodes are nodes that are fully inside the frustum. "Partially visible" nodes intersect the edges of the frustum. + // Since the nodes of an octree have lots of overlapping boxes with coplanar edges, it's easier to view these separately, so + // we have a few debug booleans to toggle which ones to draw. + if (nodeIsContainedInFrustum && worklistData->m_debugCtx->m_drawFullyVisibleNodes) + { + auxGeomPtr->DrawAabb(nodeData.m_bounds, Colors::Lime, RPI::AuxGeomDraw::DrawStyle::Line, RPI::AuxGeomDraw::DepthTest::Off); + } + else if (!nodeIsContainedInFrustum && worklistData->m_debugCtx->m_drawPartiallyVisibleNodes) + { + auxGeomPtr->DrawAabb(nodeData.m_bounds, Colors::Yellow, RPI::AuxGeomDraw::DrawStyle::Line, RPI::AuxGeomDraw::DepthTest::Off); + } + + //Draw bounds on individual objects + if (worklistData->m_debugCtx->m_drawBoundingBoxes || worklistData->m_debugCtx->m_drawBoundingSpheres || worklistData->m_debugCtx->m_drawLodRadii) + { + for (AzFramework::VisibilityEntry* visibleEntry : nodeData.m_entries) + { + if (visibleEntry->m_typeFlags & AzFramework::VisibilityEntry::TYPE_RPI_Cullable) + { + Cullable* c = static_cast(visibleEntry->m_userData); + if (worklistData->m_debugCtx->m_drawBoundingBoxes) + { + auxGeomPtr->DrawObb(c->m_cullData.m_boundingObb, Matrix3x4::Identity(), + nodeIsContainedInFrustum ? Colors::Lime : Colors::Yellow, AuxGeomDraw::DrawStyle::Line); + } + + if (worklistData->m_debugCtx->m_drawBoundingSpheres) + { + auxGeomPtr->DrawSphere(c->m_cullData.m_boundingSphere.GetCenter(), c->m_cullData.m_boundingSphere.GetRadius(), + Color(0.5f, 0.5f, 0.5f, 0.3f), AuxGeomDraw::DrawStyle::Shaded); + } + + if (worklistData->m_debugCtx->m_drawLodRadii) + { + auxGeomPtr->DrawSphere(c->m_cullData.m_boundingSphere.GetCenter(), + c->m_lodData.m_lodSelectionRadius, + Color(1.0f, 0.5f, 0.0f, 0.3f), RPI::AuxGeomDraw::DrawStyle::Shaded); + } + } + } + } + } } } -#if AZ_TRAIT_MASKED_OCCLUSION_CULLING_SUPPORTED - MaskedOcclusionCulling::CullingResult TestOcclusionCulling(AzFramework::VisibilityEntry* visibleEntry) + if (worklistData->m_debugCtx->m_enableStats) { - if (!m_jobData->m_maskedOcclusionCulling) - { - return MaskedOcclusionCulling::CullingResult::VISIBLE; - } + CullingDebugContext::CullStats& cullStats = worklistData->m_debugCtx->GetCullStatsForView(worklistData->m_view); - if (visibleEntry->m_boundingVolume.Contains(m_jobData->m_view->GetCameraTransform().GetTranslation())) - { - // camera is inside bounding volume - return MaskedOcclusionCulling::CullingResult::VISIBLE; - } + //no need for mutex here since these are all atomics + cullStats.m_numVisibleDrawPackets += numDrawPackets; + cullStats.m_numVisibleCullables += numVisibleCullables; + ++cullStats.m_numJobs; + } + } - const Vector3& minBound = visibleEntry->m_boundingVolume.GetMin(); - const Vector3& maxBound = visibleEntry->m_boundingVolume.GetMax(); +#if AZ_TRAIT_MASKED_OCCLUSION_CULLING_SUPPORTED + static MaskedOcclusionCulling::CullingResult TestOcclusionCulling( + const AZStd::shared_ptr& worklistData, + AzFramework::VisibilityEntry* visibleEntry) + { + if (!worklistData->m_maskedOcclusionCulling) + { + return MaskedOcclusionCulling::CullingResult::VISIBLE; + } - // compute bounding volume corners - Vector4 corners[8]; - corners[0] = m_jobData->m_view->GetWorldToClipMatrix() * Vector4(minBound.GetX(), minBound.GetY(), minBound.GetZ(), 1.0f); - corners[1] = m_jobData->m_view->GetWorldToClipMatrix() * Vector4(minBound.GetX(), minBound.GetY(), maxBound.GetZ(), 1.0f); - corners[2] = m_jobData->m_view->GetWorldToClipMatrix() * Vector4(maxBound.GetX(), minBound.GetY(), maxBound.GetZ(), 1.0f); - corners[3] = m_jobData->m_view->GetWorldToClipMatrix() * Vector4(maxBound.GetX(), minBound.GetY(), minBound.GetZ(), 1.0f); - corners[4] = m_jobData->m_view->GetWorldToClipMatrix() * Vector4(minBound.GetX(), maxBound.GetY(), minBound.GetZ(), 1.0f); - corners[5] = m_jobData->m_view->GetWorldToClipMatrix() * Vector4(minBound.GetX(), maxBound.GetY(), maxBound.GetZ(), 1.0f); - corners[6] = m_jobData->m_view->GetWorldToClipMatrix() * Vector4(maxBound.GetX(), maxBound.GetY(), maxBound.GetZ(), 1.0f); - corners[7] = m_jobData->m_view->GetWorldToClipMatrix() * Vector4(maxBound.GetX(), maxBound.GetY(), minBound.GetZ(), 1.0f); + if (visibleEntry->m_boundingVolume.Contains(worklistData->m_view->GetCameraTransform().GetTranslation())) + { + // camera is inside bounding volume + return MaskedOcclusionCulling::CullingResult::VISIBLE; + } - // find min clip-space depth and NDC min/max - float minDepth = FLT_MAX; - float ndcMinX = FLT_MAX; - float ndcMinY = FLT_MAX; - float ndcMaxX = -FLT_MAX; - float ndcMaxY = -FLT_MAX; - for (uint32_t index = 0; index < 8; ++index) - { - minDepth = AZStd::min(minDepth, corners[index].GetW()); + const Vector3& minBound = visibleEntry->m_boundingVolume.GetMin(); + const Vector3& maxBound = visibleEntry->m_boundingVolume.GetMax(); - // convert to NDC - corners[index] /= corners[index].GetW(); - - ndcMinX = AZStd::min(ndcMinX, corners[index].GetX()); - ndcMinY = AZStd::min(ndcMinY, corners[index].GetY()); - ndcMaxX = AZStd::max(ndcMaxX, corners[index].GetX()); - ndcMaxY = AZStd::max(ndcMaxY, corners[index].GetY()); - } + // compute bounding volume corners + Vector4 corners[8]; + corners[0] = worklistData->m_view->GetWorldToClipMatrix() * Vector4(minBound.GetX(), minBound.GetY(), minBound.GetZ(), 1.0f); + corners[1] = worklistData->m_view->GetWorldToClipMatrix() * Vector4(minBound.GetX(), minBound.GetY(), maxBound.GetZ(), 1.0f); + corners[2] = worklistData->m_view->GetWorldToClipMatrix() * Vector4(maxBound.GetX(), minBound.GetY(), maxBound.GetZ(), 1.0f); + corners[3] = worklistData->m_view->GetWorldToClipMatrix() * Vector4(maxBound.GetX(), minBound.GetY(), minBound.GetZ(), 1.0f); + corners[4] = worklistData->m_view->GetWorldToClipMatrix() * Vector4(minBound.GetX(), maxBound.GetY(), minBound.GetZ(), 1.0f); + corners[5] = worklistData->m_view->GetWorldToClipMatrix() * Vector4(minBound.GetX(), maxBound.GetY(), maxBound.GetZ(), 1.0f); + corners[6] = worklistData->m_view->GetWorldToClipMatrix() * Vector4(maxBound.GetX(), maxBound.GetY(), maxBound.GetZ(), 1.0f); + corners[7] = worklistData->m_view->GetWorldToClipMatrix() * Vector4(maxBound.GetX(), maxBound.GetY(), minBound.GetZ(), 1.0f); + // find min clip-space depth and NDC min/max + float minDepth = FLT_MAX; + float ndcMinX = FLT_MAX; + float ndcMinY = FLT_MAX; + float ndcMaxX = -FLT_MAX; + float ndcMaxY = -FLT_MAX; + for (uint32_t index = 0; index < 8; ++index) + { + minDepth = AZStd::min(minDepth, corners[index].GetW()); if (minDepth < 0.00000001f) { - return MaskedOcclusionCulling::VISIBLE; + return MaskedOcclusionCulling::CullingResult::VISIBLE; } - // test against the occlusion buffer, which contains only the manually placed occlusion planes - return m_jobData->m_maskedOcclusionCulling->TestRect(ndcMinX, ndcMinY, ndcMaxX, ndcMaxY, minDepth); + + // convert to NDC + corners[index] /= corners[index].GetW(); + + ndcMinX = AZStd::min(ndcMinX, corners[index].GetX()); + ndcMinY = AZStd::min(ndcMinY, corners[index].GetY()); + ndcMaxX = AZStd::max(ndcMaxX, corners[index].GetX()); + ndcMaxY = AZStd::max(ndcMaxY, corners[index].GetY()); } + + // test against the occlusion buffer, which contains only the manually placed occlusion planes + return worklistData->m_maskedOcclusionCulling->TestRect(ndcMinX, ndcMinY, ndcMaxX, ndcMaxY, minDepth); + } #endif - }; - void CullingScene::ProcessCullables(const Scene& scene, View& view, AZ::Job& parentJob) + void CullingScene::ProcessCullablesCommon(const Scene& scene, View& view, AZ::Frustum& frustum, void*& maskedOcclusionCulling) { - AZ_PROFILE_SCOPE(RPI, "CullingScene::ProcessCullables() - %s", view.GetName().GetCStr()); + AZ_PROFILE_SCOPE(RPI, "CullingScene::ProcessCullablesCommon() - %s", view.GetName().GetCStr()); - const Matrix4x4& worldToClip = view.GetWorldToClipMatrix(); - Frustum frustum = Frustum::CreateFromMatrixColumnMajor(worldToClip); if (m_debugCtx.m_freezeFrustums) { AZStd::lock_guard lock(m_debugCtx.m_frozenFrustumsMutex); @@ -536,7 +548,7 @@ namespace AZ #if AZ_TRAIT_MASKED_OCCLUSION_CULLING_SUPPORTED // setup occlusion culling, if necessary - MaskedOcclusionCulling* maskedOcclusionCulling = m_occlusionPlanes.empty() ? nullptr : view.GetMaskedOcclusionCulling(); + maskedOcclusionCulling = m_occlusionPlanes.empty() ? nullptr : view.GetMaskedOcclusionCulling(); if (maskedOcclusionCulling) { // frustum cull occlusion planes @@ -578,23 +590,27 @@ namespace AZ static uint32_t indices[6] = { 0, 1, 2, 2, 3, 0 }; // render into the occlusion buffer, specifying BACKFACE_NONE so it functions as a double-sided occluder - maskedOcclusionCulling->RenderTriangles((float*)verts, indices, 2, nullptr, MaskedOcclusionCulling::BACKFACE_NONE); + static_cast(maskedOcclusionCulling)->RenderTriangles(verts, indices, 2, nullptr, MaskedOcclusionCulling::BACKFACE_NONE); } } #endif + } + + void CullingScene::ProcessCullablesJobs(const Scene& scene, View& view, AZ::Job& parentJob) + { + AZ_PROFILE_SCOPE(RPI, "CullingScene::ProcessCullablesJobs() - %s", view.GetName().GetCStr()); + + const Matrix4x4& worldToClip = view.GetWorldToClipMatrix(); + AZ::Frustum frustum = Frustum::CreateFromMatrixColumnMajor(worldToClip); + + void* maskedOcclusionCulling = nullptr; + ProcessCullablesCommon(scene, view, frustum, maskedOcclusionCulling); WorkListType worklist; - AZStd::shared_ptr jobData = AZStd::make_shared(); - jobData->m_debugCtx = &m_debugCtx; - jobData->m_scene = &scene; - jobData->m_view = &view; - jobData->m_frustum = frustum; -#if AZ_TRAIT_MASKED_OCCLUSION_CULLING_SUPPORTED - jobData->m_maskedOcclusionCulling = maskedOcclusionCulling; -#endif + AZStd::shared_ptr worklistData = MakeWorklistData(m_debugCtx, scene, view, frustum, maskedOcclusionCulling); - auto nodeVisitorLambda = [jobData, &parentJob, &worklist](const AzFramework::IVisibilityScene::NodeData& nodeData) -> void + auto nodeVisitorLambda = [worklistData, &parentJob, &worklist](const AzFramework::IVisibilityScene::NodeData& nodeData) -> void { AZ_PROFILE_SCOPE(RPI, "nodeVisitorLambda()"); AZ_Assert(nodeData.m_entries.size() > 0, "should not get called with 0 entries"); @@ -606,8 +622,13 @@ namespace AZ if (worklist.size() == worklist.capacity()) { + // capture worklistData & worklist by value + auto processWorklist = [worklistData, worklist]() + { + ProcessWorklist(worklistData, worklist); + }; //Kick off a job to process the (full) worklist - AddObjectsToViewJob* job = aznew AddObjectsToViewJob(jobData, worklist); //pool allocated (cheap), auto-deletes when job finishes + AZ::Job* job = AZ::CreateJobFunction(processWorklist, true); worklist.clear(); parentJob.SetContinuation(job); job->Start(); @@ -616,7 +637,7 @@ namespace AZ if (m_debugCtx.m_enableFrustumCulling) { - m_visScene->Enumerate(frustum, nodeVisitorLambda); + m_visScene->Enumerate(frustum, nodeVisitorLambda); } else { @@ -625,21 +646,76 @@ namespace AZ if (worklist.size() > 0) { - AZStd::shared_ptr remainingJobData = AZStd::make_shared(); - remainingJobData->m_debugCtx = &m_debugCtx; - remainingJobData->m_scene = &scene; - remainingJobData->m_view = &view; - remainingJobData->m_frustum = frustum; -#if AZ_TRAIT_MASKED_OCCLUSION_CULLING_SUPPORTED - remainingJobData->m_maskedOcclusionCulling = maskedOcclusionCulling; -#endif - //Kick off a job to process any remaining workitems - AddObjectsToViewJob* job = aznew AddObjectsToViewJob(remainingJobData, worklist); //pool allocated (cheap), auto-deletes when job finishes + // capture worklistData & worklist by value + auto processWorklist = [worklistData, worklist]() + { + ProcessWorklist(worklistData, worklist); + }; + //Kick off a job to process the (full) worklist + AZ::Job* job = AZ::CreateJobFunction(processWorklist, true); parentJob.SetContinuation(job); job->Start(); } } + void CullingScene::ProcessCullablesTG(const Scene& scene, View& view, AZ::TaskGraph& taskGraph) + { + AZ_PROFILE_SCOPE(RPI, "CullingScene::ProcessCullablesTG() - %s", view.GetName().GetCStr()); + + const Matrix4x4& worldToClip = view.GetWorldToClipMatrix(); + AZ::Frustum frustum = Frustum::CreateFromMatrixColumnMajor(worldToClip); + + void* maskedOcclusionCulling = nullptr; + ProcessCullablesCommon(scene, view, frustum, maskedOcclusionCulling); + + AZStd::unique_ptr worklist = AZStd::make_unique(); + + AZStd::shared_ptr worklistData = MakeWorklistData(m_debugCtx, scene, view, frustum, maskedOcclusionCulling); + static const AZ::TaskDescriptor descriptor{ "AZ::RPI::ProcessWorklist", "Graphics" }; + + auto nodeVisitorLambda = [worklistData, &taskGraph, &worklist](const AzFramework::IVisibilityScene::NodeData& nodeData) -> void + { + AZ_PROFILE_SCOPE(RPI, "nodeVisitorLambda()"); + AZ_Assert(nodeData.m_entries.size() > 0, "should not get called with 0 entries"); + AZ_Assert(worklist->size() < worklist->capacity(), "we should always have room to push a node on the queue"); + + //Queue up a small list of work items (NodeData*) which will be pushed to a worker task once the queue is full. + //This reduces the number of tasks in flight, reducing task-system overhead. + worklist->emplace_back(AZStd::move(nodeData)); + + if (worklist->size() == worklist->capacity()) + { + //Task takes ownership of the worklist unique ptr + taskGraph.AddTask( descriptor, [worklistData, worklist = AZStd::move(worklist)]() + { + ProcessWorklist(worklistData, *worklist.get()); + // allow worklist to go out of scope and be deleted + }); + worklist = AZStd::make_unique(); + } + }; + + if (m_debugCtx.m_enableFrustumCulling) + { + m_visScene->Enumerate(frustum, nodeVisitorLambda); + } + else + { + m_visScene->EnumerateNoCull(nodeVisitorLambda); + } + + if (worklist->size() > 0) + { + //Task takes ownership of the worklist unique ptr + taskGraph.AddTask( descriptor, [worklistData, worklist = AZStd::move(worklist)]() + { + ProcessWorklist(worklistData, *worklist.get()); + // allow worklist to go out of scope and be deleted + }); + } + } + + uint32_t AddLodDataToView(const Vector3& pos, const Cullable::LodData& lodData, RPI::View& view) { #ifdef AZ_CULL_PROFILE_DETAILED @@ -702,6 +778,8 @@ namespace AZ AZ::Name visSceneName(AZStd::string::format("RenderCullScene[%s]", m_parentScene->GetName().GetCStr())); m_visScene = AZ::Interface::Get()->CreateVisibilityScene(visSceneName); + m_taskGraphActive = AZ::Interface::Get(); + #ifdef AZ_CULL_DEBUG_ENABLED AZ_Assert(CountObjectsInScene() == 0, "The culling system should start with 0 entries in this scene."); #endif @@ -719,13 +797,27 @@ namespace AZ } } - void CullingScene::BeginCulling(const AZStd::vector& views) + void CullingScene::BeginCullingTaskGraph(const AZStd::vector& views) { - AZ_PROFILE_SCOPE(RPI, "CullingScene: BeginCulling"); - m_cullDataConcurrencyCheck.soft_lock(); + AZ::TaskGraph taskGraph; + AZ::TaskDescriptor beginCullingDescriptor{"RPI_CullingScene_BeginCullingView", "Graphics"}; + for (auto& view : views) + { + taskGraph.AddTask( + beginCullingDescriptor, + [&view]() + { + view->BeginCulling(); + }); + } - m_debugCtx.ResetCullStats(); - m_debugCtx.m_numCullablesInScene = GetNumCullables(); + AZ::TaskGraphEvent waitForCompletion; + taskGraph.Submit(&waitForCompletion); + waitForCompletion.Wait(); + } + + void CullingScene::BeginCullingJobs(const AZStd::vector& views) + { AZ::JobCompletion beginCullingCompletion; for (auto& view : views) @@ -741,6 +833,26 @@ namespace AZ } beginCullingCompletion.StartAndWaitForCompletion(); + } + + void CullingScene::BeginCulling(const AZStd::vector& views) + { + AZ_PROFILE_SCOPE(RPI, "CullingScene: BeginCulling"); + m_cullDataConcurrencyCheck.soft_lock(); + + m_debugCtx.ResetCullStats(); + m_debugCtx.m_numCullablesInScene = GetNumCullables(); + + m_taskGraphActive = AZ::Interface::Get(); + + if (m_taskGraphActive && m_taskGraphActive->IsTaskGraphActive()) + { + BeginCullingTaskGraph(views); + } + else + { + BeginCullingJobs(views); + } AuxGeomDrawPtr auxGeom; if (m_debugCtx.m_debugDraw) diff --git a/Gems/Atom/RPI/Code/Source/RPI.Public/Scene.cpp b/Gems/Atom/RPI/Code/Source/RPI.Public/Scene.cpp index 41fefda656..8ec968943d 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Public/Scene.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Public/Scene.cpp @@ -111,7 +111,7 @@ namespace AZ { if (m_taskGraphActive) { - WaitTGEvent(m_simulationFinishedTGEvent, &m_simulationFinishedWorkActive); + WaitAndCleanTGEvent(AZStd::move(m_simulationFinishedTGEvent)); } else { @@ -385,8 +385,8 @@ namespace AZ }); } simulationTG.Detach(); - m_simulationFinishedWorkActive = true; - simulationTG.Submit(&m_simulationFinishedTGEvent); + m_simulationFinishedTGEvent = AZStd::make_unique(); + simulationTG.Submit(m_simulationFinishedTGEvent.get()); } void Scene::SimulateJobs() @@ -419,7 +419,7 @@ namespace AZ // If previous simulation job wasn't done, wait for it to finish. if (m_taskGraphActive) { - WaitTGEvent(m_simulationFinishedTGEvent, &m_simulationFinishedWorkActive); + WaitAndCleanTGEvent(AZStd::move(m_simulationFinishedTGEvent)); } else { @@ -449,17 +449,14 @@ namespace AZ } } - void Scene::WaitTGEvent(AZ::TaskGraphEvent& completionTGEvent, AZStd::atomic_bool* workToWaitOn ) + void Scene::WaitAndCleanTGEvent(AZStd::unique_ptr&& completionTGEvent) { - AZ_PROFILE_SCOPE(RPI, "Scene: WaitAndCleanCompletionJob"); - if (!workToWaitOn || workToWaitOn->load()) + AZ_PROFILE_SCOPE(RPI, "Scene: WaitAndCleanTGEvent"); + if (completionTGEvent) { - completionTGEvent.Wait(); - } - if (workToWaitOn) - { - workToWaitOn->store(false); + completionTGEvent->Wait(); } + // allow completionTGEvent to go out of scope and be deleted } void Scene::WaitAndCleanCompletionJob(AZ::JobCompletion*& completionJob) @@ -499,12 +496,12 @@ namespace AZ void Scene::CollectDrawPacketsTaskGraph() { - AZ_PROFILE_SCOPE(RPI, "CollectDrawPackets"); + AZ_PROFILE_SCOPE(RPI, "CollectDrawPacketsTaskGraph"); AZ::TaskGraphEvent collectDrawPacketsTGEvent; static const AZ::TaskDescriptor collectDrawPacketsTGDesc{"RPI_Scene_PrepareRender_CollectDrawPackets", "Graphics"}; - AZ::TaskGraph collectDrawPacketsTG; - // Launch FeatureProcessor::Render() jobs + + // Launch FeatureProcessor::Render() taskgraphs for (auto& fp : m_featureProcessors) { collectDrawPacketsTG.AddTask( @@ -520,32 +517,48 @@ namespace AZ // 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) + static const AZ::TaskDescriptor processCullablesDescriptor{"AZ::RPI::Scene::ProcessCullables", "Graphics"}; + AZ::TaskGraphEvent processCullablesTGEvent; + AZ::TaskGraph processCullablesTG; + if (parallelOctreeTraversal) { - 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) + for (ViewPtr& viewPtr : m_renderPacket.m_views) { - processCullablesJob->SetDependent(&processCullablesCompletion); - processCullablesJob->Start(); - } - else - { - processCullablesJob->StartAndWaitForCompletion(); + processCullablesTG.AddTask(processCullablesDescriptor, [this, &viewPtr, &processCullablesTGEvent]() + { + AZ::TaskGraph subTaskGraph; + m_cullingScene->ProcessCullablesTG(*this, *viewPtr, subTaskGraph); + if (!subTaskGraph.IsEmpty()) + { + subTaskGraph.Detach(); + subTaskGraph.Submit(&processCullablesTGEvent); + } + }); } } + else + { + for (ViewPtr& viewPtr : m_renderPacket.m_views) + { + m_cullingScene->ProcessCullablesTG(*this, *viewPtr, processCullablesTG); + } + } + bool processCullablesHasWork = !processCullablesTG.IsEmpty(); + if (processCullablesHasWork) + { + processCullablesTG.Submit(&processCullablesTGEvent); + } - WaitTGEvent(collectDrawPacketsTGEvent); - processCullablesCompletion.StartAndWaitForCompletion(); + collectDrawPacketsTGEvent.Wait(); + if (processCullablesHasWork) // skip the wait if there is no work to do + { + processCullablesTGEvent.Wait(); + } } void Scene::CollectDrawPacketsJobs() { - AZ_PROFILE_SCOPE(RPI, "CollectDrawPackets"); + AZ_PROFILE_SCOPE(RPI, "CollectDrawPacketsJobs"); AZ::JobCompletion* collectDrawPacketsCompletion = aznew AZ::JobCompletion(); // Launch FeatureProcessor::Render() jobs @@ -567,7 +580,7 @@ namespace AZ { 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 + m_cullingScene->ProcessCullablesJobs(*this, *viewPtr, thisJob); // can't call directly because ProcessCullables needs a parent job }, true, nullptr); //auto-deletes if (m_cullingScene->GetDebugContext().m_parallelOctreeTraversal) @@ -600,7 +613,7 @@ namespace AZ }); } finalizeDrawListsTG.Submit(&finalizeDrawListsTGEvent); - WaitTGEvent(finalizeDrawListsTGEvent); + finalizeDrawListsTGEvent.Wait(); } void Scene::FinalizeDrawListsJobs() @@ -626,7 +639,7 @@ namespace AZ if (m_taskGraphActive) { - WaitTGEvent(m_simulationFinishedTGEvent, &m_simulationFinishedWorkActive); + WaitAndCleanTGEvent(AZStd::move(m_simulationFinishedTGEvent)); } else {