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 | 日本語
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
- Example
- Determinism
- Reset, observation and rollback
- What is included
- Vehicle dynamics
- Validation and known defects
- Cargo features
- Bindings
- Performance
- Minimum supported Rust version
- Building and testing
- Related crates
- License
Installation
Without the standard library (requires alloc):
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 ;
// Create physics world
let config = default;
let mut world = new;
// Add a dynamic body
let body = new_dynamic;
let body_id = world.add_body;
// Step simulation (60 frames at 1/60 second)
let dt = from_ratio;
for _ in 0..60
// Body should have fallen under gravity
let pos = world.bodies.position;
assert!;
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 |
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 severalDynamicVehicles in onePhysicsWorldfrom input tracks or a per-frame control function (fixed index order); following metrics: time to collision (gap / closing speed,Nonewhen 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-frameDriverInputs stored as rawFix128reproduce every chassis and wheel state bit for bit, with a versioned byte encoding; malformed / truncated recordings and mismatched scenarios are rejected with a typedReplayError
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 ;
use ;
let mut car = new;
let road = FlatGround ;
let condition = dry_asphalt;
let env = Environment ;
car.input.brake = ONE;
// every frame, before world.step(dt):
car.update;
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_sharethe mass carried by one wheel); a light chassis on stiff springs at a largedtoscillates with growing amplitude. HeightFielditself does not applyorigin.yand has a known normal defect at the grid border. The height-field road follows the field's ownsample_height(so the road sits at the height it returns, withoutorigin.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
# WebAssembly package for the browser (web/pkg); needs the wasm-bindgen CLI of the version in Cargo.lock
# Python wheel (target/wheels)
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
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"