Ymir/docs/soa-world.md
Nibu ea7b3f3d7f soa-world doc: rolling resistance in the descriptor
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 04:08:18 +00:00

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_body takes 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, parent and limb (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) and torque, xyz, mutable through the view. step applies 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_joint takes 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) and joint_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): Box3DSoaView exposes each lane as a span over native memory (ReadOnlySpan for state, Span for write lanes). Spans are invalid after Dispose. Count and JointCount are 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.