ludic/packages/ludic.physics/native/shim/jph_alloc.inl
Orkuncakilkaya e9de2b6f27 feat(ludic.physics): Jolt's heap counted in the shim (plan 25) - jph_heap_bytes/peak/allocs, phys_heap_bytes/peak/allocs
Jolt's Allocate/Reallocate/Free/AlignedAllocate/AlignedFree go to libc's malloc with the size in a
16-byte header, and atomic counters keep live bytes, the peak and the blocks asked for (its job
threads allocate too). The fence reads the three exports extern_weak to judge Jolt by its plateau.
jolt_heap_test: a ground in and out 1100 times holds 46,482,514 bytes after the first 100 and after
all of them, the peak does not move, and a closed world gives back every byte it took. The macOS
library is rebuilt; the Windows DLL still has to be rebuilt on the PC.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:49:38 +03:00

68 lines
2.5 KiB
C++

// jph_alloc.inl - Jolt's allocations counted apart from the game's (plan 25): libc's malloc underneath,
// each block carrying its size in a 16-byte header, so live bytes, the peak and the count are exact.
// The fence reads them (extern_weak) to judge Jolt by its plateau, not per frame.
#include <atomic>
#include <cstdlib>
#include <cstring>
namespace {
constexpr size_t kHead = 16;
std::atomic<int64_t> g_live{0}, g_peak{0}, g_allocs{0};
void heap_add(int64_t n) {
int64_t now = g_live.fetch_add(n, std::memory_order_relaxed) + n;
int64_t p = g_peak.load(std::memory_order_relaxed);
while (now > p && !g_peak.compare_exchange_weak(p, now, std::memory_order_relaxed)) {}
}
void *heap_alloc(size_t n) {
char *b = static_cast<char *>(std::malloc(n + kHead));
if (!b) return nullptr;
std::memcpy(b, &n, sizeof n);
g_allocs.fetch_add(1, std::memory_order_relaxed);
heap_add(int64_t(n));
return b + kHead;
}
void heap_free(void *p) {
if (!p) return;
char *b = static_cast<char *>(p) - kHead;
size_t n;
std::memcpy(&n, b, sizeof n);
heap_add(-int64_t(n));
std::free(b);
}
void *heap_realloc(void *p, size_t, size_t n) {
if (!p) return heap_alloc(n);
char *b = static_cast<char *>(p) - kHead;
size_t old;
std::memcpy(&old, b, sizeof old);
char *q = static_cast<char *>(std::realloc(b, n + kHead));
if (!q) return nullptr;
std::memcpy(q, &n, sizeof n);
heap_add(int64_t(n) - int64_t(old));
return q + kHead;
}
// aligned: over-asked, the counted block's pointer kept in the word before the aligned one
void *heap_aligned(size_t n, size_t align) {
if (align < sizeof(void *)) align = sizeof(void *);
char *raw = static_cast<char *>(heap_alloc(n + align + sizeof(void *)));
if (!raw) return nullptr;
uintptr_t at = (reinterpret_cast<uintptr_t>(raw) + sizeof(void *) + align - 1) & ~(uintptr_t(align) - 1);
reinterpret_cast<void **>(at)[-1] = raw;
return reinterpret_cast<void *>(at);
}
void heap_aligned_free(void *p) {
if (p) heap_free(static_cast<void **>(p)[-1]);
}
void heap_register() {
JPH::Allocate = heap_alloc;
JPH::Reallocate = heap_realloc;
JPH::Free = heap_free;
JPH::AlignedAllocate = heap_aligned;
JPH::AlignedFree = heap_aligned_free;
}
} // namespace
// bytes Jolt holds now, the most it ever held, and how many blocks it has asked for
JPH_SHIM int64_t jph_heap_bytes(void) { return g_live.load(std::memory_order_relaxed); }
JPH_SHIM int64_t jph_heap_peak(void) { return g_peak.load(std::memory_order_relaxed); }
JPH_SHIM int64_t jph_heap_allocs(void) { return g_allocs.load(std::memory_order_relaxed); }