4 KiB
Headless SoA world
A headless 3D Box3D world with a structure-of-arrays state view, for Lodur and Bad Day Simulator. Box3D decides every physical outcome; Ymir creates bodies, steps the world and exposes state.
Why it is not the retained session
ymir_box3d_session_* (ymir_box3d.c) is the planar Aetheria session: gravity is zero, Y is locked,
only yaw rotates (motionLocks.linearY/angularX/angularZ), and state leaves by copy_bodies into an
array of structs. A body cannot fall or tumble there, and every read is a copy. The SoA world is a
separate native object (ymir_box3d_soa.c, ABI 1) so the Aetheria session's contract is untouched.
Contract
- Native (ABI 2):
ymir_box3d_soa_create(capacity, joint_capacity, gravity) / add_body / add_joint / step / get_view / destroy.add_bodytakes one descriptor (sphere, capsule, box, static box; position, rotation, velocity, size, density, friction, restitution, rolling resistance, parent, limb). Capacities are fixed at creation, so lane pointers never move. - Body lanes (
ymir_box3d_soa_view):stable_id,kind,parentandlimb(set at creation, not read by Box3D: the limb tree for the consumer), position xyz, rotation quaternion xyzw, linear and angular velocity xyz. One contiguous array per component, indexed by body. - Write lanes: per-body world
force(at the centre of mass) andtorque, xyz, mutable through the view.stepapplies them to the dynamic bodies and clears them, so a lane holds one step's intent. A non-finite value refuses the whole step before any body is touched. - Joints:
add_jointtakes two body indices (B is the child), a world anchor and axis, and a kind (revolute, spherical, weld). The joint frame has its z axis on the axis (the hinge axis); the creation pose is the joint's zero. Optional limits: revolute angle, spherical swing cone and twist. Joint lanes:joint_kind,joint_body_a/b,joint_angle(Box3D's revolute angle or spherical twist),joint_frame(world orientation of frame A, quaternion),joint_rotation(frame B relative to frame A, quaternion, w >= 0) andjoint_angular_velocity(B minus A, in frame A). A consumer can compute a joint torque from these and write it as equal and opposite torques on the two body lanes. - State lanes change only inside
step. Between steps a caller may read any state lane in place. - Managed (
src/Ymir.Box3D/Box3DSoaWorld.cs, internal):Box3DSoaViewexposes each lane as a span over native memory (ReadOnlySpanfor state,Spanfor write lanes). Spans are invalid afterDispose.CountandJointCountare as of when the view was taken, so take it after the last add. - Determinism: one Box3D worker, bodies stepped in insertion order, same inputs give identical lane bytes on the same build. It is not claimed across platforms or CPUs.
- Concurrency: creating Box3D worlds from several threads at once aborts the process (seen in tests); create worlds one at a time.
Tests
tests/Ymir.Box3D.Soa.Tests, run serially: gravity and rest read through one held view, address stability
across steps, two runs byte-equal at six checkpoints (25 spheres colliding on a slab), a one-ulp input
difference diverging, argument refusal, dispose; and for joints: hinge angle and the relative-rotation
lanes agreeing, an off-axis hinge, a limit and a weld, a spherical joint leaving the swing plane, force and
torque lanes (exact acceleration, cleared after the step, static bodies ignoring them, NaN refusing the
step), capsule and box rest heights with parent and limb lanes, refusals, and bit-identical repeat runs of
a torque-driven jointed body.
Not done
Contact facts for the SoA world (Begin/Hit/End), body removal with a stable ordering rule, compound bodies
and per-shape material, joint motors and springs (Lodur drives joints from torque lanes instead), and a
public typed surface in Ymir.Core (the wrapper is internal, visible to the test assembly). Windows build
and Unity consumption are unrun; cross-platform bit-identity is unclaimed.