Skip to content

feat: fixed-timestep update loop decoupled from render - #185

Merged
TommyRadan merged 1 commit into
masterfrom
feature/fixed-timestep-update-loop
Jun 18, 2026
Merged

feat: fixed-timestep update loop decoupled from render#185
TommyRadan merged 1 commit into
masterfrom
feature/fixed-timestep-update-loop

Conversation

@TommyRadan

Copy link
Copy Markdown
Owner

Summary

Implements #168. Splits engine::tick() into a fixed-step update decoupled from a variable-rate render, so deterministic gameplay, animation, and physics no longer depend on frame rate.

Changes

  • core::time — fixed-delta accounting alongside the existing wall-clock delta:
    • accumulate(frame_delta_time) feeds elapsed real time into an accumulator, clamped to max_steps_per_frame steps — the spiral-of-death guard (a slow frame falls behind real time rather than queueing an unbounded backlog).
    • next_fixed_step() drains one whole fixed step; drive with while (time->next_fixed_step()) { update(); }.
    • interpolation_alpha() exposes the leftover remainder in [0, 1) for render-time smoothing between simulated states.
    • fixed_delta_time() accessor; rate + clamp are constructor parameters (default 60 Hz, 5 steps).
  • runtime::engine::tick — pumps input once per rendered frame, drains the accumulator emitting core::frame per fixed step (game logic), then propagates scene-graph transforms and renders once. render_* events still fire per render inside renderer->render().
  • window::tick — no longer emits core::frame; it now only pumps OS input at the variable render rate.

Where update vs. render fire

  • Fixed step: core::frame (game-logic update). Carries the fixed delta, so per-second motion is preserved across frame rates — behaviour-preserving for the existing demos.
  • Per render: input pump, scenes->update() (transform propagation), and the render_scene / render_ui / render_debug events.

Tests

  • New tests/core/time_test.cpp covers draining, remainder carry-over across frames, interpolation_alpha bounds, and the spiral-of-death clamp. core/time.cpp wired into the headless test target.
  • clang-format and clang-tidy naming gates pass; Release build + the 7 new tests pass locally.

Closes #168

Split engine::tick() into an accumulator-driven fixed-step update and a
variable-rate render so simulation behaviour no longer depends on frame
rate. core::time gains fixed-delta accounting: accumulate() feeds elapsed
wall-clock time into a clamped accumulator (the spiral-of-death guard caps
it at max_steps_per_frame), next_fixed_step() drains whole steps, and
interpolation_alpha() exposes the remainder for render-time smoothing.

Game-logic (core::frame) now fires once per fixed step, driven by the
engine; window::tick() only pumps input per rendered frame. The render_*
events still fire per render inside renderer->render(), and scene-graph
component propagation runs once per frame before the draw walk.

Closes #168
@github-actions

Copy link
Copy Markdown

CI build artifacts

Windows Debug and Release builds are attached to the workflow run summary under Artifacts:

  • AlphaEngine-windows-Debug — Debug AlphaEngine.exe + Khronos validation layer
  • AlphaEngine-windows-Release — Release AlphaEngine.exe

Run #254 — commit 9c7be94.

@TommyRadan
TommyRadan merged commit b7a3f80 into master Jun 18, 2026
6 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.

feat: fixed-timestep update loop decoupled from render

1 participant