Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -254,4 +254,5 @@ if(LAIGE_BUILD_TESTS)
add_subdirectory(tools/api) # M0-TOOL-01: laige-api-scanner
add_subdirectory(tools/detcheck) # M0-TOOL-02: laige-detcheck
add_subdirectory(tools/run) # M1-HEAD-01: laige-run (+ smoke test)
add_subdirectory(tools/replay) # M1-DET-03: laige-replay
endif()
21 changes: 13 additions & 8 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ PRNG substreams, and the `seed`/`determinism` config keys;
M1-DET-02: replay recording — the versioned replay log format
(replay identity per ADR 0002), the `ReplayRecorder` (atomic
temp+rename, size-bounded), `Engine::startReplayRecording`, and
`laige-run --replay`).
`laige-run --replay`; M1-DET-03: replay execution —
`World::stateHash` (the deterministic state hash), `runReplay`
(the identity-checked re-run and its per-tick hash stream), and the
`laige-replay` runner).
Every section of the AGENTS §13 `docs/` tree exists; each entry below
links what is written and the "not yet written" section marks what is
still to land.
Expand All @@ -29,7 +32,7 @@ still to land.
- [Building Laige](getting-started/building.md) — the source of truth
for the canonical build commands, build trees, options, compiler
policy (NFR-8.10), sanitizer builds (NFR-8.2), and the current M0
status. Tool commands: `laige-run`, `laige-fuzz`, `laige-bench`,
status. Tool commands: `laige-run`, `laige-replay`, `laige-fuzz`, `laige-bench`,
`laige-detcheck`, the `laige-api` manifest target, and the
include-graph lint.

Expand Down Expand Up @@ -104,12 +107,14 @@ still to land.
`detail::IsDeterminismSafe<T>`, and
`LAIGE_DETERMINISM_SAFE(Type, MemberTypes...)` (M1-DET-01;
`laige-sim`).
- [Replay recording](api/replay.md) — the versioned replay log
format (replay identity: seed, tick rate, component schema hash,
math backend id, config hash — ADR 0002), the `ReplayRecorder`
(atomic temp+rename, size-bounded), the `parseReplay`/`loadReplay`
readers, the identity hashes, and the engine/CLI wiring
(M1-DET-02; `laige-sim`).
- [Replay recording and execution](api/replay.md) — the versioned
replay log format (replay identity: seed, tick rate, component
schema hash, math backend id, config hash — ADR 0002), the
`ReplayRecorder` (atomic temp+rename, size-bounded), the
`parseReplay`/`loadReplay` readers, the identity hashes, the
engine/CLI wiring (M1-DET-02), and the execution half:
`World::stateHash`, `replayIdentityDiff`/`runReplay`, and the
`laige-replay` runner (M1-DET-03; `laige-sim`).
- [Result / Status / error codes](api/errors.md) — `laige::Result<T,E>`,
`laige::Status`, the stable `ErrorCode` registry (M0-CORE-01).
- [Structured logging](api/logging.md) — the `laige::log` facade, sinks,
Expand Down
16 changes: 10 additions & 6 deletions docs/api/detcheck.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@ in its header comment, which this page mirrors); CTest suite:
(e.g. Debug+ASan vs Release, or two compiler builds) and asserts that the
per-tick state hashes are identical — bit-identity of the deterministic
state across configurations (NFR-8.3, FR-1.4). The engine state-hash API
arrives with **M1-DET-03** (`world.state_hash`); this tool therefore works
against the **hash-file output contract** defined below and compares two
such streams.
landed with **M1-DET-03** (`World::stateHash`,
[api/entity.md](entity.md)); this tool works against the **hash-file
output contract** defined below and compares two such streams. The
current scenario fixture still prints its own ad-hoc hash stream; the
scenario wiring that switches it to the `World::stateHash` stream lands
with M1-SAMPLE-01 (the tool's line-by-line comparison is unchanged).

## Scenario contract

Expand Down Expand Up @@ -156,9 +159,10 @@ body index and a nudge in [-4, 3] applied to x. Per-tick hash: FNV-1a 64
and compiler by the language standard; no float anywhere in the workload.
**Hash scope (M0):** tick counter + seed + body words. The Prng position
is a pure function of (seed, nudge history) in this workload; the
definitive scope — including PRNG state and the exact hash function — is
defined by M1-DET-03's `world.state_hash`, which replaces the ad-hoc FNV
computation of scenarios (the tool's line-by-line comparison is unchanged).
definitive scope — including PRNG state and the exact hash function —
is M1-DET-03's `World::stateHash`
([api/entity.md](entity.md)), which the scenario wiring switches to
with M1-SAMPLE-01 (the tool's line-by-line comparison is unchanged).

## Performance and bounds

Expand Down
5 changes: 4 additions & 1 deletion docs/api/engine.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,10 @@ Cross-build/platform determinism is **M1-DET-04** (the detcheck
matrix); replay **recording** landed with **M1-DET-02**
([api/replay.md](replay.md) — `Engine::startReplayRecording`, the
versioned log format, and `laige-run --replay`); replay **execution**
(the `laige-replay` runner and `world.state_hash`) is **M1-DET-03**.
landed with **M1-DET-03** — `World::stateHash` (the deterministic
state hash, [api/entity.md](entity.md)) and `runReplay` / the
`laige-replay` runner ([api/replay.md](replay.md), "The execution
half").

## Replay recording (M1-DET-02)

Expand Down
49 changes: 49 additions & 0 deletions docs/api/entity.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,51 @@ stale handle assert in debug and degrade in release.
bit-identical `Entity` handle sequences on every platform, so
handles are replay state from M1 on.

## The deterministic state hash (`World::stateHash`, M1-DET-03)

```cpp
std::uint64_t World::stateHash(std::uint64_t tick) const noexcept;
```

The 64-bit FNV-1a hash of the world's **authoritative sim state** at
`tick` completed ticks — the per-tick value the replay runner
(`runReplay`, [api/replay.md](replay.md)) and the detcheck scenario
contract ([api/detcheck.md](detcheck.md)) compare run against run.

- **Pure function of the live state** — never of the operation
history that produced it. Two worlds that converge on the same live
state (same live handles, same component bytes, same PRNG substream
states) hash identically, whatever their create/destroy interleavings
were; dead-slot generations, the free-list order, and empty archetypes
are deliberately out (see the header's scope list, entity.h).
- **Canonical stream (FNV-1a 64, the house word-stream convention —
big-endian per u64 word; basis `0xcbf29ce484222325`, prime
`0x100000001b3`, fnv.org):** `tick` → live count → per live slot
ascending `(slot, generation)` → per non-empty archetype in
lexicographic signature order `(component ids ascending, row count,
then the raw component bytes row-major in signature column order)` →
per system ascending `(system id, has-substream flag, substream
seed/state1/state2)`. Component bytes go in raw memory order (every
P0 target is little-endian — PRD §6).
- **Determinism scope (ARCH-010):** same build, platform, architecture,
and compiler — the hash is pure integers over a canonical byte
order; no wall clock, no addresses, no unordered containers.
Cross-build identity is M1-DET-04's detcheck matrix.
- **Performance (PERF-002/003, DOC-004):** `O(capacity + live component
bytes + kMaxArchetypes²)` (the per-set ordering is an insertion sort
over ≤ 256 non-empty archetypes); **no allocation** (fixed stack
state), no logging, no side effects, `const`. COLD path: the replay
runner and detcheck scenarios call it once per tick; the engine's
per-tick hot path never does.
- **Not a cryptographic hash** — a state-difference detector for
determinism verification (FR-1.4/FR-11.3), not a security primitive
(DEP-002).

Verified by `ctest -R replay_replay` (the `StateHash.*` suites: the
known-answer vector, capacity independence, tick/handle sensitivity,
component and archetype sensitivity, convergence equality, PRNG
sensitivity, and the zero-allocation proof).

## Usage (performant pattern)

```cpp
Expand Down Expand Up @@ -311,3 +356,7 @@ if (!world.isValid(handle)) { /* stale — drop it, log if unexpected */ }
`guardrailStats()`, the `ecs/entity_budget_{25,50,100}` and
`ecs/churn_per_frame` warns (the "Guardrails" section above);
suite `ctest -R ecs_guardrails`.
- **M1-DET-03 (done):** `World::stateHash` — the deterministic state
hash ("The deterministic state hash" section above); the replay
execution half and the `laige-replay` runner live in
[api/replay.md](replay.md); suite `ctest -R replay_replay`.
6 changes: 4 additions & 2 deletions docs/api/iteration_order.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,10 @@ floats, addresses, wall clocks, or platform intrinsics enter it. The
scope the contract promises is therefore the widest ARCH-010 allows:
**the same state visited identically on every supported platform,
architecture, and compiler** (same build). The state *hash* that
M1-DET-03 computes over the visit order inherits this scope once the
component-value encoding is pinned there.
M1-DET-03's `World::stateHash` computes inherits this scope: the
canonical stream feeds the component values in this visit order
(entity rows ascending, columns in the archetype signature order —
[api/entity.md](entity.md)).

## Performance (DOC-004)

Expand Down
9 changes: 6 additions & 3 deletions docs/api/prng.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ every statistical sense, but they are *not* cryptographically independent
| `std::uint32_t next_range(std::uint32_t min, std::uint32_t max)` | Uniform in `[min, max)`; `max - min in [1, 2^32 - 1]`. `min >= max`: debug assert, documented UB in release (span underflow). Expected < 2 draws; the rejection probability per draw is `< 1/2` (it is `(2^64 mod n) / 2^64`, which is 0 for power-of-two spans). |
| `float next_float01()` | Uniform in `[0, 1)`, 24-bit resolution. Never 1.0; 0.0 with probability 2^-24. |
| `std::uint64_t seed() const` | The construction seed (save/replay identity). |
| `std::uint64_t statePart1() const` | The first state word (the xorshift128+ `s0_`; M1-DET-03: feeds `World::stateHash`). Read-only, O(1), no side effects. |
| `std::uint64_t statePart2() const` | The second state word (`s1_`). Same contract as `statePart1()`. |
| `Prng substream(std::uint32_t id) const` | `deriveSubstream(seed(), id)`; a fresh stream position, independent of this stream's current state. |
| `static Prng deriveSubstream(std::uint64_t seed, std::uint32_t id)` | The documented derivation: `Prng(seed + id * K)`. Composes (see Algorithm). |
| `static void seedState(std::uint64_t seed, std::uint64_t& part1, std::uint64_t& part2)` | Seed-to-state mapping; exposed for determinism verification and M1 save/replay (a saved stream is `(seed, part1, part2)`). |
Expand Down Expand Up @@ -174,9 +176,10 @@ system as `SystemContext::rng` (non-null in deterministic mode,
`nullptr` when disabled; see
[api/system_registry.md](system_registry.md)). The stream is advanced
in place during the system's draws, so the registry's stream state is
the replay state (its inclusion in the state hash lands with
M1-DET-03). Scope and guarantees:
[concepts/determinism.md](../concepts/determinism.md).
the replay state: M1-DET-03's `World::stateHash` feeds each system's
substream `(seed, statePart1, statePart2)` into the hash (see
[api/replay.md](replay.md), the state-hash scope). Scope and
guarantees: [concepts/determinism.md](../concepts/determinism.md).

## Misuse warnings

Expand Down
Loading
Loading