diff --git a/CMakeLists.txt b/CMakeLists.txt index 08bdbce8ae..7e138b1f29 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,6 +34,7 @@ option(BUILD_NIFTEST "Build nif file tester" ON) option(BUILD_DOCS "Build documentation." OFF ) option(BUILD_WITH_CODE_COVERAGE "Enable code coverage with gconv" OFF) option(BUILD_UNITTESTS "Enable Unittests with Google C++ Unittest" OFF) +option(BUILD_BENCHMARKS "Build benchmarks with Google Benchmark" OFF) option(BULLET_USE_DOUBLES "Use double precision for Bullet" ON) set(OpenGL_GL_PREFERENCE LEGACY) # Use LEGACY as we use GL2; GLNVD is for GL3 and up. @@ -549,6 +550,10 @@ if (BUILD_UNITTESTS) add_subdirectory( apps/openmw_test_suite ) endif() +if (BUILD_BENCHMARKS) + add_subdirectory(apps/benchmarks) +endif() + if (WIN32) if (MSVC) if (OPENMW_MP_BUILD) diff --git a/apps/benchmarks/CMakeLists.txt b/apps/benchmarks/CMakeLists.txt new file mode 100644 index 0000000000..b7170003ee --- /dev/null +++ b/apps/benchmarks/CMakeLists.txt @@ -0,0 +1,34 @@ +cmake_minimum_required(VERSION 3.11) + +set(BENCHMARK_ENABLE_TESTING OFF) +set(BENCHMARK_ENABLE_INSTALL OFF) +set(BENCHMARK_ENABLE_GTEST_TESTS OFF) + +set(SAVED_CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") + +string(REPLACE "-Wsuggest-override" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") +string(REPLACE "-Wundef" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") +include(FetchContent) +FetchContent_Declare(benchmark + URL https://github.com/google/benchmark/archive/refs/tags/v1.5.2.zip + URL_HASH MD5=49395b757a7c4656d70f1328d93efd00 + SOURCE_DIR fetched/benchmark +) +FetchContent_MakeAvailableExcludeFromAll(benchmark) + +set(CMAKE_CXX_FLAGS "${SAVED_CMAKE_CXX_FLAGS}") + +openmw_add_executable(openmw_detournavigator_navmeshtilescache_benchmark detournavigator/navmeshtilescache.cpp) +target_compile_options(openmw_detournavigator_navmeshtilescache_benchmark PRIVATE -Wall) +target_compile_features(openmw_detournavigator_navmeshtilescache_benchmark PRIVATE cxx_std_17) +target_link_libraries(openmw_detournavigator_navmeshtilescache_benchmark benchmark::benchmark components) + +if (UNIX AND NOT APPLE) + target_link_libraries(openmw_detournavigator_navmeshtilescache_benchmark ${CMAKE_THREAD_LIBS_INIT}) +endif() + +if (MSVC) + if (CMAKE_CL_64) + set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /bigobj") + endif (CMAKE_CL_64) +endif (MSVC) diff --git a/apps/benchmarks/detournavigator/navmeshtilescache.cpp b/apps/benchmarks/detournavigator/navmeshtilescache.cpp new file mode 100644 index 0000000000..10aa0672a7 --- /dev/null +++ b/apps/benchmarks/detournavigator/navmeshtilescache.cpp @@ -0,0 +1,215 @@ +#include + +#include + +#include +#include +#include + +namespace +{ + using namespace DetourNavigator; + + struct Key + { + osg::Vec3f mAgentHalfExtents; + TilePosition mTilePosition; + RecastMesh mRecastMesh; + std::vector mOffMeshConnections; + }; + + struct Item + { + Key mKey; + NavMeshData mValue; + }; + + template + TilePosition generateTilePosition(int max, Random& random) + { + std::uniform_int_distribution distribution(0, max); + return TilePosition(distribution(random), distribution(random)); + } + + template + osg::Vec3f generateAgentHalfExtents(float min, float max, Random& random) + { + std::uniform_int_distribution distribution(min, max); + return osg::Vec3f(distribution(random), distribution(random), distribution(random)); + } + + template + void generateVertices(OutputIterator out, std::size_t number, Random& random) + { + std::uniform_real_distribution distribution(0.0, 1.0); + std::generate_n(out, 3 * (number - number % 3), [&] { return distribution(random); }); + } + + template + void generateIndices(OutputIterator out, int max, std::size_t number, Random& random) + { + std::uniform_int_distribution distribution(0, max); + std::generate_n(out, number - number % 3, [&] { return distribution(random); }); + } + + AreaType toAreaType(int index) + { + switch (index) + { + case 0: return AreaType_null; + case 1: return AreaType_water; + case 2: return AreaType_door; + case 3: return AreaType_pathgrid; + case 4: return AreaType_ground; + } + return AreaType_null; + } + + template + AreaType generateAreaType(Random& random) + { + std::uniform_int_distribution distribution(0, 4); + return toAreaType(distribution(random));; + } + + template + void generateAreaTypes(OutputIterator out, std::size_t triangles, Random& random) + { + std::generate_n(out, triangles, [&] { return generateAreaType(random); }); + } + + template + void generateWater(OutputIterator out, std::size_t count, Random& random) + { + std::uniform_real_distribution distribution(0.0, 1.0); + std::generate_n(out, count, [&] { + const btVector3 shift(distribution(random), distribution(random), distribution(random)); + return RecastMesh::Water {1, btTransform(btMatrix3x3::getIdentity(), shift)}; + }); + } + + template + void generateOffMeshConnection(OutputIterator out, std::size_t count, Random& random) + { + std::uniform_real_distribution distribution(0.0, 1.0); + std::generate_n(out, count, [&] { + const osg::Vec3f start(distribution(random), distribution(random), distribution(random)); + const osg::Vec3f end(distribution(random), distribution(random), distribution(random)); + return OffMeshConnection {start, end, generateAreaType(random)}; + }); + } + + template + Key generateKey(std::size_t triangles, Random& random) + { + const osg::Vec3f agentHalfExtents = generateAgentHalfExtents(0.5, 1.5, random); + const TilePosition tilePosition = generateTilePosition(10000, random); + const std::size_t generation = std::uniform_int_distribution(0, 100)(random); + const std::size_t revision = std::uniform_int_distribution(0, 10000)(random); + std::vector vertices; + generateVertices(std::back_inserter(vertices), triangles * 1.98, random); + std::vector indices; + generateIndices(std::back_inserter(indices), static_cast(vertices.size() / 3) - 1, vertices.size() * 1.53, random); + std::vector areaTypes; + generateAreaTypes(std::back_inserter(areaTypes), indices.size() / 3, random); + std::vector water; + generateWater(std::back_inserter(water), 2, random); + const std::size_t trianglesPerChunk = 256; + RecastMesh recastMesh(generation, revision, std::move(indices), std::move(vertices), + std::move(areaTypes), std::move(water), trianglesPerChunk); + std::vector offMeshConnections; + generateOffMeshConnection(std::back_inserter(offMeshConnections), 300, random); + return Key {agentHalfExtents, tilePosition, std::move(recastMesh), std::move(offMeshConnections)}; + } + + constexpr std::size_t trianglesPerTile = 310; + + template + void generateKeys(OutputIterator out, std::size_t count, Random& random) + { + std::generate_n(out, count, [&] { return generateKey(trianglesPerTile, random); }); + } + + template + void fillCache(OutputIterator out, Random& random, NavMeshTilesCache& cache) + { + std::size_t size = cache.getStats().mNavMeshCacheSize; + + while (true) + { + Key key = generateKey(trianglesPerTile, random); + cache.set(key.mAgentHalfExtents, key.mTilePosition, key.mRecastMesh, key.mOffMeshConnections, NavMeshData()); + *out++ = std::move(key); + const std::size_t newSize = cache.getStats().mNavMeshCacheSize; + if (size >= newSize) + break; + size = newSize; + } + } + + template + void getFromFilledCache(benchmark::State& state) + { + NavMeshTilesCache cache(maxCacheSize); + std::minstd_rand random; + std::vector keys; + fillCache(std::back_inserter(keys), random, cache); + generateKeys(std::back_inserter(keys), keys.size() * (100 - hitPercentage) / 100, random); + std::size_t n = 0; + + while (state.KeepRunning()) + { + const auto& key = keys[n++ % keys.size()]; + const auto result = cache.get(key.mAgentHalfExtents, key.mTilePosition, key.mRecastMesh, key.mOffMeshConnections); + benchmark::DoNotOptimize(result); + } + } + + constexpr auto getFromFilledCache_1m_100hit = getFromFilledCache<1 * 1024 * 1024, 100>; + constexpr auto getFromFilledCache_4m_100hit = getFromFilledCache<4 * 1024 * 1024, 100>; + constexpr auto getFromFilledCache_16m_100hit = getFromFilledCache<16 * 1024 * 1024, 100>; + constexpr auto getFromFilledCache_64m_100hit = getFromFilledCache<64 * 1024 * 1024, 100>; + constexpr auto getFromFilledCache_1m_70hit = getFromFilledCache<1 * 1024 * 1024, 70>; + constexpr auto getFromFilledCache_4m_70hit = getFromFilledCache<4 * 1024 * 1024, 70>; + constexpr auto getFromFilledCache_16m_70hit = getFromFilledCache<16 * 1024 * 1024, 70>; + constexpr auto getFromFilledCache_64m_70hit = getFromFilledCache<64 * 1024 * 1024, 70>; + + template + void setToBoundedNonEmptyCache(benchmark::State& state) + { + NavMeshTilesCache cache(maxCacheSize); + std::minstd_rand random; + std::vector keys; + fillCache(std::back_inserter(keys), random, cache); + generateKeys(std::back_inserter(keys), keys.size() * 2, random); + std::reverse(keys.begin(), keys.end()); + std::size_t n = 0; + + while (state.KeepRunning()) + { + const auto& key = keys[n++ % keys.size()]; + const auto result = cache.set(key.mAgentHalfExtents, key.mTilePosition, key.mRecastMesh, key.mOffMeshConnections, NavMeshData()); + benchmark::DoNotOptimize(result); + } + } + + constexpr auto setToBoundedNonEmptyCache_1m = setToBoundedNonEmptyCache<1 * 1024 * 1024>; + constexpr auto setToBoundedNonEmptyCache_4m = setToBoundedNonEmptyCache<4 * 1024 * 1024>; + constexpr auto setToBoundedNonEmptyCache_16m = setToBoundedNonEmptyCache<16 * 1024 * 1024>; + constexpr auto setToBoundedNonEmptyCache_64m = setToBoundedNonEmptyCache<64 * 1024 * 1024>; +} // namespace + +BENCHMARK(getFromFilledCache_1m_100hit); +BENCHMARK(getFromFilledCache_4m_100hit); +BENCHMARK(getFromFilledCache_16m_100hit); +BENCHMARK(getFromFilledCache_64m_100hit); +BENCHMARK(getFromFilledCache_1m_70hit); +BENCHMARK(getFromFilledCache_4m_70hit); +BENCHMARK(getFromFilledCache_16m_70hit); +BENCHMARK(getFromFilledCache_64m_70hit); +BENCHMARK(setToBoundedNonEmptyCache_1m); +BENCHMARK(setToBoundedNonEmptyCache_4m); +BENCHMARK(setToBoundedNonEmptyCache_16m); +BENCHMARK(setToBoundedNonEmptyCache_64m); + +BENCHMARK_MAIN(); diff --git a/components/detournavigator/chunkytrimesh.hpp b/components/detournavigator/chunkytrimesh.hpp index 9f6275ec87..7f98f5d22a 100644 --- a/components/detournavigator/chunkytrimesh.hpp +++ b/components/detournavigator/chunkytrimesh.hpp @@ -46,6 +46,9 @@ namespace DetourNavigator ChunkyTriMesh(const std::vector& verts, const std::vector& tris, const std::vector& flags, const std::size_t trisPerChunk); + ChunkyTriMesh(ChunkyTriMesh&&) = default; + ChunkyTriMesh& operator=(ChunkyTriMesh&&) = default; + ChunkyTriMesh(const ChunkyTriMesh&) = delete; ChunkyTriMesh& operator=(const ChunkyTriMesh&) = delete; diff --git a/components/detournavigator/navmeshtilescache.cpp b/components/detournavigator/navmeshtilescache.cpp index b6048da589..608ade4ab7 100644 --- a/components/detournavigator/navmeshtilescache.cpp +++ b/components/detournavigator/navmeshtilescache.cpp @@ -88,27 +88,27 @@ namespace DetourNavigator return Value(*this, iterator); } - void NavMeshTilesCache::reportStats(unsigned int frameNumber, osg::Stats& stats) const + NavMeshTilesCache::Stats NavMeshTilesCache::getStats() const { - std::size_t navMeshCacheSize = 0; - std::size_t usedNavMeshTiles = 0; - std::size_t cachedNavMeshTiles = 0; - std::size_t hitCount = 0; - std::size_t getCount = 0; - + Stats result; { const std::lock_guard lock(mMutex); - navMeshCacheSize = mUsedNavMeshDataSize; - usedNavMeshTiles = mBusyItems.size(); - cachedNavMeshTiles = mFreeItems.size(); - hitCount = mHitCount; - getCount = mGetCount; + result.mNavMeshCacheSize = mUsedNavMeshDataSize; + result.mUsedNavMeshTiles = mBusyItems.size(); + result.mCachedNavMeshTiles = mFreeItems.size(); + result.mHitCount = mHitCount; + result.mGetCount = mGetCount; } + return result; + } - stats.setAttribute(frameNumber, "NavMesh CacheSize", navMeshCacheSize); - stats.setAttribute(frameNumber, "NavMesh UsedTiles", usedNavMeshTiles); - stats.setAttribute(frameNumber, "NavMesh CachedTiles", cachedNavMeshTiles); - stats.setAttribute(frameNumber, "NavMesh CacheHitRate", static_cast(hitCount) / getCount * 100.0); + void NavMeshTilesCache::reportStats(unsigned int frameNumber, osg::Stats& out) const + { + 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 CacheHitRate", static_cast(stats.mHitCount) / stats.mGetCount * 100.0); } void NavMeshTilesCache::removeLeastRecentlyUsed() diff --git a/components/detournavigator/navmeshtilescache.hpp b/components/detournavigator/navmeshtilescache.hpp index 25f4dc1878..d994bbab43 100644 --- a/components/detournavigator/navmeshtilescache.hpp +++ b/components/detournavigator/navmeshtilescache.hpp @@ -188,6 +188,15 @@ namespace DetourNavigator ItemIterator mIterator; }; + struct Stats + { + std::size_t mNavMeshCacheSize; + std::size_t mUsedNavMeshTiles; + std::size_t mCachedNavMeshTiles; + std::size_t mHitCount; + std::size_t mGetCount; + }; + NavMeshTilesCache(const std::size_t maxNavMeshDataSize); Value get(const osg::Vec3f& agentHalfExtents, const TilePosition& changedTile, @@ -197,6 +206,8 @@ namespace DetourNavigator const RecastMesh& recastMesh, const std::vector& offMeshConnections, NavMeshData&& value); + Stats getStats() const; + void reportStats(unsigned int frameNumber, osg::Stats& stats) const; private: