newton physics · XPBD restitution · 2026-07-23

XPBD restitution: the decoupling, the flat-shape problem, and a per-manifold fix

Analysis behind the restitution-decoupling PR (#14) and the stacked per-manifold prototype (branch dylanturpin/newton-collab:draft/pr14-manifold-restitution). Everything quantitative here was measured (RTX 3090, warp 1.15 dev); nothing is asserted from reading code alone unless marked.

The one-video version

Real simulated trajectories from both branches (schematic side view): e=1.0 drop from 0.30 m; columns are rigid_contact_restitution_iterations = 1 / 2 / 8 / 32; sphere (blue), cylinder (orange), box (green). Top row — current scheme: flat shapes barely bounce at practical iteration counts; only the 32-iteration column recovers them, at the cost quantified in §3. Bottom row — per-manifold solve: ~96% rebound for every shape, identical in every column.

1 · Background: what the decoupling PR fixes, and that it works

Enabling restitution on SolverXPBD used to switch every body in the scene to position-delta-derived velocity integration — trajectories changed even with no contacts anywhere (a contact-free pendulum went 86× out of tolerance; that finding is what the upstream #2602 deprecation work ran into). Eric's #14 cuts that wire: bodies always stay on Newton's public (v_com, ω) convention, and restitution becomes a velocity-level contact pass after the positional solve.

Verified on the branch: contact-free trajectories are bit-identical with restitution on/off (the PR's invariance test asserts exact array equality and passes); the full xpbd test file is green with the review fixes applied — including six parent_f Newton's-second-law tests that were failing because of the old coupling and pass with #14; the restitution-off path costs nothing extra (§5); and the margin-gate failure mode — a positional correction applied while restitution is gated out — is structurally impossible in the new scheme: the active-contact recording is a superset of every contact the positional solver corrects.

2 · The remaining problem: flat shapes barely bounce

Rebound recovery vs restitution iterations, by shape

e=1.0 body dropped 0.30 m onto a restitutive surface; recovery = post-impact rebound peak ÷ release height; the elastic regression test asserts ≥ 80%.

sphere (1 contact) cylinder (rim manifold) box (4-corner manifold)
0 20 40 60 80 100 % of release height recovered test bound · 80% shipped default 1 2 4 8 16 32 restitution iterations (log scale) sphere · 96% at 1–8 iterations cylinder box
At the shipped default (2 iterations) flat shapes recover ~17–23% — visually they just don't bounce. The sphere is exactly right at one iteration, which is why sphere-heavy demos look convincing. The old (pre-#14) path fails the same scenario in the opposite direction — 0.3165 m peak on a 0.30 m drop, i.e. energy injection: its per-contact weight argument was dead code, so N contacts over-applied up to N×. Metric note: box values are instrumented first-rebound ratios (the test's own peak metric under-reads that series at low iteration counts); cylinder and sphere use the test's metric.

Why: the per-body average cancels the angular coupling

Each contact sizes its impulse with its full effective mass K = m⁻¹ + (r×n)ᵀI⁻¹(r×n) — linear plus lever-arm response. On a symmetric manifold the angular components of the summed impulses oppose and cancel, so after the per-body average only the linear fraction lands: residual ratio 1 − m⁻¹/K per iteration. Instrumented and matched exactly: box predicted 0.8276, measured 0.8276 at every iteration; sphere (r ∥ n, no angular term) converges in one. Two aggravators: whatever a substep fails to converge is permanently lost (the next substep re-anchors its target), and asymmetric rim manifolds don't fully cancel — they inject spurious spin instead.

Flat manifold (box corners) COM angular parts oppose → cancel in the sum each impulse was sized counting on its angular share — only m⁻¹/K of the needed change survives per iteration Sphere (single contact) lever arm ∥ normal → no angular term (K = m⁻¹) converges in one iteration

3 · Why "just raise iterations" doesn't work economically

Solver cost vs restitution iterations

1,024-body mixed contact scene, XPBD, 10 substeps @ 100 fps, CUDA-graph-captured, 200 timed frames, key configs ×3.

current scheme, by outer iterations per-manifold prototype
1.0 1.5 2.0 2.5 ms / frame 1 2 4 8 16 32 restitution iterations (log scale) restitution off · 0.99 old restitution path · 1.07 per-manifold · 1.30 1.23 shipped default (+22% vs off) 1.95 ≈2.7 extrapolated
Each outer iteration is three kernel launches × substeps (~0.06 ms/frame here). Reaching flat-shape correctness needs ~32 iterations ≈ 2.7× the restitution-off cost — and tuning an iteration count per scene is a burden neither maintainers nor users should carry. The orange point is the per-manifold prototype: converged at the default, 1.30 ms/frame.

4 · The per-manifold solve

Stop treating a box's four corners as four independent problems. Restitution-eligible contacts are grouped by body pair (a fixed-size open-addressing hash table fused into the existing recording kernel — the contact stream is not pair-contiguous, and a radix-sort variant measured too slow). One GPU thread per manifold then runs a small Gauss–Seidel solve: 8 fixed inner sweeps over ≤8 contacts held in registers, accumulated impulses clamped ≥ 0, full linear+angular response per contact, contact order canonicalized. The outer iteration loop survives only as cross-manifold coupling (stacks/chains), and the inner sweeps are register-resident — effectively free, so there is nothing to tune.

CheckResult
Rebound at the default (2 outer iterations) sphere 95.6%, cylinder 95.6%, box 95.6% (was 96 / 17 / 23) — identical at outer = 1, 2, 4, 8
Cost (same 1,024-body scene) 1.30 vs 1.23 ms/frame — correct flat-shape restitution for ~+6%
Full xpbd test file 62/62 green, CPU and CUDA, including the bit-exact invariance test
Rocking (10° tilted box, e=0.5) no energy gain — the impulse clamp holds on asymmetric impacts
Momentum (head-on equal masses, e=1, zero g) error exactly 0

5 · The common case pays nothing

Restitution is off (or every coefficient is zero) in the default and benchmark configurations, so the bar is: no cost there. Measured three-way, same session (ms/frame):

Pathpre-#14#14+manifoldReading
Literal enable_restitution=False, 1,024-body scene1.021.001.03deltas ≤2%, ranges overlap — noise
True + all-zero coefficients1.161.051.04~9% faster: the old path ran the velocity rewrite + restitution kernel even at e=0
Pyramid contact example (4,200 boxes, the ASV scene)4.213.873.85~8% faster on the representative benchmark workload

Expect XPBD benchmarks to step down ~5–10% where call sites pass enable_restitution=True with zero coefficients, and not to move where restitution is off entirely.

6 · Proposal

  1. #14 merges as-is (with the review's test/changelog fixes): the decoupling is correct, verified, and independently valuable — it should not wait on restitution-quality work.
  2. The per-manifold scheme iterates as a stacked PR (prototype-marked). Its open items are the review's asks, not blockers for #14: the 8-contact manifold cap needs a reduction pass for mesh/SDF patches (design input welcome), gradients through the hash grouping aren't meaningful yet, multi-manifold bodies keep float-atomic accumulation variance, per-step buffer allocation should eventually hoist to init, and the substep re-anchoring behavior is unchanged — irrelevant once impacts converge within one pass (which they now do), but relevant to any future configuration that under-converges.
  3. Iteration pins drop once the manifold scheme lands — the default converges, and rigid_contact_restitution_iterations stops being a knob anyone needs to think about.
  4. Upstream: the call-site + decoupling package travels to newton-physics/newton as the #2602 resolution after the manifold review settles — flagged here, deliberately not scheduled yet.