Skip to main content

box3d_rust/
determinism.rs

1//! Falling-ragdoll determinism scene helpers from `shared/determinism.c`.
2//!
3//! Builds a grid of ragdolls over mesh grounds and hashes settled transforms.
4//! The final EXPECTED_HASH gate in `test_determinism.c` stays deferred until
5//! task-6 lands mesh narrow-phase (bodies currently fall through meshes).
6//!
7//! The content hash itself (`b3Hash` / [`crate::core::hash`]) already lives in
8//! [`crate::core`]; this module adds the world-transform byte layout used by
9//! the scene.
10//!
11//! SPDX-FileCopyrightText: 2022 Erin Catto
12//! SPDX-License-Identifier: MIT
13
14#![allow(clippy::unnecessary_cast)] // Pos is f64 under double-precision
15
16use crate::body::{body_get_transform, create_body};
17use crate::core::{hash, HASH_INIT};
18use crate::human::{create_human, Human, BONE_COUNT};
19use crate::math_functions::{Pos, WorldTransform, VEC3_ONE};
20use crate::mesh::{create_grid_mesh, create_torus_mesh, destroy_mesh, MeshData};
21use crate::shape::create_mesh_shape;
22use crate::types::{default_body_def, default_shape_def};
23use crate::world::{world_get_awake_body_count, world_get_body_events, World};
24
25/// Humans per ground cell. (RAGDOLL_GROUP_SIZE)
26pub const RAGDOLL_GROUP_SIZE: usize = 2;
27
28/// Grid dimension (NxN cells). (RAGDOLL_GRID_COUNT)
29pub const RAGDOLL_GRID_COUNT: usize = 2;
30
31const GRID_SIZE: f32 = 15.0;
32
33/// One cell of ragdolls. (RagdollGroup)
34#[derive(Debug, Clone, Default)]
35pub struct RagdollGroup {
36    pub humans: [Human; RAGDOLL_GROUP_SIZE],
37}
38
39/// Falling-ragdoll scene state. (FallingRagdollData)
40#[derive(Debug)]
41pub struct FallingRagdollData {
42    pub groups: [RagdollGroup; RAGDOLL_GRID_COUNT * RAGDOLL_GRID_COUNT],
43    pub grid_mesh: Option<MeshData>,
44    pub torus_mesh: Option<MeshData>,
45    pub column_count: i32,
46    pub column_index: i32,
47    pub step_count: i32,
48    pub sleep_step: i32,
49    pub hash: u32,
50}
51
52impl Default for FallingRagdollData {
53    fn default() -> Self {
54        FallingRagdollData {
55            groups: std::array::from_fn(|_| RagdollGroup::default()),
56            grid_mesh: None,
57            torus_mesh: None,
58            column_count: 0,
59            column_index: 0,
60            step_count: 0,
61            sleep_step: 0,
62            hash: 0,
63        }
64    }
65}
66
67/// Hash a world transform's raw little-endian field bytes in C declaration
68/// order, matching `b3Hash(hash, (uint8_t*)&xf, sizeof(b3WorldTransform))` on
69/// little-endian hosts. (determinism.c UpdateFallingRagdolls)
70pub fn hash_world_transform(h: u32, xf: &WorldTransform) -> u32 {
71    let mut bytes = Vec::with_capacity(40);
72    bytes.extend_from_slice(&xf.p.x.to_le_bytes());
73    bytes.extend_from_slice(&xf.p.y.to_le_bytes());
74    bytes.extend_from_slice(&xf.p.z.to_le_bytes());
75    bytes.extend_from_slice(&xf.q.v.x.to_le_bytes());
76    bytes.extend_from_slice(&xf.q.v.y.to_le_bytes());
77    bytes.extend_from_slice(&xf.q.v.z.to_le_bytes());
78    bytes.extend_from_slice(&xf.q.s.to_le_bytes());
79    hash(h, &bytes)
80}
81
82fn create_group(
83    data: &mut FallingRagdollData,
84    world: &mut World,
85    row_index: usize,
86    column_index: usize,
87) {
88    debug_assert!(row_index < RAGDOLL_GRID_COUNT && column_index < RAGDOLL_GRID_COUNT);
89
90    let group_index = row_index * RAGDOLL_GRID_COUNT + column_index;
91
92    let span = RAGDOLL_GRID_COUNT as f32 * GRID_SIZE;
93    let group_distance = 1.0 * span / RAGDOLL_GRID_COUNT as f32;
94
95    let mut position = Pos {
96        x: (-0.5 * span + group_distance * (column_index as f32 + 0.5)) as _,
97        y: 15.0 as _,
98        z: (-0.5 * span + group_distance * (row_index as f32 + 0.5)) as _,
99    };
100
101    let friction_torque = 5.0;
102    let hertz = 1.0;
103    let damping_ratio = 0.7;
104    let colorize = false;
105
106    for i in 0..RAGDOLL_GROUP_SIZE {
107        let human = &mut data.groups[group_index].humans[i];
108        create_human(
109            human,
110            world,
111            position,
112            friction_torque,
113            hertz,
114            damping_ratio,
115            group_index as i32,
116            0,
117            colorize,
118        );
119        position.x = (position.x as f32 + 0.75) as _;
120    }
121}
122
123/// Build mesh grounds and spawn the ragdoll grid. (CreateFallingRagdolls)
124pub fn create_falling_ragdolls(world: &mut World) -> FallingRagdollData {
125    let mut data = FallingRagdollData::default();
126
127    let half_mesh_grid_rows = 4;
128    let mesh_grid_cell_width = GRID_SIZE / (2.0 * half_mesh_grid_rows as f32);
129    data.grid_mesh = create_grid_mesh(
130        2 * half_mesh_grid_rows,
131        2 * half_mesh_grid_rows,
132        mesh_grid_cell_width,
133        0,
134        true,
135    );
136    data.torus_mesh = create_torus_mesh(16, 16, 0.25 * GRID_SIZE, 1.0);
137
138    let span = GRID_SIZE * RAGDOLL_GRID_COUNT as f32;
139    let mut body_def = default_body_def();
140    let shape_def = default_shape_def();
141
142    // Clone mesh data for shape attach; FallingRagdollData keeps the originals
143    // for DestroyFallingRagdolls (C keeps borrowed pointers).
144    let grid_mesh = data.grid_mesh.as_ref().expect("grid mesh").clone();
145    let torus_mesh = data.torus_mesh.as_ref().expect("torus mesh").clone();
146
147    body_def.position.x = (-0.5 * span + 0.5 * GRID_SIZE) as _;
148    for i in 0..RAGDOLL_GRID_COUNT {
149        body_def.position.z = (-0.5 * span + 0.5 * GRID_SIZE) as _;
150        for j in 0..RAGDOLL_GRID_COUNT {
151            let body = create_body(world, &body_def);
152            create_mesh_shape(world, body, &shape_def, &grid_mesh, VEC3_ONE);
153            create_mesh_shape(world, body, &shape_def, &torus_mesh, VEC3_ONE);
154
155            create_group(&mut data, world, i, j);
156
157            body_def.position.z = (body_def.position.z as f32 + GRID_SIZE) as _;
158        }
159
160        body_def.position.x = (body_def.position.x as f32 + GRID_SIZE) as _;
161    }
162
163    data
164}
165
166/// Advance sleep/hash tracking after a world step. Returns true once settled.
167/// (UpdateFallingRagdolls)
168pub fn update_falling_ragdolls(world: &World, data: &mut FallingRagdollData) -> bool {
169    if data.hash == 0 {
170        let body_events = world_get_body_events(world);
171
172        if body_events.is_empty() {
173            let awake_count = world_get_awake_body_count(world);
174            debug_assert!(awake_count == 0);
175
176            data.hash = HASH_INIT;
177            for i in 0..RAGDOLL_GRID_COUNT {
178                for j in 0..RAGDOLL_GRID_COUNT {
179                    for k in 0..RAGDOLL_GROUP_SIZE {
180                        let group_index = i * RAGDOLL_GRID_COUNT + j;
181                        let human = &data.groups[group_index].humans[k];
182
183                        for b in 0..BONE_COUNT {
184                            let body_id = human.bones[b].body_id;
185                            let xf = body_get_transform(world, body_id);
186                            data.hash = hash_world_transform(data.hash, &xf);
187                        }
188                    }
189                }
190            }
191
192            data.sleep_step = data.step_count;
193        }
194    }
195
196    data.step_count += 1;
197
198    data.hash != 0
199}
200
201/// Release owned mesh data. (DestroyFallingRagdolls)
202pub fn destroy_falling_ragdolls(data: &mut FallingRagdollData) {
203    if let Some(mesh) = data.grid_mesh.take() {
204        destroy_mesh(mesh);
205    }
206    if let Some(mesh) = data.torus_mesh.take() {
207        destroy_mesh(mesh);
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214    use crate::types::default_world_def;
215
216    #[test]
217    fn create_and_destroy_falling_ragdolls() {
218        let mut world = World::new(&default_world_def());
219        let mut data = create_falling_ragdolls(&mut world);
220        assert!(data.grid_mesh.is_some());
221        assert!(data.torus_mesh.is_some());
222        assert!(!data.groups[0].humans[0].bones[0].body_id.is_null());
223        destroy_falling_ragdolls(&mut data);
224        assert!(data.grid_mesh.is_none());
225    }
226
227    #[test]
228    fn hash_world_transform_is_stable() {
229        let xf = WorldTransform {
230            p: Pos {
231                x: 1.0 as _,
232                y: 2.0 as _,
233                z: 3.0 as _,
234            },
235            q: crate::math_functions::QUAT_IDENTITY,
236        };
237        let h1 = hash_world_transform(HASH_INIT, &xf);
238        let h2 = hash_world_transform(HASH_INIT, &xf);
239        assert_eq!(h1, h2);
240        assert_ne!(h1, HASH_INIT);
241    }
242}