Skip to main content

box3d_rust/
constraint_graph.rs

1// Port of the constraint graph data model from
2// box3d-cpp-reference/src/constraint_graph.h. Logic from constraint_graph.c
3// lands in the solver bring-up commit.
4//
5// The C b3GraphColor carries transient pointers into arena scratch
6// (wideConstraints / manifoldConstraints / contactConstraints), rebuilt every
7// step by the solver. The Rust solver phase owns that scratch as Vecs local to
8// the step; the persistent graph state here is the bitset and the sim arrays.
9//
10// SPDX-FileCopyrightText: 2025 Erin Catto
11// SPDX-License-Identifier: MIT
12
13use crate::bitset::BitSet;
14use crate::constants::GRAPH_COLOR_COUNT;
15use crate::contact::ContactSpec;
16use crate::joint::JointSim;
17use crate::math_functions::max_int;
18
19/// Constraints that cannot fit the graph color limit. (B3_OVERFLOW_INDEX)
20pub const OVERFLOW_INDEX: i32 = GRAPH_COLOR_COUNT - 1;
21
22/// Keeps dyn-dyn constraints at lower solver priority than dyn-static.
23/// (B3_DYNAMIC_COLOR_COUNT)
24pub const DYNAMIC_COLOR_COUNT: i32 = GRAPH_COLOR_COUNT - 4;
25
26/// (b3GraphColor)
27#[derive(Debug, Clone, Default)]
28pub struct GraphColor {
29    /// Indexed by bodyId; oversized to encompass static bodies. Bits are never
30    /// traversed or counted. Unused on the overflow color.
31    pub body_set: BitSet,
32
33    /// Cache friendly arrays
34    pub joint_sims: Vec<JointSim>,
35
36    pub convex_contacts: Vec<i32>,
37    pub contacts: Vec<ContactSpec>,
38}
39
40/// (b3ConstraintGraph)
41#[derive(Debug, Clone, Default)]
42pub struct ConstraintGraph {
43    /// Including overflow at the end
44    pub colors: Vec<GraphColor>,
45}
46
47impl ConstraintGraph {
48    /// (b3CreateGraph)
49    pub fn new(body_capacity: i32) -> ConstraintGraph {
50        const _: () = assert!(GRAPH_COLOR_COUNT >= 2, "must have at least two colors");
51        const _: () = assert!(
52            OVERFLOW_INDEX == GRAPH_COLOR_COUNT - 1,
53            "bad overflow index"
54        );
55
56        let body_capacity = max_int(body_capacity, 8);
57
58        let mut colors = Vec::with_capacity(GRAPH_COLOR_COUNT as usize);
59        // Initialize graph color bit set. No bitset for overflow color.
60        for i in 0..GRAPH_COLOR_COUNT {
61            let mut color = GraphColor::default();
62            if i < OVERFLOW_INDEX {
63                color.body_set = BitSet::new(body_capacity as u32);
64                color.body_set.set_bit_count_and_clear(body_capacity as u32);
65            }
66            colors.push(color);
67        }
68
69        ConstraintGraph { colors }
70    }
71}
72
73/// Contacts are created non-touching; once touching they enter the graph.
74/// (b3AddContactToGraph)
75pub fn add_contact_to_graph(world: &mut crate::world::World, contact_id: i32) {
76    use crate::contact::contact_flags;
77    use crate::core::NULL_INDEX;
78    use crate::types::BodyType;
79
80    let contact = &world.contacts[contact_id as usize];
81    debug_assert!(contact.manifold_count() > 0);
82    debug_assert!((contact.flags & contact_flags::TOUCHING) != 0);
83
84    let body_id_a = contact.edges[0].body_id;
85    let body_id_b = contact.edges[1].body_id;
86    let type_a = world.bodies[body_id_a as usize].type_;
87    let type_b = world.bodies[body_id_b as usize].type_;
88    debug_assert!(type_a == BodyType::Dynamic || type_b == BodyType::Dynamic);
89
90    let mut color_index = OVERFLOW_INDEX;
91
92    if type_a == BodyType::Dynamic && type_b == BodyType::Dynamic {
93        for i in 0..DYNAMIC_COLOR_COUNT {
94            let color = &world.constraint_graph.colors[i as usize];
95            if color.body_set.get_bit(body_id_a as u32) || color.body_set.get_bit(body_id_b as u32)
96            {
97                continue;
98            }
99            world.constraint_graph.colors[i as usize]
100                .body_set
101                .set_bit_grow(body_id_a as u32);
102            world.constraint_graph.colors[i as usize]
103                .body_set
104                .set_bit_grow(body_id_b as u32);
105            color_index = i;
106            break;
107        }
108    } else if type_a == BodyType::Dynamic {
109        for i in (1..OVERFLOW_INDEX).rev() {
110            if world.constraint_graph.colors[i as usize]
111                .body_set
112                .get_bit(body_id_a as u32)
113            {
114                continue;
115            }
116            world.constraint_graph.colors[i as usize]
117                .body_set
118                .set_bit_grow(body_id_a as u32);
119            color_index = i;
120            break;
121        }
122    } else if type_b == BodyType::Dynamic {
123        for i in (1..OVERFLOW_INDEX).rev() {
124            if world.constraint_graph.colors[i as usize]
125                .body_set
126                .get_bit(body_id_b as u32)
127            {
128                continue;
129            }
130            world.constraint_graph.colors[i as usize]
131                .body_set
132                .set_bit_grow(body_id_b as u32);
133            color_index = i;
134            break;
135        }
136    }
137
138    let is_mesh =
139        (world.contacts[contact_id as usize].flags & contact_flags::SIM_MESH_CONTACT) != 0;
140    let is_scalar = is_mesh || color_index == OVERFLOW_INDEX;
141    let local_index = if is_scalar {
142        world.constraint_graph.colors[color_index as usize]
143            .contacts
144            .len() as i32
145    } else {
146        world.constraint_graph.colors[color_index as usize]
147            .convex_contacts
148            .len() as i32
149    };
150
151    let local_a = world.bodies[body_id_a as usize].local_index;
152    let local_b = world.bodies[body_id_b as usize].local_index;
153    let manifold_count = world.contacts[contact_id as usize].manifold_count();
154
155    {
156        let contact = &mut world.contacts[contact_id as usize];
157        contact.color_index = color_index;
158        contact.local_index = local_index;
159        contact.body_sim_index_a = if type_a == BodyType::Static {
160            NULL_INDEX
161        } else {
162            local_a
163        };
164        contact.body_sim_index_b = if type_b == BodyType::Static {
165            NULL_INDEX
166        } else {
167            local_b
168        };
169    }
170
171    let color = &mut world.constraint_graph.colors[color_index as usize];
172    if is_scalar {
173        debug_assert!(manifold_count < u16::MAX as i32);
174        color.contacts.push(ContactSpec {
175            contact_id,
176            manifold_start: 0,
177            manifold_count: manifold_count as u16,
178        });
179    } else {
180        color.convex_contacts.push(contact_id);
181    }
182}
183
184/// Pick a graph color for a joint between the two bodies. (b3AssignJointColor)
185///
186/// C compiles the coloring away when B3_FORCE_OVERFLOW is set; the reference
187/// pins it to 0, so the coloring always runs.
188fn assign_joint_color(
189    graph: &mut ConstraintGraph,
190    body_id_a: i32,
191    body_id_b: i32,
192    type_a: crate::types::BodyType,
193    type_b: crate::types::BodyType,
194) -> i32 {
195    use crate::types::BodyType;
196
197    debug_assert!(type_a == BodyType::Dynamic || type_b == BodyType::Dynamic);
198
199    if type_a == BodyType::Dynamic && type_b == BodyType::Dynamic {
200        // Dynamic constraint colors cannot encroach on colors reserved for static constraints
201        for i in 0..DYNAMIC_COLOR_COUNT {
202            let color = &graph.colors[i as usize];
203            if color.body_set.get_bit(body_id_a as u32) || color.body_set.get_bit(body_id_b as u32)
204            {
205                continue;
206            }
207
208            let color = &mut graph.colors[i as usize];
209            color.body_set.set_bit_grow(body_id_a as u32);
210            color.body_set.set_bit_grow(body_id_b as u32);
211            return i;
212        }
213    } else if type_a == BodyType::Dynamic {
214        // Static constraint colors build from the end to get higher priority than dyn-dyn constraints
215        for i in (1..OVERFLOW_INDEX).rev() {
216            let color = &graph.colors[i as usize];
217            if color.body_set.get_bit(body_id_a as u32) {
218                continue;
219            }
220
221            graph.colors[i as usize]
222                .body_set
223                .set_bit_grow(body_id_a as u32);
224            return i;
225        }
226    } else if type_b == BodyType::Dynamic {
227        // Static constraint colors build from the end to get higher priority than dyn-dyn constraints
228        for i in (1..OVERFLOW_INDEX).rev() {
229            let color = &graph.colors[i as usize];
230            if color.body_set.get_bit(body_id_b as u32) {
231                continue;
232            }
233
234            graph.colors[i as usize]
235                .body_set
236                .set_bit_grow(body_id_b as u32);
237            return i;
238        }
239    }
240
241    OVERFLOW_INDEX
242}
243
244/// Allocate a zeroed joint sim slot in the graph for the joint and set the
245/// joint's color/local indices. Returns (color_index, local_index).
246/// (b3CreateJointInGraph — C returns the sim pointer; the caller writes it)
247pub fn create_joint_in_graph(world: &mut crate::world::World, joint_id: i32) -> (i32, i32) {
248    let body_id_a = world.joints[joint_id as usize].edges[0].body_id;
249    let body_id_b = world.joints[joint_id as usize].edges[1].body_id;
250    let type_a = world.bodies[body_id_a as usize].type_;
251    let type_b = world.bodies[body_id_b as usize].type_;
252
253    let color_index = assign_joint_color(
254        &mut world.constraint_graph,
255        body_id_a,
256        body_id_b,
257        type_a,
258        type_b,
259    );
260
261    let color = &mut world.constraint_graph.colors[color_index as usize];
262    let local_index = color.joint_sims.len() as i32;
263    color.joint_sims.push(crate::joint::JointSim::default());
264
265    let joint = &mut world.joints[joint_id as usize];
266    joint.color_index = color_index;
267    joint.local_index = local_index;
268    (color_index, local_index)
269}
270
271/// (b3AddJointToGraph)
272pub fn add_joint_to_graph(
273    world: &mut crate::world::World,
274    joint_sim: crate::joint::JointSim,
275    joint_id: i32,
276) {
277    let (color_index, local_index) = create_joint_in_graph(world, joint_id);
278    world.constraint_graph.colors[color_index as usize].joint_sims[local_index as usize] =
279        joint_sim;
280}
281
282/// (b3RemoveJointFromGraph)
283pub fn remove_joint_from_graph(
284    world: &mut crate::world::World,
285    body_id_a: i32,
286    body_id_b: i32,
287    color_index: i32,
288    local_index: i32,
289) {
290    debug_assert!(0 <= color_index && color_index < GRAPH_COLOR_COUNT);
291
292    if color_index != OVERFLOW_INDEX {
293        // May clear static bodies, no effect
294        let color = &mut world.constraint_graph.colors[color_index as usize];
295        color.body_set.clear_bit(body_id_a as u32);
296        color.body_set.clear_bit(body_id_b as u32);
297    }
298
299    let color = &mut world.constraint_graph.colors[color_index as usize];
300    let moved_index = color.joint_sims.len() as i32 - 1;
301    color.joint_sims.swap_remove(local_index as usize);
302    if moved_index != local_index {
303        // Fix moved joint
304        let moved_id = color.joint_sims[local_index as usize].joint_id;
305        let moved_joint = &mut world.joints[moved_id as usize];
306        debug_assert!(moved_joint.set_index == crate::solver_set::AWAKE_SET);
307        debug_assert!(moved_joint.color_index == color_index);
308        debug_assert!(moved_joint.local_index == moved_index);
309        moved_joint.local_index = local_index;
310    }
311}
312
313/// (b3RemoveContactFromGraph)
314pub fn remove_contact_from_graph(
315    world: &mut crate::world::World,
316    body_id_a: i32,
317    body_id_b: i32,
318    color_index: i32,
319    local_index: i32,
320    mesh_contact: bool,
321) {
322    debug_assert!(0 <= color_index && color_index < GRAPH_COLOR_COUNT);
323
324    if color_index != OVERFLOW_INDEX {
325        let color = &mut world.constraint_graph.colors[color_index as usize];
326        color.body_set.clear_bit(body_id_a as u32);
327        color.body_set.clear_bit(body_id_b as u32);
328    }
329
330    let color = &mut world.constraint_graph.colors[color_index as usize];
331    if mesh_contact || color_index == OVERFLOW_INDEX {
332        let moved_index = color.contacts.len() as i32 - 1;
333        color.contacts.swap_remove(local_index as usize);
334        if moved_index != local_index {
335            let moved_contact_id = color.contacts[local_index as usize].contact_id;
336            let moved = &mut world.contacts[moved_contact_id as usize];
337            debug_assert!(moved.set_index == crate::solver_set::AWAKE_SET);
338            debug_assert!(moved.color_index == color_index);
339            debug_assert!(moved.local_index == moved_index);
340            moved.local_index = local_index;
341        }
342    } else {
343        let moved_index = color.convex_contacts.len() as i32 - 1;
344        color.convex_contacts.swap_remove(local_index as usize);
345        if moved_index != local_index {
346            let moved_contact_id = color.convex_contacts[local_index as usize];
347            let moved = &mut world.contacts[moved_contact_id as usize];
348            debug_assert!(moved.set_index == crate::solver_set::AWAKE_SET);
349            debug_assert!(moved.color_index == color_index);
350            debug_assert!(moved.local_index == moved_index);
351            debug_assert!((moved.flags & crate::contact::contact_flags::SIM_MESH_CONTACT) == 0);
352            moved.local_index = local_index;
353        }
354    }
355}