# Physics

Hands-on walkthroughs: [Tutorials — First physics](/docs/tutorials/first-physics.md), [First character controller](/docs/tutorials/first-character-controller.md).

## Architecture

Physics is a backend-neutral engine service. `src/hikari/src/physics/backend.zig` defines body, collider, query, collision, and material contracts. `physics/tenkai3d_backend.zig` is the current middleware adapter; engine and scene code must not import Tenkai types directly.

```text
Entity PhysicsComponent / PhysicsProxy
  → PhysicsSubsystem
  → PhysicsBackend contract
  → Tenkai3D adapter
  → Tenkai3D middleware
```

This boundary keeps a future middleware replacement local to an adapter rather than spreading vendor types through world and entity code.

## Scene integration

Every entity has a transform. Entities opt into physics by listing `hi.ComponentPhysics` on `defineActor` and authoring `"components": { "physics": { … } }` in scene JSON (`com.hikari.scene` version 1). The component associates an actor with body/collider descriptions and a `PhysicsProxy`; render scale remains independent of collider dimensions. Runtime code can also add/remove `hi.ComponentPhysics` through the [component API](/docs/guides/gameplay-api.md#runtime-component-membership).

Each Physics attachment has a World-monotonic identity. Create snapshots/replies
and the 64-byte simulation proxy carry it, so removing and re-adding Physics on
the same actor cannot accept an old body assignment or pose. Rejected assignments
release their body even when the component row no longer exists. Component
mutation pauses the step thread before taking World locks and uses the normal
resource-release queue.

**Enable.** `PhysicsComponent.is_active` (scene `physics.is_active`, default `true`) is the component switch. Simulation uses **effective** enable: entity active **and** `is_active` (see [Scenes and gameplay — Active / enable trio](/docs/systems/scenes-and-gameplay.md#active--enable-trio)). When effective is off, the subsystem does not create or sync the body and releases any existing Tenkai handle so re-enable can recreate it. Host `setVelocity` / forces / impulse / torque no-op when not effective. Overlap queries skip inactive components. Game API: `hi.world().setPhysicsActive` / `isPhysicsActive` (authored switch only).

**Two pause strengths** (do not mix them up):

| Scope | Mechanism | Used by |
|-------|-----------|---------|
| Additive streaming + short edits | Cooperative `ScenePhysicsBarrier` (`World.ScenePhysicsMutationScope` / `scene_ops.parkPhysics`) | Layer unload, additive spawn pumps, inspector mutations. Parks *between* steps; keeps the 16 MB thread. **Barrier before any world lock.** |
| Wholesale world invalidation | Hard `PhysicsSubsystem.stop` / `start` (job instantiate window) | Full replace `SceneLoadJob` (tick or sync drain). Joins the step thread; `start()` rebuilds body maps. |

Hard stop joins the step thread for wholesale world invalidation (`stop()` / `start()`). The step hot path never takes the scene-graph world lock — create/release/transform/force/CCT intents cross a sealed SPSC command ring. Streaming must not pay a hard join; a cell crossing would otherwise spend tens of milliseconds on the join alone.

**Lock order (enforced in debug):** never wait on `ScenePhysicsBarrier` while holding `World.lockExclusive` / `lockShared`. Acquire the barrier first, then the world lock. Debug builds track world-lock depth in TLS (`debug/lock_order.zig`) and panic on inversion; ReleaseFast/ReleaseSmall compile the checks out.

The physics step loop paces at 60 Hz through `ScenePhysicsBarrier.paceUntil`: one high-resolution OS wait to (deadline − spin) plus a spin tail. Crucially the pace holds `at_safe_point` for the entire wait, so a game-thread `beginMutation` acquires the barrier without waiting on the OS sleep. A solid sleep that dropped the safe point would leave `at_safe_point` false for almost a full physics frame; pacing with the safe point held avoids that class of cooperative-barrier wait. Hierarchy transform propagation still runs before physics/render consumers need updated world transforms.

The step honours **effective** time scale (`getEffectiveTimeScale`: host `sim_hold` wins over game `time_scale`): `0` skips integration outright and paces the full period (same fast-path as `gameplay_suspended`); values > 0 scale the wall dt fed into the accumulator. Wall dt is **clamped to ≤ 0.1 s** before scale (engine step thread and Tenkai `step_simulation` both clamp) so a hitch cannot death-spiral via substep escalation. Simulated time is spent in whole `step_dt` chunks from an accumulator (up to 4 catch-up steps, abandoned once the frame budget is spent). UI and `TickContext.unscaled_dt` stay on the real clock.

### Application suspension

Application suspension (macOS resign-active / Windows deactivate) holds the active World's simulation before the frontend stops gameplay ticks. `SessionCore.noteForegroundSuspend` waits for the existing physics safe point, then applies the host hold; editor Play and standalone use the same path. The worker remains available for scene mutations, but does not integrate bodies or repeat character intent while gameplay is frozen. Editor Pause and game `time_scale` are preserved independently. Resume finishes residency preparation and clears the foreground hold; the host clock drops the suspended span.

`SimulationHold` carries a transition epoch as well as the held bit. Physics drops its accumulator and rebases its pace/measurement clock on the first resumed iteration, including a complete suspend/resume cycle missed while the OS had the worker asleep. A boolean alone can miss that cycle and feed background time into catch-up. Reapplying an unchanged hold does not change the epoch. Background execution requires both gameplay and physics to continue; stopping only gameplay leaves controllers applying stale velocity indefinitely.

### Threading (command ring)

```text
Game thread                              Physics thread
───────────                              ──────────────
mutate PhysicsComponent (dirty/queues)
publishPhysicsCommands() ──flush──►      drain PhysicsCommandQueue
  lifecycle: create / release / joints     create/release under pool
  mutate: transform / velocity /           apply transforms/forces
          force / torque / CCT
drainPhysicsReplies() ◄──flush──         step (dt ≤ 0.1) → characterMove
  handle assign / CCT flags /              publish ResultBuffer + QuerySnapshotBuffer
  collision events                         enqueue PhysicsReply
```

- **Inbound:** `PhysicsCommandQueue` — two rings (lifecycle vs per-tick mutate) sealed/drained as one pair. Same SPSC discipline as `RenderCommandQueue` (lock-free enqueue, one mutex seal, one mutex drain). Game publishes once per tick (after purge) and after `endScenePhysicsMutation` / physics `start()`.
- **Outbound proxies / queries:** unchanged SPSC triple-buffers (`PhysicsResultBuffer`, `QuerySnapshotBuffer`). Game-thread body ↔ entity mapping resolves through the published snapshot rows (`resolveBody` / `bodyIdOf`); the pool's tables belong to the physics thread and are never read from the game thread. Collision replies carry their component index for the same reason.
- **Outbound component writes:** `PhysicsReplyQueue` for handle assignment, CCT grounded flags, and collision events — physics never takes the scene-graph world lock on the step hot path. Collision event lists are game-thread only (`drainPhysicsReplies` → `dispatchCollisionEvents`).
- **Roster:** physics owns `active_bodies` from create/release commands (bootstrapped once on hard `start()` from live handles).

Proxy publish is sparse: unchanged sleepers/statics skip `writeAt`, and after priming also skip `getBodyState` until poked (teleport / force / torque / CCT) or a staggered sleeper revalidate (every 8 physics ticks, lane = `store_index % 8`). Query snapshots still fill from the peeked proxy when polling is skipped. The per-slot skip bit clears on body release and on attach of a new body at that `store_index` so recreate / disable→enable never inherits a previous tenant's primed skip.

## Backend responsibilities

The backend contract includes body/collider lifetime, transform synchronization, forces/torques, locks, raycasts, and collision data. The subsystem owns the adapter instance and translates world operations into that contract. Collision delivery reaches entity behavior through `onCollision` when applicable.

## Physics diagnostics

Build with `--profiler-physics` (`-Dprofiler-physics`) to populate the editor's right-dock **Physics** tab. Kaji defines `TENKAI_ENABLE_DIAGNOSTICS` on the Tenkai build from this flag. Independent of CPU profiler Recording (`--profiler-timing`), `--profiler-audio`, and `--profiler-residency`; defaults off. Without it, the tab remains present and explains which flag is required.

Diagnostics preserve physics-thread ownership: the physics thread asks the optional backend-neutral diagnostic vtable for a fixed snapshot at 5 Hz, merges engine resource-pool counters, and publishes one synchronized copy. The editor only reads that copy; it never traverses a live middleware world, takes the world lock, or allocates on the physics thread. The sampler, publisher storage, backend call, and live-memory counter operations compile out when the flag is disabled.

The tab reports live/dynamic/static/kinematic bodies, awake versus sleeping dynamics, characters, collider shapes and sensors, contact pairs/points/events, collision-buffer use/overflow, body-table occupancy, and cooked/runtime collision resources. Under **Resources**, runtime triangle meshes break down as:

| Field | Meaning |
|-------|---------|
| **Runtime collision meshes** | Pool-tracked generated meshes (any lifetime state) |
| **Attach pins** | Sum of `setCollisionMesh` pins waiting for body create / component teardown — usually **0** in steady state |
| **Creator refs held** | Meshes whose create-side reference was not yet released — usually **0** after the create → set → release pattern |

A rising pin or creator count over a session points at remesh outrunning physics sync, missing `releaseCollisionMesh`, or actors destroyed mid-flight. Colliders still referencing geometry after a clean release are owned by Tenkai and are **not** double-counted here.

Hikari's physics allocator reports exact requested live/peak bytes. Middleware native memory is shown only when the backend declares it exact: Kaji `--profiler-physics` enables `TENKAI_ENABLE_DIAGNOSTICS` on the Tenkai build; **exact** native bytes require Tenkai compiled with `ODIN_DEBUG` (release + flag alone → unavailable / non-exact, not a fabricated estimate).

Future physics backends implement the same optional snapshot contract in their adapter. Backend-specific structures and handles never cross into editor code.

## Material and shapes

The engine exposes physics material and combine-mode types through `hikari_api.zig`, along with body/collider descriptions and collider-shape definitions. Keep authored API additions as scene/physics data, not backend-only knobs. Add the corresponding document/world leaf updates when a property is authorable in the editor.

### Compound colliders and mass properties

Tenkai bodies may own multiple colliders. Each collider has a body-local position and rotation; its world pose is cached when the body pose changes. The broadphase keeps one union AABB per body, while narrowphase contacts and persistent manifolds are keyed per collider pair. This keeps resident-grid and pair-generation cost body-scaled without merging distinct child contacts.

`PhysicsBackend.ColliderDesc` exposes `local_position`, `local_rotation`, and relative `density`. Scene physics may author `colliders`: the first child stays inline in `PhysicsComponent`, while only the compound tail allocates. Each child owns shape or cooked collision, local pose, density, material, sensor state, category and mask. Body creation attaches the complete set transactionally before publishing the handle.

```json
{
  "physics": {
    "body_type": "Dynamic",
    "mass": 3.0,
    "colliders": [
      { "shape": "Box", "box_size": [1.5, 0.5, 1.0], "local_position": [-0.5, 0, 0], "density": 2.0 },
      { "shape": "Hull", "collision": "asset://./models/crate", "local_position": [0.75, 0, 0], "local_rotation_euler": [0, 30, 0], "material": "wood" }
    ]
  }
}
```

For dynamic compounds, the body `mass` is the requested total mass. Tenkai distributes it by each child's `density × volume`, computes the body-local center of mass, rotates each child tensor into body axes, then applies the parallel-axis theorem. Box, sphere and capsule properties are analytic; cooked hulls use Shinra's exact polyhedral moments. Linear velocity is the center-of-mass velocity; the published body transform remains the authored body origin.

Bodies may still provide `BodyDesc.mass_properties` (or C API `tenkai_set_body_mass_properties`) as an expert override: body-local COM plus a symmetric positive-definite inertia tensor about that COM. Runtime-created hulls without cooked moments require this path for dynamic use. Planes and triangle meshes remain static/kinematic collision. `tenkai_reset_body_mass_properties` returns a body to automatic primitive/cooked-hull compound calculation.

### Contact policy and events

Contact response is collider-owned and evaluated once while preparing a manifold; there is no game callback in the solver hot path. The primary collider and every compound child support:

- `contact_response_enabled`: detect and report the contact without applying impulses or position correction when false.
- `one_way`, `one_way_normal`, `one_way_slop`: accept contacts only on the collider-local solid side while the other body approaches that side. The default normal is local +Y.
- `surface_velocity`: collider-local conveyor velocity. The friction solver targets the relative surface velocity without moving a static body.
- `friction_override` / `restitution_override`: optional coefficients applied before the pair's normal combine-mode rule.

The same fields are available in scene JSON, `SpawnPhysics`, `PhysicsComponentView` / `PhysicsComponentUpdate`, the backend-neutral `ColliderDesc`, and `tenkai_set_collider_contact_policy`. Runtime component updates recreate the affected body at the normal physics safe point; they never mutate Tenkai from the game thread.

Tenkai owns contact lifecycle per collider pair. The fixed collision ring publishes exactly one `begin` or `persist` event per touching pair per simulation tick and one `end` when it separates or is removed. Sleeping pairs remain `persist` rather than producing a false exit. Events are finalized after the velocity solve and include the representative deepest point, normal, penetration, and total normal/tangent impulse across the manifold. Hikari forwards those phases directly to `onCollision` as `enter` / `stay` / `exit`; it no longer rebuilds lifecycle with game-thread contact hash maps. An exit retains the last geometry and reports zero impulses.

### Triangle mesh colliders (static)

**Content cook (preferred):** Shinra emits `.shincollision` (`mode: mesh`) from glTF. Scene/physics set `collision` to a **extension-free asset URI** (e.g. `asset://./models/level`); the physics pool loads once and uploads to Tenkai.

**Direct API:** `hi.world().createCollisionMesh(vertices, indices)`, then `setCollisionMesh` (below). Prefer static/kinematic bodies.

**Runtime (generated) collision:** for geometry the game builds — voxel chunks, carved terrain — prefer the budgeted unit so visual and collision stay in lockstep:

```zig
var geom = hi.RuntimeGeometry.init(allocator, .{ .stride = hi.vertex_layout.stride });
defer geom.deinit();
geom.bind(chunk_actor);

geom.markDirty();
geom.beginBuild();
// fill geom.mesh + geom.col_positions / col_indices (workers OK)
geom.finishBuild();

var budget = hi.GeometryApplyBudget.init(8);
_ = geom.tryApply(&budget); // create → set (pin) → release creator
// Many actors: hi.RuntimeGeometry.applyBatch(&.{ &a, &b }, &budget)
```

Low-level path (same ownership rules, open-coded):

```zig
const mesh = try hi.world().createCollisionMesh(verts, indices);
// Pin is taken by setCollisionMesh so release is safe before the physics thread
// builds the body on the next sync.
hi.world().setCollisionMesh(chunk_actor, mesh);
hi.world().releaseCollisionMesh(mesh); // creator reference
```

The backend mesh and hull entry points take tightly packed `[3]f32` vertices: cooked `.shincollision` vertices are already in that layout (scaled in place), and `createCollisionMesh` packs the game's `@Vector(3, f32)` input once. Do not pass a raw SIMD vector buffer to the C ABI yourself — on this target each `Vector3` is 16 bytes with padding, while Tenkai expects 12-byte vertices.

**Lifetime.** Collision meshes and hulls are refcounted in Tenkai. Create returns a **creator reference**; each collider referencing the resource holds another. `setCollisionMesh` **pins** the mesh for the component's reference lifetime (body create also takes a short-lived snapshot pin). Entity teardown drops the component pin via `World.resource_pool` (wired from `PhysicsSubsystem` — not a module-global). That makes create → set → release correct even though body create is deferred one sync. After the body exists, the collider's Tenkai reference keeps the geometry alive.

| Holder | Taken by | Dropped by |
|--------|----------|------------|
| Creator | `tenkai_create_triangle_mesh` / `tenkai_create_convex_hull` | `tenkai_release_*`, `PhysicsResourcePool.releaseRuntimeCollisionMesh`, `World.releaseCollisionMesh` |
| Collider | `tenkai_create_mesh_collider` / `tenkai_create_hull_collider` | collider removal (including via `remove_body`) |

Over-release is a no-op at every layer (zero/unknown ids are ignored, and the pool only forwards ids it issued), so a double release cannot double free live geometry. `PhysicsResourcePool` tracks runtime meshes and reclaims any still outstanding at teardown with a warning — that path means a chunk was dropped without releasing its collision, which grows unbounded over a session.

Cooked `.shincollision` resources are cached by path, and the cache holds their creator reference until pool teardown, so a cached id cannot dangle after its last collider dies.

Verified by `src/tenkai3d/examples/mesh_lifetime` (`odin run examples/mesh_lifetime` from `src/tenkai3d`): unattached release, release-while-attached, sharing across two colliders, defensive double release, 256 remesh cycles ending at zero resident meshes, plus the same pattern for convex hulls. Shape casts, convex TOI CCD, and mesh seams: `examples/ccd`.

Create APIs return a **creator reference**; callers must `tenkai_release_*` (or `World.releaseCollisionMesh`) after attach. Missing release leaks until world destroy (bounded, not a crash).

**Chunking:** Prefer static mesh world AABBs with longest edge ≲ ~64 m (Tenkai `GRID_RECOMMENDED_CHUNK_METERS`). Larger single slabs use the spatial tree. Chunking still improves mesh BVH locality and streaming; stream/split level collision with the scene.

**Box rooms:** six slabs must overlap at every edge (side walls span the full inner height *and* depth). Knife-edge corners let a sphere leave both AABBs and never get another wall contact. Tenkai projects denser piles out of statics more aggressively than dynamic–dynamic stacks; do not rely on a game-thread teleport to keep a sealed room closed.

Narrowphase: sphere / capsule / box vs mesh; raycast; CCT. Mesh–mesh skipped. Mesh cooking records indexed-triangle adjacency; contacts and casts replace radial internal-edge normals with the shared smooth surface normal. Boundary, hard, and non-manifold edges remain active. Author collision meshes with shared indices and consistent outward winding.

### Broadphase and queries (Tenkai)

Regular body pairs use an incremental fat-proxy uniform grid (worlds currently select **3 m** cells; the standalone grid helper defaults to **1.5 m**). Every finite resident also has one leaf in a balanced AABB tree. Oversized movers query all finite leaves; ordinary movers query only subtrees containing oversized leaves. Active-pair ownership prevents duplicates and includes sleeping ordinary bodies touched by an oversized mover. Candidate storage follows emitted overlaps, without a quadratic reservation for oversized residents. Proxies exceeding the motion-specific span limit or **512 grid cells** bypass cell insertion. Infinite planes remain in a separate list.

Raycasts, shape casts, overlaps, CCD candidate queries, and character-neighbor queries traverse the tree. Each tree query visits a finite resident once; planes occupy a disjoint roster. Traversal needs no deduplication map, query stamps, scratch allocation, or grid-volume walk; long rays have no grid-step cutoff. Mesh rays and sweeps additionally descend intersected mesh BVH leaves. Final penetration recovery queries only static subtrees, avoiding repeated visits to dynamic neighbors and duplicate grid cells. Corrected poses refresh proxies before sleep, so subsequent queries see current positions. Tree node storage is recycled and follows peak residency; static proxies remain resident until changed. Tree membership cannot change inside a visitor or during worker dispatch.

The unified query contract is `QueryFilter` + `QueryHit` with three collectors: `any` exits on its first accepted hit, `closest` keeps one best hit, and `all` fills the caller's buffer. Filters cover collision category/mask, dynamic/static/kinematic motion, sensor inclusion, ignored body, and ignored collider. Body and collider filters run before narrow phase. Hits identify body, collider, and mesh triangle (`subshape_id`) where available, plus fraction, distance, position, normal, and overlap penetration.

Supported procedural shapes are sphere, oriented box, capsule, and registered convex hull. `queryRaycast`, `queryShapeCast`, and `queryOverlap` are allocation-free from the game SDK through Tenkai: the caller's `[]PhysicsQueryHit` storage is reused through every layer, with body ids translated in place to generation-safe actor handles. The `all` collector returns at most `out.len`; use a deliberately bounded gameplay buffer. The sphere/box convenience overlaps are `queryOverlap` with the `all` collector and sensors included; a body's colliders report back to back and map to one actor. Hull rays advance against the support map within finite hull bounds, so hit distance and face normal do not depend on the requested ray range; zero range means unlimited.

### Convex hull colliders (static + dynamic)

**Content cook:** Shinra `mode: convex` validates a closed convex triangle surface. `mode: convex_decomposition` runs bounded V-HACD offline for dynamic concave props. SCC3 stores exact unit-density volume, COM and inertia per hull; Hikari expands compounds into ordinary colliders, so Tenkai's existing mass aggregation, casts, and CCD apply without a compound-only solver path. Same `collision` scene field.

Hull ids come from cooked collision; there is no game-facing runtime hull creation.

Scene-authored cooked dynamic hulls need no manual body override. Explicit `mass_properties` remains available when gameplay intentionally needs a custom mass distribution. `inertia` is the full tensor about `center_of_mass`; both are authored before actor scale:

```json
{
  "mass": 1.5,
  "mass_properties": {
    "center_of_mass": [0, 0, 0],
    "inertia": [[0.6, 0, 0], [0, 0.6, 0], [0, 0, 0.6]]
  }
}
```

Changing `mass` or `mass_properties` rebuilds the body so the live solver state cannot retain stale inertia. Static and kinematic bodies ignore the override.

Narrowphase: GJK/EPA (hull–hull, prim–hull), hull–plane, hull–mesh. Translation casts use conservative advancement over the same convex support maps.

## Motion types

`PhysicsComponent.BodyType` maps to Tenkai motion types through the adapter:

| BodyType | Motion | Notes |
|----------|--------|-------|
| Dynamic | `TENKAI_MOTION_DYNAMIC` | Finite mass; forces and gravity apply |
| Static | `TENKAI_MOTION_STATIC` | Infinite mass; never integrates |
| Kinematic | `TENKAI_MOTION_KINEMATIC` | Infinite mass; integrates scripted velocity each substep (platforms) |
| Trigger | Static + sensor collider | Overlap events only |

Kinematic bodies use `CollisionCategory.KINEMATIC`. Setting velocity on a kinematic wakes contacting dynamics.

## Shape casts, CCD, and characters

`tenkai_shape_cast` translates a box, sphere, capsule, or convex-hull collider from an explicit world pose. It returns the nearest body and collider, normalized travel fraction, distance, target-surface point, and outward normal. The query is allocation-free, honors collider filters, traverses the spatial tree, and visits only swept mesh BVH leaves.

Simulation uses the same casts for true linear CCD. Each substep solves every moving body's velocity while collider poses remain at the start pose, then casts with the solved velocity before any pose moves. A dynamic convex body casts when its substep travel reaches its smallest support radius—the point at which it can skip completely across a surface. Casts use pair-relative displacement and pose integration is clamped to the earliest time of impact. Moving-vs-moving pairs are covered without a rewind pass. A tiny post-TOI advance creates an ordinary shallow manifold so the normal solver removes closing velocity. Shorter and angular motion stays on the cheaper discrete/speculative path. Speculative contacts are made only for pairs that are closing, within their approach distance this substep plus a 2 mm pad. A near neighbour moving apart gets no contact row. Discrete and speculative contact normals both point from body A to body B; query normals point outward from the target. CCD and final static recovery honor contact response, one-way policy, sensor flags, collision filters, and joint collision suppression. Disabled response never clamps motion.

Capsule character controllers are exposed on physics actors via `is_character_controller` (plus optional `max_slope_deg` / `step_offset`). Enabling the flag forces a capsule collider and kinematic body. Physics thread order each tick:

1. **Drain gameplay commands** (`set_transform`, `character_intent`, forces) into Tenkai
2. **Step** (kinematics integrate, including platforms)
3. **`characterMove`** against the post-command pose
4. **Publish proxies** / query snapshot / collision buffer

`linear_velocity` is player **intent** only (pushed every publish as `character_intent`). When grounded, Tenkai adds the support body's surface velocity (point velocity) so elevators/conveyors carry the character — do **not** re-add `platform_velocity` in game code (double-apply). After each move, `character_grounded` / `character_hit_ceiling` / `character_ground_normal` / `character_platform_velocity` update on the component; `physicsState().platform_velocity` is observational (FX/debug). Jump frames (`desired_velocity.y > 0.01`) skip ground snap and clear support after one-frame ride inherit. Character bodies skip kinematic position integration. Movement, sliding, step clearance, and ground snapping sweep the complete capsule. Those sweeps and penetration recovery share simulation response policy; sensors and disabled contacts do not block movement. Step descent can probe the full step offset, while normal ground snapping uses the short support probe.

**Why commands before move:** pose writes (teleports, face-walk `setRotationEuler`) mark `position_dirty` / `rotation_dirty` and enqueue `set_transform`. Applying that *after* `characterMove` rewinds the body to the pre-move component pose and cancels the slide — the player looks frozen while intent still updates. Commands first, move second, proxies last is the only order that keeps teleports and CCT motion coherent.

**CCT × triggers/sensors:** Characters are infinite-mass kinematics. Pair generation still tests pairs that include a character body so contacts reach the collision buffer (without that exception, static/sensor × CCT would be dropped as “both inactive”). After `characterMove`, Tenkai also reports **sensor** overlaps into the buffer so enter/stay events fire the same frame the controller steps into a volume. Sensors never depenetrate the CCT (overlap only). Game code uses normal `onCollision` on Trigger actors — no proximity hacks.

Linear CCD covers box/sphere/capsule/hull movers against convex, plane, and triangle-mesh targets. Pure rotational sweeps are conservative only through the existing speculative path; translation casts keep orientation fixed over the cast.

Game modules: `WorldApi` (`hi.world()`) — `setVelocity` / `velocity`, `physicsState`, `setPhysicsActive` / `isPhysicsActive`, `physicsComponentState` / `updatePhysicsComponent`, filtered `queryRaycast` / `queryShapeCast` / `queryOverlap`, `raycast`, forces/impulse/torque, and `overlapSphere` / `overlapBox`.

Unified queries and the sphere/box convenience overlaps take a shared simulation read lock, which may wait for an in-progress step, and return exact live collider/triangle results. Convenience overlaps return `!usize`, report unavailable/stopped physics and backend failures, and inspect at most `min(out.len, 64)` collider hits before mapping active owners into the caller’s output. An empty output slice returns zero without querying. They never substitute component bounds or a snapshot for a failed query.

The legacy raycast remains snapshot-first: each physics tick publishes a POD query snapshot (`QuerySnapshotBuffer`) with a reserved, per-slot spatial BVH; the raycast avoids waiting on a mid-step Tenkai lock and refines only when `sim_lock.tryLockShared` succeeds. Snapshot publication refits the BVH without allocation; membership changes and every 64th slot publication rebuild balanced Morton order. Rays traverse nearer bounds first and shorten their search after a hit; the snapshot raycast skips a caller's own body by handle. Each slot also keeps body-id and handle lookup tables, rebuilt only with the BVH. Planes remain unbounded, and conservative bounds preserve the existing shape tests. Mesh/hull in the snapshot use a coarse world AABB.

Component updates cover body/collider kind, mass, sleep and sensor policy, dimensions, filters, velocities, and axis locks. World gravity is launch-time `PhysicsConfig.gravity` (`configs/game.json` → `physics`); it is not a live component field. Kawa writes CCT intent the same way Zig does — `Physics.update({ linear_velocity = … })` / `hi.world().setVelocity` — see [First character controller](/docs/tutorials/first-character-controller.md).

## Tenkai C / Zig API surface

Tenkai’s public contract is `src/tenkai3d/include/tenkai3d.h`. Hikari consumes a **subset** through `src/hikari/src/physics/tenkai3d/internal.zig`, the only file that sees the C types; `tenkai3d/types.zig` aliases the backend-neutral vocabulary, and `tenkai3d_backend.zig` owns the world and library refcount and calls `internal` directly. Game/scene code stays on the backend-neutral contract; only the adapter may call `tenkai_*`. Engine path: `createBody` + `createCollider` + `attach` (no convenience spawn chain).

## Body lifetime and solver scheduling

Tenkai body handles remain 64-bit at the C and Zig boundaries. The low 32 bits select a reusable slot and the high 32 bits identify its generation. Deletion removes contacts, joints, and grid membership before releasing the slot. Reuse increments the generation; an exhausted generation retires its slot instead of aliasing an old handle. Treat handles as opaque values. Slot/pool capacity follows peak simultaneous residency, while per-substep union-find storage and clearing follow the current active body count. A long spawn/delete session therefore does not accumulate historical-ID iteration cost.

Awake non-static bodies have explicit active membership, maintained on creation, wake/sleep, motion changes, and pending deletion. Contacts and joints enter active rosters when either endpoint is active. Static and sleeping residents remain in the spatial index and retain graph adjacency, but no longer participate in repeated integration, CCD, contact preparation, solver-island, or sleep scans. At synchronization points, changed rosters produce work snapshots sorted by existing resident order; this preserves solver ordering while worker iteration and sleep transitions remain safe. Joint wake propagation finishes before assigning compact solver indices. Snapshot and union-find capacity follows peak activity.

Sleeping contact pairs still emit the existing per-tick `persist` events. Event delivery therefore retains work proportional to the number of reported pairs; the active optimization removes repeated substep solver/maintenance scans, not the event contract. Static transform changes publish their spatial proxies immediately. Grid roster removal uses stored slots, and periodic pruning visits only cells queued when they became empty instead of scanning all resident cells.

Each substep runs in this order: integrate forces into velocities, detect contacts at the current poses, warm start and solve velocities, run the position pass, then CCD and pose integration with the solved velocities. Because poses move only after the solve, a resting contact cancels gravity's approach before anything moves. Contact and joint impulses warm start at full strength from the previous substep. Tall stacks and jointed chains depend on that carried support.

After the substep, a slow body in contact has its remaining velocity zeroed only when its speed dropped over the substep, so contacts are removing its motion. A body gaining speed is starting to slide, roll or fall, and keeps its velocity even while it is below the sleep threshold. This lets islands reach exact rest for sleep without pinning bodies on slopes.

### Mass-ratio islands

Sequential impulse moves a heavy body's load through a light one by roughly the mass ratio per iteration, so a light box under a tonne, or a light chain carrying a heavy load, never converges. An island whose dynamic masses span 50:1 or more, and that has at most 1,024 such rows, also gets a direct solve (`solver_direct.odin`). Its joint equality rows (point, angle and hinge-swing locks, rigid distance) and its loaded contact normals are assembled into A = J M⁻¹ Jᵀ. A is factored once per substep with a sparse LDLᵀ under a minimum-degree ordering, which is linear time for chains and ragdoll trees. Every sequential-impulse iteration ends with one exact back-substitution. The rows share the sequential-impulse accumulators, so warm starts, reaction forces and joint breakage are unchanged, while friction, limits, motors and springs stay iterative.

Drift is corrected in a second metric that bounds the mass ratio at 10:1. Joints are then corrected after pose integration by a Levenberg–Marquardt projection, relinearized at the current poses and accepted only if it lowers the worst joint error. A single linear correction linearized at the start of the substep fails here: to remove millimetres of stretch from a nearly straight chain under a heavy load, the linear optimum folds the light links by tenths of a radian, and the fold adds more drift than it removes. Contact rows carry a 1% relative diagonal so that a face's four coplanar points share their load instead of splitting it into huge opposite impulses. Islands beyond the row budget, such as a jointed net or a large pile under a heavy body, stay on sequential impulse.

Static penetration recovery moves a body back out to the solver's slop depth (1 mm), not to exact touch, and leaves its velocity alone. The next substep's contact solve owns approach velocity: removing it during recovery would erase restitution, and an exactly touching round-versus-mesh pair produces no contact at all.

### Solver data path

For the velocity phase, each substep copies the velocities of its awake bodies into a compact solver-body array (32 bytes per body; slot 0 is an immovable body with zero velocity) and writes the dynamic ones back afterwards. Contacts, joints and the direct solve read and write only this array during that window. Masses, inertia and poses come from the bodies and do not change within the substep.

Constraints are colored so that no dynamic body appears twice in a color. There are 24 colors, as in Box2D v3: constraints between two dynamic bodies take the first 20, constraints with one immovable side start at color 1, and the last color is overflow. Contacts of one color are packed four manifolds to a block, one manifold per SIMD lane, with each manifold's points stacked lane by lane. Within a color, manifolds are ordered by point count so that a block's lanes carry similar work. This is safe because lanes of one color are independent. A block loads its bodies' velocities once, solves every point (normal, then both friction directions, point by point), and stores the velocities once. Accumulators stay in the block for the whole velocity phase and are copied back to the manifolds at its end. Overflow blocks run one lane at a time, which is exactly sequential impulse.

Joints have their own color graph and are solved color by color before the contacts in every iteration, so contacts keep the last word on penetration. A color's joints run in parallel. An island with fewer than 64 joints (a chain, a ragdoll) instead solves them sequentially in creation order. Color order on a chain is red-black, so one iteration propagates only half as far, and a 30:1 chain's error doubled under it. Parallel joint colors only pay off for large joint sets such as a net. Colors are assigned incrementally when a manifold or joint is created, and rebuilt when an endpoint changes motion type.

Preparation runs on the workers as well. The main thread only copies the solver bodies and sizes the island buffers. Color ordering, block layout, effective masses, the restitution reference and warm starts happen per island on whichever thread owns that island. The only exception is the direct solve's factorization, which allocates and so stays on the main thread. Joint warm starts land before contact preparation captures each contact's pre-solve normal velocity, and contact warm starts follow it.

Small islands are assigned whole to the worker pool. An island with 1,024 or more rows (contact points, plus four per joint) uses a resident team for its prepare, velocity and position phases, with one OS wake/join per phase. The team meets at a barrier after each non-empty color. Within a color, each participant takes a contiguous range; overflow stays with the leader. Stage order and arithmetic are the same on one thread and on the team, so results do not depend on the worker count.

Each pool worker has its own start semaphore. Dispatch and world mutation must remain serialized across worlds because the pool is process-global; read-only static/kinematic solver bodies may be shared within a dispatch. Large-island scheduling deliberately caps the participating team and keeps small workloads local to avoid synchronization overhead.

## Automated physics gate

Run `python3 src/tenkai3d/tools/test_physics.py --soak` from the repository root (Windows: `python`). Python 3 and the product Odin toolchain must be available; `--odin=<path>` or `ODIN` selects the compiler. The macOS and Windows product smoke matrices run this gate before their product builds and stop on a failure.

The gate compiles the regression, query/contact contracts, API, shape, CCD, joint, resource-lifetime, character-rest, and residency harnesses with optimization, debug memory tracking, finite-state checks, and island/color invariants. New harnesses cover 100,000 body/collider lifetimes and stale handles, invalid creation, a 1000:1 supported mass-ratio fixture, repeated sleep/wake with support replacement, fast angular motion, and a seeded connected box pile. The contracts fixture covers simplex witness preservation, hull ray range and normals, translated hull overlaps, capsule corner separation, speculative normal orientation, contact-policy sweeps, joint suppression, character steps/ceilings/platforms, and observational state reads. Body-state queries reject non-finite poses, velocities, and derived energy or momentum without repairing or changing simulation poses. The angular test exercises the existing discrete/speculative path; it does not establish rotational time-of-impact coverage.

The activity fixture audits roster/backlink invariants, complete sleep/wake transitions, frozen contact events, force/velocity activation, static teleports, motion changes, joint-chain wake closure, and body-slot reuse. Checked builds enable `TENKAI_VERIFY_ACTIVITY`; these full-residency audits compile out of production stepping.

The streaming fixture holds 500 dynamic props awake while scaling from 10,000 to 100,000 static bodies. It replaces 64 cold tiles per step at new coordinates, checks immediate queries and stale handles, verifies unchanged active contacts/containment, and drains retired grid cells. Three runs at each size report steady-step and streaming-plus-mutation mean/p99 times. A relative gate limits growth with fixed activity; it does not impose a universal hardware budget. This is a middleware boundary benchmark, separate from the Hikari scene-loader/barrier cost.

The spatial fixture checks randomized tree updates, height/bounds/filter invariants, bounded node reuse, brute-force pair and query equivalence, motion changes, planes, huge queries, and distant rays. Oversized fixtures run 512, 2,000, and 8,000 separated 200 m slabs with 128 active props; they verify expected contacts/rays and bound candidate capacity independently of resident count.

The connected pile runs three times under both serial and parallel large-island scheduling. Quantized final-state digests must match across repeats and configurations on the same host, including a run with all worker threads disabled. The gate compares median p95 step times and fails if parallel scheduling is over 10% slower than its serial control. `--max-p95-ms=<budget>` adds a hardware-specific absolute budget. This is a regression guard, not a promise that arbitrary dense piles fit a 60 Hz frame. The optional soak simulates another seed for 60 seconds. Sphere containment runs cover 600 and 2,000 moving bodies.

The solver-limit fixture (`examples/solver_limits`, optimized build, product substeps of 4 × 1/240 s) recreates the stress scenes from Augmented Vertex Block Descent (Giles, Diaz, Yuksel, SIGGRAPH 2025). It gates behavior Tenkai guarantees today:

- A 340-brick, 40-course wall kept awake stands for 30 s with no brick moving 10 cm. With sleep enabled it is asleep within 2 s and moves less than 2 cm.
- A 0.7 m ball of 43 kg to 10 t, falling at 45 m/s onto a 1 m slab, penetrates less than 5 cm and comes to rest.
- A 1 kg box under a box 10 to 10,000 times heavier sinks less than 2 cm and sleeps.
- A 50-link ball-jointed chain with an end mass of up to 1,000 × a link keeps joint error under 0.1 m, never gains energy, and swings through the bottom of its arc.
- Sliding stopping distances are within 10% of Coulomb. A μ 0.45 box on a 30° ramp slides within 10% of Coulomb, whether pushed or starting from rest, and μ 0.7 holds.
- A 500 kg ball at 30 m/s breaks through a 250-brick wall without launching bricks faster than 1.5× its own speed or pinching any brick more than 10 cm into the slab.
- A wall welded by 2,000 N breakable fixed joints holds its own weight, and a 200 kg ball breaks some welds but not the wall.

Metrics prefixed `limit_` measure what Tenkai does not do yet. They are reported, not gated, so solver work has a baseline. Both are beyond the direct solver's reach (see *Mass-ratio islands*). Values measured on 2026-09-25:

| Metric | Current | Physical target |
|---|---|---|
| `limit_pendulum_50000_joint_error` | 2.2 m (the chain holds for 2 s, then tears) | < 0.1 m |
| `limit_net_ball_held` / `limit_net_joint_error` | 0 / 5.0 m (a 1,600 kg ball tears through a 30 × 30 net of 0.1 kg plates) | 1 / < 0.1 m |

Logs, compiler identity, commands, measurements, and the final pass/fail result are written to `build/reports/tenkai3d-tests/report.json` and adjacent logs (`--output-dir` overrides the directory). Reports live outside the disposable build cache so product clean builds retain them. Any harness failure, crash, timeout, reported allocator leak/bad free, state mismatch, or configured performance violation returns a nonzero exit code. Cross-platform bitwise determinism is not asserted.

## Phase measurements

Run `python3 src/tenkai3d/tools/profile_physics.py` for three repeated optimized runs of the seeded 1,024-box pile, the existing 2,000-sphere ballpit, the 6,400-static residency scene, the oversized fixtures, and the 10k/100k streaming fixtures. `--repeats`, `--oversized-counts`, `--output-dir`, and `--odin` select the workload and report location. `--binary-prefix` can measure previously saved instrumented binaries for a local before/after comparison.

`TENKAI_PROFILE=true` records monotonic wall-clock phase durations per simulation step, accumulated over its substeps: integration, CCD, grid maintenance, broadphase, narrowphase, contact maintenance, preparation, velocity solve, position solve, static penetration recovery, and final maintenance. It also counts candidates, hits, prepared rows, actual overflow-color rows, and broadphase tree-node visits. The preparation phase includes island construction, caches, and warm starting. Parallel phase times include dispatch and synchronization, rather than summed worker CPU time. Total includes top-level work outside individual phases; phase percentiles are independent and must not be added together.

The report contains per-run mean/p50/p95 values and their medians, fixture parameters, counters, and raw logs in `build/reports/tenkai3d-phases/current/`. Run comparisons on the same idle host with the same compiler and flags. Collection is disabled by default and does not change the public C ABI. These fixture measurements establish costs for their stated workloads; they are not a general engine frame-rate guarantee.

## Verification and replacement

For physics work, validate that the Zig declarations still match Tenkai3D's ABI and build the relevant product. A new SDK should implement the same engine-owned backend contract and register via the registry; it must not leak its native handles into the game API. When changing `tenkai_*` symbols, update `tenkai3d.h`, Odin `exports_*.odin`, Zig `@cImport` consumers, and `examples/` in the same change.
