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
17 changes: 14 additions & 3 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ enforcement; M1-LOOP-01: the fixed-timestep game loop core;
M1-LOOP-02: the per-tick presentation snapshot + interpolation
state; M1-HEAD-01: the headless engine run — `Engine`
(config → world → systems → loop) and the `laige-run` binary;
M1-CFG-01: the declarative game config — the version 1
`config.json` schema (tick rate, budgets, camera defaults, asset
roots, determinism block), `loadGameConfig`, the programmatic
override merge, and the debug-only hot reload of non-simulation
keys (`laige/sim/config.h`);
M1-DET-01: deterministic mode — the SimMath-only sim guarantee
(the G-R8 compile-time trait + the CI source scan), the per-system
PRNG substreams, and the `seed`/`determinism` config keys;
Expand Down Expand Up @@ -112,9 +117,14 @@ still to land.
- [Headless engine run](api/engine.md) — `laige::Engine`
(config → world → systems → loop): `run_headless(maxTicks)` the
bounded + server run forms, the ordered idempotent CONC-006
shutdown, the provisional config surface (now including the
`seed`/`determinism` keys, M1-DET-01), the backend selection at
init, and the `laige-run` CLI (M1-HEAD-01; `laige-sim` + `tools/run`).
shutdown, the backend selection at init, and the `laige-run`
CLI (M1-HEAD-01; `laige-sim` + `tools/run`).
- [Declarative game config](api/config.md) — the version 1
`config.json` schema (the required `version` key, tick rate,
budgets, camera defaults, asset roots, the determinism block),
`EngineConfig`, `loadGameConfig`, the `EngineConfigOverride`
merge, and the debug-only `ConfigHotReloader` (M1-CFG-01;
`laige-sim`).
- [Determinism-safe storage](api/determinism.md) — the G-R8
compile-time trait: `SimMathBackend`, `DeterminismConfig`,
`detail::IsDeterminismSafe<T>`, and
Expand Down Expand Up @@ -232,6 +242,7 @@ still to land.
[game_loop.md](api/game_loop.md),
[presentation.md](api/presentation.md),
[engine.md](api/engine.md),
[config.md](api/config.md),
[determinism.md](api/determinism.md),
[replay.md](api/replay.md).)

Expand Down
325 changes: 325 additions & 0 deletions docs/api/config.md

Large diffs are not rendered by default.

108 changes: 42 additions & 66 deletions docs/api/engine.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@ it down cleanly. It is the first full-stack surface of the engine and
the CI smoke-test target (`laige-run --headless`, this repo's `tools/run`).

Public header: `src/laige-sim/include/laige/sim/engine.h` (`Engine`,
`EngineConfig`, `parseEngineConfig`, the range constants, the full
contract); implementation: `src/laige-sim/engine.cpp`. CLI:
`tools/run/laige-run.cpp` (target `laige-run`). Unit suite:
`ctest -R engine` (`tests/laige-sim/engine_tests.cpp`); smoke test:
the range constants, the full contract); implementation:
`src/laige-sim/engine.cpp`. The config surface (`EngineConfig`,
`loadGameConfig`, the override merge, the hot reloader) is
`src/laige-sim/include/laige/sim/config.h` — see
[api/config.md](config.md). CLI: `tools/run/laige-run.cpp` (target
`laige-run`). Unit suite: `ctest -R engine`
(`tests/laige-sim/engine_tests.cpp`) and `ctest -R config`
(`tests/laige-sim/game_config_tests.cpp`); smoke test:
`ctest -R laige_run_smoke` (every P0 OS job, 1000 ticks @ 60 Hz).

```cpp
Expand Down Expand Up @@ -68,69 +72,41 @@ leaks the world; the explicit call is the documented teardown (it
retires the logging facade, which `Logger::init` re-arms for a later
engine in the process).

## The config surface (provisional)

`EngineConfig{tickRateHz, entityCapacity, churnPerFrameBudget, seed,
determinism}` and `parseEngineConfig(const JsonValue&)` are the
**provisional** config surface for M1-HEAD-01. M1-CFG-01 owns the final
versioned config schema (PRD §10, ARCH-007: persistent data MUST be
versioned); when M1-CFG-01 lands, the JSON parse moves behind its
versioned reader and this surface is folded into it. The provisional
keys:

| key | type | range | default |
|---|---|---|---|
| `tick_rate_hz` | exact integer | 20–120 | `kDefaultTickRateHz` (60) |
| `entity_budget` | exact integer | 0–65536 | `0` (an empty scene — a valid world that creates no entities; entity creation on it fails `BudgetExhausted`) |
| `churn_per_frame_budget` | exact integer | 0–4294967295 | `kDefaultChurnPerFrameBudget` (256) |
| `seed` | exact integer | 0–2^53 (JSON) / 0–2^64−1 (struct) | `kDefaultSimulationSeed` (0) |
| `determinism` | object (below) | — | `{enabled: true, math: "fixed_point_16_16"}` |

The `determinism` object (M1-DET-01; see
[concepts/determinism.md](../concepts/determinism.md) for the scope
and [api/determinism.md](determinism.md) for the types):

| nested key | type | range | default |
|---|---|---|---|
| `determinism.enabled` | bool | — | `true` |
| `determinism.math` | string | `"fixed_point_16_16"` \| `"float_pinned_32"` | `"fixed_point_16_16"` |

## The config surface (M1-CFG-01: the final versioned schema)

`EngineConfig` and the JSON/file loaders now live in
[api/config.md](config.md) (`laige/sim/config.h`): the **version 1**
declarative schema (the REQUIRED `version` key, ARCH-007), the
`tick_rate_hz` / `entity_budget` / `churn_per_frame_budget` / `seed`
/ `determinism` keys, and the declared presentation blocks
(`budgets`, `camera`, `asset_roots` — stored now, consumed in M2).
The provisional M1-HEAD-01 surface was folded into it: the five
original `EngineConfig` members keep their order (existing aggregate
initializers compile unchanged), the provisional JSON keys carried
over, and the migration from a provisional document is adding
`"version": 1`.

What the engine consumes directly:

- `Engine::create(config)` re-validates the typed config's tick rate
(the config surface's rejection, `config/tick_rate_invalid`,
subsystem `config`) — the struct is public, so a hand-built
out-of-range config is rejected identically.
- The **seed is part of replay identity** (ADR 0002) and is logged on
`engine/run_started`. In JSON it is bounded to `2^53` because ADR
0003 stores numbers as doubles (exact to 2^53); the programmatic
`EngineConfig.seed` is the full `uint64_t`. A seed above the JSON
bound, a non-integer, or a negative is rejected
(`config/seed_invalid`).
- `enabled` selects deterministic mode (per-system PRNG substreams,
the replay promise); `false` is the documented escape hatch
(no substreams, `SystemContext.rng == nullptr`). `math` selects the
SimMath backend the engine registers (the built-in component and the
presentation snapshot).

- **Unknown keys** are ignored with one rate-limited
`config/unknown_key` warn per key (forward-compatible with
M1-CFG-01's additions; LOG-004).
- **Rejections** (first failure wins, one rate-limited warn each):
`config/not_an_object` (document is not a JSON object),
`config/tick_rate_invalid` (absent/out of range/non-integer),
`config/entity_budget_invalid`, `config/churn_budget_invalid`,
`config/seed_invalid` (non-integer / out of range / above the 2^53
JSON bound / wrong type), `config/determinism_invalid` (not an
object), `config/determinism_enabled_invalid` (not a bool),
`config/determinism_math_invalid` (not one of the two backend ids) —
each maps to `ErrorCode::InvalidArgument` (NFR-13.3 grammar:
`{codeId}|{what}|{why}|{fix}|{docAnchor}`, see `errors.md`). An
**unknown key inside `determinism`** is not a rejection: it warns
(`config/unknown_key`) and is ignored, like the top-level unknown-key
rule (forward-compat with M1-CFG-01).
- `Engine::create` re-validates the `EngineConfig` struct itself (the
struct is public; the JSON path is not the only constructor), so a
hand-built out-of-range config is rejected identically.

`parseEngineConfig` is a cold path (O(document keys); it allocates
only for the warn fields when a key is rejected) and is the only
place the JSON document is read — the `EngineConfig` struct is the
value the engine consumes.
`engine/run_started`; `determinism.enabled` selects deterministic
mode and `determinism.math` the SimMath backend the engine
registers (see [concepts/determinism.md](../concepts/determinism.md)
for the scope and [api/determinism.md](determinism.md) for the
types).
- The replay identity's `configHash` covers only the
simulation-affecting fields — the declared presentation values
(budgets/camera/asset roots) are excluded, so the hash encoding is
unchanged by the final schema ([api/replay.md](replay.md)).

The full key table, the versioning rules, the rejection table, the
`loadGameConfig` file loader, the `EngineConfigOverride` merge, and
the debug-only `ConfigHotReloader` are in
[api/config.md](config.md).

## The run contract (`run_headless`)

Expand Down
10 changes: 7 additions & 3 deletions docs/api/replay.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,13 @@ no wall clock (ARCH-010):
differently. O(types), stack-only (769 words max — no allocation).
- **`configHash(const EngineConfig&)`** — over
`[tag 1, tickRateHz, entityCapacity, churnPerFrameBudget, seed,
determinism.enabled, determinism.math]`. The tag word identifies this
provisional encoding; M1-CFG-01 refines the config schema and this
encoding with it, under the format's versioning.
determinism.enabled, determinism.math]`. The tag word identifies
this encoding. M1-CFG-01 grew the config schema (version, budgets,
camera, asset_roots), but the encoding covers **only the
simulation-affecting fields** — the declared presentation values
never touch simulation (ARCH-009) — so the encoding (tag 1) is
**unchanged** and every committed baseline/replay log stays valid
([api/config.md](config.md)).
- **`makeReplayIdentity(const World&, const EngineConfig&)`** —
assembles the header's `ReplayIdentity` from the two hashes plus the
config's seed/tick rate/backend.
Expand Down
29 changes: 19 additions & 10 deletions docs/concepts/determinism.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,9 +175,10 @@ Disabling determinism is an escape hatch for non-deterministic
prototypes, not a different math policy: the same SimMath ops, the same
storage rules — only the PRNG and the replay promise are switched off.

## The config surface (provisional)
## The config surface (M1-CFG-01: the final versioned schema)

`EngineConfig` (laige/sim/engine.h) carries the two keys:
`EngineConfig` (laige/sim/config.h — moved there by M1-CFG-01)
carries the two keys:

```cpp
struct EngineConfig {
Expand All @@ -186,20 +187,28 @@ struct EngineConfig {
std::uint32_t churnPerFrameBudget{...};
std::uint64_t seed{laige::kDefaultSimulationSeed}; // 0..2^64-1 (programmatic)
DeterminismConfig determinism{}; // {enabled, math}
BudgetsConfig budgets{}; // M1-CFG-01 declared values
CameraConfig camera{}; // M1-CFG-01 declared values
std::vector<std::string> assetRoots{}; // M1-CFG-01 declared values
};
```

- `DeterminismConfig { bool enabled{true}; SimMathBackend math{FixedPoint16_16}; }`
with `SimMathBackend::FixedPoint16_16` (id `fixed_point_16_16`, the
default) and `SimMathBackend::FloatPinned32` (id `float_pinned_32`).
- The JSON surface is **provisional** (M1-HEAD-01): `parseEngineConfig`
accepts `{"seed": 0..2^53, "determinism": {"enabled": bool,
"math": "fixed_point_16_16"|"float_pinned_32"}}`. The seed is bounded
to `2^53` in JSON because ADR 0003 stores numbers as doubles (exact to
2^53); the programmatic `EngineConfig.seed` is the full `uint64_t`.
Unknown nested keys warn (`config/unknown_key`) and are ignored — the
forward-compat rule. **M1-CFG-01 owns the final config schema**; these
keys land on the provisional surface until then.
- The JSON surface is the **version 1 schema** (M1-CFG-01,
`laige::parseEngineConfig` / `laige::loadGameConfig` — see
[api/config.md](../api/config.md)): `{"version": 1, "seed": 0..2^53,
"determinism": {"enabled": bool,
"math": "fixed_point_16_16"|"float_pinned_32"}, ...}`. The seed is
bounded to `2^53` in JSON because ADR 0003 stores numbers as doubles
(exact to 2^53); the programmatic `EngineConfig.seed` is the full
`uint64_t`. Unknown keys (any level) warn (`config/unknown_key`)
and are ignored — the forward-compat rule. Both the seed and the
determinism block are simulation-affecting: they are part of the
replay identity's `configHash` and are hot-reload-refused (the
declared presentation values — budgets, camera, asset_roots — are
not).
- The engine selects the backend once at init (compile-time dispatch,
ADR 0002): it registers the matching built-in component
(`Position2DFpx16` or `Position2DFp32`) first and builds the
Expand Down
11 changes: 7 additions & 4 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,13 @@ the standard ctest suite in every P0 job (and both sanitizer trees):
per-system PRNG substreams match `Prng::deriveSubstream` exactly and
are independent; `deterministic == false` yields `SystemContext.rng ==
nullptr`; the engine selects the configured SimMath backend (built-in
component + presentation snapshot); and the provisional `seed` /
`determinism` config keys (defaults, valid values, the rejection
table). The machine-greppable `determinism-tick-stream` line lands in
the ctest output.
component + presentation snapshot); and the `seed` / `determinism`
config keys (defaults, valid values, the rejection table — the
version 1 schema's determinism block, M1-CFG-01). The
machine-greppable `determinism-tick-stream` line lands in the ctest
output. (The full config schema — version gate, budgets, camera,
asset roots, overrides, hot reload — is `ctest -R config`,
`tests/laige-sim/game_config_tests.cpp`.)
- **`ctest -R trait_compile`** — the G-R8 trait compile-checks
(`tests/laige-sim/compile_fail/`, generated `cmake -P` check scripts):
one positive fixture (a marked determinism-safe component compiles)
Expand Down
Loading
Loading