1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
use crate::alloc_prelude::*;
use crate::dynamics::JointGraphEdge;
use crate::dynamics::solver::manifold_store::ManifoldStore;
use crate::dynamics::solver::solver_contact_graph::ContactRef;
use crate::math::{SIMD_LAST_INDEX, SIMD_WIDTH};
use crate::dynamics::RigidBodyHandle;
pub(crate) struct ParallelInteractionGroups {
bodies_color: Vec<u128>, // Workspace.
interaction_indices: Vec<usize>, // Workspace.
interaction_colors: Vec<usize>, // Workspace.
sorted_interactions: Vec<usize>,
groups: Vec<usize>,
group_colors: Vec<u8>,
}
impl ParallelInteractionGroups {
pub fn new() -> Self {
Self {
bodies_color: Vec::new(),
interaction_indices: Vec::new(),
interaction_colors: Vec::new(),
sorted_interactions: Vec::new(),
groups: Vec::new(),
group_colors: Vec::new(),
}
}
pub fn group(&self, i: usize) -> &[usize] {
let range = self.groups[i]..self.groups[i + 1];
&self.sorted_interactions[range]
}
/// The color id assigned to the `i`-th group.
pub fn group_color(&self, i: usize) -> u8 {
self.group_colors[i]
}
pub fn num_groups(&self) -> usize {
self.groups.len().saturating_sub(1)
}
/// Greedy per-color grouping over the joints' stamped solver-body ids (`solver_body_ids`:
/// `active_set_id` per side, or `u32::MAX` for a world-attached side — fixed, sleeping, or
/// no body). Never dereferences the rigid-body set. NOTE: multibody-linked interactions
/// must not be passed here (categorization routes them to the serial generic path): the
/// coloring would need to remap a multibody's links to one representative body to keep
/// same-color interactions truly independent.
#[inline]
fn keep_or_pick(stored: u8, color_mask: u128, pick: impl FnOnce() -> usize) -> usize {
if stored < 128 && color_mask & (1u128 << stored) == 0 {
return stored as usize;
}
pick()
}
pub fn group_interactions(
&mut self,
num_island_bodies: usize,
interactions: &mut [JointGraphEdge],
interaction_indices: &[usize],
// Per-body masks (indexed by rigid-body arena index) of colors already used by another
// constraint kind (e.g. the contacts' persistent solver colors): colors assigned here
// avoid them, so same-id groups of both colorings never share a body. Empty = disabled.
external_color_masks: &[u128],
) {
self.bodies_color.clear();
self.interaction_indices.clear();
self.groups.clear();
self.sorted_interactions.clear();
self.interaction_colors.clear();
self.group_colors.clear();
let external_mask = |handle: RigidBodyHandle| {
external_color_masks
.get(handle.into_raw_parts().0 as usize)
.copied()
.unwrap_or(0)
};
// Color 128 is the "couldn't color" overflow bucket (used when a body's
// 128-bit color mask is exhausted); consumers must solve it serially.
let mut color_len = [0; 129];
self.bodies_color.resize(num_island_bodies, 0u128);
self.interaction_indices
.extend_from_slice(interaction_indices);
self.interaction_colors.resize(interaction_indices.len(), 0);
let bcolors = &mut self.bodies_color;
for (interaction_id, color) in self
.interaction_indices
.iter()
.zip(self.interaction_colors.iter_mut())
{
// Solver-body ids stamped by the selection pass; `u32::MAX` is a
// world-attached side (fixed, sleeping, or no body): it doesn't
// conflict with anything.
let joint = &interactions[*interaction_id].weight;
let stored_color = joint.solver_color;
let [id1, id2] = joint.solver_body_ids;
let is_fixed1 = id1 == u32::MAX;
let is_fixed2 = id2 == u32::MAX;
// Colors used by the external coloring on the pair's bodies (indexed
// by rigid-body arena index — no arena read); the color chosen below
// must avoid them.
let ext_mask = external_mask(joint.body1) | external_mask(joint.body2);
// Grow the masks to cover appended (frontier) solver-body slots.
let max_id = if is_fixed1 { 0 } else { id1 as usize }.max(if is_fixed2 {
0
} else {
id2 as usize
});
if max_id >= bcolors.len() {
bcolors.resize(max_id + 1, 0u128);
}
match (is_fixed1, is_fixed2) {
(false, false) => {
// Reserve the top colors for dynamic-vs-fixed contacts so those are solved
// last (see `SOLVER_DYNAMIC_COLOR_COUNT`); dyn-vs-dyn overflows instead of
// encroaching on that band.
let color_mask = bcolors[id1 as usize] | bcolors[id2 as usize] | ext_mask;
let dynamic_free =
!color_mask & ((1u128 << crate::geometry::SOLVER_DYNAMIC_COLOR_COUNT) - 1);
*color = Self::keep_or_pick(stored_color, color_mask, || {
dynamic_free.trailing_zeros() as usize
});
color_len[*color] += 1;
if *color < 128 {
bcolors[id1 as usize] |= 1 << *color;
bcolors[id2 as usize] |= 1 << *color;
}
}
(true, false) | (false, true) => {
let id = if is_fixed1 { id2 } else { id1 } as usize;
let color_mask = bcolors[id] | ext_mask;
let free = !color_mask;
*color = Self::keep_or_pick(stored_color, color_mask, || {
if free == 0 {
128
} else {
127 - free.leading_zeros() as usize
}
});
color_len[*color] += 1;
if *color < 128 {
bcolors[id] |= 1 << *color;
}
}
(true, true) => unreachable!(),
}
}
// Persist the assignment: the next rebuild starts from these colors, so a cold
// assembly reproduces the layout a warm one is holding.
for (interaction_id, color) in self
.interaction_indices
.iter()
.zip(self.interaction_colors.iter())
{
interactions[*interaction_id].weight.solver_color = *color as u8;
}
let mut sort_offsets = [0; 129];
let mut last_offset = 0;
for i in 0..129 {
if color_len[i] != 0 {
self.groups.push(last_offset);
self.group_colors.push(i as u8);
sort_offsets[i] = last_offset;
last_offset += color_len[i];
}
}
self.sorted_interactions
.resize(interaction_indices.len(), 0);
for (interaction_id, color) in interaction_indices
.iter()
.zip(self.interaction_colors.iter())
{
self.sorted_interactions[sort_offsets[*color]] = *interaction_id;
sort_offsets[*color] += 1;
}
self.groups.push(self.sorted_interactions.len());
}
}
/// Per-interaction data prefetched once at the beginning of `group_manifold_refs`
/// so the contact-count passes don't re-read the rigid-body set.
#[derive(Copy, Clone)]
struct InteractionToGroup {
/// `active_set_id` of the first body, or `u32::MAX` if it doesn't conflict
/// (fixed, kinematic-less, or sleeping).
id1: u32,
/// Same as `id1` for the second body.
id2: u32,
/// Position of this interaction inside `interaction_indices`.
position: u32,
}
pub(crate) struct InteractionGroups {
// The buckets are indexed by the bit index of the `u128` conflict masks.
bucket_len: [u8; 128],
body_masks: Vec<u128>,
/// Workspace: per row-layout signature, the mask of buckets currently
/// accumulating joints of that signature (joints of different signatures
/// can't share a SIMD group).
to_group: Vec<InteractionToGroup>,
/// Bucket workspace of [`Self::group_manifold_refs`].
ref_bucket_slots: Box<[[ContactRef; SIMD_WIDTH]; 128]>,
pub simd_ref_interactions: Vec<ContactRef>,
pub nongrouped_ref_interactions: Vec<ContactRef>,
}
impl InteractionGroups {
pub fn new() -> Self {
Self {
bucket_len: [0; 128],
body_masks: Vec::new(),
to_group: Vec::new(),
ref_bucket_slots: Box::new([[ContactRef::PADDING; SIMD_WIDTH]; 128]),
simd_ref_interactions: Vec::new(),
nongrouped_ref_interactions: Vec::new(),
}
}
pub fn clear_groups(&mut self) {
self.simd_ref_interactions.clear();
self.nongrouped_ref_interactions.clear();
}
/// Single-threaded, bucket-direct grouping: groups the overflow-color manifolds (which may
/// share bodies) into body-disjoint SIMD groups, reading stamped ids/counts through the store.
pub fn group_manifold_refs(
&mut self,
num_island_bodies: usize,
store: &ManifoldStore,
refs: &[ContactRef],
) {
self.body_masks.resize(num_island_bodies, 0u128);
self.to_group.clear();
self.to_group.reserve(refs.len());
let mut max_id = 0usize;
for (position, r) in refs.iter().enumerate() {
let data = &store.get(*r).data;
let [id1, id2] = data.solver_body_ids;
if id1 == u32::MAX && id2 == u32::MAX {
continue;
}
let num_contacts = data.num_active_contacts() as u32;
if num_contacts == 0 {
continue;
}
if id1 != u32::MAX {
max_id = max_id.max(id1 as usize);
}
if id2 != u32::MAX {
max_id = max_id.max(id2 as usize);
}
self.to_group.push(InteractionToGroup {
id1,
id2,
position: position as u32,
});
}
if max_id >= self.body_masks.len() {
self.body_masks.resize(max_id + 1, 0u128);
}
// Single grouping pass: the wide constraint kernels handle per-lane
// contact counts, so a group may freely mix counts.
{
let mut occupied_mask = 0u128;
for i in 0..self.to_group.len() {
let meta = self.to_group[i];
let interaction_r = refs[meta.position as usize];
let is_fixed1 = meta.id1 == u32::MAX;
let is_fixed2 = meta.id2 == u32::MAX;
let mask1 = if !is_fixed1 {
self.body_masks[meta.id1 as usize]
} else {
0
};
let mask2 = if !is_fixed2 {
self.body_masks[meta.id2 as usize]
} else {
0
};
let conflicts = mask1 | mask2;
let conflictfree_targets = !(conflicts & occupied_mask);
let conflictfree_occupied_targets = conflictfree_targets & occupied_mask;
let target_index = if conflictfree_occupied_targets != 0 {
// Try to fill partial WContacts first.
conflictfree_occupied_targets.trailing_zeros()
} else {
conflictfree_targets.trailing_zeros()
};
if target_index == 128 {
// The interaction conflicts with every bucket we can manage.
self.nongrouped_ref_interactions.push(interaction_r);
continue;
}
let target_mask_bit = 1 << target_index;
let bucket = &mut self.ref_bucket_slots[target_index as usize];
let bucket_len = &mut self.bucket_len[target_index as usize];
if *bucket_len as usize == SIMD_LAST_INDEX {
// We completed our group.
bucket[SIMD_LAST_INDEX] = interaction_r;
self.simd_ref_interactions.extend_from_slice(&bucket[..]);
*bucket_len = 0;
occupied_mask &= !target_mask_bit;
} else {
bucket[*bucket_len as usize] = interaction_r;
*bucket_len += 1;
occupied_mask |= target_mask_bit;
}
// NOTE: fixed bodies don't transmit forces. Therefore they don't
// imply any interaction conflicts.
if !is_fixed1 {
self.body_masks[meta.id1 as usize] |= target_mask_bit;
}
if !is_fixed2 {
self.body_masks[meta.id2 as usize] |= target_mask_bit;
}
}
// Flush the partially-filled buckets.
for target_index in 0..128 {
let bucket_len = self.bucket_len[target_index] as usize;
if bucket_len != 0 {
self.nongrouped_ref_interactions
.extend_from_slice(&self.ref_bucket_slots[target_index][..bucket_len]);
self.bucket_len[target_index] = 0;
}
}
self.body_masks.iter_mut().for_each(|e| *e = 0);
}
assert!(
self.simd_ref_interactions.len() % SIMD_WIDTH == 0,
"Invalid SIMD contact grouping."
);
}
}