alice-physics 3.0.1

Deterministic physics engine: 128-bit fixed-point rigid bodies with bit-exact, replayable results, typed observation and rollback for search and planning, plus FEM, CFD and 3D-print analysis modules
Documentation

ALICE-Physics

A deterministic physics engine for Rust. The rigid-body core runs on 128-bit fixed-point numbers, so the same inputs produce the same bits on every CPU, compiler and operating system.

English | 日本語

crates.io docs.rs MSRV CI License

ALICE-Physics is built for cases where bit-exact replay is a requirement: rollback and lockstep netcode, server-side replay verification, search and planning loops that need to branch and rewind a simulation, and reproducible engineering or research batches. Next to the rigid-body solver it includes a set of engineering modules (FEM, CFD, heat transfer, composites, 3D-print checks), all held to the same cross-platform determinism.

It is not a drop-in replacement for a floating-point game engine. Fixed-point arithmetic costs time, and scenes with thousands of interacting bodies at 60 fps are outside its design point.

Contents

Installation

cargo add alice-physics

Without the standard library (requires alloc):

cargo add alice-physics --no-default-features

Example

A body falling under gravity for one second. The same code is the crate-level doctest in src/lib.rs, so it is compiled and run by cargo test.

use alice_physics::{PhysicsWorld, PhysicsConfig, RigidBody, Fix128, Vec3Fix};

// Create physics world
let config = PhysicsConfig::default();
let mut world = PhysicsWorld::new(config);

// Add a dynamic body
let body = RigidBody::new_dynamic(
    Vec3Fix::from_int(0, 10, 0),  // position
    Fix128::ONE,                   // mass = 1
);
let body_id = world.add_body(body);

// Step simulation (60 frames at 1/60 second)
let dt = Fix128::from_ratio(1, 60);
for _ in 0..60 {
    world.step(dt);
}

// Body should have fallen under gravity
let pos = world.bodies[body_id].position;
assert!(pos.y < Fix128::from_int(10), "Body fell under gravity");

Runnable programs live in examples/. Good places to start:

Example Shows
basic_physics world setup, bodies, stepping
world_api_tour every PhysicsWorld setter, query and event drain
rollback_netcode snapshot, rollback and checksum verification
world_snapshot_branching whole-world snapshot, branching from it, restoring a world that holds an SDF collider
sdf_contacts the contacts a step resolved against SDF colliders (point, normal, depth, approach speed), read after the step and by a participant inside it
joint_limits_and_breaking joint types, limits, motors, breakable joints
cloth_simulation XPBD cloth
cfd_smoke_plume the integrated CFD solver
print_full_safety the 3D-print safety pipeline
cargo run --release --example rollback_netcode

Determinism

Every module is bit-exact across platforms, for one of two reasons:

Tier Modules Why the bits agree
Fix128 core rigid-body solver, joints, distance and contact constraints, BVH broad-phase, GJK / EPA, CCD, sleeping, scene I/O, netcode snapshot and rollback, neural controller, and every module whose public API uses Fix128 / Vec3Fix / QuatFix Pure integer arithmetic. Pinned by tests/determinism_golden.rs.
f32 / f64 field modules (28: acoustic_wave, aeroelasticity, anomaly, convex_decompose, erosion, fracture, gpu_sdf, phase_change, piezoelectric, pressure, privacy, rolling_contact, sdf_adaptive, sdf_ccd, sdf_character, sdf_collider, sdf_destruction, sdf_fem_mesh, sdf_manifold, sdf_sph, sdf_wind_field, sim_field, sim_modifier, sketch, spherical_terrain, thermal, thin_wall, transient_thermal) IEEE 754 + - * / sqrt and fused multiply-add are exact on every target Rust supports. Every transcendental (sin, exp, ln, powf, …) goes through alice-det-math, re-exported as alice_physics::det_math, never the platform libm. clippy.toml turns a stray libm call into a CI error. Pinned by tests/determinism_golden_f32.rs.

The module list in the second row is generated by scripts/f32_modules.py, and CI fails when it disagrees with src/. Nine more modules carry floats only across an I/O boundary (FFI, Python, replay and similar) and do no simulation arithmetic in f32.

Both golden suites run in CI on macOS (ARM and x86), Linux (ARM and x86), Windows and wasm32-wasip1.

No entropy, no clock, no seeded hash order. Production code (everything in src/ outside test modules) draws no OS entropy, reads no wall clock and uses no HashMap / HashSet / RandomState with the default (OS-seeded) hasher; its maps use a fixed-seed hasher or a BTreeMap. The one exception is the deprecated privacy::XorShift64::from_entropy, whose clock seed is its documented behaviour, and the deprecated constructors that call it. clippy's disallowed-types / disallowed-methods (clippy.toml) and scripts/entropy_guard.py check this in CI. Randomness a caller wants comes from a key or seed the caller passes, or from the privacy-crypto feature's SecureRng::from_entropy / DpNoise::try_from_entropy, which only the caller calls (with wasm, from the browser's crypto.getRandomValues).

Your own code. ClosureSdf takes a user closure. The solver stays deterministic, but inside the closure you need to call alice_physics::det_math::{sin, exp, …} instead of f32::sin and friends.

Naming the law. alice_physics::PHYSICS_SEMANTICS_ID identifies the arithmetic and step implementation of a build: it is folded from the pinned golden outputs and the alice-det-math identifier, and changes exactly when one of them changes. PhysicsWorld::law_id(&PHYSICS_SEMANTICS_ID) hashes it with the rules a world is configured with (config, contact cache, continuous collision, sleep thresholds, materials, force fields and participants), not its bodies. Two worlds with the same law identifier, state and inputs step to the same bits, provided the hooks, contact modifiers, SDF colliders and GPU bridge they hold also behave the same; those are outside the identifier. Run cargo run --example law_id_world to print both.

Not covered. Targets that do not follow IEEE 754 for the basic operations, such as 32-bit x86 built for x87 (i586, no SSE2), and builds that use fast-math style flags.

Determinism is not correctness. Matching bits say every peer computes the same numbers, not that the numbers are right. That is checked separately; see Validation and known defects.

Reset, observation and rollback

Because a step is bit-exact, a PhysicsWorld can be used as a step function that a search or planning loop calls many times: try an action, read the result, rewind, try another. These APIs support that use:

API What it gives you
PhysicsWorld::reset_world() every field back to the state PhysicsWorld::new produces, so the same start and the same inputs give the same bits on a second run
observe_body / observe_bodies a typed BodyObservation (position, velocity, rotation, angular velocity, sleeping, in_contact) for a goal check to read, instead of parsing the state blob
serialize_state / deserialize_state save and restore a branch point; restoring is refused when the bodies in the world (mass, shape, filter, material) differ from the ones that were saved, even if the count matches
snapshot_world / from_world_snapshot / restore_world save the whole world (bodies, joints, constraints, colliders, force fields, materials, filters, events, sleep state, broad-phase tree, warm-start caches, overflow flag) in one versioned blob with a checksum, and restore it into a new or existing world; every later step is bit-identical to the original's. The header carries the writer's stepping-semantics identifier and the world's law_id; a blob written under other semantics is refused, and restore_world_checked also compares the law_id with an expected one. A bad blob is rejected with a WorldSnapshotError that says why
step_n(n, dt) step(dt) run n times, the same call the Python step_n and WASM stepN bindings make
overflow_detected() reports when Fix128 arithmetic left its range, so a diverged run is not mistaken for a valid one; the flag survives a rollback
Vec3Fix::checked_* / checked_length_scaled / try_normalize_scaled a length or dot product whose square would leave the Fix128 range (|v| ≥ 2^31.5 ≈ 3.04e9) returns None instead of wrapping; the scaled versions return the correct length and direction there and are bit-identical to length / try_normalize inside the range
netcode::SimulationChecksum a checksum derived from the same bytes as the saved state

examples/world_auditor_observation.rs shows reset and observation. The contracts are pinned by the tests/wm0*_*.rs files.

Scope. serialize_state covers rigid bodies only: their motion, sleep state and the overflow flag; the caller rebuilds joints, force fields, filters and materials, as in rollback netcode. snapshot_world covers every PhysicsWorld field except what is code rather than data: SDF fields, pre-solve hooks, contact modifiers and a GPU bridge stay in the world being restored into, and their counts must match. The field-by-field table is in the docs of PhysicsWorld::snapshot_world. Motors, character controllers, cloth, fluids, FEM and vehicles are not PhysicsWorld fields and are saved by the caller. The crate provides the step function, not a search algorithm.

Participants (unstable). Other physics can run inside the world's step through the world_participant::Participant trait: register it with PhysicsWorld::add_participant, advance with try_step (or try_step_parallel), read it with observe_participant. Each participant is called once per substep in an order fixed by the ports it declares, may push forces onto bodies and exchange values through shared fields, and a failure is recorded as a sticky fault (fault / clear_fault) instead of wrapping. snapshot_world saves each participant's state; the participants themselves are code, so the world being restored into must already hold the same ones in the same order. The types and rules of world_participant may still change before they are settled, and such a change can come in a minor release; the module docs list the current contract.

What is included

Every public module, grouped by area and with a one-line summary, is listed in docs/MODULES.md. API details are on docs.rs.

Area Highlights
Rigid bodies XPBD solver with an optional temporal Gauss-Seidel backend, sleeping and islands, CCD, rollback-ready state serialization
Collision GJK / EPA, linear BVH or persistent dynamic AABB tree broad-phase, box, sphere, capsule, cylinder, cone, ellipsoid, torus, wedge, convex hull, compound, triangle mesh, height field, SDF colliders; world ray queries against those shapes (PhysicsWorld::cast_ray)
Joints ball, hinge, fixed, slider, spring, D6, cone-twist, plus pulley, gear, weld, rack-and-pinion and mouse joints; breakable joints and PD motors
Soft bodies XPBD rope and cloth (with self-collision), position-based fluids, FEM-XPBD deformables, cutting
Gameplay character controller, vehicles (a simple model and a per-wheel dynamics model with tyres, brakes, ABS, road surfaces and weather), ragdolls, IK bridge, client-side prediction, deterministic RNG with Gaussian draws, contact events, simulated lidar / contact / IMU sensors
Solid mechanics linear-elastic FEM on P1 / P2 / P3 tetrahedra, corotational large rotation, J2 plasticity, hyperelasticity, thermo-mechanical coupling, adaptive refinement, beams, buckling, fatigue, composites
Fluids and fields MAC-grid CFD with several pressure solvers, RANS / LES turbulence closures, VOF and level set, SPH, compressible flow, heat transfer, Maxwell FDTD with per-cell materials
Aerodynamics standard atmosphere (ISA 1976, up to 20 km), wing lift and drag with stall, rotor thrust and torque
Molecular dynamics Lennard-Jones, Morse, Coulomb and screened Coulomb pair potentials with cutoff and shift, velocity Verlet with a periodic cell list (minimum image)
Crowds social force model of pedestrian motion: driving term, repulsion weighted by the view angle, body force and sliding friction in contact, walls
3D printing material database, thin-wall and overhang checks, warp risk, layer adhesion, print orientation, a combined safety pipeline
2D a separate 2D XPBD engine with its own shapes and joints

Domain decomposition. The CFD pressure projection can run as z slabs that exchange one halo layer, with results bit-identical to the single-process solve for any number of ranks. This has been measured on one host only (up to eight processes over loopback TCP). Runs across several hosts have not been done, and there is no MPI backend.

Not every module is wired into PhysicsWorld. The table counts public modules by where their items are actually called from; calls from examples/ do not count. CI measures it with scripts/integration_levels.py; each module's label is in the Integration column of docs/MODULES.md and the details are in docs/integration-levels.md.

How a module is used Modules
step: runs when PhysicsWorld steps 24
world API: used through another PhysicsWorld method 15
binding: reached from the C ABI, Python or WebAssembly bindings 2
standalone: a Rust API you call yourself; PhysicsWorld does not call it 125
unused: no caller outside tests 1

Vehicle dynamics

vehicle_dynamics::DynamicVehicle is a car model in which each wheel acts on the chassis at its own contact point.

  • suspension and tyre forces are applied per wheel, so steering produces yaw and braking or cornering shifts load between axles and sides
  • each wheel has a spin state driven by drive torque, brake torque and the tyre force; brakes can lock a wheel, and ABS holds the slip ratio near a target
  • tyre forces come from a brush model or a Magic Formula model, limited by a friction ellipse
  • the road is a plane, a slope, a height field, a triangle mesh or an SDF; grip is the road material times a weather factor (dry, wet, snow, ice), with a hydroplaning loss on flooded roads
  • a locked wheel holds the car in place on a slope below the static friction limit, and slides at the kinetic coefficient above it
  • engine torque curve, gearbox, engine braking, open or locked differential, and aerodynamic drag and lift
  • vehicle_dynamics::scenario — run several DynamicVehicles in one PhysicsWorld from input tracks or a per-frame control function (fixed index order); following metrics: time to collision (gap / closing speed, None when not closing) and time headway; a stopping-distance meter (horizontal path from brake-on until forward speed reaches zero); lossless replay — initial state plus per-frame DriverInputs stored as raw Fix128 reproduce every chassis and wheel state bit for bit, with a versioned byte encoding; malformed / truncated recordings and mismatched scenarios are rejected with a typed ReplayError

The older vehicle::Vehicle is unchanged. It drives on a flat plane at ground_height and applies the sum of all wheel forces at the centre of mass, so it has no per-wheel load transfer, wheel lock or tyre model. Use vehicle_dynamics when stopping distance or cornering has to follow the physics.

use alice_physics::vehicle_dynamics::surface::{FlatGround, RoadCondition};
use alice_physics::vehicle_dynamics::{DynamicVehicle, DynamicVehicleConfig, Environment};

let mut car = DynamicVehicle::new(DynamicVehicleConfig::passenger_car());
let road = FlatGround { height: Fix128::ZERO };
let condition = RoadCondition::dry_asphalt();
let env = Environment { condition: &condition, wind: None, time: Fix128::ZERO };
car.input.brake = Fix128::ONE;
// every frame, before world.step(dt):
car.update(&mut world.bodies[chassis], &road, &env, dt);

examples/vehicle_dynamics.rs runs this setup and checks locked-wheel stopping distances on dry, wet and icy roads against v0² / (2 μ_k g), compares braking with and without ABS, and holds a car on a slope. examples/vehicle_scenario.rs runs two cars in one lane, brakes the follower on time to collision, and replays the recorded run bit for bit. The closed-form tests are in tests/analytic_vehicle_dynamics.rs and tests/analytic_vehicle_scenario.rs.

Known limitations

  • In full sliding, the brush model turns the force towards the sliding direction; the Magic Formula model does not (it scales the pure-slip forces onto the friction ellipse).
  • Wheel forces are applied as one impulse at the start of each frame. The suspension is stable only while (ω_n dt)² + 2 c dt / m_share < 4 (ω_n = √(k / m_share), m_share the mass carried by one wheel); a light chassis on stiff springs at a large dt oscillates with growing amplitude.
  • HeightField itself does not apply origin.y and has a known normal defect at the grid border. The height-field road follows the field's own sample_height (so the road sits at the height it returns, without origin.y) and computes its normal with one-sided differences at the border, so a wheel probe is not affected by the border defect.

Validation and known defects

Analytic and reference tests in tests/ compare results against closed-form solutions or published reference data (textbook formulas, Ghia et al. for the lid-driven cavity, and others). Golden hashes only detect change, so they are kept separate from these tests.

docs/oracle-status.md is generated from tests/ and lists every test by status. It includes the known defects: tests that are kept red on purpose (#[ignore = "known defect: …"]) until the implementation is fixed. Read that list before relying on a module for production numbers.

docs/integration-status.md is generated from rust-analyzer's resolved references and shows, for every public item, whether anything outside the tests reaches it, and whether only an example does.

Modules without a reference test, such as warp_risk and layer_adhesion (empirical fits), implement the cited equation but are not validated predictions. docs/MODULES.md marks them.

Cargo features

Feature Default Description
std yes Standard library. Disable for no_std (needs alloc).
simd SSE2 paths for Vec3Fix operations on x86_64. Results are bit-identical to the scalar paths.
parallel Parallel constraint solving with Rayon over graph-coloured batches.
ffi C ABI for Unity, Unreal Engine and other hosts. Cannot be combined with wasm.
wasm WebAssembly bindings through wasm-bindgen. Requires std; cannot be combined with ffi.
python Python bindings (PyO3 + NumPy).
gpu-solver-bridge GpuSolverBridge trait for external GPU solver backends. No cost when off.
privacy-crypto privacy re-exports alice-crypto's differential-privacy primitives (SecureRng, DpNoise, dp_int, randomized_response, …) and adds KeyedRappor. Off by default, so std alone pulls no entropy source. alice-crypto is then a public dependency: its breaking releases are breaking for this crate. On wasm32-unknown-unknown enable it together with wasm (--features std,wasm,privacy-crypto), which supplies the browser's entropy; std,privacy-crypto alone fails to compile there with getrandom's "you may need to enable the "js" feature" error.
neural Deterministic neural controller using alice-ml.
replay Replay recording and playback using alice-db.
analytics Simulation profiling using alice-analytics.

Bindings

Target Where Notes
C / C++ include/alice_physics.h --features ffi (the commands below build the cdylib and the staticlib)
Unity (C#) bindings/AlicePhysics.cs P/Invoke over the C ABI
Unreal Engine 5 unreal-plugin/ Blueprint component over part of the C ABI
Python src/python.rs --features python (maturin, commands below); PhysicsWorld (bodies, collision radius and shapes, static colliders, joints) and DeterministicSimulation classes, NumPy batch APIs
WebAssembly web/ --features wasm; WasmPhysicsWorld (bodies, collision radius and shapes, static colliders, joints) and a Three.js viewer (built with the commands below)

The bindings cover what you need to build and step a scene (bodies, collision radius and shapes, static colliders, joints, impulses, state serialization), not every module: the standalone modules in the table above are Rust only. CI checks the C ABI against its consumers (scripts/integration_levels.py): the C header, bindings/AlicePhysics.h and the Unity bindings declare every exported function; the Unreal Engine component wraps part of them, and the functions it does not wrap are listed with the reason in docs/integration-levels.md.

# C ABI (C / C++, Unity, Unreal Engine): the shared and the static library in target/release
cargo rustc --lib --release --features ffi --crate-type cdylib
cargo rustc --lib --release --features ffi --crate-type staticlib
# WebAssembly package for the browser (web/pkg); needs the wasm-bindgen CLI of the version in Cargo.lock
bash scripts/build_wasm_pkg.sh web web/pkg
# Python wheel (target/wheels)
maturin build --release --features python

Performance

Measured with cargo bench --bench physics_bench (criterion, release profile) on Apple silicon at version 1.2.0. Absolute numbers depend on the machine, so re-run the benchmark on your target before quoting them.

Operation Measured
Fix128 multiply 1.1 ns
Fix128 divide 141 ns
Fix128 square root 193–354 ns
Vec3Fix normalize 618 ns
10 bodies × 60 steps, default config 398 µs
1000 overlapping spheres, first frame 65 ms

Sleeping bodies

step leaves a sleeping body out of every stage while it is at rest and not attached to a joint or distance constraint (PhysicsWorld::set_sleep_skip, on by default; the results are bit-identical with it off). Its contacts with awake bodies are found through a persistent tree of the sleeping bodies, so the broad-phase is built over the awake bodies only. PhysicsWorld::stage_work reports the work of the last step per stage.

Measured with cargo bench --bench world_scale (criterion, release profile, one step of 8 substeps) on arm64 / 10 cores / 32 GiB, with other processes running (expect ±15 %). Spheres of radius 0.5 on a 2 m grid; the awake ones drift above the sleeping ones and never touch them.

Bodies Asleep Skip on Skip off
10 000 0 % 113 ms 122 ms
10 000 90 % 9.7 ms 96 ms
10 000 99 % 0.88 ms 121 ms
100 000 0 % 1.31 s 1.24 s
100 000 90 % 105 ms 1.31 s
100 000 99 % 10.8 ms 1.15 s

A sleeping body still costs about 30 ns per step: a check that it was not edited between steps (bodies is a public field) and its idle_frames count, which the state snapshot carries.

Minimum supported Rust version

Minimum supported Rust version: 1.85 (the rust-version in Cargo.toml).

A CI job builds the library with exactly this version for the default features, no_std, and std,simd,parallel,ffi,gpu-solver-bridge. Raising the MSRV is a minor-version change, never a patch. The neural, replay and analytics features follow the MSRV of the crates they pull in.

Building and testing

cargo build --release
cargo test
cargo test --features "simd,parallel,ffi,gpu-solver-bridge"
cargo bench --bench physics_bench
cargo bench --bench world_scale

wasm and ffi cannot be enabled together, so --all-features does not build; test wasm in a separate run. scripts/preflight.sh reproduces the CI checks locally (--quick skips the integration and oracle tests).

Related crates

Crate Role
alice-det-math the deterministic transcendental functions both this crate and ALICE-SDF use
ALICE-SDF signed distance functions; supplies SDF colliders evaluated with the same math
ALICE-LOL a language and law verifier that compiles to ALICE-SDF trees

Keep a single version of alice-det-math in your dependency graph. Two versions mean two implementations of the same function, and determinism is lost. Check with cargo tree -i alice-det-math.

Release history is in CHANGELOG.md. Planned work is in docs/ROADMAP.md.

License

Dual-licensed under AGPL-3.0-or-later OR LicenseRef-Commercial. Pick either.

Option Use it when
AGPL-3.0-or-later your project is AGPL-compatible open source, or you use it internally
Commercial closed-source products, proprietary SaaS, firmware, or engine plugin redistribution

The AGPL applies to anything that links alice-physics, including through the C ABI, the Unity / Unreal bindings, the Python bindings or the WebAssembly build. Commercial licence enquiries: contact@extoria.co.jp

The optional neural and replay features pull in additional AGPL-licensed crates. analytics and the required alice-det-math are Apache-2.0 (both since their 0.5 releases); alice-crypto, pulled in by the optional privacy-crypto feature, has the same licence as this crate.

Copyright (C) 2024-2026 Moroya Sakamoto

References

  • Müller et al., "XPBD: Position-Based Simulation of Compliant Constrained Dynamics"
  • Ericson, Real-Time Collision Detection (GJK / EPA)
  • Volder, "The CORDIC Trigonometric Computing Technique"