alice_physics/law_id.rs
1//! Content identifier of the law a [`PhysicsWorld`]
2//! is configured with: [`PhysicsWorld::law_id`].
3//!
4//! Two worlds that start from the same state and receive the same inputs
5//! produce the same bits when they follow the same law. The law has two
6//! parts, each named by a 32-byte identifier:
7//!
8//! * the **arithmetic and the step implementation** of a build, named by
9//! [`PHYSICS_SEMANTICS_ID`](crate::semantics::PHYSICS_SEMANTICS_ID);
10//! * the **rules a given world is configured with**, named by
11//! `PhysicsWorld::law_id`, which hashes them together with a semantics
12//! identifier.
13
14/// Domain separation for world law identifiers, published so that an
15/// independent implementation can reproduce one byte for byte.
16pub const LAW_ID_DOMAIN: &[u8] = b"alice-physics/law-id/v1";
17
18/// Identifies the encoding of a world law identifier. A change to the
19/// encoding needs a new tag, since it invalidates every identifier published
20/// under the old one.
21pub const WORLD_LAW_KIND: &[u8] = b"physics-world/v1";
22
23use crate::force::ForceField;
24use crate::material::CombineRule;
25use crate::math::{Fix128, Vec3Fix};
26use crate::solver::{PhysicsWorld, SolverBackend};
27use sha2::{Digest, Sha256};
28
29#[cfg(not(feature = "std"))]
30use alloc::vec::Vec;
31
32/// The canonical byte encoding of a world's law, written into a hasher.
33struct Encoder(Sha256);
34
35impl Encoder {
36 /// A length prefix: the count as a big-endian `u64`.
37 fn len(&mut self, n: usize) {
38 self.0.update((n as u64).to_be_bytes());
39 }
40 /// A length-prefixed byte string (also used for the names of enum values).
41 fn bytes(&mut self, b: &[u8]) {
42 self.len(b.len());
43 self.0.update(b);
44 }
45 fn u32(&mut self, v: u32) {
46 self.0.update(v.to_be_bytes());
47 }
48 fn u64(&mut self, v: u64) {
49 self.0.update(v.to_be_bytes());
50 }
51 fn bool(&mut self, v: bool) {
52 self.0.update([u8::from(v)]);
53 }
54 /// The raw bits of a `Fix128`, high word then low word, big-endian.
55 fn fix(&mut self, v: Fix128) {
56 self.0.update(v.hi.to_be_bytes());
57 self.0.update(v.lo.to_be_bytes());
58 }
59 fn vec3(&mut self, v: Vec3Fix) {
60 self.fix(v.x);
61 self.fix(v.y);
62 self.fix(v.z);
63 }
64 fn combine(&mut self, rule: CombineRule) {
65 self.bytes(match rule {
66 CombineRule::Average => b"average",
67 CombineRule::Min => b"min",
68 CombineRule::Max => b"max",
69 CombineRule::Multiply => b"multiply",
70 });
71 }
72 fn field(&mut self, field: &ForceField) {
73 match *field {
74 ForceField::Directional {
75 direction,
76 strength,
77 } => {
78 self.bytes(b"directional");
79 self.vec3(direction);
80 self.fix(strength);
81 }
82 ForceField::Point {
83 center,
84 strength,
85 repulsive,
86 max_force,
87 } => {
88 self.bytes(b"point");
89 self.vec3(center);
90 self.fix(strength);
91 self.bool(repulsive);
92 self.fix(max_force);
93 }
94 ForceField::Drag { coefficient } => {
95 self.bytes(b"drag");
96 self.fix(coefficient);
97 }
98 ForceField::Buoyancy {
99 surface_y,
100 density,
101 drag,
102 } => {
103 self.bytes(b"buoyancy");
104 self.fix(surface_y);
105 self.fix(density);
106 self.fix(drag);
107 }
108 ForceField::Vortex {
109 center,
110 axis,
111 strength,
112 falloff_radius,
113 } => {
114 self.bytes(b"vortex");
115 self.vec3(center);
116 self.vec3(axis);
117 self.fix(strength);
118 self.fix(falloff_radius);
119 }
120 ForceField::Explosion {
121 center,
122 strength,
123 radius,
124 falloff_power,
125 } => {
126 self.bytes(b"explosion");
127 self.vec3(center);
128 self.fix(strength);
129 self.fix(radius);
130 self.fix(falloff_power);
131 }
132 ForceField::Magnetic {
133 position,
134 moment,
135 strength,
136 } => {
137 self.bytes(b"magnetic");
138 self.vec3(position);
139 self.vec3(moment);
140 self.fix(strength);
141 }
142 }
143 }
144}
145
146impl PhysicsWorld {
147 /// Identifier of the law this world steps by, hashed with `semantics_id`
148 /// (pass [`PHYSICS_SEMANTICS_ID`](crate::semantics::PHYSICS_SEMANTICS_ID)
149 /// for this build).
150 ///
151 /// # What it covers
152 ///
153 /// The rules a step reads, not the state it acts on: the solver config,
154 /// the contact cache settings, `sdf_collision_radius`, continuous
155 /// collision, the sleep thresholds, the material table, the force fields
156 /// and the registered participants. The encoding below lists every field.
157 ///
158 /// Not covered:
159 ///
160 /// * the state: bodies, constraints and joints, including joint motors
161 /// (their gains and targets live on the joint, so they are state);
162 /// * the broadphase kind and the sleep skip, since every choice gives the
163 /// same bits;
164 /// * the caller-supplied code a world can hold: pre-solve hooks, contact
165 /// modifiers, SDF colliders and an installed GPU solver bridge.
166 ///
167 /// `config.warm_start_factor` is covered although the CPU step does not
168 /// read it: it is passed to an installed GPU solver bridge, whose result
169 /// may depend on it.
170 ///
171 /// # Guarantee, and its limit
172 ///
173 /// Two worlds with the same identifier, the same state and the same
174 /// inputs step to the same bits, **provided the hooks, modifiers, SDF
175 /// colliders and bridge they hold also behave the same**, since those are
176 /// outside the identifier. A participant contributes only its kind, step
177 /// rule and ports: two participants of one kind that differ in their
178 /// coefficients get the same identifier until participants expose their
179 /// coefficients. The converse does not hold either: worlds whose laws step
180 /// identically may still differ here (for example two force fields
181 /// declared in the other order), so this is not a deduplication key.
182 /// One case of this: `config.solver_backend` is covered, but
183 /// `step_parallel`, `try_step_parallel` and the GPU bridge
184 /// paths run XPBD whatever it says, so on those paths two worlds that
185 /// differ only in the backend have different identifiers and step to the
186 /// same bits.
187 ///
188 /// # Encoding
189 ///
190 /// SHA-256 over the concatenation below, which an independent
191 /// implementation can reproduce byte for byte (the layout of the law
192 /// identifiers of `alice-zip`). Primitive encodings:
193 ///
194 /// | Value | Bytes |
195 /// |-------|-------|
196 /// | length, count, `usize` | `u64` big-endian |
197 /// | `u32` | 4 bytes big-endian |
198 /// | `MaterialId` (`u16`) | widened to `u64`, big-endian |
199 /// | `bool` | one byte, `0` or `1` |
200 /// | `Fix128` | raw high word (`i64`) then low word (`u64`), big-endian |
201 /// | `Vec3Fix` | `x`, `y`, `z` as `Fix128` |
202 /// | byte string, enum value | length, then the bytes; an enum value is written as the tag given in the table |
203 /// | list | count, then the items |
204 /// | `Option<T>` | `bool` (`1` for `Some`), then `T` when present |
205 ///
206 /// Fields, in order:
207 ///
208 /// | # | Field | Encoding |
209 /// |---|-------|----------|
210 /// | 1 | [`LAW_ID_DOMAIN`] | byte string |
211 /// | 2 | [`WORLD_LAW_KIND`] | byte string |
212 /// | 3 | `semantics_id` | the 32 bytes, no length |
213 /// | 4 | `config.substeps`, `config.iterations` | `u64`, `u64` |
214 /// | 5 | `config.gravity`, `config.damping` | `Vec3Fix`, `Fix128` |
215 /// | 6 | `config.solver_backend` | tag `xpbd` or `tgs` |
216 /// | 7 | `config.warm_start_factor` | `Fix128` |
217 /// | 8 | contact cache `warm_start_factor`, `max_stale_frames` | `Fix128`, `u32` |
218 /// | 9 | `sdf_collision_radius` | `Fix128` |
219 /// | 10 | continuous collision: enabled, motion threshold | `bool`, `Fix128` |
220 /// | 11 | sleep `linear_threshold`, `angular_threshold`, `frames_to_sleep` | `Fix128`, `Fix128`, `u32` |
221 /// | 12 | default friction and restitution combine rules | tag `average`, `min`, `max` or `multiply`, twice |
222 /// | 13 | materials, in id order | list of: id (`MaterialId`), `static_friction`, `dynamic_friction`, `restitution` (`Fix128` each), friction combine, restitution combine (tags) |
223 /// | 14 | pair overrides, sorted by `(mat_a, mat_b)` with `mat_a <= mat_b` | list of: `mat_a`, `mat_b` (`MaterialId`), `friction`, `restitution` (`Fix128`) |
224 /// | 15 | force fields, in the order they were added | list of: `enabled` (`bool`), the bodies it is limited to (`Option` of a list of `u64`, sorted ascending without duplicates), then the field |
225 /// | 16 | participants, in registration order | list of: kind (`u32`), step rule, ports |
226 ///
227 /// A field is its tag followed by its parameters in declaration order:
228 /// `directional` (direction `Vec3Fix`, strength), `point` (center
229 /// `Vec3Fix`, strength, repulsive `bool`, max_force), `drag`
230 /// (coefficient), `buoyancy` (surface_y, density, drag), `vortex` (center
231 /// `Vec3Fix`, axis `Vec3Fix`, strength, falloff_radius), `explosion`
232 /// (center `Vec3Fix`, strength, radius, falloff_power), `magnetic`
233 /// (position `Vec3Fix`, moment `Vec3Fix`, strength); every parameter not
234 /// marked otherwise is a `Fix128`. A step rule is the tag
235 /// `follow-substep`, `fixed` followed by its `dt` (`Fix128`), or
236 /// `subcycle`. A port is its id (`u32`) followed by the tag `read`,
237 /// `write` or `read-committed`.
238 ///
239 /// Sorting the pair overrides and the body lists keeps the identifier
240 /// independent of the order they were registered in, which the step does
241 /// not depend on either. Force fields and participants keep their order,
242 /// since the step applies them in that order.
243 #[must_use]
244 pub fn law_id(&self, semantics_id: &[u8; 32]) -> [u8; 32] {
245 let mut e = Encoder(Sha256::new());
246 e.bytes(LAW_ID_DOMAIN);
247 e.bytes(WORLD_LAW_KIND);
248 e.0.update(semantics_id);
249
250 let c = &self.config;
251 e.u64(c.substeps as u64);
252 e.u64(c.iterations as u64);
253 e.vec3(c.gravity);
254 e.fix(c.damping);
255 e.bytes(match c.solver_backend {
256 SolverBackend::Xpbd => b"xpbd",
257 SolverBackend::Tgs => b"tgs",
258 });
259 e.fix(c.warm_start_factor);
260
261 e.fix(self.contact_cache.warm_start_factor);
262 e.u32(self.contact_cache.max_stale_frames);
263 e.fix(self.sdf_collision_radius);
264
265 let ccd = self.continuous_collision();
266 e.bool(ccd.is_enabled());
267 e.fix(ccd.motion_threshold());
268
269 let sleep = self.islands.config;
270 e.fix(sleep.linear_threshold);
271 e.fix(sleep.angular_threshold);
272 e.u32(sleep.frames_to_sleep);
273
274 let m = &self.material_table;
275 e.combine(m.default_friction_combine);
276 e.combine(m.default_restitution_combine);
277 e.len(m.materials.len());
278 for mat in &m.materials {
279 e.u64(u64::from(mat.id));
280 e.fix(mat.static_friction);
281 e.fix(mat.dynamic_friction);
282 e.fix(mat.restitution);
283 e.combine(mat.friction_combine);
284 e.combine(mat.restitution_combine);
285 }
286 let mut overrides: Vec<_> = m.pair_overrides.iter().collect();
287 overrides.sort_by_key(|p| (p.mat_a, p.mat_b));
288 e.len(overrides.len());
289 for p in overrides {
290 e.u64(u64::from(p.mat_a));
291 e.u64(u64::from(p.mat_b));
292 e.fix(p.friction);
293 e.fix(p.restitution);
294 }
295
296 e.len(self.force_fields.len());
297 for f in &self.force_fields {
298 e.bool(f.enabled);
299 match &f.affected_bodies {
300 None => e.bool(false),
301 Some(bodies) => {
302 let mut bodies = bodies.clone();
303 bodies.sort_unstable();
304 bodies.dedup();
305 e.bool(true);
306 e.len(bodies.len());
307 for b in bodies {
308 e.u64(b as u64);
309 }
310 }
311 }
312 e.field(&f.field);
313 }
314
315 self.encode_participants(&mut e);
316 e.0.finalize().into()
317 }
318
319 #[cfg(feature = "std")]
320 fn encode_participants(&self, e: &mut Encoder) {
321 use crate::world_participant::{PortAccess, StepRule};
322 let participants = self.participant_law_parts();
323 e.len(participants.len());
324 for (kind, step_rule, ports) in &participants {
325 e.u32(kind.get());
326 match *step_rule {
327 StepRule::FollowSubstep => e.bytes(b"follow-substep"),
328 StepRule::Fixed(dt) => {
329 e.bytes(b"fixed");
330 e.fix(dt);
331 }
332 StepRule::Subcycle => e.bytes(b"subcycle"),
333 }
334 e.len(ports.len());
335 for port in ports {
336 e.u32(port.id().get());
337 e.bytes(match port.access() {
338 PortAccess::Read => b"read",
339 PortAccess::Write => b"write",
340 PortAccess::ReadCommitted => b"read-committed",
341 });
342 }
343 }
344 }
345
346 /// Without `std` no participant can be registered: the list is empty.
347 #[cfg(not(feature = "std"))]
348 fn encode_participants(&self, e: &mut Encoder) {
349 e.len(0);
350 }
351}
352
353// The encoding oracle of `tests/law_id_world.rs`, compiled into the lib tests
354// as well, so that the `--lib` line coverage measures it (the file is
355// unchanged and still runs as an integration test).
356#[cfg(all(test, feature = "std"))]
357#[allow(unreachable_patterns)] // a catch-all for `#[non_exhaustive]` enums, unreachable inside the crate
358#[path = "../tests/law_id_world.rs"]
359mod world_law_oracle;
360
361// `crate::semantics` must stay free of `cfg` (its table is the same in every
362// build, `tests/physics_semantics_id.rs`), so its decoder is tested here.
363#[cfg(all(test, feature = "std"))]
364mod semantics_decoder {
365 use crate::semantics::{hex32, PHYSICS_SEMANTICS_ID, PHYSICS_SEMANTICS_PINS};
366
367 fn hex(b: &[u8; 32]) -> String {
368 b.iter().map(|x| format!("{x:02x}")).collect()
369 }
370
371 /// `hex32` decodes at run time exactly as it does in the constants: every
372 /// digit, both ranges, and the byte order (first two digits = byte 0).
373 #[test]
374 fn hex32_decodes_every_digit_in_order() {
375 let s = "0123456789abcdef0123456789abcdef0123456789abcdeffedcba9876543210";
376 let b = hex32(s);
377 assert_eq!((b[0], b[7], b[24], b[31]), (0x01, 0xef, 0xfe, 0x10));
378 assert_eq!(hex(&b), s);
379 }
380
381 /// The published identifier decodes to the hex its documentation shows.
382 #[test]
383 fn the_identifier_matches_its_documented_hex() {
384 let doc = "cd9aa3b89257fc8b3e5d6714362ce4e52d4ff142baf30de2f80324e44bdd3469";
385 assert_eq!(hex(&PHYSICS_SEMANTICS_ID), doc);
386 assert_eq!(hex32(doc), PHYSICS_SEMANTICS_ID);
387 assert!(PHYSICS_SEMANTICS_PINS
388 .iter()
389 .any(|(n, _)| *n == "alice-det-math"));
390 }
391
392 #[test]
393 #[should_panic(expected = "a digest is 64 hex digits")]
394 fn hex32_refuses_a_short_digest() {
395 let _ = hex32("00");
396 }
397
398 #[test]
399 #[should_panic(expected = "not a lower-case hex digit")]
400 fn hex32_refuses_an_upper_case_digit() {
401 let _ = hex32("ABababababababababababababababababababababababababababababababab");
402 }
403}