[M1-LOOP-01] Fixed-timestep game loop core - #29
Merged
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 tomaxCatchUpTicksboundedrunSystemsdispatches; no allocation, no logging on the success path (PERF-002/003, LOG-003). Runs exactlymin(due − ticksRun, maxCatchUpTicks)ticks per frame.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).maxCatchUpTicksand drops exactlywant − maxCatchUpTicks— counted (GameLoopStats.droppedTicks/droppedFrames) and logged once per episode via the rate-limitedloop/tick_droppedwarn (fieldsdropped,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()once per frame,runSystemsonce per tick (the entity.h contract; G-R3/G-R4 windows are per presentation frame).runSystemsInvalidArgumentsurfaces (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.Options::nowNs(nanoseconds on a monotonic epoch;nullptr→ headlesssteady_clock— injectable for tests, theLoggerOptions::ClockFnprecedent; the M2 windowed clock plugs in here).GameLoopStats— since-construction profiler feed (M1-PROF-01).Tests
New
GameLoopsuite (11 tests) + CTest entrygame_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=2summary); 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); defaultsteady_clockdrives real frames; move transfers state and stops the source; zero-allocation window (300 frames / 600 ticks →allocs=0).Verified
ctest -R game_loopgreen; full suite 44/44 on all six local trees (buildDebug GCC 16.2.1,build-asanASan+UBSan leak-free,build-tsan,build-clang22.1.8,build-release,build-shared); zero new warnings under NFR-8.10.tools/laige-include-lintOK (30 source files, 1/10 vendored deps).laige-api.jsonregenerated (505 → 530 symbols; +25, additive-only) withapi-real-treegreen.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 indocs/api/system_timing.md,include/laige/sim/system.h(the scheduler sketch now referencesGameLoop),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).