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>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-28 15:49:38 +03:00
parent 7fdd6582be
commit e9de2b6f27
5 changed files with 115 additions and 4 deletions

Binary file not shown.

View file

@ -45,3 +45,10 @@ extern function jph_body_angular_impulse(w: pointer, id: int, x: float, y: float
extern function jph_body_spin(w: pointer, id: int, out: pointer) -> void = "jph_body_spin"
extern function jph_body_set_pose(w: pointer, id: int, x: float, y: float, z: float, qx: float, qy: float, qz: float, qw: float, vx: float, vy: float, vz: float) -> void = "jph_body_set_pose"
extern function jph_shape_heightfield_bspline(tex: pointer, n: int, ox: float, oz: float, cell: float) -> pointer = "jph_shape_heightfield_bspline"
extern function jph_heap_bytes() -> long = "jph_heap_bytes"
extern function jph_heap_peak() -> long = "jph_heap_peak"
extern function jph_heap_allocs() -> long = "jph_heap_allocs"
# what Jolt holds now, the most it has held, and the blocks it has asked for (counted in the shim)
export function phys_heap_bytes() -> long { return jph_heap_bytes() }
export function phys_heap_peak() -> long { return jph_heap_peak() }
export function phys_heap_allocs() -> long { return jph_heap_allocs() }

View file

@ -0,0 +1,68 @@
// 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); }

View file

@ -120,10 +120,12 @@ void *keep(const ShapeSettings::ShapeResult &r) {
}
} // namespace
// Jolt's allocator, factory and types, once per process; 0 when ready
#include "jph_alloc.inl"
// Jolt's allocator (counted, jph_alloc.inl), factory and types, once per process; 0 when ready
JPH_SHIM int jph_init(void) {
if (g_ready) return 0;
RegisterDefaultAllocator();
heap_register();
Factory::sInstance = new Factory();
RegisterTypes();
g_ready = true;

View file

@ -0,0 +1,34 @@
# jolt_heap_test.ludic - Jolt's own heap, counted in the shim (plan 25): a ground coming and going a
# thousand times holds what it held after the first hundred, and a world closed gives back what it took
import "ludic.physics"
import "ludic.base"
program JoltHeapTest {
numbers float
function cycle(physics_st: mut PhysicsState, h: []float, k: int) -> void {
let s = phys_heightfield(physics_st, h, 33, float(k % 7) * 64.0, 0.0, 2.0)
let b = phys_ground_add(physics_st, s)
phys_step(physics_st)
phys_remove(physics_st, b)
phys_shape_free(physics_st, s)
phys_settle(physics_st)
}
test "a ground in and out a thousand times holds Jolt's heap where it settled, and closing gives it back" (physics_st: mut PhysicsState) {
expect(phys_open(physics_st, 64, 1))
phys_close(physics_st)
let before = phys_heap_bytes() # the factory and the types, made once per process
expect(phys_open(physics_st, 64, 1))
expect(phys_heap_bytes() > before)
let h = floats(33 * 33)
for k in 0 .. 100 { cycle(physics_st, h, k) }
let held = phys_heap_bytes()
let peak = phys_heap_peak()
for k in 0 .. 1000 { cycle(physics_st, h, k) }
print(`jolt heap: {held} bytes held after 100 grounds, {phys_heap_bytes()} after 1100, peak {phys_heap_peak()}, {phys_heap_allocs()} blocks asked for`)
expect(phys_heap_bytes() == held)
expect(phys_heap_peak() == peak)
phys_close(physics_st)
print(`jolt heap: {before} bytes before the world, {phys_heap_bytes()} after it closed`)
expect(phys_heap_bytes() == before)
}
}