nav, physics: Recast's and Detour's memory counted (nav_heap_bytes / _peak / _allocs, a counted allocator registered at load, tile bytes staged from it); package tests that crossing the map and back holds the native bytes

The user's walk showed the footprint rising outside the Ludic heap. Jolt's bytes were already counted;
the navmesh's were not, so nothing could see them. Now:
- ludic.nav nav_heap_test: fifty crossings of a four-tile map by the resident index hold the tiles in and
  the native bytes to the first crossing's; a thousand crowd walkers in and out grow nothing; a reset
  gives back every byte the mesh took.
- ludic.physics cross_heap_test: a 1 km map of 64 m chunks kept to a ring round a player crossing
  corner to corner and back, each chunk a heightfield, twelve owned posts and six owned scaled hulls
  as the game makes them: six more crossings hold the bodies, the shapes and Jolt's bytes and peak
  exactly. (A post made as an offset of a cylinder, with only the offset owned, leaked the cylinder
  every time: the game's solid_pillar owns its cylinder directly.)

lib/macos-arm64 rebuilt; lib/windows-x64 needs native/build.sh run on the PC for the new exports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-29 13:32:18 +03:00
parent 7b91300962
commit b368912c78
9 changed files with 226 additions and 7 deletions

View file

@ -0,0 +1,42 @@
// nav_alloc.inl - Recast's and Detour's allocations counted apart from the game's (plan 25), as
// ludic.physics counts Jolt's: libc's malloc underneath, each block carrying its size in a 16-byte
// header, so the tiles, queries and crowds are measured exactly. Registered when the library loads.
#include <atomic>
namespace {
constexpr size_t kNavHead = 16;
std::atomic<long long> g_nav_live{0}, g_nav_peak{0}, g_nav_allocs{0};
void *nav_heap_alloc(size_t n) {
char *b = static_cast<char *>(std::malloc(n + kNavHead));
if (!b) return nullptr;
std::memcpy(b, &n, sizeof n);
g_nav_allocs.fetch_add(1, std::memory_order_relaxed);
long long now = g_nav_live.fetch_add((long long)n, std::memory_order_relaxed) + (long long)n;
long long p = g_nav_peak.load(std::memory_order_relaxed);
while (now > p && !g_nav_peak.compare_exchange_weak(p, now, std::memory_order_relaxed)) {}
return b + kNavHead;
}
void nav_heap_free(void *p) {
if (!p) return;
char *b = static_cast<char *>(p) - kNavHead;
size_t n;
std::memcpy(&n, b, sizeof n);
g_nav_live.fetch_sub((long long)n, std::memory_order_relaxed);
std::free(b);
}
void *nav_dt_alloc(size_t n, dtAllocHint) { return nav_heap_alloc(n); }
void *nav_rc_alloc(size_t n, rcAllocHint) { return nav_heap_alloc(n); }
struct NavHeapRegister {
NavHeapRegister() {
dtAllocSetCustom(nav_dt_alloc, nav_heap_free);
rcAllocSetCustom(nav_rc_alloc, nav_heap_free);
}
} g_nav_heap_register;
} // namespace
// bytes Recast and Detour hold now, the most they ever held, and how many blocks they asked for
NAV_SHIM long long nav_heap_bytes(void) { return g_nav_live.load(std::memory_order_relaxed); }
NAV_SHIM long long nav_heap_peak(void) { return g_nav_peak.load(std::memory_order_relaxed); }
NAV_SHIM long long nav_heap_allocs(void) { return g_nav_allocs.load(std::memory_order_relaxed); }

View file

@ -57,8 +57,8 @@ NAV_SHIM int nav_index_size(void *h, int i) { return static_cast<Nav *>(h)->tile
// tile i's bytes (staged by nav_stage, owned from here on) added to the mesh; 0 when refused
NAV_SHIM int nav_index_in(void *h, int i, unsigned char *data, int size) {
Nav *n = static_cast<Nav *>(h);
if (i < 0 || i >= (int)n->tiles.size() || n->tiles[i].in || !data) { free(data); return 0; }
if (dtStatusFailed(n->mesh->addTile(data, size, DT_TILE_FREE_DATA, (dtTileRef)n->tiles[i].ref, nullptr))) { free(data); return 0; }
if (i < 0 || i >= (int)n->tiles.size() || n->tiles[i].in || !data) { dtFree(data); return 0; }
if (dtStatusFailed(n->mesh->addTile(data, size, DT_TILE_FREE_DATA, (dtTileRef)n->tiles[i].ref, nullptr))) { dtFree(data); return 0; }
n->tiles[i].in = true;
return 1;
}

View file

@ -7,6 +7,8 @@
#include <DetourNavMeshQuery.h>
#include <DetourCommon.h>
#include <DetourCrowd.h>
#include <RecastAlloc.h>
#include <DetourAlloc.h>
#include <cstring>
#include <cstdlib>
#include <cmath>
@ -18,6 +20,8 @@
#define NAV_SHIM extern "C" __attribute__((visibility("default")))
#endif
#include "nav_alloc.inl"
#ifdef _WIN32
extern "C" __declspec(dllimport) int __stdcall QueryPerformanceCounter(long long *);
extern "C" __declspec(dllimport) int __stdcall QueryPerformanceFrequency(long long *);

View file

@ -103,10 +103,11 @@ NAV_SHIM void *nav_load(const unsigned char *buf, int size) {
// a file read straight into the shim's own memory rather than a Ludic slice (which is never given
// back): nav_stage hands out the room, nav_load_staged parses it and frees it, whatever the outcome
NAV_SHIM void nav_unstage(unsigned char *buf) { free(buf); }
NAV_SHIM unsigned char *nav_stage(int size) { return size > 0 ? static_cast<unsigned char *>(malloc(size)) : nullptr; }
// staged from Detour's own (counted) allocator, since a tile's bytes are handed to it to free
NAV_SHIM void nav_unstage(unsigned char *buf) { dtFree(buf); }
NAV_SHIM unsigned char *nav_stage(int size) { return size > 0 ? static_cast<unsigned char *>(dtAlloc(size, DT_ALLOC_PERM)) : nullptr; }
NAV_SHIM void *nav_load_staged(unsigned char *buf, int size) {
void *h = buf ? nav_load(buf, size) : nullptr;
free(buf);
dtFree(buf);
return h;
}