diff --git a/apps/openmw_test_suite/CMakeLists.txt b/apps/openmw_test_suite/CMakeLists.txt index 6a93eaa397..19b31f78be 100644 --- a/apps/openmw_test_suite/CMakeLists.txt +++ b/apps/openmw_test_suite/CMakeLists.txt @@ -43,6 +43,7 @@ if (GTEST_FOUND AND GMOCK_FOUND) detournavigator/tilecachedrecastmeshmanager.cpp detournavigator/navmeshdb.cpp detournavigator/serialization.cpp + detournavigator/asyncnavmeshupdater.cpp serialization/binaryreader.cpp serialization/binarywriter.cpp diff --git a/apps/openmw_test_suite/detournavigator/asyncnavmeshupdater.cpp b/apps/openmw_test_suite/detournavigator/asyncnavmeshupdater.cpp new file mode 100644 index 0000000000..d245838858 --- /dev/null +++ b/apps/openmw_test_suite/detournavigator/asyncnavmeshupdater.cpp @@ -0,0 +1,201 @@ +#include "settings.hpp" + +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include + +namespace +{ + using namespace testing; + using namespace DetourNavigator; + using namespace DetourNavigator::Tests; + + void addHeightFieldPlane(TileCachedRecastMeshManager& recastMeshManager) + { + const osg::Vec2i cellPosition(0, 0); + const int cellSize = 8192; + recastMeshManager.addHeightfield(cellPosition, cellSize, HeightfieldPlane {0}); + } + + struct DetourNavigatorAsyncNavMeshUpdaterTest : Test + { + Settings mSettings = makeSettings(); + TileCachedRecastMeshManager mRecastMeshManager {mSettings.mRecast}; + OffMeshConnectionsManager mOffMeshConnectionsManager {mSettings.mRecast}; + const osg::Vec3f mAgentHalfExtents {29, 29, 66}; + const TilePosition mPlayerTile {0, 0}; + const std::string mWorldspace = "sys::default"; + Loading::Listener mListener; + }; + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, for_all_jobs_done_when_empty_wait_should_terminate) + { + AsyncNavMeshUpdater updater {mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr}; + updater.wait(mListener, WaitConditionType::allJobsDone); + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, for_required_tiles_present_when_empty_wait_should_terminate) + { + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr); + updater.wait(mListener, WaitConditionType::requiredTilesPresent); + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, post_should_generate_navmesh_tile) + { + mRecastMeshManager.setWorldspace(mWorldspace); + addHeightFieldPlane(mRecastMeshManager); + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr); + const auto navMeshCacheItem = std::make_shared(makeEmptyNavMesh(mSettings), 1); + const std::map changedTiles {{TilePosition {0, 0}, ChangeType::add}}; + updater.post(mAgentHalfExtents, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(mListener, WaitConditionType::allJobsDone); + EXPECT_NE(navMeshCacheItem->lockConst()->getImpl().getTileRefAt(0, 0, 0), 0); + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, repeated_post_should_lead_to_cache_hit) + { + mRecastMeshManager.setWorldspace(mWorldspace); + addHeightFieldPlane(mRecastMeshManager); + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr); + const auto navMeshCacheItem = std::make_shared(makeEmptyNavMesh(mSettings), 1); + const std::map changedTiles {{TilePosition {0, 0}, ChangeType::add}}; + updater.post(mAgentHalfExtents, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(mListener, WaitConditionType::allJobsDone); + { + const auto stats = updater.getStats(); + ASSERT_EQ(stats.mCache.mGetCount, 1); + ASSERT_EQ(stats.mCache.mHitCount, 0); + } + updater.post(mAgentHalfExtents, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(mListener, WaitConditionType::allJobsDone); + { + const auto stats = updater.getStats(); + EXPECT_EQ(stats.mCache.mGetCount, 2); + EXPECT_EQ(stats.mCache.mHitCount, 1); + } + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, post_for_update_change_type_should_not_update_cache) + { + mRecastMeshManager.setWorldspace(mWorldspace); + addHeightFieldPlane(mRecastMeshManager); + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr); + const auto navMeshCacheItem = std::make_shared(makeEmptyNavMesh(mSettings), 1); + const std::map changedTiles {{TilePosition {0, 0}, ChangeType::update}}; + updater.post(mAgentHalfExtents, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(mListener, WaitConditionType::allJobsDone); + { + const auto stats = updater.getStats(); + ASSERT_EQ(stats.mCache.mGetCount, 1); + ASSERT_EQ(stats.mCache.mHitCount, 0); + } + updater.post(mAgentHalfExtents, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(mListener, WaitConditionType::allJobsDone); + { + const auto stats = updater.getStats(); + EXPECT_EQ(stats.mCache.mGetCount, 2); + EXPECT_EQ(stats.mCache.mHitCount, 0); + } + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, post_should_write_generated_tile_to_db) + { + mRecastMeshManager.setWorldspace(mWorldspace); + addHeightFieldPlane(mRecastMeshManager); + auto db = std::make_unique(":memory:"); + NavMeshDb* const dbPtr = db.get(); + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, std::move(db)); + const auto navMeshCacheItem = std::make_shared(makeEmptyNavMesh(mSettings), 1); + const TilePosition tilePosition {0, 0}; + const std::map changedTiles {{tilePosition, ChangeType::add}}; + updater.post(mAgentHalfExtents, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(mListener, WaitConditionType::allJobsDone); + const auto recastMesh = mRecastMeshManager.getMesh(mWorldspace, tilePosition); + ASSERT_NE(recastMesh, nullptr); + ShapeId nextShapeId {1}; + const std::vector objects = makeDbRefGeometryObjects(recastMesh->getMeshSources(), + [&] (const MeshSource& v) { return resolveMeshSource(*dbPtr, v, nextShapeId); }); + const auto tile = dbPtr->findTile(mWorldspace, tilePosition, serialize(mSettings.mRecast, *recastMesh, objects)); + ASSERT_TRUE(tile.has_value()); + EXPECT_EQ(tile->mTileId, 1); + EXPECT_EQ(tile->mVersion, mSettings.mNavMeshVersion); + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, post_when_writing_to_db_disabled_should_not_write) + { + mRecastMeshManager.setWorldspace(mWorldspace); + addHeightFieldPlane(mRecastMeshManager); + auto db = std::make_unique(":memory:"); + NavMeshDb* const dbPtr = db.get(); + mSettings.mWriteToNavMeshDb = false; + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, std::move(db)); + const auto navMeshCacheItem = std::make_shared(makeEmptyNavMesh(mSettings), 1); + const TilePosition tilePosition {0, 0}; + const std::map changedTiles {{tilePosition, ChangeType::add}}; + updater.post(mAgentHalfExtents, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(mListener, WaitConditionType::allJobsDone); + const auto recastMesh = mRecastMeshManager.getMesh(mWorldspace, tilePosition); + ASSERT_NE(recastMesh, nullptr); + ShapeId nextShapeId {1}; + const std::vector objects = makeDbRefGeometryObjects(recastMesh->getMeshSources(), + [&] (const MeshSource& v) { return resolveMeshSource(*dbPtr, v, nextShapeId); }); + const auto tile = dbPtr->findTile(mWorldspace, tilePosition, serialize(mSettings.mRecast, *recastMesh, objects)); + ASSERT_FALSE(tile.has_value()); + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, post_should_read_from_db_on_cache_miss) + { + mRecastMeshManager.setWorldspace(mWorldspace); + addHeightFieldPlane(mRecastMeshManager); + mSettings.mMaxNavMeshTilesCacheSize = 0; + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, std::make_unique(":memory:")); + const auto navMeshCacheItem = std::make_shared(makeEmptyNavMesh(mSettings), 1); + const std::map changedTiles {{TilePosition {0, 0}, ChangeType::add}}; + updater.post(mAgentHalfExtents, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(mListener, WaitConditionType::allJobsDone); + { + const auto stats = updater.getStats(); + ASSERT_EQ(stats.mCache.mGetCount, 1); + ASSERT_EQ(stats.mCache.mHitCount, 0); + ASSERT_TRUE(stats.mDb.has_value()); + ASSERT_EQ(stats.mDb->mGetTileCount, 1); + ASSERT_EQ(stats.mDbGetTileHits, 0); + } + updater.post(mAgentHalfExtents, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(mListener, WaitConditionType::allJobsDone); + { + const auto stats = updater.getStats(); + EXPECT_EQ(stats.mCache.mGetCount, 2); + EXPECT_EQ(stats.mCache.mHitCount, 0); + ASSERT_TRUE(stats.mDb.has_value()); + EXPECT_EQ(stats.mDb->mGetTileCount, 2); + EXPECT_EQ(stats.mDbGetTileHits, 1); + } + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, on_changing_player_tile_post_should_remove_tiles_out_of_range) + { + mRecastMeshManager.setWorldspace(mWorldspace); + addHeightFieldPlane(mRecastMeshManager); + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr); + const auto navMeshCacheItem = std::make_shared(makeEmptyNavMesh(mSettings), 1); + const std::map changedTilesAdd {{TilePosition {0, 0}, ChangeType::add}}; + updater.post(mAgentHalfExtents, navMeshCacheItem, mPlayerTile, mWorldspace, changedTilesAdd); + updater.wait(mListener, WaitConditionType::allJobsDone); + ASSERT_NE(navMeshCacheItem->lockConst()->getImpl().getTileRefAt(0, 0, 0), 0); + const std::map changedTilesRemove {{TilePosition {0, 0}, ChangeType::remove}}; + const TilePosition playerTile(100, 100); + updater.post(mAgentHalfExtents, navMeshCacheItem, playerTile, mWorldspace, changedTilesRemove); + updater.wait(mListener, WaitConditionType::allJobsDone); + EXPECT_EQ(navMeshCacheItem->lockConst()->getImpl().getTileRefAt(0, 0, 0), 0); + } +} diff --git a/apps/openmw_test_suite/detournavigator/navigator.cpp b/apps/openmw_test_suite/detournavigator/navigator.cpp index 0254d1b813..e7cd0a7db0 100644 --- a/apps/openmw_test_suite/detournavigator/navigator.cpp +++ b/apps/openmw_test_suite/detournavigator/navigator.cpp @@ -1,4 +1,5 @@ #include "operators.hpp" +#include "settings.hpp" #include #include @@ -32,10 +33,11 @@ namespace { using namespace testing; using namespace DetourNavigator; + using namespace DetourNavigator::Tests; struct DetourNavigatorNavigatorTest : Test { - Settings mSettings; + Settings mSettings = makeSettings(); std::unique_ptr mNavigator; const osg::Vec3f mPlayerPosition; const std::string mWorldspace; @@ -62,34 +64,6 @@ namespace , mOut(mPath) , mStepSize(28.333332061767578125f) { - mSettings.mEnableWriteRecastMeshToFile = false; - mSettings.mEnableWriteNavMeshToFile = false; - mSettings.mEnableRecastMeshFileNameRevision = false; - mSettings.mEnableNavMeshFileNameRevision = false; - mSettings.mRecast.mBorderSize = 16; - mSettings.mRecast.mCellHeight = 0.2f; - mSettings.mRecast.mCellSize = 0.2f; - mSettings.mRecast.mDetailSampleDist = 6; - mSettings.mRecast.mDetailSampleMaxError = 1; - mSettings.mRecast.mMaxClimb = 34; - mSettings.mRecast.mMaxSimplificationError = 1.3f; - mSettings.mRecast.mMaxSlope = 49; - mSettings.mRecast.mRecastScaleFactor = 0.017647058823529415f; - mSettings.mRecast.mSwimHeightScale = 0.89999997615814208984375f; - mSettings.mRecast.mMaxEdgeLen = 12; - mSettings.mDetour.mMaxNavMeshQueryNodes = 2048; - mSettings.mRecast.mMaxVertsPerPoly = 6; - mSettings.mRecast.mRegionMergeArea = 400; - mSettings.mRecast.mRegionMinArea = 64; - mSettings.mRecast.mTileSize = 64; - mSettings.mWaitUntilMinDistanceToPlayer = std::numeric_limits::max(); - mSettings.mAsyncNavMeshUpdaterThreads = 1; - mSettings.mMaxNavMeshTilesCacheSize = 1024 * 1024; - mSettings.mDetour.mMaxPolygonPathSize = 1024; - mSettings.mDetour.mMaxSmoothPathSize = 1024; - mSettings.mDetour.mMaxPolys = 4096; - mSettings.mMaxTilesNumber = 512; - mSettings.mMinUpdateInterval = std::chrono::milliseconds(50); mNavigator.reset(new NavigatorImpl(mSettings, std::make_unique(":memory:"))); } }; @@ -1013,7 +987,7 @@ namespace mNavigator->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::allJobsDone); - const Version expectedVersion {1, 1}; + const Version expectedVersion {1, 4}; const auto navMeshes = mNavigator->getNavMeshes(); ASSERT_EQ(navMeshes.size(), 1); diff --git a/apps/openmw_test_suite/detournavigator/navmeshtilescache.cpp b/apps/openmw_test_suite/detournavigator/navmeshtilescache.cpp index ec5576a634..cbd68e0fe1 100644 --- a/apps/openmw_test_suite/detournavigator/navmeshtilescache.cpp +++ b/apps/openmw_test_suite/detournavigator/navmeshtilescache.cpp @@ -80,51 +80,9 @@ namespace return result; } - template - void clone(const T* src, T*& dst, std::size_t size) - { - dst = static_cast(permRecastAlloc(static_cast(size) * sizeof(T))); - std::memcpy(dst, src, size * sizeof(T)); - } - - void clone(const rcPolyMesh& src, rcPolyMesh& dst) - { - dst.nverts = src.nverts; - dst.npolys = src.npolys; - dst.maxpolys = src.maxpolys; - dst.nvp = src.nvp; - rcVcopy(dst.bmin, src.bmin); - rcVcopy(dst.bmax, src.bmax); - dst.cs = src.cs; - dst.ch = src.ch; - dst.borderSize = src.borderSize; - dst.maxEdgeError = src.maxEdgeError; - clone(src.verts, dst.verts, getVertsLength(dst)); - clone(src.polys, dst.polys, getPolysLength(dst)); - clone(src.regs, dst.regs, getRegsLength(dst)); - clone(src.flags, dst.flags, getFlagsLength(dst)); - clone(src.areas, dst.areas, getAreasLength(dst)); - } - - void clone(const rcPolyMeshDetail& src, rcPolyMeshDetail& dst) - { - dst.nmeshes = src.nmeshes; - dst.nverts = src.nverts; - dst.ntris = src.ntris; - clone(src.meshes, dst.meshes, getMeshesLength(dst)); - clone(src.verts, dst.verts, getVertsLength(dst)); - clone(src.tris, dst.tris, getTrisLength(dst)); - } - std::unique_ptr clone(const PreparedNavMeshData& value) { - auto result = std::make_unique(); - result->mUserId = value.mUserId; - result->mCellHeight = value.mCellHeight; - result->mCellSize = value.mCellSize; - clone(value.mPolyMesh, result->mPolyMesh); - clone(value.mPolyMeshDetail, result->mPolyMeshDetail); - return result; + return std::make_unique(value); } Mesh makeMesh() diff --git a/apps/openmw_test_suite/detournavigator/settings.hpp b/apps/openmw_test_suite/detournavigator/settings.hpp new file mode 100644 index 0000000000..dc37dc7550 --- /dev/null +++ b/apps/openmw_test_suite/detournavigator/settings.hpp @@ -0,0 +1,50 @@ +#ifndef OPENMW_TEST_SUITE_DETOURNAVIGATOR_SETTINGS_H +#define OPENMW_TEST_SUITE_DETOURNAVIGATOR_SETTINGS_H + +#include + +#include +#include + +namespace DetourNavigator +{ + namespace Tests + { + inline Settings makeSettings() + { + Settings result; + result.mEnableWriteRecastMeshToFile = false; + result.mEnableWriteNavMeshToFile = false; + result.mEnableRecastMeshFileNameRevision = false; + result.mEnableNavMeshFileNameRevision = false; + result.mRecast.mBorderSize = 16; + result.mRecast.mCellHeight = 0.2f; + result.mRecast.mCellSize = 0.2f; + result.mRecast.mDetailSampleDist = 6; + result.mRecast.mDetailSampleMaxError = 1; + result.mRecast.mMaxClimb = 34; + result.mRecast.mMaxSimplificationError = 1.3f; + result.mRecast.mMaxSlope = 49; + result.mRecast.mRecastScaleFactor = 0.017647058823529415f; + result.mRecast.mSwimHeightScale = 0.89999997615814208984375f; + result.mRecast.mMaxEdgeLen = 12; + result.mDetour.mMaxNavMeshQueryNodes = 2048; + result.mRecast.mMaxVertsPerPoly = 6; + result.mRecast.mRegionMergeArea = 400; + result.mRecast.mRegionMinArea = 64; + result.mRecast.mTileSize = 64; + result.mWaitUntilMinDistanceToPlayer = std::numeric_limits::max(); + result.mAsyncNavMeshUpdaterThreads = 1; + result.mMaxNavMeshTilesCacheSize = 1024 * 1024; + result.mDetour.mMaxPolygonPathSize = 1024; + result.mDetour.mMaxSmoothPathSize = 1024; + result.mDetour.mMaxPolys = 4096; + result.mMaxTilesNumber = 512; + result.mMinUpdateInterval = std::chrono::milliseconds(50); + result.mWriteToNavMeshDb = true; + return result; + } + } +} + +#endif diff --git a/components/detournavigator/asyncnavmeshupdater.cpp b/components/detournavigator/asyncnavmeshupdater.cpp index a808030478..47d984030f 100644 --- a/components/detournavigator/asyncnavmeshupdater.cpp +++ b/components/detournavigator/asyncnavmeshupdater.cpp @@ -3,6 +3,9 @@ #include "makenavmesh.hpp" #include "settings.hpp" #include "version.hpp" +#include "serialization.hpp" +#include "navmeshdbutils.hpp" +#include "dbrefgeometryobject.hpp" #include #include @@ -15,70 +18,102 @@ #include #include #include +#include -namespace +namespace DetourNavigator { - using DetourNavigator::ChangeType; - using DetourNavigator::TilePosition; - using DetourNavigator::UpdateType; - using DetourNavigator::ChangeType; - using DetourNavigator::Job; - using DetourNavigator::JobIt; - - int getManhattanDistance(const TilePosition& lhs, const TilePosition& rhs) + namespace { - return std::abs(lhs.x() - rhs.x()) + std::abs(lhs.y() - rhs.y()); - } + int getManhattanDistance(const TilePosition& lhs, const TilePosition& rhs) + { + return std::abs(lhs.x() - rhs.x()) + std::abs(lhs.y() - rhs.y()); + } - int getMinDistanceTo(const TilePosition& position, int maxDistance, - const std::set>& pushedTiles, - const std::set>& presentTiles) - { - int result = maxDistance; - for (const auto& [halfExtents, tile] : pushedTiles) - if (presentTiles.find(std::tie(halfExtents, tile)) == presentTiles.end()) - result = std::min(result, getManhattanDistance(position, tile)); - return result; - } + int getMinDistanceTo(const TilePosition& position, int maxDistance, + const std::set>& pushedTiles, + const std::set>& presentTiles) + { + int result = maxDistance; + for (const auto& [halfExtents, tile] : pushedTiles) + if (presentTiles.find(std::tie(halfExtents, tile)) == presentTiles.end()) + result = std::min(result, getManhattanDistance(position, tile)); + return result; + } - UpdateType getUpdateType(ChangeType changeType) noexcept - { - if (changeType == ChangeType::update) - return UpdateType::Temporary; - return UpdateType::Persistent; - } + auto getPriority(const Job& job) noexcept + { + return std::make_tuple(-static_cast>(job.mState), job.mProcessTime, + job.mChangeType, job.mTryNumber, job.mDistanceToPlayer, job.mDistanceToOrigin); + } - auto getPriority(const Job& job) noexcept - { - return std::make_tuple(job.mProcessTime, job.mChangeType, job.mTryNumber, job.mDistanceToPlayer, job.mDistanceToOrigin); - } + struct LessByJobPriority + { + bool operator()(JobIt lhs, JobIt rhs) const noexcept + { + return getPriority(*lhs) < getPriority(*rhs); + } + }; - struct LessByJobPriority - { - bool operator()(JobIt lhs, JobIt rhs) const noexcept + void insertPrioritizedJob(JobIt job, std::deque& queue) { - return getPriority(*lhs) < getPriority(*rhs); + const auto it = std::upper_bound(queue.begin(), queue.end(), job, LessByJobPriority {}); + queue.insert(it, job); } - }; - void insertPrioritizedJob(JobIt job, std::deque& queue) - { - const auto it = std::upper_bound(queue.begin(), queue.end(), job, LessByJobPriority {}); - queue.insert(it, job); - } + auto getDbPriority(const Job& job) noexcept + { + return std::make_tuple(static_cast>(job.mState), + job.mChangeType, job.mDistanceToPlayer, job.mDistanceToOrigin); + } - auto getAgentAndTile(const Job& job) noexcept - { - return std::make_tuple(job.mAgentHalfExtents, job.mChangedTile); + struct LessByJobDbPriority + { + bool operator()(JobIt lhs, JobIt rhs) const noexcept + { + return getDbPriority(*lhs) < getDbPriority(*rhs); + } + }; + + void insertPrioritizedDbJob(JobIt job, std::deque& queue) + { + const auto it = std::upper_bound(queue.begin(), queue.end(), job, LessByJobDbPriority {}); + queue.insert(it, job); + } + + auto getAgentAndTile(const Job& job) noexcept + { + return std::make_tuple(job.mAgentHalfExtents, job.mChangedTile); + } + + std::unique_ptr makeDbWorker(AsyncNavMeshUpdater& updater, std::unique_ptr&& db, const Settings& settings) + { + if (db == nullptr) + return nullptr; + return std::make_unique(updater, std::move(db), TileVersion(settings.mNavMeshVersion), settings.mRecast); + } + + void updateJobs(std::deque& jobs, TilePosition playerTile, int maxTiles) + { + for (JobIt job : jobs) + { + job->mDistanceToPlayer = getManhattanDistance(job->mChangedTile, playerTile); + if (!shouldAddTile(job->mChangedTile, playerTile, maxTiles)) + job->mChangeType = ChangeType::remove; + } + } + + std::size_t getNextJobId() + { + static std::atomic_size_t nextJobId {1}; + return nextJobId.fetch_add(1); + } } -} -namespace DetourNavigator -{ Job::Job(const osg::Vec3f& agentHalfExtents, std::weak_ptr navMeshCacheItem, std::string_view worldspace, const TilePosition& changedTile, ChangeType changeType, int distanceToPlayer, std::chrono::steady_clock::time_point processTime) - : mAgentHalfExtents(agentHalfExtents) + : mId(getNextJobId()) + , mAgentHalfExtents(agentHalfExtents) , mNavMeshCacheItem(std::move(navMeshCacheItem)) , mWorldspace(worldspace) , mChangedTile(changedTile) @@ -94,9 +129,9 @@ namespace DetourNavigator : mSettings(settings) , mRecastMeshManager(recastMeshManager) , mOffMeshConnectionsManager(offMeshConnectionsManager) - , mDb(std::move(db)) , mShouldStop() , mNavMeshTilesCache(settings.mMaxNavMeshTilesCacheSize) + , mDbWorker(makeDbWorker(*this, std::move(db), mSettings)) { for (std::size_t i = 0; i < mSettings.get().mAsyncNavMeshUpdaterThreads; ++i) mThreads.emplace_back([&] { process(); }); @@ -105,6 +140,8 @@ namespace DetourNavigator AsyncNavMeshUpdater::~AsyncNavMeshUpdater() { mShouldStop = true; + if (mDbWorker != nullptr) + mDbWorker->stop(); std::unique_lock lock(mMutex); mWaiting.clear(); mHasJob.notify_all(); @@ -128,18 +165,12 @@ namespace DetourNavigator return; const dtNavMeshParams params = *navMeshCacheItem->lockConst()->getImpl().getParams(); + const int maxTiles = std::min(mSettings.get().mMaxTilesNumber, params.maxTiles); - const std::lock_guard lock(mMutex); + std::unique_lock lock(mMutex); if (playerTileChanged) - { - for (JobIt job : mWaiting) - { - job->mDistanceToPlayer = getManhattanDistance(job->mChangedTile, playerTile); - if (!shouldAddTile(job->mChangedTile, playerTile, std::min(mSettings.get().mMaxTilesNumber, params.maxTiles))) - job->mChangeType = ChangeType::remove; - } - } + updateJobs(mWaiting, playerTile, maxTiles); for (const auto& [changedTile, changeType] : changedTiles) { @@ -152,6 +183,9 @@ namespace DetourNavigator const JobIt it = mJobs.emplace(mJobs.end(), agentHalfExtents, navMeshCacheItem, worldspace, changedTile, changeType, getManhattanDistance(changedTile, playerTile), processTime); + Log(Debug::Debug) << "Post job " << it->mId << " for agent=(" << it->mAgentHalfExtents << ")" + << " changedTile=(" << it->mChangedTile << ")"; + if (playerTileChanged) mWaiting.push_back(it); else @@ -166,6 +200,11 @@ namespace DetourNavigator if (!mWaiting.empty()) mHasJob.notify_all(); + + lock.unlock(); + + if (playerTileChanged && mDbWorker != nullptr) + mDbWorker->updateJobs(playerTile, maxTiles); } void AsyncNavMeshUpdater::wait(Loading::Listener& listener, WaitConditionType waitConditionType) @@ -243,25 +282,40 @@ namespace DetourNavigator mProcessingTiles.wait(mProcessed, [] (const auto& v) { return v.empty(); }); } - void AsyncNavMeshUpdater::reportStats(unsigned int frameNumber, osg::Stats& stats) const + AsyncNavMeshUpdater::Stats AsyncNavMeshUpdater::getStats() const { - std::size_t jobs = 0; - std::size_t waiting = 0; - std::size_t pushed = 0; - + Stats result; { const std::lock_guard lock(mMutex); - jobs = mJobs.size(); - waiting = mWaiting.size(); - pushed = mPushed.size(); + result.mJobs = mJobs.size(); + result.mWaiting = mWaiting.size(); + result.mPushed = mPushed.size(); } + result.mProcessing = mProcessingTiles.lockConst()->size(); + if (mDbWorker != nullptr) + result.mDb = mDbWorker->getStats(); + result.mCache = mNavMeshTilesCache.getStats(); + result.mDbGetTileHits = mDbGetTileHits.load(std::memory_order_relaxed); + return result; + } - stats.setAttribute(frameNumber, "NavMesh Jobs", jobs); - stats.setAttribute(frameNumber, "NavMesh Waiting", waiting); - stats.setAttribute(frameNumber, "NavMesh Pushed", pushed); - stats.setAttribute(frameNumber, "NavMesh Processing", mProcessingTiles.lockConst()->size()); + void reportStats(const AsyncNavMeshUpdater::Stats& stats, unsigned int frameNumber, osg::Stats& out) + { + out.setAttribute(frameNumber, "NavMesh Jobs", static_cast(stats.mJobs)); + out.setAttribute(frameNumber, "NavMesh Waiting", static_cast(stats.mWaiting)); + out.setAttribute(frameNumber, "NavMesh Pushed", static_cast(stats.mPushed)); + out.setAttribute(frameNumber, "NavMesh Processing", static_cast(stats.mProcessing)); - mNavMeshTilesCache.reportStats(frameNumber, stats); + if (stats.mDb.has_value()) + { + out.setAttribute(frameNumber, "NavMesh DbJobs", static_cast(stats.mDb->mJobs)); + + if (stats.mDb->mGetTileCount > 0) + out.setAttribute(frameNumber, "NavMesh DbCacheHitRate", static_cast(stats.mDbGetTileHits) + / static_cast(stats.mDb->mGetTileCount) * 100.0); + } + + reportStats(stats.mCache, frameNumber, out); } void AsyncNavMeshUpdater::process() noexcept @@ -274,12 +328,26 @@ namespace DetourNavigator { if (JobIt job = getNextJob(); job != mJobs.end()) { - const auto processed = processJob(*job); - unlockTile(job->mAgentHalfExtents, job->mChangedTile); - if (processed) - removeJob(job); - else - repost(job); + const JobStatus status = processJob(*job); + Log(Debug::Debug) << "Processed job " << job->mId << " with status=" << status; + switch (status) + { + case JobStatus::Done: + unlockTile(job->mAgentHalfExtents, job->mChangedTile); + if (job->mGeneratedNavMeshData != nullptr) + mDbWorker->enqueueJob(job); + else + removeJob(job); + break; + case JobStatus::Fail: + repost(job); + break; + case JobStatus::MemoryCacheMiss: + { + mDbWorker->enqueueJob(job); + break; + } + } } else cleanupLastUpdates(); @@ -292,34 +360,156 @@ namespace DetourNavigator Log(Debug::Debug) << "Stop navigator jobs processing by thread=" << std::this_thread::get_id(); } - bool AsyncNavMeshUpdater::processJob(const Job& job) + JobStatus AsyncNavMeshUpdater::processJob(Job& job) { - Log(Debug::Debug) << "Process job for agent=(" << std::fixed << std::setprecision(2) << job.mAgentHalfExtents << ")" - " by thread=" << std::this_thread::get_id(); - - const auto start = std::chrono::steady_clock::now(); + Log(Debug::Debug) << "Processing job " << job.mId << " by thread=" << std::this_thread::get_id(); const auto navMeshCacheItem = job.mNavMeshCacheItem.lock(); if (!navMeshCacheItem) - return true; + return JobStatus::Done; - const auto recastMesh = mRecastMeshManager.get().getMesh(job.mWorldspace, job.mChangedTile); const auto playerTile = *mPlayerTile.lockConst(); + const auto params = *navMeshCacheItem->lockConst()->getImpl().getParams(); + + if (!shouldAddTile(job.mChangedTile, playerTile, std::min(mSettings.get().mMaxTilesNumber, params.maxTiles))) + { + Log(Debug::Debug) << "Ignore add tile by job " << job.mId << ": too far from player"; + navMeshCacheItem->lock()->removeTile(job.mChangedTile); + return JobStatus::Done; + } + + switch (job.mState) + { + case JobState::Initial: + return processInitialJob(job, *navMeshCacheItem); + case JobState::WithDbResult: + return processJobWithDbResult(job, *navMeshCacheItem); + } + + return JobStatus::Done; + } + + JobStatus AsyncNavMeshUpdater::processInitialJob(Job& job, GuardedNavMeshCacheItem& navMeshCacheItem) + { + Log(Debug::Debug) << "Processing initial job " << job.mId; + + std::shared_ptr recastMesh = mRecastMeshManager.get().getMesh(job.mWorldspace, job.mChangedTile); + + if (recastMesh == nullptr) + { + Log(Debug::Debug) << "Null recast mesh for job " << job.mId; + navMeshCacheItem.lock()->markAsEmpty(job.mChangedTile); + return JobStatus::Done; + } + + if (isEmpty(*recastMesh)) + { + Log(Debug::Debug) << "Empty bounds for job " << job.mId; + navMeshCacheItem.lock()->markAsEmpty(job.mChangedTile); + return JobStatus::Done; + } + + NavMeshTilesCache::Value cachedNavMeshData = mNavMeshTilesCache.get(job.mAgentHalfExtents, job.mChangedTile, *recastMesh); + std::unique_ptr preparedNavMeshData; + const PreparedNavMeshData* preparedNavMeshDataPtr = nullptr; + + if (cachedNavMeshData) + { + preparedNavMeshDataPtr = &cachedNavMeshData.get(); + } + else + { + if (job.mChangeType != ChangeType::update && mDbWorker != nullptr) + { + job.mRecastMesh = std::move(recastMesh); + return JobStatus::MemoryCacheMiss; + } + + preparedNavMeshData = prepareNavMeshTileData(*recastMesh, job.mChangedTile, job.mAgentHalfExtents, mSettings.get().mRecast); + + if (preparedNavMeshData == nullptr) + { + Log(Debug::Debug) << "Null navmesh data for job " << job.mId; + navMeshCacheItem.lock()->markAsEmpty(job.mChangedTile); + return JobStatus::Done; + } + + if (job.mChangeType == ChangeType::update) + { + preparedNavMeshDataPtr = preparedNavMeshData.get(); + } + else + { + cachedNavMeshData = mNavMeshTilesCache.set(job.mAgentHalfExtents, job.mChangedTile, + *recastMesh, std::move(preparedNavMeshData)); + preparedNavMeshDataPtr = cachedNavMeshData ? &cachedNavMeshData.get() : preparedNavMeshData.get(); + } + } + const auto offMeshConnections = mOffMeshConnectionsManager.get().get(job.mChangedTile); - const auto status = updateNavMesh(job.mAgentHalfExtents, recastMesh.get(), job.mWorldspace, job.mChangedTile, - playerTile, offMeshConnections, mSettings, navMeshCacheItem, mNavMeshTilesCache, - getUpdateType(job.mChangeType), mDb, mNextShapeId); + const UpdateNavMeshStatus status = navMeshCacheItem.lock()->updateTile(job.mChangedTile, std::move(cachedNavMeshData), + makeNavMeshTileData(*preparedNavMeshDataPtr, offMeshConnections, job.mAgentHalfExtents, job.mChangedTile, mSettings.get().mRecast)); + + return handleUpdateNavMeshStatus(status, job, navMeshCacheItem, *recastMesh); + } + + JobStatus AsyncNavMeshUpdater::processJobWithDbResult(Job& job, GuardedNavMeshCacheItem& navMeshCacheItem) + { + Log(Debug::Debug) << "Processing job with db result " << job.mId; + + std::unique_ptr preparedNavMeshData; + bool generatedNavMeshData = false; + + if (job.mCachedTileData.has_value() && job.mCachedTileData->mVersion == mSettings.get().mNavMeshVersion) + { + preparedNavMeshData = std::make_unique(); + if (deserialize(job.mCachedTileData->mData, *preparedNavMeshData)) + ++mDbGetTileHits; + else + preparedNavMeshData = nullptr; + } + + if (preparedNavMeshData == nullptr) + { + preparedNavMeshData = prepareNavMeshTileData(*job.mRecastMesh, job.mChangedTile, job.mAgentHalfExtents, mSettings.get().mRecast); + generatedNavMeshData = true; + } - if (recastMesh != nullptr) + if (preparedNavMeshData == nullptr) { - const Version navMeshVersion = navMeshCacheItem->lockConst()->getVersion(); - mRecastMeshManager.get().reportNavMeshChange(job.mChangedTile, - Version {recastMesh->getGeneration(), recastMesh->getRevision()}, - navMeshVersion); + Log(Debug::Debug) << "Null navmesh data for job " << job.mId; + navMeshCacheItem.lock()->markAsEmpty(job.mChangedTile); + return JobStatus::Done; } + auto cachedNavMeshData = mNavMeshTilesCache.set(job.mAgentHalfExtents, job.mChangedTile, *job.mRecastMesh, + std::move(preparedNavMeshData)); + + const auto offMeshConnections = mOffMeshConnectionsManager.get().get(job.mChangedTile); + + const PreparedNavMeshData* preparedNavMeshDataPtr = cachedNavMeshData ? &cachedNavMeshData.get() : preparedNavMeshData.get(); + const UpdateNavMeshStatus status = navMeshCacheItem.lock()->updateTile(job.mChangedTile, std::move(cachedNavMeshData), + makeNavMeshTileData(*preparedNavMeshDataPtr, offMeshConnections, job.mAgentHalfExtents, job.mChangedTile, mSettings.get().mRecast)); + + const JobStatus result = handleUpdateNavMeshStatus(status, job, navMeshCacheItem, *job.mRecastMesh); + + if (result == JobStatus::Done && job.mChangeType != ChangeType::update + && mDbWorker != nullptr && mSettings.get().mWriteToNavMeshDb && generatedNavMeshData) + job.mGeneratedNavMeshData = std::make_unique(*preparedNavMeshDataPtr); + + return result; + } + + JobStatus AsyncNavMeshUpdater::handleUpdateNavMeshStatus(UpdateNavMeshStatus status, + const Job& job, const GuardedNavMeshCacheItem& navMeshCacheItem, const RecastMesh& recastMesh) + { + const Version navMeshVersion = navMeshCacheItem.lockConst()->getVersion(); + mRecastMeshManager.get().reportNavMeshChange(job.mChangedTile, + Version {recastMesh.getGeneration(), recastMesh.getRevision()}, + navMeshVersion); + if (status == UpdateNavMeshStatus::removed || status == UpdateNavMeshStatus::lost) { const std::scoped_lock lock(mMutex); @@ -331,23 +521,9 @@ namespace DetourNavigator mPresentTiles.insert(std::make_tuple(job.mAgentHalfExtents, job.mChangedTile)); } - const auto finish = std::chrono::steady_clock::now(); - - writeDebugFiles(job, recastMesh.get()); - - using FloatMs = std::chrono::duration; + writeDebugFiles(job, &recastMesh); - const Version version = navMeshCacheItem->lockConst()->getVersion(); - Log(Debug::Debug) << std::fixed << std::setprecision(2) << - "Cache updated for agent=(" << job.mAgentHalfExtents << ")" << - " tile=" << job.mChangedTile << - " status=" << status << - " generation=" << version.mGeneration << - " revision=" << version.mRevision << - " time=" << std::chrono::duration_cast(finish - start).count() << "ms" << - " thread=" << std::this_thread::get_id(); - - return isSuccess(status); + return isSuccess(status) ? JobStatus::Done : JobStatus::Fail; } JobIt AsyncNavMeshUpdater::getNextJob() @@ -376,8 +552,12 @@ namespace DetourNavigator mWaiting.pop_front(); + if (job->mRecastMesh != nullptr) + return job; + if (!lockTile(job->mAgentHalfExtents, job->mChangedTile)) { + Log(Debug::Debug) << "Failed to lock tile by " << job->mId; ++job->mTryNumber; insertPrioritizedJob(job, mWaiting); return mJobs.end(); @@ -415,6 +595,8 @@ namespace DetourNavigator void AsyncNavMeshUpdater::repost(JobIt job) { + unlockTile(job->mAgentHalfExtents, job->mChangedTile); + if (mShouldStop || job->mTryNumber > 2) return; @@ -433,17 +615,15 @@ namespace DetourNavigator bool AsyncNavMeshUpdater::lockTile(const osg::Vec3f& agentHalfExtents, const TilePosition& changedTile) { - if (mSettings.get().mAsyncNavMeshUpdaterThreads <= 1) - return true; + Log(Debug::Debug) << "Locking tile agent=(" << agentHalfExtents << ") changedTile=(" << changedTile << ")"; return mProcessingTiles.lock()->emplace(agentHalfExtents, changedTile).second; } void AsyncNavMeshUpdater::unlockTile(const osg::Vec3f& agentHalfExtents, const TilePosition& changedTile) { - if (mSettings.get().mAsyncNavMeshUpdaterThreads <= 1) - return; auto locked = mProcessingTiles.lock(); locked->erase(std::tie(agentHalfExtents, changedTile)); + Log(Debug::Debug) << "Unlocked tile agent=(" << agentHalfExtents << ") changedTile=(" << changedTile << ")"; if (locked->empty()) mProcessed.notify_all(); } @@ -469,9 +649,201 @@ namespace DetourNavigator } } + void AsyncNavMeshUpdater::enqueueJob(JobIt job) + { + Log(Debug::Debug) << "Enqueueing job " << job->mId << " by thread=" << std::this_thread::get_id(); + const std::lock_guard lock(mMutex); + insertPrioritizedJob(job, mWaiting); + mHasJob.notify_all(); + } + void AsyncNavMeshUpdater::removeJob(JobIt job) { + Log(Debug::Debug) << "Removing job " << job->mId << " by thread=" << std::this_thread::get_id(); const std::lock_guard lock(mMutex); mJobs.erase(job); } + + void DbJobQueue::push(JobIt job) + { + const std::lock_guard lock(mMutex); + insertPrioritizedDbJob(job, mJobs); + mHasJob.notify_all(); + } + + std::optional DbJobQueue::pop() + { + std::unique_lock lock(mMutex); + mHasJob.wait(lock, [&] { return mShouldStop || !mJobs.empty(); }); + if (mJobs.empty()) + return std::nullopt; + const JobIt job = mJobs.front(); + mJobs.pop_front(); + return job; + } + + void DbJobQueue::update(TilePosition playerTile, int maxTiles) + { + const std::lock_guard lock(mMutex); + updateJobs(mJobs, playerTile, maxTiles); + std::sort(mJobs.begin(), mJobs.end(), LessByJobDbPriority {}); + } + + void DbJobQueue::stop() + { + const std::lock_guard lock(mMutex); + mJobs.clear(); + mShouldStop = true; + mHasJob.notify_all(); + } + + std::size_t DbJobQueue::size() const + { + const std::lock_guard lock(mMutex); + return mJobs.size(); + } + + DbWorker::DbWorker(AsyncNavMeshUpdater& updater, std::unique_ptr&& db, + TileVersion version, const RecastSettings& recastSettings) + : mUpdater(updater) + , mRecastSettings(recastSettings) + , mDb(std::move(db)) + , mVersion(version) + , mNextTileId(mDb->getMaxTileId() + 1) + , mNextShapeId(mDb->getMaxShapeId() + 1) + , mThread([this] { run(); }) + { + } + + DbWorker::~DbWorker() + { + stop(); + mThread.join(); + } + + void DbWorker::enqueueJob(JobIt job) + { + Log(Debug::Debug) << "Enqueueing db job " << job->mId << " by thread=" << std::this_thread::get_id(); + mQueue.push(job); + } + + DbWorker::Stats DbWorker::getStats() const + { + Stats result; + result.mJobs = mQueue.size(); + result.mGetTileCount = mGetTileCount.load(std::memory_order::memory_order_relaxed); + return result; + } + + void DbWorker::stop() + { + mShouldStop = true; + mQueue.stop(); + } + + void DbWorker::run() noexcept + { + constexpr std::size_t writesPerTransaction = 100; + auto transaction = mDb->startTransaction(); + while (!mShouldStop) + { + try + { + if (const auto job = mQueue.pop()) + processJob(*job); + if (mWrites > writesPerTransaction) + { + mWrites = 0; + transaction.commit(); + transaction = mDb->startTransaction(); + } + } + catch (const std::exception& e) + { + Log(Debug::Error) << "DbWorker exception: " << e.what(); + } + } + transaction.commit(); + } + + void DbWorker::processJob(JobIt job) + { + const auto process = [&] (auto f) + { + try + { + f(job); + } + catch (const std::exception& e) + { + Log(Debug::Error) << "DbWorker exception while processing job " << job->mId << ": " << e.what(); + } + }; + + if (job->mGeneratedNavMeshData != nullptr) + { + process([&] (JobIt job) { processWritingJob(job); }); + mUpdater.removeJob(job); + return; + } + + process([&] (JobIt job) { processReadingJob(job); }); + job->mState = JobState::WithDbResult; + mUpdater.enqueueJob(job); + } + + void DbWorker::processReadingJob(JobIt job) + { + Log(Debug::Debug) << "Processing db read job " << job->mId; + + if (job->mInput.empty()) + { + Log(Debug::Debug) << "Serializing input for job " << job->mId; + const ShapeId shapeId = mNextShapeId; + const std::vector objects = makeDbRefGeometryObjects(job->mRecastMesh->getMeshSources(), + [&] (const MeshSource& v) { return resolveMeshSource(*mDb, v, mNextShapeId); }); + if (shapeId != mNextShapeId) + ++mWrites; + job->mInput = serialize(mRecastSettings, *job->mRecastMesh, objects); + } + + job->mCachedTileData = mDb->getTileData(job->mWorldspace, job->mChangedTile, job->mInput); + ++mGetTileCount; + } + + void DbWorker::processWritingJob(JobIt job) + { + ++mWrites; + + Log(Debug::Debug) << "Processing db write job " << job->mId; + + if (job->mInput.empty()) + { + Log(Debug::Debug) << "Serializing input for job " << job->mId; + const std::vector objects = makeDbRefGeometryObjects(job->mRecastMesh->getMeshSources(), + [&] (const MeshSource& v) { return resolveMeshSource(*mDb, v, mNextShapeId); }); + job->mInput = serialize(mRecastSettings, *job->mRecastMesh, objects); + } + + if (const auto& cachedTileData = job->mCachedTileData) + { + Log(Debug::Debug) << "Update db tile by job " << job->mId; + job->mGeneratedNavMeshData->mUserId = cachedTileData->mTileId; + mDb->updateTile(cachedTileData->mTileId, mVersion, serialize(*job->mGeneratedNavMeshData)); + return; + } + + const auto cached = mDb->findTile(job->mWorldspace, job->mChangedTile, job->mInput); + if (cached.has_value() && cached->mVersion == mVersion) + { + Log(Debug::Debug) << "Ignore existing db tile by job " << job->mId; + return; + } + + job->mGeneratedNavMeshData->mUserId = mNextTileId; + Log(Debug::Debug) << "Insert db tile by job " << job->mId; + mDb->insertTile(mNextTileId, job->mWorldspace, job->mChangedTile, + mVersion, job->mInput, serialize(*job->mGeneratedNavMeshData)); + ++mNextTileId.t; + } } diff --git a/components/detournavigator/asyncnavmeshupdater.hpp b/components/detournavigator/asyncnavmeshupdater.hpp index 44d5be58d5..541d86fe9c 100644 --- a/components/detournavigator/asyncnavmeshupdater.hpp +++ b/components/detournavigator/asyncnavmeshupdater.hpp @@ -21,6 +21,7 @@ #include #include #include +#include class dtNavMesh; @@ -54,8 +55,15 @@ namespace DetourNavigator return stream << "ChangeType::" << static_cast(value); } + enum class JobState + { + Initial, + WithDbResult, + }; + struct Job { + const std::size_t mId; const osg::Vec3f mAgentHalfExtents; const std::weak_ptr mNavMeshCacheItem; const std::string mWorldspace; @@ -65,6 +73,11 @@ namespace DetourNavigator ChangeType mChangeType; int mDistanceToPlayer; const int mDistanceToOrigin; + JobState mState = JobState::Initial; + std::vector mInput; + std::shared_ptr mRecastMesh; + std::optional mCachedTileData; + std::unique_ptr mGeneratedNavMeshData; Job(const osg::Vec3f& agentHalfExtents, std::weak_ptr navMeshCacheItem, std::string_view worldspace, const TilePosition& changedTile, ChangeType changeType, int distanceToPlayer, @@ -73,27 +86,124 @@ namespace DetourNavigator using JobIt = std::list::iterator; + enum class JobStatus + { + Done, + Fail, + MemoryCacheMiss, + }; + + inline std::ostream& operator<<(std::ostream& stream, JobStatus value) + { + switch (value) + { + case JobStatus::Done: return stream << "JobStatus::Done"; + case JobStatus::Fail: return stream << "JobStatus::Fail"; + case JobStatus::MemoryCacheMiss: return stream << "JobStatus::MemoryCacheMiss"; + } + return stream << "JobStatus::" << static_cast>(value); + } + + class DbJobQueue + { + public: + void push(JobIt job); + + std::optional pop(); + + void update(TilePosition playerTile, int maxTiles); + + void stop(); + + std::size_t size() const; + + private: + mutable std::mutex mMutex; + std::condition_variable mHasJob; + std::deque mJobs; + bool mShouldStop = false; + }; + + class AsyncNavMeshUpdater; + + class DbWorker + { + public: + struct Stats + { + std::size_t mJobs = 0; + std::size_t mGetTileCount = 0; + }; + + DbWorker(AsyncNavMeshUpdater& updater, std::unique_ptr&& db, + TileVersion version, const RecastSettings& recastSettings); + + ~DbWorker(); + + Stats getStats() const; + + void enqueueJob(JobIt job); + + void updateJobs(TilePosition playerTile, int maxTiles) { mQueue.update(playerTile, maxTiles); } + + void stop(); + + private: + AsyncNavMeshUpdater& mUpdater; + const RecastSettings& mRecastSettings; + const std::unique_ptr mDb; + const TileVersion mVersion; + TileId mNextTileId; + ShapeId mNextShapeId; + DbJobQueue mQueue; + std::atomic_bool mShouldStop {false}; + std::atomic_size_t mGetTileCount {0}; + std::size_t mWrites = 0; + std::thread mThread; + + inline void run() noexcept; + + inline void processJob(JobIt job); + + inline void processReadingJob(JobIt job); + + inline void processWritingJob(JobIt job); + }; + class AsyncNavMeshUpdater { public: + struct Stats + { + std::size_t mJobs = 0; + std::size_t mWaiting = 0; + std::size_t mPushed = 0; + std::size_t mProcessing = 0; + std::size_t mDbGetTileHits = 0; + std::optional mDb; + NavMeshTilesCache::Stats mCache; + }; + AsyncNavMeshUpdater(const Settings& settings, TileCachedRecastMeshManager& recastMeshManager, OffMeshConnectionsManager& offMeshConnectionsManager, std::unique_ptr&& db); ~AsyncNavMeshUpdater(); - void post(const osg::Vec3f& agentHalfExtents, const SharedNavMeshCacheItem& mNavMeshCacheItem, + void post(const osg::Vec3f& agentHalfExtents, const SharedNavMeshCacheItem& navMeshCacheItem, const TilePosition& playerTile, std::string_view worldspace, const std::map& changedTiles); void wait(Loading::Listener& listener, WaitConditionType waitConditionType); - void reportStats(unsigned int frameNumber, osg::Stats& stats) const; + Stats getStats() const; + + void enqueueJob(JobIt job); + + void removeJob(JobIt job); private: std::reference_wrapper mSettings; std::reference_wrapper mRecastMeshManager; std::reference_wrapper mOffMeshConnectionsManager; - Misc::ScopeGuarded> mDb; - ShapeId mNextShapeId {1}; std::atomic_bool mShouldStop; mutable std::mutex mMutex; std::condition_variable mHasJob; @@ -108,14 +218,21 @@ namespace DetourNavigator std::map, std::chrono::steady_clock::time_point> mLastUpdates; std::set> mPresentTiles; std::vector mThreads; + std::unique_ptr mDbWorker; + std::atomic_size_t mDbGetTileHits {0}; void process() noexcept; - bool processJob(const Job& job); + JobStatus processJob(Job& job); - JobIt getNextJob(); + inline JobStatus processInitialJob(Job& job, GuardedNavMeshCacheItem& navMeshCacheItem); + + inline JobStatus processJobWithDbResult(Job& job, GuardedNavMeshCacheItem& navMeshCacheItem); + + inline JobStatus handleUpdateNavMeshStatus(UpdateNavMeshStatus status, const Job& job, + const GuardedNavMeshCacheItem& navMeshCacheItem, const RecastMesh& recastMesh); - JobIt getJob(std::deque& jobs, bool changeLastUpdate); + JobIt getNextJob(); void postThreadJob(JobIt job, std::deque& queue); @@ -134,9 +251,9 @@ namespace DetourNavigator int waitUntilJobsDoneForNotPresentTiles(const std::size_t initialJobsLeft, std::size_t& maxJobsLeft, Loading::Listener& listener); void waitUntilAllJobsDone(); - - inline void removeJob(JobIt job); }; + + void reportStats(const AsyncNavMeshUpdater::Stats& stats, unsigned int frameNumber, osg::Stats& out); } #endif diff --git a/components/detournavigator/makenavmesh.cpp b/components/detournavigator/makenavmesh.cpp index 8b764b91b4..070a227454 100644 --- a/components/detournavigator/makenavmesh.cpp +++ b/components/detournavigator/makenavmesh.cpp @@ -544,89 +544,4 @@ namespace DetourNavigator return navMesh; } - - UpdateNavMeshStatus updateNavMesh(const osg::Vec3f& agentHalfExtents, const RecastMesh* recastMesh, - const std::string& worldspace, const TilePosition& changedTile, const TilePosition& playerTile, - const std::vector& offMeshConnections, const Settings& settings, - const SharedNavMeshCacheItem& navMeshCacheItem, NavMeshTilesCache& navMeshTilesCache, UpdateType updateType, - Misc::ScopeGuarded>& db, ShapeId& nextShapeId) - { - Log(Debug::Debug) << std::fixed << std::setprecision(2) << - "Update NavMesh with multiple tiles:" << - " agentHeight=" << getHeight(settings.mRecast, agentHalfExtents) << - " agentMaxClimb=" << getMaxClimb(settings.mRecast) << - " agentRadius=" << getRadius(settings.mRecast, agentHalfExtents) << - " changedTile=(" << changedTile << ")" << - " playerTile=(" << playerTile << ")" << - " changedTileDistance=" << getDistance(changedTile, playerTile); - - if (!recastMesh) - { - Log(Debug::Debug) << "Ignore add tile: recastMesh is null"; - return navMeshCacheItem->lock()->removeTile(changedTile); - } - - if (isEmpty(*recastMesh)) - { - Log(Debug::Debug) << "Ignore add tile: recastMesh is empty"; - return navMeshCacheItem->lock()->removeTile(changedTile); - } - - const dtNavMeshParams params = *navMeshCacheItem->lockConst()->getImpl().getParams(); - - if (!shouldAddTile(changedTile, playerTile, std::min(settings.mMaxTilesNumber, params.maxTiles))) - { - Log(Debug::Debug) << "Ignore add tile: too far from player"; - return navMeshCacheItem->lock()->removeTile(changedTile); - } - - auto cachedNavMeshData = navMeshTilesCache.get(agentHalfExtents, changedTile, *recastMesh); - bool cached = static_cast(cachedNavMeshData); - - if (!cachedNavMeshData) - { - std::optional stored; - if (const auto dbLocked = db.lock(); *dbLocked != nullptr) - { - const std::vector objects = makeDbRefGeometryObjects(recastMesh->getMeshSources(), - [&] (const MeshSource& v) { return resolveMeshSource(**dbLocked, v, nextShapeId); }); - stored = (*dbLocked)->getTileData(worldspace, changedTile, serialize(settings.mRecast, *recastMesh, objects)); - } - - std::unique_ptr prepared; - if (stored.has_value() && stored->mVersion == settings.mNavMeshVersion) - { - prepared = std::make_unique(); - if (!deserialize(stored->mData, *prepared)) - prepared = nullptr; - } - - if (prepared == nullptr) - prepared = prepareNavMeshTileData(*recastMesh, changedTile, agentHalfExtents, settings.mRecast); - - if (prepared == nullptr) - { - Log(Debug::Debug) << "Ignore add tile: NavMeshData is null"; - return navMeshCacheItem->lock()->removeTile(changedTile); - } - - if (updateType == UpdateType::Temporary) - return navMeshCacheItem->lock()->updateTile(changedTile, NavMeshTilesCache::Value(), - makeNavMeshTileData(*prepared, offMeshConnections, agentHalfExtents, changedTile, settings.mRecast)); - - cachedNavMeshData = navMeshTilesCache.set(agentHalfExtents, changedTile, *recastMesh, std::move(prepared)); - - if (!cachedNavMeshData) - { - Log(Debug::Debug) << "Navigator cache overflow"; - return navMeshCacheItem->lock()->updateTile(changedTile, NavMeshTilesCache::Value(), - makeNavMeshTileData(*prepared, offMeshConnections, agentHalfExtents, changedTile, settings.mRecast)); - } - } - - const auto updateStatus = navMeshCacheItem->lock()->updateTile(changedTile, std::move(cachedNavMeshData), - makeNavMeshTileData(cachedNavMeshData.get(), offMeshConnections, agentHalfExtents, changedTile, settings.mRecast)); - - return UpdateNavMeshStatusBuilder(updateStatus).cached(cached).getResult(); - } } diff --git a/components/detournavigator/makenavmesh.hpp b/components/detournavigator/makenavmesh.hpp index 40ead79ad6..14919ab134 100644 --- a/components/detournavigator/makenavmesh.hpp +++ b/components/detournavigator/makenavmesh.hpp @@ -58,18 +58,6 @@ namespace DetourNavigator const TilePosition& tile, const RecastSettings& settings); NavMeshPtr makeEmptyNavMesh(const Settings& settings); - - enum class UpdateType - { - Persistent, - Temporary - }; - - UpdateNavMeshStatus updateNavMesh(const osg::Vec3f& agentHalfExtents, const RecastMesh* recastMesh, - const std::string& worldspace, const TilePosition& changedTile, const TilePosition& playerTile, - const std::vector& offMeshConnections, const Settings& settings, - const SharedNavMeshCacheItem& navMeshCacheItem, NavMeshTilesCache& navMeshTilesCache, UpdateType updateType, - Misc::ScopeGuarded>& db, ShapeId& nextShapeId); } #endif diff --git a/components/detournavigator/navmeshcacheitem.cpp b/components/detournavigator/navmeshcacheitem.cpp index decf45de2c..ffcb0a7359 100644 --- a/components/detournavigator/navmeshcacheitem.cpp +++ b/components/detournavigator/navmeshcacheitem.cpp @@ -50,7 +50,8 @@ namespace DetourNavigator { return UpdateNavMeshStatus::ignored; } - const auto removed = ::removeTile(*mImpl, position); + bool removed = ::removeTile(*mImpl, position); + removed = mEmptyTiles.erase(position) > 0 || removed; const auto addStatus = addTile(*mImpl, navMeshData.mValue.get(), navMeshData.mSize); if (dtStatusSucceed(addStatus)) { @@ -82,7 +83,8 @@ namespace DetourNavigator UpdateNavMeshStatus NavMeshCacheItem::removeTile(const TilePosition& position) { - const auto removed = ::removeTile(*mImpl, position); + bool removed = ::removeTile(*mImpl, position); + removed = mEmptyTiles.erase(position) > 0 || removed; if (removed) { mUsedTiles.erase(position); @@ -90,4 +92,21 @@ namespace DetourNavigator } return UpdateNavMeshStatusBuilder().removed(removed).getResult(); } + + UpdateNavMeshStatus NavMeshCacheItem::markAsEmpty(const TilePosition& position) + { + bool removed = ::removeTile(*mImpl, position); + removed = mEmptyTiles.insert(position).second || removed; + if (removed) + { + mUsedTiles.erase(position); + ++mVersion.mRevision; + } + return UpdateNavMeshStatusBuilder().removed(removed).getResult(); + } + + bool NavMeshCacheItem::isEmptyTile(const TilePosition& position) const + { + return mEmptyTiles.find(position) != mEmptyTiles.end(); + } } diff --git a/components/detournavigator/navmeshcacheitem.hpp b/components/detournavigator/navmeshcacheitem.hpp index 5d3b404080..ae4a2de66b 100644 --- a/components/detournavigator/navmeshcacheitem.hpp +++ b/components/detournavigator/navmeshcacheitem.hpp @@ -12,6 +12,7 @@ #include #include +#include struct dtMeshTile; @@ -147,6 +148,10 @@ namespace DetourNavigator UpdateNavMeshStatus removeTile(const TilePosition& position); + UpdateNavMeshStatus markAsEmpty(const TilePosition& position); + + bool isEmptyTile(const TilePosition& position) const; + template void forEachUsedTile(Function&& function) const { @@ -166,6 +171,7 @@ namespace DetourNavigator NavMeshPtr mImpl; Version mVersion; std::map mUsedTiles; + std::set mEmptyTiles; }; using GuardedNavMeshCacheItem = Misc::ScopeGuarded; diff --git a/components/detournavigator/navmeshmanager.cpp b/components/detournavigator/navmeshmanager.cpp index 9c8cb5389c..399af8a6a9 100644 --- a/components/detournavigator/navmeshmanager.cpp +++ b/components/detournavigator/navmeshmanager.cpp @@ -211,7 +211,7 @@ namespace DetourNavigator const auto shouldAdd = shouldAddTile(tile, playerTile, maxTiles); const auto presentInNavMesh = bool(navMesh.getTileAt(tile.x(), tile.y(), 0)); if (shouldAdd && !presentInNavMesh) - tilesToPost.insert(std::make_pair(tile, ChangeType::add)); + tilesToPost.insert(std::make_pair(tile, locked->isEmptyTile(tile) ? ChangeType::update : ChangeType::add)); else if (!shouldAdd && presentInNavMesh) tilesToPost.insert(std::make_pair(tile, ChangeType::mixed)); else @@ -243,7 +243,7 @@ namespace DetourNavigator void NavMeshManager::reportStats(unsigned int frameNumber, osg::Stats& stats) const { - mAsyncNavMeshUpdater.reportStats(frameNumber, stats); + DetourNavigator::reportStats(mAsyncNavMeshUpdater.getStats(), frameNumber, stats); } RecastMeshTiles NavMeshManager::getRecastMeshTiles() const diff --git a/components/detournavigator/navmeshtilescache.cpp b/components/detournavigator/navmeshtilescache.cpp index 3d595f13a8..bbda8a3179 100644 --- a/components/detournavigator/navmeshtilescache.cpp +++ b/components/detournavigator/navmeshtilescache.cpp @@ -79,12 +79,11 @@ namespace DetourNavigator return result; } - void NavMeshTilesCache::reportStats(unsigned int frameNumber, osg::Stats& out) const + void reportStats(const NavMeshTilesCache::Stats& stats, unsigned int frameNumber, osg::Stats& out) { - const Stats stats = getStats(); - out.setAttribute(frameNumber, "NavMesh CacheSize", stats.mNavMeshCacheSize); - out.setAttribute(frameNumber, "NavMesh UsedTiles", stats.mUsedNavMeshTiles); - out.setAttribute(frameNumber, "NavMesh CachedTiles", stats.mCachedNavMeshTiles); + out.setAttribute(frameNumber, "NavMesh CacheSize", static_cast(stats.mNavMeshCacheSize)); + out.setAttribute(frameNumber, "NavMesh UsedTiles", static_cast(stats.mUsedNavMeshTiles)); + out.setAttribute(frameNumber, "NavMesh CachedTiles", static_cast(stats.mCachedNavMeshTiles)); if (stats.mGetCount > 0) out.setAttribute(frameNumber, "NavMesh CacheHitRate", static_cast(stats.mHitCount) / stats.mGetCount * 100.0); } diff --git a/components/detournavigator/navmeshtilescache.hpp b/components/detournavigator/navmeshtilescache.hpp index fdafa0c6d6..e7e0b6c7a8 100644 --- a/components/detournavigator/navmeshtilescache.hpp +++ b/components/detournavigator/navmeshtilescache.hpp @@ -144,8 +144,6 @@ namespace DetourNavigator Stats getStats() const; - void reportStats(unsigned int frameNumber, osg::Stats& stats) const; - private: mutable std::mutex mMutex; std::size_t mMaxNavMeshDataSize; @@ -163,6 +161,8 @@ namespace DetourNavigator void releaseItem(ItemIterator iterator); }; + + void reportStats(const NavMeshTilesCache::Stats& stats, unsigned int frameNumber, osg::Stats& out); } #endif diff --git a/components/detournavigator/preparednavmeshdata.cpp b/components/detournavigator/preparednavmeshdata.cpp index 77c70eade3..a737ae19a5 100644 --- a/components/detournavigator/preparednavmeshdata.cpp +++ b/components/detournavigator/preparednavmeshdata.cpp @@ -4,6 +4,8 @@ #include +#include + namespace { void initPolyMeshDetail(rcPolyMeshDetail& value) noexcept @@ -24,6 +26,15 @@ namespace DetourNavigator initPolyMeshDetail(mPolyMeshDetail); } + PreparedNavMeshData::PreparedNavMeshData(const PreparedNavMeshData& other) + : mUserId(other.mUserId) + , mCellSize(other.mCellSize) + , mCellHeight(other.mCellHeight) + { + copyPolyMesh(other.mPolyMesh, mPolyMesh); + copyPolyMeshDetail(other.mPolyMeshDetail, mPolyMeshDetail); + } + PreparedNavMeshData::~PreparedNavMeshData() noexcept { freePolyMeshDetail(mPolyMeshDetail); diff --git a/components/detournavigator/preparednavmeshdata.hpp b/components/detournavigator/preparednavmeshdata.hpp index 3566cfc71b..b3de7a447f 100644 --- a/components/detournavigator/preparednavmeshdata.hpp +++ b/components/detournavigator/preparednavmeshdata.hpp @@ -18,7 +18,7 @@ namespace DetourNavigator rcPolyMeshDetail mPolyMeshDetail; PreparedNavMeshData() noexcept; - PreparedNavMeshData(const PreparedNavMeshData&) = delete; + PreparedNavMeshData(const PreparedNavMeshData& other); ~PreparedNavMeshData() noexcept; diff --git a/components/detournavigator/recast.cpp b/components/detournavigator/recast.cpp index f3c7768430..c1d14c0aa8 100644 --- a/components/detournavigator/recast.cpp +++ b/components/detournavigator/recast.cpp @@ -46,4 +46,35 @@ namespace DetourNavigator rcFree(value.verts); rcFree(value.tris); } + + void copyPolyMesh(const rcPolyMesh& src, rcPolyMesh& dst) + { + dst.nverts = src.nverts; + dst.npolys = src.npolys; + dst.maxpolys = src.maxpolys; + dst.nvp = src.nvp; + rcVcopy(dst.bmin, src.bmin); + rcVcopy(dst.bmax, src.bmax); + dst.cs = src.cs; + dst.ch = src.ch; + dst.borderSize = src.borderSize; + dst.maxEdgeError = src.maxEdgeError; + permRecastAlloc(dst); + std::memcpy(dst.verts, src.verts, getVertsLength(src) * sizeof(*dst.verts)); + std::memcpy(dst.polys, src.polys, getPolysLength(src) * sizeof(*dst.polys)); + std::memcpy(dst.regs, src.regs, getRegsLength(src) * sizeof(*dst.regs)); + std::memcpy(dst.flags, src.flags, getFlagsLength(src) * sizeof(*dst.flags)); + std::memcpy(dst.areas, src.areas, getAreasLength(src) * sizeof(*dst.areas)); + } + + void copyPolyMeshDetail(const rcPolyMeshDetail& src, rcPolyMeshDetail& dst) + { + dst.nmeshes = src.nmeshes; + dst.nverts = src.nverts; + dst.ntris = src.ntris; + permRecastAlloc(dst); + std::memcpy(dst.meshes, src.meshes, getMeshesLength(src) * sizeof(*dst.meshes)); + std::memcpy(dst.verts, src.verts, getVertsLength(src) * sizeof(*dst.verts)); + std::memcpy(dst.tris, src.tris, getTrisLength(src) * sizeof(*dst.tris)); + } } diff --git a/components/detournavigator/recast.hpp b/components/detournavigator/recast.hpp index 8b9042b661..1811d35772 100644 --- a/components/detournavigator/recast.hpp +++ b/components/detournavigator/recast.hpp @@ -2,6 +2,7 @@ #define OPENMW_COMPONENTS_DETOURNAVIGATOR_RECAST_H #include +#include #include #include @@ -62,6 +63,10 @@ namespace DetourNavigator void permRecastAlloc(rcPolyMeshDetail& value); void freePolyMeshDetail(rcPolyMeshDetail& value) noexcept; + + void copyPolyMesh(const rcPolyMesh& src, rcPolyMesh& dst); + + void copyPolyMeshDetail(const rcPolyMeshDetail& src, rcPolyMeshDetail& dst); } #endif diff --git a/components/detournavigator/settings.cpp b/components/detournavigator/settings.cpp index 68baeb51d8..cc2c685992 100644 --- a/components/detournavigator/settings.cpp +++ b/components/detournavigator/settings.cpp @@ -63,6 +63,7 @@ namespace DetourNavigator result.mMinUpdateInterval = std::chrono::milliseconds(::Settings::Manager::getInt("min update interval ms", "Navigator")); result.mNavMeshVersion = ::Settings::Manager::getInt("nav mesh version", "Navigator"); result.mEnableNavMeshDiskCache = ::Settings::Manager::getBool("enable nav mesh disk cache", "Navigator"); + result.mWriteToNavMeshDb = ::Settings::Manager::getBool("write to navmeshdb", "Navigator"); return result; } diff --git a/components/detournavigator/settings.hpp b/components/detournavigator/settings.hpp index ab8a95c649..e6be8017d5 100644 --- a/components/detournavigator/settings.hpp +++ b/components/detournavigator/settings.hpp @@ -40,6 +40,7 @@ namespace DetourNavigator bool mEnableRecastMeshFileNameRevision = false; bool mEnableNavMeshFileNameRevision = false; bool mEnableNavMeshDiskCache = false; + bool mWriteToNavMeshDb = false; RecastSettings mRecast; DetourSettings mDetour; int mWaitUntilMinDistanceToPlayer = 0; diff --git a/components/resource/stats.cpp b/components/resource/stats.cpp index b3705f69cc..d97ddd1d6f 100644 --- a/components/resource/stats.cpp +++ b/components/resource/stats.cpp @@ -393,6 +393,8 @@ void StatsHandler::setUpScene(osgViewer::ViewerBase *viewer) "NavMesh Waiting", "NavMesh Pushed", "NavMesh Processing", + "NavMesh DbJobs", + "NavMesh DbCacheHitRate", "NavMesh CacheSize", "NavMesh UsedTiles", "NavMesh CachedTiles", diff --git a/docs/source/reference/modding/settings/navigator.rst b/docs/source/reference/modding/settings/navigator.rst index 15059356ff..6d00c770bc 100644 --- a/docs/source/reference/modding/settings/navigator.rst +++ b/docs/source/reference/modding/settings/navigator.rst @@ -65,6 +65,15 @@ If true navmesh cache stored on disk will be used in addition to memory cache. If navmesh tile is not present in memory cache, it will be looked up in the disk cache. If it's not found there it will be generated. +write to navmeshdb +------------------ + +:Type: boolean +:Range: True/False +:Default: False + +If true generated navmesh tiles will be stored into disk cache while game is running. + Advanced settings ***************** diff --git a/files/settings-default.cfg b/files/settings-default.cfg index 1752b4f5a0..57faaba11d 100644 --- a/files/settings-default.cfg +++ b/files/settings-default.cfg @@ -937,6 +937,9 @@ nav mesh version = 1 # Use navigation mesh cache stored on disk (true, false) enable nav mesh disk cache = true +# Cache navigation mesh tiles to disk (true, false) +write to navmeshdb = false + [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.