Mesh-output determinism¶
The pipeline-level determinism contract for emitted geometry, enforced by a
pinned manifest. It complements the kernel's predicate sign manifest
(rust/geometry/src/kernel/manifest.rs), which pins exact-predicate SIGNS;
this contract pins the actual bytes the pipeline emits.
What is byte-stable¶
Running process_geometry over the synthetic fixture in
ifc_lite_processing::determinism produces, bit-for-bit:
- Per mesh, in emit order:
express_id,geometry_class, position f32 bits, normal f32 bits, triangle indices, and the per-element f64origin. These are hashed into THREE separate per-mesh fields so the cross-target guard can hold each to a different standard:positions_hash(positions) andindices_origin_hash(identity + topology + placement) are byte-identical on every target for every mesh, with no exemption;normals_hashis the only surface allowed the documented trig gap, and only for the curved mesh. This is what makes the "positions are byte-identical cross-target, even for the round column" claim below a test-enforced invariant rather than prose. Emit order itself is part of the contract: entity jobs are processed in file scan order and rayon's ordered collect preserves it, so the mesh list is identical regardless of thread count (verified withRAYON_NUM_THREADS=1). - The three flat prepass wire arrays, which are an EXPLICIT sorted contract since this change (previously an implicit FxHashMap-iteration artifact):
flat_voids(keys, counts, values)sorted by host id, u32 ascending (rust/processing/src/prepass.rs);flat_material_colors(ids, counts, rgba8)sorted by element id, u32 ascending. Per-host opening lists and per-element colour lists keep their file order.flat_styles_rgba8(ids, rgba8)sorted by id, u32 ascending (ids span geometry, material and element ids across the layered style precedence). Each array carries its own hash and entry count in the manifest (voids_hash,material_colors_hash,styles_hash), so a drift report names the wire surface that diverged.
The fingerprint (FNV-1a 64, same scheme as the kernel manifest) is pinned in
rust/processing/tests/manifests/mesh_determinism.json together with mesh,
vertex and triangle counts and per-mesh hashes, so a failure identifies WHICH
mesh diverged.
At what settings¶
TessellationQuality::Medium-- the byte-identity density (seerust/geometry/src/tessellation.rs).- Local-frame vertex storage ON. This is the one flag whose DEFAULT differs
per target (
local_frame_enabled()inrust/geometry/src/router/transforms/mod.rs: ON for wasm32, the shipping viewer path; OFF for native unlessIFC_LITE_LOCAL_FRAME=1). The manifest harness equalizes it by forcing the flag ON on every target viaifc_lite_geometry::local_frame_set_enabled_override(Some(true)), so the pinned bytes are the local-frame output (per-element f64origin+ element-local f32 positions). Native default output (absolute f32 coords) is a different, internally deterministic encoding of the same geometry and is NOT what the manifest pins. OpeningFilterMode::Default, default streaming options, no env overrides (IFC_LITE_RECT_FAST,IFC_LITE_DISABLE_DEGENERATE_BACKSTOPetc. unset).
Across which targets¶
- x86_64 native == arm64 native: both assert the SAME manifest file
(
mesh_determinism.json). The arm64 leg runs in.github/workflows/determinism.yml. - wasm32: asserts its own pinned manifest
(
mesh_determinism.wasm32.json), which is byte-identical to the native one EXCEPT the documented trig gap below. A native-side guard test (wasm_manifest_differs_only_in_the_trig_gap) pins that the two files never drift apart beyond that gap, and fails loudly (with unification instructions) if the gap ever closes.
Both legs call the same compute_mesh_manifest() in
rust/processing/src/determinism.rs, so fixture and hashing cannot drift
between targets.
Known gap: libm trig ULP (wasm32 vs native)¶
Circle tessellation computes profile points with sin/cos. Platform libms
disagree by ~1 ULP on some inputs; f32 rounding absorbs that in positions
(they ARE byte-identical), but the smooth radial side normals of
circular-ish extrusions (extrude_profile in
rust/geometry/src/extrusion.rs) are derived from the f64 profile points, so
near-zero normal components (true value ~0, e.g. sin at the angle pi) keep
the full residue: measured -7.0e-17 native vs -2.1e-17 wasm32 on the fixture
column. Closing this requires deterministic trig (e.g. the pure-Rust libm
crate) in the profile tessellation path -- a separate work item. Until then
the round column's normals_hash is the ONLY divergence between the two pinned
manifests: its positions_hash and indices_origin_hash match native ==
wasm32 (empirically confirmed and asserted per-field by the guard), so a future
change that diverged the curved mesh's POSITIONS across targets would fail the
guard instead of hiding inside a combined per-mesh hash.
What was fixed to get here¶
consolidate_coplanar (rust/geometry/src/csg/consolidate.rs) grouped cut
triangles into plane buckets keyed in an FxHashMap and emitted the output
mesh in bucket iteration order. FxHasher mixes usize-wide chunks, so its
iteration order differs between 64-bit native and 32-bit wasm32: the same
CSG cut emitted the same triangles in a different order per target. The
buckets are now a BTreeMap (Ord-keyed, target-independent iteration; same
pattern as facet_weld's normal buckets), which made every CSG-cut mesh in
the battery byte-identical native == wasm32.
What is NOT covered¶
- Other tessellation tiers (Lowest/Low/High/Highest) are internally deterministic but not cross-tier comparable, and are not pinned.
- Native default (local-frame OFF) output is not pinned; it shares all the same code except the frame subtraction.
- The kernel predicate sign manifest is a separate, existing guarantee; this contract does not replace it and the battery here does not exercise every kernel path (it covers rect-fast cuts; kernel-cut coverage rides the geometry crate's snapshot suite).
- Wire arrays other than voids/material-colors/styles (job lists, entity index payloads) are diagnostics/routing, not geometry, and are not pinned.
Re-pinning¶
An intended geometry change re-pins both files: run
cargo test -p ifc-lite-processing --test mesh_determinism -- --nocapture,
copy the printed actual JSON into mesh_determinism.json, then run
wasm-pack test --node rust/wasm-bindings --test mesh_determinism and copy
its printed actual JSON into mesh_determinism.wasm32.json. The
wasm_manifest_differs_only_in_the_trig_gap guard verifies the pair stays
in lockstep.