diff --git a/Code/Framework/AzFramework/AzFramework/Terrain/TerrainDataRequestBus.h b/Code/Framework/AzFramework/AzFramework/Terrain/TerrainDataRequestBus.h index 7909a7ec83..a7d65e2bb5 100644 --- a/Code/Framework/AzFramework/AzFramework/Terrain/TerrainDataRequestBus.h +++ b/Code/Framework/AzFramework/AzFramework/Terrain/TerrainDataRequestBus.h @@ -8,10 +8,12 @@ #pragma once #include +#include #include #include #include #include +#include #include #include #include @@ -193,6 +195,147 @@ namespace AzFramework //! Given a ray, return the closest intersection with terrain. virtual RenderGeometry::RayResult GetClosestIntersection(const RenderGeometry::RayRequest& ray) const = 0; + //! A JobContext used to run jobs spawned by calls to the various Process*Async functions. + class TerrainJobContext : public AZ::JobContext + { + public: + TerrainJobContext(AZ::JobManager& jobManager, + int numJobsToComplete) + : JobContext(jobManager) + , m_numJobsToComplete(numJobsToComplete) + { + } + + // When a terrain job context is cancelled, all associated + // jobs are still guaranteed to at least begin processing, + // and if any ProcessAsyncParams::m_completionCallback was + // set it's guaranteed to be called even in the event of a + // cancellation. If a job only begins processing after its + // associated job context has been cancelled, no processing + // will occur and the callback will be invoked immediately, + // otherwise the job may either run to completion or cease + // processing early; the callback is invoked in all cases, + // provided one was specified with the original request. + void Cancel() { m_isCancelled = true; } + + // Was this TerrainJobContext cancelled? + bool IsCancelled() const { return m_isCancelled; } + + // Called by the TerrainSystem when a job associated with + // this TerrainJobContext completes. Returns true if this + // was the final job to be completed, or false otherwise. + bool OnJobCompleted() { return (--m_numJobsToComplete == 0); } + + private: + AZStd::atomic_int m_numJobsToComplete = 0; + AZStd::atomic_bool m_isCancelled = false; + }; + + //! Alias for an optional callback function to invoke when the various Process*Async functions complete. + //! The TerrainJobContext, returned from the original Process*Async function call, is passed as a param + //! to the callback function so it can be queried to see if the job was cancelled or completed normally. + typedef AZStd::function)> ProcessAsyncCompleteCallback; + + //! A parameter group struct that can optionally be passed to the various Process*Async API functions. + struct ProcessAsyncParams + { + //! The default minimum number ofpositions per async terrain request job. + static constexpr int32_t MinPositionsPerJobDefault = 8; + + //! The default number of jobs which async terrain requests will be split into. + static constexpr int32_t NumJobsDefault = 1; + + //! The maximum number of jobs which async terrain requests will be split into. + //! This is not the value itself, rather a constant that can be used to request + //! the work be split into the maximum number of job manager threads available. + static constexpr int32_t NumJobsMax = -1; + + //! The desired number of jobs to split async terrain requests into. + //! The actual value used will be clamped to the number of available job manager threads. + //! + //! Note: Currently, splitting the work over multiple threads causes contention when + //! locking various mutexes, resulting in slower overall wall time for async + //! requests split over multiple threads vs one where all the work is done on + //! a single thread. The latter is still preferable over a regular synchronous + //! call because it is just as quick and prevents the main thread from blocking. + //! This note should be removed once the mutex contention issues have been addressed. + int32_t m_desiredNumberOfJobs = NumJobsDefault; + + //! The minimum number of positions per async terrain request job. + int32_t m_minPositionsPerJob = MinPositionsPerJobDefault; + + //! The callback function that will be invoked when a call to a Process*Async function completes. + //! If the job is cancelled, the completion callback will not be invoked. + ProcessAsyncCompleteCallback m_completionCallback = nullptr; + }; + + //! Asynchronous versions of the various 'Process*' API functions declared above. + //! It's the responsibility of the caller to ensure all callbacks are threadsafe. + virtual AZStd::shared_ptr ProcessHeightsFromListAsync( + const AZStd::span& inPositions, + SurfacePointListFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT, + AZStd::shared_ptr params = nullptr) const = 0; + virtual AZStd::shared_ptr ProcessNormalsFromListAsync( + const AZStd::span& inPositions, + SurfacePointListFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT, + AZStd::shared_ptr params = nullptr) const = 0; + virtual AZStd::shared_ptr ProcessSurfaceWeightsFromListAsync( + const AZStd::span& inPositions, + SurfacePointListFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT, + AZStd::shared_ptr params = nullptr) const = 0; + virtual AZStd::shared_ptr ProcessSurfacePointsFromListAsync( + const AZStd::span& inPositions, + SurfacePointListFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT, + AZStd::shared_ptr params = nullptr) const = 0; + virtual AZStd::shared_ptr ProcessHeightsFromListOfVector2Async( + const AZStd::span& inPositions, + SurfacePointListFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT, + AZStd::shared_ptr params = nullptr) const = 0; + virtual AZStd::shared_ptr ProcessNormalsFromListOfVector2Async( + const AZStd::span& inPositions, + SurfacePointListFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT, + AZStd::shared_ptr params = nullptr) const = 0; + virtual AZStd::shared_ptr ProcessSurfaceWeightsFromListOfVector2Async( + const AZStd::span& inPositions, + SurfacePointListFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT, + AZStd::shared_ptr params = nullptr) const = 0; + virtual AZStd::shared_ptr ProcessSurfacePointsFromListOfVector2Async( + const AZStd::span& inPositions, + SurfacePointListFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT, + AZStd::shared_ptr params = nullptr) const = 0; + virtual AZStd::shared_ptr ProcessHeightsFromRegionAsync( + const AZ::Aabb& inRegion, + const AZ::Vector2& stepSize, + SurfacePointRegionFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT, + AZStd::shared_ptr params = nullptr) const = 0; + virtual AZStd::shared_ptr ProcessNormalsFromRegionAsync( + const AZ::Aabb& inRegion, + const AZ::Vector2& stepSize, + SurfacePointRegionFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT, + AZStd::shared_ptr params = nullptr) const = 0; + virtual AZStd::shared_ptr ProcessSurfaceWeightsFromRegionAsync( + const AZ::Aabb& inRegion, + const AZ::Vector2& stepSize, + SurfacePointRegionFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT, + AZStd::shared_ptr params = nullptr) const = 0; + virtual AZStd::shared_ptr ProcessSurfacePointsFromRegionAsync( + const AZ::Aabb& inRegion, + const AZ::Vector2& stepSize, + SurfacePointRegionFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT, + AZStd::shared_ptr params = nullptr) const = 0; + private: // Private variations of the GetSurfacePoint API exposed to BehaviorContext that returns a value instead of // using an "out" parameter. The "out" parameter is useful for reusing memory allocated in SurfacePoint when diff --git a/Code/Framework/AzFramework/Tests/Mocks/Terrain/MockTerrainDataRequestBus.h b/Code/Framework/AzFramework/Tests/Mocks/Terrain/MockTerrainDataRequestBus.h index 4b9658eebb..8f130ceeae 100644 --- a/Code/Framework/AzFramework/Tests/Mocks/Terrain/MockTerrainDataRequestBus.h +++ b/Code/Framework/AzFramework/Tests/Mocks/Terrain/MockTerrainDataRequestBus.h @@ -106,5 +106,30 @@ namespace UnitTest GetTerrainRaycastEntityContextId, AzFramework::EntityContextId()); MOCK_CONST_METHOD1( GetClosestIntersection, AzFramework::RenderGeometry::RayResult(const AzFramework::RenderGeometry::RayRequest&)); + MOCK_CONST_METHOD4( + ProcessHeightsFromListAsync, AZStd::shared_ptr(const AZStd::span&, AzFramework::Terrain::SurfacePointListFillCallback, Sampler, AZStd::shared_ptr)); + MOCK_CONST_METHOD4( + ProcessNormalsFromListAsync, AZStd::shared_ptr(const AZStd::span&, AzFramework::Terrain::SurfacePointListFillCallback, Sampler, AZStd::shared_ptr)); + MOCK_CONST_METHOD4( + ProcessSurfaceWeightsFromListAsync, AZStd::shared_ptr(const AZStd::span&, AzFramework::Terrain::SurfacePointListFillCallback, Sampler, AZStd::shared_ptr)); + MOCK_CONST_METHOD4( + ProcessSurfacePointsFromListAsync, AZStd::shared_ptr(const AZStd::span&, AzFramework::Terrain::SurfacePointListFillCallback, Sampler, AZStd::shared_ptr)); + MOCK_CONST_METHOD4( + ProcessHeightsFromListOfVector2Async, AZStd::shared_ptr(const AZStd::span&, AzFramework::Terrain::SurfacePointListFillCallback, Sampler, AZStd::shared_ptr)); + MOCK_CONST_METHOD4( + ProcessNormalsFromListOfVector2Async, AZStd::shared_ptr(const AZStd::span&, AzFramework::Terrain::SurfacePointListFillCallback, Sampler, AZStd::shared_ptr)); + MOCK_CONST_METHOD4( + ProcessSurfaceWeightsFromListOfVector2Async, AZStd::shared_ptr(const AZStd::span&, AzFramework::Terrain::SurfacePointListFillCallback, Sampler, AZStd::shared_ptr)); + MOCK_CONST_METHOD4( + ProcessSurfacePointsFromListOfVector2Async, AZStd::shared_ptr(const AZStd::span&, AzFramework::Terrain::SurfacePointListFillCallback, Sampler, AZStd::shared_ptr)); + MOCK_CONST_METHOD5( + ProcessHeightsFromRegionAsync, AZStd::shared_ptr(const AZ::Aabb&, const AZ::Vector2&, AzFramework::Terrain::SurfacePointRegionFillCallback, Sampler, AZStd::shared_ptr)); + MOCK_CONST_METHOD5( + ProcessNormalsFromRegionAsync, AZStd::shared_ptr(const AZ::Aabb&, const AZ::Vector2&, AzFramework::Terrain::SurfacePointRegionFillCallback, Sampler, AZStd::shared_ptr)); + MOCK_CONST_METHOD5( + ProcessSurfaceWeightsFromRegionAsync, AZStd::shared_ptr(const AZ::Aabb&, const AZ::Vector2&, AzFramework::Terrain::SurfacePointRegionFillCallback, Sampler, AZStd::shared_ptr)); + MOCK_CONST_METHOD5( + ProcessSurfacePointsFromRegionAsync, AZStd::shared_ptr(const AZ::Aabb&, const AZ::Vector2&, AzFramework::Terrain::SurfacePointRegionFillCallback, Sampler, AZStd::shared_ptr)); + }; } // namespace UnitTest diff --git a/Gems/Terrain/Code/Source/Components/TerrainWorldDebuggerComponent.cpp b/Gems/Terrain/Code/Source/Components/TerrainWorldDebuggerComponent.cpp index 7b8020c3ad..899c5e4665 100644 --- a/Gems/Terrain/Code/Source/Components/TerrainWorldDebuggerComponent.cpp +++ b/Gems/Terrain/Code/Source/Components/TerrainWorldDebuggerComponent.cpp @@ -170,9 +170,10 @@ namespace Terrain // they'll get refreshed the next time we need to draw them. for (auto& sector : m_wireframeSectors) { + AZStd::lock_guard lock(sector.m_sectorStateMutex); if (!dirtyRegion2D.IsValid() || dirtyRegion2D.Overlaps(sector.m_aabb)) { - sector.m_isDirty = true; + sector.SetDirty(); } } } @@ -283,10 +284,13 @@ namespace Terrain sectorAabb.Clamp(worldBounds); // If the world space box for the sector doesn't match, set it and mark the sector as dirty so we refresh the height data. - if (sector.m_aabb != sectorAabb) { - sector.m_aabb = sectorAabb; - sector.m_isDirty = true; + AZStd::lock_guard lock(sector.m_sectorStateMutex); + if (sector.m_aabb != sectorAabb) + { + sector.m_aabb = sectorAabb; + sector.SetDirty(); + } } } } @@ -295,12 +299,18 @@ namespace Terrain // (Sectors that are outside the world bounds won't have any valid data, so they'll get skipped) for (auto& sector : m_wireframeSectors) { + AZStd::lock_guard lock(sector.m_sectorStateMutex); + if (sector.m_jobContext) + { + // The previous async request for this sector has yet to complete. + continue; + } + if (sector.m_isDirty) { RebuildSectorWireframe(sector, heightDataResolution); } - - if (!sector.m_lineVertices.empty()) + else if (!sector.m_lineVertices.empty()) { const AZ::Color primaryColor = AZ::Color(0.25f, 0.25f, 0.25f, 1.0f); debugDisplay.DrawLines(sector.m_lineVertices, primaryColor); @@ -319,6 +329,7 @@ namespace Terrain void TerrainWorldDebuggerComponent::RebuildSectorWireframe(WireframeSector& sector, float gridResolution) { + AZStd::lock_guard lock(sector.m_sectorStateMutex); if (!sector.m_isDirty) { return; @@ -346,16 +357,24 @@ namespace Terrain sector.m_lineVertices.reserve(numSamplesX * numSamplesY * 4); // This keeps track of the height from the previous point for the _ line. - float previousHeight = 0.0f; + sector.m_previousHeight = 0.0f; // This keeps track of the heights from the previous row for the | line. - AZStd::vector rowHeights(numSamplesX); + sector.m_rowHeights.clear(); + sector.m_rowHeights.resize(numSamplesX); // For each terrain height value in the region, create the _| grid lines for that point and cache off the height value // for use with subsequent grid line calculations. - auto ProcessHeightValue = [gridResolution, &previousHeight, &rowHeights, §or] + auto ProcessHeightValue = [gridResolution, §or] (size_t xIndex, size_t yIndex, const AzFramework::SurfaceData::SurfacePoint& surfacePoint, [[maybe_unused]] bool terrainExists) { + AZStd::lock_guard lock(sector.m_sectorStateMutex); + if (sector.m_isDirty) + { + // Bail out if this sector has become dirty again since the async request started. + return; + } + // Don't add any vertices for the first column or first row. These grid lines will be handled by an adjacent sector, if // there is one. if ((xIndex > 0) && (yIndex > 0)) @@ -363,23 +382,49 @@ namespace Terrain float x = surfacePoint.m_position.GetX() - gridResolution; float y = surfacePoint.m_position.GetY() - gridResolution; - sector.m_lineVertices.emplace_back(AZ::Vector3(x, surfacePoint.m_position.GetY(), previousHeight)); + sector.m_lineVertices.emplace_back(AZ::Vector3(x, surfacePoint.m_position.GetY(), sector.m_previousHeight)); sector.m_lineVertices.emplace_back(surfacePoint.m_position); - sector.m_lineVertices.emplace_back(AZ::Vector3(surfacePoint.m_position.GetX(), y, rowHeights[xIndex])); + sector.m_lineVertices.emplace_back(AZ::Vector3(surfacePoint.m_position.GetX(), y, sector.m_rowHeights[xIndex])); sector.m_lineVertices.emplace_back(surfacePoint.m_position); } // Save off the heights so that we can use them to draw subsequent columns and rows. - previousHeight = surfacePoint.m_position.GetZ(); - rowHeights[xIndex] = surfacePoint.m_position.GetZ(); + sector.m_previousHeight = surfacePoint.m_position.GetZ(); + sector.m_rowHeights[xIndex] = surfacePoint.m_position.GetZ(); }; - + + auto completionCallback = [§or](AZStd::shared_ptr) + { + // This must happen outside the lock, + // otherwise we will get a deadlock if + // WireframeSector::Reset is waiting for + // the completion event to be signalled. + sector.m_jobCompletionEvent->release(); + + // Reset the job context once the async request has completed, + // clearing the way for future requests to be made for this sector. + AZStd::lock_guard lock(sector.m_sectorStateMutex); + sector.m_jobContext.reset(); + }; + + AZStd::shared_ptr asyncParams + = AZStd::make_shared(); + asyncParams->m_completionCallback = completionCallback; + + sector.m_jobCompletionEvent = AZStd::make_unique(); AZ::Vector2 stepSize = AZ::Vector2(gridResolution); - AzFramework::Terrain::TerrainDataRequestBus::Broadcast(&AzFramework::Terrain::TerrainDataRequests::ProcessHeightsFromRegion, - region, stepSize, ProcessHeightValue, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT); + AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult( + sector.m_jobContext, + &AzFramework::Terrain::TerrainDataRequests::ProcessHeightsFromRegionAsync, + region, + stepSize, + ProcessHeightValue, + AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, + asyncParams); } + void TerrainWorldDebuggerComponent::OnTerrainDataChanged(const AZ::Aabb& dirtyRegion, TerrainDataChangedMask dataChangedMask) { if (dataChangedMask & (TerrainDataChangedMask::Settings | TerrainDataChangedMask::HeightData)) @@ -395,5 +440,76 @@ namespace Terrain } } + TerrainWorldDebuggerComponent::WireframeSector::WireframeSector(const WireframeSector& other) + { + AZStd::lock_guard lock(m_sectorStateMutex); + m_jobContext = other.m_jobContext; + m_aabb = other.m_aabb; + m_lineVertices = other.m_lineVertices; + m_rowHeights = other.m_rowHeights; + m_previousHeight = other.m_previousHeight; + m_isDirty = other.m_isDirty; + } + TerrainWorldDebuggerComponent::WireframeSector::WireframeSector(WireframeSector&& other) + { + AZStd::lock_guard lock(m_sectorStateMutex); + m_jobContext = AZStd::move(other.m_jobContext); + m_aabb = AZStd::move(other.m_aabb); + m_lineVertices = AZStd::move(other.m_lineVertices); + m_rowHeights = AZStd::move(other.m_rowHeights); + m_previousHeight = AZStd::move(other.m_previousHeight); + m_isDirty = AZStd::move(other.m_isDirty); + } + + TerrainWorldDebuggerComponent::WireframeSector& TerrainWorldDebuggerComponent::WireframeSector::operator=(const WireframeSector& other) + { + AZStd::lock_guard lock(m_sectorStateMutex); + m_jobContext = other.m_jobContext; + m_aabb = other.m_aabb; + m_lineVertices = other.m_lineVertices; + m_rowHeights = other.m_rowHeights; + m_previousHeight = other.m_previousHeight; + m_isDirty = other.m_isDirty; + return *this; + } + + TerrainWorldDebuggerComponent::WireframeSector& TerrainWorldDebuggerComponent::WireframeSector::operator=(WireframeSector&& other) + { + AZStd::lock_guard lock(m_sectorStateMutex); + m_jobContext = AZStd::move(other.m_jobContext); + m_aabb = AZStd::move(other.m_aabb); + m_lineVertices = AZStd::move(other.m_lineVertices); + m_rowHeights = AZStd::move(other.m_rowHeights); + m_previousHeight = AZStd::move(other.m_previousHeight); + m_isDirty = AZStd::move(other.m_isDirty); + return *this; + } + + void TerrainWorldDebuggerComponent::WireframeSector::Reset() + { + AZStd::lock_guard lock(m_sectorStateMutex); + if (m_jobContext) + { + // Cancel the job and wait until it completes. + m_jobContext->Cancel(); + m_jobCompletionEvent->acquire(); + m_jobCompletionEvent.reset(); + m_jobContext.reset(); + } + m_aabb = AZ::Aabb::CreateNull(); + m_lineVertices.clear(); + m_rowHeights.clear(); + m_previousHeight = 0.0f; + m_isDirty = true; + } + + void TerrainWorldDebuggerComponent::WireframeSector::SetDirty() + { + m_isDirty = true; + if (m_jobContext) + { + m_jobContext->Cancel(); + } + } } // namespace Terrain diff --git a/Gems/Terrain/Code/Source/Components/TerrainWorldDebuggerComponent.h b/Gems/Terrain/Code/Source/Components/TerrainWorldDebuggerComponent.h index 13c602c48d..7339dc9bb3 100644 --- a/Gems/Terrain/Code/Source/Components/TerrainWorldDebuggerComponent.h +++ b/Gems/Terrain/Code/Source/Components/TerrainWorldDebuggerComponent.h @@ -88,8 +88,25 @@ namespace Terrain // the wireframe representation in each direction. struct WireframeSector { + WireframeSector() = default; + ~WireframeSector() = default; + WireframeSector(const WireframeSector& other); + WireframeSector(WireframeSector&& other); + WireframeSector& operator=(const WireframeSector& other); + WireframeSector& operator=(WireframeSector&& other); + + void Reset(); + + // This should only be called within the scope of a lock on m_sectorStateMutex. + void SetDirty(); + + AZStd::shared_ptr m_jobContext; + AZStd::unique_ptr m_jobCompletionEvent; + AZStd::recursive_mutex m_sectorStateMutex; AZ::Aabb m_aabb{ AZ::Aabb::CreateNull() }; AZStd::vector m_lineVertices; + AZStd::vector m_rowHeights; + float m_previousHeight = 0.0f; bool m_isDirty{ true }; }; diff --git a/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.cpp b/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.cpp index 6ce83094e4..2a8f1ba1ca 100644 --- a/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.cpp +++ b/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.cpp @@ -61,6 +61,10 @@ TerrainSystem::TerrainSystem() m_requestedSettings = m_currentSettings; m_requestedSettings.m_worldBounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(-512.0f), AZ::Vector3(512.0f)); + + // Use the global JobManager for terrain jobs (we could create our own dedicated terrain JobManager if needed). + AZ::JobManagerBus::BroadcastResult(m_terrainJobManager, &AZ::JobManagerEvents::GetManager); + AZ_Assert(m_terrainJobManager, "No global JobManager found."); } TerrainSystem::~TerrainSystem() @@ -106,6 +110,16 @@ void TerrainSystem::Activate() void TerrainSystem::Deactivate() { + { + // Cancel all active terrain jobs, and wait until they have completed. + AZStd::unique_lock lock(m_activeTerrainJobContextMutex); + for (auto activeTerrainJobContext : m_activeTerrainJobContexts) + { + activeTerrainJobContext->Cancel(); + } + m_activeTerrainJobContextMutexConditionVariable.wait(lock, [this]{ return m_activeTerrainJobContexts.empty(); }); + } + // Stop listening to the bus even before we signal DestroyBegin so that way any calls to the terrain system as a *result* of // calling DestroyBegin will fail to reach the terrain system. AzFramework::Terrain::TerrainDataRequestBus::Handler::BusDisconnect(); @@ -670,6 +684,130 @@ AzFramework::RenderGeometry::RayResult TerrainSystem::GetClosestIntersection( return m_terrainRaycastContext.RayIntersect(ray); } +AZStd::shared_ptr TerrainSystem::ProcessHeightsFromListAsync( + const AZStd::span& inPositions, + AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback, + Sampler sampleFilter, + AZStd::shared_ptr params) const +{ + return ProcessFromListAsync(AZStd::bind(&TerrainSystem::ProcessHeightsFromList, this, AZStd::placeholders::_1, std::placeholders::_2, std::placeholders::_3), + inPositions, perPositionCallback, sampleFilter, params); +} + +AZStd::shared_ptr TerrainSystem::ProcessNormalsFromListAsync( + const AZStd::span& inPositions, + AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback, + Sampler sampleFilter, + AZStd::shared_ptr params) const +{ + return ProcessFromListAsync(AZStd::bind(&TerrainSystem::ProcessNormalsFromList, this, AZStd::placeholders::_1, std::placeholders::_2, std::placeholders::_3), + inPositions, perPositionCallback, sampleFilter, params); +} + +AZStd::shared_ptr TerrainSystem::ProcessSurfaceWeightsFromListAsync( + const AZStd::span& inPositions, + AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback, + Sampler sampleFilter, + AZStd::shared_ptr params) const +{ + return ProcessFromListAsync(AZStd::bind(&TerrainSystem::ProcessSurfaceWeightsFromList, this, AZStd::placeholders::_1, std::placeholders::_2, std::placeholders::_3), + inPositions, perPositionCallback, sampleFilter, params); +} + +AZStd::shared_ptr TerrainSystem::ProcessSurfacePointsFromListAsync( + const AZStd::span& inPositions, + AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback, + Sampler sampleFilter, + AZStd::shared_ptr params) const +{ + return ProcessFromListAsync(AZStd::bind(&TerrainSystem::ProcessSurfacePointsFromList, this, AZStd::placeholders::_1, std::placeholders::_2, std::placeholders::_3), + inPositions, perPositionCallback, sampleFilter, params); +} + +AZStd::shared_ptr TerrainSystem::ProcessHeightsFromListOfVector2Async( + const AZStd::span& inPositions, + AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback, + Sampler sampleFilter, + AZStd::shared_ptr params) const +{ + return ProcessFromListAsync(AZStd::bind(&TerrainSystem::ProcessHeightsFromListOfVector2, this, AZStd::placeholders::_1, std::placeholders::_2, std::placeholders::_3), + inPositions, perPositionCallback, sampleFilter, params); +} + +AZStd::shared_ptr TerrainSystem::ProcessNormalsFromListOfVector2Async( + const AZStd::span& inPositions, + AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback, + Sampler sampleFilter, + AZStd::shared_ptr params) const +{ + return ProcessFromListAsync(AZStd::bind(&TerrainSystem::ProcessNormalsFromListOfVector2, this, AZStd::placeholders::_1, std::placeholders::_2, std::placeholders::_3), + inPositions, perPositionCallback, sampleFilter, params); +} + +AZStd::shared_ptr TerrainSystem::ProcessSurfaceWeightsFromListOfVector2Async( + const AZStd::span& inPositions, + AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback, + Sampler sampleFilter, + AZStd::shared_ptr params) const +{ + return ProcessFromListAsync(AZStd::bind(&TerrainSystem::ProcessSurfaceWeightsFromListOfVector2, this, AZStd::placeholders::_1, std::placeholders::_2, std::placeholders::_3), + inPositions, perPositionCallback, sampleFilter, params); +} + +AZStd::shared_ptr TerrainSystem::ProcessSurfacePointsFromListOfVector2Async( + const AZStd::span& inPositions, + AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback, + Sampler sampleFilter, + AZStd::shared_ptr params) const +{ + return ProcessFromListAsync(AZStd::bind(&TerrainSystem::ProcessSurfacePointsFromListOfVector2, this, AZStd::placeholders::_1, std::placeholders::_2, std::placeholders::_3), + inPositions, perPositionCallback, sampleFilter, params); +} + +AZStd::shared_ptr TerrainSystem::ProcessHeightsFromRegionAsync( + const AZ::Aabb& inRegion, + const AZ::Vector2& stepSize, + AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback, + Sampler sampleFilter, + AZStd::shared_ptr params) const +{ + return ProcessFromRegionAsync(AZStd::bind(&TerrainSystem::ProcessHeightsFromRegion, this, AZStd::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4), + inRegion, stepSize, perPositionCallback, sampleFilter, params); +} + +AZStd::shared_ptr TerrainSystem::ProcessNormalsFromRegionAsync( + const AZ::Aabb& inRegion, + const AZ::Vector2& stepSize, + AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback, + Sampler sampleFilter, + AZStd::shared_ptr params) const +{ + return ProcessFromRegionAsync(AZStd::bind(&TerrainSystem::ProcessNormalsFromRegion, this, AZStd::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4), + inRegion, stepSize, perPositionCallback, sampleFilter, params); +} + +AZStd::shared_ptr TerrainSystem::ProcessSurfaceWeightsFromRegionAsync( + const AZ::Aabb& inRegion, + const AZ::Vector2& stepSize, + AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback, + Sampler sampleFilter, + AZStd::shared_ptr params) const +{ + return ProcessFromRegionAsync(AZStd::bind(&TerrainSystem::ProcessSurfaceWeightsFromRegion, this, AZStd::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4), + inRegion, stepSize, perPositionCallback, sampleFilter, params); +} + +AZStd::shared_ptr TerrainSystem::ProcessSurfacePointsFromRegionAsync( + const AZ::Aabb& inRegion, + const AZ::Vector2& stepSize, + AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback, + Sampler sampleFilter, + AZStd::shared_ptr params) const +{ + return ProcessFromRegionAsync(AZStd::bind(&TerrainSystem::ProcessSurfacePointsFromRegion, this, AZStd::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4), + inRegion, stepSize, perPositionCallback, sampleFilter, params); +} + AZ::EntityId TerrainSystem::FindBestAreaEntityAtPosition(float x, float y, AZ::Aabb& bounds) const { AZ::Vector3 inPosition = AZ::Vector3(x, y, 0); diff --git a/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.h b/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.h index 7895ab96cc..a4a309c0a7 100644 --- a/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.h +++ b/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.h @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -192,7 +193,89 @@ namespace Terrain AzFramework::RenderGeometry::RayResult GetClosestIntersection( const AzFramework::RenderGeometry::RayRequest& ray) const override; + AZStd::shared_ptr ProcessHeightsFromListAsync( + const AZStd::span& inPositions, + AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT, + AZStd::shared_ptr params = nullptr) const override; + AZStd::shared_ptr ProcessNormalsFromListAsync( + const AZStd::span& inPositions, + AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT, + AZStd::shared_ptr params = nullptr) const override; + AZStd::shared_ptr ProcessSurfaceWeightsFromListAsync( + const AZStd::span& inPositions, + AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT, + AZStd::shared_ptr params = nullptr) const override; + AZStd::shared_ptr ProcessSurfacePointsFromListAsync( + const AZStd::span& inPositions, + AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT, + AZStd::shared_ptr params = nullptr) const override; + AZStd::shared_ptr ProcessHeightsFromListOfVector2Async( + const AZStd::span& inPositions, + AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT, + AZStd::shared_ptr params = nullptr) const override; + AZStd::shared_ptr ProcessNormalsFromListOfVector2Async( + const AZStd::span& inPositions, + AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT, + AZStd::shared_ptr params = nullptr) const override; + AZStd::shared_ptr ProcessSurfaceWeightsFromListOfVector2Async( + const AZStd::span& inPositions, + AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT, + AZStd::shared_ptr params = nullptr) const override; + AZStd::shared_ptr ProcessSurfacePointsFromListOfVector2Async( + const AZStd::span& inPositions, + AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT, + AZStd::shared_ptr params = nullptr) const override; + AZStd::shared_ptr ProcessHeightsFromRegionAsync( + const AZ::Aabb& inRegion, + const AZ::Vector2& stepSize, + AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT, + AZStd::shared_ptr params = nullptr) const override; + AZStd::shared_ptr ProcessNormalsFromRegionAsync( + const AZ::Aabb& inRegion, + const AZ::Vector2& stepSize, + AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT, + AZStd::shared_ptr params = nullptr) const override; + AZStd::shared_ptr ProcessSurfaceWeightsFromRegionAsync( + const AZ::Aabb& inRegion, + const AZ::Vector2& stepSize, + AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT, + AZStd::shared_ptr params = nullptr) const override; + AZStd::shared_ptr ProcessSurfacePointsFromRegionAsync( + const AZ::Aabb& inRegion, + const AZ::Vector2& stepSize, + AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT, + AZStd::shared_ptr params = nullptr) const override; + private: + template + AZStd::shared_ptr ProcessFromListAsync( + SynchronousFunctionType synchronousFunction, + const AZStd::span& inPositions, + AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT, + AZStd::shared_ptr params = nullptr) const; + + template + AZStd::shared_ptr ProcessFromRegionAsync( + SynchronousFunctionType synchronousFunction, + const AZ::Aabb& inRegion, + const AZ::Vector2& stepSize, + AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT, + AZStd::shared_ptr params = nullptr) const; + void ClampPosition(float x, float y, AZ::Vector2& outPosition, AZ::Vector2& normalizedDelta) const; bool InWorldBounds(float x, float y) const; @@ -268,5 +351,166 @@ namespace Terrain AZStd::map m_registeredAreas; mutable TerrainRaycastContext m_terrainRaycastContext; + + AZ::JobManager* m_terrainJobManager = nullptr; + mutable AZStd::mutex m_activeTerrainJobContextMutex; + mutable AZStd::condition_variable m_activeTerrainJobContextMutexConditionVariable; + mutable AZStd::deque> m_activeTerrainJobContexts; }; + + template + inline AZStd::shared_ptr TerrainSystem::ProcessFromListAsync( + SynchronousFunctionType synchronousFunction, + const AZStd::span& inPositions, + AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback, + Sampler sampleFilter, + AZStd::shared_ptr params) const + { + // Determine the number of jobs to split the work into based on: + // 1. The number of available worker threads. + // 2. The desired number of jobs as passed in. + // 3. The number of positions being processed. + const int32_t numWorkerThreads = m_terrainJobManager->GetNumWorkerThreads(); + const int32_t numJobsDesired = params ? params->m_desiredNumberOfJobs : ProcessAsyncParams::NumJobsDefault; + const int32_t numJobsMax = (numJobsDesired > 0) ? AZStd::min(numWorkerThreads, numJobsDesired) : numWorkerThreads; + const int32_t numPositionsToProcess = static_cast(inPositions.size()); + const int32_t minPositionsPerJob = params && (params->m_desiredNumberOfJobs > 0) ? params->m_desiredNumberOfJobs : ProcessAsyncParams::MinPositionsPerJobDefault; + const int32_t numJobs = AZStd::min(numJobsMax, numPositionsToProcess / minPositionsPerJob); + if (numJobs <= 0) + { + AZ_Warning("TerrainSystem", false, "No positions to process."); + return nullptr; + } + + // Create a terrain job context, track it, and split the work across multiple jobs. + AZStd::shared_ptr jobContext = AZStd::make_shared(*m_terrainJobManager, numJobs); + { + AZStd::unique_lock lock(m_activeTerrainJobContextMutex); + m_activeTerrainJobContexts.push_back(jobContext); + } + const int32_t numPositionsPerJob = numPositionsToProcess / numJobs; + for (int32_t i = 0; i < numJobs; ++i) + { + // If the number of positions can't be divided evenly by the number of jobs, + // ensure we still process the remaining positions along with the final job. + const size_t subSpanOffset = i * numPositionsPerJob; + const size_t subSpanCount = (i < numJobs - 1) ? numPositionsPerJob : AZStd::dynamic_extent; + + // Define the job function using the sub span of positions to process. + const AZStd::span& positionsToProcess = inPositions.subspan(subSpanOffset, subSpanCount); + auto jobFunction = [this, synchronousFunction, positionsToProcess, perPositionCallback, sampleFilter, jobContext, params]() + { + // Process the sub span of positions, unless the associated job context has been cancelled. + if (!jobContext->IsCancelled()) + { + synchronousFunction(positionsToProcess, perPositionCallback, sampleFilter); + } + + // Decrement the number of completions remaining, invoke the completion callback if this happens + // to be the final job completed, and remove this TerrainJobContext from the list of active ones. + const bool wasLastJobCompleted = jobContext->OnJobCompleted(); + if (wasLastJobCompleted) + { + if (params && params->m_completionCallback) + { + params->m_completionCallback(jobContext); + } + + { + AZStd::unique_lock lock(m_activeTerrainJobContextMutex); + m_activeTerrainJobContexts.erase(AZStd::find(m_activeTerrainJobContexts.begin(), + m_activeTerrainJobContexts.end(), + jobContext)); + m_activeTerrainJobContextMutexConditionVariable.notify_one(); + } + } + }; + + // Create the job and start it immediately. + AZ::Job* processJob = AZ::CreateJobFunction(jobFunction, true, jobContext.get()); + processJob->Start(); + } + + return jobContext; + } + + template + inline AZStd::shared_ptr TerrainSystem::ProcessFromRegionAsync( + SynchronousFunctionType synchronousFunction, + const AZ::Aabb& inRegion, + const AZ::Vector2& stepSize, + AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback, + Sampler sampleFilter, + AZStd::shared_ptr params) const + { + // ToDo: Determine the number of jobs to split the work into based on: + // 1. The number of available worker threads. + // 2. The desired number of jobs as passed in. + // 3. The size of the area being processed. + // + // Note: We are currently restricting the number of worker threads to one + // because splitting the work over multiple threads causes contention when + // locking various mutexes, resulting in slower overall wall time for async + // requests split over multiple threads vs one where all the work is done on + // a single thread. The latter is still preferable over a regular synchronous + // call because it is just as quick and prevents the main thread from blocking. + // Once the mutex contention issues have been addressed, we should come up with + // an algorithm to break up 'inRegion' into sub-regions (or lists of positions?) + // so that async calls automatically split the work between available job manager + // worker threads, unless the ProcessAsyncParams specifiy a desired number of jobs. + const int32_t numWorkerThreads = m_terrainJobManager->GetNumWorkerThreads(); + const int32_t numJobsDesired = params ? params->m_desiredNumberOfJobs : ProcessAsyncParams::NumJobsDefault; + int32_t numJobs = (numJobsDesired > 0) ? AZStd::min(numWorkerThreads, numJobsDesired) : numWorkerThreads; + if (numJobs != 1) + { + // Temp until we figure out how to break up the region. + AZ_Warning("TerrainSystem", false, "We don't yet support breaking up regions."); + numJobs = 1; + } + + // Create a terrain job context and split the work across multiple jobs. + AZStd::shared_ptr jobContext = AZStd::make_shared(*m_terrainJobManager, numJobs); + { + AZStd::unique_lock lock(m_activeTerrainJobContextMutex); + m_activeTerrainJobContexts.push_back(jobContext); + } + for (int32_t i = 0; i < numJobs; ++i) + { + // Define the job function using the sub region of positions to process. + const AZ::Aabb& subRegion = inRegion; // ToDo: Figure out how to break up the region. + auto jobFunction = [this, synchronousFunction, subRegion, stepSize, perPositionCallback, sampleFilter, jobContext, params]() + { + // Process the sub region of positions, unless the associated job context has been cancelled. + if (!jobContext->IsCancelled()) + { + synchronousFunction(subRegion, stepSize, perPositionCallback, sampleFilter); + } + + // Decrement the number of completions remaining, invoke the completion callback if this happens + // to be the final job completed, and remove this TerrainJobContext from the list of active ones. + const bool wasLastJobCompleted = jobContext->OnJobCompleted(); + if (wasLastJobCompleted) + { + if (params && params->m_completionCallback) + { + params->m_completionCallback(jobContext); + } + + { + AZStd::unique_lock lock(m_activeTerrainJobContextMutex); + m_activeTerrainJobContexts.erase(AZStd::find(m_activeTerrainJobContexts.begin(), + m_activeTerrainJobContexts.end(), + jobContext)); + m_activeTerrainJobContextMutexConditionVariable.notify_one(); + } + } + }; + + // Create the job and start it immediately. + AZ::Job* processJob = AZ::CreateJobFunction(jobFunction, true, jobContext.get()); + processJob->Start(); + } + + return jobContext; + } } // namespace Terrain diff --git a/Gems/Terrain/Code/Tests/TerrainSystemBenchmarks.cpp b/Gems/Terrain/Code/Tests/TerrainSystemBenchmarks.cpp index 6aad3f9568..7327dc4158 100644 --- a/Gems/Terrain/Code/Tests/TerrainSystemBenchmarks.cpp +++ b/Gems/Terrain/Code/Tests/TerrainSystemBenchmarks.cpp @@ -13,6 +13,8 @@ #include #include #include +#include +#include #include #include #include @@ -263,7 +265,17 @@ namespace UnitTest auto spawnerShapeRequests = CreateMockShape(worldBounds, testLayerSpawnerEntity->GetId()); ActivateEntity(testLayerSpawnerEntity.get()); + // Create the global job manager. + auto serializeContext = AZStd::make_unique(); + auto jobManagerComponentDescriptor = AZ::JobManagerComponent::CreateDescriptor(); + jobManagerComponentDescriptor->Reflect(serializeContext.get()); + auto jobManagerEntity = AZStd::make_unique(); + jobManagerEntity->CreateComponent(); + jobManagerEntity->Init(); + jobManagerEntity->Activate(); + // Create the terrain system (do this after creating the terrain layer entity to ensure that we don't need any data refreshes) + // Also ensure to do this after creating the global JobManager. auto terrainSystem = CreateAndActivateTerrainSystem(queryResolution, worldBounds); // Call the terrain API we're testing for every height and width in our ranges. @@ -372,6 +384,51 @@ namespace UnitTest ->Args({ 4096, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) ->Unit(::benchmark::kMillisecond); + BENCHMARK_DEFINE_F(TerrainSystemBenchmarkFixture, BM_ProcessHeightsRegionAsync)(benchmark::State& state) + { + // Run the benchmark + RunTerrainApiBenchmark( + state, + []([[maybe_unused]] float queryResolution, const AZ::Aabb& worldBounds, + AzFramework::Terrain::TerrainDataRequests::Sampler sampler) + { + auto perPositionCallback = []([[maybe_unused]] size_t xIndex, [[maybe_unused]] size_t yIndex, + const AzFramework::SurfaceData::SurfacePoint& surfacePoint, [[maybe_unused]] bool terrainExists) + { + benchmark::DoNotOptimize(surfacePoint.m_position.GetZ()); + }; + + AZStd::semaphore completionEvent; + auto completionCallback = [&completionEvent](AZStd::shared_ptr) + { + completionEvent.release(); + }; + + AZStd::shared_ptr asyncParams + = AZStd::make_shared(); + asyncParams->m_completionCallback = completionCallback; + + AZ::Vector2 stepSize = AZ::Vector2(queryResolution); + AzFramework::Terrain::TerrainDataRequestBus::Broadcast( + &AzFramework::Terrain::TerrainDataRequests::ProcessHeightsFromRegionAsync, worldBounds, stepSize, perPositionCallback, sampler, asyncParams); + + completionEvent.acquire(); + } + ); + } + + BENCHMARK_REGISTER_F(TerrainSystemBenchmarkFixture, BM_ProcessHeightsRegionAsync) + ->Args({ 1024, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR) }) + ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR) }) + ->Args({ 4096, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR) }) + ->Args({ 1024, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP) }) + ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP) }) + ->Args({ 4096, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP) }) + ->Args({ 1024, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Args({ 4096, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Unit(::benchmark::kMillisecond); + BENCHMARK_DEFINE_F(TerrainSystemBenchmarkFixture, BM_ProcessHeightsList)(benchmark::State& state) { // Run the benchmark @@ -406,6 +463,51 @@ namespace UnitTest ->Args({ 4096, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) ->Unit(::benchmark::kMillisecond); + BENCHMARK_DEFINE_F(TerrainSystemBenchmarkFixture, BM_ProcessHeightsListAsync)(benchmark::State& state) + { + // Run the benchmark + RunTerrainApiBenchmark( + state, + [this]([[maybe_unused]] float queryResolution, const AZ::Aabb& worldBounds, + AzFramework::Terrain::TerrainDataRequests::Sampler sampler) + { + AZStd::vector inPositions; + GenerateInputPositionsList(queryResolution, worldBounds, inPositions); + + auto perPositionCallback = [](const AzFramework::SurfaceData::SurfacePoint& surfacePoint, [[maybe_unused]] bool terrainExists) + { + benchmark::DoNotOptimize(surfacePoint.m_position.GetZ()); + }; + + AZStd::semaphore completionEvent; + auto completionCallback = [&completionEvent](AZStd::shared_ptr) + { + completionEvent.release(); + }; + + AZStd::shared_ptr asyncParams + = AZStd::make_shared(); + asyncParams->m_completionCallback = completionCallback; + AzFramework::Terrain::TerrainDataRequestBus::Broadcast( + &AzFramework::Terrain::TerrainDataRequests::ProcessHeightsFromListAsync, inPositions, perPositionCallback, sampler, asyncParams); + + completionEvent.acquire(); + } + ); + } + + BENCHMARK_REGISTER_F(TerrainSystemBenchmarkFixture, BM_ProcessHeightsListAsync) + ->Args({ 1024, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR) }) + ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR) }) + ->Args({ 4096, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR) }) + ->Args({ 1024, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP) }) + ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP) }) + ->Args({ 4096, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP) }) + ->Args({ 1024, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Args({ 4096, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Unit(::benchmark::kMillisecond); + BENCHMARK_DEFINE_F(TerrainSystemBenchmarkFixture, BM_GetNormal)(benchmark::State& state) { // Run the benchmark @@ -467,6 +569,48 @@ namespace UnitTest ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) ->Unit(::benchmark::kMillisecond); + BENCHMARK_DEFINE_F(TerrainSystemBenchmarkFixture, BM_ProcessNormalsRegionAsync)(benchmark::State& state) + { + // Run the benchmark + RunTerrainApiBenchmark( + state, + []([[maybe_unused]] float queryResolution, const AZ::Aabb& worldBounds, + AzFramework::Terrain::TerrainDataRequests::Sampler sampler) + { + auto perPositionCallback = []([[maybe_unused]] size_t xIndex, [[maybe_unused]] size_t yIndex, + const AzFramework::SurfaceData::SurfacePoint& surfacePoint, [[maybe_unused]] bool terrainExists) + { + benchmark::DoNotOptimize(surfacePoint.m_normal); + }; + + AZStd::semaphore completionEvent; + auto completionCallback = [&completionEvent](AZStd::shared_ptr) + { + completionEvent.release(); + }; + + AZStd::shared_ptr asyncParams + = AZStd::make_shared(); + asyncParams->m_completionCallback = completionCallback; + + AZ::Vector2 stepSize = AZ::Vector2(queryResolution); + AzFramework::Terrain::TerrainDataRequestBus::Broadcast( + &AzFramework::Terrain::TerrainDataRequests::ProcessNormalsFromRegionAsync, worldBounds, stepSize, perPositionCallback, sampler, asyncParams); + + completionEvent.acquire(); + } + ); + } + + BENCHMARK_REGISTER_F(TerrainSystemBenchmarkFixture, BM_ProcessNormalsRegionAsync) + ->Args({ 1024, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR) }) + ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR) }) + ->Args({ 1024, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP) }) + ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP) }) + ->Args({ 1024, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Unit(::benchmark::kMillisecond); + BENCHMARK_DEFINE_F(TerrainSystemBenchmarkFixture, BM_ProcessNormalsList)(benchmark::State& state) { // Run the benchmark @@ -498,6 +642,48 @@ namespace UnitTest ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) ->Unit(::benchmark::kMillisecond); + BENCHMARK_DEFINE_F(TerrainSystemBenchmarkFixture, BM_ProcessNormalsListAsync)(benchmark::State& state) + { + // Run the benchmark + RunTerrainApiBenchmark( + state, + [this]([[maybe_unused]] float queryResolution, const AZ::Aabb& worldBounds, + AzFramework::Terrain::TerrainDataRequests::Sampler sampler) + { + AZStd::vector inPositions; + GenerateInputPositionsList(queryResolution, worldBounds, inPositions); + + auto perPositionCallback = [](const AzFramework::SurfaceData::SurfacePoint& surfacePoint, [[maybe_unused]] bool terrainExists) + { + benchmark::DoNotOptimize(surfacePoint.m_normal); + }; + + AZStd::semaphore completionEvent; + auto completionCallback = [&completionEvent](AZStd::shared_ptr) + { + completionEvent.release(); + }; + + AZStd::shared_ptr asyncParams + = AZStd::make_shared(); + asyncParams->m_completionCallback = completionCallback; + AzFramework::Terrain::TerrainDataRequestBus::Broadcast( + &AzFramework::Terrain::TerrainDataRequests::ProcessNormalsFromListAsync, inPositions, perPositionCallback, sampler, asyncParams); + + completionEvent.acquire(); + } + ); + } + + BENCHMARK_REGISTER_F(TerrainSystemBenchmarkFixture, BM_ProcessNormalsListAsync) + ->Args({ 1024, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR) }) + ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR) }) + ->Args({ 1024, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP) }) + ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP) }) + ->Args({ 1024, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Unit(::benchmark::kMillisecond); + BENCHMARK_DEFINE_F(TerrainSystemBenchmarkFixture, BM_GetSurfaceWeights)(benchmark::State& state) { // Run the benchmark @@ -560,6 +746,48 @@ namespace UnitTest ->Args({ 2048, 4, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) ->Unit(::benchmark::kMillisecond); + BENCHMARK_DEFINE_F(TerrainSystemBenchmarkFixture, BM_ProcessSurfaceWeightsRegionAsync)(benchmark::State& state) + { + // Run the benchmark + RunTerrainApiBenchmark( + state, + []([[maybe_unused]] float queryResolution, const AZ::Aabb& worldBounds, + AzFramework::Terrain::TerrainDataRequests::Sampler sampler) + { + auto perPositionCallback = []([[maybe_unused]] size_t xIndex, [[maybe_unused]] size_t yIndex, + const AzFramework::SurfaceData::SurfacePoint& surfacePoint, [[maybe_unused]] bool terrainExists) + { + benchmark::DoNotOptimize(surfacePoint.m_surfaceTags); + }; + + AZStd::semaphore completionEvent; + auto completionCallback = [&completionEvent](AZStd::shared_ptr) + { + completionEvent.release(); + }; + + AZStd::shared_ptr asyncParams + = AZStd::make_shared(); + asyncParams->m_completionCallback = completionCallback; + + AZ::Vector2 stepSize = AZ::Vector2(queryResolution); + AzFramework::Terrain::TerrainDataRequestBus::Broadcast( + &AzFramework::Terrain::TerrainDataRequests::ProcessSurfaceWeightsFromRegionAsync, worldBounds, stepSize, perPositionCallback, sampler, asyncParams); + + completionEvent.acquire(); + } + ); + } + + BENCHMARK_REGISTER_F(TerrainSystemBenchmarkFixture, BM_ProcessSurfaceWeightsRegionAsync) + ->Args({ 1024, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Args({ 1024, 2, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Args({ 2048, 2, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Args({ 1024, 4, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Args({ 2048, 4, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Unit(::benchmark::kMillisecond); + BENCHMARK_DEFINE_F(TerrainSystemBenchmarkFixture, BM_ProcessSurfaceWeightsList)(benchmark::State& state) { // Run the benchmark @@ -591,6 +819,48 @@ namespace UnitTest ->Args({ 2048, 4, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) ->Unit(::benchmark::kMillisecond); + BENCHMARK_DEFINE_F(TerrainSystemBenchmarkFixture, BM_ProcessSurfaceWeightsListAsync)(benchmark::State& state) + { + // Run the benchmark + RunTerrainApiBenchmark( + state, + [this]([[maybe_unused]] float queryResolution, const AZ::Aabb& worldBounds, + AzFramework::Terrain::TerrainDataRequests::Sampler sampler) + { + AZStd::vector inPositions; + GenerateInputPositionsList(queryResolution, worldBounds, inPositions); + + auto perPositionCallback = [](const AzFramework::SurfaceData::SurfacePoint& surfacePoint, [[maybe_unused]] bool terrainExists) + { + benchmark::DoNotOptimize(surfacePoint.m_surfaceTags); + }; + + AZStd::semaphore completionEvent; + auto completionCallback = [&completionEvent](AZStd::shared_ptr) + { + completionEvent.release(); + }; + + AZStd::shared_ptr asyncParams + = AZStd::make_shared(); + asyncParams->m_completionCallback = completionCallback; + AzFramework::Terrain::TerrainDataRequestBus::Broadcast( + &AzFramework::Terrain::TerrainDataRequests::ProcessSurfaceWeightsFromListAsync, inPositions, perPositionCallback, sampler, asyncParams); + + completionEvent.acquire(); + } + ); + } + + BENCHMARK_REGISTER_F(TerrainSystemBenchmarkFixture, BM_ProcessSurfaceWeightsListAsync) + ->Args({ 1024, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Args({ 1024, 2, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Args({ 2048, 2, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Args({ 1024, 4, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Args({ 2048, 4, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Unit(::benchmark::kMillisecond); + BENCHMARK_DEFINE_F(TerrainSystemBenchmarkFixture, BM_GetSurfacePoints)(benchmark::State& state) { // Run the benchmark @@ -653,6 +923,48 @@ namespace UnitTest ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) ->Unit(::benchmark::kMillisecond); + BENCHMARK_DEFINE_F(TerrainSystemBenchmarkFixture, BM_ProcessSurfacePointsRegionAsync)(benchmark::State& state) + { + // Run the benchmark + RunTerrainApiBenchmark( + state, + []([[maybe_unused]] float queryResolution, const AZ::Aabb& worldBounds, + AzFramework::Terrain::TerrainDataRequests::Sampler sampler) + { + auto perPositionCallback = []([[maybe_unused]] size_t xIndex, [[maybe_unused]] size_t yIndex, + const AzFramework::SurfaceData::SurfacePoint& surfacePoint, [[maybe_unused]] bool terrainExists) + { + benchmark::DoNotOptimize(surfacePoint); + }; + + AZStd::semaphore completionEvent; + auto completionCallback = [&completionEvent](AZStd::shared_ptr) + { + completionEvent.release(); + }; + + AZStd::shared_ptr asyncParams + = AZStd::make_shared(); + asyncParams->m_completionCallback = completionCallback; + + AZ::Vector2 stepSize = AZ::Vector2(queryResolution); + AzFramework::Terrain::TerrainDataRequestBus::Broadcast( + &AzFramework::Terrain::TerrainDataRequests::ProcessSurfacePointsFromRegionAsync, worldBounds, stepSize, perPositionCallback, sampler, asyncParams); + + completionEvent.acquire(); + } + ); + } + + BENCHMARK_REGISTER_F(TerrainSystemBenchmarkFixture, BM_ProcessSurfacePointsRegionAsync) + ->Args({ 1024, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR) }) + ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR) }) + ->Args({ 1024, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP) }) + ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP) }) + ->Args({ 1024, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Unit(::benchmark::kMillisecond); + BENCHMARK_DEFINE_F(TerrainSystemBenchmarkFixture, BM_ProcessSurfacePointsList)(benchmark::State& state) { // Run the benchmark @@ -684,6 +996,48 @@ namespace UnitTest ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) ->Unit(::benchmark::kMillisecond); + BENCHMARK_DEFINE_F(TerrainSystemBenchmarkFixture, BM_ProcessSurfacePointsListAsync)(benchmark::State& state) + { + // Run the benchmark + RunTerrainApiBenchmark( + state, + [this]([[maybe_unused]] float queryResolution, const AZ::Aabb& worldBounds, + AzFramework::Terrain::TerrainDataRequests::Sampler sampler) + { + AZStd::vector inPositions; + GenerateInputPositionsList(queryResolution, worldBounds, inPositions); + + auto perPositionCallback = [](const AzFramework::SurfaceData::SurfacePoint& surfacePoint, [[maybe_unused]] bool terrainExists) + { + benchmark::DoNotOptimize(surfacePoint); + }; + + AZStd::semaphore completionEvent; + auto completionCallback = [&completionEvent](AZStd::shared_ptr) + { + completionEvent.release(); + }; + + AZStd::shared_ptr asyncParams + = AZStd::make_shared(); + asyncParams->m_completionCallback = completionCallback; + AzFramework::Terrain::TerrainDataRequestBus::Broadcast( + &AzFramework::Terrain::TerrainDataRequests::ProcessSurfacePointsFromListAsync, inPositions, perPositionCallback, sampler, asyncParams); + + completionEvent.acquire(); + } + ); + } + + BENCHMARK_REGISTER_F(TerrainSystemBenchmarkFixture, BM_ProcessSurfacePointsListAsync) + ->Args({ 1024, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR) }) + ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR) }) + ->Args({ 1024, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP) }) + ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP) }) + ->Args({ 1024, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Unit(::benchmark::kMillisecond); + BENCHMARK_DEFINE_F(TerrainSystemBenchmarkFixture, BM_GetClosestIntersectionRandom)(benchmark::State& state) { // Run the benchmark diff --git a/Gems/Terrain/Code/Tests/TerrainSystemTest.cpp b/Gems/Terrain/Code/Tests/TerrainSystemTest.cpp index 1277e9ed20..05f5072551 100644 --- a/Gems/Terrain/Code/Tests/TerrainSystemTest.cpp +++ b/Gems/Terrain/Code/Tests/TerrainSystemTest.cpp @@ -7,7 +7,9 @@ */ #include +#include #include +#include #include @@ -64,6 +66,7 @@ namespace UnitTest }; AZ::ComponentApplication m_app; + AZStd::unique_ptr m_jobManagerEntity = nullptr; AZStd::unique_ptr> m_boxShapeRequests; AZStd::unique_ptr> m_shapeRequests; @@ -72,12 +75,20 @@ namespace UnitTest void SetUp() override { + AZ::AllocatorInstance::Create(); + AZ::AllocatorInstance::Create(); + AZ::ComponentApplication::Descriptor appDesc; appDesc.m_memoryBlocksByteSize = 20 * 1024 * 1024; appDesc.m_recordingMode = AZ::Debug::AllocationRecords::RECORD_NO_RECORDS; appDesc.m_stackRecordLevels = 20; m_app.Create(appDesc); + + // Create the global job manager. + m_jobManagerEntity = CreateEntity(); + CreateComponent(m_jobManagerEntity.get()); + ActivateEntity(m_jobManagerEntity.get()); } void TearDown() override @@ -86,7 +97,15 @@ namespace UnitTest m_shapeRequests.reset(); m_terrainAreaHeightRequests.reset(); m_terrainAreaSurfaceRequests.reset(); + + // Destroy the global job manager. + m_jobManagerEntity->Deactivate(); + m_jobManagerEntity.reset(); + m_app.Destroy(); + + AZ::AllocatorInstance::Destroy(); + AZ::AllocatorInstance::Destroy(); } AZStd::unique_ptr CreateEntity() @@ -1059,4 +1078,70 @@ namespace UnitTest terrainSystem->ProcessSurfacePointsFromRegion(testRegionBox, stepSize, perPositionCallback, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT); } + + TEST_F(TerrainSystemTest, TerrainProcessAsyncCancellation) + { + // Tests cancellation of the asynchronous terrain API. + + const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(-10.0f, -10.0f, -5.0f, 10.0f, 10.0f, 15.0f); + auto entity = CreateAndActivateMockTerrainLayerSpawner( + spawnerBox, + [](AZ::Vector3& position, bool& terrainExists) + { + // Our generated height will be X + Y. + position.SetZ(position.GetX() + position.GetY()); + terrainExists = true; + }); + + // Create and activate the terrain system with our testing defaults for world bounds, and a query resolution at 1 meter intervals. + auto terrainSystem = CreateAndActivateTerrainSystem(); + + // Generate some input positions. + AZStd::vector inPositions; + for (int i = 0; i < 16; ++i) + { + inPositions.push_back({1.0f, 1.0f, 1.0f}); + } + + // Setup the per position callback so that we can cancel the entire request when it is first invoked. + AZStd::atomic_bool asyncRequestCancelled = false; + AZStd::semaphore asyncRequestStartedEvent; + AZStd::semaphore asyncRequestCancelledEvent; + auto perPositionCallback = [&asyncRequestCancelled, &asyncRequestStartedEvent, &asyncRequestCancelledEvent]([[maybe_unused]] const AzFramework::SurfaceData::SurfacePoint& surfacePoint, [[maybe_unused]] bool terrainExists) + { + if (!asyncRequestCancelled) + { + // Indicate that the async request has started. + asyncRequestStartedEvent.release(); + + // Wait until the async request has been cancelled before allowing it to continue. + asyncRequestCancelledEvent.acquire(); + asyncRequestCancelled = true; + } + }; + + // Setup the completion callback so we can check that the entire request was cancelled. + AZStd::semaphore asyncRequestCompletedEvent; + auto completionCallback = [&asyncRequestCompletedEvent](AZStd::shared_ptr terrainJobContext) + { + EXPECT_TRUE(terrainJobContext->IsCancelled()); + asyncRequestCompletedEvent.release(); + }; + + // Invoke the async request. + AZStd::shared_ptr asyncParams + = AZStd::make_shared(); + asyncParams->m_completionCallback = completionCallback; + AZStd::shared_ptr terrainJobContext + = terrainSystem->ProcessHeightsFromListAsync(inPositions, perPositionCallback, AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR, asyncParams); + + // Wait until the async request has started before cancelling it. + asyncRequestStartedEvent.acquire(); + terrainJobContext->Cancel(); + asyncRequestCancelled = true; + asyncRequestCancelledEvent.release(); + + // Now wait until the async request has completed after being cancelled. + asyncRequestCompletedEvent.acquire(); + } } // namespace UnitTest