Skip to content
hikari
RenderingEditorAIToolchainDocumentation
GitHub
hikari

© 2026 Flying Rat Studio.
All rights reserved.

Explore the engineDocumentationContributorsLicenseBack to top
Documentation / Systems
Browse docs
Overview
Tutorials16
OverviewFirst game projectFirst entityAuthored user_data and the inspectorFirst mesh and materialFirst physics body and triggerFirst character controllerFirst session servicesFirst UIFirst input actionFirst messagesPlay, Edit, and scenesFirst runtime spawnFirst motionFirst skeletal animationAssets in Play (soft refs and hot reload)Dynamic editor recompile
Guides16
OverviewDevelopment guideGameplay APIChoosing component storageBuild and packagingProject filePluginsHikari Plugin APIData-driven content, JSON, and pathsScripting with KawaShader authoringTime of dayUser interfaceUI layoutUI widgetsMigration from Unity / UnrealActor and component lifecycle
Systems28
OverviewArchitectureApplication lifecycleFrontends and driversPlatforms and supportSession services and cross-scene stateScenes and gameplayActor communication (hi.actors)Game-facing refsSave / replication wire versionCoordinate space and camera conventionsRenderingRenderer architecture mapFrame governorGPU particlesVisual ZonesVolumetric mediaInputAudioPhysicsMotion KitTemporal KitAssets and ShinraPrefabsAsset residencyAsset formats (Shinra pipeline)UI and editorEditor asset hot reloadEditor Project Selector
Language reference2
OverviewAkari language referenceKawa language reference
Engine overview
Start exploring
  • No matching sections. Try fewer words or another topic.
NavigateEnter Openesc Close
Overview
Tutorials16
OverviewFirst game projectFirst entityAuthored user_data and the inspectorFirst mesh and materialFirst physics body and triggerFirst character controllerFirst session servicesFirst UIFirst input actionFirst messagesPlay, Edit, and scenesFirst runtime spawnFirst motionFirst skeletal animationAssets in Play (soft refs and hot reload)Dynamic editor recompile
Guides16
OverviewDevelopment guideGameplay APIChoosing component storageBuild and packagingProject filePluginsHikari Plugin APIData-driven content, JSON, and pathsScripting with KawaShader authoringTime of dayUser interfaceUI layoutUI widgetsMigration from Unity / UnrealActor and component lifecycle
Systems28
OverviewArchitectureApplication lifecycleFrontends and driversPlatforms and supportSession services and cross-scene stateScenes and gameplayActor communication (hi.actors)Game-facing refsSave / replication wire versionCoordinate space and camera conventionsRenderingRenderer architecture mapFrame governorGPU particlesVisual ZonesVolumetric mediaInputAudioPhysicsMotion KitTemporal KitAssets and ShinraPrefabsAsset residencyAsset formats (Shinra pipeline)UI and editorEditor asset hot reloadEditor Project Selector
Language reference2
OverviewAkari language referenceKawa language reference
Engine overview
Systems20 min read

Physics

On this page
On this pageArchitectureScene integrationThreading (command ring)Backend responsibilitiesPhysics diagnosticsMaterial and shapesCompound colliders and mass propertiesContact policy and eventsTriangle mesh colliders (static)Broadphase and queries (Tenkai)Convex hull colliders (static + dynamic)Motion typesShape casts, CCD, and charactersTenkai C / Zig API surfaceBody lifetime and solver schedulingAutomated physics gatePhase measurementsVerification and replacement Back to top

Hands-on walkthroughs: Tutorials — First physics, First character controller.

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.

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). 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):

ScopeMechanismUsed by
Additive streaming + short editsCooperative 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 invalidationHard 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 cross used to burn ~20 ms twice on 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. Earlier revisions used chunked ~1 ms sleeps that re-entered the barrier because a solid 16 ms sleep otherwise left at_safe_point false for almost a full physics frame — pacing-with-safe-point removes that class of 10–15 ms cooperative-barrier waits. 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.

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 queries never read body_id_to_handle.
  • 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:

FieldMeaning
Runtime collision meshesPool-tracked generated meshes (any lifetime state)
Attach pinsSum of setCollisionMesh pins waiting for body create / component teardown — usually 0 in steady state
Creator refs heldMeshes 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 content-root URI (e.g. asset://./models/level.shincollision); the physics pool loads once and uploads to Tenkai.

Direct API: createTriangleMesh(vertices, indices) → MeshID, then shape .mesh / setMeshCollider. 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 = 12 });
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

createCollisionMesh / cooked .shincollision upload pack @Vector(3, f32) into tightly packed xyz for Tenkai. 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.

HolderTaken byDropped by
Creatortenkai_create_triangle_mesh / tenkai_create_convex_hulltenkai_release_*, PhysicsResourcePool.releaseRuntimeCollisionMesh, World.releaseCollisionMesh
Collidertenkai_create_mesh_collider / tenkai_create_hull_collidercollider 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. Traversal needs no 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.

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.

Direct API: createConvexHull(vertices) → HullID (min 4, already convex). Geometry-only runtime hulls remain valid for static/kinematic use; dynamic use needs a body override because no source topology exists from which to derive exact mass.

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:

BodyTypeMotionNotes
DynamicTENKAI_MOTION_DYNAMICFinite mass; forces and gravity apply
StaticTENKAI_MOTION_STATICInfinite mass; never integrates
KinematicTENKAI_MOTION_KINEMATICInfinite mass; integrates scripted velocity each substep (platforms)
TriggerStatic + sensor colliderOverlap 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, walks the uniform grid, and visits only swept mesh BVH leaves.

Simulation uses the same casts for true linear CCD. Each substep first finalizes every moving body's velocity while collider poses remain at the start pose. 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.

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.

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, legacy raycast, forces/impulse/torque, and legacy overlapSphere / overlapBox. Unified queries take a shared simulation read lock and return exact live collider/triangle results. Legacy ray/overlap calls remain snapshot-first: each physics tick publishes a POD query snapshot (QuerySnapshotBuffer); they avoid waiting on a mid-step Tenkai lock and refine only when sim_lock.tryLockShared succeeds. 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.

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 → Tenkai3D → tenkai3d_backend.zig). Game/scene code stays on the backend-neutral contract; only the adapter may call tenkai_*. Engine path: createBody + createCollider + attach (no convenience spawn chain). Tenkai2D is not wired into this 3D path.

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 dense body count. A long spawn/delete session therefore does not accumulate historical-ID iteration cost.

Small islands are assigned whole to the worker pool. Large contact islands use a resident team for each velocity or position phase, with one OS wake/join per phase. Stable color buckets separate manifolds that cannot write the same dynamic body. Velocity tasks own up to 64 complete manifolds and solve all their contact-point waves locally before the next color barrier; position tasks use the same color independence. Shared-body overflow and joint constraints remain sequential. This preserves Gauss–Seidel dependencies between colors without waking threads for every contact wave. The serial control uses the same prepared layout and arithmetic.

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/test.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 existing regression, 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 angular test exercises the existing discrete/speculative path; it does not establish rotational time-of-impact coverage.

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.

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/profile.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, and the oversized 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.

PreviousAudioNext Motion Kit

Documentation follows the current engine checkout.

Snapshot cc148c75Source docs/systems/physics.md
On this pageArchitectureScene integrationThreading (command ring)Backend responsibilitiesPhysics diagnosticsMaterial and shapesCompound colliders and mass propertiesContact policy and eventsTriangle mesh colliders (static)Broadphase and queries (Tenkai)Convex hull colliders (static + dynamic)Motion typesShape casts, CCD, and charactersTenkai C / Zig API surfaceBody lifetime and solver schedulingAutomated physics gatePhase measurementsVerification and replacement Back to top