# CLAUDE.determinism.md

The cross-platform determinism contract for the transform fingerprint. Desktop x64, WASM, and
Android (arm64-v8a and x86_64) must produce bit-identical hashes for the same sim.

This file covers the **hash**, and only the parts of it that are traps — things no compiler and no
naming convention will catch. The other determinism rules live where they belong: `crMath` for trig
and pow (see CLAUDE.md § Conventions → Math), the fixed timestep in `crApp`, and the per-platform
FP flag contract (FMA contraction, prebuilt-library caveats) in the comment atop `src/main.cpp`.

---

## The trap: entt view order is a function of the whole create/destroy history

`crEcs::HashTransforms` walks `registry.view<const CTransform>` in the **`CTransform` pool's packed
order**. entt's sparse set appends on create and does **swap-and-pop** on erase. So the position of
any hashed entity in that walk depends on every create and destroy that ever touched the pool —
**including entities that are excluded from the hash**.

The consequence is not intuitive:

> A decorative entity that is correctly tagged `CTag_NonHashTarget`, and whose own transform is
> therefore never hashed, can still change the hash — by shifting where the *real* sim entities land
> in the iteration order.

So excluding a component from the fingerprint is **not** enough to make it harmless.

### The rule this forces

**Every `registry.create()` and `registry.destroy()` must happen on the sim clock** — inside
`FixedUpdatePre` / `FixedUpdatePost` or something they call. Never from `Update()`, `Render()`, or any
render-rate path. A game driven from its UI has to queue what the UI asked for and drain it in a
fixed step, because the UI itself runs on the render clock.

Move one cosmetic spawn to the render clock and the fingerprint becomes frame-rate dependent, which
reads as a determinism bug in sim code that is in fact perfectly deterministic. Nothing in the type
system stops this; the entity churn and the hash are in different files.

Two framework paths already obey this and are easy to assume otherwise about:

- particle teardown — `particles->FixedUpdate`, called from `crApp::FixedUpdate`, not from `Update`
- `crRibbonTrailSystem` creates no entities at all

---

## `CTag_NonHashTarget` is opt-OUT, deliberately

Tag the entities to **exclude**, never the ones to include.

Forgetting the tag on a decorative entity produces a loud mismatch — you find it immediately.
Tagging what to *include* instead would fail the other way: a forgotten tag silently drops a real sim
entity from the fingerprint, and the verification passes while covering less than it claims. A
verification tool must never fail by quietly passing.

Required on anything animated on the render clock — view-facing billboards cannot be made
deterministic at all — and wanted even on cosmetics that happen to be deterministic, since a
fingerprint covering scenery is one an art tweak invalidates.

The physics body is **not** the criterion. A sim entity may integrate by hand with no `b3Body` at
all, and filtering on `CPhysicsBody` would silently drop it — a false pass.

---

## What the hash covers

`crEcs::HashTransforms` — FNV-1a over each `CTransform.current`, then the **entity count folded in
last**, so a divergence in *how many* things exist is caught as well as *where* they are. The count is
`uint64_t` on purpose: `size_t` is 8 bytes on win64 and 4 on wasm32.

`b3Transform` is hashed as raw bytes and has **no padding** (`b3Vec3` 12 + `b3Quat` 16 = 28, align 4).
If `b3Vec3` ever becomes SIMD-aligned, uninitialized padding enters the hash and platforms diverge for
no reason a sim change could explain.

---

## Verifying

`Game_Dev` is the scene for this. It is the framework's test bed and it has **no player in it** —
two AI commanders fight on their own — and a run that takes no input is the only kind that can be
compared to another run. A game in progress fails both counts: its sim keeps changing underneath the
measurement, and any click makes the two runs different sims.

Point the selector (`src/game/Game.h`) at it and run. It emits the stamps itself, so there is no
harness to rebuild. They land on **absolute** sim steps, never per-round ones: the step index never
resets, so the same steps stay comparable across platforms even when a round boundary falls
somewhere else. What each stamped field means belongs to that scene, not here.

**Two desktop runs first, then desktop vs each other platform.** A single run proves nothing: it
cannot separate nondeterminism within one platform from nondeterminism between two, and the first is
by far the more common bug.
