Skip to main content

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}