Skip to content

[M1-LOOP-01] Fixed-timestep game loop core - #29

Merged
offdev merged 2 commits into
masterfrom
m1-loop-01-game-loop
Sep 14, 2026
Merged

offdev merged 2 commits into
masterfrom
m1-loop-01-game-loop

Conversation

@offdev

@offdev offdev commented Sep 14, 2026

Copy link
Copy Markdown
Owner

M1-LOOP-01 · Fixed-timestep game loop core (FR-1.1, ARCH-002, PRD §10.2/§10.3)

Implements exactly the M1-LOOP-01 scope from roadmap/M1-heartbeat.md — nothing else.

What lands

  • GameLoop (src/laige-sim/include/laige/sim/game_loop.h, src/laige-sim/game_loop.cpp): the accumulator loop that advances the simulation in integer ticks, decoupled from the presentation frame cadence (ARCH-002):
    • create(world, schedule, options) — typed config, validated: tick rate 20–120 Hz (default 60), max catch-up ticks ≥ 1 (default 5). Every rejection: InvalidArgument + one rate-limited warn (loop/tick_rate_invalid, loop/catchup_invalid) — never silent (FR-12.3).
    • frame() — one clock read, a few integer ops, up to maxCatchUpTicks bounded runSystems dispatches; no allocation, no logging on the success path (PERF-002/003, LOG-003). Runs exactly min(due − ticksRun, maxCatchUpTicks) ticks per frame.
    • Exact due computation: due(now) = floor(elapsedNs × rate / 10⁹) in pure integer arithmetic (seconds/sub-seconds split — overflow-free, no floating point, no drift — ARCH-010). No stored accumulator state: the remainder is re-derived from the clock. A synthetic 10 s clock at 60 Hz yields exactly 600 ticks (a float ms accumulator floors to 599).
    • Overload behavior: when demand exceeds the catch-up cap, the frame runs exactly maxCatchUpTicks and drops exactly want − maxCatchUpTicks — counted (GameLoopStats.droppedTicks/droppedFrames) and logged once per episode via the rate-limited loop/tick_dropped warn (fields dropped, total_dropped, max_catch_up, tick_rate_hz; NFR-13.3 5-field grammar; LOG-004 summary at shutdown). Per-frame work stays bounded — no unbounded catch-up, no silent starvation (PERF-008).
    • beginFrame wiring: beginFrame() once per frame, runSystems once per tick (the entity.h contract; G-R3/G-R4 windows are per presentation frame).
    • Failures: stale/malformed schedule → the runSystems InvalidArgument surfaces (failed ticks not counted, world untouched, tick count frozen until the loop is recreated); moved-from loop is stopped (frame() → InvalidArgument, no log); non-monotonic clock reading below the start reference asserts in debug / clamps in release.
    • Clock source: Options::nowNs (nanoseconds on a monotonic epoch; nullptr → headless steady_clock — injectable for tests, the LoggerOptions::ClockFn precedent; the M2 windowed clock plugs in here).
    • GameLoopStats — since-construction profiler feed (M1-PROF-01).
    • Determinism scope (ARCH-009/010): the tick sequence is a pure function of (clock readings, rate, cap) — integer-only; frames/drops are presentation/diagnostic state, never authoritative.

Tests

New GameLoop suite (11 tests) + CTest entry game_loop (the step's Verify command; added to the TSan property list): config validation + warns + read-back; first frame zero ticks; exact 600 ticks over the synthetic 10 s clock; overload drops exactly 8/16/24 and logs once per episode (grammar check + suppressed=2 summary); healthy cadence 120 ticks / zero drops / silent with one dispatch per tick; stale schedule freezes the tick count and surfaces the Status; backward clock jump (release clamp / debug assert in a forked SIGABRT child); default steady_clock drives real frames; move transfers state and stops the source; zero-allocation window (300 frames / 600 ticks → allocs=0).

Verified

  • ctest -R game_loop green; full suite 44/44 on all six local trees (build Debug GCC 16.2.1, build-asan ASan+UBSan leak-free, build-tsan, build-clang 22.1.8, build-release, build-shared); zero new warnings under NFR-8.10.
  • tools/laige-include-lint OK (30 source files, 1/10 vendored deps).
  • laige-api.json regenerated (505 → 530 symbols; +25, additive-only) with api-real-tree green.

Docs (same change, DOC-007)

New docs/api/game_loop.md (full contract: the two cadences, the exact due computation, config/validation, overload behavior, beginFrame wiring, failure behavior, determinism scope, profiler feed, Performance section, misuse warnings) + cross-refs in docs/api/system_timing.md, include/laige/sim/system.h (the scheduler sketch now references GameLoop), docs/README.md, src/laige-sim/README.md.

Compat

Additive only — no existing symbol or behavior changed.

Roadmap: M1-LOOP-01 checked, progress board M1 10 → 11, change log entry (commit 30f3013).

@offdev
offdev merged commit bf3d2b5 into master Sep 14, 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