peclet-dem 0.4.0
Performance-portable XPBD Discrete Element Method (Kokkos + ArborX)
Loading...
Searching...
No Matches
solver_velocity.hpp
Go to the documentation of this file.
1
9#ifndef DEM_SOLVER_VELOCITY_HPP
10#define DEM_SOLVER_VELOCITY_HPP
11
12#include <climits>
13#include <Kokkos_Core.hpp>
14#include <tuple>
15#include <utility>
16#include <vector>
17
18#include "contact_preprocessing.hpp" // ManifoldC, CpExec/CpMem
19#include "dem_portable.hpp"
20#include "solver_fused.hpp"
21
22namespace peclet::dem {
23
24namespace detail {
25KOKKOS_INLINE_FUNCTION F3 ld3(Kokkos::View<const float* [3], CpMem> v, int i) {
26 return F3{v(i, 0), v(i, 1), v(i, 2)};
27}
28// v^T I_world^-1 v with I_world^-1 = R I_local^-1 R^T -> (R^T v) diag(invI_local) (R^T v).
29KOKKOS_INLINE_FUNCTION float genInvMass(F3 tau, F3 invIlocal, F4 q) {
30 // Isotropic inertia (spheres): the principal frame is irrelevant -- skip both rotations.
31 if (invIlocal.x == invIlocal.y && invIlocal.y == invIlocal.z)
32 return dot3(tau, tau) * invIlocal.x;
33 const F3 t = invRotateVector(q, tau);
34 return t.x * t.x * invIlocal.x + t.y * t.y * invIlocal.y + t.z * t.z * invIlocal.z;
35}
36} // namespace detail
37
40 Kokkos::View<const ManifoldC*, CpMem> manifolds, int numManifolds,
41 Kokkos::View<const float*, CpMem> invMass, Kokkos::View<const float* [3], CpMem> invInertia,
42 Kokkos::View<const float* [4], CpMem> quat, Kokkos::View<const float* [3], CpMem> velPred,
43 Kokkos::View<const float* [3], CpMem> angVelPred, Kokkos::View<const int*, CpMem> realIdx,
44 float growthRate, float restitutionNormal, float restVelThreshold,
45 Kokkos::View<float* [3], CpMem> deltaVel, Kokkos::View<float* [3], CpMem> deltaAngVel,
46 Kokkos::View<int*, CpMem> velCounts, Kokkos::View<const int*, CpMem> onlyColor = {},
47 int colorFilter = 0, Kokkos::View<const unsigned char*, CpMem> persistent = {},
48 Kokkos::View<const float* [3], CpMem> posPred = {}, F3 gHat = {},
49 Kokkos::View<const unsigned char*, CpMem> grounded = {}) {
51 using detail::ld3;
52 CpExec space;
53 const bool filt = onlyColor.extent(0) > 0;
54 const bool usePersist = persistent.extent(0) > 0;
55 Kokkos::parallel_for(
56 "peclet::dem::solve_velocity", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
57 KOKKOS_LAMBDA(int idx) {
58 if (filt && onlyColor(idx) != colorFilter)
59 return; // Jacobi fallback pass: only the manifolds the colouring could not place
60 const ManifoldC m = manifolds(idx);
61 if (m.num_points <= 0)
62 return;
63
64 const int idA = m.bodyA, idB = m.bodyB;
65 const int realA = realIdx(idA);
66 int realB = idB;
67 if (idB >= 0) {
68 realB = realIdx(idB);
69 if (realA > realB)
70 return; // periodic dedup
71 }
72
73 const float invMassA = invMass(realA);
74 const float invMassB = (idB >= 0) ? invMass(realB) : 0.0f;
75 const F3 invIA = ld3(invInertia, realA);
76 const F3 invIB = (idB >= 0) ? ld3(invInertia, realB) : F3{0, 0, 0};
77 const F4 qA = F4{quat(realA, 0), quat(realA, 1), quat(realA, 2), quat(realA, 3)};
78 const F4 qB = (idB >= 0)
79 ? F4{quat(realB, 0), quat(realB, 1), quat(realB, 2), quat(realB, 3)}
80 : F4{0, 0, 0, 1};
81
82 const F3 vA = ld3(velPred, realA), wA = ld3(angVelPred, realA);
83 F3 vB{0, 0, 0}, wB{0, 0, 0};
84 if (idB >= 0) {
85 vB = ld3(velPred, realB);
86 wB = ld3(angVelPred, realB);
87 }
88
89 const F3 Nsum{m.normal_sum.x, m.normal_sum.y, m.normal_sum.z};
90 const F3 TauA{m.torque_armA_sum.x, m.torque_armA_sum.y, m.torque_armA_sum.z};
91 const F3 TauB{m.torque_armB_sum.x, m.torque_armB_sum.y, m.torque_armB_sum.z};
92
93 const float invN = 1.0f / static_cast<float>(m.num_points);
94
95 // Moving-wall boundary (idB<0): the static "body B" carries the wall's surface velocity
96 // (count-averaged over the contact patch) so restitution is against the wall's motion, and
97 // its own restitution (a < 0 average keeps the global material — planes, body-body).
98 float restitution = restitutionNormal;
99 if (idB < 0)
100 vB = scale3(F3{m.wallVel_sum.x, m.wallVel_sum.y, m.wallVel_sum.z}, invN);
101 { // per-wall AND per-pair material override (a < 0 average keeps the global material)
102 const float ra = m.restitution_sum * invN;
103 if (ra >= 0.0f)
104 restitution = ra;
105 }
106 const F3 rAavg = scale3(F3{m.rA_sum.x, m.rA_sum.y, m.rA_sum.z}, invN);
107 const F3 rBavg = scale3(F3{m.rB_sum.x, m.rB_sum.y, m.rB_sum.z}, invN);
108
109 const float lenN = Kokkos::sqrt(dot3(Nsum, Nsum));
110 if (lenN < 1e-9f)
111 return;
112
113 // Separation vector for the approaching-sign gate + growth velocity. For a BOUNDARY
114 // (idB<0), rBavg is the ABSOLUTE wall contact point (kept absolute for the position solve's
115 // plane linearisation), NOT a body-relative lever — so rAavg - rBavg would depend on where
116 // the contact sits in world space, flipping `alignment` (and thus the approaching test)
117 // around a curved wall / a wall far from the origin and injecting energy (grains "jump" on
118 // the way down a rotating drum). The grain's own contact lever rAavg is the meaningful
119 // relative vector (dot(Nsum, rAavg) = radius > 0, a consistent convention). Body-body keeps
120 // rAavg-rBavg.
121 const F3 diffCenters = (idB < 0) ? rAavg : sub3(rAavg, rBavg);
122 const F3 vGrowth = scale3(diffCenters, growthRate);
123
124 float vn = dot3(vA, Nsum) + dot3(wA, TauA) + dot3(vB, F3{-Nsum.x, -Nsum.y, -Nsum.z}) +
125 dot3(wB, TauB);
126 vn += dot3(vGrowth, Nsum);
127
128 const float alignment = dot3(Nsum, diffCenters);
129 if (alignment > 0.0f) {
130 if (vn < 0.0f)
131 return; // sphere convention: approaching if vn>0
132 } else {
133 if (vn > 0.0f)
134 return; // inverted convention
135 }
136
137 const float Nsq = dot3(Nsum, Nsum);
138 float wA_n = Nsq * invMassA + genInvMass(TauA, invIA, qA);
139 float wB_n = Nsq * invMassB + genInvMass(TauB, invIB, qB);
140 // Shock propagation (Guendelman et al. 2003) for persistent LOADED body-body contacts: an
141 // inelastic pairwise solve conserves momentum, so a deep column merely homogenises its fall
142 // (the floor drains one layer per sweep and the pile never cools -- the phantom-fall
143 // state). Treating the LOWER body of a loaded contact as static drains the column's
144 // momentum through the support chain into the ground: the upper body is corrected, the
145 // lower keeps its (already supported) velocity. Near-horizontal pairs stay symmetric; new
146 // contacts and g = 0 runs are untouched (momentum-conserving impacts with material
147 // restitution).
148 // ... but ONLY when the lower body is not moving UPWARD: correcting the upper body against
149 // a static or FALLING support strictly removes momentum (monotone drainage into the
150 // ground), while one-sidedness against a RISING support copies its bounce velocity up the
151 // chain with no mass penalty and fountains the whole column. Rising supports (floor
152 // bounces, bubble eruptions) therefore keep the symmetric momentum-conserving impulse.
153 bool applyA = true, applyB = true;
154 if (usePersist && persistent(idx) != 0 && idB >= 0) {
155 const F3 dx = sub3(ldF3(posPred, idA), ldF3(posPred, idB)); // ghost-aware pair geometry
156 const float up = -(dx.x * gHat.x + dx.y * gHat.y + dx.z * gHat.z); // >0: A above B
157 const float thr = 0.3f * Kokkos::sqrt(dot3(dx, dx));
158 const float riseThr =
159 4.0f * restVelThreshold; // rise = -v.gHat (gHat points down-gravity)
160 // ... and the support must be GROUNDED (contact path to the floor): a gas-borne emulsion
161 // or lifted slug keeps symmetric momentum-conserving impulses, so its weight stays on
162 // the gas -- only genuinely supported chains drain into the ground.
163 if (up > thr && -dot3(vB, gHat) <= riseThr && grounded(realB) > 0) {
164 wB_n = 0.0f;
165 applyB = false;
166 restitution = 0.0f; // shock pass is inelastic: e > 0 one-sided would bounce bodies
167 // off the ground with unpaid momentum
168 } else if (up < -thr && -dot3(vA, gHat) <= riseThr && grounded(realA) > 0) {
169 wA_n = 0.0f;
170 applyA = false;
171 restitution = 0.0f;
172 }
173 }
174 const float wTotal = wA_n + wB_n;
175 if (wTotal <= 0.0f)
176 return;
177
178 // Resting-contact regularization (the standard PBD/XPBD restitution threshold): bounce only
179 // when the physical approach speed |vn|/|Nsum| exceeds ~2 g dt (what one substep of free
180 // fall gains). Below it the contact is RESTING — its vn is integration noise, and bouncing
181 // it every substep across a dense pile's contact chains (impulses Jacobi-SUMMED per body,
182 // no mass splitting) pumps energy without bound: a settled 180k glass-bead bed switched to
183 // e=0.8 reached |v| ~ 3e6 cells/s within 15 substeps. With e=0 the impulse still cancels
184 // the approach velocity — exactly the quasi-static dissipation a resting pile needs.
185 if (Kokkos::fabs(vn) < restVelThreshold * lenN)
186 restitution = 0.0f;
187
188 const float lambda = (-restitution * vn - vn) / wTotal;
189
190 const F3 Jlin = scale3(Nsum, lambda);
191 const F3 JangA = scale3(TauA, lambda);
192 const F3 JangB = scale3(TauB, lambda);
193
194 // Linear delta on A.
195 if (applyA) {
196 Kokkos::atomic_add(&deltaVel(realA, 0), Jlin.x * invMassA);
197 Kokkos::atomic_add(&deltaVel(realA, 1), Jlin.y * invMassA);
198 Kokkos::atomic_add(&deltaVel(realA, 2), Jlin.z * invMassA);
199 // Angular delta on A: dw_world = R (invI_local * (R^T Jang)).
200 {
201 const F3 Jl = invRotateVector(qA, JangA);
202 const F3 dwl{Jl.x * invIA.x, Jl.y * invIA.y, Jl.z * invIA.z};
203 const F3 dww = rotateVector(qA, dwl);
204 Kokkos::atomic_add(&deltaAngVel(realA, 0), dww.x);
205 Kokkos::atomic_add(&deltaAngVel(realA, 1), dww.y);
206 Kokkos::atomic_add(&deltaAngVel(realA, 2), dww.z);
207 }
208 }
209 if (idB >= 0 && applyB) {
210 Kokkos::atomic_add(&deltaVel(realB, 0), -Jlin.x * invMassB);
211 Kokkos::atomic_add(&deltaVel(realB, 1), -Jlin.y * invMassB);
212 Kokkos::atomic_add(&deltaVel(realB, 2), -Jlin.z * invMassB);
213 const F3 Jl = invRotateVector(qB, JangB);
214 const F3 dwl{Jl.x * invIB.x, Jl.y * invIB.y, Jl.z * invIB.z};
215 const F3 dww = rotateVector(qB, dwl);
216 Kokkos::atomic_add(&deltaAngVel(realB, 0), dww.x);
217 Kokkos::atomic_add(&deltaAngVel(realB, 1), dww.y);
218 Kokkos::atomic_add(&deltaAngVel(realB, 2), dww.z);
219 Kokkos::atomic_add(&velCounts(realB), 1);
220 }
221 if (applyA)
222 Kokkos::atomic_add(&velCounts(realA), 1);
223 });
224 space.fence();
225}
226
232template <class V3, class Vi>
233inline void applyVelocityDeltasAveragedKokkos(int n, V3 velPred, V3 angVelPred, V3 deltaVel,
234 V3 deltaAngVel, Vi velCounts) {
235 CpExec space;
236 Kokkos::parallel_for(
237 "peclet::dem::apply_vel_avg", Kokkos::RangePolicy<CpExec>(space, 0, n), KOKKOS_LAMBDA(int i) {
238 const int count = velCounts(i);
239 if (count <= 0)
240 return;
241 // Over-relaxed average: omega=2 halves the convergence loss of plain 1/count averaging
242 // (the crush-dissipation rate) while staying far below the raw-sum overshoot (omega=count)
243 // that detonates a resting pile at e=0.8. count==1 (binary collision) stays exact.
244 const float f = Kokkos::fmin(1.0f, 2.0f / static_cast<float>(count));
245 for (int c = 0; c < 3; ++c) {
246 velPred(i, c) += deltaVel(i, c) * f;
247 angVelPred(i, c) += deltaAngVel(i, c) * f;
248 deltaVel(i, c) = 0.0f;
249 deltaAngVel(i, c) = 0.0f;
250 }
251 velCounts(i) = 0;
252 });
253 space.fence();
254}
255
256// ============================ colored Gauss–Seidel velocity solve ============================
257// The Jacobi solve above sums every touching manifold's impulse onto a body, then relaxes the sum
258// by the contact count to stay stable — a stable but UNDER-converged approximation that
259// under-dissipates in dense multi-contact regions (effective restitution rises above the prescribed
260// e). The colored Gauss–Seidel path removes that approximation: graph-colour the manifolds so no
261// two sharing a real body share a colour, then sweep colour-by-colour applying each impulse IN
262// PLACE (read the current velocity, apply, write) — a body sees the updates of every
263// previously-solved contact in the same sweep. Within a colour the manifolds are an independent set
264// (no shared body), so the in-place read-modify-write is race-free WITHOUT atomics or averaging,
265// and the fixed-point is the true coupled multi-contact solution, so the dissipation is correct by
266// construction. count==1 (a binary collision) is identical to the Jacobi path; the difference is
267// confined to dense clusters.
268
277inline int colorManifoldsKokkos(Kokkos::View<const ManifoldC*, CpMem> manifolds, int numManifolds,
278 Kokkos::View<const int*, CpMem> realIdx, int numReal,
279 Kokkos::View<int*, CpMem> mColor,
280 Kokkos::View<long long*, CpMem> bodyWinner,
281 Kokkos::View<std::uint64_t*, CpMem> bodyMask, int& leftover,
282 Kokkos::View<const unsigned char*, CpMem> sleepMask = {}) {
283 leftover = 0;
284 CpExec space;
285 if (numManifolds <= 0 || numReal <= 0)
286 return 0;
287 const bool sleepOn = sleepMask.extent(0) > 0;
288 Kokkos::parallel_for(
289 "peclet::dem::color_init_bodies", Kokkos::RangePolicy<CpExec>(space, 0, numReal),
290 KOKKOS_LAMBDA(int i) { bodyMask(i) = 0; });
291 // -2 inactive (skip forever), -1 uncoloured, >=0 committed colour.
292 Kokkos::parallel_for(
293 "peclet::dem::color_init_manifolds", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
294 KOKKOS_LAMBDA(int idx) {
295 const ManifoldC m = manifolds(idx);
296 if (m.num_points <= 0 || (sleepOn && sleepMask(idx))) {
297 mColor(idx) = -2; // inactive or a frozen (both-asleep) island manifold
298 return;
299 }
300 if (m.bodyB >= 0 && realIdx(m.bodyA) > realIdx(m.bodyB)) {
301 mColor(idx) = -2; // periodic dedup: the (realA<=realB) twin carries this contact
302 return;
303 }
304 mColor(idx) = -1;
305 });
306
307 int remaining = 1, prevRemaining = -1;
308 const int maxRounds = numReal + 2; // safety bound; converges in ~max-degree rounds in practice
309 for (int round = 0; round < maxRounds && remaining > 0; ++round) {
310 Kokkos::parallel_for(
311 "peclet::dem::color_reset_winner", Kokkos::RangePolicy<CpExec>(space, 0, numReal),
312 KOKKOS_LAMBDA(int i) { bodyWinner(i) = -1; });
313 Kokkos::parallel_for(
314 "peclet::dem::color_contend", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
315 KOKKOS_LAMBDA(int idx) {
316 if (mColor(idx) != -1)
317 return;
318 const ManifoldC m = manifolds(idx);
319 const long long key = colorKey(idx);
320 Kokkos::atomic_max(&bodyWinner(realIdx(m.bodyA)), key);
321 if (m.bodyB >= 0)
322 Kokkos::atomic_max(&bodyWinner(realIdx(m.bodyB)), key);
323 });
324 int rem = 0;
325 Kokkos::parallel_reduce(
326 "peclet::dem::color_commit", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
327 KOKKOS_LAMBDA(int idx, int& acc) {
328 if (mColor(idx) != -1)
329 return;
330 const ManifoldC m = manifolds(idx);
331 const int ea = realIdx(m.bodyA);
332 const int eb = (m.bodyB >= 0) ? realIdx(m.bodyB) : -1;
333 // Winner iff it holds BOTH its endpoints -> no uncoloured conflict, sole writer of ea/eb.
334 const long long key = colorKey(idx);
335 if (bodyWinner(ea) != key || (eb >= 0 && bodyWinner(eb) != key)) {
336 acc += 1;
337 return;
338 }
339 std::uint64_t forbidden = bodyMask(ea);
340 if (eb >= 0)
341 forbidden |= bodyMask(eb);
342 int c = 0;
343 while (c < 62 && (forbidden & (std::uint64_t(1) << c)))
344 ++c; // lowest free colour (cap 63; dense sphere degree ~12, far below)
345 mColor(idx) = c;
346 const std::uint64_t bit = std::uint64_t(1) << c;
347 bodyMask(ea) |= bit;
348 if (eb >= 0)
349 bodyMask(eb) |= bit;
350 },
351 rem);
352 space.fence();
353 if (rem == prevRemaining)
354 break; // colour-mask saturation (degree > 62): leftovers stay -1, Jacobi fallback applies
355 // them
356 prevRemaining = rem;
357 remaining = rem;
358 }
359
360 int maxc = -1;
361 Kokkos::parallel_reduce(
362 "peclet::dem::color_max", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
363 KOKKOS_LAMBDA(int idx, int& mx) {
364 if (mColor(idx) > mx)
365 mx = mColor(idx);
366 },
367 Kokkos::Max<int>(maxc));
368 Kokkos::parallel_reduce(
369 "peclet::dem::color_leftover", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
370 KOKKOS_LAMBDA(int idx, int& acc) {
371 if (mColor(idx) == -1)
372 acc += 1;
373 },
374 leftover);
375 space.fence();
376 return maxc + 1;
377}
378
396 Kokkos::View<const ManifoldC*, CpMem> manifolds, int numManifolds,
397 Kokkos::View<const int*, CpMem> realIdx, int numReal,
398 Kokkos::View<const unsigned long long*, CpMem> prevKeys,
399 Kokkos::View<const int*, CpMem> prevColor, int prevCount, Kokkos::View<int*, CpMem> mColor,
400 Kokkos::View<long long*, CpMem> bodyWinner, Kokkos::View<std::uint64_t*, CpMem> bodyMask,
401 int& leftover, bool forceFull, Kokkos::View<const unsigned char*, CpMem> sleepMask = {}) {
402 leftover = 0;
403 CpExec space;
404 if (numManifolds <= 0 || numReal <= 0)
405 return 0;
406 Kokkos::parallel_for(
407 "peclet::dem::icolor_init_bodies", Kokkos::RangePolicy<CpExec>(space, 0, numReal),
408 KOKKOS_LAMBDA(int i) { bodyMask(i) = 0; });
409 const bool full = forceFull || prevCount <= 0;
410 const bool sleepOn = sleepMask.extent(0) > 0;
411 // Seed each manifold's colour: -2 inactive/dedup/both-asleep, else the carried colour (matched by
412 // pair key) or -1 (new / full recolour).
413 Kokkos::parallel_for(
414 "peclet::dem::icolor_seed", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
415 KOKKOS_LAMBDA(int idx) {
416 const ManifoldC m = manifolds(idx);
417 if (m.num_points <= 0 || (sleepOn && sleepMask(idx))) {
418 mColor(idx) = -2; // inactive or a frozen (both-asleep) island manifold
419 return;
420 }
421 if (m.bodyB >= 0 && realIdx(m.bodyA) > realIdx(m.bodyB)) {
422 mColor(idx) = -2; // periodic dedup twin
423 return;
424 }
425 int c = -1;
426 if (!full) {
427 const unsigned long long k = pairKeyOf(m, realIdx);
428 int lo = 0, hi = prevCount;
429 while (lo < hi) {
430 const int mid = (lo + hi) >> 1;
431 if (prevKeys(mid) < k)
432 lo = mid + 1;
433 else
434 hi = mid;
435 }
436 if (lo < prevCount && prevKeys(lo) == k) {
437 const int pc = prevColor(lo);
438 if (pc >= 0)
439 c = pc; // carry (leftover -1 last step -> re-arbitrate as new)
440 }
441 }
442 mColor(idx) = c;
443 });
444 // Seed the per-body masks from the carried colours (survivors are conflict-free on single-GPU).
445 if (!full)
446 Kokkos::parallel_for(
447 "peclet::dem::icolor_seed_mask", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
448 KOKKOS_LAMBDA(int idx) {
449 const int c = mColor(idx);
450 if (c < 0)
451 return;
452 const ManifoldC m = manifolds(idx);
453 const std::uint64_t bit = std::uint64_t(1) << c;
454 Kokkos::atomic_or(&bodyMask(realIdx(m.bodyA)), bit);
455 if (m.bodyB >= 0)
456 Kokkos::atomic_or(&bodyMask(realIdx(m.bodyB)), bit);
457 });
458 // Jones-Plassmann arbitration over the uncoloured (-1) set only — identical body of work to
459 // colorManifoldsKokkos, but the frozen carried colours restrict it to the few new manifolds.
460 int remaining = 1, prevRemaining = -1;
461 const int maxRounds = numReal + 2;
462 for (int round = 0; round < maxRounds && remaining > 0; ++round) {
463 Kokkos::parallel_for(
464 "peclet::dem::icolor_reset_winner", Kokkos::RangePolicy<CpExec>(space, 0, numReal),
465 KOKKOS_LAMBDA(int i) { bodyWinner(i) = -1; });
466 Kokkos::parallel_for(
467 "peclet::dem::icolor_contend", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
468 KOKKOS_LAMBDA(int idx) {
469 if (mColor(idx) != -1)
470 return;
471 const ManifoldC m = manifolds(idx);
472 const long long key = colorKey(idx);
473 Kokkos::atomic_max(&bodyWinner(realIdx(m.bodyA)), key);
474 if (m.bodyB >= 0)
475 Kokkos::atomic_max(&bodyWinner(realIdx(m.bodyB)), key);
476 });
477 int rem = 0;
478 Kokkos::parallel_reduce(
479 "peclet::dem::icolor_commit", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
480 KOKKOS_LAMBDA(int idx, int& acc) {
481 if (mColor(idx) != -1)
482 return;
483 const ManifoldC m = manifolds(idx);
484 const int ea = realIdx(m.bodyA);
485 const int eb = (m.bodyB >= 0) ? realIdx(m.bodyB) : -1;
486 const long long key = colorKey(idx);
487 if (bodyWinner(ea) != key || (eb >= 0 && bodyWinner(eb) != key)) {
488 acc += 1;
489 return;
490 }
491 std::uint64_t forbidden = bodyMask(ea);
492 if (eb >= 0)
493 forbidden |= bodyMask(eb);
494 int c = 0;
495 while (c < 62 && (forbidden & (std::uint64_t(1) << c)))
496 ++c;
497 mColor(idx) = c;
498 const std::uint64_t bit = std::uint64_t(1) << c;
499 bodyMask(ea) |= bit;
500 if (eb >= 0)
501 bodyMask(eb) |= bit;
502 },
503 rem);
504 space.fence();
505 if (rem == prevRemaining)
506 break; // colour-mask saturation: leftovers stay -1 (Jacobi fallback)
507 prevRemaining = rem;
508 remaining = rem;
509 }
510 int maxc = -1;
511 Kokkos::parallel_reduce(
512 "peclet::dem::icolor_max", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
513 KOKKOS_LAMBDA(int idx, int& mx) {
514 if (mColor(idx) > mx)
515 mx = mColor(idx);
516 },
517 Kokkos::Max<int>(maxc));
518 Kokkos::parallel_reduce(
519 "peclet::dem::icolor_leftover", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
520 KOKKOS_LAMBDA(int idx, int& acc) {
521 if (mColor(idx) == -1)
522 acc += 1;
523 },
524 leftover);
525 space.fence();
526 return maxc + 1;
527}
528
540
543inline void computeVn0Kokkos(Kokkos::View<const ManifoldC*, CpMem> manifolds, int numManifolds,
544 Kokkos::View<const float* [3], CpMem> velPred,
545 Kokkos::View<const float* [3], CpMem> angVelPred,
546 Kokkos::View<const int*, CpMem> realIdx, float growthRate,
547 Kokkos::View<float*, CpMem> vn0, Kokkos::View<float* [3], CpMem> vt0) {
548 using detail::ld3;
549 CpExec space;
550 Kokkos::parallel_for(
551 "peclet::dem::pgs_vn0", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
552 KOKKOS_LAMBDA(int idx) {
553 const ManifoldC m = manifolds(idx);
554 if (m.num_points <= 0)
555 return;
556 const int idA = m.bodyA, idB = m.bodyB;
557 const int realA = realIdx(idA);
558 if (idB >= 0 && realA > realIdx(idB))
559 return; // periodic dedup
560 const float invN = 1.0f / static_cast<float>(m.num_points);
561 const F3 Nsum{m.normal_sum.x, m.normal_sum.y, m.normal_sum.z};
564 const F3 vA = ld3(velPred, realA), wA = ld3(angVelPred, realA);
565 F3 vB{0, 0, 0}, wB{0, 0, 0};
566 if (idB >= 0) {
567 vB = ld3(velPred, realIdx(idB));
568 wB = ld3(angVelPred, realIdx(idB));
569 } else {
570 vB = scale3(F3{m.wallVel_sum.x, m.wallVel_sum.y, m.wallVel_sum.z}, invN);
571 }
572 const F3 rAavg = scale3(F3{m.rA_sum.x, m.rA_sum.y, m.rA_sum.z}, invN);
573 const F3 rBavg = scale3(F3{m.rB_sum.x, m.rB_sum.y, m.rB_sum.z}, invN);
574 const F3 diffCenters = (idB < 0) ? rAavg : sub3(rAavg, rBavg);
575 const F3 vGrowth = scale3(diffCenters, growthRate);
576 float vn = dot3(vA, Nsum) + dot3(wA, TauA) + dot3(vB, F3{-Nsum.x, -Nsum.y, -Nsum.z}) +
577 dot3(wB, TauB);
578 vn += dot3(vGrowth, Nsum);
579 vn0(idx) = vn;
580 // Pre-solve tangential surface velocity at the averaged contact point (physical units):
581 // the reference for the tangential-restitution target -beta * vt0 (Walton impact law).
582 {
583 const float lenN = Kokkos::sqrt(dot3(Nsum, Nsum));
584 F3 vt{0, 0, 0};
585 if (lenN > 1e-9f) {
586 const F3 nhat = scale3(Nsum, 1.0f / lenN);
587 const int realB2 = (idB >= 0) ? realIdx(idB) : -1;
588 const F3 wAv = ld3(angVelPred, realA);
589 const F3 wBv = (realB2 >= 0) ? ld3(angVelPred, realB2) : F3{0, 0, 0};
590 const F3 vrel = sub3(add3(vA, cross3v(wAv, rAavg)), add3(vB, cross3v(wBv, rBavg)));
591 vt = sub3(vrel, scale3(nhat, dot3(vrel, nhat)));
592 }
593 vt0(idx, 0) = vt.x;
594 vt0(idx, 1) = vt.y;
595 vt0(idx, 2) = vt.z;
596 }
597 });
598}
599
613 Kokkos::View<const ManifoldC*, CpMem> manifolds, int numManifolds,
614 Kokkos::View<const float*, CpMem> invMass, Kokkos::View<const float* [3], CpMem> invInertia,
615 Kokkos::View<const float* [4], CpMem> quat, Kokkos::View<const float* [3], CpMem> velPred,
616 Kokkos::View<const float* [3], CpMem> angVelPred, Kokkos::View<const int*, CpMem> realIdx,
617 float growthRate, float restitutionNormal, float restVelThreshold,
618 Kokkos::View<const float*, CpMem> vn0, Kokkos::View<const float*, CpMem> lambdaAcc,
619 Kokkos::View<const float*, CpMem> restRel, Kokkos::View<float*, CpMem> restBank,
620 Kokkos::View<float*, CpMem> restVPeak) {
621 using detail::genInvMass;
622 using detail::ld3;
623 CpExec space;
624 Kokkos::parallel_for(
625 "peclet::dem::rest_bank_update", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
626 KOKKOS_LAMBDA(int idx) {
627 const ManifoldC m = manifolds(idx);
628 if (m.num_points <= 0)
629 return;
630 const int idA = m.bodyA, idB = m.bodyB;
631 const int realA = realIdx(idA);
632 const int realB = (idB >= 0) ? realIdx(idB) : idB;
633 if (idB >= 0 && realA > realB)
634 return; // periodic dedup: the canonical twin owns the bank
635 const float invN = 1.0f / static_cast<float>(m.num_points);
636 const F3 Nsum{m.normal_sum.x, m.normal_sum.y, m.normal_sum.z};
637 const float lenN = Kokkos::sqrt(dot3(Nsum, Nsum));
638 if (lenN < 1e-9f)
639 return;
642 const F3 vA = ld3(velPred, realA), wA = ld3(angVelPred, realA);
643 F3 vB{0, 0, 0}, wB{0, 0, 0};
644 if (idB >= 0) {
645 vB = ld3(velPred, realB);
646 wB = ld3(angVelPred, realB);
647 } else {
648 vB = scale3(F3{m.wallVel_sum.x, m.wallVel_sum.y, m.wallVel_sum.z}, invN);
649 }
650 const F3 rAavg = scale3(F3{m.rA_sum.x, m.rA_sum.y, m.rA_sum.z}, invN);
651 const F3 rBavg = scale3(F3{m.rB_sum.x, m.rB_sum.y, m.rB_sum.z}, invN);
652 const F3 diffCenters = (idB < 0) ? rAavg : sub3(rAavg, rBavg);
653 const F3 vGrowth = scale3(diffCenters, growthRate);
654 float vn = dot3(vA, Nsum) + dot3(wA, TauA) + dot3(vB, F3{-Nsum.x, -Nsum.y, -Nsum.z}) +
655 dot3(wB, TauB);
656 vn += dot3(vGrowth, Nsum);
657 const float alignment = dot3(Nsum, diffCenters);
658 const float sgn = (alignment > 0.0f) ? 1.0f : -1.0f;
659 // Symmetric effective inverse mass (the release/bank ledger is momentum bookkeeping at
660 // the pair's own masses, independent of any stabilization sidedness this substep).
661 const F3 invIA = ld3(invInertia, realA);
662 const F3 invIB = (idB >= 0) ? ld3(invInertia, realB) : F3{0, 0, 0};
663 const F4 qA = F4{quat(realA, 0), quat(realA, 1), quat(realA, 2), quat(realA, 3)};
664 const F4 qB = (idB >= 0)
665 ? F4{quat(realB, 0), quat(realB, 1), quat(realB, 2), quat(realB, 3)}
666 : F4{0, 0, 0, 1};
667 const float Nsq = dot3(Nsum, Nsum);
668 const float wTotal = Nsq * invMass(realA) + genInvMass(TauA, invIA, qA) +
669 Nsq * ((idB >= 0) ? invMass(realB) : 0.0f) +
670 genInvMass(TauB, invIB, qB);
671 if (wTotal <= 0.0f)
672 return;
673 float e = restitutionNormal;
674 {
675 const float ra = m.restitution_sum * invN;
676 if (ra >= 0.0f)
677 e = ra;
678 }
679 const float v0til = sgn * vn0(idx);
680 const float vtilEnd = sgn * vn;
681 float owed = restBank(idx);
682 // Event state: refresh the peak on a kinetic approach, age it 1/256 per substep.
683 float vPeak = restVPeak(idx) * (1.0f - 1.0f / 256.0f);
684 if (v0til > restVelThreshold * lenN)
685 vPeak = Kokkos::fmax(vPeak, v0til / lenN);
686 if (vPeak > restVelThreshold) {
687 // Active event: bank e x this substep's compression FLUX (full applied normal impulse
688 // minus the reflection share — the momentum the chain actually transmitted; the
689 // m_eff-scale approach-destruction measure under-banks a chain-loaded impact by ~3000x),
690 // pay down the separation already delivered, and deduct the release channel's spend.
691 // A clean one-substep binary impact nets 0: pTot = (1+e) m_eff v0, pR = e m_eff v0.
692 const float pR = Kokkos::fmax(0.0f, -vtilEnd) / wTotal * lenN;
693 const float pTot = Kokkos::fmax(lambdaAcc(idx), 0.0f) * lenN;
694 const float pC = Kokkos::fmax(0.0f, pTot - pR);
695 owed += e * pC - pR - Kokkos::fmax(restRel(idx), 0.0f) * lenN;
696 restBank(idx) = Kokkos::fmax(owed, 0.0f);
697 restVPeak(idx) = vPeak;
698 } else {
699 restBank(idx) = 0.0f; // event aged out (or none): the residual budget evaporates
700 restVPeak(idx) = 0.0f;
701 }
702 });
703}
704
711inline void decayBodyOrphanKokkos(Kokkos::View<float*, CpMem> orphan,
712 Kokkos::View<float*, CpMem> orphanVPeak, int numOwned,
713 float restVelThreshold) {
714 CpExec space;
715 Kokkos::parallel_for(
716 "peclet::dem::rest_orphan_decay", Kokkos::RangePolicy<CpExec>(space, 0, numOwned),
717 KOKKOS_LAMBDA(int i) {
718 const float decayed = orphanVPeak(i) * (1.0f - 1.0f / 64.0f);
719 if (decayed <= restVelThreshold || orphan(i) <= 0.0f) {
720 orphan(i) = 0.0f;
721 orphanVPeak(i) = 0.0f;
722 } else {
723 orphan(i) *= (1.0f - 1.0f / 64.0f);
724 orphanVPeak(i) = decayed;
725 }
726 });
727}
728
738inline void scatterOrphanBanksKokkos(Kokkos::View<const unsigned long long*, CpMem> prevKeys,
739 Kokkos::View<const float*, CpMem> prevRestBank,
740 Kokkos::View<const float*, CpMem> prevRestVPeak,
741 Kokkos::View<const unsigned char*, CpMem> matched,
742 int prevCount, Kokkos::View<const float*, CpMem> invMass,
743 Kokkos::View<float*, CpMem> orphan,
744 Kokkos::View<float*, CpMem> orphanVPeak,
745 Kokkos::View<const int*, CpMem> gidSorted = {},
746 Kokkos::View<const int*, CpMem> slotSorted = {}) {
747 CpExec space;
748 const int nMap = static_cast<int>(gidSorted.extent(0));
749 const int nBody = static_cast<int>(orphan.extent(0));
750 Kokkos::parallel_for(
751 "peclet::dem::rest_orphan_scatter", Kokkos::RangePolicy<CpExec>(space, 0, prevCount),
752 KOKKOS_LAMBDA(int e) {
753 if (matched(e))
754 return;
755 const float owed = prevRestBank(e);
756 if (owed <= 0.0f)
757 return;
758 const unsigned long long k = prevKeys(e);
759 if (k == ~0ull)
760 return;
761 const unsigned hi = static_cast<unsigned>(k >> 32);
762 const unsigned lo = static_cast<unsigned>(k & 0xFFFFFFFFu);
763 auto resolve = [&](unsigned id) -> int {
764 if (nMap == 0) // single-GPU: identities ARE real slots
765 return (static_cast<int>(id) < nBody) ? static_cast<int>(id) : -1;
766 int a = 0, b = nMap; // MPI: binary-search the sorted gid -> slot map
767 while (a < b) {
768 const int m = (a + b) >> 1;
769 if (gidSorted(m) < static_cast<int>(id))
770 a = m + 1;
771 else
772 b = m;
773 }
774 return (a < nMap && gidSorted(a) == static_cast<int>(id)) ? slotSorted(a) : -1;
775 };
776 const int sA = resolve(hi);
777 const int sB = (lo != 0xFFFFFFFFu) ? resolve(lo) : -1;
778 const float vpk = prevRestVPeak(e);
779 float shareA = 1.0f; // boundary (wall) pair: everything to the particle
780 if (lo != 0xFFFFFFFFu) {
781 if (sB >= 0 && sA >= 0) {
782 const float wA = invMass(sA), wB = invMass(sB);
783 if (wA + wB > 0.0f)
784 shareA = wB / (wA + wB); // heavier endpoint (smaller invMass) keeps more
785 } else {
786 shareA = 0.5f; // body-body with an unresolvable endpoint (MPI edge): conservative half
787 }
788 }
789 if (sA >= 0 && shareA > 0.0f) {
790 Kokkos::atomic_add(&orphan(sA), owed * shareA);
791 Kokkos::atomic_max(&orphanVPeak(sA), vpk);
792 }
793 if (sB >= 0 && shareA < 1.0f) {
794 Kokkos::atomic_add(&orphan(sB), owed * (1.0f - shareA));
795 Kokkos::atomic_max(&orphanVPeak(sB), vpk);
796 }
797 });
798}
799
802inline std::tuple<double, float, int> restBankStatsKokkos(Kokkos::View<const float*, CpMem> bank,
803 int n) {
804 double s = 0.0;
805 float mx = 0.0f;
806 int cnt = 0;
807 if (n > 0) {
808 Kokkos::parallel_reduce(
809 "peclet::dem::rest_bank_stats", Kokkos::RangePolicy<CpExec>(0, n),
810 KOKKOS_LAMBDA(int i, double& ls, float& lm, int& lc) {
811 const float v = bank(i);
812 ls += v;
813 if (v > lm)
814 lm = v;
815 if (v > 0.0f)
816 ++lc;
817 },
818 s, Kokkos::Max<float>(mx), Kokkos::Sum<int>(cnt));
819 }
820 return {s, mx, cnt};
821}
822
830inline void computeSideFlagsKokkos(Kokkos::View<const ManifoldC*, CpMem> manifolds,
831 int numManifolds, Kokkos::View<const int*, CpMem> realIdx,
832 Kokkos::View<const unsigned char*, CpMem> persistent,
833 Kokkos::View<const unsigned char*, CpMem> grounded,
834 Kokkos::View<const float* [3], CpMem> posPred,
835 Kokkos::View<const float* [3], CpMem> velPred, F3 gHat,
836 float riseThr, Kokkos::View<unsigned char*, CpMem> sideFlag,
837 Kokkos::View<const float*, CpMem> vn0, float approachThr) {
838 using detail::ld3;
839 CpExec space;
840 Kokkos::parallel_for(
841 "peclet::dem::pgs_side_flags", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
842 KOKKOS_LAMBDA(int idx) {
843 const unsigned char wasPersistent = persistent(idx); // read BEFORE the write: the caller
844 sideFlag(idx) = 0; // may alias persistent and sideFlag (flag reuses the persistence view)
845 const ManifoldC m = manifolds(idx);
846 if (m.num_points <= 0 || m.bodyB < 0 || wasPersistent == 0)
847 return;
848 // BALLISTIC GATE: one-sided grounding is a statics device -- a pair whose pre-solve
849 // relative normal speed exceeds the quasi-static scale (a few substeps of free fall)
850 // is shock-loaded or shearing and must stay momentum-conserving. Without this, a fast
851 // impactor meets an infinite-mass bed (measured: a 5 m/s ball stops at the surface of a
852 // 25k bed) and flowing regions over-resist (silo discharge -24%).
853 if (Kokkos::fabs(vn0(idx)) > approachThr)
854 return;
855 const int realA = realIdx(m.bodyA), realB = realIdx(m.bodyB);
856 if (realA > realB)
857 return; // periodic dedup
858 const F3 dx = sub3(ldF3(posPred, m.bodyA), ldF3(posPred, m.bodyB));
859 const float up = -(dx.x * gHat.x + dx.y * gHat.y + dx.z * gHat.z); // >0: A above B
860 const float thr = 0.3f * Kokkos::sqrt(dot3(dx, dx));
861 if (up > thr && -dot3(ld3(velPred, realB), gHat) <= riseThr && grounded(realB) > 0) {
862 sideFlag(idx) = 1;
863 } else if (up < -thr && -dot3(ld3(velPred, realA), gHat) <= riseThr &&
864 grounded(realA) > 0) {
865 sideFlag(idx) = 2;
866 }
867 });
868}
869
872 Kokkos::View<const ManifoldC*, CpMem> manifolds, int numManifolds,
873 Kokkos::View<const float*, CpMem> invMass, Kokkos::View<const float* [3], CpMem> invInertia,
874 Kokkos::View<const float* [4], CpMem> quat, Kokkos::View<float* [3], CpMem> velPred,
875 Kokkos::View<float* [3], CpMem> angVelPred, Kokkos::View<const int*, CpMem> realIdx,
876 Kokkos::View<const float*, CpMem> warmP, Kokkos::View<float* [3], CpMem> warmT) {
877 using detail::ld3;
878 CpExec space;
879 Kokkos::parallel_for(
880 "peclet::dem::pgs_warm_apply", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
881 KOKKOS_LAMBDA(int idx) {
882 const float p = warmP(idx);
883 F3 lt{warmT(idx, 0), warmT(idx, 1), warmT(idx, 2)};
884 const bool hasT = (lt.x != 0.0f || lt.y != 0.0f || lt.z != 0.0f);
885 if (p == 0.0f && !hasT)
886 return;
887 const ManifoldC m = manifolds(idx);
888 if (m.num_points <= 0)
889 return;
890 const int idA = m.bodyA, idB = m.bodyB;
891 const int realA = realIdx(idA);
892 const int realB = (idB >= 0) ? realIdx(idB) : idB;
893 if (idB >= 0 && realA > realB)
894 return; // periodic dedup (warmP is 0 for dups anyway)
895 const float invN = 1.0f / static_cast<float>(m.num_points);
896 const F3 Nsum{m.normal_sum.x, m.normal_sum.y, m.normal_sum.z};
899 const F3 rAavg = scale3(F3{m.rA_sum.x, m.rA_sum.y, m.rA_sum.z}, invN);
900 const F3 rBavg = scale3(F3{m.rB_sum.x, m.rB_sum.y, m.rB_sum.z}, invN);
901 const F3 diffCenters = (idB < 0) ? rAavg : sub3(rAavg, rBavg);
902 const float alignment = dot3(Nsum, diffCenters);
903 const float sgn = (alignment > 0.0f) ? 1.0f : -1.0f;
904 const float lambda = -sgn * p;
905 const F3 Jlin = scale3(Nsum, lambda);
906 const float invMassA = invMass(realA);
907 const float invMassB = (idB >= 0) ? invMass(realB) : 0.0f;
908 Kokkos::atomic_add(&velPred(realA, 0), Jlin.x * invMassA);
909 Kokkos::atomic_add(&velPred(realA, 1), Jlin.y * invMassA);
910 Kokkos::atomic_add(&velPred(realA, 2), Jlin.z * invMassA);
911 {
912 const F4 qA = F4{quat(realA, 0), quat(realA, 1), quat(realA, 2), quat(realA, 3)};
913 const F3 Jl = invRotateVector(qA, scale3(TauA, lambda));
914 const F3 invIA = ld3(invInertia, realA);
915 const F3 dwl{Jl.x * invIA.x, Jl.y * invIA.y, Jl.z * invIA.z};
916 const F3 dww = rotateVector(qA, dwl);
917 Kokkos::atomic_add(&angVelPred(realA, 0), dww.x);
918 Kokkos::atomic_add(&angVelPred(realA, 1), dww.y);
919 Kokkos::atomic_add(&angVelPred(realA, 2), dww.z);
920 }
921 if (idB >= 0) {
922 Kokkos::atomic_add(&velPred(realB, 0), -Jlin.x * invMassB);
923 Kokkos::atomic_add(&velPred(realB, 1), -Jlin.y * invMassB);
924 Kokkos::atomic_add(&velPred(realB, 2), -Jlin.z * invMassB);
925 const F4 qB = F4{quat(realB, 0), quat(realB, 1), quat(realB, 2), quat(realB, 3)};
926 const F3 Jl = invRotateVector(qB, scale3(TauB, lambda));
927 const F3 invIB = ld3(invInertia, realB);
928 const F3 dwl{Jl.x * invIB.x, Jl.y * invIB.y, Jl.z * invIB.z};
929 const F3 dww = rotateVector(qB, dwl);
930 Kokkos::atomic_add(&angVelPred(realB, 0), dww.x);
931 Kokkos::atomic_add(&angVelPred(realB, 1), dww.y);
932 Kokkos::atomic_add(&angVelPred(realB, 2), dww.z);
933 }
934 // Tangential warm impulse: project the stored world-frame accumulator onto the CURRENT
935 // tangent plane (the normal moved a little between substeps), write it back so the sweep
936 // accumulator starts consistent, then apply +lt on A / -lt on B at the averaged arms.
937 if (hasT) {
938 const float lenN2 = Kokkos::sqrt(dot3(Nsum, Nsum));
939 if (lenN2 > 1e-9f) {
940 const F3 nhat = scale3(Nsum, 1.0f / lenN2);
941 lt = sub3(lt, scale3(nhat, dot3(lt, nhat)));
942 }
943 warmT(idx, 0) = lt.x;
944 warmT(idx, 1) = lt.y;
945 warmT(idx, 2) = lt.z;
946 Kokkos::atomic_add(&velPred(realA, 0), lt.x * invMassA);
947 Kokkos::atomic_add(&velPred(realA, 1), lt.y * invMassA);
948 Kokkos::atomic_add(&velPred(realA, 2), lt.z * invMassA);
949 {
950 const F4 qA = F4{quat(realA, 0), quat(realA, 1), quat(realA, 2), quat(realA, 3)};
951 const F3 Jl = invRotateVector(qA, cross3v(rAavg, lt));
952 const F3 invIA = ld3(invInertia, realA);
953 const F3 dwl{Jl.x * invIA.x, Jl.y * invIA.y, Jl.z * invIA.z};
954 const F3 dww = rotateVector(qA, dwl);
955 Kokkos::atomic_add(&angVelPred(realA, 0), dww.x);
956 Kokkos::atomic_add(&angVelPred(realA, 1), dww.y);
957 Kokkos::atomic_add(&angVelPred(realA, 2), dww.z);
958 }
959 if (idB >= 0) {
960 Kokkos::atomic_add(&velPred(realB, 0), -lt.x * invMassB);
961 Kokkos::atomic_add(&velPred(realB, 1), -lt.y * invMassB);
962 Kokkos::atomic_add(&velPred(realB, 2), -lt.z * invMassB);
963 const F4 qB = F4{quat(realB, 0), quat(realB, 1), quat(realB, 2), quat(realB, 3)};
964 const F3 Jl = invRotateVector(qB, cross3v(rBavg, scale3(lt, -1.0f)));
965 const F3 invIB = ld3(invInertia, realB);
966 const F3 dwl{Jl.x * invIB.x, Jl.y * invIB.y, Jl.z * invIB.z};
967 const F3 dww = rotateVector(qB, dwl);
968 Kokkos::atomic_add(&angVelPred(realB, 0), dww.x);
969 Kokkos::atomic_add(&angVelPred(realB, 1), dww.y);
970 Kokkos::atomic_add(&angVelPred(realB, 2), dww.z);
971 }
972 }
973 });
974}
975
986 Kokkos::View<const ManifoldC*, CpMem> manifolds;
987 Kokkos::View<const float*, CpMem> invMass;
988 Kokkos::View<const float* [3], CpMem> invInertia;
989 Kokkos::View<const float* [4], CpMem> quat;
990 Kokkos::View<float* [3], CpMem> velPred;
991 Kokkos::View<float* [3], CpMem> angVelPred;
992 Kokkos::View<const int*, CpMem> realIdx;
996 Kokkos::View<float, CpMem> maxApproach;
997 // Quasi-static residual (corrections on contacts with |vn0| <= 4 restVelThreshold): the
998 // multilevel pass's stop criterion. The colored caller aliases this to maxApproach (the
999 // duplicate atomic_max is idempotent), so main-loop behaviour is unchanged.
1000 Kokkos::View<float, CpMem> maxApproachQS;
1001 Kokkos::View<float*, CpMem> lambdaAcc;
1002 Kokkos::View<const float*, CpMem> vn0;
1003 Kokkos::View<const unsigned char*, CpMem> sideFlag;
1004 Kokkos::View<float* [3], CpMem> lambdaT;
1006 Kokkos::View<const float* [3], CpMem> vt0;
1008 Kokkos::View<const float*, CpMem> posImpulse;
1009 // Event-level (Poisson) restitution release (restitutionModel == 1; all three views empty
1010 // otherwise): restBank(idx) is the pair's remaining OWED separation impulse (physical units,
1011 // warm-carried by pair key), restRel(idx) the per-substep release accumulator (lambda units,
1012 // zeroed each substep), and restPersistent(idx) the existed-last-substep flag (used only by
1013 // the restNewtonOff A/B). Per-substep Newton restitution stays ALIVE by default alongside the
1014 // bank — its micro-reflections are genuine returned energy, and the accounting's pR term
1015 // deducts each one from the owed budget so the channels never double-count (measured: forcing
1016 // e = 0 on persistent contacts cost more rebound than the bank recovered). restBank is
1017 // writable: a releasing pair whose own budget rails can DRAW from its bodies' orphan accounts
1018 // (transferred into restBank at the moment of need, so the post-solve accounting sees one
1019 // consistent pair ledger). Same-body contacts never run concurrently (colouring), so the
1020 // in-place body-account read-modify-write is race-free without atomics.
1021 Kokkos::View<float*, CpMem> restBank;
1022 Kokkos::View<float*, CpMem> restRel;
1023 Kokkos::View<const unsigned char*, CpMem> restPersistent;
1024 // Event peak approach speed (physical): caps the release separation-velocity target at
1025 // e x vPeak — the event-level rebound speed — so a large flux-banked budget against a light
1026 // partner becomes a SUSTAINED unloading push over many substeps (the Hertz-like collective
1027 // rebound) instead of an impulsive dump (a 190 m/s kick to a 1e-5 kg grain, measured absurd).
1028 Kokkos::View<const float*, CpMem> restVPeak;
1029 // Optional release sidedness (restOneSided; the shock-propagation pass in reverse): hold a
1030 // grounded LOWER side and push only the upper. Measured WORSE than symmetric release on the
1031 // 25k Dosta impact (+0.60 vs +0.88 rebound) — the light partner's downward reaction is what
1032 // re-compresses and re-releases the layers below, so symmetric stays the default; kept as an
1033 // env-gated A/B (PECLET_DEM_REST_ONESIDED=1).
1034 F3 restGHat{0, 0, 0};
1035 Kokkos::View<const unsigned char*, CpMem> restGrounded;
1036 // A/B toggles (env-driven, see solve_driver.hpp): bank-owns-restitution / one-sided release.
1037 bool restNewtonOff = false;
1038 bool restOneSided = false;
1039 // Orphan accounts (see Particles::bodyOrphan): balance + carried event peak speed per REAL
1040 // body. The peak matters as much as the balance — a contact formed late under a decelerating
1041 // impactor only saw the residual approach, so its own e*vPeak target would cap the rebound at
1042 // e x the late-stage speed; the orphaned peak restores the full event's velocity scale.
1043 Kokkos::View<float*, CpMem> restOrphan;
1044 Kokkos::View<const float*, CpMem> restOrphanVPeak;
1045
1046 KOKKOS_FUNCTION void solveOne(int idx) const {
1047 using detail::genInvMass;
1048 using detail::ld3;
1049 {
1050 const ManifoldC m = manifolds(idx);
1051 const int idA = m.bodyA, idB = m.bodyB;
1052 const int realA = realIdx(idA);
1053 const int realB = (idB >= 0) ? realIdx(idB) : idB;
1054 const float invMassA = invMass(realA);
1055 const float invMassB = (idB >= 0) ? invMass(realB) : 0.0f;
1056 const F3 invIA = ld3(invInertia, realA);
1057 const F3 invIB = (idB >= 0) ? ld3(invInertia, realB) : F3{0, 0, 0};
1058 const F4 qA = F4{quat(realA, 0), quat(realA, 1), quat(realA, 2), quat(realA, 3)};
1059 const F4 qB = (idB >= 0) ? F4{quat(realB, 0), quat(realB, 1), quat(realB, 2), quat(realB, 3)}
1060 : F4{0, 0, 0, 1};
1061 const F3 vA = ld3(velPred, realA), wA = ld3(angVelPred, realA);
1062 F3 vB{0, 0, 0}, wB{0, 0, 0};
1063 const float invN = 1.0f / static_cast<float>(m.num_points);
1064 float restitution = restitutionNormal;
1065 if (idB >= 0) {
1066 vB = ld3(velPred, realB);
1067 wB = ld3(angVelPred, realB);
1068 } else {
1069 vB = scale3(F3{m.wallVel_sum.x, m.wallVel_sum.y, m.wallVel_sum.z}, invN);
1070 }
1071 { // per-wall AND per-pair material override (a < 0 average keeps the global material)
1072 const float ra = m.restitution_sum * invN;
1073 if (ra >= 0.0f)
1074 restitution = ra;
1075 }
1076 const float eMat = restitution; // raw material e (release cap), before the event gates
1077 const F3 Nsum{m.normal_sum.x, m.normal_sum.y, m.normal_sum.z};
1080 const F3 rAavg = scale3(F3{m.rA_sum.x, m.rA_sum.y, m.rA_sum.z}, invN);
1081 const F3 rBavg = scale3(F3{m.rB_sum.x, m.rB_sum.y, m.rB_sum.z}, invN);
1082 const float lenN = Kokkos::sqrt(dot3(Nsum, Nsum));
1083 if (lenN < 1e-9f)
1084 return;
1085 const F3 diffCenters = (idB < 0) ? rAavg : sub3(rAavg, rBavg);
1086 const F3 vGrowth = scale3(diffCenters, growthRate);
1087 float vn = dot3(vA, Nsum) + dot3(wA, TauA) + dot3(vB, F3{-Nsum.x, -Nsum.y, -Nsum.z}) +
1088 dot3(wB, TauB);
1089 vn += dot3(vGrowth, Nsum);
1090 const float alignment = dot3(Nsum, diffCenters);
1091 const float sgn = (alignment > 0.0f) ? 1.0f : -1.0f;
1092 const float Nsq = dot3(Nsum, Nsum);
1093 float wA_n = Nsq * invMassA + genInvMass(TauA, invIA, qA);
1094 float wB_n = Nsq * invMassB + genInvMass(TauB, invIB, qB);
1095 // Shock propagation INSIDE the PGS sweep (Guendelman staged solve, per-contact form):
1096 // sidedness was decided ONCE this substep (computeSideFlagsKokkos) so the warm start,
1097 // accumulator and every sweep share one consistent ledger. The held ground side absorbs
1098 // the reaction (recursively down to the floor); e is forced 0 on one-sided contacts.
1099 bool applyA = true, applyB = true;
1100 const unsigned char sf = sideFlag(idx);
1101 if (sf == 1) {
1102 wB_n = 0.0f;
1103 applyB = false;
1104 restitution = 0.0f;
1105 } else if (sf == 2) {
1106 wA_n = 0.0f;
1107 applyA = false;
1108 restitution = 0.0f;
1109 }
1110 const float wTotal = wA_n + wB_n;
1111 if (wTotal <= 0.0f)
1112 return;
1113 // Restitution bias on the PRE-SOLVE approach (resting threshold as in the one-shot path).
1114 const float v0til = sgn * vn0(idx);
1115 if (Kokkos::fabs(vn0(idx)) < restVelThreshold * lenN)
1116 restitution = 0.0f;
1117 // Poisson mode keeps per-substep Newton restitution ALIVE alongside the bank: the
1118 // micro-reflections it produces are genuine returned energy (measured: forcing e = 0 on
1119 // persistent contacts cost more rebound than the bank recovered), and the accounting's pR
1120 // term deducts every reflection from the owed budget, so the two channels never
1121 // double-count. (PECLET_DEM_REST_NEWTON_OFF=1 re-enables the bank-owns-everything A/B.)
1122 if (restRel.extent(0) > 0 && restPersistent.extent(0) > 0 && restPersistent(idx) != 0 &&
1124 restitution = 0.0f;
1125 const float target = (v0til > 0.0f) ? -restitution * v0til : 0.0f;
1126 const float vtil = sgn * vn;
1127 const float dp = (vtil - target) / wTotal;
1128 const float pOld = lambdaAcc(idx);
1129 float pNew = pOld + dp;
1130 if (pNew < 0.0f)
1131 pNew = 0.0f;
1132 const float dApplied = pNew - pOld;
1133 if (dApplied != 0.0f) {
1134 lambdaAcc(idx) = pNew;
1135 Kokkos::atomic_max(&maxApproach(), Kokkos::fabs(dApplied) * wTotal / lenN);
1136 if (Kokkos::fabs(vn0(idx)) <= 4.0f * restVelThreshold * lenN)
1137 Kokkos::atomic_max(&maxApproachQS(), Kokkos::fabs(dApplied) * wTotal / lenN);
1138 const float lambda = -sgn * dApplied;
1139 const F3 Jlin = scale3(Nsum, lambda);
1140 const F3 JangA = scale3(TauA, lambda);
1141 const F3 JangB = scale3(TauB, lambda);
1142 if (applyA) {
1143 velPred(realA, 0) += Jlin.x * invMassA;
1144 velPred(realA, 1) += Jlin.y * invMassA;
1145 velPred(realA, 2) += Jlin.z * invMassA;
1146 {
1147 const F3 Jl = invRotateVector(qA, JangA);
1148 const F3 dwl{Jl.x * invIA.x, Jl.y * invIA.y, Jl.z * invIA.z};
1149 const F3 dww = rotateVector(qA, dwl);
1150 angVelPred(realA, 0) += dww.x;
1151 angVelPred(realA, 1) += dww.y;
1152 angVelPred(realA, 2) += dww.z;
1153 }
1154 }
1155 if (idB >= 0 && applyB) {
1156 velPred(realB, 0) += -Jlin.x * invMassB;
1157 velPred(realB, 1) += -Jlin.y * invMassB;
1158 velPred(realB, 2) += -Jlin.z * invMassB;
1159 const F3 Jl = invRotateVector(qB, JangB);
1160 const F3 dwl{Jl.x * invIB.x, Jl.y * invIB.y, Jl.z * invIB.z};
1161 const F3 dww = rotateVector(qB, dwl);
1162 angVelPred(realB, 0) += dww.x;
1163 angVelPred(realB, 1) += dww.y;
1164 angVelPred(realB, 2) += dww.z;
1165 }
1166 } // dApplied != 0
1167
1168 // ---- Event-level (Poisson) restitution release ----
1169 // A pair with banked compression (restBank > 0) that is NOT in a kinetic approach pushes
1170 // toward the event's separation-velocity target -owed*w — what releasing the full remaining
1171 // budget delivers against the pair's effective mass, i.e. the event-level rebound speed.
1172 // The push runs through its OWN accumulator clamped to [0, owed] (the friction-cone-shaped
1173 // budget cap): a free pair leaves at the target speed, while a loaded chain re-absorbs the
1174 // attempt through lambda >= 0 on its other contacts with the total injected impulse bounded
1175 // by the budget — velocity-targeted, never impulsive, no unbounded force fight. One-sided
1176 // (side-flagged) contacts are held externally and never release. Spent budget is deducted
1177 // once per substep by updateRestitutionBankKokkos. Unloading detection: KINETICALLY
1178 // separating pre-solve approach (beyond the resting threshold) — jitter separations during
1179 // compression (position-solve pushback, chain oscillation) fire sub-threshold every substep
1180 // and would drain the bank as fast as it fills (measured: bank plateaued at ~1/6 of the
1181 // event flux with a v0til < 0 gate); the genuine rebound onset separates kinetically.
1182 if (restRel.extent(0) > 0 && sf == 0 && v0til < -restVelThreshold * lenN) {
1183 float owed = restBank(idx);
1184 float vPeak = restVPeak.extent(0) > 0 ? restVPeak(idx) : 0.0f;
1185 // Orphan availability at the endpoints: makes budget-less fresh pairs under an event
1186 // carrier eligible, and lifts the velocity target to the orphaned event peak.
1187 float orphA = 0.0f, orphB = 0.0f;
1188 if (restOrphan.extent(0) > 0) {
1189 orphA = restOrphan(realA);
1190 if (orphA > 0.0f)
1191 vPeak = Kokkos::fmax(vPeak, restOrphanVPeak(realA));
1192 if (idB >= 0) {
1193 orphB = restOrphan(realB);
1194 if (orphB > 0.0f)
1195 vPeak = Kokkos::fmax(vPeak, restOrphanVPeak(realB));
1196 }
1197 }
1198 if ((owed > 0.0f || orphA > 0.0f || orphB > 0.0f) && vPeak > 0.0f) {
1199 const F3 vA3 = ld3(velPred, realA), wA3 = ld3(angVelPred, realA);
1200 F3 vB3{0, 0, 0}, wB3{0, 0, 0};
1201 if (idB >= 0) {
1202 vB3 = ld3(velPred, realB);
1203 wB3 = ld3(angVelPred, realB);
1204 } else {
1205 vB3 = scale3(F3{m.wallVel_sum.x, m.wallVel_sum.y, m.wallVel_sum.z}, invN);
1206 }
1207 float vn3 = dot3(vA3, Nsum) + dot3(wA3, TauA) + dot3(vB3, F3{-Nsum.x, -Nsum.y, -Nsum.z}) +
1208 dot3(wB3, TauB);
1209 vn3 += dot3(vGrowth, Nsum);
1210 const float vtil3 = sgn * vn3;
1211 // One-sided release against a grounded support (see restGHat comment): hold the lower
1212 // grounded side, push only the other. dx = posA - posB = rB - rA (contact identity).
1213 bool relA = true, relB = (idB >= 0);
1214 float wRel = wTotal;
1215 if (restOneSided && idB >= 0 && restGrounded.extent(0) > 0) {
1216 const F3 dx = sub3(rBavg, rAavg);
1217 const float up = -(dx.x * restGHat.x + dx.y * restGHat.y + dx.z * restGHat.z);
1218 const float thr3 = 0.3f * Kokkos::sqrt(dot3(dx, dx));
1219 if (up > thr3 && restGrounded(realB) > 0) { // A above grounded B: push A only
1220 relB = false;
1221 wRel = Nsq * invMassA + genInvMass(TauA, invIA, qA);
1222 } else if (up < -thr3 && restGrounded(realA) > 0) { // B above grounded A
1223 relA = false;
1224 wRel = Nsq * invMassB + genInvMass(TauB, invIB, qB);
1225 }
1226 }
1227 if (wRel > 0.0f) {
1228 // Separation-velocity target: the event-level rebound speed e x vPeak. The budget is
1229 // enforced by the accumulator clamp alone — folding it into the velocity target
1230 // (owed * wRel) prematurely stalls a one-sided release against a heavy impactor
1231 // (owed/m_ball ~ cm/s) with most of the budget unspent. Physical velocity -> vtil
1232 // units is x lenN.
1233 const float vTphys = eMat * vPeak;
1234 const float targetR = -vTphys * lenN;
1235 const float dpR = (vtil3 - targetR) / wRel;
1236 float cap = owed / lenN; // physical budget in lambda units
1237 const float rOld = restRel(idx);
1238 float rNew = rOld + dpR;
1239 if (rNew < 0.0f)
1240 rNew = 0.0f;
1241 // Own budget railed with orphan balance at the endpoints: draw the shortfall from the
1242 // body accounts INTO the pair bank (heavier-first order is irrelevant; drained in
1243 // sequence). The drawn amount is spent by this very increment, so the post-solve
1244 // accounting's owed -= released cancels it exactly.
1245 if (rNew > cap && (orphA > 0.0f || orphB > 0.0f)) {
1246 float need = (rNew - cap) * lenN;
1247 float draw = 0.0f;
1248 if (orphA > 0.0f) {
1249 const float d = Kokkos::fmin(need, orphA);
1250 restOrphan(realA) = orphA - d;
1251 need -= d;
1252 draw += d;
1253 }
1254 if (need > 0.0f && orphB > 0.0f) {
1255 const float d = Kokkos::fmin(need, orphB);
1256 restOrphan(realB) = orphB - d;
1257 draw += d;
1258 }
1259 if (draw > 0.0f) {
1260 owed += draw;
1261 restBank(idx) = owed;
1262 cap = owed / lenN;
1263 }
1264 }
1265 if (rNew > cap)
1266 rNew = cap;
1267 const float dR = rNew - rOld;
1268 if (dR != 0.0f) {
1269 restRel(idx) = rNew;
1270 Kokkos::atomic_max(&maxApproach(), Kokkos::fabs(dR) * wRel / lenN);
1271 if (Kokkos::fabs(vn0(idx)) <= 4.0f * restVelThreshold * lenN)
1272 Kokkos::atomic_max(&maxApproachQS(), Kokkos::fabs(dR) * wRel / lenN);
1273 const float lambdaR = -sgn * dR;
1274 const F3 JlinR = scale3(Nsum, lambdaR);
1275 if (relA) {
1276 velPred(realA, 0) += JlinR.x * invMassA;
1277 velPred(realA, 1) += JlinR.y * invMassA;
1278 velPred(realA, 2) += JlinR.z * invMassA;
1279 const F3 Jl = invRotateVector(qA, scale3(TauA, lambdaR));
1280 const F3 dwl{Jl.x * invIA.x, Jl.y * invIA.y, Jl.z * invIA.z};
1281 const F3 dww = rotateVector(qA, dwl);
1282 angVelPred(realA, 0) += dww.x;
1283 angVelPred(realA, 1) += dww.y;
1284 angVelPred(realA, 2) += dww.z;
1285 }
1286 if (relB) {
1287 velPred(realB, 0) += -JlinR.x * invMassB;
1288 velPred(realB, 1) += -JlinR.y * invMassB;
1289 velPred(realB, 2) += -JlinR.z * invMassB;
1290 const F3 Jl = invRotateVector(qB, scale3(TauB, lambdaR));
1291 const F3 dwl{Jl.x * invIB.x, Jl.y * invIB.y, Jl.z * invIB.z};
1292 const F3 dww = rotateVector(qB, dwl);
1293 angVelPred(realB, 0) += dww.x;
1294 angVelPred(realB, 1) += dww.y;
1295 angVelPred(realB, 2) += dww.z;
1296 }
1297 }
1298 }
1299 }
1300 }
1301
1302 // ---- Friction cone (sequential tangential impulse) ----
1303 // Accumulated world-frame tangential impulse lambdaT on body A, updated by the same
1304 // colored nonlinear GS: incremental impulse -vt/w_t along the current slip direction,
1305 // then projection onto the Coulomb disc |lambdaT| <= mu * lambdaN (physical impulse:
1306 // the normal accumulator is scaled by |Nsum|). Static stick falls out naturally: at
1307 // vt = 0 the accumulator holds whatever tangential load the cone admits. Sidedness
1308 // mirrors the normal solve (a held side neither moves nor adds compliance).
1309 {
1310 float mu = frictionDynamic;
1311 const float fa = m.friction_sum * invN;
1312 if (fa >= 0.0f)
1313 mu = fa;
1314 F3 ltOld{lambdaT(idx, 0), lambdaT(idx, 1), lambdaT(idx, 2)};
1315 const bool haveOld = (ltOld.x != 0.0f || ltOld.y != 0.0f || ltOld.z != 0.0f);
1316 if (mu > 0.0f || haveOld) {
1317 const F3 nhat = scale3(Nsum, 1.0f / lenN);
1318 const F3 vA2 = ld3(velPred, realA), wA2 = ld3(angVelPred, realA);
1319 F3 vB2{0, 0, 0}, wB2{0, 0, 0};
1320 if (idB >= 0) {
1321 vB2 = ld3(velPred, realB);
1322 wB2 = ld3(angVelPred, realB);
1323 } else {
1324 vB2 = scale3(F3{m.wallVel_sum.x, m.wallVel_sum.y, m.wallVel_sum.z}, invN);
1325 }
1326 const F3 vrel = sub3(add3(vA2, cross3v(wA2, rAavg)), add3(vB2, cross3v(wB2, rBavg)));
1327 const F3 vt = sub3(vrel, scale3(nhat, dot3(vrel, nhat)));
1328 // Walton tangential restitution: for a COLLIDING contact the target surface
1329 // velocity is -beta * vt0 (pre-solve tangential velocity); sustained contacts
1330 // (below the resting threshold) and one-sided stabilization contacts run beta = 0
1331 // -- the same event classification that gates normal restitution. The cone clamp
1332 // below turns this into the stick/slide transition of the (e, mu, beta) law.
1333 float beta = restitutionTangent;
1334 if (beta != 0.0f && (sf != 0 || Kokkos::fabs(vn0(idx)) < restVelThreshold * lenN))
1335 beta = 0.0f;
1336 F3 vtErr = vt;
1337 if (beta != 0.0f && vt0.extent(0) > 0) {
1338 F3 v0{vt0(idx, 0), vt0(idx, 1), vt0(idx, 2)};
1339 v0 = sub3(v0, scale3(nhat, dot3(v0, nhat))); // current tangent plane
1340 vtErr = add3(vt, scale3(v0, beta));
1341 }
1342 const float vtLen = Kokkos::sqrt(dot3(vtErr, vtErr));
1343 F3 ltNew = ltOld;
1344 float wT = invMassA + invMassB;
1345 if (vtLen > 1e-9f) {
1346 const F3 that = scale3(vtErr, 1.0f / vtLen);
1347 float wAt = (sf == 2) ? 0.0f : invMassA + genInvMass(cross3v(rAavg, that), invIA, qA);
1348 float wBt = (sf == 1 || idB < 0)
1349 ? 0.0f
1350 : invMassB + genInvMass(cross3v(rBavg, that), invIB, qB);
1351 wT = wAt + wBt;
1352 if (wT > 1e-9f)
1353 ltNew = add3(ltOld, scale3(that, -vtLen / wT));
1354 }
1355 ltNew = sub3(ltNew, scale3(nhat, dot3(ltNew, nhat)));
1356 // Coulomb bound = mu * TOTAL normal load: velocity-impulse channel (lambdaAcc,
1357 // physical impulse = lambdaAcc * |Nsum|) + the position-projection channel carried
1358 // from last substep (already physical impulse units).
1359 float nTot = Kokkos::fmax(lambdaAcc(idx), 0.0f) * lenN;
1360 // The carry is a QUASI-STATIC corrector: for colliding contacts the one-substep lag
1361 // double-counts (crater contacts already carry a large velocity impulse) -- gate by
1362 // the same event classification as e and beta.
1363 if (posImpulse.extent(0) > 0 && Kokkos::fabs(vn0(idx)) < restVelThreshold * lenN)
1364 nTot += Kokkos::fmax(posImpulse(idx), 0.0f);
1365 const float bound = mu * nTot;
1366 const float ltLen = Kokkos::sqrt(dot3(ltNew, ltNew));
1367 if (ltLen > bound)
1368 ltNew = (bound > 0.0f) ? scale3(ltNew, bound / ltLen) : F3{0, 0, 0};
1369 const F3 dApp = sub3(ltNew, ltOld);
1370 if (dApp.x != 0.0f || dApp.y != 0.0f || dApp.z != 0.0f) {
1371 lambdaT(idx, 0) = ltNew.x;
1372 lambdaT(idx, 1) = ltNew.y;
1373 lambdaT(idx, 2) = ltNew.z;
1374 const float dLen = Kokkos::sqrt(dot3(dApp, dApp));
1375 if (wT > 1e-9f) {
1376 Kokkos::atomic_max(&maxApproach(), dLen * wT);
1377 if (Kokkos::fabs(vn0(idx)) <= 4.0f * restVelThreshold * lenN)
1378 Kokkos::atomic_max(&maxApproachQS(), dLen * wT);
1379 }
1380 if (applyA) {
1381 velPred(realA, 0) += dApp.x * invMassA;
1382 velPred(realA, 1) += dApp.y * invMassA;
1383 velPred(realA, 2) += dApp.z * invMassA;
1384 const F3 Jl = invRotateVector(qA, cross3v(rAavg, dApp));
1385 const F3 dwl{Jl.x * invIA.x, Jl.y * invIA.y, Jl.z * invIA.z};
1386 const F3 dww = rotateVector(qA, dwl);
1387 angVelPred(realA, 0) += dww.x;
1388 angVelPred(realA, 1) += dww.y;
1389 angVelPred(realA, 2) += dww.z;
1390 }
1391 if (idB >= 0 && applyB) {
1392 velPred(realB, 0) += -dApp.x * invMassB;
1393 velPred(realB, 1) += -dApp.y * invMassB;
1394 velPred(realB, 2) += -dApp.z * invMassB;
1395 const F3 Jl = invRotateVector(qB, cross3v(rBavg, scale3(dApp, -1.0f)));
1396 const F3 dwl{Jl.x * invIB.x, Jl.y * invIB.y, Jl.z * invIB.z};
1397 const F3 dww = rotateVector(qB, dwl);
1398 angVelPred(realB, 0) += dww.x;
1399 angVelPred(realB, 1) += dww.y;
1400 angVelPred(realB, 2) += dww.z;
1401 }
1402 }
1403 }
1404 }
1405 }
1406 }
1407};
1408
1413 Kokkos::View<const ManifoldC*, CpMem> manifolds, Kokkos::View<const float*, CpMem> invMass,
1414 Kokkos::View<const float* [3], CpMem> invInertia, Kokkos::View<const float* [4], CpMem> quat,
1415 Kokkos::View<float* [3], CpMem> velPred, Kokkos::View<float* [3], CpMem> angVelPred,
1416 Kokkos::View<const int*, CpMem> realIdx, float growthRate, float restitutionNormal,
1417 float restVelThreshold, Kokkos::View<float, CpMem> maxApproach,
1418 Kokkos::View<float, CpMem> maxApproachQS, Kokkos::View<float*, CpMem> lambdaAcc,
1419 Kokkos::View<const float*, CpMem> vn0, Kokkos::View<const unsigned char*, CpMem> sideFlag,
1420 Kokkos::View<float* [3], CpMem> lambdaT, float frictionDynamic,
1421 Kokkos::View<const float* [3], CpMem> vt0, float restitutionTangent,
1422 Kokkos::View<const float*, CpMem> posImpulse, Kokkos::View<float*, CpMem> restBank,
1423 Kokkos::View<float*, CpMem> restRel, Kokkos::View<const unsigned char*, CpMem> restPersistent,
1424 Kokkos::View<const float*, CpMem> restVPeak, F3 restGHat,
1425 Kokkos::View<const unsigned char*, CpMem> restGrounded, bool restNewtonOff, bool restOneSided,
1426 Kokkos::View<float*, CpMem> restOrphan, Kokkos::View<const float*, CpMem> restOrphanVPeak) {
1427 return PGSManifoldSweep{manifolds,
1428 invMass,
1429 invInertia,
1430 quat,
1431 velPred,
1432 angVelPred,
1433 realIdx,
1434 growthRate,
1435 restitutionNormal,
1436 restVelThreshold,
1437 maxApproach,
1438 maxApproachQS.data() ? maxApproachQS : maxApproach,
1439 lambdaAcc,
1440 vn0,
1441 sideFlag,
1442 lambdaT,
1443 frictionDynamic,
1444 vt0,
1445 restitutionTangent,
1446 posImpulse,
1447 restBank,
1448 restRel,
1449 restPersistent,
1450 restVPeak,
1451 restGHat,
1452 restGrounded,
1453 restNewtonOff,
1454 restOneSided,
1455 restOrphan,
1456 restOrphanVPeak};
1457}
1458
1463 Kokkos::View<const ManifoldC*, CpMem> manifolds, int numManifolds,
1464 Kokkos::View<const int*, CpMem> mColor, int numColors,
1465 Kokkos::View<const float*, CpMem> invMass, Kokkos::View<const float* [3], CpMem> invInertia,
1466 Kokkos::View<const float* [4], CpMem> quat, Kokkos::View<float* [3], CpMem> velPred,
1467 Kokkos::View<float* [3], CpMem> angVelPred, Kokkos::View<const int*, CpMem> realIdx,
1468 float growthRate, float restitutionNormal, float restVelThreshold,
1469 Kokkos::View<float, CpMem> maxApproach, Kokkos::View<float*, CpMem> lambdaAcc,
1470 Kokkos::View<const float*, CpMem> vn0, Kokkos::View<const unsigned char*, CpMem> sideFlag,
1471 Kokkos::View<float* [3], CpMem> lambdaT, float frictionDynamic,
1472 Kokkos::View<const float* [3], CpMem> vt0 = {}, float restitutionTangent = 0.0f,
1473 Kokkos::View<const float*, CpMem> posImpulse = {},
1474 Kokkos::View<float, CpMem> maxApproachQS = {}, Kokkos::View<float*, CpMem> restBank = {},
1475 Kokkos::View<float*, CpMem> restRel = {},
1476 Kokkos::View<const unsigned char*, CpMem> restPersistent = {},
1477 Kokkos::View<const float*, CpMem> restVPeak = {}, F3 restGHat = {},
1478 Kokkos::View<const unsigned char*, CpMem> restGrounded = {}, bool restNewtonOff = false,
1479 bool restOneSided = false, Kokkos::View<float*, CpMem> restOrphan = {},
1480 Kokkos::View<const float*, CpMem> restOrphanVPeak = {},
1481 Kokkos::View<const int*, CpMem> colorPerm = {}, const std::vector<int>* colorOffs = nullptr,
1482 const FusedSweepCtx* fused = nullptr, const FusedLoopSpec* loop = nullptr) {
1483 CpExec space;
1484 const PGSManifoldSweep f = makePGSManifoldSweep(
1485 manifolds, invMass, invInertia, quat, velPred, angVelPred, realIdx, growthRate,
1486 restitutionNormal, restVelThreshold, maxApproach, maxApproachQS, lambdaAcc, vn0, sideFlag,
1487 lambdaT, frictionDynamic, vt0, restitutionTangent, posImpulse, restBank, restRel,
1488 restPersistent, restVPeak, restGHat, restGrounded, restNewtonOff, restOneSided, restOrphan,
1489 restOrphanVPeak);
1490 // Fused mode (CUDA): one persistent kernel iterates the colours device-side with a grid
1491 // barrier between them — same per-manifold math, same colour ordering, bit-identical to the
1492 // launch loop below (see solver_fused.hpp). Loop mode additionally iterates the whole
1493 // adaptive loop on-device against the residual the caller's stop would have read.
1494#ifdef KOKKOS_ENABLE_CUDA
1495 if (loop) {
1496 if (fused && fused->maxBucket > 0 && colorOffs)
1497 return demLaunchFusedSweepLoop(space, f, colorPerm, *fused, numColors, *loop,
1498 (maxApproachQS.data() ? maxApproachQS : maxApproach).data());
1499 return false;
1500 }
1501 if (fused && fused->maxBucket > 0 && colorOffs &&
1502 demLaunchFusedColorSweep(space, f, colorPerm, *fused, numColors))
1503 return true;
1504#else
1505 (void)fused;
1506 if (loop)
1507 return false;
1508#endif
1509 // Dense-bucket mode (colorOffs from buildColorBucketsKokkos): each colour launch covers only
1510 // its own manifolds instead of scanning all of them — bit-identical (colour classes are
1511 // body-disjoint). No fence: the caller's residual readback synchronizes, and an explicit fence
1512 // here would serialize host submission with GPU execution (the step is submission-bound).
1513 for (int color = 0; color < numColors; ++color) {
1514 if (colorOffs) {
1515 const int b = (*colorOffs)[color], e = (*colorOffs)[color + 1];
1516 if (b == e)
1517 continue;
1518 Kokkos::parallel_for(
1519 "peclet::dem::solve_velocity_pgs", Kokkos::RangePolicy<CpExec>(space, b, e),
1520 KOKKOS_LAMBDA(int i2) { f.solveOne(colorPerm(i2)); });
1521 } else {
1522 Kokkos::parallel_for(
1523 "peclet::dem::solve_velocity_pgs", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
1524 KOKKOS_LAMBDA(int idx) {
1525 if (mColor(idx) == color)
1526 f.solveOne(idx);
1527 });
1528 }
1529 }
1530 return true;
1531}
1532
1541inline void buildLevelColorBucketsKokkos(Kokkos::View<const ManifoldC*, CpMem> manifolds,
1542 int numManifolds, Kokkos::View<const int*, CpMem> realIdx,
1543 Kokkos::View<const int*, CpMem> mColor,
1544 Kokkos::View<const int*, CpMem> heights,
1545 Kokkos::View<int*, CpMem> keys,
1546 Kokkos::View<int*, CpMem> perm,
1547 std::vector<std::pair<int, int>>& buckets) {
1548 buckets.clear();
1549 if (numManifolds <= 0)
1550 return;
1551 CpExec space;
1552 Kokkos::parallel_for(
1553 "peclet::dem::level_keys", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
1554 KOKKOS_LAMBDA(int idx) {
1555 perm(idx) = idx;
1556 const int c = mColor(idx);
1557 const ManifoldC m = manifolds(idx);
1558 if (m.num_points <= 0 || c < 0) {
1559 keys(idx) = INT_MAX;
1560 return;
1561 }
1562 int h = heights(realIdx(m.bodyA));
1563 if (m.bodyB >= 0) {
1564 const int hB = heights(realIdx(m.bodyB));
1565 if (hB < h)
1566 h = hB;
1567 }
1568 if (h > 1023)
1569 h = 1023;
1570 keys(idx) = h * 64 + c;
1571 });
1572 const auto rng = Kokkos::pair<int, int>(0, numManifolds);
1573 auto kd = Kokkos::subview(keys, rng);
1574 auto pd = Kokkos::subview(perm, rng);
1575 Kokkos::Experimental::sort_by_key(space, kd, pd);
1576 auto hk = Kokkos::create_mirror_view(kd);
1577 Kokkos::deep_copy(space, hk, kd);
1578 for (int b = 0; b < numManifolds && hk(b) != INT_MAX;) {
1579 int e = b + 1;
1580 while (e < numManifolds && hk(e) == hk(b))
1581 ++e;
1582 buckets.emplace_back(b, e);
1583 b = e;
1584 }
1585}
1586
1594 Kokkos::View<const int*, CpMem> perm,
1595 const std::vector<std::pair<int, int>>& buckets,
1596 bool topDown) {
1597 CpExec space;
1598 const int nb = static_cast<int>(buckets.size());
1599 for (int i = 0; i < nb; ++i) {
1600 const auto [b, e] = buckets[topDown ? nb - 1 - i : i];
1601 Kokkos::parallel_for(
1602 "peclet::dem::solve_velocity_pgs_lvl", Kokkos::RangePolicy<CpExec>(space, b, e),
1603 KOKKOS_LAMBDA(int i2) { f.solveOne(perm(i2)); });
1604 }
1605 space.fence();
1606}
1607
1616 Kokkos::View<const ManifoldC*, CpMem> manifolds, int numManifolds,
1617 Kokkos::View<const int*, CpMem> mColor, int numColors,
1618 Kokkos::View<const float*, CpMem> invMass, Kokkos::View<const float* [3], CpMem> invInertia,
1619 Kokkos::View<const float* [4], CpMem> quat, Kokkos::View<float* [3], CpMem> velPred,
1620 Kokkos::View<float* [3], CpMem> angVelPred, Kokkos::View<const int*, CpMem> realIdx,
1621 float growthRate, float restitutionNormal, float restVelThreshold,
1622 Kokkos::View<float, CpMem> maxApproach,
1623 Kokkos::View<const unsigned char*, CpMem> persistent = {},
1624 Kokkos::View<const float* [3], CpMem> posPred = {}, F3 gHat = {},
1625 Kokkos::View<const unsigned char*, CpMem> grounded = {}) {
1626 using detail::genInvMass;
1627 using detail::ld3;
1628 CpExec space;
1629 const bool usePersist = persistent.extent(0) > 0;
1630 for (int color = 0; color < numColors; ++color) {
1631 Kokkos::parallel_for(
1632 "peclet::dem::solve_velocity_gs", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
1633 KOKKOS_LAMBDA(int idx) {
1634 if (mColor(idx) != color)
1635 return;
1636 const ManifoldC m = manifolds(idx);
1637
1638 const int idA = m.bodyA, idB = m.bodyB;
1639 const int realA = realIdx(idA);
1640 const int realB = (idB >= 0) ? realIdx(idB) : idB;
1641
1642 const float invMassA = invMass(realA);
1643 const float invMassB = (idB >= 0) ? invMass(realB) : 0.0f;
1644 const F3 invIA = ld3(invInertia, realA);
1645 const F3 invIB = (idB >= 0) ? ld3(invInertia, realB) : F3{0, 0, 0};
1646 const F4 qA = F4{quat(realA, 0), quat(realA, 1), quat(realA, 2), quat(realA, 3)};
1647 const F4 qB = (idB >= 0)
1648 ? F4{quat(realB, 0), quat(realB, 1), quat(realB, 2), quat(realB, 3)}
1649 : F4{0, 0, 0, 1};
1650
1651 const F3 vA = ld3(velPred, realA), wA = ld3(angVelPred, realA);
1652 F3 vB{0, 0, 0}, wB{0, 0, 0};
1653 if (idB >= 0) {
1654 vB = ld3(velPred, realB);
1655 wB = ld3(angVelPred, realB);
1656 }
1657
1658 const F3 Nsum{m.normal_sum.x, m.normal_sum.y, m.normal_sum.z};
1659 const F3 TauA{m.torque_armA_sum.x, m.torque_armA_sum.y, m.torque_armA_sum.z};
1660 const F3 TauB{m.torque_armB_sum.x, m.torque_armB_sum.y, m.torque_armB_sum.z};
1661
1662 const float invN = 1.0f / static_cast<float>(m.num_points);
1663
1664 float restitution = restitutionNormal;
1665 if (idB < 0)
1666 vB = scale3(F3{m.wallVel_sum.x, m.wallVel_sum.y, m.wallVel_sum.z}, invN);
1667 { // per-wall AND per-pair material override (a < 0 average keeps the global material)
1668 const float ra = m.restitution_sum * invN;
1669 if (ra >= 0.0f)
1670 restitution = ra;
1671 }
1672 const F3 rAavg = scale3(F3{m.rA_sum.x, m.rA_sum.y, m.rA_sum.z}, invN);
1673 const F3 rBavg = scale3(F3{m.rB_sum.x, m.rB_sum.y, m.rB_sum.z}, invN);
1674
1675 const float lenN = Kokkos::sqrt(dot3(Nsum, Nsum));
1676 if (lenN < 1e-9f)
1677 return;
1678
1679 const F3 diffCenters = (idB < 0) ? rAavg : sub3(rAavg, rBavg);
1680 const F3 vGrowth = scale3(diffCenters, growthRate);
1681
1682 float vn = dot3(vA, Nsum) + dot3(wA, TauA) + dot3(vB, F3{-Nsum.x, -Nsum.y, -Nsum.z}) +
1683 dot3(wB, TauB);
1684 vn += dot3(vGrowth, Nsum);
1685
1686 const float alignment = dot3(Nsum, diffCenters);
1687 if (alignment > 0.0f) {
1688 if (vn < 0.0f)
1689 return;
1690 } else {
1691 if (vn > 0.0f)
1692 return;
1693 }
1694
1695 const float Nsq = dot3(Nsum, Nsum);
1696 float wA_n = Nsq * invMassA + genInvMass(TauA, invIA, qA);
1697 float wB_n = Nsq * invMassB + genInvMass(TauB, invIB, qB);
1698 // Shock propagation for persistent loaded contacts -- see solveVelocityKokkos.
1699 bool applyA = true, applyB = true;
1700 if (usePersist && persistent(idx) != 0 && idB >= 0) {
1701 const F3 dx = sub3(ldF3(posPred, idA), ldF3(posPred, idB));
1702 const float up = -(dx.x * gHat.x + dx.y * gHat.y + dx.z * gHat.z);
1703 const float thr = 0.3f * Kokkos::sqrt(dot3(dx, dx));
1704 const float riseThr = 4.0f * restVelThreshold; // non-rising + grounded support gate
1705 if (up > thr && -dot3(vB, gHat) <= riseThr && grounded(realB) > 0) {
1706 wB_n = 0.0f;
1707 applyB = false;
1708 restitution = 0.0f; // shock pass is inelastic — see solveVelocityKokkos
1709 } else if (up < -thr && -dot3(vA, gHat) <= riseThr && grounded(realA) > 0) {
1710 wA_n = 0.0f;
1711 applyA = false;
1712 restitution = 0.0f;
1713 }
1714 }
1715 const float wTotal = wA_n + wB_n;
1716 if (wTotal <= 0.0f)
1717 return;
1718
1719 // Record this approaching pair's physical approach speed for the adaptive stop: the
1720 // caller ends the velocity loop once no manifold approaches faster than the resting
1721 // threshold.
1722 Kokkos::atomic_max(&maxApproach(), Kokkos::fabs(vn) / lenN);
1723
1724 if (Kokkos::fabs(vn) < restVelThreshold * lenN)
1725 restitution = 0.0f;
1726
1727 const float lambda = (-restitution * vn - vn) / wTotal;
1728
1729 const F3 Jlin = scale3(Nsum, lambda);
1730 const F3 JangA = scale3(TauA, lambda);
1731 const F3 JangB = scale3(TauB, lambda);
1732
1733 // Linear + angular delta on A (dw_world = R (invI_local * (R^T Jang))), applied in place.
1734 if (applyA) {
1735 velPred(realA, 0) += Jlin.x * invMassA;
1736 velPred(realA, 1) += Jlin.y * invMassA;
1737 velPred(realA, 2) += Jlin.z * invMassA;
1738 {
1739 const F3 Jl = invRotateVector(qA, JangA);
1740 const F3 dwl{Jl.x * invIA.x, Jl.y * invIA.y, Jl.z * invIA.z};
1741 const F3 dww = rotateVector(qA, dwl);
1742 angVelPred(realA, 0) += dww.x;
1743 angVelPred(realA, 1) += dww.y;
1744 angVelPred(realA, 2) += dww.z;
1745 }
1746 }
1747 if (idB >= 0 && applyB) {
1748 velPred(realB, 0) += -Jlin.x * invMassB;
1749 velPred(realB, 1) += -Jlin.y * invMassB;
1750 velPred(realB, 2) += -Jlin.z * invMassB;
1751 const F3 Jl = invRotateVector(qB, JangB);
1752 const F3 dwl{Jl.x * invIB.x, Jl.y * invIB.y, Jl.z * invIB.z};
1753 const F3 dww = rotateVector(qB, dwl);
1754 angVelPred(realB, 0) += dww.x;
1755 angVelPred(realB, 1) += dww.y;
1756 angVelPred(realB, 2) += dww.z;
1757 }
1758 });
1759 // No host fence here: consecutive parallel_for on one execution space are stream-ordered on the
1760 // device, so colour c+1's kernel already observes colour c's in-place writes (the Gauss–Seidel
1761 // dependency). A per-colour fence would only stall the host. One fence after the sweep
1762 // suffices.
1763 }
1764 space.fence();
1765}
1766
1767} // namespace peclet::dem
1768
1769#endif // DEM_SOLVER_VELOCITY_HPP
dem — portable (Kokkos) contact->manifold reduction, replacing the thrust-based reduce_contacts_to_ma...
dem — portable POD types + math + analytic SDFs shared by the Kokkos kernel ports.
float genInvMass(F3 tau, F3 invIlocal, F4 q)
F3 ld3(Kokkos::View< const float *[3], CpMem > v, int i)
void warmStartApplyKokkos(Kokkos::View< const ManifoldC *, CpMem > manifolds, int numManifolds, Kokkos::View< const float *, CpMem > invMass, Kokkos::View< const float *[3], CpMem > invInertia, Kokkos::View< const float *[4], CpMem > quat, Kokkos::View< float *[3], CpMem > velPred, Kokkos::View< float *[3], CpMem > angVelPred, Kokkos::View< const int *, CpMem > realIdx, Kokkos::View< const float *, CpMem > warmP, Kokkos::View< float *[3], CpMem > warmT)
Apply the warm-start impulses up front (order-independent: fixed impulses, atomic adds).
void updateRestitutionBankKokkos(Kokkos::View< const ManifoldC *, CpMem > manifolds, int numManifolds, Kokkos::View< const float *, CpMem > invMass, Kokkos::View< const float *[3], CpMem > invInertia, Kokkos::View< const float *[4], CpMem > quat, Kokkos::View< const float *[3], CpMem > velPred, Kokkos::View< const float *[3], CpMem > angVelPred, Kokkos::View< const int *, CpMem > realIdx, float growthRate, float restitutionNormal, float restVelThreshold, Kokkos::View< const float *, CpMem > vn0, Kokkos::View< const float *, CpMem > lambdaAcc, Kokkos::View< const float *, CpMem > restRel, Kokkos::View< float *, CpMem > restBank, Kokkos::View< float *, CpMem > restVPeak)
Event-level (Poisson) restitution bookkeeping, once per substep AFTER all velocity phases (restitutio...
void applyVelocityDeltasAveragedKokkos(int n, V3 velPred, V3 angVelPred, V3 deltaVel, V3 deltaAngVel, Vi velCounts)
Apply the accumulated velocity deltas AVERAGED by the per-body manifold count — the velocity- solve t...
int colorManifoldsIncrementalKokkos(Kokkos::View< const ManifoldC *, CpMem > manifolds, int numManifolds, Kokkos::View< const int *, CpMem > realIdx, int numReal, Kokkos::View< const unsigned long long *, CpMem > prevKeys, Kokkos::View< const int *, CpMem > prevColor, int prevCount, Kokkos::View< int *, CpMem > mColor, Kokkos::View< long long *, CpMem > bodyWinner, Kokkos::View< std::uint64_t *, CpMem > bodyMask, int &leftover, bool forceFull, Kokkos::View< const unsigned char *, CpMem > sleepMask={})
Incremental (warm-started) manifold colouring for the single-GPU PGS path.
F3 cross3v(F3 a, F3 b)
void scatterOrphanBanksKokkos(Kokkos::View< const unsigned long long *, CpMem > prevKeys, Kokkos::View< const float *, CpMem > prevRestBank, Kokkos::View< const float *, CpMem > prevRestVPeak, Kokkos::View< const unsigned char *, CpMem > matched, int prevCount, Kokkos::View< const float *, CpMem > invMass, Kokkos::View< float *, CpMem > orphan, Kokkos::View< float *, CpMem > orphanVPeak, Kokkos::View< const int *, CpMem > gidSorted={}, Kokkos::View< const int *, CpMem > slotSorted={})
Orphan transfer: previous-ledger entries NOT matched by any current manifold (their pair died this su...
void computeVn0Kokkos(Kokkos::View< const ManifoldC *, CpMem > manifolds, int numManifolds, Kokkos::View< const float *[3], CpMem > velPred, Kokkos::View< const float *[3], CpMem > angVelPred, Kokkos::View< const int *, CpMem > realIdx, float growthRate, Kokkos::View< float *, CpMem > vn0, Kokkos::View< float *[3], CpMem > vt0)
-— Warm-started projected Gauss-Seidel (PGS) velocity solve -— Nonsmooth contact dynamics (Moreau-Jea...
Kokkos::View< int *, CpMem > Vi
Kokkos::View< float *[3], CpMem > V3
F3 invRotateVector(F4 q, F3 v)
void solveVelocityKokkos(Kokkos::View< const ManifoldC *, CpMem > manifolds, int numManifolds, Kokkos::View< const float *, CpMem > invMass, Kokkos::View< const float *[3], CpMem > invInertia, Kokkos::View< const float *[4], CpMem > quat, Kokkos::View< const float *[3], CpMem > velPred, Kokkos::View< const float *[3], CpMem > angVelPred, Kokkos::View< const int *, CpMem > realIdx, float growthRate, float restitutionNormal, float restVelThreshold, Kokkos::View< float *[3], CpMem > deltaVel, Kokkos::View< float *[3], CpMem > deltaAngVel, Kokkos::View< int *, CpMem > velCounts, Kokkos::View< const int *, CpMem > onlyColor={}, int colorFilter=0, Kokkos::View< const unsigned char *, CpMem > persistent={}, Kokkos::View< const float *[3], CpMem > posPred={}, F3 gHat={}, Kokkos::View< const unsigned char *, CpMem > grounded={})
Accumulate normal-restitution velocity deltas for numManifolds manifolds.
F3 ldF3(const V &v, int i)
PGSManifoldSweep makePGSManifoldSweep(Kokkos::View< const ManifoldC *, CpMem > manifolds, Kokkos::View< const float *, CpMem > invMass, Kokkos::View< const float *[3], CpMem > invInertia, Kokkos::View< const float *[4], CpMem > quat, Kokkos::View< float *[3], CpMem > velPred, Kokkos::View< float *[3], CpMem > angVelPred, Kokkos::View< const int *, CpMem > realIdx, float growthRate, float restitutionNormal, float restVelThreshold, Kokkos::View< float, CpMem > maxApproach, Kokkos::View< float, CpMem > maxApproachQS, Kokkos::View< float *, CpMem > lambdaAcc, Kokkos::View< const float *, CpMem > vn0, Kokkos::View< const unsigned char *, CpMem > sideFlag, Kokkos::View< float *[3], CpMem > lambdaT, float frictionDynamic, Kokkos::View< const float *[3], CpMem > vt0, float restitutionTangent, Kokkos::View< const float *, CpMem > posImpulse, Kokkos::View< float *, CpMem > restBank, Kokkos::View< float *, CpMem > restRel, Kokkos::View< const unsigned char *, CpMem > restPersistent, Kokkos::View< const float *, CpMem > restVPeak, F3 restGHat, Kokkos::View< const unsigned char *, CpMem > restGrounded, bool restNewtonOff, bool restOneSided, Kokkos::View< float *, CpMem > restOrphan, Kokkos::View< const float *, CpMem > restOrphanVPeak)
Build the shared per-manifold sweep functor (the colored launch loop, the fused kernels and the fused...
void solveVelocityColoredGSKokkos(Kokkos::View< const ManifoldC *, CpMem > manifolds, int numManifolds, Kokkos::View< const int *, CpMem > mColor, int numColors, Kokkos::View< const float *, CpMem > invMass, Kokkos::View< const float *[3], CpMem > invInertia, Kokkos::View< const float *[4], CpMem > quat, Kokkos::View< float *[3], CpMem > velPred, Kokkos::View< float *[3], CpMem > angVelPred, Kokkos::View< const int *, CpMem > realIdx, float growthRate, float restitutionNormal, float restVelThreshold, Kokkos::View< float, CpMem > maxApproach, Kokkos::View< const unsigned char *, CpMem > persistent={}, Kokkos::View< const float *[3], CpMem > posPred={}, F3 gHat={}, Kokkos::View< const unsigned char *, CpMem > grounded={})
Colored Gauss–Seidel normal-restitution solve: sweep the numColors colour classes in order,...
F3 rotateVector(F4 q, F3 v)
void decayBodyOrphanKokkos(Kokkos::View< float *, CpMem > orphan, Kokkos::View< float *, CpMem > orphanVPeak, int numOwned, float restVelThreshold)
Orphan-account aging, once per substep over the OWNED bodies: both the balance and the carried event ...
float dot3(F3 a, F3 b)
F3 sub3(F3 a, F3 b)
bool solveVelocityPGSKokkos(Kokkos::View< const ManifoldC *, CpMem > manifolds, int numManifolds, Kokkos::View< const int *, CpMem > mColor, int numColors, Kokkos::View< const float *, CpMem > invMass, Kokkos::View< const float *[3], CpMem > invInertia, Kokkos::View< const float *[4], CpMem > quat, Kokkos::View< float *[3], CpMem > velPred, Kokkos::View< float *[3], CpMem > angVelPred, Kokkos::View< const int *, CpMem > realIdx, float growthRate, float restitutionNormal, float restVelThreshold, Kokkos::View< float, CpMem > maxApproach, Kokkos::View< float *, CpMem > lambdaAcc, Kokkos::View< const float *, CpMem > vn0, Kokkos::View< const unsigned char *, CpMem > sideFlag, Kokkos::View< float *[3], CpMem > lambdaT, float frictionDynamic, Kokkos::View< const float *[3], CpMem > vt0={}, float restitutionTangent=0.0f, Kokkos::View< const float *, CpMem > posImpulse={}, Kokkos::View< float, CpMem > maxApproachQS={}, Kokkos::View< float *, CpMem > restBank={}, Kokkos::View< float *, CpMem > restRel={}, Kokkos::View< const unsigned char *, CpMem > restPersistent={}, Kokkos::View< const float *, CpMem > restVPeak={}, F3 restGHat={}, Kokkos::View< const unsigned char *, CpMem > restGrounded={}, bool restNewtonOff=false, bool restOneSided=false, Kokkos::View< float *, CpMem > restOrphan={}, Kokkos::View< const float *, CpMem > restOrphanVPeak={}, Kokkos::View< const int *, CpMem > colorPerm={}, const std::vector< int > *colorOffs=nullptr, const FusedSweepCtx *fused=nullptr, const FusedLoopSpec *loop=nullptr)
Returns true when the sweep (or, with loop, the whole iteration loop) was submitted; false ONLY in lo...
CpExec::memory_space CpMem
void buildLevelColorBucketsKokkos(Kokkos::View< const ManifoldC *, CpMem > manifolds, int numManifolds, Kokkos::View< const int *, CpMem > realIdx, Kokkos::View< const int *, CpMem > mColor, Kokkos::View< const int *, CpMem > heights, Kokkos::View< int *, CpMem > keys, Kokkos::View< int *, CpMem > perm, std::vector< std::pair< int, int > > &buckets)
Bucket the active coloured manifolds by (support level, colour) for the level-ordered ("multilevel") ...
int colorManifoldsKokkos(Kokkos::View< const ManifoldC *, CpMem > manifolds, int numManifolds, Kokkos::View< const int *, CpMem > realIdx, int numReal, Kokkos::View< int *, CpMem > mColor, Kokkos::View< long long *, CpMem > bodyWinner, Kokkos::View< std::uint64_t *, CpMem > bodyMask, int &leftover, Kokkos::View< const unsigned char *, CpMem > sleepMask={})
Greedy graph-colour the manifolds: no two manifolds sharing a real body get the same colour.
void solveVelocityPGSBucketsKokkos(const PGSManifoldSweep &f, Kokkos::View< const int *, CpMem > perm, const std::vector< std::pair< int, int > > &buckets, bool topDown)
Level-ordered symmetric sweep: launch one PGS kernel per (level, colour) bucket, ascending (bottom-up...
F3 scale3(F3 a, float s)
long long colorKey(int idx)
splitmix32 finalizer: a well-mixed pseudo-random priority per edge index.
F3 add3(F3 a, F3 b)
std::tuple< double, float, int > restBankStatsKokkos(Kokkos::View< const float *, CpMem > bank, int n)
Poisson-restitution diagnostics: (sum, max, count>0) over the committed owed-impulse store (namespace...
void computeSideFlagsKokkos(Kokkos::View< const ManifoldC *, CpMem > manifolds, int numManifolds, Kokkos::View< const int *, CpMem > realIdx, Kokkos::View< const unsigned char *, CpMem > persistent, Kokkos::View< const unsigned char *, CpMem > grounded, Kokkos::View< const float *[3], CpMem > posPred, Kokkos::View< const float *[3], CpMem > velPred, F3 gHat, float riseThr, Kokkos::View< unsigned char *, CpMem > sideFlag, Kokkos::View< const float *, CpMem > vn0, float approachThr)
Decide each persistent contact's treatment ONCE per substep (before any impulse is applied): 0 = symm...
Kokkos::DefaultExecutionSpace CpExec
unsigned long long pairKeyOf(const ManifoldC &m, Kokkos::View< const int *, CpMem > realIdx)
Persistent-contact detection for the gravity-gated restitution rule.
dem — fused colour sweeps: one persistent kernel per sweep instead of one kernel launch per colour.
Portable mirror of ManifoldConstraint.
One full colored PGS sweep.
Kokkos::View< float, CpMem > maxApproach
Kokkos::View< float, CpMem > maxApproachQS
Kokkos::View< float *[3], CpMem > lambdaT
Kokkos::View< const float *, CpMem > restOrphanVPeak
Kokkos::View< const unsigned char *, CpMem > restPersistent
Kokkos::View< const float *, CpMem > invMass
Kokkos::View< const float *[4], CpMem > quat
Kokkos::View< const int *, CpMem > realIdx
Kokkos::View< const float *[3], CpMem > vt0
Kokkos::View< const float *[3], CpMem > invInertia
Kokkos::View< const unsigned char *, CpMem > sideFlag
Kokkos::View< float *, CpMem > lambdaAcc
Kokkos::View< float *[3], CpMem > velPred
Kokkos::View< const float *, CpMem > restVPeak
Kokkos::View< const float *, CpMem > vn0
Kokkos::View< float *, CpMem > restOrphan
Kokkos::View< const float *, CpMem > posImpulse
Kokkos::View< const ManifoldC *, CpMem > manifolds
Kokkos::View< float *[3], CpMem > angVelPred
Kokkos::View< const unsigned char *, CpMem > restGrounded
Kokkos::View< float *, CpMem > restRel
Kokkos::View< float *, CpMem > restBank