diff --git a/README.md b/README.md index 0e2cda3..ebe7c29 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ The upstream C++ API is unchanged: `rcConfig`, `rcBuildPolyMesh`, `dtNavMesh`, `dtNavMeshQuery`, `dtCrowd`, `dtTileCache` and every other public name are the upstream entities, in the global namespace where upstream declares them. The four libraries (Recast, Detour, DetourCrowd, DetourTileCache) are vendored from -the 1.6.0 release with three patches (`patches/`) and built with no contraction +the 1.6.0 release with four patches (`patches/`) and built with no contraction of `a*b+c` into an FMA and no fast math (`-ffp-contract=off`, `-fno-fast-math`), the same in every profile (`NDEBUG`: upstream's asserts off). The trigonometry does not come from the platform's C library, whose `cosf` and `sinf` differ by @@ -34,7 +34,7 @@ tests, compared on every CI row). ## Getting started ```sh -mcpp new path --template mcpplibs.mwrap.recastnavigation@1.6.0.1 +mcpp new path --template mcpplibs.mwrap.recastnavigation@1.6.0.2 cd path mcpp run ``` @@ -44,7 +44,7 @@ it and prints the way from one side of the wall to the other, around its end. ```toml [dependencies] -mcpplibs.mwrap.recastnavigation = "1.6.0.1" +mcpplibs.mwrap.recastnavigation = "1.6.0.2" # For the same path everywhere: Detour's inline math is compiled in your units too. [build] @@ -86,11 +86,11 @@ The 64-bit polygon references (`DT_POLYREF64`) and the virtual query filter - Not here: DebugUtils (drawing a mesh through a debug renderer's callbacks), the demo (RecastDemo) and upstream's tests. The assert hooks (`rcAssertFailSetCustom`, `dtAssertFailSetCustom`) exist only in a build without `NDEBUG`; the package builds with it in every profile. - Not `compat.recastnavigation`: the index also has the same release as a traditional package; this wrap vendors its own copy because the same bits everywhere need the library itself compiled with `-ffp-contract=off`, no fast math and `NDEBUG` in every profile, which a dependency's build does not take from its consumer. A program links one of the two, not both. - A tile cache needs a compressor: `dtTileCacheCompressor` is an interface the program implements (upstream's demo uses FastLZ, which is not vendored). -- Patched (`patches/`, the files' tarball and patched checksums in `upstream/documents.json`): 0001 makes DetourCrowd's obstacle avoidance and DetourTileCache's oriented boxes call Detour's own `dtMathCosf` and `dtMathSinf` instead of `cosf` and `sinf` (upstream-worthy); 0002 makes the hooks and Recast's two `cosf` calls use `port/` (wrapper-specific). 0003 fixes the adaptive sampling pattern for an even `adaptiveDivs`: a ring's last direction was written one slot past the one counted, so the crowd sampled an uninitialised direction, and past the array's end at 32 divisions and 4 rings (upstream issues #496, #798). An infinity or a NaN gives the one quiet NaN `0x7fc00000` on every platform. +- Patched (`patches/`, the files' tarball and patched checksums in `upstream/documents.json`): 0001 makes DetourCrowd's obstacle avoidance and DetourTileCache's oriented boxes call Detour's own `dtMathCosf` and `dtMathSinf` instead of `cosf` and `sinf` (upstream-worthy); 0002 makes the hooks and Recast's two `cosf` calls use `port/` (wrapper-specific). 0003 fixes the adaptive sampling pattern for an even `adaptiveDivs`: a ring's last direction was written one slot past the one counted, so the crowd sampled an uninitialised direction, and past the array's end at 32 divisions and 4 rings (upstream issues #496, #798). 0004 makes the BV tree and the contours' hole merging sort with `std::stable_sort` instead of `qsort`, which is not stable: glibc, macOS, the Windows UCRT and emscripten order equal keys differently, so a mesh with ties (common on a grid) had different bytes by platform (upstream-worthy). An infinity or a NaN gives the one quiet NaN `0x7fc00000` on every platform. ## Verification -- `mcpp test`: a navmesh built from a floor and a wall, the same bytes twice; a path around the wall, its straight path, a ray along the mesh stopped by the wall; a crowd of four walked through the doorway to the far side; eight agents crossing a circle through each other with four obstacle-avoidance settings (3 to 32 sample directions); the mesh's bytes and both crowds' states pinned; the adapted constants. `tests/trig_test.cpp`: sin, cos and atan2 against the double reference over a dense sweep (under 1 ulp; the results fingerprinted), C's special cases; `RECAST_TRIG_EXHAUSTIVE=1` sweeps every float. +- `mcpp test`: a navmesh built from a floor and a wall, the same bytes twice; a path around the wall, its straight path, a ray along the mesh stopped by the wall; a crowd of four walked through the doorway to the far side; eight agents crossing a circle through each other with four obstacle-avoidance settings (3 to 32 sample directions); a hall tiled in four, its pillars and rooms on a grid so the BV items and contours tie, eight agents walked across it; the meshes' bytes and the crowds' states pinned; the adapted constants. `tests/trig_test.cpp`: sin, cos and atan2 against the double reference over a dense sweep (under 1 ulp; the results fingerprinted), C's special cases; `RECAST_TRIG_EXHAUSTIVE=1` sweeps every float. - `python3 tools/check_exports.py`: the module is exactly `tools/surface.json`; given a clang++ (required on CI's LLVM row) the record is derived again from the headers and compared. - `python3 tools/test_template.py`: `mcpp new` from an installed copy, a strict build, and a run of the generated project. - CI: Linux x86-64 and arm64 (GCC 16, LLVM 23), macOS and Windows (LLVM 23), each in the dev and release profiles, and wasm32 under node — the same pinned hashes on every row. diff --git a/README.zh.md b/README.zh.md index a389755..ccebf29 100644 --- a/README.zh.md +++ b/README.zh.md @@ -11,7 +11,7 @@ 上游的 C++ API 保持原样:`rcConfig`、`rcBuildPolyMesh`、`dtNavMesh`、`dtNavMeshQuery`、`dtCrowd`、`dtTileCache` 以及其他公开名字都是上游的实体,在上游声明它们的全局命名空间里。四个库(Recast、Detour、DetourCrowd、 -DetourTileCache)取自 1.6.0 发布包并打了三个补丁(`patches/`),构建时不把 `a*b+c` 合并为 FMA、不用快速数学 +DetourTileCache)取自 1.6.0 发布包并打了四个补丁(`patches/`),构建时不把 `a*b+c` 合并为 FMA、不用快速数学 (`-ffp-contract=off`、`-fno-fast-math`),且每种配置都相同(`NDEBUG`:不打开上游的断言)。三角函数不取自平台的 C 库(glibc、macOS、Windows UCRT 与 emscripten 的 `cosf`、`sinf` 相差一个 ulp):Detour 的钩子 `dtMathCosf`、 `dtMathSinf`、`dtMathAtan2f`(群体与瓦片缓存现在按上游本意调用它们)和 Recast 求可行走坡度的 `cosf` 都由 @@ -27,7 +27,7 @@ C 库(glibc、macOS、Windows UCRT 与 emscripten 的 `cosf`、`sinf` 相差 ## 快速开始 ```sh -mcpp new path --template mcpplibs.mwrap.recastnavigation@1.6.0.1 +mcpp new path --template mcpplibs.mwrap.recastnavigation@1.6.0.2 cd path mcpp run ``` @@ -36,7 +36,7 @@ mcpp run ```toml [dependencies] -mcpplibs.mwrap.recastnavigation = "1.6.0.1" +mcpplibs.mwrap.recastnavigation = "1.6.0.2" # 各平台得到同样的路线:Detour 的内联数学也在你的编译单元中编译。 [build] @@ -73,11 +73,11 @@ int main() { - 不在此:DebugUtils(通过调试渲染器的回调绘制网格)、演示程序(RecastDemo)与上游测试。断言钩子(`rcAssertFailSetCustom`、`dtAssertFailSetCustom`)只在未定义 `NDEBUG` 的构建中存在;本包在每种配置都定义它。 - 不是 `compat.recastnavigation`:索引里也有同一发布的传统包;本封装自带一份副本,因为“处处同位”要求库本身以 `-ffp-contract=off`、不用快速数学、各配置都定义 `NDEBUG` 编译,而依赖的构建不接受消费者的这些标志。一个程序只链接两者之一。 - 分块缓存需要一个压缩器:`dtTileCacheCompressor` 是由程序实现的接口(上游演示用的 FastLZ 未收录)。 -- 补丁(`patches/`,文件在发布包中与打补丁后的校验和见 `upstream/documents.json`):0001 让 DetourCrowd 的避障与 DetourTileCache 的有向盒改调 Detour 自己的 `dtMathCosf`、`dtMathSinf`,不再直接调 `cosf`、`sinf`(值得回馈上游);0002 让这些钩子和 Recast 的两处 `cosf` 使用 `port/`(本封装专有)。0003 修正 `adaptiveDivs` 为偶数时的自适应采样模式:每圈最后一个方向被写到计数槽位的下一个,群体因而采样一个未初始化的方向,在 32 个方向、4 圈时还写出数组末尾(上游 issue #496、#798)。无穷与 NaN 在每个平台都得到同一个静默 NaN `0x7fc00000`。 +- 补丁(`patches/`,文件在发布包中与打补丁后的校验和见 `upstream/documents.json`):0001 让 DetourCrowd 的避障与 DetourTileCache 的有向盒改调 Detour 自己的 `dtMathCosf`、`dtMathSinf`,不再直接调 `cosf`、`sinf`(值得回馈上游);0002 让这些钩子和 Recast 的两处 `cosf` 使用 `port/`(本封装专有)。0003 修正 `adaptiveDivs` 为偶数时的自适应采样模式:每圈最后一个方向被写到计数槽位的下一个,群体因而采样一个未初始化的方向,在 32 个方向、4 圈时还写出数组末尾(上游 issue #496、#798)。0004 让 BV 树与轮廓的孔洞合并改用 `std::stable_sort` 排序,不再用不稳定的 `qsort`:glibc、macOS、Windows UCRT 与 emscripten 对相等键的排列不同,带并列键的网格(网格状布局很常见)因而在各平台得到不同的字节(值得回馈上游)。无穷与 NaN 在每个平台都得到同一个静默 NaN `0x7fc00000`。 ## 验证 -- `mcpp test`:由地面与墙构建导航网格,两次得到相同字节;绕墙的路径、其直线路径、沿网格被墙挡住的射线;四个智能体组成的群体穿过门洞走到另一侧;八个智能体以四种避障设置(每圈 3 到 32 个采样方向)相互穿过一个圆;钉住网格字节与两个群体状态的哈希;改写的常量。`tests/trig_test.cpp`:sin、cos、atan2 在密集扫描上与 double 参考比较(小于 1 ulp,结果取指纹)以及 C 的特殊情形;`RECAST_TRIG_EXHAUSTIVE=1` 遍历全部 float。 +- `mcpp test`:由地面与墙构建导航网格,两次得到相同字节;绕墙的路径、其直线路径、沿网格被墙挡住的射线;四个智能体组成的群体穿过门洞走到另一侧;八个智能体以四种避障设置(每圈 3 到 32 个采样方向)相互穿过一个圆;分成四块的大厅,柱子与房间排在网格上,使 BV 项与轮廓出现并列键,八个智能体穿过它;钉住网格字节与群体状态的哈希;改写的常量。`tests/trig_test.cpp`:sin、cos、atan2 在密集扫描上与 double 参考比较(小于 1 ulp,结果取指纹)以及 C 的特殊情形;`RECAST_TRIG_EXHAUSTIVE=1` 遍历全部 float。 - `python3 tools/check_exports.py`:模块与 `tools/surface.json` 完全一致;给定 clang++(CI 的 LLVM 行必需)时再次从头文件推导记录并比对。 - `python3 tools/test_template.py`:从安装副本 `mcpp new`、严格构建并运行生成的项目。 - CI:Linux x86-64 与 arm64(GCC 16、LLVM 23),macOS 与 Windows(LLVM 23),各在 dev 与 release 配置下,以及 node 下的 wasm32——每一行都是同样的钉住哈希。 diff --git a/mcpp.toml b/mcpp.toml index cc47084..da2f89a 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -2,7 +2,7 @@ [package] namespace = "mcpplibs.mwrap" name = "recastnavigation" -version = "1.6.0.1" +version = "1.6.0.2" description = "Recast & Detour 1.6.0 C++ modules for mcpp (import mcpplibs.mwrap.recastnavigation;): navigation meshes built from triangles, path queries, crowds of agents, tiled meshes with obstacles" license = "Apache-2.0" authors = ["mcpp-community"] @@ -29,6 +29,10 @@ kind = "lib" # float). sqrtf, floorf, ceilf and fabsf are exact under IEEE everywhere. # patches/0003: with an even adaptiveDivs the crowd sampled an uninitialised # direction (upstream issue #496), which no flag makes the same everywhere. +# patches/0004: the BV tree and the contours' hole merging sort with +# std::stable_sort instead of the C library's qsort, which is not stable and +# orders equal keys differently between glibc, macOS, the Windows UCRT and +# emscripten's musl, so a mesh with ties had different bytes by platform. # NDEBUG: the same library in every profile — upstream declares its assert # hooks (rcAssertFailSetCustom, dtAssertFailSetCustom) and checks only when # NDEBUG is not defined, which would make the module's names differ between a diff --git a/patches/0004-stable-sorts.patch b/patches/0004-stable-sorts.patch new file mode 100644 index 0000000..0476ffc --- /dev/null +++ b/patches/0004-stable-sorts.patch @@ -0,0 +1,79 @@ +Stable sorts: the same BV tree and contours on every platform + +dtCreateNavMeshData's BV tree (subdivide: compareItemX, compareItemY, +compareItemZ) and rcBuildContours' hole merging (mergeRegionHoles: +compareHoles, compareDiagDist) sort with the C library's qsort. qsort is not +stable, and glibc, macOS libSystem, the Windows UCRT and emscripten's musl +put items with equal keys in different orders; on a mesh laid out on a grid +the keys tie often, so the same input gave different BV trees and contour +merges, different mesh bytes, and a crowd on the mesh walked differently by +platform. The five calls use std::stable_sort with the same comparators: a +stable sort's result is determined by the comparator and the input order, so +it is the same everywhere. Where the keys do not tie nothing changes. +Upstream-worthy, not yet upstream. + +Apply from upstream/recastnavigation: git apply -p1 ../../patches/0004-stable-sorts.patch + +diff --git a/Detour/Source/DetourNavMeshBuilder.cpp b/Detour/Source/DetourNavMeshBuilder.cpp +index e93a976..b52f041 100644 +--- a/Detour/Source/DetourNavMeshBuilder.cpp ++++ b/Detour/Source/DetourNavMeshBuilder.cpp +@@ -17,6 +17,7 @@ + // + + #include ++#include + #include + #include + #include +@@ -142,17 +143,17 @@ static void subdivide(BVItem* items, int nitems, int imin, int imax, int& curNod + if (axis == 0) + { + // Sort along x-axis +- qsort(items+imin, inum, sizeof(BVItem), compareItemX); ++ std::stable_sort(items+imin, items+imin+inum, [](const BVItem& a, const BVItem& b) { return compareItemX(&a, &b) < 0; }); + } + else if (axis == 1) + { + // Sort along y-axis +- qsort(items+imin, inum, sizeof(BVItem), compareItemY); ++ std::stable_sort(items+imin, items+imin+inum, [](const BVItem& a, const BVItem& b) { return compareItemY(&a, &b) < 0; }); + } + else + { + // Sort along z-axis +- qsort(items+imin, inum, sizeof(BVItem), compareItemZ); ++ std::stable_sort(items+imin, items+imin+inum, [](const BVItem& a, const BVItem& b) { return compareItemZ(&a, &b) < 0; }); + } + + int isplit = imin+inum/2; +diff --git a/Recast/Source/RecastContour.cpp b/Recast/Source/RecastContour.cpp +index 5508a98..7e0f112 100644 +--- a/Recast/Source/RecastContour.cpp ++++ b/Recast/Source/RecastContour.cpp +@@ -17,6 +17,7 @@ + // + + #include ++#include + #include + #include + #include +@@ -724,7 +725,7 @@ static void mergeRegionHoles(rcContext* ctx, rcContourRegion& region) + for (int i = 0; i < region.nholes; i++) + findLeftMostVertex(region.holes[i].contour, ®ion.holes[i].minx, ®ion.holes[i].minz, ®ion.holes[i].leftmost); + +- qsort(region.holes, region.nholes, sizeof(rcContourHole), compareHoles); ++ std::stable_sort(region.holes, region.holes + region.nholes, [](const rcContourHole& a, const rcContourHole& b) { return compareHoles(&a, &b) < 0; }); + + int maxVerts = region.outline->nverts; + for (int i = 0; i < region.nholes; i++) +@@ -770,7 +771,7 @@ static void mergeRegionHoles(rcContext* ctx, rcContourRegion& region) + } + } + // Sort potential diagonals by distance, we want to make the connection as short as possible. +- qsort(diags, ndiags, sizeof(rcPotentialDiagonal), compareDiagDist); ++ std::stable_sort(&diags[0], &diags[0] + ndiags, [](const rcPotentialDiagonal& a, const rcPotentialDiagonal& b) { return compareDiagDist(&a, &b) < 0; }); + + // Find a diagonal that is not intersecting the outline not the remaining holes. + index = -1; diff --git a/tests/navmesh_test.cpp b/tests/navmesh_test.cpp index 08f07ab..7aad76c 100644 --- a/tests/navmesh_test.cpp +++ b/tests/navmesh_test.cpp @@ -52,7 +52,12 @@ std::uint64_t fnv(std::span values, std::uint64_t hash = 0xcbf29ce4 // Recast's pipeline for one tile, as upstream's solo-mesh sample runs it: the triangles rasterised into a // heightfield, what an agent cannot stand on or pass under filtered out, regions, contours, polygons and the // detail mesh; then Detour's data of it. -std::vector build(const Soup& soup) { +// A tile of a tiled mesh: its cells' bounds (the border added around them) and its place in the grid. +struct Tile { + std::array low, high; + int x = 0, z = 0, size = 0; +}; +std::vector build(const Soup& soup, const Tile* tile = nullptr) { rcConfig config{}; config.cs = 0.2f; config.ch = 0.1f; @@ -69,7 +74,18 @@ std::vector build(const Soup& soup) { config.detailSampleMaxError = 0.1f; const int vertex_count = static_cast(soup.vertices.size() / 3), triangle_count = static_cast(soup.triangles.size() / 3); rcCalcBounds(soup.vertices.data(), vertex_count, config.bmin, config.bmax); - rcCalcGridSize(config.bmin, config.bmax, config.cs, &config.width, &config.height); + if (tile) { + // As upstream's tile sample: the tile's cells and a border of walkableRadius + 3 cells around them. + config.tileSize = tile->size; + config.borderSize = config.walkableRadius + 3; + config.width = config.height = tile->size + 2 * config.borderSize; + config.bmin[0] = tile->low[0] - static_cast(config.borderSize) * config.cs; + config.bmin[2] = tile->low[2] - static_cast(config.borderSize) * config.cs; + config.bmax[0] = tile->high[0] + static_cast(config.borderSize) * config.cs; + config.bmax[2] = tile->high[2] + static_cast(config.borderSize) * config.cs; + } else { + rcCalcGridSize(config.bmin, config.bmax, config.cs, &config.width, &config.height); + } rcContext context(false); rcHeightfield* solid = rcAllocHeightfield(); @@ -85,7 +101,7 @@ std::vector build(const Soup& soup) { rcFreeHeightField(solid); EXPECT_TRUE(rcErodeWalkableArea(&context, config.walkableRadius, *compact)); EXPECT_TRUE(rcBuildDistanceField(&context, *compact)); - EXPECT_TRUE(rcBuildRegions(&context, *compact, 0, config.minRegionArea, config.mergeRegionArea)); + EXPECT_TRUE(rcBuildRegions(&context, *compact, config.borderSize, config.minRegionArea, config.mergeRegionArea)); rcContourSet* contours = rcAllocContourSet(); EXPECT_TRUE(rcBuildContours(&context, *compact, config.maxSimplificationError, config.maxEdgeLen, *contours)); rcPolyMesh* mesh = rcAllocPolyMesh(); @@ -118,6 +134,10 @@ std::vector build(const Soup& soup) { params.cs = config.cs; params.ch = config.ch; params.buildBvTree = true; + if (tile) { + params.tileX = tile->x; + params.tileY = tile->z; + } unsigned char* data = nullptr; int size = 0; EXPECT_TRUE(dtCreateNavMeshData(¶ms, &data, &size)); @@ -271,6 +291,96 @@ TEST(Navmesh, CrowdAvoidance) { dtFreeNavMesh(mesh); } +// A regular hall, tiled: a 24 m floor, a 5 x 5 grid of 1 m pillars every 4 m, and a closed 3 m room in each +// quarter whose floor is cut off from the hall around it. Everything is on the grid, so the BV tree's items +// tie on their bounds and the contours' holes and diagonals tie on their keys: Recast and Detour sorted them +// with the C library's qsort, which is not stable, and glibc, macOS, musl (the Web) and the Windows UCRT put +// equal keys in different orders: the mesh's bytes, and so a crowd on it, differed by platform. Since +// 1.6.0.2 they sort with std::stable_sort (patches/0004). The four tiles' bytes and a crowd's places on the +// tiled mesh are pinned: the same on Linux, macOS, Windows and the Web. +TEST(Navmesh, TiledHallTies) { + Soup soup; + soup.quad({-12, 0, -12}, {-12, 0, 12}, {12, 0, 12}, {12, 0, -12}); + for (int row = -2; row <= 2; ++row) + for (int column = -2; column <= 2; ++column) { + const float x = 4.0f * static_cast(column), z = 4.0f * static_cast(row); + soup.box({x - 0.5f, 0, z - 0.5f}, {x + 0.5f, 2, z + 0.5f}); + } + for (const float x : {-7.0f, 7.0f}) + for (const float z : {-7.0f, 7.0f}) { + soup.box({x - 1.6f, 0, z - 1.6f}, {x + 1.6f, 2, z - 1.4f}); + soup.box({x - 1.6f, 0, z + 1.4f}, {x + 1.6f, 2, z + 1.6f}); + soup.box({x - 1.6f, 0, z - 1.4f}, {x - 1.4f, 2, z + 1.4f}); + soup.box({x + 1.4f, 0, z - 1.4f}, {x + 1.6f, 2, z + 1.4f}); + } + + constexpr int tile_cells = 60; // 12 m in cells of 0.2 + dtNavMeshParams params{}; + params.orig[0] = -12; + params.orig[2] = -12; + params.tileWidth = params.tileHeight = 12; + params.maxTiles = 4; + params.maxPolys = 1 << 12; + dtNavMesh* mesh = dtAllocNavMesh(); + ASSERT_TRUE(dtStatusSucceed(mesh->init(¶ms))); + std::uint64_t tiles_hash = 0xcbf29ce484222325ull; + for (int z = 0; z < 2; ++z) + for (int x = 0; x < 2; ++x) { + const float low_x = -12.0f + 12.0f * static_cast(x), low_z = -12.0f + 12.0f * static_cast(z); + const Tile tile{{low_x, 0, low_z}, {low_x + 12, 2, low_z + 12}, x, z, tile_cells}; + const std::vector bytes = build(soup, &tile); + ASSERT_GT(bytes.size(), 0u); + tiles_hash = fnv(bytes, tiles_hash); + auto* data = static_cast(dtAlloc(static_cast(bytes.size()), DT_ALLOC_PERM)); + std::ranges::copy(bytes, data); + ASSERT_TRUE(dtStatusSucceed(mesh->addTile(data, static_cast(bytes.size()), DT_TILE_FREE_DATA, 0, nullptr))); + } + + // Eight agents from the hall's edges to the opposite edges, between the pillars and around the rooms. + dtNavMeshQuery* query = dtAllocNavMeshQuery(); + ASSERT_TRUE(dtStatusSucceed(query->init(mesh, 2048))); + dtCrowd* crowd = dtAllocCrowd(); + ASSERT_TRUE(crowd->init(8, 0.5f, mesh)); + dtQueryFilter filter; + const float extents[3] = {1, 2, 1}; + std::array, 8> targets{}; + for (int index = 0; index < 8; ++index) { + const float across = -10.0f + 2.5f * static_cast(index % 4) + 0.25f; + const float at[3] = {index < 4 ? across : -11, 0, index < 4 ? -11 : across}; + const float opposite[3] = {index < 4 ? -across : 11, 0, index < 4 ? 11 : -across}; + dtPolyRef start_ref = 0, end_ref = 0; + float start_at[3]; + ASSERT_TRUE(dtStatusSucceed(query->findNearestPoly(at, extents, &filter, &start_ref, start_at))); + ASSERT_TRUE(dtStatusSucceed(query->findNearestPoly(opposite, extents, &filter, &end_ref, targets[index].data()))); + ASSERT_NE(start_ref, 0u); + ASSERT_NE(end_ref, 0u); + dtCrowdAgentParams agent{}; + agent.radius = 0.4f; + agent.height = 2; + agent.maxAcceleration = 8; + agent.maxSpeed = 3; + agent.collisionQueryRange = agent.radius * 12; + agent.pathOptimizationRange = agent.radius * 30; + agent.separationWeight = 1; + agent.updateFlags = DT_CROWD_ANTICIPATE_TURNS | DT_CROWD_OPTIMIZE_VIS | DT_CROWD_OPTIMIZE_TOPO | DT_CROWD_OBSTACLE_AVOIDANCE | DT_CROWD_SEPARATION; + agent.obstacleAvoidanceType = 3; + ASSERT_EQ(crowd->addAgent(start_at, &agent), index); + EXPECT_TRUE(crowd->requestMoveTarget(index, end_ref, targets[index].data())); + } + for (int step = 0; step < 900; ++step) crowd->update(1.0f / 60, nullptr); + std::vector places; + for (int index = 0; index < 8; ++index) { + const dtCrowdAgent* walker = crowd->getAgent(index); + places.insert(places.end(), walker->npos, walker->npos + 3); + } + std::printf("tiled hall: tiles %016llx, crowd %016llx\n", static_cast(tiles_hash), static_cast(fnv(places))); + EXPECT_EQ(tiles_hash, 0x3cfa3346e30b08f1ull) << "the four tiles' bytes"; + EXPECT_EQ(fnv(places), 0x858c19b1c3879c98ull) << "the crowd's places"; + dtFreeCrowd(crowd); + dtFreeNavMeshQuery(query); + dtFreeNavMesh(mesh); +} + TEST(Navmesh, Constants) { static_assert(nav::DT_VERTS_PER_POLYGON == 6); static_assert(nav::RC_WALKABLE_AREA == 63); diff --git a/upstream/documents.json b/upstream/documents.json index dcabb7d..a60e7ac 100644 --- a/upstream/documents.json +++ b/upstream/documents.json @@ -18,6 +18,11 @@ "upstream_commit": null, "upstream_issue": "https://github.com/recastnavigation/recastnavigation/issues/496", "why": "sampleVelocityAdaptive wrote a ring's last direction one slot past the one it counted for an even adaptiveDivs: the crowd sampled an uninitialised direction (different by platform and profile), and at adaptiveDivs 32 with adaptiveRings 4 wrote past the end of its stack array (upstream issues #496, #798, open)." + }, + { + "path": "patches/0004-stable-sorts.patch", + "upstream_commit": null, + "why": "dtCreateNavMeshData's BV tree and rcBuildContours' hole merging sorted with the C library's qsort, which is not stable: glibc, macOS, the Windows UCRT and emscripten's musl order equal keys differently, so a mesh whose BV items or contour holes and diagonals tie (common on a grid) had different bytes, and a crowd on it walked differently, by platform; std::stable_sort with the same comparators gives one order everywhere. Upstream-worthy, not yet upstream." } ], "documents": [ @@ -105,7 +110,11 @@ { "path": "upstream/recastnavigation/Detour/Source/DetourNavMeshBuilder.cpp", "source": "https://github.com/recastnavigation/recastnavigation/blob/v1.6.0/Detour/Source/DetourNavMeshBuilder.cpp", - "sha256": "bbe5adf461cae282291e498db50550eb3a12706e352dd83021165d38950119f3" + "sha256": "eb9626c9099b63241125d202b611f6678e323530c1e1d8704d6be8913b6bf368", + "upstream_sha256": "bbe5adf461cae282291e498db50550eb3a12706e352dd83021165d38950119f3", + "patches": [ + "patches/0004-stable-sorts.patch" + ] }, { "path": "upstream/recastnavigation/Detour/Source/DetourNavMeshQuery.cpp", @@ -248,7 +257,11 @@ { "path": "upstream/recastnavigation/Recast/Source/RecastContour.cpp", "source": "https://github.com/recastnavigation/recastnavigation/blob/v1.6.0/Recast/Source/RecastContour.cpp", - "sha256": "f5ad218147949dccc71885dfd1bc4bf2e303a35571b6da37d3696f8460220a39" + "sha256": "1cfb763a97ba708debe598bc9a34c464f6bea567b34255ba035696f20c5e612c", + "upstream_sha256": "f5ad218147949dccc71885dfd1bc4bf2e303a35571b6da37d3696f8460220a39", + "patches": [ + "patches/0004-stable-sorts.patch" + ] }, { "path": "upstream/recastnavigation/Recast/Source/RecastFilter.cpp", diff --git a/upstream/recastnavigation/Detour/Source/DetourNavMeshBuilder.cpp b/upstream/recastnavigation/Detour/Source/DetourNavMeshBuilder.cpp index e93a976..b52f041 100644 --- a/upstream/recastnavigation/Detour/Source/DetourNavMeshBuilder.cpp +++ b/upstream/recastnavigation/Detour/Source/DetourNavMeshBuilder.cpp @@ -17,6 +17,7 @@ // #include +#include #include #include #include @@ -142,17 +143,17 @@ static void subdivide(BVItem* items, int nitems, int imin, int imax, int& curNod if (axis == 0) { // Sort along x-axis - qsort(items+imin, inum, sizeof(BVItem), compareItemX); + std::stable_sort(items+imin, items+imin+inum, [](const BVItem& a, const BVItem& b) { return compareItemX(&a, &b) < 0; }); } else if (axis == 1) { // Sort along y-axis - qsort(items+imin, inum, sizeof(BVItem), compareItemY); + std::stable_sort(items+imin, items+imin+inum, [](const BVItem& a, const BVItem& b) { return compareItemY(&a, &b) < 0; }); } else { // Sort along z-axis - qsort(items+imin, inum, sizeof(BVItem), compareItemZ); + std::stable_sort(items+imin, items+imin+inum, [](const BVItem& a, const BVItem& b) { return compareItemZ(&a, &b) < 0; }); } int isplit = imin+inum/2; diff --git a/upstream/recastnavigation/Recast/Source/RecastContour.cpp b/upstream/recastnavigation/Recast/Source/RecastContour.cpp index 5508a98..7e0f112 100644 --- a/upstream/recastnavigation/Recast/Source/RecastContour.cpp +++ b/upstream/recastnavigation/Recast/Source/RecastContour.cpp @@ -17,6 +17,7 @@ // #include +#include #include #include #include @@ -724,7 +725,7 @@ static void mergeRegionHoles(rcContext* ctx, rcContourRegion& region) for (int i = 0; i < region.nholes; i++) findLeftMostVertex(region.holes[i].contour, ®ion.holes[i].minx, ®ion.holes[i].minz, ®ion.holes[i].leftmost); - qsort(region.holes, region.nholes, sizeof(rcContourHole), compareHoles); + std::stable_sort(region.holes, region.holes + region.nholes, [](const rcContourHole& a, const rcContourHole& b) { return compareHoles(&a, &b) < 0; }); int maxVerts = region.outline->nverts; for (int i = 0; i < region.nholes; i++) @@ -770,7 +771,7 @@ static void mergeRegionHoles(rcContext* ctx, rcContourRegion& region) } } // Sort potential diagonals by distance, we want to make the connection as short as possible. - qsort(diags, ndiags, sizeof(rcPotentialDiagonal), compareDiagDist); + std::stable_sort(&diags[0], &diags[0] + ndiags, [](const rcPotentialDiagonal& a, const rcPotentialDiagonal& b) { return compareDiagDist(&a, &b) < 0; }); // Find a diagonal that is not intersecting the outline not the remaining holes. index = -1;