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