> Cinder 0.1.0 documentation: ADR-0012: Phase 6 subsystems: physics, animation, audio and input
> Canonical page: https://cinder.ash10.dev/docs/adr/0012-physics-animation-audio-input/ · Source: `docs/adr/0012-physics-animation-audio-input.md` in the engine repository (engine commit 47c9352, imported 2026-11-09)
> Index of every page for agents: https://cinder.ash10.dev/llms.txt

# ADR-0012: Phase 6 subsystems: physics, animation, audio and input

Status: accepted, 2026-11-09.

## Context

Phase 6 adds the gameplay subsystems over the pinned dependencies: Jolt (joltc) for physics,
miniaudio for audio, SDL3 events for input, and the engine's own animation evaluation (the brief
asks for SIMD Mojo on the job system). Every decision below was taken against the rules of
ADR-0003 (Mojo never runs inside a C callback) and section 10 (reuse a pinned dependency before
writing a subsystem).

## Decisions

### Physics (`cinder/physics`)

1. **Jolt keeps its own thread pool.** `JPH_PhysicsSystem_Update` runs on a
   `JPH_JobSystemThreadPool` created per world (2 workers by default) instead of the engine's job
   system. joltc's job system cannot be driven by an external scheduler without callbacks into
   Mojo, which ADR-0003 forbids; Jolt's pool is idle outside the step so the two systems do not
   contend. Revisit if profiling (Phase 8) shows the step starving render jobs.
2. **Object layers are bits.** Up to 32 layers; query masks are 32-bit; two broad-phase layers
   (static, moving) keep static geometry from being tested against itself. Collision pairs are
   declared once in `LayerConfig`.
3. **All callbacks are C.** Query filters (`cn_physics_*_filter_*`), the closest-hit collectors
   for rays and shape casts (`cn_physics_cast_ray`, `cn_physics_cast_shape`) and the contact and
   activation listeners live in `native/src/physics_shim.c` / `physics_events.c`. Results are
   returned through plain structs or the event queue and read on the calling thread.
4. **Character controller = CharacterVirtual.** Position is at the feet (the capsule is offset
   through `shapeOffset`), only the lower hemisphere supports the character, ground velocity is
   inherited (moving platforms), `ExtendedUpdate` walks stairs up to `step_height` and sticks to
   the floor. Pushing dynamic bodies uses Jolt's `maxStrength`.
5. **ECS sync keeps two poses.** `RigidBody` stores the previous and current simulated pose;
   `write_transforms(alpha)` blends them for rendering between fixed steps. Kinematic bodies
   follow their entity's `LocalTransform` through `MoveKinematic` (so they carry riders); dynamic
   bodies drive the entity; static bodies are left alone.
6. **Leak accounting without aborts.** The world counts shapes, bodies and characters;
   `live_objects()` must be zero before shutdown (tests assert it). The destructor does not abort,
   so a failing test reports its real assertion rather than the leak that follows it.
7. **Determinism is Jolt's.** Jolt's deterministic mode is on by default; the test replays a
   recorded input stream (impulses at given steps) into two fresh worlds and asserts bit-identical
   positions for 72 bodies, and that a different stream diverges.

### Animation (`cinder/anim`)

1. **Skeleton = joints plus ancestors.** `skeleton_from_gltf` includes every skin joint and the
   ancestors needed to reach a common root, in parent-before-child order, so animated parents
   move their children. The palette is `inverse(mesh node) * bone world * inverse bind`, the same
   convention the importer uses at the bind pose (tested against it), so the render instance
   keeps the mesh node's transform.
2. **Clips stay glTF-shaped.** `Clip`/`ClipChannel` (node, path, interpolation, components,
   times, values) are the cooked `.cclip` format; sampling implements step, linear (slerp for
   rotations) and cubic spline exactly as the glTF specification describes. A cooked `.cskel`
   (CSKL v1) carries the skeleton; the cooker emits one per skin.
3. **Blend trees run on a normalised phase.** Leaves sample at `phase * duration`, so blended
   cycles stay in step; the phase advances at the weighted average of the leaves' cycle rates.
   1D nodes interpolate between the two nearest thresholds; 2D nodes use gradient-band
   interpolation (smooth, sums to one, exact at each child's point).
4. **Crossfades blend whole poses.** `play(state, fade)` keeps the outgoing state running and
   blends pose to pose with a linear weight; translations and scales lerp, rotations nlerp along
   the shorter arc.
5. **Root motion and events are animator features.** Root motion removes the root bone's
   horizontal translation from the pose and accumulates it (loop wraps are unwrapped exactly,
   including several cycles in one step). Events are authored per state at a phase and fire once
   per crossing. glTF has neither, so they are not part of the cooked format.
6. **One job per animator.** `AnimationSystem.update` submits each playing animator as a job;
   jobs touch only their own animator and the results are bit-identical to serial evaluation
   (tested). The system publishes `JointPalette` components that `build_render_scene` turns into
   skinning matrices and morph weights.

### Audio (`cinder/audio`)

1. **miniaudio's engine, node graph and resource manager are used as they are.** Buses are
   `ma_sound_group`s (allocated in the shim because the struct is opaque to Mojo); streamed
   sounds use `MA_SOUND_FLAG_STREAM`, everything else is decoded up front.
2. **Headless engines for tests.** `noDevice` engines are pulled with `ma_engine_read_pcm_frames`,
   which makes spatialisation, bus volumes and sound-end callbacks testable deterministically.
3. **Sound-end and log callbacks post events.** `update()` drains them; one-shots release
   themselves on their end event. (The shim posted the sound id in the wrong event field before
   this phase; the audio test now covers the path.)
4. **The listener follows an entity.** `AudioListener` on the camera entity drives position and
   orientation from its `WorldTransform`; `AudioSource` entities position their sounds.

### Input (`cinder/input`)

1. **Actions and axes over device codes.** Bindings are (device, code, scale, dead zone);
   digital sources contribute `scale` while held, analog ones their value after a rescaled dead
   zone. Mouse deltas accumulate within a frame.
2. **Rebinding by listening.** The next matching event replaces an action's bindings or adds an
   axis binding; the capturing press does not fire the action.
3. **Text file, exact floats.** The map saves as `cinder-input 1` lines using the scene
   format's exact float formatter, so a save/load round trip is byte-identical.

## Consequences

- Phase 7 tools read `PhysicsWorld.live_objects()`, `AudioEngine.live_sounds()` and the renderer's
  resource counts for the shutdown leak report.
- The sample game (Phase 8) builds its character on `CharacterController` + `Animator` root
  motion disabled (controller-driven) and a locomotion 1D blend tree.
- `JPH_Init` is process-wide and never shut down while a world exists; worlds call it (it is
  idempotent in joltc).
