Skip to main content

box3d_rust/
core.rs

1// Port of the surviving behavior from box3d-cpp-reference/src/core.c, core.h,
2// include/box3d/base.h, and src/ctz.h needed by math_functions / constants /
3// bitset / table.
4//
5// core.c is largely an allocator / threading / timing shim. Rust covers that
6// natively, so those pieces are not ported. What remains here is the runtime
7// length-unit scale, the precision query, and the ctz.h bit helpers.
8//
9// SPDX-FileCopyrightText: 2025 Erin Catto
10// SPDX-License-Identifier: MIT
11
12use core::sync::atomic::{AtomicU32, Ordering};
13
14/// Used to indicate an unset or invalid index value. (base.h: B3_NULL_INDEX)
15pub const NULL_INDEX: i32 = -1;
16
17/// Use to validate definitions. (core.h: B3_SECRET_COOKIE)
18pub const SECRET_COOKIE: i32 = 1152023;
19
20// The length-unit scale is a single global that the user sets once at startup.
21// C stores it as a plain `static float`; we store the bit pattern in an atomic
22// so the global is sound under Rust's threading rules. The observable value is
23// identical. 0x3F80_0000 is the bit pattern of 1.0f32.
24static LENGTH_UNITS_PER_METER_BITS: AtomicU32 = AtomicU32::new(0x3F80_0000);
25
26/// Box3D bases all length units on meters. Set this to use different units for
27/// all length values passed to and returned from Box3D. Must be set at
28/// application startup, before any other Box3D calls.
29pub fn set_length_units_per_meter(length_units: f32) {
30    debug_assert!(crate::math_functions::is_valid_float(length_units) && length_units > 0.0);
31    LENGTH_UNITS_PER_METER_BITS.store(length_units.to_bits(), Ordering::Relaxed);
32}
33
34/// Get the current length units per meter.
35pub fn get_length_units_per_meter() -> f32 {
36    f32::from_bits(LENGTH_UNITS_PER_METER_BITS.load(Ordering::Relaxed))
37}
38
39// The CCD stall threshold is a single global in C (`static float
40// b3_stallThreshold = FLT_MAX`) used only to log continuous-collision steps that
41// exceed it (solver.c / shape.c). Stored as an atomic bit pattern for the same
42// soundness reason as the length unit above. 0x7F7F_FFFF is FLT_MAX.
43static STALL_THRESHOLD_BITS: AtomicU32 = AtomicU32::new(0x7F7F_FFFF);
44
45/// Set the CCD stall threshold, in seconds. (core.c: b3SetStallThreshold)
46pub fn set_stall_threshold(seconds: f32) {
47    debug_assert!(crate::math_functions::is_valid_float(seconds) && seconds > 0.0);
48    STALL_THRESHOLD_BITS.store(seconds.to_bits(), Ordering::Relaxed);
49}
50
51/// Get the CCD stall threshold, in seconds. (core.c: b3GetStallThreshold)
52pub fn get_stall_threshold() -> f32 {
53    f32::from_bits(STALL_THRESHOLD_BITS.load(Ordering::Relaxed))
54}
55
56/// @return true if the library was built with the `double-precision` feature
57/// (large world mode), mirroring `BOX3D_DOUBLE_PRECISION`.
58pub fn is_double_precision() -> bool {
59    cfg!(feature = "double-precision")
60}
61
62// ---------------------------------------------------------------------------
63// Bit helpers (ctz.h). The C versions are thin wrappers over compiler
64// intrinsics (__builtin_ctz / _BitScanForward / __popcnt). The count-leading
65// and count-trailing intrinsics are undefined for a zero argument in C; every
66// caller guarantees a nonzero argument, and Rust's intrinsics are well defined
67// (returning the bit width) even for zero, so the ported callers behave
68// identically.
69// ---------------------------------------------------------------------------
70
71/// Count trailing zeros of a 32-bit block. (ctz.h: b3CTZ32)
72pub fn ctz32(block: u32) -> u32 {
73    block.trailing_zeros()
74}
75
76/// Count leading zeros of a 32-bit value. (ctz.h: b3CLZ32)
77pub fn clz32(value: u32) -> u32 {
78    value.leading_zeros()
79}
80
81/// Count trailing zeros of a 64-bit block. (ctz.h: b3CTZ64)
82pub fn ctz64(block: u64) -> u32 {
83    block.trailing_zeros()
84}
85
86/// Population count of a 64-bit block. (ctz.h: b3PopCount64)
87pub fn pop_count64(block: u64) -> i32 {
88    block.count_ones() as i32
89}
90
91/// (ctz.h: b3IsPowerOf2)
92pub fn is_power_of2(x: i32) -> bool {
93    (x & (x - 1)) == 0
94}
95
96/// (ctz.h: b3BoundingPowerOf2)
97pub fn bounding_power_of2(x: i32) -> i32 {
98    if x <= 1 {
99        return 1;
100    }
101
102    32 - clz32((x as u32) - 1) as i32
103}
104
105/// (ctz.h: b3RoundUpPowerOf2)
106pub fn round_up_power_of2(x: i32) -> i32 {
107    if x <= 1 {
108        return 1;
109    }
110
111    1 << (32 - clz32((x as u32) - 1))
112}
113
114/// Position of the most significant bit = floor(log2(x)). (ctz.h: b3LowerPowerOf2Exponent)
115pub fn lower_power_of_2_exponent(x: i32) -> i32 {
116    debug_assert!(x > 0);
117    let clz = clz32(x as u32) as i32;
118
119    // Position of most significant bit = floor(log2(M))
120    31 - clz
121}
122
123// ---------------------------------------------------------------------------
124// Content hash (base.h / timer.c). Word-oriented djb2 over 8-byte little-endian
125// chunks — not the byte-wise recurrence used by Box2D.
126// ---------------------------------------------------------------------------
127
128/// Initial value for [`hash`]. (base.h: B3_HASH_INIT)
129pub const HASH_INIT: u32 = 5381;
130
131/// Hash `data` into `hash` (djb2-style, 8-byte little-endian words then bytes).
132/// (timer.c: b3Hash)
133pub fn hash(hash: u32, data: &[u8]) -> u32 {
134    let mut result = hash;
135    let mut i = 0;
136    let count = data.len();
137
138    while i + 8 <= count {
139        // Little-endian load; matches memcpy of uint64_t on LE hosts (and the
140        // explicit byte-swap path on BE in the C source).
141        let word = u64::from_le_bytes(data[i..i + 8].try_into().unwrap());
142        result = result
143            .wrapping_shl(5)
144            .wrapping_add(result)
145            .wrapping_add(word as u32);
146        result = result
147            .wrapping_shl(5)
148            .wrapping_add(result)
149            .wrapping_add((word >> 32) as u32);
150        i += 8;
151    }
152
153    while i < count {
154        result = result
155            .wrapping_shl(5)
156            .wrapping_add(result)
157            .wrapping_add(data[i] as u32);
158        i += 1;
159    }
160
161    result
162}
163
164/// Geometry content hashes reserve zero to mean unhashed. (core.h: b3NonZeroHash)
165pub fn non_zero_hash(hash: u32) -> u32 {
166    if hash != 0 {
167        hash
168    } else {
169        1
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn bit_helpers() {
179        assert_eq!(ctz32(0b1000), 3);
180        assert_eq!(clz32(1), 31);
181        assert_eq!(clz32(9), 31 - 3);
182        assert_eq!(ctz64(1u64 << 40), 40);
183        assert_eq!(pop_count64(0xFFFF_FFFF_FFFF_FFFF), 64);
184        assert!(is_power_of2(8));
185        assert!(!is_power_of2(6));
186        assert_eq!(round_up_power_of2(5), 8);
187        assert_eq!(round_up_power_of2(1), 1);
188        assert_eq!(bounding_power_of2(5), 3);
189        assert_eq!(lower_power_of_2_exponent(9), 3);
190    }
191
192    #[test]
193    fn stall_threshold_round_trip() {
194        // Default mirrors C's `b3_stallThreshold = FLT_MAX`.
195        assert_eq!(get_stall_threshold(), f32::MAX);
196
197        // Matches sample_continuous.cpp Stall: b3SetStallThreshold(0.001f).
198        set_stall_threshold(0.001);
199        assert_eq!(get_stall_threshold(), 0.001);
200
201        // Restore the default so other tests observe C's initial value.
202        set_stall_threshold(f32::MAX);
203        assert_eq!(get_stall_threshold(), f32::MAX);
204    }
205}