physics: Jolt's floor sized for what moves - pairs 4096, contacts 2048 and a 4 MB temp heap by default (phys_open_sized for more), and a body past max_bodies or a step past the pairs or contacts is Mem.over, never a quiet -1; world.ludic split from shapes.ludic
An empty world at the game's size held 51839 KB of Jolt's heap (200000 bodies, 65536 pairs, 20480 contacts, a 32 MB temp heap), the jolt= floor under every walk. Pairs and contacts come from what moves - drops, boats, a few walkers - and a valley's still things make none. Now 11743 KB at 200000 bodies and 8461 KB at the 98304 the game opens for (jolt_floor_test holds it under 16 MB). The temp heap still falls back to malloc for a step that wants more. lib/macos-arm64 rebuilt; lib/windows-x64 needs native/build.sh on the PC (jph_world_new's new sizes). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
031fcad641
commit
b4af12025d
11 changed files with 95 additions and 52 deletions
|
|
@ -16,7 +16,10 @@ export property PhysPose {
|
|||
function ph_add(physics_st: mut PhysicsState, shape: int, x: float, y: float, z: float, yaw: float, motion: int, layer: int, mass: float) -> int {
|
||||
let s = ph_shape(physics_st, shape)
|
||||
if physics_st.ph_world == null or s == null { return -1 }
|
||||
return jph_body_add(physics_st.ph_world, s, x, y, z, yaw, motion, layer, mass, 0.6, 0.1)
|
||||
let id = jph_body_add(physics_st.ph_world, s, x, y, z, yaw, motion, layer, mass, 0.6, 0.1)
|
||||
# 25.5a: a world full to the bodies it was opened for is a failure, never a thing quietly not there
|
||||
if id < 0 and jph_world_bodies(physics_st.ph_world) >= physics_st.ph_max { Mem.over("a physics body past phys_open's max_bodies") }
|
||||
return id
|
||||
}
|
||||
|
||||
# the ground: a heightfield or a mesh at the world's origin
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import "state.ludic"
|
|||
import "fact_pool.ludic"
|
||||
import "shape_ids.ludic"
|
||||
import "owned.ludic"
|
||||
import "world.ludic"
|
||||
import "shapes.ludic"
|
||||
import "bodies.ludic"
|
||||
import "queries.ludic"
|
||||
|
|
|
|||
BIN
packages/ludic.physics/lib/macos-arm64/libludicjolt.dylib
(Stored with Git LFS)
BIN
packages/ludic.physics/lib/macos-arm64/libludicjolt.dylib
(Stored with Git LFS)
Binary file not shown.
|
|
@ -1,6 +1,6 @@
|
|||
# native.ludic - the shim's symbols (native/shim/jph_shim.cpp). A world and a shape are handles
|
||||
# the package keeps; a body is an int id. None of this is exported.
|
||||
extern function jph_world_new(max_bodies: int, threads: int) -> pointer = "jph_world_new"
|
||||
extern function jph_world_new(max_bodies: int, threads: int, max_pairs: int, max_contacts: int, temp_kb: int) -> pointer = "jph_world_new"
|
||||
extern function jph_world_free(w: pointer) -> void = "jph_world_free"
|
||||
extern function jph_world_gravity(w: pointer, y: float) -> void = "jph_world_gravity"
|
||||
extern function jph_world_step(w: pointer, dt: float, sub: int) -> int = "jph_world_step"
|
||||
|
|
|
|||
|
|
@ -93,11 +93,11 @@ public:
|
|||
|
||||
struct World {
|
||||
// pairs and contacts are for what MOVES: a valley of sixty thousand still trunks makes none
|
||||
// between them, so these are Jolt's own sizes, not the body count's; the temporary memory falls
|
||||
// back to the heap rather than aborting a step
|
||||
World(int max_bodies, int threads)
|
||||
: temp(32 * 1024 * 1024), jobs(cMaxPhysicsJobs, cMaxPhysicsBarriers, threads) {
|
||||
sys.Init(max_bodies, 0, 65536, 20480, bpl, ovb, olp);
|
||||
// between them, so they are sized for the moving things, not the body count; the temporary memory
|
||||
// falls back to the heap rather than aborting a step. All four are the package's (phys_open)
|
||||
World(int max_bodies, int threads, int max_pairs, int max_contacts, int temp_kb)
|
||||
: temp(size_t(temp_kb) * 1024), jobs(cMaxPhysicsJobs, cMaxPhysicsBarriers, threads) {
|
||||
sys.Init(max_bodies, 0, max_pairs, max_contacts, bpl, ovb, olp);
|
||||
sys.SetContactListener(&contacts);
|
||||
}
|
||||
TempAllocatorImplWithMallocFallback temp;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
// jph_world.inl - a physics world: made, stepped, emptied of its contacts, freed
|
||||
|
||||
// a world for up to max_bodies; threads 0 means the machine's cores less one, at most four
|
||||
JPH_SHIM void *jph_world_new(int max_bodies, int threads) {
|
||||
// a world for up to max_bodies, max_pairs touching pairs and max_contacts contact constraints, with a
|
||||
// temp heap of temp_kb; threads 0 means the machine's cores less one, at most four
|
||||
JPH_SHIM void *jph_world_new(int max_bodies, int threads, int max_pairs, int max_contacts, int temp_kb) {
|
||||
if (jph_init() != 0) return nullptr;
|
||||
if (threads <= 0) {
|
||||
int hw = int(std::thread::hardware_concurrency()) - 1;
|
||||
threads = hw < 1 ? 1 : (hw > 4 ? 4 : hw);
|
||||
}
|
||||
World *w = new World(max_bodies, threads);
|
||||
World *w = new World(max_bodies, threads, max_pairs, max_contacts, temp_kb);
|
||||
w->sys.SetGravity(Vec3(0.0f, -9.81f, 0.0f));
|
||||
return w;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,43 +1,5 @@
|
|||
# shapes.ludic - the world opened and closed, and shapes: made in C, kept here, named by an int.
|
||||
# A shape may be used by any number of bodies; phys_close frees them all.
|
||||
|
||||
# a world for up to max_bodies (threads 0: the cores less one, at most four); false if C refused
|
||||
export function phys_open(physics_st: mut PhysicsState, max_bodies: int, threads: int) -> bool {
|
||||
phys_close(physics_st)
|
||||
ph_buffers()
|
||||
physics_st.ph_world = jph_world_new(max_bodies, threads)
|
||||
ph_own_reset(physics_st, max_bodies)
|
||||
physics_st.ph_gen += 1
|
||||
physics_st.ph_acc = 0.0
|
||||
physics_st.ph_steps = 0
|
||||
return physics_st.ph_world != null
|
||||
}
|
||||
|
||||
export function phys_is_open(physics_st: PhysicsState) -> bool { return physics_st.ph_world != null }
|
||||
# which world this is: a body, shape or walker id kept from an older one means nothing now
|
||||
export function phys_generation(physics_st: PhysicsState) -> int { return physics_st.ph_gen }
|
||||
|
||||
# everything let go: the world, every body in it and every shape (a world swap, a quit); the lists
|
||||
# that held them are emptied and kept, since nothing is given back
|
||||
export function phys_close(physics_st: mut PhysicsState) -> void {
|
||||
if physics_st.ph_walkers != null {
|
||||
for i in 0 .. len(physics_st.ph_walkers) {
|
||||
if physics_st.ph_walkers[i] != null { jph_char_free(physics_st.ph_walkers[i]) }
|
||||
}
|
||||
}
|
||||
if physics_st.ph_walkers != null { List.clear(physics_st.ph_walkers) }
|
||||
if physics_st.ph_world != null { jph_world_free(physics_st.ph_world) }
|
||||
physics_st.ph_world = null
|
||||
if physics_st.ph_shapes != null {
|
||||
for i in 0 .. len(physics_st.ph_shapes) {
|
||||
if physics_st.ph_shapes[i] != null { jph_shape_free(physics_st.ph_shapes[i]) }
|
||||
}
|
||||
}
|
||||
if physics_st.ph_shapes != null { List.clear(physics_st.ph_shapes) }
|
||||
List.clear(physics_st.ph_spare)
|
||||
physics_st.ph_nf = 0
|
||||
if physics_st.ph_facts != null { q_clear(physics_st.ph_facts) }
|
||||
}
|
||||
# shapes.ludic - shapes: made in C, kept here, named by an int. A shape may be used by any number of
|
||||
# bodies; phys_close (world.ludic) frees them all.
|
||||
|
||||
# a crate: half extents in metres
|
||||
@creates(PhysShape)
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ export state PhysicsState {
|
|||
ph_step: float = 0.016666668 # seconds a step
|
||||
ph_sub: int = 1 # collision steps in each
|
||||
ph_max_steps: int = 4 # a slow frame catches up this far and no further
|
||||
ph_max: int = 0 # the bodies the world was opened for: one more is a failure, not a -1
|
||||
ph_acc: float = 0.0
|
||||
ph_steps: int = 0
|
||||
ph_gen: int = 0 # how many worlds have been opened: an id from an older one is stale
|
||||
|
|
@ -75,3 +76,9 @@ export function phys_config(physics_st: mut PhysicsState, step: float, sub: int,
|
|||
physics_st.ph_hard = hard
|
||||
}
|
||||
function ph_facts__new() -> Queue<PhysFact> { return queue_new("physics.facts") }
|
||||
|
||||
# what moves, sized: touching pairs, contact constraints and the step's temp heap (KB). A valley's still
|
||||
# things make no pairs between them; drops, boats and the walkers do. Past a size a step fails loudly
|
||||
export const PH_PAIRS: int = 4096
|
||||
export const PH_CONTACTS: int = 2048
|
||||
export const PH_TEMP_KB: int = 4096
|
||||
|
|
|
|||
|
|
@ -23,7 +23,8 @@ export function phys_step(physics_st: mut PhysicsState) -> void {
|
|||
if physics_st.ph_world == null { return }
|
||||
ph_buffers()
|
||||
ph_buoy_all(physics_st)
|
||||
jph_world_step(physics_st.ph_world, physics_st.ph_step, physics_st.ph_sub)
|
||||
# 25.5a: Jolt's pairs or contacts full (PH_PAIRS, PH_CONTACTS) is a failure: past it things pass through
|
||||
if jph_world_step(physics_st.ph_world, physics_st.ph_step, physics_st.ph_sub) != 0 { Mem.over("a physics step past phys_open's pairs or contacts") }
|
||||
physics_st.ph_steps += 1
|
||||
ph_contacts(physics_st)
|
||||
}
|
||||
|
|
|
|||
25
packages/ludic.physics/tests/jolt_floor_test.ludic
Normal file
25
packages/ludic.physics/tests/jolt_floor_test.ludic
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# jolt_floor_test.ludic - what an empty world costs in Jolt's own heap at the game's size (plan 25):
|
||||
# the floor under every body, from the body table, the broad phase, the contact caches and the temp heap
|
||||
import "ludic.physics"
|
||||
import "ludic.base"
|
||||
program JoltFloorTest {
|
||||
numbers float
|
||||
function floor_at(physics_st: mut PhysicsState, bodies: int) -> long {
|
||||
expect(phys_open(physics_st, 64, 1))
|
||||
phys_close(physics_st)
|
||||
let before = phys_heap_bytes()
|
||||
expect(phys_open(physics_st, bodies, 0))
|
||||
let f = phys_heap_bytes() - before
|
||||
phys_close(physics_st)
|
||||
return f
|
||||
}
|
||||
|
||||
# it was 51839 KB at 200000 bodies with 65536 pairs, 20480 contacts and a 32 MB temp heap
|
||||
test "an empty world at the game's size costs under 16 MB" (physics_st: mut PhysicsState) {
|
||||
let a = floor_at(physics_st, 200000)
|
||||
let b = floor_at(physics_st, 98304)
|
||||
print(`jolt floor: {a / long(1024)} KB at 200000 bodies, {b / long(1024)} KB at 98304`)
|
||||
expect(b < long(16 * 1024 * 1024))
|
||||
expect(a > b)
|
||||
}
|
||||
}
|
||||
43
packages/ludic.physics/world.ludic
Normal file
43
packages/ludic.physics/world.ludic
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# world.ludic - the world opened and closed: its sizes, and everything in it let go at once
|
||||
|
||||
# a world for up to max_bodies (threads 0: the cores less one, at most four); false if C refused.
|
||||
# Pairs, contacts and the temp heap are sized for the things that MOVE (PH_PAIRS, ...)
|
||||
export function phys_open(physics_st: mut PhysicsState, max_bodies: int, threads: int) -> bool { return phys_open_sized(physics_st, max_bodies, threads, PH_PAIRS, PH_CONTACTS, PH_TEMP_KB) }
|
||||
# the same with Jolt's other three sizes given: touching pairs, contact constraints, the temp heap in KB
|
||||
export function phys_open_sized(physics_st: mut PhysicsState, max_bodies: int, threads: int, max_pairs: int, max_contacts: int, temp_kb: int) -> bool {
|
||||
phys_close(physics_st)
|
||||
ph_buffers()
|
||||
physics_st.ph_world = jph_world_new(max_bodies, threads, max_pairs, max_contacts, temp_kb)
|
||||
physics_st.ph_max = max_bodies
|
||||
ph_own_reset(physics_st, max_bodies)
|
||||
physics_st.ph_gen += 1
|
||||
physics_st.ph_acc = 0.0
|
||||
physics_st.ph_steps = 0
|
||||
return physics_st.ph_world != null
|
||||
}
|
||||
|
||||
export function phys_is_open(physics_st: PhysicsState) -> bool { return physics_st.ph_world != null }
|
||||
# which world this is: a body, shape or walker id kept from an older one means nothing now
|
||||
export function phys_generation(physics_st: PhysicsState) -> int { return physics_st.ph_gen }
|
||||
|
||||
# everything let go: the world, every body in it and every shape (a world swap, a quit); the lists
|
||||
# that held them are emptied and kept, since nothing is given back
|
||||
export function phys_close(physics_st: mut PhysicsState) -> void {
|
||||
if physics_st.ph_walkers != null {
|
||||
for i in 0 .. len(physics_st.ph_walkers) {
|
||||
if physics_st.ph_walkers[i] != null { jph_char_free(physics_st.ph_walkers[i]) }
|
||||
}
|
||||
}
|
||||
if physics_st.ph_walkers != null { List.clear(physics_st.ph_walkers) }
|
||||
if physics_st.ph_world != null { jph_world_free(physics_st.ph_world) }
|
||||
physics_st.ph_world = null
|
||||
if physics_st.ph_shapes != null {
|
||||
for i in 0 .. len(physics_st.ph_shapes) {
|
||||
if physics_st.ph_shapes[i] != null { jph_shape_free(physics_st.ph_shapes[i]) }
|
||||
}
|
||||
}
|
||||
if physics_st.ph_shapes != null { List.clear(physics_st.ph_shapes) }
|
||||
List.clear(physics_st.ph_spare)
|
||||
physics_st.ph_nf = 0
|
||||
if physics_st.ph_facts != null { q_clear(physics_st.ph_facts) }
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue