mod framework;
use std::path::Path;
use std::process::Command;
use framework::*;
use oxijolt_sys::*;
const CHILD_ENV: &str = "OXIJOLT_SYS_DIGEST_CHILD";
const TICKS: usize = 120;
const COLUMNS: usize = 5;
const LAYERS: usize = 4;
const BODY_RECORD_SIZE: usize = 4 + 3 * size_of::<Real>() + 4 * 4 + 3 * 4 + 3 * 4 + 1;
const BODY_COUNT: usize = 1 + COLUMNS * LAYERS;
struct BoxSpec {
half_extent: JPH_Vec3,
position: JPH_RVec3,
motion_type: JPH_MotionType,
layer: JPH_ObjectLayer,
activation: JPH_Activation,
}
fn scene() -> Vec<BoxSpec> {
let mut specs = vec![BoxSpec {
half_extent: vec3(100.0, 1.0, 100.0),
position: rvec3(0.0, -1.0, 0.0),
motion_type: JPH_MotionType_Static,
layer: OL_NON_MOVING,
activation: JPH_Activation_DontActivate,
}];
for column in 0..COLUMNS {
for layer in 0..LAYERS {
let x = 1.5 * column as Real + 0.15 * layer as Real;
let y = 0.5 + 1.05 * layer as Real;
specs.push(BoxSpec {
half_extent: vec3(0.5, 0.5, 0.5),
position: rvec3(x, y, 0.0),
motion_type: JPH_MotionType_Dynamic,
layer: OL_MOVING,
activation: JPH_Activation_Activate,
});
}
}
specs
}
fn run_scene(worker_threads: i32, reversed: bool) -> Vec<u8> {
let world = TestWorld::new(worker_threads);
let bodies = world.body_interface();
let mut specs = scene();
if reversed {
specs.reverse();
}
let mut ids: Vec<JPH_BodyID> = specs
.iter()
.map(|spec| {
create_box(
bodies,
spec.half_extent,
spec.position,
spec.motion_type,
spec.layer,
spec.activation,
)
})
.collect();
if reversed {
ids.reverse();
}
let mut digest = Vec::with_capacity(TICKS * BODY_COUNT * BODY_RECORD_SIZE);
for _ in 0..TICKS {
world.step(1.0 / 60.0);
for &id in &ids {
record_body(bodies, id, &mut digest);
}
}
for &id in &ids {
unsafe { JPH_BodyInterface_RemoveAndDestroyBody(bodies, id) };
}
digest
}
fn record_body(bodies: *mut JPH_BodyInterface, id: JPH_BodyID, digest: &mut Vec<u8>) {
let mut position = rvec3(0.0, 0.0, 0.0);
let mut rotation = quat_identity();
let mut linear = vec3(0.0, 0.0, 0.0);
let mut angular = vec3(0.0, 0.0, 0.0);
let active = unsafe {
JPH_BodyInterface_GetPosition(bodies, id, &mut position);
JPH_BodyInterface_GetRotation(bodies, id, &mut rotation);
JPH_BodyInterface_GetLinearVelocity(bodies, id, &mut linear);
JPH_BodyInterface_GetAngularVelocity(bodies, id, &mut angular);
JPH_BodyInterface_IsActive(bodies, id)
};
digest.extend_from_slice(&id.to_le_bytes());
for value in [position.x, position.y, position.z] {
digest.extend_from_slice(&value.to_bits().to_le_bytes());
}
for value in [
rotation.x, rotation.y, rotation.z, rotation.w, linear.x, linear.y, linear.z, angular.x,
angular.y, angular.z,
] {
digest.extend_from_slice(&value.to_bits().to_le_bytes());
}
digest.push(u8::from(active));
}
fn first_tick_ids(digest: &[u8]) -> Vec<JPH_BodyID> {
digest[..BODY_COUNT * BODY_RECORD_SIZE]
.as_chunks::<BODY_RECORD_SIZE>()
.0
.iter()
.map(|record| JPH_BodyID::from_le_bytes(record[..4].try_into().unwrap()))
.collect()
}
fn nth_body_id(n: usize) -> JPH_BodyID {
(1 << 23) | n as JPH_BodyID
}
#[test]
#[ignore = "child process of digest_is_identical_across_thread_counts"]
fn determinism_child() {
let Ok(request) = std::env::var(CHILD_ENV) else {
return;
};
let mut parts = request.splitn(3, ',');
let threads: i32 = parts.next().unwrap().parse().unwrap();
let reversed = match parts.next().unwrap() {
"forward" => false,
"reversed" => true,
order => panic!("unknown order {order}"),
};
let output = parts.next().unwrap();
std::fs::write(output, run_scene(threads, reversed)).unwrap();
}
fn digest_in_child(threads: i32, order: &str, output: &Path) -> Vec<u8> {
let status = Command::new(std::env::current_exe().unwrap())
.args([
"determinism_child",
"--exact",
"--ignored",
"--test-threads=1",
])
.env(CHILD_ENV, format!("{threads},{order},{}", output.display()))
.status()
.unwrap();
assert!(status.success(), "child {threads},{order} failed: {status}");
std::fs::read(output).unwrap()
}
#[test]
fn digest_is_identical_across_thread_counts() {
let dir = std::env::temp_dir().join(format!("oxijolt-sys-digest-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let one_thread = digest_in_child(1, "forward", &dir.join("a"));
let four_threads = digest_in_child(4, "forward", &dir.join("b"));
let reversed = digest_in_child(1, "reversed", &dir.join("c"));
std::fs::remove_dir_all(&dir).unwrap();
assert_eq!(one_thread.len(), TICKS * BODY_COUNT * BODY_RECORD_SIZE);
if let Some(offset) = one_thread
.iter()
.zip(&four_threads)
.position(|(a, b)| a != b)
{
let tick = offset / (BODY_COUNT * BODY_RECORD_SIZE);
panic!("1-thread and 4-thread digests differ at byte {offset} (tick {tick})");
}
assert_eq!(one_thread.len(), four_threads.len());
let created_forward: Vec<JPH_BodyID> = (0..BODY_COUNT).map(nth_body_id).collect();
let created_reversed: Vec<JPH_BodyID> = (0..BODY_COUNT).rev().map(nth_body_id).collect();
assert_eq!(first_tick_ids(&one_thread), created_forward);
assert_eq!(first_tick_ids(&reversed), created_reversed);
}