Skip to main content

box3d_rust/solver/
solve.rs

1//! Serial solve driver from solver.c: island split → prepare → sub-step loop →
2//! restitution → store → finalize → joint events → hit events → broad-phase
3//! enlarge → island sleep.
4//!
5//! SPDX-FileCopyrightText: 2025 Erin Catto
6//! SPDX-License-Identifier: MIT
7#![allow(clippy::needless_range_loop)]
8
9use super::continuous::solve_continuous;
10use super::integrate::{finalize_bodies, integrate_positions, integrate_velocities};
11use super::StepContext;
12use crate::body::body_flags;
13use crate::broad_phase::{proxy_id, proxy_type};
14use crate::constants::{GRAPH_COLOR_COUNT, RELAX_ITERATIONS, SOLVER_ITERATIONS};
15use crate::constraint_graph::OVERFLOW_INDEX;
16use crate::contact_solver::{
17    apply_restitution, apply_restitution_convex, flag_hit_events, prepare_color_contacts,
18    solve_contacts, solve_contacts_convex, store_impulses, warm_start_contacts,
19    warm_start_contacts_convex, ContactConstraint,
20};
21use crate::core::NULL_INDEX;
22use crate::events::{BodyMoveEvent, JointEvent};
23use crate::id::{BodyId, JointId};
24use crate::island::split_island;
25use crate::joint::{get_joint_reaction, prepare_joint, solve_joint, warm_start_joint};
26use crate::math_functions::WORLD_TRANSFORM_IDENTITY;
27use crate::shape::shape_flags;
28use crate::solver_set::{try_sleep_island, AWAKE_SET};
29use crate::types::BodyType;
30use crate::world::World;
31
32/// Solve joints then contacts for overflow + colors. During biased solve,
33/// force/torque thresholds set bits in `joint_state_bit_set`. (serial of
34/// b3SolveJointsTask / b3SolveContacts_Convex + b3SolveContacts_Mesh + overflow)
35fn solve_joints_then_contacts(
36    world: &mut World,
37    color_constraints: &mut [Vec<ContactConstraint>],
38    convex_counts: &[usize],
39    context: &StepContext,
40    use_bias: bool,
41) {
42    // Overflow first — always Mesh path (C: b3SolveContacts_Overflow)
43    {
44        let mut joint_sims =
45            std::mem::take(&mut world.constraint_graph.colors[OVERFLOW_INDEX as usize].joint_sims);
46        {
47            let states = &mut world.solver_sets[AWAKE_SET as usize].body_states;
48            for joint in &mut joint_sims {
49                solve_joint(joint, context, states, use_bias);
50            }
51        }
52        if use_bias {
53            for joint in &mut joint_sims {
54                maybe_flag_joint_reaction(world, joint, context.inv_h);
55            }
56        }
57        world.constraint_graph.colors[OVERFLOW_INDEX as usize].joint_sims = joint_sims;
58
59        let states = &mut world.solver_sets[AWAKE_SET as usize].body_states;
60        debug_assert_eq!(convex_counts[OVERFLOW_INDEX as usize], 0);
61        solve_contacts(
62            &mut color_constraints[OVERFLOW_INDEX as usize],
63            states,
64            context,
65            use_bias,
66        );
67    }
68
69    for color_index in 0..OVERFLOW_INDEX as usize {
70        let mut joint_sims =
71            std::mem::take(&mut world.constraint_graph.colors[color_index].joint_sims);
72        {
73            let states = &mut world.solver_sets[AWAKE_SET as usize].body_states;
74            for joint in &mut joint_sims {
75                solve_joint(joint, context, states, use_bias);
76            }
77        }
78        if use_bias {
79            for joint in &mut joint_sims {
80                maybe_flag_joint_reaction(world, joint, context.inv_h);
81            }
82        }
83        world.constraint_graph.colors[color_index].joint_sims = joint_sims;
84
85        let states = &mut world.solver_sets[AWAKE_SET as usize].body_states;
86        let convex_count = convex_counts[color_index];
87        let (convex, mesh) = color_constraints[color_index].split_at_mut(convex_count);
88        // C graph blocks: joints → wide/convex → mesh
89        solve_contacts_convex(convex, states, context, use_bias);
90        solve_contacts(mesh, states, context, use_bias);
91    }
92}
93
94fn maybe_flag_joint_reaction(world: &mut World, joint: &crate::joint::JointSim, inv_h: f32) {
95    if !(joint.force_threshold < f32::MAX || joint.torque_threshold < f32::MAX) {
96        return;
97    }
98    if world.task_contexts[0]
99        .joint_state_bit_set
100        .get_bit(joint.joint_id as u32)
101    {
102        return;
103    }
104
105    let (force, torque) = get_joint_reaction(world, joint, inv_h);
106
107    // Check thresholds. A zero threshold means all awake joints get reported.
108    if force >= joint.force_threshold || torque >= joint.torque_threshold {
109        world.task_contexts[0]
110            .joint_state_bit_set
111            .set_bit(joint.joint_id as u32);
112    }
113}
114
115/// Solve with graph coloring. (b3Solve — serial)
116///
117/// Does not increment `world.step_index` — World::step owns that.
118pub fn solve(world: &mut World, context: &StepContext) {
119    let awake_body_count = world.solver_sets[AWAKE_SET as usize].body_sims.len();
120    if awake_body_count == 0 {
121        world.broad_phase.validate_no_enlarged();
122        return;
123    }
124
125    world.body_move_events.resize(
126        awake_body_count,
127        BodyMoveEvent {
128            user_data: 0,
129            transform: WORLD_TRANSFORM_IDENTITY,
130            body_id: BodyId::default(),
131            fell_asleep: false,
132        },
133    );
134
135    let contact_id_capacity = world.contact_id_pool.id_capacity();
136    let joint_id_capacity = world.joint_id_pool.id_capacity();
137    {
138        let task_context = &mut world.task_contexts[0];
139        task_context
140            .hit_event_bit_set
141            .set_bit_count_and_clear(contact_id_capacity as u32);
142        task_context.has_hit_events = false;
143        task_context
144            .joint_state_bit_set
145            .set_bit_count_and_clear(joint_id_capacity as u32);
146    }
147
148    // Split an awake island. This modifies:
149    // - world island array and solver set
150    // - island indices on bodies, contacts, and joints
151    // C runs this as a task in parallel with the constraint solve (it cannot
152    // overlap FinalizeBodies); the serial port runs it here, at the point
153    // where C enqueues it. (b3SplitIslandTask)
154    if world.split_island_id != NULL_INDEX {
155        split_island(world, world.split_island_id);
156        world.split_island_id = NULL_INDEX;
157    }
158
159    // Prepare joints for every color (incl. overflow) before contacts.
160    // mem::take avoids &World + &mut joint_sims from the same World.
161    for color_index in 0..GRAPH_COLOR_COUNT as usize {
162        let mut joint_sims =
163            std::mem::take(&mut world.constraint_graph.colors[color_index].joint_sims);
164        for joint in &mut joint_sims {
165            prepare_joint(world, joint, context);
166        }
167        world.constraint_graph.colors[color_index].joint_sims = joint_sims;
168    }
169
170    // Prepare contact constraints for every color (convex prefix + mesh suffix).
171    let mut color_constraints: Vec<Vec<ContactConstraint>> = (0..GRAPH_COLOR_COUNT as usize)
172        .map(|_| Vec::new())
173        .collect();
174    let mut convex_counts = vec![0usize; GRAPH_COLOR_COUNT as usize];
175
176    for color_index in 0..GRAPH_COLOR_COUNT as usize {
177        let convex_ids = world.constraint_graph.colors[color_index]
178            .convex_contacts
179            .clone();
180        let mesh_specs = world.constraint_graph.colors[color_index].contacts.clone();
181        let contacts = &world.contacts;
182        let sims = &world.solver_sets[AWAKE_SET as usize].body_sims;
183        let states = &world.solver_sets[AWAKE_SET as usize].body_states;
184        convex_counts[color_index] = prepare_color_contacts(
185            &mut color_constraints[color_index],
186            &convex_ids,
187            &mesh_specs,
188            contacts,
189            sims,
190            states,
191            context,
192        );
193    }
194
195    // Sub-step loop
196    let sub_step_count = context.sub_step_count;
197    for _sub_step_index in 0..sub_step_count {
198        integrate_velocities(world, context);
199
200        // Warm start: joints then contacts; overflow first, then colors.
201        // constraint_graph and solver_sets are distinct World fields.
202        {
203            let World {
204                constraint_graph,
205                solver_sets,
206                ..
207            } = world;
208            let states = &mut solver_sets[AWAKE_SET as usize].body_states;
209            for joint in &mut constraint_graph.colors[OVERFLOW_INDEX as usize].joint_sims {
210                warm_start_joint(joint, states);
211            }
212            warm_start_contacts(&mut color_constraints[OVERFLOW_INDEX as usize], states);
213            for color_index in 0..OVERFLOW_INDEX as usize {
214                for joint in &mut constraint_graph.colors[color_index].joint_sims {
215                    warm_start_joint(joint, states);
216                }
217                let convex_count = convex_counts[color_index];
218                let (convex, mesh) = color_constraints[color_index].split_at_mut(convex_count);
219                warm_start_contacts_convex(convex, states);
220                warm_start_contacts(mesh, states);
221            }
222        }
223
224        for _ in 0..SOLVER_ITERATIONS {
225            let use_bias = true;
226            solve_joints_then_contacts(
227                world,
228                &mut color_constraints,
229                &convex_counts,
230                context,
231                use_bias,
232            );
233        }
234
235        integrate_positions(world, context);
236
237        for _ in 0..RELAX_ITERATIONS {
238            let use_bias = false;
239            solve_joints_then_contacts(
240                world,
241                &mut color_constraints,
242                &convex_counts,
243                context,
244                use_bias,
245            );
246        }
247    }
248
249    // Restitution
250    {
251        let states = &mut world.solver_sets[AWAKE_SET as usize].body_states;
252        // Overflow always uses Mesh restitution (C: b3ApplyRestitution_Overflow)
253        apply_restitution(
254            &mut color_constraints[OVERFLOW_INDEX as usize],
255            states,
256            context,
257        );
258        for color_index in 0..OVERFLOW_INDEX as usize {
259            let convex_count = convex_counts[color_index];
260            let (convex, mesh) = color_constraints[color_index].split_at_mut(convex_count);
261            apply_restitution_convex(convex, states, context);
262            apply_restitution(mesh, states, context);
263        }
264    }
265
266    // Store impulses (overflow without hit-event flagging, colors with)
267    {
268        store_impulses(
269            &color_constraints[OVERFLOW_INDEX as usize],
270            &mut world.contacts,
271        );
272
273        for color_index in 0..OVERFLOW_INDEX as usize {
274            store_impulses(&color_constraints[color_index], &mut world.contacts);
275        }
276
277        let neg_hit_threshold = -world.hit_event_threshold;
278        let mut has_hit = world.task_contexts[0].has_hit_events;
279        for color_index in 0..OVERFLOW_INDEX as usize {
280            flag_hit_events(
281                &color_constraints[color_index],
282                &world.contacts,
283                &mut world.task_contexts[0].hit_event_bit_set,
284                &mut has_hit,
285                neg_hit_threshold,
286            );
287        }
288        world.task_contexts[0].has_hit_events = has_hit;
289    }
290
291    // Finalize bodies (CCD for non-bullets; bullets queued)
292    let mut bullet_bodies: Vec<i32> = Vec::with_capacity(awake_body_count);
293    {
294        let awake_island_count = world.solver_sets[AWAKE_SET as usize].island_sims.len();
295        let task_context = &mut world.task_contexts[0];
296        task_context.sensor_hits.clear();
297        task_context
298            .enlarged_sim_bit_set
299            .set_bit_count_and_clear(awake_body_count as u32);
300        task_context
301            .awake_island_bit_set
302            .set_bit_count_and_clear(awake_island_count as u32);
303        task_context.split_island_id = NULL_INDEX;
304        task_context.split_sleep_time = 0.0;
305    }
306
307    finalize_bodies(world, context, &mut bullet_bodies);
308
309    // Report joint events (C block between finalize and hit events).
310    {
311        debug_assert!(world.joint_events.is_empty());
312        let world_id = world.world_id;
313        let word_count = world.task_contexts[0].joint_state_bit_set.block_count();
314        for k in 0..word_count {
315            let mut word = world.task_contexts[0].joint_state_bit_set.block(k);
316            while word != 0 {
317                let ctz = word.trailing_zeros();
318                let joint_id = (64 * k + ctz) as i32;
319
320                debug_assert!((joint_id as usize) < world.joints.len());
321
322                let joint = &world.joints[joint_id as usize];
323                debug_assert!(joint.set_index == AWAKE_SET);
324
325                world.joint_events.push(JointEvent {
326                    joint_id: JointId {
327                        index1: joint_id + 1,
328                        world0: world_id,
329                        generation: joint.generation,
330                    },
331                    user_data: joint.user_data,
332                });
333
334                word &= word - 1;
335            }
336        }
337    }
338
339    // Report hit events flagged during store impulses. C runs this after the
340    // joint events pass.
341    {
342        use crate::events::ContactHitEvent;
343        use crate::id::{ContactId, ShapeId};
344        use crate::math_functions::{lerp, lerp_position, offset_pos, VEC3_ZERO};
345
346        debug_assert!(world.contact_hit_events.is_empty());
347
348        // Fast path: if no worker flagged any hit-event candidates during
349        // b2StoreImpulsesTask, skip entirely.
350        if world.task_contexts[0].has_hit_events {
351            let threshold = world.hit_event_threshold;
352            let world_id = world.world_id;
353
354            let word_count = world.task_contexts[0].hit_event_bit_set.block_count();
355            for k in 0..word_count {
356                let mut word = world.task_contexts[0].hit_event_bit_set.block(k);
357                while word != 0 {
358                    let ctz = word.trailing_zeros();
359                    let contact_id = (64 * k + ctz) as i32;
360
361                    let contact = &world.contacts[contact_id as usize];
362                    debug_assert!(
363                        contact.set_index == AWAKE_SET && contact.color_index != NULL_INDEX
364                    );
365
366                    let shape_a = &world.shapes[contact.shape_id_a as usize];
367                    let shape_b = &world.shapes[contact.shape_id_b as usize];
368                    let body_a = &world.bodies[shape_a.body_id as usize];
369                    let body_b = &world.bodies[shape_b.body_id as usize];
370                    // (b3GetBodySim)
371                    let sim_a = &world.solver_sets[body_a.set_index as usize].body_sims
372                        [body_a.local_index as usize];
373                    let sim_b = &world.solver_sets[body_b.set_index as usize].body_sims
374                        [body_b.local_index as usize];
375                    let mid_center = lerp_position(sim_a.center, sim_b.center, 0.5);
376
377                    let mut approach_speed = threshold;
378                    let mut point = mid_center;
379                    let mut normal = VEC3_ZERO;
380                    let mut found = false;
381                    let mut triangle_index = 0;
382                    for manifold in &contact.manifolds {
383                        for p in 0..manifold.point_count as usize {
384                            let mp = &manifold.points[p];
385                            let mp_approach_speed = -mp.normal_velocity;
386
387                            // Need to check total impulse because the point may be speculative and not colliding
388                            if mp_approach_speed > approach_speed && mp.total_normal_impulse > 0.0 {
389                                approach_speed = mp_approach_speed;
390                                point = offset_pos(mid_center, lerp(mp.anchor_a, mp.anchor_b, 0.5));
391                                normal = manifold.normal;
392                                triangle_index = mp.triangle_index;
393                                found = true;
394                            }
395                        }
396                    }
397
398                    if found {
399                        let event = ContactHitEvent {
400                            shape_id_a: ShapeId {
401                                index1: shape_a.id + 1,
402                                world0: world_id,
403                                generation: shape_a.generation,
404                            },
405                            shape_id_b: ShapeId {
406                                index1: shape_b.id + 1,
407                                world0: world_id,
408                                generation: shape_b.generation,
409                            },
410                            contact_id: ContactId {
411                                index1: contact.contact_id + 1,
412                                world0: world_id,
413                                padding: 0,
414                                generation: contact.generation,
415                            },
416                            point,
417                            normal,
418                            approach_speed,
419                            // shapeB is never a compound today (asserted in b3CreateContact), so the
420                            // childIndex argument is irrelevant for it. shapeA carries the compound.
421                            user_material_id_a: shape_a
422                                .get_shape_user_material_id(contact.child_index, triangle_index),
423                            user_material_id_b: shape_b
424                                .get_shape_user_material_id(0, triangle_index),
425                        };
426                        world.contact_hit_events.push(event);
427                    }
428
429                    word &= word - 1;
430                }
431            }
432        }
433    }
434
435    // Enlarge broad-phase proxies for shapes whose fat AABB grew.
436    // Fast bullets only buffer moves here — final AABB comes from the bullet pass.
437    {
438        world.broad_phase.validate_no_enlarged();
439
440        let word_count = world.task_contexts[0].enlarged_sim_bit_set.block_count();
441        for k in 0..word_count {
442            let mut word = world.task_contexts[0].enlarged_sim_bit_set.block(k);
443            while word != 0 {
444                let ctz = word.trailing_zeros();
445                let body_sim_index = (64 * k + ctz) as usize;
446
447                let body_sim = world.solver_sets[AWAKE_SET as usize].body_sims[body_sim_index];
448                let body_id = body_sim.body_id;
449                let mut shape_id = world.bodies[body_id as usize].head_shape_id;
450
451                if (body_sim.flags & (body_flags::IS_BULLET | body_flags::IS_FAST))
452                    == (body_flags::IS_BULLET | body_flags::IS_FAST)
453                {
454                    while shape_id != NULL_INDEX {
455                        let proxy_key = world.shapes[shape_id as usize].proxy_key;
456                        world.broad_phase.buffer_move(proxy_key);
457                        shape_id = world.shapes[shape_id as usize].next_shape_id;
458                    }
459                } else {
460                    while shape_id != NULL_INDEX {
461                        if (world.shapes[shape_id as usize].flags & shape_flags::ENLARGED_AABB) != 0
462                        {
463                            let proxy_key = world.shapes[shape_id as usize].proxy_key;
464                            let fat_aabb = world.shapes[shape_id as usize].fat_aabb;
465                            world.broad_phase.enlarge_proxy(proxy_key, fat_aabb);
466                            world.shapes[shape_id as usize].flags &= !shape_flags::ENLARGED_AABB;
467                        }
468                        shape_id = world.shapes[shape_id as usize].next_shape_id;
469                    }
470                }
471
472                word &= word - 1;
473            }
474        }
475
476        world.broad_phase.validate();
477    }
478
479    // Bullet continuous pass, then serial enlarge of bullet proxies.
480    if !bullet_bodies.is_empty() {
481        for &sim_index in &bullet_bodies {
482            solve_continuous(world, sim_index);
483        }
484
485        let dynamic_tree = BodyType::Dynamic as usize;
486        for &sim_index in &bullet_bodies {
487            let bullet_sim =
488                &mut world.solver_sets[AWAKE_SET as usize].body_sims[sim_index as usize];
489            if (bullet_sim.flags & body_flags::ENLARGE_BOUNDS) == 0 {
490                continue;
491            }
492            bullet_sim.flags &= !body_flags::ENLARGE_BOUNDS;
493            let body_id = bullet_sim.body_id;
494
495            let mut shape_id = world.bodies[body_id as usize].head_shape_id;
496            while shape_id != NULL_INDEX {
497                if (world.shapes[shape_id as usize].flags & shape_flags::ENLARGED_AABB) == 0 {
498                    shape_id = world.shapes[shape_id as usize].next_shape_id;
499                    continue;
500                }
501                world.shapes[shape_id as usize].flags &= !shape_flags::ENLARGED_AABB;
502
503                let proxy_key = world.shapes[shape_id as usize].proxy_key;
504                let proxy_id_ = proxy_id(proxy_key);
505                debug_assert!(proxy_type(proxy_key) == BodyType::Dynamic);
506                debug_assert!(
507                    world.broad_phase.moved_proxies[dynamic_tree].get_bit(proxy_id_ as u32)
508                );
509
510                let fat_aabb = world.shapes[shape_id as usize].fat_aabb;
511                world.broad_phase.trees[dynamic_tree].enlarge_proxy(proxy_id_, fat_aabb);
512
513                shape_id = world.shapes[shape_id as usize].next_shape_id;
514            }
515        }
516    }
517
518    // Report sensor hits. This may include bullet sensor hits.
519    {
520        let mut drained: Vec<(i32, i32, u16)> = Vec::new();
521        for task_context in &world.task_contexts {
522            for hit in &task_context.sensor_hits {
523                let sensor_index = world.shapes[hit.sensor_id as usize].sensor_index;
524                let generation = world.shapes[hit.visitor_id as usize].generation;
525                drained.push((sensor_index, hit.visitor_id, generation));
526            }
527        }
528        for (sensor_index, visitor_id, generation) in drained {
529            world.sensors[sensor_index as usize]
530                .hits
531                .push(crate::sensor::Visitor {
532                    shape_id: visitor_id,
533                    generation,
534                });
535        }
536    }
537
538    // Island sleeping
539    // This must be done last because putting islands to sleep invalidates the enlarged body bits.
540    if world.enable_sleep {
541        // Collect split island candidate for the next time step. No need to split if sleeping is disabled.
542        debug_assert!(world.split_island_id == NULL_INDEX);
543        let mut split_sleep_timer = 0.0f32;
544        {
545            let task_context = &world.task_contexts[0];
546            if task_context.split_island_id != NULL_INDEX
547                && task_context.split_sleep_time >= split_sleep_timer
548            {
549                debug_assert!(task_context.split_sleep_time > 0.0);
550
551                // Tie breaking for determinism. Largest island id wins. C needs this
552                // due to work stealing across workers; kept for the serial port so a
553                // multi-worker build later cannot change the outcome.
554                let tied_but_smaller = task_context.split_sleep_time == split_sleep_timer
555                    && task_context.split_island_id < world.split_island_id;
556                if !tied_but_smaller {
557                    world.split_island_id = task_context.split_island_id;
558                    split_sleep_timer = task_context.split_sleep_time;
559                }
560            }
561        }
562        let _ = split_sleep_timer;
563
564        // Need to process in reverse because this moves islands to sleeping solver sets.
565        let count = world.solver_sets[AWAKE_SET as usize].island_sims.len();
566        for island_index in (0..count).rev() {
567            if world.task_contexts[0]
568                .awake_island_bit_set
569                .get_bit(island_index as u32)
570            {
571                // this island is still awake
572                continue;
573            }
574
575            let island_id =
576                world.solver_sets[AWAKE_SET as usize].island_sims[island_index].island_id;
577
578            try_sleep_island(world, island_id);
579        }
580
581        world.validate_solver_sets();
582    }
583}