diff --git a/docs/README.md b/docs/README.md index 73e565a..698633a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -212,7 +212,7 @@ still to land. - [ADR index](decisions/README.md) — 0001 (name and license), 0002 (deterministic math), 0003 (config JSON), 0004 (GoogleTest - vendoring). + vendoring), 0005 (iso default: 2:1 dimetric). ## Compatibility diff --git a/docs/decisions/0005-iso-default.md b/docs/decisions/0005-iso-default.md new file mode 100644 index 0000000..95eb085 --- /dev/null +++ b/docs/decisions/0005-iso-default.md @@ -0,0 +1,143 @@ +# ADR 0005 — Default isometric projection for new projects: 2:1 dimetric + +- **Status:** Accepted +- **Date:** 2026-09-25 +- **Decider:** Roadmap step M2-DEC-01 (D-ISO) +- **Refs:** PRD v0.2 note, §4 (projection list), §15 (M2 exit), §18 item 8; + FR-2.4, FR-2.5, FR-2.11, AC-4.1, AC-4.4, NFR-13.5; roadmap D-ISO, M2-PROJ-01, + M2-CAM-02, M2-ISO-01, M2-SCENE-01, M2-SAMPLE-01, M2-AC-01 + +## Context + +PRD v0.2 makes isometric the **primary projection**: the default template, the +reference scene for all visual/performance acceptance tests, and the focus of +new first-class requirements (depth keys, picking, grid-snap camera). PRD §18 +item 8 (roadmap decision D-ISO) leaves one question open: which isometric +variant is the **template default** — 2:1 dimetric (the pixel-art standard) or +true 30°/60° isometric. Both must remain selectable per scene (FR-2.5). + +The PRD already points one way, and this ADR confirms it rather than +re-deciding from scratch: + +- §4: "Isometric (primary): 2:1 dimetric (pixel-art default) and true 30°/60° + isometric, plus arbitrary shear per scene." +- FR-2.5: "Isometric (primary; **2:1 default**, true iso, custom shear)." +- Roadmap D-ISO register row, default if unresolved: "2:1 dimetric + (PRD v0.2: pixel-art default)". +- Downstream M2 steps already assume it: M2-CAM-02 ("2:1 dimetric (default + per D-ISO)"), M2-SAMPLE-01 (`iso-arena.laige`, "2:1 dimetric default (per + D-ISO)"), AC-4.4 ("isometric is the default for the project template"). + +Both candidates are oblique presentations of the same axis-aligned 2D +simulation (PRD §4: no oblique simulation); they differ only in the camera +constants: + +| | 2:1 dimetric (pixel-art standard) | True 30°/60° isometric | +|---|---|---| +| Construction | Affine: 45°-azimuth oblique view with the vertical squashed to ½ — the two ground axes are equally foreshortened, the vertical differs ("di"metric) | True orthographic axonometric: 45° azimuth, 35.264° elevation (`arcsin 1/√3`) — all three axes equally foreshortened | +| Screen delta per unit ground step | `(±2, 1)·k` — **integer** | `(±√3, 1)·k` — irrational | +| Ground-axis angle to screen horizontal | 26.565° (`arctan ½`) | 30° (hence "30°/60°": axes at 30°, tile corner at 60°) | +| Ground-axis foreshortening | `√2/2 ≈ 0.707` | `√(2/3) ≈ 0.816` | +| Vertical foreshortening | ½ | `√(2/3) ≈ 0.816` | +| Grid corners at 1× zoom | integer pixel positions | sub-pixel positions (√3 factor) | + +The engine machinery is preset-independent: both variants are affine maps +with both world axes projecting **downward** on screen and a positive +`(x + y)` screen-y contribution, so the axis-aligned depth key +`f(x, y, tile height, layer)` (FR-2.2; exact 32-bit form is M2-ISO-01) and the +O(1) picking inverse (FR-2.11; M2-ISO-03) work for both — only the constants +differ. Both presets are one oblique camera matrix built once per scene +(M2-GL-03 / M2-CAM-02); there is **no per-frame cost difference** between +them. The choice is aesthetic and workflow, not a capability or performance +question. + +## Decision + +- **The default projection for new projects and templates is 2:1 dimetric.** + The engine's default scene config selects it: projection mode `iso` + (already the default per PRD v0.2) with iso preset `dimetric_2_1`. +- **Both presets remain selectable per scene/view** (FR-2.5, the AC-4.1 + config-only pattern): the scene config carries one iso-preset selector with + the values `dimetric_2_1` (**default**), `true_iso_30_60`, and a custom + shear (the two axis screen-deltas, per FR-2.5's "arbitrary shear per + scene"; M2-CAM-02 owns the exact config key and the matrix builders from + M2-GL-03). A game that wants true 30°/60° changes only its config — no + simulation, depth-key, picking, or asset change. +- The default template (`iso-arena.laige`, NFR-13.5) and the M2-SCENE-01 + reference scene render in 2:1 dimetric; M2-AC-01 asserts the engine default + (AC-4.4) and M2-EXIT-01 checks the template config. + +Rationale: + +1. **Pixel-art standard.** Laige's flagship genre is the isometric ARPG + (PRD §3, §15) and the engine's default art paths (bitmap glyphs, tilemaps, + the reference scene) are pixel-art-oriented; 2:1 dimetric is the standard + pixel-art isometric projection. +2. **Integer grid↔screen mapping at 1× zoom.** `(±2, 1)·k` maps integer grid + coordinates to integer pixel positions at 1× — no sub-pixel shimmer, no + anti-aliasing ambiguity, and clean pick-boundary behavior (M2-ISO-03's + documented boundary rule). True iso's `√3/2` factor puts every grid corner + at an irrational position, which degrades the pixel-art workflow at 1×. +3. **Zero reversal cost.** The PRD already designates 2:1 as the pixel-art + default (§4, FR-2.5) and the roadmap register's unresolved default is 2:1; + confirming costs nothing, while making true iso the default would invert + the PRD's designation for a style that is not the flagship. +4. **No simulation impact.** The choice is presentation-only (ARCH-009): + simulation stays axis-aligned 2D, depth keys are computed from the + axis-aligned world, never screen space (PRD §4, FR-2.2), and replication + is unaffected. + +## Alternatives considered + +- **True 30°/60° isometric as the default** — the "photographic" iso look + common in non-pixel-art iso games. Rejected as the default: it inverts the + PRD v0.2/§4 designation, and its irrational projection factor degrades the + pixel-art workflow (sub-pixel shimmer at 1×). It remains fully available + per scene. +- **No default — make the user choose** — rejected: AC-4.4 and the M2 exit + criterion ("isometric is the default template") require a single + out-of-the-box look, and forcing the choice onto the majority case adds + friction where API-001 wants the obvious path to be the good one. +- **Arbitrary custom shear as the default** — rejected: it generalizes the + mechanism but defines no template look; custom shear remains the third + preset value. + +## Evidence + +- This is a design decision **confirming an already-stated PRD default**, not + a measured performance claim: both presets are one oblique camera matrix + with no per-frame cost difference (2:1's integer deltas are, if anything, + the cheaper constant set), so there is no performance differential to + measure (CORE-001). +- Normative references: PRD v0.2 note, §4, §15, §18 item 8; FR-2.5; AC-4.4; + NFR-13.5; roadmap D-ISO register row and the M2-CAM-02/M2-SAMPLE-01 scope + wording that D-ISO unblocks. +- Enforcement after this ADR: M2-CAM-02 implements the preset selector with + 2:1 dimetric as the default; M2-AC-01 asserts the engine default; M2-EXIT-01 + checks the template config (the "Verify: ADR exists" step gate is met by + this document). + +## Consequences + +- M2-PROJ-01 / M2-CAM-02 implement the preset selector with `dimetric_2_1` as + the engine default; M2-GL-03's matrix builders support all three preset + classes (2:1 dimetric, 30°/60°, custom shear) from the start — pure + functions, trivial cost. +- The M2-SCENE-01 reference scene and all golden references (M2-GOLD-01) are + pinned to 2:1 dimetric; changing the default later is a golden-referencing + change (review conditions below). +- New-project templates and the future editor's (M5) "new iso scene" default + use 2:1 dimetric. +- No changes to simulation, determinism, replication, or the depth-key + formula (ARCH-009, PRD §4). + +## Review conditions + +- Reopen (new ADR + PRD revision) if pixel-art isometric stops being the + flagship style and template demand measurably favors the other look. +- Reopen if 2:1 dimetric ever fails a render budget that true 30°/60° meets — + no mechanism is known for this (same matrix class, no per-frame cost + difference), but the budget suite (M2-PERF-01) would expose it. +- Any change of the default preset must regenerate and re-approve the + M2-GOLD-01 golden references in the same change (TEST-008), and re-run + M2-AC-01's engine-default assertion. diff --git a/docs/decisions/README.md b/docs/decisions/README.md index 5c00a4b..3f338ab 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -11,3 +11,4 @@ alternatives, evidence, consequences, and review conditions; the full | [0002](0002-deterministic-math.md) | Deterministic math strategy (SimMath) | Accepted | 2026-09-10 | | [0003](0003-config-json.md) | Config JSON strategy | Accepted | 2026-09-10 | | [0004](0004-google-test-vendoring.md) | Vendoring GoogleTest as the dev-only test dependency | Accepted | 2026-09-10 | +| [0005](0005-iso-default.md) | Default isometric projection for new projects: 2:1 dimetric (D-ISO) | Accepted | 2026-09-25 | diff --git a/roadmap/M2-rendering-2.5d.md b/roadmap/M2-rendering-2.5d.md index 4c63d54..23f6184 100644 --- a/roadmap/M2-rendering-2.5d.md +++ b/roadmap/M2-rendering-2.5d.md @@ -19,7 +19,7 @@ if M2 slips, and its status is recorded in M2-EXIT-01. ## Decisions -- [ ] **M2-DEC-01 · Confirm 2:1 dimetric as template default (D-ISO)** +- [x] **M2-DEC-01 · Confirm 2:1 dimetric as template default (D-ISO)** - **Refs:** PRD §18.8, v0.2 note - **Depends:** — - **Scope:** diff --git a/roadmap/README.md b/roadmap/README.md index 8998db5..c75ab34 100644 --- a/roadmap/README.md +++ b/roadmap/README.md @@ -140,7 +140,7 @@ Steps tagged `DEC` close these. ADRs live in `docs/decisions/`. | D-NAME | Engine name & license (MIT proposed) | M0-REPO-01 | keep "Laige", MIT | **Decided 2026-09-10:** keep "Laige" (*Legendary AI Game Engine*); MIT (ADR 0001) | | D-MATH | Fixed-point default for all deterministic paths, or float-pinned + FP only for lockstep | M1-DET-01, M3-PHYS-11 | Q16.16 default for deterministic mode (PRD §10.3 recommends it for lockstep/MMO) | **Decided 2026-09-10:** SimMath — config-selectable; `fpx16_16` default + `fp32_pinned` opt-in (ADR 0002) | | D-JSON | Config JSON: tiny in-engine parser vs vendored library | M0-CORE-07 | in-engine bounded parser (no new dep) | **Decided 2026-09-10:** in-engine bounded parser, no new dep (ADR 0003) | -| D-ISO | 2:1 dimetric vs true iso as template default | M2-CAM-02 | 2:1 dimetric (PRD v0.2: pixel-art default) | — open | +| D-ISO | 2:1 dimetric vs true iso as template default | M2-CAM-02 | 2:1 dimetric (PRD v0.2: pixel-art default) | **Decided 2026-09-25:** 2:1 dimetric as template default (ADR 0005); both presets selectable per scene | | D-UI | Confirm minimal retained UI widget set for M2 | M2-UI-01 | the FR-2.8 P0 list (panel/button/text/image/list/slider/input) | — open | | D-EDITOR | Editor embedded vs standalone | M5-ED-01 | standalone binary (FR-8.7) | — open | | D-LUA | Confirm Lua 5.4 (vs no scripting in 1.0, vs WASM) | M4-SCRIPT-01 | Lua 5.4, optional module, off by default | — open | @@ -157,7 +157,7 @@ Updated in the same PR that closes steps. "Done" = box checked + Verify green. |---|---|---|---| | M0 | 22 | 22 | ✅ complete (2026-09-13, M0-EXIT-01) | | M1 | 25 | 25 | ✅ complete (M1-EXIT-01, 2026-09-25) | -| M2 | 33 | 0 | ⬜ not started | +| M2 | 33 | 1 | ⬜ in progress | | M3 | 36 | 0 | ⬜ not started | | M4 | 12 | 0 | ⬜ not started | | M5 | 21 | 0 | ⬜ not started | @@ -165,7 +165,7 @@ Updated in the same PR that closes steps. "Done" = box checked + Verify green. | M7 | 15 | 0 | ⬜ not started | | M8 | 8 | 0 | ⬜ not started | | M9 | 6 | 0 | ⬜ proposals only | -| **Total** | **194** | **47** | | +| **Total** | **194** | **48** | | --- @@ -219,6 +219,7 @@ One line per completed (or split/renumbered) step. | 2026-09-24 | M1-ALLOC-01 | `4564029` / PR #50 | Zero sim-loop allocation assertion (G-R1, PRD §9.3, §8.1 `sim_heap_allocs` target 0; PERF-003, FR-12.3; M1-ALLOC-01 scope, nothing else): the allocation watch (new public header `src/laige-core/include/laige/alloc_watch.h` + `src/laige-core/alloc_watch.cpp`) — a process-wide heap-allocation counter behind strong global `operator new`/`new[]` (+ nothrow, + sized deletes) in laige-core, compiled into every non-sanitizer tree (`LAIGE_ALLOC_WATCH=1` PUBLIC on laige-core; the sanitizer trees degrade to inline no-ops with `allocWatchLive()` false — the established fallback: the leak-free sanitizer run + the pool reservation delta, M0-CORE-02/05 precedent): the armed-window model (`allocWatchArm()` = three relaxed stores — first-site, count, armed flag; `allocWatchRead()` = two relaxed loads → `AllocWatchReading{allocs, firstSite}`; the single-owner window, the sim owner thread, CONC-001; first-site semantics: the allocating call's own return address — `__builtin_return_address(0)` on GCC/Clang, `_ReturnAddress()` on MSVC — evaluated in the operator-new frame, the first offender winning via a relaxed CAS that fails once recorded); the attribution contract: `laige::detail::LoggingAllocationGuard` — the logging facade's emit path (the `LAIGE_LOG` macro block + `Logger::record`) marks its own heap work (the field value strings, the rate-state, the sink's message formatting) so it is not attributed to the sim loop's G-R1 window — the engine's documented in-tick degradations (a G-R5 `budget_overrun`/`budget_critical`, a replay `record_failed`, a guardrail warn) still log (NFR-13.3 5-field grammar, rate-limited, actionable) and never trip G-R1, while any other in-tick allocation (a system's local `std::vector`, engine storage growth) still fails at its call site; the per-tick check (`GameLoop::runOneTick`, `#if !NDEBUG`, game_loop.cpp): arm BEFORE the tick body (the frame's `beginFrame` + one `runSystems` dispatch + the attached profiler + the `onTick` hook + the replay recorder), read AFTER a completed tick (`status.ok()` — a failed tick is not checked, the profiler's "a failed tick is not recorded" contract): a nonzero count logs one `alloc/sim_tick_allocation` Error event (fields `tick`/`allocs`/`site`, NFR-13.3) and then fails the debug assert (FR-12.3: actionable, never silent) — the standing hot-path guardrail for every later sim/render step (roadmap README §6, "Global invariants"); release builds compile the whole check out (CPP-012) — an allocating tick degrades through the already-logged pool accounting (pool overflow, pools.md) and the per-frame `simAllocs` delta (profiler.md) instead, never a crash; tests: the new `ZeroAlloc` suite + `zero_alloc` CTest entry (added to the TSan property list) over the shared `laige-sim_tests` executable — the 10k-entity M1-ECS-07 workload through the `GameLoop` (700 direct warm-up ticks bring every archetype to its high water BEFORE the window; then 10k ticks at 60 Hz on the synthetic clock — `kTickNs = 16666667` = ceil(10⁹/60), the game_loop_tests constant; the floor 16666666 drifts off the exact due count over 10k ticks) — per-tick window reads 0 allocs (the engine's own arm resets the watch each tick), the reservation delta 0, rows/entity invariants, the FNV-1a visit checksum, and the machine-greppable `zero-alloc window:` line; a scratch system with a deliberate `std::vector` fails the tick assert — proven in a forked SIGABRT child (POSIX; `GTEST_SKIP` on Windows), the release/sanitizer branch running 5 clean ticks (the Verify clause's deliberate-then-revert scratch kept as the standing negative test — the violation lives in the test TU, never in engine code); the watch's first-site capture checked directly; the test-side counter shim moved to laige-core (`tests/**/logging_alloc_counter.h` wraps the watch; the `LAIGE_ALLOC_COUNTER` test definition is gated on the same trees as `LAIGE_ALLOC_WATCH`, so the probes and the engine's assertion always agree); `HeadlessFramePathAllocatesNothing` (engine_tests) now reads per-tick window semantics (the engine's three one-shot setup allocations land before the first arm); docs in the same change: `docs/api/alloc_watch.md` (new — the window model, the per-tick assertion, the attribution contract, release builds, scope, cost, threading, misuse, example) + `game_loop.md` (the zero-allocation section + the Performance cost line) + `profiler.md` cross-ref + the docs/README index; `laige-api.json` regenerated (820 symbols from 25 headers, api-real-tree green); local Verify: `ctest -R zero_alloc` green, the full canonical ctest 92/92, and 92/92 on build-release/build-shared/build-asan/build-tsan/build-clang, zero warnings on every tree, the determinism + include lints OK; CI (observed via the GitHub API): the PR ci-pull.yml run 35988075605 on c9587fa green — all 8 jobs passed (linux-gcc/clang/asan+UBSan/tsan each ctest 92/92, the determinism check + source scan, the include-graph lint + dependency count, the public API manifest drift); macOS/Windows jobs skipped (label-gated, default-Linux P0 selection) | | 2026-09-25 | M1-BENCH-01 | `—` | 10k-entity simulation-tick benchmark (PRD §8.1 ≤3.0 ms avg / ≤5 ms p99, ADR 0002, CORE-001; M1-BENCH-01 scope, nothing else): the `sim-tick` suite in `laige-bench` (`tools/bench/laige-bench.cpp` + CMake) — the PRD §8.1 "Simulation tick" workload: 10 000 entities at the 100% scene budget (capacity 10000, deterministic, seed `0x1F055EED` — the repo-wide test-seed convention), 2 000 dynamic bodies carrying the built-in `Position2D` plus the workload component `BenchVel` (a `{Vec2 v}`, `LAIGE_COMPONENT` + `LAIGE_DETERMINISM_SAFE` marks for both backends) and 8 000 bare entities; deterministic index-derived initial state (50×50 grid span — one body per cell of the first 2 000 cells, `x = (i/50)%50 − 25`, `y = i%50`; `vx ∈ -3..3`, `vy ∈ -5..5` units/tick — max coordinate magnitude 20 049 units over the 4 000-tick run, inside fpx16_16's ±32 768 Q16.16 range: no saturating-overflow edge in the measured window); two systems in registration order — `BenchMove` (1 ms declared budget; `pos += vel` through the active backend's `SimMath::add`, the ADR 0002 pinned op surface) and `BenchHash` (2 ms budget; the per-tick `World::stateHash(tick−1)` — the M1 determinism work made part of the measured tick) — driven at 60 Hz by the `GameLoop` on an EXACT synthetic clock (one due tick/frame: 16 666 667 ns — the M1-LOOP-01 exact integer due computation); one measured sample = one completed tick (beginFrame + the systems + the loop's bookkeeping; the presentation snapshot is excluded — ARCH-009); canonical shape `--runs=3000 --warmup=1000` (n=3000, histogram capacity 3000, no truncation); every measured tick in debug non-sanitizer builds passes the engine's OWN G-R1 per-tick zero-allocation assertion (M1-ALLOC-01 — an allocating tick aborts the run, so a PASS is a zero-alloc-asserted tick); tool changes (smallest complete change): `--math=` (the ADR 0002 backend selection; default fpx16; other suites ignore it), **repeatable** `--budget=` (one run gates EVERY named budget against the same histogram — each entry's `metric` picks its statistic; duplicate name = usage error; any FAIL exits 2), suite setup/teardown hooks (the stateless synthetic suite keeps nullptrs), and a compiler-id fix (Clang checked before GCC: Clang 22 defines `__GNUC__` and its `__VERSION__` is "Clang 22.1.8" — the old order mislabeled clang builds as "GCC Clang 22.1.8"; the id is now built from `__clang_major__/minor/patchlevel`, stable across Clang versions); CMake: `laige-bench` links `laige-sim` and gains `laige_apply_simmath_policy` (the TU carries deterministic sim math — ADR 0002 pinned flags), plus two CI gate entries `laige_bench_sim_tick_{fpx16,fp32}` (the canonical 3000/1000 shape, `--budget=sim_tick_avg --budget=sim_tick_p99`, `LAIGE_BUDGETS_PATH` = the repo root; EXCLUDED from the ASan/TSan trees — instrumentation inflates the tick's absolute cost: ASan+UBSan measures ≈1.64 ms/tick on this workload, 2.7× the canonical 0.60 ms, so a sanitizer measurement would measure the instrumentation, not the sim — the m1-profiler-cost precedent; those trees verify the workload's safety properties instead); the PRD §8.1 budget-regression gate is these entries inside every P0 job's full `ctest` (the CI perf lane, PRD §14 — no separate workflow step needed); **first recorded `measured` values in `budgets.json`** (the M0 convention's `0 = not yet measured` retired for the two entries — recorded as the WORSE of the two backends on the canonical Debug tree, since the budget covers both backends and the 10% regression band (methodology §5) stays conservative): `sim_tick_avg` (mean) **0.598405** ms, `sim_tick_p99` (p99) **0.615179** ms; **fourth baseline** `docs/benchmarks/baselines/m1-sim-tick.md` (full AGENTS §12 metadata, verbatim budget-checked runs + the ctest gate, the cross-tree/cross-compiler table, the interpretation — records that M1 measures the ECS-only slice of the §8.1 workload: kinematic movement + the state hash; the PRD's remaining tick content (physics, presentation) lands in M3/M4 and supersedes the baseline per methodology §4; the file also records the initial-state grid-shape fix folded into this PR — the first commit stacked the 2 000 bodies in one grid column instead of one-per-cell over the 50×50 span; re-measured on every tree (numbers moved <2%, both backends still PASS; the recorded numbers are the post-fix ones); docs updated in the same change (DOC-007): `docs/benchmarks/README.md` + `docs/benchmarks/baselines/README.md` (the stale "every entry still `measured: 0`" lines; the profiler-cost baseline entry that was never added), `docs/api/budget_harness.md` (the new CLI form, the sim-tick suite, the measured-field note, both suites' determinism); no new public API header — `laige-api.json` unchanged (api-real-tree green in the full-suite runs); local Verify (2026-09-25, AMD Ryzen 9 7950X3D / CachyOS): canonical Debug g++ 16.2.1 — fpx16_16 mean=0.598405 p99=0.606001, fp32_pinned mean=0.591438 p99=0.615179 — BOTH budgets PASS on BOTH backends (5.0× / 8.1× inside the 3.0/5.0 ms targets); Release g++ 0.0801446/0.08439 (≈7.5× the Debug cost — the instrumented, unoptimized tree the gate measures); Clang 22.1.8 Debug 0.913303/0.935979 (1.5× GCC Debug — codegen difference, both far inside budget); shared-lib Debug 0.623729/0.639315; **the run's `final_hash` fingerprint is bit-identical across all four non-instrumented trees and both compilers, at both backends** (fpx16_16 `0x7840644a24334cd0`, fp32_pinned `0x7a60d70232e448c7` — the ADR 0002 cross-build bit-exactness demonstrated on the 4 000-tick workload); ASan (clang) run leak-free (exit 0, 600-tick safety shape; ≈1.64 ms/tick — 2.7× the canonical, the instrumentation the gate excludes); TSan `halt_on_error=1` race-free (exit 0) with one legitimate `system/budget_overrun` warn (BenchHash 2.04 ms vs its 2 ms budget under TSan's ≈4× slowdown — the engine's correct G-R5 observation, not a workload defect); full `ctest` green on all six local trees: 94/94 `build`/`build-clang`/`build-shared` (the 92 pre-existing + the two new gate entries), 93/93 `build-release` (one pre-existing Debug-only entry), 92/92 `build-asan`/`build-tsan` (the gate entries correctly absent there); zero new warnings under NFR-8.10; `tools/laige-include-lint` OK (47 source files, 1/10 deps), `tools/laige-determinism-lint` OK (28 sim files, 0 violations); CI: the PR's P0 jobs carry the gate entries in their full `ctest` (linux-gcc/clang default lane + the `ci:macos`/`ci:windows`-labeled windows-msvc/macos jobs + asan/tsan) — the M1-EXIT-01 gate records the final CI evidence (commit lands with the step's PR) | | 2026-09-25 | M1-EXIT-01 | `—` / PR #52 | M1 exit gate: all 25 M1 steps confirmed against their Scope/Verify clauses — (1) sim-tick budget green on both SimMath backends: baseline report `docs/benchmarks/baselines/m1-sim-tick.md` (canonical Debug g++ 16.2.1: fpx16_16 mean 0.598405 / p99 0.606001 ms, fp32_pinned mean 0.591438 / p99 0.615179 ms — both PASS, 5.0×/8.1× inside the 3.0/5.0 ms targets) + CI (the CI reference machine, ubuntu-24.04, is the gate): PR-lane run 36152843429 (linux-gcc + linux-clang full ctest 94/94, including the two budget-gate entries `laige_bench_sim_tick_fpx16`/`laige_bench_sim_tick_fp32` — both budgets, both backends, exit 0) and run 36155154836 (macOS arm64 + Intel full ctest, the same two gate entries green on AppleClang); (2) replay bit-exact on CI across all P0 OS jobs and both backends: the determinism matrix `docs/benchmarks/determinism-matrix.md` (every P0 OS job asserts per-tick identity of the hello baseline on both backends — `hello_baseline_fpx`/`hello_baseline_fp32` — and the always-on detcheck job compares the cross-build pairs on both backends) — all-OS evidence: merge-lane run 36037080146 on master (2026-09-24: all 7 P0 jobs + detcheck green) and this PR's label-gated P0 matrix — Linux run 36152843429, macOS arm64/Intel run 36155154836, windows-msvc run 36156322585 — every job green, including the two hello-baseline tests in each OS job's full ctest and the detcheck job; (3) zero-alloc assertion green on the 10k workload: the `zero_alloc` ctest entry (M1-ALLOC-01) green in every P0 job's full ctest in the runs above (test logs in each run's job logs; the ASan/TSan lanes additionally archive the sanitizer reports) — and the M1-BENCH-01 measurement itself is zero-alloc-asserted: every tick of both backends (4 000 each — warm-up and measured) passed the engine's G-R1 per-tick allocation assertion in the debug trees (an allocating tick aborts the run). Deferred P1 items: none — every M1 step is P0. Progress Board: M1 25/25 complete (total 47/193); the board's stale Total row (41) corrected to 47 in the gate commit | +| 2026-09-25 | M2-DEC-01 | `948b942` | D-ISO decided: 2:1 dimetric confirmed as the template default (true 30°/60° iso remains selectable per scene — the decision is presentation-only, no sim/determinism impact); ADR 0005 written in `docs/decisions/` (context, decision, alternatives, evidence, consequences, review conditions per DOC-005); ADR indexed in `docs/decisions/README.md` and `docs/README.md`; M2-DEC-01 box checked, decision register D-ISO row closed, Progress Board M2 1/33 (total 48/194); docs only, no code (board/changelog row added in the follow-up commit of the same PR) | ---