Skip to content

[M1-HEAD-01] Headless engine run (Engine + laige-run) - #31

Merged
offdev merged 2 commits into
masterfrom
m1-head-01-headless-engine-run
Sep 15, 2026
Merged

offdev merged 2 commits into
masterfrom
m1-head-01-headless-engine-run

Conversation

@offdev

@offdev offdev commented Sep 15, 2026 •

Copy link
Copy Markdown
Owner

M1-HEAD-01: the headless engine object + the laige-run binary.

What

  • Engine (src/laige-sim/include/laige/sim/engine.h, engine.cpp): config → world → systems → loop. EngineConfig + parseEngineConfig (provisional JSON surface); run_headless(maxTicks, frameBudgetTicks) (bounded + server forms); always ends in the ordered idempotent CONC-006 shutdown (loop → world clear → snapshot → world release → logging flush); lifecycle Info pair engine/run_started/engine/run_finished.
  • laige-run (tools/run): --headless CONFIG [--ticks N] [--replay LOG] (replay = M1-DET-02 stub, warned); exit codes 0/1/2; machine-greppable stdout summary; the CLI exercises the double-shutdown idempotency.
  • CI: laige_run_smoke ctest entry — --ticks 1000 @ 60 Hz against tests/laige-sim/fixtures/headless_smoke.json (TIMEOUT 300, asserts status=ok; TSan halt_on_error=1). engine ctest entry: 20 unit tests incl. double-shutdown and the zero-allocation frame-path probe.
  • Docs: docs/api/engine.md (full contract + Performance) + cross-refs; laige-api.json regenerated (555 → 573 symbols).

Key properties

  • Frame path: one clock read, one bounded loop frame, one snapshot refresh, one sleep — no allocation, no logging on the healthy path (PERF-003/LOG-003). Setup = exactly three one-shot allocations (loop object, snapshot object, 24 B/slot table); verified per-frame-zero by the test-only operator new counter (identical count for 1/2/3/10 ticks — engine-zeroalloc ticks=… allocs=3).
  • Determinism scope: same build/platform (integer tick cadence, pinned fpx16_16 backend — ADR 0002); replay/hash tests land in M1-DET-02.
  • Include-graph lint guarantees no GPU/window symbols in the headless path (ARCH-003).

Deviation (surfaced)

Declared dependency M1-CFG-01 has not landed — the JSON config surface is provisional (three unversioned keys: tick_rate_hz, entity_budget, churn_per_frame_budget). M1-CFG-01 owns the final versioned schema and will fold parseEngineConfig in; documented in docs/api/engine.md, the header preamble, and the roadmap change-log line.

Verification (local, all six trees)

Zero-warning ctest 47/47 on build (Debug g++), build-asan (ASan+UBSan, leak-free), build-tsan (TSAN_OPTIONS=halt_on_error=1 on the engine entry), build-clang, build-release, build-shared; ctest -R engine 20/20; ctest -R laige_run_smoke green (~16.7 s, status=ok); tools/laige-include-lint OK (33 source files, 1/10 vendored deps); api-real-tree green.

Headless engine run (M1-HEAD-01 scope, nothing else):

- Engine (src/laige-sim/include/laige/sim/engine.h + engine.cpp):
  config -> world -> systems -> loop. EngineConfig (tickRateHz 20-120
  default 60, entityCapacity 0-65536 default 0 = empty scene,
  churnPerFrameBudget default 256) + parseEngineConfig over the
  bounded JSON (unknown key -> warn config/unknown_key, ignored;
  rejections config/not_an_object, tick_rate_invalid,
  entity_budget_invalid, churn_budget_invalid - first failure wins,
  one rate-limited warn each, NFR-13.3 grammar). Engine::create
  pre-validates, creates the World, registers Position2DFpx16 FIRST
  (ARCH-010).
- run_headless(maxTicks, frameBudgetTicks): schedule -> GameLoop
  (with the engine per-tick hook) -> first frame (0 ticks, start
  reference) -> PresentationSnapshot anchored on the loop's exact
  startReferenceNs (ARCH-009) -> wall-clock-paced frames (one
  steady_clock read + one bounded sleep per frame; exact integer due
  math, M1-LOOP-01). The run ALWAYS ends in the ordered shutdown
  (CONC-006: loop -> world clear -> snapshot -> world release ->
  logging flush); the shutdown is idempotent (double/triple safe,
  world() -> nullptr, second run -> InvalidArgument with no log);
  maxTicks == 0 = the server form. Lifecycle Info pair
  engine/run_started / engine/run_finished; no logging on the healthy
  frame path (PERF-003/LOG-003). Setup = exactly three one-shot
  allocations (loop object, snapshot object, 24 B/slot table); zero
  per-frame allocations (verified: identical count for 1/2/3/10
  ticks - the engine-zeroalloc line).
- laige-run binary (tools/run): --headless CONFIG (1 MiB bounded
  read), --ticks N, --replay LOG stub (warned replay/replay_deferred,
  M1-DET-02), --help. Exit codes 0 ok / 1 run failure / 2
  usage-IO-config; stdout summary 'laige-run headless ticks=...
  status=ok'; the CLI exercises the double-shutdown idempotency.
- CI: laige_run_smoke ctest entry (--ticks 1000 against the fixture,
  60 Hz / 10000 slots; TIMEOUT 300, PASS_REGULAR_EXPRESSION
  status=ok, TSan halt_on_error=1). engine ctest entry (20 tests;
  TSan property list).
- Docs: docs/api/engine.md (new, full contract + Performance),
  docs/README.md, docs/getting-started/building.md, tools/README.md,
  src/laige-sim/README.md. laige-api.json regenerated (555 -> 573
  symbols; api-real-tree green).
- Deviation: declared dependency M1-CFG-01 has not landed - the JSON
  config surface is PROVISIONAL (three unversioned keys), documented
  in engine.md, the header preamble, and the roadmap change log;
  M1-CFG-01 owns the final versioned schema.
- Verified: zero-warning 47/47 ctest on all six local trees
  (build, build-asan, build-tsan, build-clang, build-release,
  build-shared); ctest -R engine green (20/20); ctest -R
  laige_run_smoke green (~16.7 s, status=ok); tools/laige-include-lint
  OK (33 source files, 1/10 vendored deps).
@offdev
offdev merged commit e25facf into master Sep 15, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant