diff --git a/packages/ludic.physics/lib/macos-arm64/libludicjolt.dylib b/packages/ludic.physics/lib/macos-arm64/libludicjolt.dylib index 36c149dd..10610d51 100755 --- a/packages/ludic.physics/lib/macos-arm64/libludicjolt.dylib +++ b/packages/ludic.physics/lib/macos-arm64/libludicjolt.dylib @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b87a8d51bcc30d4479d60d28456cc6cc529820f90281feac9b69d420e0ccb623 -size 1161296 +oid sha256:5b79fd1c0c2c8d7d5225ef4251d4e64fb454283a972cf2b16e68a83b953daf65 +size 1161504 diff --git a/packages/ludic.physics/native.ludic b/packages/ludic.physics/native.ludic index 5f47798d..775751e2 100644 --- a/packages/ludic.physics/native.ludic +++ b/packages/ludic.physics/native.ludic @@ -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() } diff --git a/packages/ludic.physics/native/shim/jph_alloc.inl b/packages/ludic.physics/native/shim/jph_alloc.inl new file mode 100644 index 00000000..59ce8a8c --- /dev/null +++ b/packages/ludic.physics/native/shim/jph_alloc.inl @@ -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 +#include +#include + +namespace { +constexpr size_t kHead = 16; +std::atomic 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(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(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(p) - kHead; + size_t old; + std::memcpy(&old, b, sizeof old); + char *q = static_cast(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(heap_alloc(n + align + sizeof(void *))); + if (!raw) return nullptr; + uintptr_t at = (reinterpret_cast(raw) + sizeof(void *) + align - 1) & ~(uintptr_t(align) - 1); + reinterpret_cast(at)[-1] = raw; + return reinterpret_cast(at); +} +void heap_aligned_free(void *p) { + if (p) heap_free(static_cast(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); } diff --git a/packages/ludic.physics/native/shim/jph_shim.cpp b/packages/ludic.physics/native/shim/jph_shim.cpp index ab3f5da9..b12f54f4 100644 --- a/packages/ludic.physics/native/shim/jph_shim.cpp +++ b/packages/ludic.physics/native/shim/jph_shim.cpp @@ -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; diff --git a/packages/ludic.physics/tests/jolt_heap_test.ludic b/packages/ludic.physics/tests/jolt_heap_test.ludic new file mode 100644 index 00000000..14879497 --- /dev/null +++ b/packages/ludic.physics/tests/jolt_heap_test.ludic @@ -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) + } +}