Skip to main content

box2d_rust/solver/
solve.rs

1// The solve driver from solver.c: b2Solve with the b2SolverTask stage
2// sequence run serially.
3//
4// The C partitions each stage into blocks claimed by workers via atomic CAS;
5// with one worker every stage is a single full-range block, so the sync
6// machinery (b2SolverStage/b2SyncBlock/atomicSyncBits) disappears and the
7// orchestrator loop in b2SolverTask becomes plain loops here. The stage
8// order, the overflow-before-colors ordering, and the ascending color order
9// are preserved exactly — they determine the float accumulation order.
10//
11// Per-color contact constraints live in local Vecs (the C uses arena scratch
12// hung off b2GraphColor / b2StepContext). The graph colors run the wide (SIMD)
13// kernels four contacts at a time, stored in blocks of SIMD_WIDTH; the overflow
14// color keeps the scalar kernels. This mirrors the C dispatch in solver.c,
15// where colored blocks route to b2*ContactsTask and the overflow color routes
16// to b2*Contacts_Overflow.
17//
18// SPDX-FileCopyrightText: 2023 Erin Catto
19// SPDX-License-Identifier: MIT
20//
21// bring-up: called by the world step slice.
22// The range loops index two parallel arrays (constraints and graph colors).
23#![allow(clippy::needless_range_loop)]
24
25use super::integrate::{finalize_bodies, integrate_positions, integrate_velocities};
26use super::StepContext;
27use crate::constraint_graph::OVERFLOW_INDEX;
28use crate::contact::contact_flags;
29use crate::contact_solver::wide_kernels::{
30    apply_restitution_wide, prepare_contacts_wide, solve_contacts_wide, store_impulses_wide,
31    warm_start_contacts_wide, wide_block_count, ContactConstraintWide,
32};
33use crate::contact_solver::{
34    apply_restitution, prepare_contacts, solve_contacts, store_impulses, warm_start_contacts,
35    ContactConstraint,
36};
37use crate::core::NULL_INDEX;
38use crate::events::{BodyMoveEvent, ContactHitEvent, JointEvent};
39use crate::id::{BodyId, ContactId, JointId, ShapeId};
40use crate::joint::{get_joint_reaction, prepare_joint, solve_joint, warm_start_joint};
41use crate::math_functions::{make_world_transform, offset_pos, TRANSFORM_IDENTITY};
42use crate::solver_set::AWAKE_SET;
43use crate::timer::{get_milliseconds, get_milliseconds_and_reset, get_ticks};
44use crate::types::BodyType;
45use crate::world::World;
46
47// (solver.c: ITERATIONS / RELAX_ITERATIONS)
48const ITERATIONS: i32 = 1;
49const RELAX_ITERATIONS: i32 = 1;
50
51/// Prepare the joints of one graph color. The C prepare stage reads world
52/// data while writing the sim, so the sims are copied out and back.
53fn prepare_color_joints(world: &mut World, color_index: i32, context: &StepContext) {
54    let count = world.constraint_graph.colors[color_index as usize]
55        .joint_sims
56        .len();
57    for i in 0..count {
58        let mut sim = world.constraint_graph.colors[color_index as usize].joint_sims[i];
59        prepare_joint(world, &mut sim, context);
60        world.constraint_graph.colors[color_index as usize].joint_sims[i] = sim;
61    }
62}
63
64/// Solve with graph coloring. (b2Solve)
65pub fn solve(world: &mut World, context: &StepContext) {
66    // Only count steps that advance the simulation
67    world.step_index += 1;
68
69    // Are there any awake bodies? This scenario should not be important for
70    // profiling.
71    let awake_body_count = world.solver_sets[AWAKE_SET as usize].body_sims.len();
72    if awake_body_count == 0 {
73        world.broad_phase.validate_no_enlarged();
74        return;
75    }
76
77    // Solver setup: buffers, move events, bit sets, island split enqueue/run.
78    // (solver.c: setupTicks → profile.solverSetup)
79    let mut setup_ticks = get_ticks();
80
81    // Prepare buffer for bullets (arena in C)
82    let mut bullet_bodies: Vec<i32> = Vec::with_capacity(awake_body_count);
83
84    // prepare for move events
85    world.body_move_events.resize(
86        awake_body_count,
87        BodyMoveEvent {
88            user_data: 0,
89            transform: make_world_transform(TRANSFORM_IDENTITY),
90            body_id: BodyId::default(),
91            fell_asleep: false,
92        },
93    );
94
95    // Reset the per-step event bit sets (b2Solve does this per worker before
96    // spawning the solver tasks)
97    let joint_id_capacity = world.joint_id_pool.id_capacity();
98    let contact_id_capacity = world.contact_id_pool.id_capacity();
99    {
100        let task_context = &mut world.task_contexts[0];
101        task_context
102            .joint_state_bit_set
103            .set_bit_count_and_clear(joint_id_capacity as u32);
104        task_context
105            .hit_event_bit_set
106            .set_bit_count_and_clear(contact_id_capacity as u32);
107        task_context.has_hit_events = false;
108    }
109
110    // Split an awake island. The C enqueues b2SplitIslandTask to run
111    // concurrently with the constraint solve and finishes it before body
112    // finalization; the serial port runs it inline here. Timing still goes to
113    // split_islands (b2SplitIslandTask) and is also nested in solver_setup
114    // when the split runs synchronously during setup.
115    if world.split_island_id != NULL_INDEX {
116        let split_ticks = get_ticks();
117        let split_id = world.split_island_id;
118        crate::island::split_island(world, split_id);
119        world.split_island_id = NULL_INDEX;
120        world.profile.split_islands += get_milliseconds(split_ticks);
121    }
122
123    world.profile.solver_setup = get_milliseconds_and_reset(&mut setup_ticks);
124
125    // Constraint solve wall time (prepare → store impulses), matching C's
126    // profile.constraints around b2SolverTask.
127    let mut constraint_ticks = get_ticks();
128    let mut ticks = get_ticks();
129
130    // === Prepare constraints ===
131    // (stage b2_stagePrepareJoints: colored joints in ascending color order)
132    for color_index in 0..OVERFLOW_INDEX {
133        prepare_color_joints(world, color_index, context);
134    }
135
136    // (stage b2_stagePrepareContacts: colored contacts run the wide kernels;
137    // the overflow color runs the scalar kernels below)
138    let mut wide_constraints: Vec<Vec<ContactConstraintWide>> =
139        (0..OVERFLOW_INDEX).map(|_| Vec::new()).collect();
140    for color_index in 0..OVERFLOW_INDEX as usize {
141        let count = world.constraint_graph.colors[color_index]
142            .contact_sims
143            .len();
144        wide_constraints[color_index]
145            .resize(wide_block_count(count), ContactConstraintWide::default());
146    }
147    let mut overflow_constraints: Vec<ContactConstraint> = Vec::new();
148    overflow_constraints.resize(
149        world.constraint_graph.colors[OVERFLOW_INDEX as usize]
150            .contact_sims
151            .len(),
152        ContactConstraint::default(),
153    );
154
155    let enable_softening = world.enable_contact_softening;
156    let contact_hertz = world.contact_hertz;
157    let contact_damping_ratio = world.contact_damping_ratio;
158    for color_index in 0..OVERFLOW_INDEX as usize {
159        let contacts = &world.constraint_graph.colors[color_index].contact_sims;
160        let states = &world.solver_sets[AWAKE_SET as usize].body_states;
161        prepare_contacts_wide(
162            &mut wide_constraints[color_index],
163            contacts,
164            states,
165            context,
166            enable_softening,
167            contact_hertz,
168            contact_damping_ratio,
169        );
170    }
171
172    // Single-threaded overflow work. These constraints don't fit in the graph
173    // coloring. (b2PrepareJoints_Overflow / b2PrepareContacts_Overflow)
174    prepare_color_joints(world, OVERFLOW_INDEX, context);
175    {
176        let contacts = &world.constraint_graph.colors[OVERFLOW_INDEX as usize].contact_sims;
177        let states = &world.solver_sets[AWAKE_SET as usize].body_states;
178        prepare_contacts(&mut overflow_constraints, contacts, states, context);
179    }
180
181    world.profile.prepare_constraints += get_milliseconds_and_reset(&mut ticks);
182
183    // === Sub-step loop ===
184    let sub_step_count = context.sub_step_count;
185    for _sub_step_index in 0..sub_step_count {
186        // Integrate velocities
187        integrate_velocities(world, context);
188        world.profile.integrate_velocities += get_milliseconds_and_reset(&mut ticks);
189
190        // Warm start constraints: overflow joints, overflow contacts, then
191        // each color (joints before contacts, matching the graph block
192        // layout).
193        {
194            let world_parts = &mut *world;
195            let graph = &mut world_parts.constraint_graph;
196            let states = &mut world_parts.solver_sets[AWAKE_SET as usize].body_states;
197
198            for joint in graph.colors[OVERFLOW_INDEX as usize].joint_sims.iter_mut() {
199                warm_start_joint(joint, states);
200            }
201            warm_start_contacts(&mut overflow_constraints, states);
202
203            for color_index in 0..OVERFLOW_INDEX as usize {
204                for joint in graph.colors[color_index].joint_sims.iter_mut() {
205                    warm_start_joint(joint, states);
206                }
207                warm_start_contacts_wide(&mut wide_constraints[color_index], states);
208            }
209        }
210        world.profile.warm_start += get_milliseconds_and_reset(&mut ticks);
211
212        // Solve constraints
213        for _ in 0..ITERATIONS {
214            let use_bias = true;
215            let world_parts = &mut *world;
216            let graph = &mut world_parts.constraint_graph;
217            let states = &mut world_parts.solver_sets[AWAKE_SET as usize].body_states;
218            let joint_state_bit_set = &mut world_parts.task_contexts[0].joint_state_bit_set;
219
220            // Overflow constraints have lower priority. Typically these are
221            // dynamic-vs-dynamic. (b2SolveJoints_Overflow does not report
222            // joint events; only the colored b2SolveJointsTask does.)
223            for joint in graph.colors[OVERFLOW_INDEX as usize].joint_sims.iter_mut() {
224                solve_joint(joint, context, states, use_bias);
225            }
226            solve_contacts(&mut overflow_constraints, states, context, use_bias);
227
228            for color_index in 0..OVERFLOW_INDEX as usize {
229                for joint in graph.colors[color_index].joint_sims.iter_mut() {
230                    solve_joint(joint, context, states, use_bias);
231
232                    if (joint.force_threshold < f32::MAX || joint.torque_threshold < f32::MAX)
233                        && !joint_state_bit_set.get_bit(joint.joint_id as u32)
234                    {
235                        let (force, torque) = get_joint_reaction(joint, context.inv_h);
236
237                        // Check thresholds. A zero threshold means all awake
238                        // joints get reported.
239                        if force >= joint.force_threshold || torque >= joint.torque_threshold {
240                            // Flag this joint for processing.
241                            joint_state_bit_set.set_bit(joint.joint_id as u32);
242                        }
243                    }
244                }
245                solve_contacts_wide(
246                    &mut wide_constraints[color_index],
247                    states,
248                    context,
249                    use_bias,
250                );
251            }
252        }
253        world.profile.solve_impulses += get_milliseconds_and_reset(&mut ticks);
254
255        // Integrate positions
256        integrate_positions(world, context);
257        world.profile.integrate_positions += get_milliseconds_and_reset(&mut ticks);
258
259        // Relax constraints
260        for _ in 0..RELAX_ITERATIONS {
261            let use_bias = false;
262            let world_parts = &mut *world;
263            let graph = &mut world_parts.constraint_graph;
264            let states = &mut world_parts.solver_sets[AWAKE_SET as usize].body_states;
265
266            for joint in graph.colors[OVERFLOW_INDEX as usize].joint_sims.iter_mut() {
267                solve_joint(joint, context, states, use_bias);
268            }
269            solve_contacts(&mut overflow_constraints, states, context, use_bias);
270
271            for color_index in 0..OVERFLOW_INDEX as usize {
272                for joint in graph.colors[color_index].joint_sims.iter_mut() {
273                    solve_joint(joint, context, states, use_bias);
274                }
275                solve_contacts_wide(
276                    &mut wide_constraints[color_index],
277                    states,
278                    context,
279                    use_bias,
280                );
281            }
282        }
283        world.profile.relax_impulses += get_milliseconds_and_reset(&mut ticks);
284    }
285
286    // Restitution: overflow first, then each color (contacts only)
287    {
288        let world_parts = &mut *world;
289        let states = &mut world_parts.solver_sets[AWAKE_SET as usize].body_states;
290
291        apply_restitution(&mut overflow_constraints, states, context);
292        for color_index in 0..OVERFLOW_INDEX as usize {
293            apply_restitution_wide(&mut wide_constraints[color_index], states, context);
294        }
295    }
296    world.profile.apply_restitution += get_milliseconds_and_reset(&mut ticks);
297
298    // Store impulses: overflow first (no hit-event flagging in the C overflow
299    // path), then the colored contacts with hit-event flagging
300    // (b2StoreImpulsesTask).
301    {
302        let world_parts = &mut *world;
303        let graph = &mut world_parts.constraint_graph;
304        let task_context = &mut world_parts.task_contexts[0];
305        let neg_hit_threshold = -world_parts.hit_event_threshold;
306
307        store_impulses(
308            &overflow_constraints,
309            &mut graph.colors[OVERFLOW_INDEX as usize].contact_sims,
310        );
311
312        for color_index in 0..OVERFLOW_INDEX as usize {
313            store_impulses_wide(
314                &wide_constraints[color_index],
315                &mut graph.colors[color_index].contact_sims,
316            );
317
318            // Check for hit events to speed up serial processing later in the
319            // step
320            for contact_sim in &graph.colors[color_index].contact_sims {
321                if contact_sim.sim_flags & contact_flags::SIM_ENABLE_HIT_EVENT != 0 {
322                    for k in 0..contact_sim.manifold.point_count as usize {
323                        let mp = &contact_sim.manifold.points[k];
324
325                        // Need to check total impulse because the point may be
326                        // speculative and not colliding
327                        if mp.normal_velocity < neg_hit_threshold && mp.total_normal_impulse > 0.0 {
328                            task_context
329                                .hit_event_bit_set
330                                .set_bit(contact_sim.contact_id as u32);
331                            task_context.has_hit_events = true;
332                            break;
333                        }
334                    }
335                }
336            }
337        }
338    }
339    world.profile.store_impulses += get_milliseconds_and_reset(&mut ticks);
340
341    // Finish island split (already done inline above in the serial port).
342    world.profile.constraints = get_milliseconds_and_reset(&mut constraint_ticks);
343
344    // === Finalize bodies ===
345    let transform_ticks = get_ticks();
346
347    // Prepare contact, enlarged body, and island bit sets used in body
348    // finalization.
349    {
350        let awake_island_count = world.solver_sets[AWAKE_SET as usize].island_sims.len();
351        let task_context = &mut world.task_contexts[0];
352        task_context.sensor_hits.clear();
353        task_context
354            .enlarged_sim_bit_set
355            .set_bit_count_and_clear(awake_body_count as u32);
356        task_context
357            .awake_island_bit_set
358            .set_bit_count_and_clear(awake_island_count as u32);
359        task_context.split_island_id = NULL_INDEX;
360        task_context.split_sleep_time = 0.0;
361    }
362
363    // Finalize bodies. Must happen after the constraint solver and after
364    // island splitting.
365    finalize_bodies(world, context, &mut bullet_bodies);
366
367    world.profile.transforms = get_milliseconds(transform_ticks);
368
369    // === Report joint events ===
370    {
371        let joint_event_ticks = get_ticks();
372        let world_id = world.world_id;
373        let word_count = world.task_contexts[0].joint_state_bit_set.block_count();
374        for k in 0..word_count {
375            let mut word = world.task_contexts[0].joint_state_bit_set.block(k);
376            while word != 0 {
377                let ctz = word.trailing_zeros();
378                let joint_id = (64 * k + ctz) as i32;
379
380                let joint = &world.joints[joint_id as usize];
381                debug_assert!(joint.set_index == AWAKE_SET);
382
383                let event = JointEvent {
384                    joint_id: JointId {
385                        index1: joint_id + 1,
386                        world0: world_id,
387                        generation: joint.generation,
388                    },
389                    user_data: joint.user_data,
390                };
391
392                world.joint_events.push(event);
393
394                // Clear the smallest set bit
395                word &= word - 1;
396            }
397        }
398        world.profile.joint_events = get_milliseconds(joint_event_ticks);
399    }
400
401    // === Report hit events ===
402    {
403        let hit_ticks = get_ticks();
404        debug_assert!(world.contact_hit_events.is_empty());
405
406        if world.task_contexts[0].has_hit_events {
407            let threshold = world.hit_event_threshold;
408            let world_id = world.world_id;
409
410            let word_count = world.task_contexts[0].hit_event_bit_set.block_count();
411            for k in 0..word_count {
412                let mut word = world.task_contexts[0].hit_event_bit_set.block(k);
413                while word != 0 {
414                    let ctz = word.trailing_zeros();
415                    let contact_id = (64 * k + ctz) as i32;
416
417                    let contact = world.contacts[contact_id as usize];
418                    debug_assert!(
419                        contact.set_index == AWAKE_SET && contact.color_index != NULL_INDEX
420                    );
421
422                    let contact_sim = &world.constraint_graph.colors[contact.color_index as usize]
423                        .contact_sims[contact.local_index as usize];
424
425                    let mut approach_speed = threshold;
426                    let mut best_point: Option<usize> = None;
427                    for p in 0..contact_sim.manifold.point_count as usize {
428                        let mp = &contact_sim.manifold.points[p];
429                        let point_approach_speed = -mp.normal_velocity;
430
431                        // Need to check total impulse because the point may be
432                        // speculative and not colliding
433                        if point_approach_speed > approach_speed && mp.total_normal_impulse > 0.0 {
434                            approach_speed = point_approach_speed;
435                            best_point = Some(p);
436                        }
437                    }
438
439                    if let Some(p) = best_point {
440                        let best = contact_sim.manifold.points[p];
441                        let normal = contact_sim.manifold.normal;
442
443                        let shape_a = &world.shapes[contact_sim.shape_id_a as usize];
444                        let shape_b = &world.shapes[contact_sim.shape_id_b as usize];
445
446                        // World contact point reconstructed from a body center
447                        // of mass and the matching anchor. The anchors were
448                        // built with the manifold, so a body that has moved
449                        // since drags the point with it. A static body has not
450                        // moved, prefer one so the common case of a fast body
451                        // striking the world stays exact.
452                        let body_a = &world.bodies[shape_a.body_id as usize];
453                        let body_b = &world.bodies[shape_b.body_id as usize];
454                        let point = if body_a.type_ != BodyType::Static
455                            && body_b.type_ == BodyType::Static
456                        {
457                            let body_sim_b = &world.solver_sets[body_b.set_index as usize]
458                                .body_sims[body_b.local_index as usize];
459                            offset_pos(body_sim_b.center, best.anchor_b)
460                        } else {
461                            let body_sim_a = &world.solver_sets[body_a.set_index as usize]
462                                .body_sims[body_a.local_index as usize];
463                            offset_pos(body_sim_a.center, best.anchor_a)
464                        };
465
466                        let event = ContactHitEvent {
467                            shape_id_a: ShapeId {
468                                index1: shape_a.id + 1,
469                                world0: world_id,
470                                generation: shape_a.generation,
471                            },
472                            shape_id_b: ShapeId {
473                                index1: shape_b.id + 1,
474                                world0: world_id,
475                                generation: shape_b.generation,
476                            },
477                            contact_id: ContactId {
478                                index1: contact.contact_id + 1,
479                                world0: world_id,
480                                padding: 0,
481                                generation: contact.generation,
482                            },
483                            point,
484                            normal,
485                            approach_speed,
486                        };
487
488                        world.contact_hit_events.push(event);
489                    }
490
491                    // Clear the smallest set bit
492                    word &= word - 1;
493                }
494            }
495        }
496        world.profile.hit_events = get_milliseconds(hit_ticks);
497    }
498
499    // === Refit broad phase ===
500    {
501        let refit_ticks = get_ticks();
502        world.broad_phase.validate_no_enlarged();
503
504        // Enlarge broad-phase proxies and build move array.
505        // Apply shape AABB changes to broad-phase. This also creates the move
506        // array which must be in deterministic order. Sim bodies are tracked
507        // because the number of shape ids can be huge. This has to happen
508        // before bullets are processed.
509        let word_count = world.task_contexts[0].enlarged_sim_bit_set.block_count();
510        for k in 0..word_count {
511            let mut word = world.task_contexts[0].enlarged_sim_bit_set.block(k);
512            while word != 0 {
513                let ctz = word.trailing_zeros();
514                let body_sim_index = (64 * k + ctz) as usize;
515
516                let (body_id, sim_flags) = {
517                    let body_sim = &world.solver_sets[AWAKE_SET as usize].body_sims[body_sim_index];
518                    (body_sim.body_id, body_sim.flags)
519                };
520
521                let mut shape_id = world.bodies[body_id as usize].head_shape_id;
522                if sim_flags
523                    & (crate::body::body_flags::IS_BULLET | crate::body::body_flags::IS_FAST)
524                    == (crate::body::body_flags::IS_BULLET | crate::body::body_flags::IS_FAST)
525                {
526                    // Fast bullet bodies don't have their final AABB yet
527                    while shape_id != NULL_INDEX {
528                        let proxy_key = world.shapes[shape_id as usize].proxy_key;
529
530                        // Shape is fast. Its aabb will be enlarged in
531                        // continuous collision. Update the move array here for
532                        // determinism because bullets are processed below in
533                        // non-deterministic order.
534                        world.broad_phase.buffer_move(proxy_key);
535
536                        shape_id = world.shapes[shape_id as usize].next_shape_id;
537                    }
538                } else {
539                    while shape_id != NULL_INDEX {
540                        // The AABB may not have been enlarged, despite the
541                        // body being flagged as enlarged. For example, a body
542                        // with multiple shapes may have not have all shapes
543                        // enlarged. A fast body may have been flagged as
544                        // enlarged despite having no shapes enlarged.
545                        if world.shapes[shape_id as usize].enlarged_aabb {
546                            let proxy_key = world.shapes[shape_id as usize].proxy_key;
547                            let fat_aabb = world.shapes[shape_id as usize].fat_aabb;
548                            world.broad_phase.enlarge_proxy(proxy_key, fat_aabb);
549                            world.shapes[shape_id as usize].enlarged_aabb = false;
550                        }
551
552                        shape_id = world.shapes[shape_id as usize].next_shape_id;
553                    }
554                }
555
556                // Clear the smallest set bit
557                word &= word - 1;
558            }
559        }
560
561        world.broad_phase.validate();
562        world.profile.refit = get_milliseconds(refit_ticks);
563    }
564
565    // === Bullets ===
566    if !bullet_bodies.is_empty() {
567        let bullet_ticks = get_ticks();
568        // Fast bullet bodies. Note: a bullet body may be moving slow.
569        // (b2BulletBodyTask)
570        for &sim_index in &bullet_bodies {
571            super::continuous::solve_continuous(world, sim_index);
572        }
573
574        // Serially enlarge broad-phase proxies for bullet shapes.
575        // This loop has non-deterministic order in C but it shouldn't affect
576        // the result; the serial port follows the bullet array order.
577        for &sim_index in &bullet_bodies {
578            let (body_id, enlarge) = {
579                let bullet_body_sim =
580                    &world.solver_sets[AWAKE_SET as usize].body_sims[sim_index as usize];
581                (
582                    bullet_body_sim.body_id,
583                    bullet_body_sim.flags & crate::body::body_flags::ENLARGE_BOUNDS != 0,
584                )
585            };
586            if !enlarge {
587                continue;
588            }
589
590            // Clear flag
591            world.solver_sets[AWAKE_SET as usize].body_sims[sim_index as usize].flags &=
592                !crate::body::body_flags::ENLARGE_BOUNDS;
593
594            let mut shape_id = world.bodies[body_id as usize].head_shape_id;
595            while shape_id != NULL_INDEX {
596                if !world.shapes[shape_id as usize].enlarged_aabb {
597                    shape_id = world.shapes[shape_id as usize].next_shape_id;
598                    continue;
599                }
600
601                // Clear flag
602                world.shapes[shape_id as usize].enlarged_aabb = false;
603
604                let proxy_key = world.shapes[shape_id as usize].proxy_key;
605                let proxy_id = crate::broad_phase::proxy_id(proxy_key);
606                debug_assert!(crate::broad_phase::proxy_type(proxy_key) == BodyType::Dynamic);
607
608                // all fast bullet shapes should already be in the move buffer
609                debug_assert!(world.broad_phase.moved_proxies[BodyType::Dynamic as usize]
610                    .get_bit(proxy_id as u32));
611
612                let fat_aabb = world.shapes[shape_id as usize].fat_aabb;
613                world.broad_phase.trees[BodyType::Dynamic as usize]
614                    .enlarge_proxy(proxy_id, fat_aabb);
615
616                shape_id = world.shapes[shape_id as usize].next_shape_id;
617            }
618        }
619        world.profile.bullets = get_milliseconds(bullet_ticks);
620    }
621
622    // === Report sensor hits ===
623    // This may include bullet sensor hits.
624    {
625        let sensor_hit_ticks = get_ticks();
626        let hits = std::mem::take(&mut world.task_contexts[0].sensor_hits);
627        for hit in hits {
628            let sensor_index = world.shapes[hit.sensor_id as usize].sensor_index;
629            let generation = world.shapes[hit.visitor_id as usize].generation;
630
631            let shape_ref = crate::sensor::Visitor {
632                shape_id: hit.visitor_id,
633                generation,
634            };
635            world.sensors[sensor_index as usize].hits.push(shape_ref);
636        }
637        world.profile.sensor_hits = get_milliseconds(sensor_hit_ticks);
638    }
639
640    // === Island sleeping ===
641    // This must be done last because putting islands to sleep invalidates the
642    // enlarged body bits.
643    if world.enable_sleep {
644        let sleep_ticks = get_ticks();
645        // Collect split island candidate for the next time step. No need to
646        // split if sleeping is disabled.
647        debug_assert!(world.split_island_id == NULL_INDEX);
648        {
649            let task_context = &world.task_contexts[0];
650            if task_context.split_island_id != NULL_INDEX && task_context.split_sleep_time >= 0.0 {
651                debug_assert!(task_context.split_sleep_time > 0.0);
652                world.split_island_id = task_context.split_island_id;
653            }
654        }
655
656        // Need to process in reverse because this moves islands to sleeping
657        // solver sets.
658        let count = world.solver_sets[AWAKE_SET as usize].island_sims.len();
659        for island_index in (0..count).rev() {
660            if world.task_contexts[0]
661                .awake_island_bit_set
662                .get_bit(island_index as u32)
663            {
664                // this island is still awake
665                continue;
666            }
667
668            let island_id =
669                world.solver_sets[AWAKE_SET as usize].island_sims[island_index].island_id;
670
671            crate::solver_set::try_sleep_island(world, island_id);
672        }
673
674        world.validate_solver_sets();
675        world.profile.sleep_islands = get_milliseconds(sleep_ticks);
676    }
677}