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
42 changes: 42 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,48 @@ function(laige_apply_engine_policy target)
endif()
endfunction()

# ---------------------------------------------------------------------------
# SimMath pinned-math policy (M0-CORE-03; ADR 0002, PRD §10.3)
# ---------------------------------------------------------------------------
# Every target that carries deterministic sim math MUST be passed through
# laige_apply_simmath_policy(). In M0 that is laige-core (the SimMath
# home) and its tests; from M1 on, every sim module (laige-sim, and
# authoritative-sim code in laige-net/laige-server) joins the list.
#
# The flags implement ADR 0002's pinned set for the fp32_pinned backend
# (full rationale in src/laige-core/include/laige/sim_math.h):
#
# GCC / Clang / AppleClang:
# -ffp-contract=off a*b+c is never fused into a single-rounding
# FMA (protects SimMath::lerp / ::length's
# documented two-rounding)
# -fno-associative-math sums are never reassociated (already the
# default; passed explicitly so the pinned set
# is visible on every compile line)
# MSVC 2022:
# /fp:precise the documented precision model; MSVC does not
# FMA-contract C expressions and never
# reassociates at this setting (passed
# explicitly — it is the default — so the
# pinned set is visible on every compile line)
#
# Banned in sim translation units, all compilers: -ffast-math /
# -funsafe-math-optimizations / /fp:fast, floating-point intrinsics,
# rounding-mode changes, and FP exception modes (re-audited at every
# toolchain upgrade, ADR 0002).
function(laige_apply_simmath_policy target)
if(CMAKE_CXX_COMPILER_ID MATCHES "^(GNU|Clang|AppleClang)$")
target_compile_options(${target} PRIVATE
-ffp-contract=off -fno-associative-math)
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
target_compile_options(${target} PRIVATE /fp:precise)
else()
message(FATAL_ERROR
"laige: no simmath compiler policy for '${CMAKE_CXX_COMPILER_ID}'; "
"supported: GNU, Clang/AppleClang, MSVC 2022 (PRD §6).")
endif()
endfunction()

# ---------------------------------------------------------------------------
# Modules (PRD §10.1 module map)
# ---------------------------------------------------------------------------
Expand Down
162 changes: 162 additions & 0 deletions docs/api/sim_math.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# SimMath — the deterministic math interface (`laige::sim`)

The one math interface for deterministic simulation code (M0-CORE-03,
ADR 0002, PRD §10.3). Public header:
`src/laige-core/include/laige/sim_math.h`; pinned instantiation:
`src/laige-core/sim_math.cpp`. Engine math ops are the only
floating-point allowed in deterministic paths — never platform
intrinsics outside the engine (PRD §10.3). The *enforcement* of
"SimMath only" in sim code lands in M1-DET-01 (G-R8); this step ships
the API, the `fp32_pinned` backend, and the pinned flag set.

## Backends (ADR 0002)

SimMath has **one op surface** and **two backends**, selected **once per
engine/zone init** from game config (`determinism.math`) and dispatched
at compile time — one template instantiation per backend, no per-call
indirection (PERF-006):

| Backend | Config id | Storage | Determinism scope (ARCH-010) |
|---|---|---|---|
| `Fp32Pinned` (this step) | `"float_pinned_32"` | IEEE binary32 (`float`) | Bit-exact across runs of the same build on the same platform/ISA. Cross-ISA is **not** promised until the CI detcheck matrix proves it (M1-DET); a failing pair is declared unsupported for this backend. |
| `Fpx16_16` (M0-CORE-04) | `"fixed_point_16_16"` | Q16.16 in `int32_t`, `int64_t` intermediates | Bit-exact across all builds, platforms, ISAs, and compilers by the language standard. The default backend; required for lockstep and authoritative MMO. |

The **backend id is part of replay identity**: replay = inputs + seed +
math backend + config hash. Cross-backend replays are not bit-exact and
are not supported. The PRD §12.3 bandwidth degradation ladder is
defined on `fpx16_16`; `fp32_pinned` zones do not participate in it.

## Quick start

```cpp
#include <laige/sim_math.h>

// Once, at engine/zone init (factory-selected, ADR 0002):
const auto math = laige::sim::SimMathFp32::create();

// Sim hot path (fixed timestep, ARCH-002):
laige::sim::SimMathFp32::Vec2 pos{1.0f, 2.0f};
laige::sim::SimMathFp32::Vec2 vel{0.5f, -0.25f};
pos = math.add(pos, math.mul(vel, 1.0f / 60.0f)); // one tick
pos = math.clamp(pos, laige::sim::SimMathFp32::Vec2{0.0f, 0.0f},
laige::sim::SimMathFp32::Vec2{64.0f, 64.0f});
vel = math.normalize(vel);
```

Game and system code is written once against the interface — there is no
per-game double code path (ADR 0002).

## The op surface

All ops are static and inline on the stateless `SimMath<Backend>` —
O(1), no allocation, `noexcept`, zero per-call indirection (PERF-006).
`Vec2`/`Vec3` are trivially copyable value types for dense sim state
(PERF-004); default-initialized to the zero vector.

| Op | Exact expression / policy |
|---|---|
| `add` / `sub` / `mul` / `div` (scalar, `Vec2`) | One IEEE binary32 operation per component, round-to-nearest-even (pinned — see below). `mul(v, v)` is component-wise; `mul(v, s)` / `mul(s, v)` is scaling. |
| `less` / `lessEqual` / `greater` / `greaterEqual` (scalar) | Ordered IEEE comparisons: false when either operand is NaN. |
| `equals` / `notEquals` (scalar, `Vec2`, `Vec3`) | IEEE: `equals` is false when either operand is NaN; `notEquals` is true. Vector forms are component-wise. |
| `isNaN` / `isInf` / `isFinite` / `isOrdered` (scalar) | IEEE classifications; `isOrdered(a,b)` is true iff neither is NaN (the `totalOrder` predicate, not the negation of arithmetic `!=`). |
| `clamp(x, lo, hi)` (scalar, `Vec2`) | Requires finite `lo`/`hi` and `lo <= hi` (debug-assert; release UB — the engine's Result/Status convention). NaN x → NaN; ±inf x → the bound. |
| `lerp(a, b, t)` (scalar, `Vec2`) | Exactly `a + (b - a) * t`: one sub, one mul, one add — **two roundings, never FMA-fused**. Not interchangeable with `a*(1-t) + b*t` (different rounding; replays diverge). t outside [0,1] extrapolates by the same expression (defined). |
| `length(v)` (`Vec2`, `Vec3`) | Exactly `sqrt(x*x + y*y [+ z*z])`: the component squarings, the adds, then one correctly-rounded sqrt (SSE2 vsqrtss / NEON vsqrt), in that order. Always ≥ 0; `length((0,0)) = +0`. |
| `normalize(v)` (`Vec2`, `Vec3`) | `v / length(v)`, component-wise IEEE division. The zero vector is defined to normalize to the zero vector — SimMath never injects NaN from a zero-length input (IEEE 0/0 would give NaN). NaN/inf components propagate per IEEE (an infinite vector can normalize to NaN components). |
| `create()` | The factory form of backend selection (stateless; holding the handle is free). |

## fp32_pinned NaN/Inf policy

Defined, not "whatever the CPU does" (full text in the header):

- **Arithmetic** (`add`/`sub`/`mul`/`div`, `sqrt`): IEEE-754 binary32,
round-to-nearest-even. A NaN operand yields NaN; 0/0 = NaN; x/0 = ±inf
(no trap); inf − inf = NaN; inf × 0 = NaN; x/inf = ±0.
- **Comparisons:** ordered — false when either operand is NaN;
`notEquals` is true when either is NaN; NaN is not equal to itself.
- **`clamp`:** NaN in → NaN out; ±inf in → the corresponding bound.
- **`lerp` / `length`:** NaN in → NaN out; the exact pinned expressions
above.
- **`normalize`:** zero vector → zero vector (policy); NaN/inf propagate
per IEEE.
- **Signed zero** follows IEEE: `-0.0f` is representable,
`-0.0f == +0.0f` is true, `0 + -0 = +0`, `-1 * 0 = -0`.
- **No op may signal an FP exception or trap** on a P0 target; no flush
to zero (denormals are first-class: e.g. `sqrt(denorm_min)` is a
finite denormal).

## fp32_pinned pinned flag set (ADR 0002)

The bit-exact promise holds only if every compiler emits the same
operation sequence. The pinned set is applied by
`laige_apply_simmath_policy()` (root `CMakeLists.txt`) to every target
that carries deterministic sim math — today `laige-core` and its tests;
from M1 on, every sim module (`laige-sim`, and authoritative-sim code in
`laige-net`/`laige-server`):

| Compiler | Flags | Why |
|---|---|---|
| GCC / Clang / AppleClang | `-ffp-contract=off -fno-associative-math` | Never fuse `a*b+c` into a single-rounding FMA (protects `lerp`'s and `length`'s documented two-rounding); never reassociate sums (already the default; passed explicitly so the pinned set is visible on every compile line). |
| MSVC 2022 | `/fp:precise` | MSVC does not FMA-contract C expressions and never reassociates at this setting (passed explicitly — it is the default — so the pinned set is visible on every compile line). |

Banned in sim translation units, all compilers (re-audited at every
toolchain upgrade, ADR 0002):

- `-ffast-math` / `-funsafe-math-optimizations` / `/fp:fast` — they
enable reassociation, reciprocal math, and FMA contraction, and remove
the NaN/Inf guarantees above.
- Floating-point intrinsics (`__builtin_*`, `_mm_*`, `_Float*`, FPU
intrinsics) outside `sim_math.h`.
- Rounding-mode changes (`fesetround`) and FP exception modes (`FE_*`,
`SetErrorMode`): the pinned mode is round-to-nearest-even, the default
on every P0 target.

The runtime FMA canaries in `tests/laige-core/math_float_tests.cpp`
(`DotProductCanaryDetectsFmaContraction`, `LerpIsNotFmaFused`) fail
loudly if the flags are ever missing — verified by a negative build with
`-ffp-contract=fast -mfma`.

## Determinism scope (ARCH-010)

`fp32_pinned` is bit-exact across **runs of the same build on the same
platform/ISA**. Cross-ISA bit-exactness (x86-64 vs arm64) is not
promised: it holds only if the compilers emit the same operation
sequence, which is defended by the pinned set and re-audited at every
toolchain upgrade. The M1-DET detcheck matrix generates the
per-platform support list; any desyncing pair is declared unsupported
for this backend. `fpx16_16` (M0-CORE-04) is the cross-ISA default and
the required backend for lockstep (AC-10.3) and authoritative MMO.

## Performance (DOC-004)

- **Complexity:** every op is O(1); no loops, no recursion.
- **Allocation:** none (value types, inline calls).
- **Indirection:** none — `SimMath<Backend>` is a stateless template;
ops are static inline with one instantiation per backend (PERF-006:
no virtual dispatch, no `std::function`, no `std::map`, no locks).
- **Budget:** both backends must meet the §8.1 sim budget (10k entities
@ 60 Hz ≤ 3 ms); measured in M1 (CORE-001).
- **Trap:** computing the same math two different ways (e.g.
`lerp(a,b,t)` vs `a*(1-t)+b*t`, or reordering a sum) produces
different bits and breaks replays. Use one pinned expression per
quantity and keep it that way.

## Misuse warnings

- Raw `float` operators in deterministic sim code bypass SimMath and
break the determinism contract (PRD §10.3). Enforcement: M1-DET-01.
- Replays across backends are not bit-exact: the backend id is part of
replay identity (ADR 0002).
- `clamp`'s `lo`/`hi` must be finite and ordered; NaN/Inf bounds are
undefined behavior in release builds (debug builds assert).
- A translation unit that instantiates `SimMath<Fp32Pinned>` must carry
the pinned flag set (`laige_apply_simmath_policy`); otherwise the
bit-exact promise for that TU is void.

## Verification

`ctest -R math_float` — suites `SimMathBasics` (bit-exact known values,
±0, commutativity, vector ops), `SimMathNanInf` (the full NaN/Inf
policy), `SimMathProperties` (idempotence, round-trip tolerances, the
pinned-flag runtime canaries), `SimMathDispatch` (stateless
compile-time dispatch, `noexcept` contract, backend contract).
33 changes: 31 additions & 2 deletions docs/getting-started/building.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,26 @@ Every engine target is passed through `laige_apply_engine_policy()`
flags do not (CPP-010 — a game linking the engine keeps its own compiler
policy).

## SimMath pinned-math policy (M0-CORE-03, ADR 0002)

Every target that carries deterministic sim math is passed through
`laige_apply_simmath_policy()` (root `CMakeLists.txt`) — in M0 that is
`laige-core` and the `laige-core_tests` executable; from M1 on, every
sim module joins the list (e.g. `laige-sim`). The flags pin the
IEEE float semantics of the `fp32_pinned` backend
(`src/laige-core/include/laige/sim_math.h` is the source of truth for the
pinned set and the NaN/Inf policy):

- GCC/Clang/AppleClang: `-ffp-contract=off -fno-associative-math`
(no FMA contraction of `a*b+c`, no reassociation — the pinned set is
visible on every compile line).
- MSVC 2022: `/fp:precise` (MSVC does not FMA-contract C expressions and
never reassociates at this setting).
- Banned in sim translation units: `-ffast-math` /
`-funsafe-math-optimizations` / `/fp:fast`, floating-point
intrinsics, rounding-mode changes, FP exception modes (re-audited at
every toolchain upgrade, ADR 0002).

## Current status (M0)

- `laige-core` builds as a static library (default) or a shared library
Expand All @@ -125,10 +145,15 @@ Every engine target is passed through `laige_apply_engine_policy()`
engine code from M0-CORE-01: `laige::Result<T,E>` / `laige::Status` and
the error-code registry (`include/laige/result.h`,
`include/laige/errors.h`, `errors.cpp`; error text follows the NFR-13.3
5-field grammar — see [docs/api/errors.md](../api/errors.md)), and the
5-field grammar — see [docs/api/errors.md](../api/errors.md)), the
structured logging facade from M0-CORE-02 (`include/laige/logging.h`,
`logging.cpp`; API contract in
[docs/api/logging.md](../api/logging.md)).
[docs/api/logging.md](../api/logging.md)), and the SimMath
deterministic-math interface with the `fp32_pinned` backend from
M0-CORE-03 (`include/laige/sim_math.h`, `sim_math.cpp`; API contract
and NaN/Inf policy in
[docs/api/sim_math.md](../api/sim_math.md), pinned flags via
`laige_apply_simmath_policy()`).
- `tests/laige-core/laige-core_tests` is a CTest link smoke test (a
GoogleTest suite since M0-DEP-01) that runs in every build tree above: it
verifies the static/shared link and checks the NFR-8.10 policy flags with
Expand All @@ -142,6 +167,10 @@ Every engine target is passed through `laige_apply_engine_policy()`
`LogRateLimit`, `LogFatal`, `LogCrash`, `LogConcurrency`, and
`LogPerformance` suites — the step's Verify command is
`ctest -R logging`.
- `math_float` is the M0-CORE-03 CTest entry: a filtered view of the same
executable covering the `SimMathBasics`, `SimMathNanInf`,
`SimMathProperties`, and `SimMathDispatch` suites — the step's Verify
command is `ctest -R math_float`.
- Every configure verifies the vendored dependency lock
(`cmake/laige-deps-lock.cmake` against `deps.lock`); a tampered or
unlisted file under `deps/` fails the configure loudly. GoogleTest is the
Expand Down
2 changes: 1 addition & 1 deletion roadmap/M0-foundations.md
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ No rendering, no physics, no networking yet — `laige-core` only.
tests prove the step's Verify clauses — zero-alloc, rate-limit
summary, Fatal termination — cohesive, not split)

- [ ] **M0-CORE-03 · SimMath interface + `fp32_pinned` backend**
- [x] **M0-CORE-03 · SimMath interface + `fp32_pinned` backend**
- **Refs:** PRD §10.3, S-7; ADR 0002 (`fp32_pinned` backend); AGENTS CORE-005
- **Depends:** M0-DEC-02, M0-CORE-01
- **Scope:**
Expand Down
11 changes: 9 additions & 2 deletions src/laige-core/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,20 @@
# smoke test in tests/laige-core/ verifies the library in both variants.

if(LAIGE_BUILD_SHARED)
add_library(laige-core SHARED version.cpp errors.cpp logging.cpp)
add_library(laige-core SHARED version.cpp errors.cpp logging.cpp
sim_math.cpp)
else()
add_library(laige-core STATIC version.cpp errors.cpp logging.cpp)
add_library(laige-core STATIC version.cpp errors.cpp logging.cpp
sim_math.cpp)
endif()

laige_apply_engine_policy(laige-core)

# M0-CORE-03: laige-core carries the SimMath deterministic-math interface
# (include/laige/sim_math.h); pin its IEEE float semantics (ADR 0002,
# PRD §10.3). Every later sim module passes through this same policy.
laige_apply_simmath_policy(laige-core)

# Public header root. Only this directory is exposed to consumers
# (CPP-010: no transitive leakage).
target_include_directories(laige-core PUBLIC
Expand Down
15 changes: 15 additions & 0 deletions src/laige-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,21 @@ Status: M0 — Foundations.
value) and `ErrorCode::IoError` (`5`) were added as small additive
extensions of M0-CORE-01 to support the FileSink→facade hand-off. API
contract: `docs/api/logging.md`; unit suite: `ctest -R logging`.
- **M0-CORE-03 (done):** SimMath — the single deterministic-math
interface (ADR 0002, PRD §10.3) with the `fp32_pinned` backend:
`laige::sim::SimMath<Backend>` (add/sub/mul/div, ordered comparison
+ IEEE NaN/Inf predicates, clamp, lerp, normalize, length over
scalar/`Vec2`/`Vec3`) and `laige::sim::Fp32Pinned`
(`include/laige/sim_math.h`, pinned instantiation in `sim_math.cpp`).
The pinned flag set of ADR 0002 (`-ffp-contract=off
-fno-associative-math` GCC/Clang/AppleClang, `/fp:precise` MSVC) is
applied by `laige_apply_simmath_policy()` to `laige-core` and the
test targets; the NaN/Inf policy is documented in the header and
tested (incl. runtime FMA canaries). API contract:
`docs/api/sim_math.md`; unit suite: `ctest -R math_float`. This step
also fixed a latent CTest filter bug (quoted `--gtest_filter` args
silently under-ran the `result_status`/`logging` Verify suites —
see `tests/laige-core/CMakeLists.txt`).
- Further public headers land with each M0-CORE-xx step.

Canonical build commands:
Expand Down
Loading
Loading