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 version of Rapier, and the same version of the Rust compiler, will result in the exact same simulation results. However, doing this on two different computers may result in completely different results.
Two simulations run with the same initial conditions if all the simulation structures are initialized with the same values, rigid-bodies/colliders/joints are constructed the same way, and they are added/removed to sets (rigid-body sets, etc.) in the exact same order.
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
enhanced-determinismfeature of Rapier is enabled. Note that theenhanced-determinismfeature cannot be enabled at the same time as thesimd8feature, which changes the SIMD lane width and is therefore its own determinism domain. Theparallelfeature, 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 of the rayon pool (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 as well as WASM targets.
- If some of the values used to initialized Rapier structures are computed using floating points functions (sin, cos, tan, etc.) other
than addition/subtraction/multiplication/division, then you need to make sure the functions being used originate from
the
ComplexFieldorRealFieldtraits fromnalgebra(re-exported by Rapier asna). For example, doComplexField::sin(0.4)(whereComplexFieldis imported byuse rapier3d::na::ComplexField ) instead ofuse bevy_rapier3d::na::ComplexField0.4.sin():
// WRONG version:
// The following will not work cross-platform-deterministically because the values
// given to `ColliderBuilder::translation` won't be cross-platform deterministic.
let collider = ColliderBuilder::ball(0.5)
.translation(Vector::new(1.0f32.sqrt(), 2.0f32.sin(), 3.0f32.cos()))
.build();
// CORRECT version:
// The following will work cross-platform-deterministically because we use the
// functions from nalgebra.
let collider = ColliderBuilder::ball(0.5)
.translation(Vector::new(
ComplexField::sqrt(1.0),
ComplexField::sin(2.0),
ComplexField::cos(3.0),
))
.build();
// WRONG version:
// The following will not work cross-platform-deterministically because the values
// given to `Transform::from_xyz` won't be cross-platform deterministic.
commands.spawn((
Transform::from_xyz(1.0f32.sqrt(), 2.0f32.sin(), 3.0f32.cos()),
Collider::ball(0.5),
));
// CORRECT version:
// The following will work cross-platform-deterministically because we use the
// functions from nalgebra.
commands.spawn((
Transform::from_xyz(
ComplexField::sqrt(1.0),
ComplexField::sin(2.0),
ComplexField::cos(3.0),
),
Collider::ball(0.5),
));
In Bevy, a few more conditions must be met:
- The simulation must be advanced by the same timesteps on every machine, which excludes the default
TimestepMode::Variabletimestep mode since it depends on the frame rate. UseTimestepMode::Fixedinstead, ideally with the physics running in theFixedUpdateschedule so the number of timesteps doesn't depend on the frame rate either:
App::new()
.add_plugins(DefaultPlugins)
// Run `FixedUpdate` 60 times per second, and advance the simulation by exactly 1/60
// seconds at each of these updates, whatever the frame rate.
.insert_resource(Time::<Fixed>::from_hz(60.0))
.insert_resource(TimestepMode::Fixed {
dt: 1.0 / 60.0,
substeps: 1,
})
.add_plugins(RapierPhysicsPlugin::<NoUserData>::default().in_fixed_schedule())
- The entities with physics components must be spawned (and modified, and despawned) in the same order, with the same components, since this is what determines the order in which the plugin inserts them into the sets.
- The math functions of Bevy (e.g. the trigonometric functions used by
Quat::from_rotation_y) are not cross-platform deterministic either unless thelibmfeature of Bevy is enabled.
The WASM/Typescript/JavaScript version of Rapier is fully cross-platform deterministic. This means that running the same simulation
(with the same initial conditions) using the same version of Rapier, on two different machines (even with different
browsers, operating systems, and processors, will give the exact same results. In particular, creating a snapshot of the
World with world.takeSnapshot() and taking a MD5 hash of the resulting byte array will return the exact same hash
on different machines (assuming the snapshot itself is taken after the same number of timesteps).
Two simulations run with the same initial conditions if all the simulation parameters are initialized with the same values, rigid-bodies/colliders/joints are constructed the same way, and they are added/removed in the exact same order.
Keep in mind that all the values used to initialize the physics simulation must result from cross-platform
deterministic operations to in order to preserve the determinism of the physics engine itself. Otherwise, you won't
get the exact same initial conditions on all the platforms. In particular, transcendental functions like
Math.sin, Math.cos are not cross-platform determinism and may give different results on different platforms.
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.
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-determinismfeature, 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=offwith 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.,
r3Sinandr3Cos, instead of the functions of the C standard library, e.g.,sinfandcosf, which give different results on different platforms. Note that the helper functions ofrapier_math.h(e.g.r3RotationFromAxisAngle) already rely onr3Sinandr3Cos:
/* 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);
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 bindings, will result in the exact same simulation results. However, doing this on two different computers may result in completely different results.
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 bindings are built with the
determinismfeature, e.g., withmaturin develop --release -F determinism -m bindings/python/rapier-py-3d/Cargo.toml(see building from source). The parallelism, which is always enabled, 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.
- If some of the values used to initialize Rapier structures are computed using floating points functions (sin, cos,
tan, exp, etc.) other than addition/subtraction/multiplication/division/square root, then you need to make sure the
functions being used are the ones provided by the
rapier3d.mathmodule, e.g.,rapier3d.math.sin, instead of the functions of themathmodule of Python, which rely on the C standard library of the platform. This applies to NumPy as well: its vectorized functions (e.g.numpy.sin) may even give different results on two processors of the same platform, depending on the SIMD instructions they support:
# WRONG version:
# The following will not work cross-platform-deterministically because the functions of the
# `math` module (and of NumPy) give different results on different platforms.
collider = rp.Collider.ball(0.5).translation((math.exp(1.0), math.sin(2.0), math.cos(3.0))).build()
import rapier3d.math as rpm
# CORRECT version:
# The following will work cross-platform-deterministically because we use the functions
# of Rapier.
collider = rp.Collider.ball(0.5).translation((rpm.exp(1.0), rpm.sin(2.0), rpm.cos(3.0))).build()
It is recommended to check that the bindings actually loaded by your application are built with the determinism
feature, which is indicated by the enhanced_determinism property of rapier3d.build_features():
# Make sure the loaded bindings are built with the `determinism` feature.
if not rp.build_features().enhanced_determinism:
print("Warning: the rapier3d bindings aren't cross-platform deterministic.")
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 given by PhysicsWorld.snapshot:
# Two simulations are in the exact same state if their snapshots are identical.
digest = hashlib.sha256(world.snapshot()).hexdigest()
print("State after 100 timesteps:", digest)