From fc18ed81940ea85af94b319c314e8220e467fe75 Mon Sep 17 00:00:00 2001 From: Graham Pentheny Date: Sun, 31 Dec 2023 14:55:41 -0500 Subject: [PATCH] Some unit tests for heightfield filtering functions (#682) This adds some unit tests for the functions in RecastFilter.cpp, and updates docs around these functions. This also splits up the Tests_Recast.cpp file into a few smaller, more focused files. --- Recast/Include/Recast.h | 22 +- Recast/Source/RecastFilter.cpp | 32 +- Tests/Recast/Bench_rcVector.cpp | 168 ++++++++++ Tests/Recast/Tests_Alloc.cpp | 260 ++++++++++++++++ Tests/Recast/Tests_Recast.cpp | 468 ++-------------------------- Tests/Recast/Tests_RecastFilter.cpp | 389 +++++++++++++++++++++++ 6 files changed, 872 insertions(+), 467 deletions(-) create mode 100644 Tests/Recast/Bench_rcVector.cpp create mode 100644 Tests/Recast/Tests_Alloc.cpp create mode 100644 Tests/Recast/Tests_RecastFilter.cpp diff --git a/Recast/Include/Recast.h b/Recast/Include/Recast.h index 2a5e075c7..2104aa5eb 100644 --- a/Recast/Include/Recast.h +++ b/Recast/Include/Recast.h @@ -321,6 +321,8 @@ struct rcHeightfield float cs; ///< The size of each cell. (On the xz-plane.) float ch; ///< The height of each cell. (The minimum increment along the y-axis.) rcSpan** spans; ///< Heightfield of spans (width*height). + + // memory pool for rcSpan instances. rcSpanPool* pools; ///< Linked list of span pools. rcSpan* freelist; ///< The next free span. @@ -998,23 +1000,21 @@ bool rcRasterizeTriangles(rcContext* context, const float* verts, const unsigned char* triAreaIDs, int numTris, rcHeightfield& heightfield, int flagMergeThreshold = 1); -/// Marks non-walkable spans as walkable if their maximum is within @p walkableClimb of a walkable neighbor. +/// Marks non-walkable spans as walkable if their maximum is within @p walkableClimb of the span below them. /// -/// Allows the formation of walkable regions that will flow over low lying -/// objects such as curbs, and up structures such as stairways. +/// This removes small obstacles that the agent would be able to walk over such as curbs, and also allows agents to move up structures such as stairs. /// -/// Two neighboring spans are walkable if: rcAbs(currentSpan.smax - neighborSpan.smax) < walkableClimb +/// Obstacle spans are marked walkable if: obstacleSpan.smax - walkableSpan.smax < walkableClimb /// -/// @warning Will override the effect of #rcFilterLedgeSpans. So if both filters are used, call -/// #rcFilterLedgeSpans after calling this filter. +/// @warning Will override the effect of #rcFilterLedgeSpans. If both filters are used, call #rcFilterLedgeSpans only after applying this filter. /// /// @see rcHeightfield, rcConfig /// /// @ingroup recast -/// @param[in,out] context The build context to use during the operation. +/// @param[in,out] context The build context to use during the operation. /// @param[in] walkableClimb Maximum ledge height that is considered to still be traversable. /// [Limit: >=0] [Units: vx] -/// @param[in,out] heightfield A fully built heightfield. (All spans have been added.) +/// @param[in,out] heightfield A fully built heightfield. (All spans have been added.) void rcFilterLowHangingWalkableObstacles(rcContext* context, int walkableClimb, rcHeightfield& heightfield); /// Marks spans that are ledges as not-walkable. @@ -1037,10 +1037,12 @@ void rcFilterLowHangingWalkableObstacles(rcContext* context, int walkableClimb, /// @param[in,out] heightfield A fully built heightfield. (All spans have been added.) void rcFilterLedgeSpans(rcContext* context, int walkableHeight, int walkableClimb, rcHeightfield& heightfield); -/// Marks walkable spans as not walkable if the clearance above the span is less than the specified height. +/// Marks walkable spans as not walkable if the clearance above the span is less than the specified walkableHeight. /// /// For this filter, the clearance above the span is the distance from the span's -/// maximum to the next higher span's minimum. (Same grid column.) +/// maximum to the minimum of the next higher span in the same column. +/// If there is no higher span in the column, the clearance is computed as the +/// distance from the top of the span to the maximum heightfield height. /// /// @see rcHeightfield, rcConfig /// @ingroup recast diff --git a/Recast/Source/RecastFilter.cpp b/Recast/Source/RecastFilter.cpp index 8f3414b64..1ecf85883 100644 --- a/Recast/Source/RecastFilter.cpp +++ b/Recast/Source/RecastFilter.cpp @@ -21,6 +21,11 @@ #include +namespace +{ + const int MAX_HEIGHTFIELD_HEIGHT = 0xffff; // TODO (graham): Move this to a more visible constant and update usages. +} + void rcFilterLowHangingWalkableObstacles(rcContext* context, const int walkableClimb, rcHeightfield& heightfield) { rcAssert(context); @@ -59,8 +64,7 @@ void rcFilterLowHangingWalkableObstacles(rcContext* context, const int walkableC } } -void rcFilterLedgeSpans(rcContext* context, const int walkableHeight, const int walkableClimb, - rcHeightfield& heightfield) +void rcFilterLedgeSpans(rcContext* context, const int walkableHeight, const int walkableClimb, rcHeightfield& heightfield) { rcAssert(context); @@ -68,7 +72,6 @@ void rcFilterLedgeSpans(rcContext* context, const int walkableHeight, const int const int xSize = heightfield.width; const int zSize = heightfield.height; - const int MAX_HEIGHT = 0xffff; // TODO (graham): Move this to a more visible constant and update usages. // Mark border spans. for (int z = 0; z < zSize; ++z) @@ -84,10 +87,10 @@ void rcFilterLedgeSpans(rcContext* context, const int walkableHeight, const int } const int bot = (int)(span->smax); - const int top = span->next ? (int)(span->next->smin) : MAX_HEIGHT; + const int top = span->next ? (int)(span->next->smin) : MAX_HEIGHTFIELD_HEIGHT; // Find neighbours minimum height. - int minNeighborHeight = MAX_HEIGHT; + int minNeighborHeight = MAX_HEIGHTFIELD_HEIGHT; // Min and max height of accessible neighbours. int accessibleNeighborMinHeight = span->smax; @@ -106,7 +109,7 @@ void rcFilterLedgeSpans(rcContext* context, const int walkableHeight, const int // From minus infinity to the first span. const rcSpan* neighborSpan = heightfield.spans[dx + dz * xSize]; - int neighborTop = neighborSpan ? (int)neighborSpan->smin : MAX_HEIGHT; + int neighborTop = neighborSpan ? (int)neighborSpan->smin : MAX_HEIGHTFIELD_HEIGHT; // Skip neighbour if the gap between the spans is too small. if (rcMin(top, neighborTop) - bot >= walkableHeight) @@ -119,7 +122,7 @@ void rcFilterLedgeSpans(rcContext* context, const int walkableHeight, const int for (neighborSpan = heightfield.spans[dx + dz * xSize]; neighborSpan; neighborSpan = neighborSpan->next) { int neighborBot = (int)neighborSpan->smax; - neighborTop = neighborSpan->next ? (int)neighborSpan->next->smin : MAX_HEIGHT; + neighborTop = neighborSpan->next ? (int)neighborSpan->next->smin : MAX_HEIGHTFIELD_HEIGHT; // Skip neighbour if the gap between the spans is too small. if (rcMin(top, neighborTop) - rcMax(bot, neighborBot) >= walkableHeight) @@ -137,19 +140,16 @@ void rcFilterLedgeSpans(rcContext* context, const int walkableHeight, const int { break; } - } } } - // The current span is close to a ledge if the drop to any - // neighbour span is less than the walkableClimb. + // The current span is close to a ledge if the drop to any neighbour span is less than the walkableClimb. if (minNeighborHeight < -walkableClimb) { span->area = RC_NULL_AREA; } - // If the difference between all neighbours is too large, - // we are at steep slope, mark the span as ledge. + // If the difference between all neighbours is too large, we are at steep slope, mark the span as ledge. else if ((accessibleNeighborMaxHeight - accessibleNeighborMinHeight) > walkableClimb) { span->area = RC_NULL_AREA; @@ -162,13 +162,11 @@ void rcFilterLedgeSpans(rcContext* context, const int walkableHeight, const int void rcFilterWalkableLowHeightSpans(rcContext* context, const int walkableHeight, rcHeightfield& heightfield) { rcAssert(context); - rcScopedTimer timer(context, RC_TIMER_FILTER_WALKABLE); - + const int xSize = heightfield.width; const int zSize = heightfield.height; - const int MAX_HEIGHT = 0xffff; - + // Remove walkable flag from spans which do not have enough // space above them for the agent to stand there. for (int z = 0; z < zSize; ++z) @@ -178,7 +176,7 @@ void rcFilterWalkableLowHeightSpans(rcContext* context, const int walkableHeight for (rcSpan* span = heightfield.spans[x + z*xSize]; span; span = span->next) { const int bot = (int)(span->smax); - const int top = span->next ? (int)(span->next->smin) : MAX_HEIGHT; + const int top = span->next ? (int)(span->next->smin) : MAX_HEIGHTFIELD_HEIGHT; if ((top - bot) < walkableHeight) { span->area = RC_NULL_AREA; diff --git a/Tests/Recast/Bench_rcVector.cpp b/Tests/Recast/Bench_rcVector.cpp new file mode 100644 index 000000000..46c1cee2d --- /dev/null +++ b/Tests/Recast/Bench_rcVector.cpp @@ -0,0 +1,168 @@ +#include +#include + +#include "catch2/catch_all.hpp" + +#include "Recast.h" +#include "RecastAlloc.h" +#include "RecastAssert.h" +#include + +// TODO: Implement benchmarking for platforms other than posix. +#ifdef __unix__ +#include +#ifdef _POSIX_TIMERS +#include +#include + +int64_t NowNanos() { + struct timespec tp; + clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &tp); + return tp.tv_nsec + 1000000000LL * tp.tv_sec; +} + +#define BM(name, iterations) \ + struct BM_ ## name { \ + static void Run() { \ + int64_t begin_time = NowNanos(); \ + for (int i = 0 ; i < iterations; i++) { \ + Body(); \ + } \ + int64_t nanos = NowNanos() - begin_time; \ + printf("BM_%-35s %ld iterations in %10ld nanos: %10.2f nanos/it\n", #name ":", (int64_t)iterations, nanos, double(nanos) / iterations); \ + } \ + static void Body(); \ + }; \ + TEST_CASE(#name) { \ + BM_ ## name::Run(); \ + } \ + void BM_ ## name::Body() + +const int64_t kNumLoops = 100; +const int64_t kNumInserts = 100000; + +// Prevent compiler from eliding a calculation. +// TODO: Implement for MSVC. +template +void DoNotOptimize(T* v) { + asm volatile ("" : "+r" (v)); +} + +BM(FlatArray_Push, kNumLoops) +{ + int cap = 64; + int* v = (int*)rcAlloc(cap * sizeof(int), RC_ALLOC_TEMP); + for (int j = 0; j < kNumInserts; j++) { + if (j == cap) { + cap *= 2; + int* tmp = (int*)rcAlloc(sizeof(int) * cap, RC_ALLOC_TEMP); + memcpy(tmp, v, j * sizeof(int)); + rcFree(v); + v = tmp; + } + v[j] = 2; + } + + DoNotOptimize(v); + rcFree(v); +} +BM(FlatArray_Fill, kNumLoops) +{ + int* v = (int*)rcAlloc(sizeof(int) * kNumInserts, RC_ALLOC_TEMP); + for (int j = 0; j < kNumInserts; j++) { + v[j] = 2; + } + + DoNotOptimize(v); + rcFree(v); +} +BM(FlatArray_Memset, kNumLoops) +{ + int* v = (int*)rcAlloc(sizeof(int) * kNumInserts, RC_ALLOC_TEMP); + memset(v, 0, kNumInserts * sizeof(int)); + + DoNotOptimize(v); + rcFree(v); +} + +BM(rcVector_Push, kNumLoops) +{ + rcTempVector v; + for (int j = 0; j < kNumInserts; j++) { + v.push_back(2); + } + DoNotOptimize(v.data()); +} +BM(rcVector_PushPreallocated, kNumLoops) +{ + rcTempVector v; + v.reserve(kNumInserts); + for (int j = 0; j < kNumInserts; j++) { + v.push_back(2); + } + DoNotOptimize(v.data()); +} +BM(rcVector_Assign, kNumLoops) +{ + rcTempVector v; + v.assign(kNumInserts, 2); + DoNotOptimize(v.data()); +} +BM(rcVector_AssignIndices, kNumLoops) +{ + rcTempVector v; + v.resize(kNumInserts); + for (int j = 0; j < kNumInserts; j++) { + v[j] = 2; + } + DoNotOptimize(v.data()); +} +BM(rcVector_Resize, kNumLoops) +{ + rcTempVector v; + v.resize(kNumInserts, 2); + DoNotOptimize(v.data()); +} + +BM(stdvector_Push, kNumLoops) +{ + std::vector v; + for (int j = 0; j < kNumInserts; j++) { + v.push_back(2); + } + DoNotOptimize(v.data()); +} +BM(stdvector_PushPreallocated, kNumLoops) +{ + std::vector v; + v.reserve(kNumInserts); + for (int j = 0; j < kNumInserts; j++) { + v.push_back(2); + } + DoNotOptimize(v.data()); +} +BM(stdvector_Assign, kNumLoops) +{ + std::vector v; + v.assign(kNumInserts, 2); + DoNotOptimize(v.data()); +} +BM(stdvector_AssignIndices, kNumLoops) +{ + std::vector v; + v.resize(kNumInserts); + for (int j = 0; j < kNumInserts; j++) { + v[j] = 2; + } + DoNotOptimize(v.data()); +} +BM(stdvector_Resize, kNumLoops) +{ + std::vector v; + v.resize(kNumInserts, 2); + DoNotOptimize(v.data()); +} + +#undef BM +#endif // _POSIX_TIMERS +#endif // __unix__ diff --git a/Tests/Recast/Tests_Alloc.cpp b/Tests/Recast/Tests_Alloc.cpp new file mode 100644 index 000000000..2e7b2eb18 --- /dev/null +++ b/Tests/Recast/Tests_Alloc.cpp @@ -0,0 +1,260 @@ +#include +#include + +#include "catch2/catch_all.hpp" + +#include "RecastAlloc.h" +#include "RecastAssert.h" + +/// Used to verify that rcVector constructs/destroys objects correctly. +struct Incrementor { + static int constructions; + static int destructions; + static int copies; + Incrementor() { constructions++; } + ~Incrementor() { destructions++; } + Incrementor(const Incrementor&) { copies++; } + Incrementor& operator=(const Incrementor&); // Deleted assignment. + + static void Reset() { + constructions = 0; + destructions = 0; + copies = 0; + } +}; + +int Incrementor::constructions = 0; +int Incrementor::destructions = 0; +int Incrementor::copies = 0; + +const int kMaxAllocSize = 1024; +const unsigned char kClearValue = 0xff; + +/// Simple alloc/free that clears the memory on free.. +void* AllocAndInit(size_t size, rcAllocHint) { + rcAssert(kMaxAllocSize >= size); + return memset(malloc(kMaxAllocSize), 0, kMaxAllocSize); +} + +void FreeAndClear(void* mem) { + if (mem) { + memset(mem, kClearValue, kMaxAllocSize); + } + free(mem); +} + +// Verifies that memory has been initialized by AllocAndInit, and not cleared by FreeAndClear. +struct Copier { + const static int kAlive; + const static int kDead; + Copier() : value(kAlive) {} + + // checks that the source of the copy is valid. + Copier(const Copier& other) : value(kAlive) { + other.Verify(); + } + Copier& operator=(const Copier&); + + // Marks the value as dead. + ~Copier() { value = kDead; } + void Verify() const { + REQUIRE(value == kAlive); + } + volatile int value; +}; + +const int Copier::kAlive = 0x1f; +const int Copier::kDead = 0xde; + +struct NotDefaultConstructible { + NotDefaultConstructible(int) {} +}; + +TEST_CASE("rcVector", "[recast, alloc]") +{ + SECTION("Vector basics.") + { + rcTempVector vec; + REQUIRE(vec.size() == 0); + vec.push_back(10); + vec.push_back(12); + REQUIRE(vec.size() == 2); + REQUIRE(vec.capacity() >= 2); + REQUIRE(vec[0] == 10); + REQUIRE(vec[1] == 12); + vec.pop_back(); + REQUIRE(vec.size() == 1); + REQUIRE(vec[0] == 10); + vec.pop_back(); + REQUIRE(vec.size() == 0); + vec.resize(100, 5); + REQUIRE(vec.size() == 100); + for (int i = 0; i < 100; i++) { + REQUIRE(vec[i] == 5); + vec[i] = i; + } + for (int i = 0; i < 100; i++) { + REQUIRE(vec[i] == i); + } + } + + SECTION("Constructors/Destructors") + { + Incrementor::Reset(); + rcTempVector vec; + REQUIRE(Incrementor::constructions == 0); + REQUIRE(Incrementor::destructions == 0); + REQUIRE(Incrementor::copies == 0); + vec.push_back(Incrementor()); + // push_back() may create and copy objects internally. + REQUIRE(Incrementor::constructions == 1); + REQUIRE(Incrementor::destructions >= 1); + // REQUIRE(Incrementor::copies >= 2); + + vec.clear(); + Incrementor::Reset(); + vec.resize(100); + // Initialized with default instance. Temporaries may be constructed, then destroyed. + REQUIRE(Incrementor::constructions == 100); + REQUIRE(Incrementor::destructions == 0); + REQUIRE(Incrementor::copies == 0); + + Incrementor::Reset(); + for (int i = 0; i < 100; i++) { + REQUIRE(Incrementor::destructions == i); + vec.pop_back(); + } + REQUIRE(Incrementor::constructions == 0); + REQUIRE(Incrementor::destructions == 100); + REQUIRE(Incrementor::copies == 0); + + vec.resize(100); + Incrementor::Reset(); + vec.clear(); + // One temp object is constructed for the default argument of resize(). + REQUIRE(Incrementor::constructions == 0); + REQUIRE(Incrementor::destructions == 100); + REQUIRE(Incrementor::copies == 0); + + Incrementor::Reset(); + vec.resize(100, Incrementor()); + REQUIRE(Incrementor::constructions == 1); + REQUIRE(Incrementor::destructions == 1); + REQUIRE(Incrementor::copies == 100); + } + + SECTION("Copying Contents") + { + + // veriyf event counts after doubling size -- should require a lot of copying and destroying. + rcTempVector vec; + Incrementor::Reset(); + vec.resize(100); + REQUIRE(Incrementor::constructions == 100); + REQUIRE(Incrementor::destructions == 0); + REQUIRE(Incrementor::copies == 0); + Incrementor::Reset(); + vec.resize(200); + REQUIRE(vec.size() == vec.capacity()); + REQUIRE(Incrementor::constructions == 100); // Construc new elements. + REQUIRE(Incrementor::destructions == 100); // Destroy old contents. + REQUIRE(Incrementor::copies == 100); // Copy old elements into new array. + } + + SECTION("Swap") + { + rcTempVector a(10, 0xa); + rcTempVector b; + + int* a_data = a.data(); + int* b_data = b.data(); + + a.swap(b); + REQUIRE(a.size() == 0); + REQUIRE(b.size() == 10); + REQUIRE(b[0] == 0xa); + REQUIRE(b[9] == 0xa); + REQUIRE(a.data() == b_data); + REQUIRE(b.data() == a_data); + } + + SECTION("Overlapping init") + { + rcAllocSetCustom(&AllocAndInit, &FreeAndClear); + rcTempVector vec; + // Force a realloc during push_back(). + vec.resize(64); + REQUIRE(vec.capacity() == vec.size()); + REQUIRE(vec.capacity() > 0); + REQUIRE(vec.size() == vec.capacity()); + + // Don't crash. + vec.push_back(vec[0]); + rcAllocSetCustom(NULL, NULL); + } + + SECTION("Vector Destructor") + { + { + rcTempVector vec; + vec.resize(10); + Incrementor::Reset(); + } + REQUIRE(Incrementor::destructions == 10); + } + + SECTION("Assign") + { + rcTempVector a(10, 0xa); + a.assign(5, 0xb); + REQUIRE(a.size() == 5); + REQUIRE(a[0] == 0xb); + REQUIRE(a[4] == 0xb); + a.assign(15, 0xc); + REQUIRE(a.size() == 15); + REQUIRE(a[0] == 0xc); + REQUIRE(a[14] == 0xc); + + rcTempVector b; + b.assign(a.data(), a.data() + a.size()); + REQUIRE(b.size() == a.size()); + REQUIRE(b[0] == a[0]); + } + + SECTION("Copy") + { + rcTempVector a(10, 0xa); + rcTempVector b(a); + REQUIRE(a.size() == 10); + REQUIRE(a.size() == b.size()); + REQUIRE(a[0] == b[0]); + REQUIRE(a.data() != b.data()); + rcTempVector c(a.data(), a.data() + a.size()); + REQUIRE(c.size() == a.size()); + REQUIRE(c[0] == a[0]); + + rcTempVector d(10); + Incrementor::Reset(); + rcTempVector e(d); + REQUIRE(Incrementor::constructions == 0); + REQUIRE(Incrementor::destructions == 0); + REQUIRE(Incrementor::copies == 10); + + Incrementor::Reset(); + rcTempVector f(d.data(), d.data() + d.size()); + REQUIRE(Incrementor::constructions == 0); + REQUIRE(Incrementor::destructions == 0); + REQUIRE(Incrementor::copies == 10); + } + + SECTION("Type Requirements") + { + // This section verifies that we don't enforce unnecessary + // requirements on the types we hold. + + // Implementing clear as resize(0) will cause this to fail + // as resize(0) requires T to be default constructible. + rcTempVector v; + v.clear(); + } +} diff --git a/Tests/Recast/Tests_Recast.cpp b/Tests/Recast/Tests_Recast.cpp index 34c61d024..62d15072a 100644 --- a/Tests/Recast/Tests_Recast.cpp +++ b/Tests/Recast/Tests_Recast.cpp @@ -4,13 +4,8 @@ #include "catch2/catch_all.hpp" #include "Recast.h" -#include "RecastAlloc.h" -#include "RecastAssert.h" -// For comparing to rcVector in benchmarks. -#include - -TEST_CASE("rcSwap") +TEST_CASE("rcSwap", "[recast]") { SECTION("Swap two values") { @@ -22,7 +17,7 @@ TEST_CASE("rcSwap") } } -TEST_CASE("rcMin") +TEST_CASE("rcMin", "[recast]") { SECTION("Min returns the lowest value.") { @@ -36,7 +31,7 @@ TEST_CASE("rcMin") } } -TEST_CASE("rcMax") +TEST_CASE("rcMax", "[recast]") { SECTION("Max returns the greatest value.") { @@ -50,7 +45,7 @@ TEST_CASE("rcMax") } } -TEST_CASE("rcAbs") +TEST_CASE("rcAbs", "[recast]") { SECTION("Abs returns the absolute value.") { @@ -60,7 +55,7 @@ TEST_CASE("rcAbs") } } -TEST_CASE("rcSqr") +TEST_CASE("rcSqr", "[recast]") { SECTION("Sqr squares a number") { @@ -70,7 +65,7 @@ TEST_CASE("rcSqr") } } -TEST_CASE("rcClamp") +TEST_CASE("rcClamp", "[recast]") { SECTION("Higher than range") { @@ -88,7 +83,7 @@ TEST_CASE("rcClamp") } } -TEST_CASE("rcSqrt") +TEST_CASE("rcSqrt", "[recast]") { SECTION("Sqrt gets the sqrt of a number") { @@ -97,7 +92,7 @@ TEST_CASE("rcSqrt") } } -TEST_CASE("rcVcross") +TEST_CASE("rcVcross", "[recast]") { SECTION("Computes cross product") { @@ -121,7 +116,7 @@ TEST_CASE("rcVcross") } } -TEST_CASE("rcVdot") +TEST_CASE("rcVdot", "[recast]") { SECTION("Dot normalized vector with itself") { @@ -140,7 +135,7 @@ TEST_CASE("rcVdot") } } -TEST_CASE("rcVmad") +TEST_CASE("rcVmad", "[recast]") { SECTION("scaled add two vectors") { @@ -165,7 +160,7 @@ TEST_CASE("rcVmad") } } -TEST_CASE("rcVadd") +TEST_CASE("rcVadd", "[recast]") { SECTION("add two vectors") { @@ -179,7 +174,7 @@ TEST_CASE("rcVadd") } } -TEST_CASE("rcVsub") +TEST_CASE("rcVsub", "[recast]") { SECTION("subtract two vectors") { @@ -193,7 +188,7 @@ TEST_CASE("rcVsub") } } -TEST_CASE("rcVmin") +TEST_CASE("rcVmin", "[recast]") { SECTION("selects the min component from the vectors") { @@ -226,7 +221,7 @@ TEST_CASE("rcVmin") } } -TEST_CASE("rcVmax") +TEST_CASE("rcVmax", "[recast]") { SECTION("selects the max component from the vectors") { @@ -259,7 +254,7 @@ TEST_CASE("rcVmax") } } -TEST_CASE("rcVcopy") +TEST_CASE("rcVcopy", "[recast]") { SECTION("copies a vector into another vector") { @@ -275,7 +270,7 @@ TEST_CASE("rcVcopy") } } -TEST_CASE("rcVdist") +TEST_CASE("rcVdist", "[recast]") { SECTION("distance between two vectors") { @@ -296,7 +291,7 @@ TEST_CASE("rcVdist") } } -TEST_CASE("rcVdistSqr") +TEST_CASE("rcVdistSqr", "[recast]") { SECTION("squared distance between two vectors") { @@ -317,7 +312,7 @@ TEST_CASE("rcVdistSqr") } } -TEST_CASE("rcVnormalize") +TEST_CASE("rcVnormalize", "[recast]") { SECTION("normalizing reduces magnitude to 1") { @@ -331,7 +326,7 @@ TEST_CASE("rcVnormalize") } } -TEST_CASE("rcCalcBounds") +TEST_CASE("rcCalcBounds", "[recast]") { SECTION("bounds of one vector") { @@ -369,7 +364,7 @@ TEST_CASE("rcCalcBounds") } } -TEST_CASE("rcCalcGridSize") +TEST_CASE("rcCalcGridSize", "[recast]") { SECTION("computes the size of an x & z axis grid") { @@ -393,7 +388,7 @@ TEST_CASE("rcCalcGridSize") } } -TEST_CASE("rcCreateHeightfield") +TEST_CASE("rcCreateHeightfield", "[recast]") { SECTION("create a heightfield") { @@ -439,7 +434,7 @@ TEST_CASE("rcCreateHeightfield") } } -TEST_CASE("rcMarkWalkableTriangles") +TEST_CASE("rcMarkWalkableTriangles", "[recast]") { rcContext* ctx = 0; float walkableSlopeAngle = 45; @@ -481,7 +476,7 @@ TEST_CASE("rcMarkWalkableTriangles") } } -TEST_CASE("rcClearUnwalkableTriangles") +TEST_CASE("rcClearUnwalkableTriangles", "[recast]") { rcContext* ctx = 0; float walkableSlopeAngle = 45; @@ -516,7 +511,7 @@ TEST_CASE("rcClearUnwalkableTriangles") } } -TEST_CASE("rcAddSpan") +TEST_CASE("rcAddSpan", "[recast]") { rcContext ctx(false); @@ -605,7 +600,7 @@ TEST_CASE("rcAddSpan") } } -TEST_CASE("rcRasterizeTriangle") +TEST_CASE("rcRasterizeTriangle", "[recast]") { rcContext ctx; float verts[] = { @@ -657,7 +652,7 @@ TEST_CASE("rcRasterizeTriangle") } } -TEST_CASE("rcRasterizeTriangle overlapping bb but non-overlapping triangle") +TEST_CASE("rcRasterizeTriangle overlapping bb but non-overlapping triangle", "[recast]") { // This is a minimal repro case for the issue fixed in PR #476 (https://github.com/recastnavigation/recastnavigation/pull/476) rcContext ctx; @@ -694,7 +689,7 @@ TEST_CASE("rcRasterizeTriangle overlapping bb but non-overlapping triangle") } } -TEST_CASE("rcRasterizeTriangle smaller than half a voxel size in x") +TEST_CASE("rcRasterizeTriangle smaller than half a voxel size in x", "[recast]") { SECTION("Skinny triangle along x axis") { @@ -761,7 +756,7 @@ TEST_CASE("rcRasterizeTriangle smaller than half a voxel size in x") } } -TEST_CASE("rcRasterizeTriangles") +TEST_CASE("rcRasterizeTriangles", "[recast]") { rcContext ctx; float verts[] = { @@ -940,410 +935,3 @@ TEST_CASE("rcRasterizeTriangles") REQUIRE(!solid.spans[1 + 2 * width]->next); } } - -// Used to verify that rcVector constructs/destroys objects correctly. -struct Incrementor { - static int constructions; - static int destructions; - static int copies; - Incrementor() { constructions++; } - ~Incrementor() { destructions++; } - Incrementor(const Incrementor&) { copies++; } - Incrementor& operator=(const Incrementor&); // Deleted assignment. - - static void Reset() { - constructions = 0; - destructions = 0; - copies = 0; - } -}; -int Incrementor::constructions = 0; -int Incrementor::destructions = 0; -int Incrementor::copies = 0; - -const int kMaxAllocSize = 1024; -const unsigned char kClearValue = 0xff; -// Simple alloc/free that clears the memory on free.. -void* AllocAndInit(size_t size, rcAllocHint) { - rcAssert(kMaxAllocSize >= size); - return memset(malloc(kMaxAllocSize), 0, kMaxAllocSize); -} -void FreeAndClear(void* mem) { - if (mem) { - memset(mem, kClearValue, kMaxAllocSize); - } - free(mem); -} -// Verifies that memory has been initialized by AllocAndInit, and not cleared by FreeAndClear. -struct Copier { - const static int kAlive; - const static int kDead; - Copier() : value(kAlive) {} - - // checks that the source of the copy is valid. - Copier(const Copier& other) : value(kAlive) { - other.Verify(); - } - Copier& operator=(const Copier&); - - // Marks the value as dead. - ~Copier() { value = kDead; } - void Verify() const { - REQUIRE(value == kAlive); - } - volatile int value; -}; -const int Copier::kAlive = 0x1f; -const int Copier::kDead = 0xde; - -struct NotDefaultConstructible { - NotDefaultConstructible(int) {} -}; - -TEST_CASE("rcVector") -{ - SECTION("Vector basics.") - { - rcTempVector vec; - REQUIRE(vec.size() == 0); - vec.push_back(10); - vec.push_back(12); - REQUIRE(vec.size() == 2); - REQUIRE(vec.capacity() >= 2); - REQUIRE(vec[0] == 10); - REQUIRE(vec[1] == 12); - vec.pop_back(); - REQUIRE(vec.size() == 1); - REQUIRE(vec[0] == 10); - vec.pop_back(); - REQUIRE(vec.size() == 0); - vec.resize(100, 5); - REQUIRE(vec.size() == 100); - for (int i = 0; i < 100; i++) { - REQUIRE(vec[i] == 5); - vec[i] = i; - } - for (int i = 0; i < 100; i++) { - REQUIRE(vec[i] == i); - } - } - - SECTION("Constructors/Destructors") - { - Incrementor::Reset(); - rcTempVector vec; - REQUIRE(Incrementor::constructions == 0); - REQUIRE(Incrementor::destructions == 0); - REQUIRE(Incrementor::copies == 0); - vec.push_back(Incrementor()); - // push_back() may create and copy objects internally. - REQUIRE(Incrementor::constructions == 1); - REQUIRE(Incrementor::destructions >= 1); - // REQUIRE(Incrementor::copies >= 2); - - vec.clear(); - Incrementor::Reset(); - vec.resize(100); - // Initialized with default instance. Temporaries may be constructed, then destroyed. - REQUIRE(Incrementor::constructions == 100); - REQUIRE(Incrementor::destructions == 0); - REQUIRE(Incrementor::copies == 0); - - Incrementor::Reset(); - for (int i = 0; i < 100; i++) { - REQUIRE(Incrementor::destructions == i); - vec.pop_back(); - } - REQUIRE(Incrementor::constructions == 0); - REQUIRE(Incrementor::destructions == 100); - REQUIRE(Incrementor::copies == 0); - - vec.resize(100); - Incrementor::Reset(); - vec.clear(); - // One temp object is constructed for the default argument of resize(). - REQUIRE(Incrementor::constructions == 0); - REQUIRE(Incrementor::destructions == 100); - REQUIRE(Incrementor::copies == 0); - - Incrementor::Reset(); - vec.resize(100, Incrementor()); - REQUIRE(Incrementor::constructions == 1); - REQUIRE(Incrementor::destructions == 1); - REQUIRE(Incrementor::copies == 100); - } - - SECTION("Copying Contents") - { - - // veriyf event counts after doubling size -- should require a lot of copying and destroying. - rcTempVector vec; - Incrementor::Reset(); - vec.resize(100); - REQUIRE(Incrementor::constructions == 100); - REQUIRE(Incrementor::destructions == 0); - REQUIRE(Incrementor::copies == 0); - Incrementor::Reset(); - vec.resize(200); - REQUIRE(vec.size() == vec.capacity()); - REQUIRE(Incrementor::constructions == 100); // Construc new elements. - REQUIRE(Incrementor::destructions == 100); // Destroy old contents. - REQUIRE(Incrementor::copies == 100); // Copy old elements into new array. - } - - SECTION("Swap") - { - rcTempVector a(10, 0xa); - rcTempVector b; - - int* a_data = a.data(); - int* b_data = b.data(); - - a.swap(b); - REQUIRE(a.size() == 0); - REQUIRE(b.size() == 10); - REQUIRE(b[0] == 0xa); - REQUIRE(b[9] == 0xa); - REQUIRE(a.data() == b_data); - REQUIRE(b.data() == a_data); - } - - SECTION("Overlapping init") - { - rcAllocSetCustom(&AllocAndInit, &FreeAndClear); - rcTempVector vec; - // Force a realloc during push_back(). - vec.resize(64); - REQUIRE(vec.capacity() == vec.size()); - REQUIRE(vec.capacity() > 0); - REQUIRE(vec.size() == vec.capacity()); - - // Don't crash. - vec.push_back(vec[0]); - rcAllocSetCustom(NULL, NULL); - } - - SECTION("Vector Destructor") - { - { - rcTempVector vec; - vec.resize(10); - Incrementor::Reset(); - } - REQUIRE(Incrementor::destructions == 10); - } - - SECTION("Assign") - { - rcTempVector a(10, 0xa); - a.assign(5, 0xb); - REQUIRE(a.size() == 5); - REQUIRE(a[0] == 0xb); - REQUIRE(a[4] == 0xb); - a.assign(15, 0xc); - REQUIRE(a.size() == 15); - REQUIRE(a[0] == 0xc); - REQUIRE(a[14] == 0xc); - - rcTempVector b; - b.assign(a.data(), a.data() + a.size()); - REQUIRE(b.size() == a.size()); - REQUIRE(b[0] == a[0]); - } - - SECTION("Copy") - { - rcTempVector a(10, 0xa); - rcTempVector b(a); - REQUIRE(a.size() == 10); - REQUIRE(a.size() == b.size()); - REQUIRE(a[0] == b[0]); - REQUIRE(a.data() != b.data()); - rcTempVector c(a.data(), a.data() + a.size()); - REQUIRE(c.size() == a.size()); - REQUIRE(c[0] == a[0]); - - rcTempVector d(10); - Incrementor::Reset(); - rcTempVector e(d); - REQUIRE(Incrementor::constructions == 0); - REQUIRE(Incrementor::destructions == 0); - REQUIRE(Incrementor::copies == 10); - - Incrementor::Reset(); - rcTempVector f(d.data(), d.data() + d.size()); - REQUIRE(Incrementor::constructions == 0); - REQUIRE(Incrementor::destructions == 0); - REQUIRE(Incrementor::copies == 10); - } - - SECTION("Type Requirements") - { - // This section verifies that we don't enforce unnecessary - // requirements on the types we hold. - - // Implementing clear as resize(0) will cause this to fail - // as resize(0) requires T to be default constructible. - rcTempVector v; - v.clear(); - } -} - -// TODO: Implement benchmarking for platforms other than posix. -#ifdef __unix__ -#include -#ifdef _POSIX_TIMERS -#include -#include - -int64_t NowNanos() { - struct timespec tp; - clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &tp); - return tp.tv_nsec + 1000000000LL * tp.tv_sec; -} - -#define BM(name, iterations) \ - struct BM_ ## name { \ - static void Run() { \ - int64_t begin_time = NowNanos(); \ - for (int i = 0 ; i < iterations; i++) { \ - Body(); \ - } \ - int64_t nanos = NowNanos() - begin_time; \ - printf("BM_%-35s %ld iterations in %10ld nanos: %10.2f nanos/it\n", #name ":", (int64_t)iterations, nanos, double(nanos) / iterations); \ - } \ - static void Body(); \ - }; \ - TEST_CASE(#name) { \ - BM_ ## name::Run(); \ - } \ - void BM_ ## name::Body() - -const int64_t kNumLoops = 100; -const int64_t kNumInserts = 100000; - -// Prevent compiler from eliding a calculation. -// TODO: Implement for MSVC. -template -void DoNotOptimize(T* v) { - asm volatile ("" : "+r" (v)); -} - -BM(FlatArray_Push, kNumLoops) -{ - int cap = 64; - int* v = (int*)rcAlloc(cap * sizeof(int), RC_ALLOC_TEMP); - for (int j = 0; j < kNumInserts; j++) { - if (j == cap) { - cap *= 2; - int* tmp = (int*)rcAlloc(sizeof(int) * cap, RC_ALLOC_TEMP); - memcpy(tmp, v, j * sizeof(int)); - rcFree(v); - v = tmp; - } - v[j] = 2; - } - - DoNotOptimize(v); - rcFree(v); -} -BM(FlatArray_Fill, kNumLoops) -{ - int* v = (int*)rcAlloc(sizeof(int) * kNumInserts, RC_ALLOC_TEMP); - for (int j = 0; j < kNumInserts; j++) { - v[j] = 2; - } - - DoNotOptimize(v); - rcFree(v); -} -BM(FlatArray_Memset, kNumLoops) -{ - int* v = (int*)rcAlloc(sizeof(int) * kNumInserts, RC_ALLOC_TEMP); - memset(v, 0, kNumInserts * sizeof(int)); - - DoNotOptimize(v); - rcFree(v); -} - -BM(rcVector_Push, kNumLoops) -{ - rcTempVector v; - for (int j = 0; j < kNumInserts; j++) { - v.push_back(2); - } - DoNotOptimize(v.data()); -} -BM(rcVector_PushPreallocated, kNumLoops) -{ - rcTempVector v; - v.reserve(kNumInserts); - for (int j = 0; j < kNumInserts; j++) { - v.push_back(2); - } - DoNotOptimize(v.data()); -} -BM(rcVector_Assign, kNumLoops) -{ - rcTempVector v; - v.assign(kNumInserts, 2); - DoNotOptimize(v.data()); -} -BM(rcVector_AssignIndices, kNumLoops) -{ - rcTempVector v; - v.resize(kNumInserts); - for (int j = 0; j < kNumInserts; j++) { - v[j] = 2; - } - DoNotOptimize(v.data()); -} -BM(rcVector_Resize, kNumLoops) -{ - rcTempVector v; - v.resize(kNumInserts, 2); - DoNotOptimize(v.data()); -} - -BM(stdvector_Push, kNumLoops) -{ - std::vector v; - for (int j = 0; j < kNumInserts; j++) { - v.push_back(2); - } - DoNotOptimize(v.data()); -} -BM(stdvector_PushPreallocated, kNumLoops) -{ - std::vector v; - v.reserve(kNumInserts); - for (int j = 0; j < kNumInserts; j++) { - v.push_back(2); - } - DoNotOptimize(v.data()); -} -BM(stdvector_Assign, kNumLoops) -{ - std::vector v; - v.assign(kNumInserts, 2); - DoNotOptimize(v.data()); -} -BM(stdvector_AssignIndices, kNumLoops) -{ - std::vector v; - v.resize(kNumInserts); - for (int j = 0; j < kNumInserts; j++) { - v[j] = 2; - } - DoNotOptimize(v.data()); -} -BM(stdvector_Resize, kNumLoops) -{ - std::vector v; - v.resize(kNumInserts, 2); - DoNotOptimize(v.data()); -} - -#undef BM -#endif // _POSIX_TIMERS -#endif // __unix__ diff --git a/Tests/Recast/Tests_RecastFilter.cpp b/Tests/Recast/Tests_RecastFilter.cpp new file mode 100644 index 000000000..1882adba2 --- /dev/null +++ b/Tests/Recast/Tests_RecastFilter.cpp @@ -0,0 +1,389 @@ +#include +#include +#include + +#include "catch2/catch_all.hpp" + +#include "Recast.h" +#include "RecastAlloc.h" + +TEST_CASE("rcFilterLowHangingWalkableObstacles", "[recast, filtering]") +{ + rcContext context; + int walkableHeight = 5; + + rcHeightfield heightfield; + heightfield.width = 1; + heightfield.height = 1; + heightfield.bmin[0] = 0; + heightfield.bmin[1] = 0; + heightfield.bmin[2] = 0; + heightfield.bmax[0] = 1; + heightfield.bmax[1] = 1; + heightfield.bmax[2] = 1; + heightfield.cs = 1; + heightfield.ch = 1; + heightfield.spans = (rcSpan**)rcAlloc(heightfield.width * heightfield.height * sizeof(rcSpan*), RC_ALLOC_PERM); + heightfield.pools = NULL; + heightfield.freelist = NULL; + + SECTION("Span with no spans above it is unchanged") + { + rcSpan* span = (rcSpan*)rcAlloc(sizeof(rcSpan), RC_ALLOC_PERM); + span->area = 1; + span->next = NULL; + span->smin = 0; + span->smax = 1; + heightfield.spans[0] = span; + + rcFilterLowHangingWalkableObstacles(&context, walkableHeight, heightfield); + + REQUIRE(heightfield.spans[0]->area == 1); + + rcFree(span); + } + + SECTION("Span with span above that is higher than walkableHeight is unchanged") + { + // Put the second span just above the first one. + rcSpan* secondSpan = (rcSpan*)rcAlloc(sizeof(rcSpan), RC_ALLOC_PERM); + secondSpan->area = 1; + secondSpan->next = NULL; + secondSpan->smin = 1 + walkableHeight; + secondSpan->smax = secondSpan->smin + 1; + + rcSpan* span = (rcSpan*)rcAlloc(sizeof(rcSpan), RC_ALLOC_PERM); + span->area = 1; + span->next = secondSpan; + span->smin = 0; + span->smax = 1; + + heightfield.spans[0] = span; + + rcFilterLowHangingWalkableObstacles(&context, walkableHeight, heightfield); + + // Check that nothing has changed. + REQUIRE(heightfield.spans[0]->area == 1); + REQUIRE(heightfield.spans[0]->next->area == 1); + + // Check again but with a more clearance + secondSpan->smin += 10; + secondSpan->smax += 10; + + rcFilterLowHangingWalkableObstacles(&context, walkableHeight, heightfield); + + // Check that nothing has changed. + REQUIRE(heightfield.spans[0]->area == 1); + REQUIRE(heightfield.spans[0]->next->area == 1); + + rcFree(span); + rcFree(secondSpan); + } + + SECTION("Marks low obstacles walkable if they're below the walkableClimb") + { + // Put the second span just above the first one. + rcSpan* secondSpan = (rcSpan*)rcAlloc(sizeof(rcSpan), RC_ALLOC_PERM); + secondSpan->area = RC_NULL_AREA; + secondSpan->next = NULL; + secondSpan->smin = 1 + (walkableHeight - 1); + secondSpan->smax = secondSpan->smin + 1; + + rcSpan* span = (rcSpan*)rcAlloc(sizeof(rcSpan), RC_ALLOC_PERM); + span->area = 1; + span->next = secondSpan; + span->smin = 0; + span->smax = 1; + + heightfield.spans[0] = span; + + rcFilterLowHangingWalkableObstacles(&context, walkableHeight, heightfield); + + // Check that the second span was changed to walkable. + REQUIRE(heightfield.spans[0]->area == 1); + REQUIRE(heightfield.spans[0]->next->area == 1); + + rcFree(span); + rcFree(secondSpan); + } + + SECTION("Low obstacle that overlaps the walkableClimb distance is not changed") + { + // Put the second span just above the first one. + rcSpan* secondSpan = (rcSpan*)rcAlloc(sizeof(rcSpan), RC_ALLOC_PERM); + secondSpan->area = RC_NULL_AREA; + secondSpan->next = NULL; + secondSpan->smin = 2 + (walkableHeight - 1); + secondSpan->smax = secondSpan->smin + 1; + + rcSpan* span = (rcSpan*)rcAlloc(sizeof(rcSpan), RC_ALLOC_PERM); + span->area = 1; + span->next = secondSpan; + span->smin = 0; + span->smax = 1; + + heightfield.spans[0] = span; + + rcFilterLowHangingWalkableObstacles(&context, walkableHeight, heightfield); + + // Check that the second span was changed to walkable. + REQUIRE(heightfield.spans[0]->area == 1); + REQUIRE(heightfield.spans[0]->next->area == RC_NULL_AREA); + + rcFree(span); + rcFree(secondSpan); + } + + SECTION("Only the first of multiple, low obstacles are marked walkable") + { + rcSpan* span = (rcSpan*)rcAlloc(sizeof(rcSpan), RC_ALLOC_PERM); + span->area = 1; + span->next = NULL; + span->smin = 0; + span->smax = 1; + heightfield.spans[0] = span; + + rcSpan* previousSpan = span; + for (int i = 0; i < 9; ++i) + { + rcSpan* nextSpan = (rcSpan*)rcAlloc(sizeof(rcSpan), RC_ALLOC_PERM); + nextSpan->area = RC_NULL_AREA; + nextSpan->next = NULL; + nextSpan->smin = previousSpan->smax + (walkableHeight - 1); + nextSpan->smax = nextSpan->smin + 1; + previousSpan->next = nextSpan; + previousSpan = nextSpan; + } + + rcFilterLowHangingWalkableObstacles(&context, walkableHeight, heightfield); + + rcSpan* currentSpan = heightfield.spans[0]; + for (int i = 0; i < 10; ++i) + { + REQUIRE(currentSpan != NULL); + // only the first and second spans should be marked as walkabl + REQUIRE(currentSpan->area == (i <= 1 ? 1 : RC_NULL_AREA)); + currentSpan = currentSpan->next; + } + + std::vector toFree; + span = heightfield.spans[0]; + for (int i = 0; i < 10; ++i) + { + toFree.push_back(span); + span = span->next; + } + + for (int i = 0; i < 10; ++i) + { + rcFree(toFree[i]); + } + } +} + +TEST_CASE("rcFilterLedgeSpans", "[recast, filtering]") +{ + rcContext context; + int walkableClimb = 5; + int walkableHeight = 10; + + rcHeightfield heightfield; + heightfield.width = 10; + heightfield.height = 10; + heightfield.bmin[0] = 0; + heightfield.bmin[1] = 0; + heightfield.bmin[2] = 0; + heightfield.bmax[0] = 10; + heightfield.bmax[1] = 1; + heightfield.bmax[2] = 10; + heightfield.cs = 1; + heightfield.ch = 1; + heightfield.spans = (rcSpan**)rcAlloc(heightfield.width * heightfield.height * sizeof(rcSpan*), RC_ALLOC_PERM); + heightfield.pools = NULL; + heightfield.freelist = NULL; + + SECTION("Edge spans are marked unwalkable") + { + // Create a flat plane. + for (int x = 0; x < heightfield.width; ++x) + { + for (int z = 0; z < heightfield.height; ++z) + { + rcSpan* span = (rcSpan*)rcAlloc(sizeof(rcSpan), RC_ALLOC_PERM); + span->area = 1; + span->next = NULL; + span->smin = 0; + span->smax = 1; + heightfield.spans[x + z * heightfield.width] = span; + } + } + + rcFilterLedgeSpans(&context, walkableHeight, walkableClimb, heightfield); + + for (int x = 0; x < heightfield.width; ++x) + { + for (int z = 0; z < heightfield.height; ++z) + { + rcSpan* span = heightfield.spans[x + z * heightfield.width]; + REQUIRE(span != NULL); + + if (x == 0 || z == 0 || x == 9 || z == 9) + { + REQUIRE(span->area == RC_NULL_AREA); + } + else + { + REQUIRE(span->area == 1); + } + + REQUIRE(span->next == NULL); + REQUIRE(span->smin == 0); + REQUIRE(span->smax == 1); + } + } + + // Free all the heightfield spans + for (int x = 0; x < heightfield.width; ++x) + { + for (int z = 0; z < heightfield.height; ++z) + { + rcFree(heightfield.spans[x + z * heightfield.width]); + } + } + } + + SECTION("Edge spans are marked unwalkable") + { + // Create a flat plane. + for (int x = 0; x < heightfield.width; ++x) + { + for (int z = 0; z < heightfield.height; ++z) + { + rcSpan* span = (rcSpan*)rcAlloc(sizeof(rcSpan), RC_ALLOC_PERM); + span->area = 1; + span->next = NULL; + span->smin = 0; + span->smax = 1; + heightfield.spans[x + z * heightfield.width] = span; + } + } + + rcFilterLedgeSpans(&context, walkableHeight, walkableClimb, heightfield); + + for (int x = 0; x < heightfield.width; ++x) + { + for (int z = 0; z < heightfield.height; ++z) + { + rcSpan* span = heightfield.spans[x + z * heightfield.width]; + REQUIRE(span != NULL); + + if (x == 0 || z == 0 || x == 9 || z == 9) + { + REQUIRE(span->area == RC_NULL_AREA); + } + else + { + REQUIRE(span->area == 1); + } + + REQUIRE(span->next == NULL); + REQUIRE(span->smin == 0); + REQUIRE(span->smax == 1); + } + } + + // Free all the heightfield spans + for (int x = 0; x < heightfield.width; ++x) + { + for (int z = 0; z < heightfield.height; ++z) + { + rcFree(heightfield.spans[x + z * heightfield.width]); + } + } + } +} + +TEST_CASE("rcFilterWalkableLowHeightSpans", "[recast, filtering]") +{ + rcContext context; + int walkableHeight = 5; + + rcHeightfield heightfield; + heightfield.width = 1; + heightfield.height = 1; + heightfield.bmin[0] = 0; + heightfield.bmin[1] = 0; + heightfield.bmin[2] = 0; + heightfield.bmax[0] = 1; + heightfield.bmax[1] = 1; + heightfield.bmax[2] = 1; + heightfield.cs = 1; + heightfield.ch = 1; + heightfield.spans = (rcSpan**)rcAlloc(heightfield.width * heightfield.height * sizeof(rcSpan*), RC_ALLOC_PERM); + heightfield.pools = NULL; + heightfield.freelist = NULL; + + SECTION("span nothing above is unchanged") + { + rcSpan* span = (rcSpan*)rcAlloc(sizeof(rcSpan), RC_ALLOC_PERM); + span->area = 1; + span->next = NULL; + span->smin = 0; + span->smax = 1; + heightfield.spans[0] = span; + + rcFilterWalkableLowHeightSpans(&context, walkableHeight, heightfield); + + REQUIRE(heightfield.spans[0]->area == 1); + + rcFree(span); + } + + SECTION("span with lots of room above is unchanged") + { + rcSpan* overheadSpan = (rcSpan*)rcAlloc(sizeof(rcSpan), RC_ALLOC_PERM); + overheadSpan->area = RC_NULL_AREA; + overheadSpan->next = NULL; + overheadSpan->smin = 10; + overheadSpan->smax = 11; + + rcSpan* span = (rcSpan*)rcAlloc(sizeof(rcSpan), RC_ALLOC_PERM); + span->area = 1; + span->next = overheadSpan; + span->smin = 0; + span->smax = 1; + heightfield.spans[0] = span; + + rcFilterWalkableLowHeightSpans(&context, walkableHeight, heightfield); + + REQUIRE(heightfield.spans[0]->area == 1); + REQUIRE(heightfield.spans[0]->next->area == RC_NULL_AREA); + + rcFree(overheadSpan); + rcFree(span); + } + + SECTION("Span with low hanging obstacle is marked as unwalkable") + { + rcSpan* overheadSpan = (rcSpan*)rcAlloc(sizeof(rcSpan), RC_ALLOC_PERM); + overheadSpan->area = RC_NULL_AREA; + overheadSpan->next = NULL; + overheadSpan->smin = 3; + overheadSpan->smax = 4; + + rcSpan* span = (rcSpan*)rcAlloc(sizeof(rcSpan), RC_ALLOC_PERM); + span->area = 1; + span->next = overheadSpan; + span->smin = 0; + span->smax = 1; + heightfield.spans[0] = span; + + rcFilterWalkableLowHeightSpans(&context, walkableHeight, heightfield); + + REQUIRE(heightfield.spans[0]->area == RC_NULL_AREA); + REQUIRE(heightfield.spans[0]->next->area == RC_NULL_AREA); + + rcFree(overheadSpan); + rcFree(span); + } +} \ No newline at end of file