Skip to main content

box3d_rust/world/
mod.rs

1// Port of the world data model from box3d-cpp-reference/src/physics_world.h
2// (b3World, b3TaskContext) plus b3Profile from types.h. Logic from
3// physics_world.c lands across the remaining bring-up commits.
4//
5// Porting decisions (mirroring box2d-rust):
6// - C keeps a global world registry. The Rust `World` is an owned object;
7//   `world_id`/`generation` are kept so body/shape/joint ids remain
8//   bit-compatible with C.
9// - The arena allocator (b3Stack) is per-step scratch; the Rust solver
10//   allocates scratch as Vecs in the step, so there is no stack field.
11// - Manifold block allocators become Vec-owned manifolds on Contact for now;
12//   a pooled allocator can return later if profiling warrants it.
13// - Scheduler/task-system fields are deferred with the single-threaded port;
14//   worker_count is kept and clamped like C.
15// - b3Recording lands with its slice; placeholders omit opaque C pointers.
16// - Hull database is a content-keyed Rc store (verstable map in C).
17// - Pre-solve and custom-filter callbacks keep the C shape as Option<fn> with
18//   a u64 context.
19//
20// SPDX-FileCopyrightText: 2025 Erin Catto
21// SPDX-License-Identifier: MIT
22
23mod api;
24mod draw;
25mod dump;
26mod query;
27mod step;
28mod validate;
29
30pub use api::*;
31pub use draw::*;
32pub use dump::*;
33pub use query::*;
34
35use crate::bitset::BitSet;
36use crate::body::Body;
37use crate::broad_phase::BroadPhase;
38use crate::constants::CONTACT_MANIFOLD_COUNT_BUCKETS;
39use crate::constraint_graph::ConstraintGraph;
40use crate::contact::Contact;
41use crate::debug_draw::{
42    CreateDebugShapeCallback, DebugLine, DebugPoint, DestroyDebugShapeCallback,
43    DEBUG_LINE_CAPACITY, DEBUG_POINT_CAPACITY,
44};
45use crate::events::{
46    BodyMoveEvent, ContactBeginTouchEvent, ContactEndTouchEvent, ContactHitEvent, JointEvent,
47    SensorBeginTouchEvent, SensorEndTouchEvent,
48};
49use crate::hull::HullDatabase;
50use crate::id::ShapeId;
51use crate::id_pool::IdPool;
52use crate::island::Island;
53use crate::joint::Joint;
54use crate::math_functions::Vec3;
55use crate::name_cache::NameCache;
56use crate::sensor::{Sensor, SensorHit, SensorTaskContext};
57use crate::shape::Shape;
58use crate::solver_set::SolverSet;
59use crate::types::{Capacity, FrictionCallback, RestitutionCallback};
60
61/// Prototype for a contact filter callback. (b3CustomFilterFcn)
62pub type CustomFilterFcn = fn(ShapeId, ShapeId, u64) -> bool;
63
64/// Prototype for a pre-solve callback. (b3PreSolveFcn)
65pub type PreSolveFcn =
66    fn(ShapeId, ShapeId, crate::math_functions::Pos, crate::math_functions::Vec3, u64) -> bool;
67
68/// Profiling data. Times are in milliseconds. (b3Profile)
69#[derive(Debug, Clone, Copy, PartialEq, Default)]
70pub struct Profile {
71    pub step: f32,
72    pub pairs: f32,
73    pub collide: f32,
74    pub solve: f32,
75    pub solver_setup: f32,
76    pub constraints: f32,
77    pub prepare_constraints: f32,
78    pub integrate_velocities: f32,
79    pub warm_start: f32,
80    pub solve_impulses: f32,
81    pub integrate_positions: f32,
82    pub relax_impulses: f32,
83    pub apply_restitution: f32,
84    pub store_impulses: f32,
85    pub split_islands: f32,
86    pub transforms: f32,
87    pub sensor_hits: f32,
88    pub joint_events: f32,
89    pub hit_events: f32,
90    pub refit: f32,
91    pub bullets: f32,
92    pub sleep_islands: f32,
93    pub sensors: f32,
94}
95
96/// Per thread task storage. (b3TaskContext)
97///
98/// Arena, debug draw buffers, and false-sharing padding from C are omitted;
99/// the serial port owns step scratch locally.
100#[derive(Debug, Clone)]
101pub struct TaskContext {
102    /// Collect per thread sensor continuous hit events.
103    pub sensor_hits: Vec<SensorHit>,
104
105    /// Align with contact id capacity; signal a change in contact status.
106    pub contact_state_bit_set: BitSet,
107
108    /// Align with joint id capacity; signal a change in joint status.
109    pub joint_state_bit_set: BitSet,
110
111    /// Align with contact id capacity; signal a hit event.
112    pub hit_event_bit_set: BitSet,
113
114    /// Fast-path flag: true when this worker set at least one bit in
115    /// hit_event_bit_set this step.
116    pub has_hit_events: bool,
117
118    /// Track bodies with shapes that have enlarged AABBs.
119    pub enlarged_sim_bit_set: BitSet,
120
121    /// Used to put islands to sleep.
122    pub awake_island_bit_set: BitSet,
123
124    /// Per worker split island candidate.
125    pub split_sleep_time: f32,
126    pub split_island_id: i32,
127
128    /// Profiling
129    pub sat_call_count: i32,
130    pub sat_cache_hit_count: i32,
131    pub distance_iterations: i32,
132    pub push_back_iterations: i32,
133    pub root_iterations: i32,
134
135    /// Number of contacts recycled this step (collide pass).
136    pub recycled_contact_count: i32,
137
138    pub manifold_counts: [i32; CONTACT_MANIFOLD_COUNT_BUCKETS],
139
140    /// Solver debug points drawn by `world_draw`. (`b3TaskContext::points`)
141    pub points: [DebugPoint; DEBUG_POINT_CAPACITY],
142    pub point_count: usize,
143
144    /// Solver debug lines drawn by `world_draw`. (`b3TaskContext::lines`)
145    pub lines: [DebugLine; DEBUG_LINE_CAPACITY],
146    pub line_count: usize,
147}
148
149impl Default for TaskContext {
150    fn default() -> Self {
151        TaskContext {
152            sensor_hits: Vec::new(),
153            contact_state_bit_set: BitSet::default(),
154            joint_state_bit_set: BitSet::default(),
155            hit_event_bit_set: BitSet::default(),
156            has_hit_events: false,
157            enlarged_sim_bit_set: BitSet::default(),
158            awake_island_bit_set: BitSet::default(),
159            split_sleep_time: 0.0,
160            split_island_id: 0,
161            sat_call_count: 0,
162            sat_cache_hit_count: 0,
163            distance_iterations: 0,
164            push_back_iterations: 0,
165            root_iterations: 0,
166            recycled_contact_count: 0,
167            manifold_counts: [0; CONTACT_MANIFOLD_COUNT_BUCKETS],
168            points: [DebugPoint::default(); DEBUG_POINT_CAPACITY],
169            point_count: 0,
170            lines: [DebugLine::default(); DEBUG_LINE_CAPACITY],
171            line_count: 0,
172        }
173    }
174}
175
176/// The world struct manages all physics entities, dynamic simulation, and
177/// asynchronous queries. (b3World)
178#[derive(Debug)]
179pub struct World {
180    pub broad_phase: BroadPhase,
181    pub constraint_graph: ConstraintGraph,
182
183    /// The body id pool allocates and recycles body ids. Aligns with `bodies`.
184    pub body_id_pool: IdPool,
185
186    /// Sparse array mapping body ids to body data stored in solver sets.
187    pub bodies: Vec<Body>,
188
189    /// Provides free list for solver sets.
190    pub solver_set_id_pool: IdPool,
191
192    /// Solver sets store sims in contiguous arrays. Set 0 is static, set 1 is
193    /// disabled, set 2 is awake; the rest are sleeping islands.
194    pub solver_sets: Vec<SolverSet>,
195
196    /// Used to create stable ids for joints.
197    pub joint_id_pool: IdPool,
198
199    /// Sparse array mapping joint ids to joints in the constraint graph or
200    /// solver sets.
201    pub joints: Vec<Joint>,
202
203    /// Used to create stable ids for contacts.
204    pub contact_id_pool: IdPool,
205
206    /// Sparse array mapping contact ids to contacts.
207    pub contacts: Vec<Contact>,
208
209    /// Used to create stable ids for islands.
210    pub island_id_pool: IdPool,
211
212    /// Persistent islands.
213    pub islands: Vec<Island>,
214
215    pub shape_id_pool: IdPool,
216
217    /// Sparse array of shapes.
218    pub shapes: Vec<Shape>,
219
220    /// Content-keyed shared hull store. (world->hullDatabase)
221    pub hull_database: HullDatabase,
222
223    /// Name cache for shape and body names.
224    pub names: NameCache,
225
226    /// Dense array of sensor data.
227    pub sensors: Vec<Sensor>,
228
229    /// Per thread storage (one entry in the single-threaded port).
230    pub task_contexts: Vec<TaskContext>,
231    pub sensor_task_contexts: Vec<SensorTaskContext>,
232
233    pub body_move_events: Vec<BodyMoveEvent>,
234    pub sensor_begin_events: Vec<SensorBeginTouchEvent>,
235    pub contact_begin_events: Vec<ContactBeginTouchEvent>,
236
237    /// End events are double buffered so the user doesn't need to flush events.
238    pub sensor_end_events: [Vec<SensorEndTouchEvent>; 2],
239    pub contact_end_events: [Vec<ContactEndTouchEvent>; 2],
240    pub end_event_array_index: i32,
241
242    pub contact_hit_events: Vec<ContactHitEvent>,
243    pub joint_events: Vec<JointEvent>,
244
245    /// Used to track debug draw.
246    pub debug_body_set: BitSet,
247    pub debug_joint_set: BitSet,
248    pub debug_contact_set: BitSet,
249    pub debug_island_set: BitSet,
250
251    /// Id that is incremented every time step.
252    pub step_index: u64,
253
254    /// Identify islands for splitting.
255    pub split_island_id: i32,
256
257    pub gravity: Vec3,
258    pub hit_event_threshold: f32,
259    pub restitution_threshold: f32,
260    pub max_linear_speed: f32,
261    pub contact_speed: f32,
262    pub contact_hertz: f32,
263    pub contact_damping_ratio: f32,
264    pub contact_recycle_distance: f32,
265
266    pub friction_callback: Option<FrictionCallback>,
267    pub restitution_callback: Option<RestitutionCallback>,
268
269    pub generation: u16,
270
271    pub profile: Profile,
272    pub sat_call_count: i32,
273    pub sat_cache_hit_count: i32,
274    pub manifold_counts: [i32; CONTACT_MANIFOLD_COUNT_BUCKETS],
275
276    pub max_capacity: Capacity,
277
278    pub pre_solve_fcn: Option<PreSolveFcn>,
279    pub pre_solve_context: u64,
280
281    pub custom_filter_fcn: Option<CustomFilterFcn>,
282    pub custom_filter_context: u64,
283
284    /// Create GPU/user shapes for debug draw. (`createDebugShape`)
285    pub create_debug_shape: Option<CreateDebugShapeCallback>,
286    /// Destroy GPU/user shapes. (`destroyDebugShape`)
287    pub destroy_debug_shape: Option<DestroyDebugShapeCallback>,
288    /// Context for the debug-shape callbacks. (`userDebugShapeContext`)
289    pub user_debug_shape_context: u64,
290
291    pub worker_count: i32,
292
293    pub user_data: u64,
294
295    /// Inverse sub-step, remembered for reporting forces and torques.
296    pub inv_h: f32,
297
298    /// Inverse full-step.
299    pub inv_dt: f32,
300
301    pub world_id: u16,
302
303    pub enable_sleep: bool,
304    pub locked: bool,
305    pub enable_warm_starting: bool,
306    pub enable_continuous: bool,
307    pub enable_speculative: bool,
308    pub in_use: bool,
309
310    /// Non-null while a recording session is active. Set by
311    /// [`crate::recording::start_recording`], cleared by
312    /// [`crate::recording::stop_recording`]. (world->recording)
313    pub recording: Option<*mut crate::recording::Recording>,
314}
315
316/// Default friction mixing: `sqrt(frictionA * frictionB)`.
317/// (static b3DefaultFrictionCallback — from types.c / physics_world.c)
318pub fn default_friction_callback(
319    friction_a: f32,
320    _material_a: u64,
321    friction_b: f32,
322    _material_b: u64,
323) -> f32 {
324    (friction_a * friction_b).sqrt()
325}
326
327/// Default restitution mixing: `max(restitutionA, restitutionB)`.
328pub fn default_restitution_callback(
329    restitution_a: f32,
330    _material_a: u64,
331    restitution_b: f32,
332    _material_b: u64,
333) -> f32 {
334    crate::math_functions::max_float(restitution_a, restitution_b)
335}
336
337impl World {
338    /// Create a world. (b3CreateWorld)
339    ///
340    /// Differences from C, all documented in the module header: there is no
341    /// global world registry (the returned World is owned; `world_id` stays 0
342    /// unless the embedder assigns one), no arena stack / manifold block
343    /// allocator yet, and the serial task path is always used
344    /// (worker_count = 1 with one task context), which is the C fallback when
345    /// no task system is supplied.
346    pub fn new(def: &crate::types::WorldDef) -> World {
347        use crate::constants::{
348            contact_recycle_distance, linear_slop, mesh_rest_offset, speculative_distance,
349        };
350        use crate::core::{NULL_INDEX, SECRET_COOKIE};
351        use crate::math_functions::max_int;
352        use crate::solver_set::{AWAKE_SET, DISABLED_SET, STATIC_SET};
353
354        debug_assert!(def.internal_value == SECRET_COOKIE);
355        debug_assert!(linear_slop() <= mesh_rest_offset());
356        debug_assert!(mesh_rest_offset() < speculative_distance());
357
358        use crate::contact::initialize_contact_registers;
359        initialize_contact_registers();
360
361        let body_capacity = max_int(
362            16,
363            def.capacity.static_body_count + def.capacity.dynamic_body_count,
364        ) as usize;
365        let shape_capacity = max_int(
366            16,
367            def.capacity.static_shape_count + def.capacity.dynamic_shape_count,
368        ) as usize;
369        let contact_capacity = max_int(16, def.capacity.contact_count) as usize;
370
371        let mut solver_set_id_pool = IdPool::new();
372        let mut solver_sets: Vec<SolverSet> = Vec::with_capacity(8);
373
374        // add empty static, disabled, and awake body sets
375        // static set
376        let mut set = SolverSet {
377            set_index: solver_set_id_pool.alloc_id(),
378            ..Default::default()
379        };
380        set.body_sims
381            .reserve(max_int(16, def.capacity.static_body_count) as usize);
382        solver_sets.push(set);
383        debug_assert!(solver_sets[STATIC_SET as usize].set_index == STATIC_SET);
384
385        // disabled set
386        solver_sets.push(SolverSet {
387            set_index: solver_set_id_pool.alloc_id(),
388            ..Default::default()
389        });
390        debug_assert!(solver_sets[DISABLED_SET as usize].set_index == DISABLED_SET);
391
392        // awake set
393        let mut awake = SolverSet {
394            set_index: solver_set_id_pool.alloc_id(),
395            ..Default::default()
396        };
397        awake
398            .body_sims
399            .reserve(max_int(16, def.capacity.dynamic_body_count) as usize);
400        awake
401            .body_states
402            .reserve(max_int(16, def.capacity.dynamic_body_count) as usize);
403        awake.contact_indices.reserve(contact_capacity);
404        solver_sets.push(awake);
405        debug_assert!(solver_sets[AWAKE_SET as usize].set_index == AWAKE_SET);
406
407        World {
408            broad_phase: BroadPhase::new(&def.capacity),
409            constraint_graph: ConstraintGraph::new(16),
410            body_id_pool: IdPool::new(),
411            bodies: Vec::with_capacity(body_capacity),
412            solver_set_id_pool,
413            solver_sets,
414            joint_id_pool: IdPool::new(),
415            joints: Vec::with_capacity(16),
416            contact_id_pool: IdPool::new(),
417            contacts: Vec::with_capacity(contact_capacity),
418            island_id_pool: IdPool::new(),
419            islands: Vec::with_capacity(max_int(16, def.capacity.dynamic_body_count) as usize),
420            shape_id_pool: IdPool::new(),
421            shapes: Vec::with_capacity(shape_capacity),
422            hull_database: HullDatabase::new(),
423            names: NameCache::new(),
424            sensors: Vec::with_capacity(4),
425            // Serial fallback: one worker context. (b3CreateWorkerContexts)
426            task_contexts: vec![TaskContext::default()],
427            sensor_task_contexts: vec![SensorTaskContext::default()],
428            body_move_events: Vec::with_capacity(4),
429            sensor_begin_events: Vec::with_capacity(4),
430            contact_begin_events: Vec::with_capacity(4),
431            sensor_end_events: [Vec::with_capacity(4), Vec::with_capacity(4)],
432            contact_end_events: [Vec::with_capacity(4), Vec::with_capacity(4)],
433            end_event_array_index: 0,
434            contact_hit_events: Vec::with_capacity(4),
435            joint_events: Vec::with_capacity(4),
436            debug_body_set: BitSet::new(256),
437            debug_joint_set: BitSet::new(256),
438            debug_contact_set: BitSet::new(256),
439            debug_island_set: BitSet::new(256),
440            step_index: 0,
441            split_island_id: NULL_INDEX,
442            gravity: def.gravity,
443            hit_event_threshold: def.hit_event_threshold,
444            restitution_threshold: def.restitution_threshold,
445            max_linear_speed: def.maximum_linear_speed,
446            contact_speed: def.contact_speed,
447            contact_hertz: def.contact_hertz,
448            contact_damping_ratio: def.contact_damping_ratio,
449            contact_recycle_distance: contact_recycle_distance(),
450            friction_callback: Some(def.friction_callback.unwrap_or(default_friction_callback)),
451            restitution_callback: Some(
452                def.restitution_callback
453                    .unwrap_or(default_restitution_callback),
454            ),
455            generation: 0,
456            profile: Profile::default(),
457            sat_call_count: 0,
458            sat_cache_hit_count: 0,
459            manifold_counts: [0; CONTACT_MANIFOLD_COUNT_BUCKETS],
460            max_capacity: def.capacity,
461            pre_solve_fcn: None,
462            pre_solve_context: 0,
463            custom_filter_fcn: None,
464            custom_filter_context: 0,
465            create_debug_shape: def.create_debug_shape,
466            destroy_debug_shape: def.destroy_debug_shape,
467            user_debug_shape_context: def.user_debug_shape_context,
468            worker_count: 1,
469            user_data: def.user_data,
470            inv_h: 0.0,
471            inv_dt: 0.0,
472            world_id: 0,
473            enable_sleep: def.enable_sleep,
474            locked: false,
475            enable_warm_starting: true,
476            enable_continuous: def.enable_continuous,
477            enable_speculative: true,
478            in_use: true,
479            recording: None,
480        }
481    }
482
483    /// (b3World_EnableContinuous)
484    pub fn enable_continuous(&mut self, flag: bool) {
485        debug_assert!(!self.locked);
486        if self.locked {
487            return;
488        }
489        self.enable_continuous = flag;
490    }
491
492    /// (b3World_IsContinuousEnabled)
493    pub fn is_continuous_enabled(&self) -> bool {
494        self.enable_continuous
495    }
496
497    /// Sensor begin/end events from the previous step. End events come from
498    /// the previous double-buffer slot. (b3World_GetSensorEvents)
499    pub fn get_sensor_events(&self) -> crate::events::SensorEvents<'_> {
500        let end_event_array_index = 1 - self.end_event_array_index;
501        crate::events::SensorEvents {
502            begin_events: &self.sensor_begin_events,
503            end_events: &self.sensor_end_events[end_event_array_index as usize],
504        }
505    }
506}