Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
```
Expand All @@ -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]
Expand Down Expand Up @@ -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.
Expand Down
10 changes: 5 additions & 5 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` 都由
Expand All @@ -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
```
Expand All @@ -36,7 +36,7 @@ mcpp run

```toml
[dependencies]
mcpplibs.mwrap.recastnavigation = "1.6.0.1"
mcpplibs.mwrap.recastnavigation = "1.6.0.2"

# 各平台得到同样的路线:Detour 的内联数学也在你的编译单元中编译。
[build]
Expand Down Expand Up @@ -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——每一行都是同样的钉住哈希。
Expand Down
6 changes: 5 additions & 1 deletion mcpp.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand All @@ -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
Expand Down
79 changes: 79 additions & 0 deletions patches/0004-stable-sorts.patch
Original file line number Diff line number Diff line change
@@ -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 <stdio.h>
+#include <algorithm>
#include <stdlib.h>
#include <string.h>
#include <float.h>
@@ -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 <math.h>
+#include <algorithm>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
@@ -724,7 +725,7 @@ static void mergeRegionHoles(rcContext* ctx, rcContourRegion& region)
for (int i = 0; i < region.nholes; i++)
findLeftMostVertex(region.holes[i].contour, &region.holes[i].minx, &region.holes[i].minz, &region.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;
Loading
Loading