From b150d681a98344e850324888471df9c9973566fe Mon Sep 17 00:00:00 2001 From: elsid Date: Thu, 20 Feb 2020 15:05:50 -0800 Subject: [PATCH] Update same navmesh tile with limited frequency --- .../detournavigator/navigator.cpp | 38 ++++++++++++ .../detournavigator/asyncnavmeshupdater.cpp | 61 ++++++++++++++++--- .../detournavigator/asyncnavmeshupdater.hpp | 25 +++++++- components/detournavigator/settings.cpp | 1 + components/detournavigator/settings.hpp | 2 + .../reference/modding/settings/navigator.rst | 14 +++++ files/settings-default.cfg | 3 + 7 files changed, 132 insertions(+), 12 deletions(-) diff --git a/apps/openmw_test_suite/detournavigator/navigator.cpp b/apps/openmw_test_suite/detournavigator/navigator.cpp index 71b5315132..2768775089 100644 --- a/apps/openmw_test_suite/detournavigator/navigator.cpp +++ b/apps/openmw_test_suite/detournavigator/navigator.cpp @@ -73,6 +73,7 @@ namespace mSettings.mTrianglesPerChunk = 256; mSettings.mMaxPolys = 4096; mSettings.mMaxTilesNumber = 512; + mSettings.mMinUpdateInterval = std::chrono::milliseconds(50); mNavigator.reset(new NavigatorImpl(mSettings)); } }; @@ -766,4 +767,41 @@ namespace Vec3fEq(215, -215, 1.8782813549041748046875) )) << mPath; } + + TEST_F(DetourNavigatorNavigatorTest, update_changed_multiple_times_object_should_delay_navmesh_change) + { + const std::vector shapes(100, btVector3(64, 64, 64)); + + mNavigator->addAgent(mAgentHalfExtents); + + for (std::size_t i = 0; i < shapes.size(); ++i) + { + const btTransform transform(btMatrix3x3::getIdentity(), btVector3(i * 32, i * 32, i * 32)); + mNavigator->addObject(ObjectId(&shapes[i]), shapes[i], transform); + } + mNavigator->update(mPlayerPosition); + mNavigator->wait(); + + const auto start = std::chrono::steady_clock::now(); + for (std::size_t i = 0; i < shapes.size(); ++i) + { + const btTransform transform(btMatrix3x3::getIdentity(), btVector3(i * 32 + 1, i * 32 + 1, i * 32 + 1)); + mNavigator->updateObject(ObjectId(&shapes[i]), shapes[i], transform); + } + mNavigator->update(mPlayerPosition); + mNavigator->wait(); + + for (std::size_t i = 0; i < shapes.size(); ++i) + { + const btTransform transform(btMatrix3x3::getIdentity(), btVector3(i * 32 + 2, i * 32 + 2, i * 32 + 2)); + mNavigator->updateObject(ObjectId(&shapes[i]), shapes[i], transform); + } + mNavigator->update(mPlayerPosition); + mNavigator->wait(); + + const auto duration = std::chrono::steady_clock::now() - start; + + EXPECT_GT(duration, mSettings.mMinUpdateInterval) + << std::chrono::duration_cast>(duration).count() << " ms"; + } } diff --git a/components/detournavigator/asyncnavmeshupdater.cpp b/components/detournavigator/asyncnavmeshupdater.cpp index 1c07384b8b..0683a43bc1 100644 --- a/components/detournavigator/asyncnavmeshupdater.cpp +++ b/components/detournavigator/asyncnavmeshupdater.cpp @@ -89,6 +89,9 @@ namespace DetourNavigator job.mChangeType = changedTile.second; job.mDistanceToPlayer = getManhattanDistance(changedTile.first, playerTile); job.mDistanceToOrigin = getManhattanDistance(changedTile.first, TilePosition {0, 0}); + job.mProcessTime = job.mChangeType == ChangeType::update + ? mLastUpdates[job.mAgentHalfExtents][job.mChangedTile] + mSettings.get().mMinUpdateInterval + : std::chrono::steady_clock::time_point(); mJobs.push(std::move(job)); } @@ -137,6 +140,8 @@ namespace DetourNavigator if (!processed) repost(std::move(*job)); } + else + cleanupLastUpdates(); } catch (const std::exception& e) { @@ -176,6 +181,7 @@ namespace DetourNavigator const auto locked = navMeshCacheItem->lockConst(); Log(Debug::Debug) << std::fixed << std::setprecision(2) << "Cache updated for agent=(" << job.mAgentHalfExtents << ")" << + " tile=" << job.mChangedTile << " status=" << status << " generation=" << locked->getGeneration() << " revision=" << locked->getNavMeshRevision() << @@ -195,12 +201,15 @@ namespace DetourNavigator while (true) { - const auto hasJob = [&] { return !mJobs.empty() || !threadQueue.mJobs.empty(); }; + const auto hasJob = [&] { + return (!mJobs.empty() && mJobs.top().mProcessTime <= std::chrono::steady_clock::now()) + || !threadQueue.mJobs.empty(); + }; if (!mHasJob.wait_for(lock, std::chrono::milliseconds(10), hasJob)) { mFirstStart.lock()->reset(); - if (getTotalThreadJobsUnsafe() == 0) + if (mJobs.empty() && getTotalThreadJobsUnsafe() == 0) mDone.notify_all(); return boost::none; } @@ -209,29 +218,40 @@ namespace DetourNavigator << threadQueue.mJobs.size() << " thread jobs by thread=" << std::this_thread::get_id(); auto job = threadQueue.mJobs.empty() - ? getJob(mJobs, mPushed) - : getJob(threadQueue.mJobs, threadQueue.mPushed); + ? getJob(mJobs, mPushed, true) + : getJob(threadQueue.mJobs, threadQueue.mPushed, false); - const auto owner = lockTile(job.mAgentHalfExtents, job.mChangedTile); + if (!job) + continue; + + const auto owner = lockTile(job->mAgentHalfExtents, job->mChangedTile); if (owner == threadId) return job; - postThreadJob(std::move(job), mThreadsQueues[owner]); + postThreadJob(std::move(*job), mThreadsQueues[owner]); } } - AsyncNavMeshUpdater::Job AsyncNavMeshUpdater::getJob(Jobs& jobs, Pushed& pushed) + boost::optional AsyncNavMeshUpdater::getJob(Jobs& jobs, Pushed& pushed, bool changeLastUpdate) { - auto job = jobs.top(); + const auto now = std::chrono::steady_clock::now(); + + if (jobs.top().mProcessTime > now) + return {}; + + Job job = std::move(jobs.top()); jobs.pop(); + if (changeLastUpdate && job.mChangeType == ChangeType::update) + mLastUpdates[job.mAgentHalfExtents][job.mChangedTile] = now; + const auto it = pushed.find(job.mAgentHalfExtents); it->second.erase(job.mChangedTile); if (it->second.empty()) pushed.erase(it); - return job; + return {std::move(job)}; } void AsyncNavMeshUpdater::writeDebugFiles(const Job& job, const RecastMesh* recastMesh) const @@ -344,4 +364,27 @@ namespace DetourNavigator return std::accumulate(mThreadsQueues.begin(), mThreadsQueues.end(), std::size_t(0), [] (auto r, const auto& v) { return r + v.second.mJobs.size(); }); } + + void AsyncNavMeshUpdater::cleanupLastUpdates() + { + const auto now = std::chrono::steady_clock::now(); + + const std::lock_guard lock(mMutex); + + for (auto agent = mLastUpdates.begin(); agent != mLastUpdates.end();) + { + for (auto tile = agent->second.begin(); tile != agent->second.end();) + { + if (now - tile->second > mSettings.get().mMinUpdateInterval) + tile = agent->second.erase(tile); + else + ++tile; + } + + if (agent->second.empty()) + agent = mLastUpdates.erase(agent); + else + ++agent; + } + } } diff --git a/components/detournavigator/asyncnavmeshupdater.hpp b/components/detournavigator/asyncnavmeshupdater.hpp index 6a3799969f..4debcd6cd3 100644 --- a/components/detournavigator/asyncnavmeshupdater.hpp +++ b/components/detournavigator/asyncnavmeshupdater.hpp @@ -32,6 +32,21 @@ namespace DetourNavigator update = 3, }; + inline std::ostream& operator <<(std::ostream& stream, ChangeType value) + { + switch (value) { + case ChangeType::remove: + return stream << "ChangeType::remove"; + case ChangeType::mixed: + return stream << "ChangeType::mixed"; + case ChangeType::add: + return stream << "ChangeType::add"; + case ChangeType::update: + return stream << "ChangeType::update"; + } + return stream << "ChangeType::" << static_cast(value); + } + class AsyncNavMeshUpdater { public: @@ -56,10 +71,11 @@ namespace DetourNavigator ChangeType mChangeType; int mDistanceToPlayer; int mDistanceToOrigin; + std::chrono::steady_clock::time_point mProcessTime; - std::tuple getPriority() const + std::tuple getPriority() const { - return std::make_tuple(mTryNumber, mChangeType, mDistanceToPlayer, mDistanceToOrigin); + return std::make_tuple(mProcessTime, mTryNumber, mChangeType, mDistanceToPlayer, mDistanceToOrigin); } friend inline bool operator <(const Job& lhs, const Job& rhs) @@ -93,6 +109,7 @@ namespace DetourNavigator Misc::ScopeGuarded> mFirstStart; NavMeshTilesCache mNavMeshTilesCache; Misc::ScopeGuarded>> mProcessingTiles; + std::map> mLastUpdates; std::map mThreadsQueues; std::vector mThreads; @@ -102,7 +119,7 @@ namespace DetourNavigator boost::optional getNextJob(); - static Job getJob(Jobs& jobs, Pushed& pushed); + boost::optional getJob(Jobs& jobs, Pushed& pushed, bool changeLastUpdate); void postThreadJob(Job&& job, Queue& queue); @@ -117,6 +134,8 @@ namespace DetourNavigator void unlockTile(const osg::Vec3f& agentHalfExtents, const TilePosition& changedTile); inline std::size_t getTotalThreadJobsUnsafe() const; + + void cleanupLastUpdates(); }; } diff --git a/components/detournavigator/settings.cpp b/components/detournavigator/settings.cpp index 735194dbaf..49aec41ff7 100644 --- a/components/detournavigator/settings.cpp +++ b/components/detournavigator/settings.cpp @@ -40,6 +40,7 @@ namespace DetourNavigator navigatorSettings.mNavMeshPathPrefix = ::Settings::Manager::getString("nav mesh path prefix", "Navigator"); navigatorSettings.mEnableRecastMeshFileNameRevision = ::Settings::Manager::getBool("enable recast mesh file name revision", "Navigator"); navigatorSettings.mEnableNavMeshFileNameRevision = ::Settings::Manager::getBool("enable nav mesh file name revision", "Navigator"); + navigatorSettings.mMinUpdateInterval = std::chrono::milliseconds(::Settings::Manager::getInt("min update interval ms", "Navigator")); return navigatorSettings; } diff --git a/components/detournavigator/settings.hpp b/components/detournavigator/settings.hpp index dc0e5dc5a0..939d825a5a 100644 --- a/components/detournavigator/settings.hpp +++ b/components/detournavigator/settings.hpp @@ -4,6 +4,7 @@ #include #include +#include namespace DetourNavigator { @@ -38,6 +39,7 @@ namespace DetourNavigator std::size_t mTrianglesPerChunk = 0; std::string mRecastMeshPathPrefix; std::string mNavMeshPathPrefix; + std::chrono::milliseconds mMinUpdateInterval; }; boost::optional makeSettingsFromSettingsManager(); diff --git a/docs/source/reference/modding/settings/navigator.rst b/docs/source/reference/modding/settings/navigator.rst index c7817b6e81..af40ac750f 100644 --- a/docs/source/reference/modding/settings/navigator.rst +++ b/docs/source/reference/modding/settings/navigator.rst @@ -74,6 +74,20 @@ Game will not eat all memory at once. Memory will be consumed in approximately linear dependency from number of nav mesh updates. But only for new locations or already dropped from cache. +min update interval ms +---------------- + +:Type: integer +:Range: >= 0 +:Default: 250 + +Minimum time duration required to pass before next navmesh update for the same tile in milliseconds. +Only tiles affected where objects are transformed. +Next update for tile with added or removed object will not be delayed. +Visible ingame effect is navmesh update around opening or closing door. +Primary usage is for rotating signs like in Seyda Neen at Arrille's Tradehouse entrance. +Decreasing this value may increase CPU usage by background threads. + Developer's settings ******************** diff --git a/files/settings-default.cfg b/files/settings-default.cfg index 6703e77326..5b587776c3 100644 --- a/files/settings-default.cfg +++ b/files/settings-default.cfg @@ -776,6 +776,9 @@ enable recast mesh render = false # Max number of navmesh tiles (value >= 0) max tiles number = 512 +# Min time duration for the same tile update in milliseconds (value >= 0) +min update interval ms = 250 + [Shadows] # Enable or disable shadows. Bear in mind that this will force OpenMW to use shaders as if "[Shaders]/force shaders" was set to true.