Skip to main content

Determinism

By default, Rapier is locally deterministic, meaning that running the exact same simulation (with the same initial conditions) twice with the same machine, using the same build of the Rapier library, will result in the exact same simulation results. However, doing this on two different computers may result in completely different results.

note

Two simulations run with the same initial conditions if their worlds are configured with the same values (gravity, integration parameters, etc.), rigid-bodies/colliders/joints are constructed the same way, and they are inserted into and removed from the world in the exact same order. Note that this includes the timestep length: the simulation must be advanced by the same sequence of timesteps on every machine, which excludes a timestep length depending on the frame rate of the application.

It is possible to make Rapier cross-platform deterministic, meaning that running a simulation with two different computers (including different OS and/or different processors) will result in the exact same results. In order to achieve this, both computers must start the simulation with the same initial conditions as discussed above, and the following additional conditions must be met:

  • The library is built with the enhanced-determinism feature, i.e., with -DRAPIER_FEATURES=enhanced-determinism. Note that it cannot be combined with -DRAPIER_SIMD_LANES=8, which changes the SIMD lane width and is therefore its own determinism domain. The parallelism (-DRAPIER_ENABLE_PARALLEL=ON), on the other hand, can be combined with it: the results of the parallel solver are identical to the results of the sequential one, and don't depend on the number of threads (nor, therefore, on the number of cores of the machine running the simulation).
  • The target platforms must strictly comply to the IEEE 754-2008 floating-points standard. This ensures that floating-point computations behave the same on all platforms. This include most modern mainstream processors.
  • Your own code computing the values given to Rapier must be compiled without any optimization changing the results of floating-point operations, e.g., without -ffast-math, and without the contraction of multiplications and additions into fused multiply-add operations (-ffp-contract=off with GCC and Clang).
  • If some of the values used to initialize Rapier structures are computed using floating points functions (sin, cos, tan, etc.) other than addition/subtraction/multiplication/division/square root, then you need to make sure the functions being used are the ones provided by Rapier, e.g., r3Sin and r3Cos, instead of the functions of the C standard library, e.g., sinf and cosf, which give different results on different platforms. Note that the helper functions of rapier_math.h (e.g. r3RotationFromAxisAngle) already rely on r3Sin and r3Cos:
/* WRONG version:
* The following will not work cross-platform-deterministically because the values
* given to the collider translation are computed by the math library of the platform. */
R3ColliderDesc collider = r3BallColliderDesc(0.5);
collider.position.translation = r3Vector(sqrtf(1.0f), sinf(2.0f), cosf(3.0f));
/* CORRECT version:
* The following will work cross-platform-deterministically because we use the
* math functions of Rapier (the square root is exactly rounded on every platform). */
R3ColliderDesc collider = r3BallColliderDesc(0.5);
collider.position.translation = r3Vector(sqrtf(1.0f), r3Sin(2.0), r3Cos(3.0));

Because the library is loaded dynamically, it is recommended to check that the library actually loaded by your application is built with the enhanced-determinism feature, which is indicated by the enhanced_determinism field of r3BuildFeatures():

/* Check that the loaded library is built with the enhanced-determinism feature. */
if (!r3BuildFeatures().enhanced_determinism) {
fprintf(stderr, "This Rapier library isn't cross-platform deterministic.\n");
}

A simple way of checking that two simulations are in the exact same state is to compare their snapshots, e.g., with a hash of the bytes serialized by r3SerializeWorld:

/* Two worlds are in the exact same state if their snapshots are identical. */
R3Bytes *snapshot = r3SerializeWorld(world);
R3ByteView bytes = r3Bytes_Data(snapshot);
uint64_t hash = 14695981039346656037ull; /* FNV-1a, or any other hash function. */
for (size_t i = 0; i < bytes.count; i++) {
hash = (hash ^ bytes.data[i]) * 1099511628211ull;
}
printf("World hash: %016llx\n", (unsigned long long)hash);
r3FreeBytes(snapshot);