Skip to content

Rotating bodies: spinning rained debris, to price the oriented-contact path at scale #83

Description

@Kieleth

Base branch: fix/physics-surgery (PR #75).

Why now

The OBB narrow phase landed. box_particle_is_rotated() in
src/core/narrow_phase.cpp selects a 15-axis SAT with reference-face clipping
for any box rotated past BOX_ROTATION_EPS (or past QUAT_UNROTATED_EPS when
is_quat_driven), obb_of_box_particle() builds the oriented box from
rotation_q or the Euler triple, and broad-phase bounds come from
aabb_of_obb(). INV-12 states the contract as law: nothing collides as its
axis-aligned bounding slab, and no bound may under-cover the body it stands for.

Every existing perf scene spawns bodies with zero rotation. The vehicle's rained
bodies are axis-aligned boxes and spheres. So the SAT path is barely exercised at
scale, and its cost at scale is unmeasured. Bodies that rain with seeded spin
turn every contact in the pile into an oriented contact, which is the point.

Read src/particle_core.h before writing this scene, and read #81 first: the
struct still carries a stale !!! PHYSICS COLLISION WARNING !!! comment claiming
physics ignores rotation_x/y/z. It does not, for boxes.

Scene

The vehicle's rain path, plus seeded orientation and spin per body.

Knobs:

  • MLP_SPIN=<sigma rad/s>: initial angular velocity drawn from a seeded normal
    on the MLP_SEED channel. 0 means today's behavior.
  • MLP_SPIN_DIST=normal|uniform.
  • MLP_ROT_INIT=1: draw the spawn orientation itself (rotation_x,
    rotation_y, rotation_z) from the same seeded channel.

MLP_ROT_INIT matters more than MLP_SPIN for the measurement. Today the solver
integrates omega_z onto rotation_z; omega_x and omega_y exist but are
unintegrated (Stage 1 of the rotational-DOF upgrade, see the comment block in
src/particle_core.h). A body given only spin therefore stays effectively
axis-aligned about two axes. Setting the spawn orientation puts 100% of bodies
on the SAT path from frame 1.

Combine freely with MLP_SIZE_DIST from the size-distributed ramp issue: a pile
of tumbling boulders and pebbles is the interesting version.

What to measure, against which baseline

The A/B here is by orientation, not by build flag. The code path is selected per
body by box_particle_is_rotated(), so an all-unrotated run and an all-rotated
run at the same body count and the same seed isolate the oriented path with zero
build variation. That is a cleaner control than any flag could give.

  1. phase_ms.physics at matched body counts, unrotated versus rotated.
  2. Contact counts. A face-clipped manifold carries up to 4 points, so the
    oriented path can change contact volume, not only per-contact cost. Report
    cost per oriented contact, not cost per frame.
  3. Fraction of bodies taking the SAT path, so the per-contact figure has a
    denominator. Instrument or derive it, do not assume 100% (settled bodies may
    relax toward axis alignment).
  4. Render side: tumbling geometry rewrites the shadow triangle set every frame
    without changing its COUNT. That is the acceleration-structure refit path
    exercised cleanly. Report AccelRefit against AccelBuild from gpu_ms,
    and Pass2ShadowRT.
  5. Frame time and GPU occupancy at both resolutions, both sides reported
    (learning 6: retina is balanced at CPU 19.74 / GPU 18.92, a one-sided win is
    capped at 0.81 ms).

Stated baseline to reproduce or refute: OBB SAT measured about +0.2 ms median
against the old axis-aligned path at Eden scale. This scene should reproduce that
at a known contact count and convert it into a per-contact number. If it does not
reproduce, that is a finding, write it down.

Determinism

Orientation and spin come only from MLP_SEED (INV-27: all randomness through
declared seeded channels). The RNG must not live in a function-local static: see
the protocol-hygiene issue, item 3. Today's statics make an in-process A-vs-A
control impossible, and INV-27's declared mechanism is exactly "runs the scene
twice in-process and demands bit identity".

Oracle strategy

A rained pile is not an oracle. If a pixel comparison is wanted for the render
side, build a static variant: N bodies placed at seeded orientations, KINEMATIC
and at rest, no rain, no dynamics. That variant is frame-count-driven and
oracle-eligible; prove it with an A-vs-A control run before claiming anything
from it.

Campaign protocol

docs/todo_plans/GPU_OPT_LEDGER.md, "The protocol": constexpr flag defaulting to
current behavior, headless A/B at fixed sizes, pixel oracles only on
proven-deterministic scenes with an A-vs-A control first, gates green with the
flag ON, numbers to the owner then visual inspection then the owner flips the
default, markers not exit codes, one ledger entry per item including negatives.

Done when

  • Per-oriented-contact cost stated with the contact count it was measured at.
  • Refit versus rebuild attribution for tumbling geometry.
  • The +0.2 ms Eden figure reproduced or explicitly refuted.
  • Ledger entry appended.

Reading order and related work

This issue is one of a set opened together on 2026-08-13 to continue the GPU
performance campaign with rotation and driven movers. All of them branch from
fix/physics-surgery (PR #75).

Read in this order:

  1. Perf protocol hygiene for the new scenes: sweep wiring, in-process A-vs-A, audit lines, run-alone #87 protocol hygiene: the plumbing and the vehicle defects. Read first, fix
    what blocks you, because every number below depends on it.
  2. Perf ramp: size-distributed rain to settled pile, measured #82 size-distributed rain ramp: the measured baseline the rest are compared
    against.
  3. Rotating bodies: spinning rained debris, to price the oriented-contact path at scale #83 spinning rained debris: prices the oriented-contact path.
  4. Rotating columns: KINEMATIC turning pillars, to measure AS refit quality under sustained motion #84 rotating KINEMATIC columns: acceleration-structure refit under sustained
    rotation, and oriented contacts against a driven mover.
  5. Translating platforms: moving KINEMATIC floors and pushers #85 translating KINEMATIC platforms: the translation control for Rotating columns: KINEMATIC turning pillars, to measure AS refit quality under sustained motion #84, and the
    driven-mover friction question.
  6. Composite stress: columns + platforms + size-distributed spinning rain (headline number, not an oracle) #86 composite stress: the headline number, explicitly not an oracle.

Supporting: #81 fixes the stale particle_core.h comment that tells readers
physics ignores rotation. Read it before writing any rotation scene.

Existing issues these touch: #6 (GPU frame stalls during BVH rebuilds on chunk
streaming, same mechanism as #84 from a different scene) and #41 (no friction
reaches a KINEMATIC body, the physics question behind #85).

Protocol of record: docs/todo_plans/GPU_OPT_LEDGER.md ("The protocol" plus the
cross-cutting learnings) and docs/todo_plans/GPU_PIPELINE_AUDIT_2026-07.md.
Invariants of record: INV-12 (true-geometry contacts), INV-27 (deterministic
given seeds), INV-30 (external writers place nothing illegal), in
tests/invariants/INVARIANTS.jsonl.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:physicsSolver, contacts, gluons, locomotionarea:renderingRasterizer, Metal, lighting, shadowsenhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions