diff --git a/packages/ludic.physics/bodies.ludic b/packages/ludic.physics/bodies.ludic index a3d4fa94..519803ab 100644 --- a/packages/ludic.physics/bodies.ludic +++ b/packages/ludic.physics/bodies.ludic @@ -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 diff --git a/packages/ludic.physics/index.ludic b/packages/ludic.physics/index.ludic index 4de40a96..7e39e3ba 100644 --- a/packages/ludic.physics/index.ludic +++ b/packages/ludic.physics/index.ludic @@ -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" diff --git a/packages/ludic.physics/lib/macos-arm64/libludicjolt.dylib b/packages/ludic.physics/lib/macos-arm64/libludicjolt.dylib index 10610d51..18dd4b42 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:5b79fd1c0c2c8d7d5225ef4251d4e64fb454283a972cf2b16e68a83b953daf65 +oid sha256:8237112881ad63698c7c1b32c45597b50a1e8f8f0aa4e99298ed2f8979f63c0a size 1161504 diff --git a/packages/ludic.physics/native.ludic b/packages/ludic.physics/native.ludic index 775751e2..f1e13c12 100644 --- a/packages/ludic.physics/native.ludic +++ b/packages/ludic.physics/native.ludic @@ -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" diff --git a/packages/ludic.physics/native/shim/jph_shim.cpp b/packages/ludic.physics/native/shim/jph_shim.cpp index b12f54f4..92f09a7e 100644 --- a/packages/ludic.physics/native/shim/jph_shim.cpp +++ b/packages/ludic.physics/native/shim/jph_shim.cpp @@ -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; diff --git a/packages/ludic.physics/native/shim/jph_world.inl b/packages/ludic.physics/native/shim/jph_world.inl index 41e2253b..348f523f 100644 --- a/packages/ludic.physics/native/shim/jph_world.inl +++ b/packages/ludic.physics/native/shim/jph_world.inl @@ -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; } diff --git a/packages/ludic.physics/shapes.ludic b/packages/ludic.physics/shapes.ludic index 0820c2b2..c599ac18 100644 --- a/packages/ludic.physics/shapes.ludic +++ b/packages/ludic.physics/shapes.ludic @@ -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) diff --git a/packages/ludic.physics/state.ludic b/packages/ludic.physics/state.ludic index 44cf4664..8800767b 100644 --- a/packages/ludic.physics/state.ludic +++ b/packages/ludic.physics/state.ludic @@ -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 { 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 diff --git a/packages/ludic.physics/system.ludic b/packages/ludic.physics/system.ludic index c2a06a95..facf9d67 100644 --- a/packages/ludic.physics/system.ludic +++ b/packages/ludic.physics/system.ludic @@ -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) } diff --git a/packages/ludic.physics/tests/jolt_floor_test.ludic b/packages/ludic.physics/tests/jolt_floor_test.ludic new file mode 100644 index 00000000..71541f16 --- /dev/null +++ b/packages/ludic.physics/tests/jolt_floor_test.ludic @@ -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) + } +} diff --git a/packages/ludic.physics/world.ludic b/packages/ludic.physics/world.ludic new file mode 100644 index 00000000..d7d83ee6 --- /dev/null +++ b/packages/ludic.physics/world.ludic @@ -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) } +}