You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
phase_ms.physics at matched body counts, unrotated versus rotated.
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.
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).
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.
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).
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.
Base branch:
fix/physics-surgery(PR #75).Why now
The OBB narrow phase landed.
box_particle_is_rotated()insrc/core/narrow_phase.cppselects a 15-axis SAT with reference-face clippingfor any box rotated past
BOX_ROTATION_EPS(or pastQUAT_UNROTATED_EPSwhenis_quat_driven),obb_of_box_particle()builds the oriented box fromrotation_qor the Euler triple, and broad-phase bounds come fromaabb_of_obb(). INV-12 states the contract as law: nothing collides as itsaxis-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.hbefore writing this scene, and read #81 first: thestruct still carries a stale
!!! PHYSICS COLLISION WARNING !!!comment claimingphysics 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 normalon the
MLP_SEEDchannel. 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_INITmatters more thanMLP_SPINfor the measurement. Today the solverintegrates
omega_zontorotation_z;omega_xandomega_yexist but areunintegrated (Stage 1 of the rotational-DOF upgrade, see the comment block in
src/particle_core.h). A body given only spin therefore stays effectivelyaxis-aligned about two axes. Setting the spawn orientation puts 100% of bodies
on the SAT path from frame 1.
Combine freely with
MLP_SIZE_DISTfrom the size-distributed ramp issue: a pileof 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-rotatedrun 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.
phase_ms.physicsat matched body counts, unrotated versus rotated.oriented path can change contact volume, not only per-contact cost. Report
cost per oriented contact, not cost per frame.
denominator. Instrument or derive it, do not assume 100% (settled bodies may
relax toward axis alignment).
without changing its COUNT. That is the acceleration-structure refit path
exercised cleanly. Report
AccelRefitagainstAccelBuildfromgpu_ms,and
Pass2ShadowRT.(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 throughdeclared 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 tocurrent 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
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:
what blocks you, because every number below depends on it.
against.
rotation, and oriented contacts against a driven mover.
driven-mover friction question.
Supporting: #81 fixes the stale
particle_core.hcomment that tells readersphysics 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 thecross-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.