Skip to content

Decouple render from fixed sim tick with interpolation (#502)#579

Merged
drsnuggles8 merged 2 commits into
masterfrom
feature/fixed-render-interpolation
Jul 7, 2026
Merged

Decouple render from fixed sim tick with interpolation (#502)#579
drsnuggles8 merged 2 commits into
masterfrom
feature/fixed-render-interpolation

Conversation

@drsnuggles8

@drsnuggles8 drsnuggles8 commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Closes #502. Second acceptance criterion of parent #456 (slice 1 — frame-rate limiter + smoothing — shipped in #501).

What

Renders an interpolated pose between the two most recent fixed-tick simulation states so motion stays smooth when the display rate isn't a multiple of the sim rate (e.g. 60 Hz sim on a 144 Hz display). The deterministic fixed-tick accumulator already existed in Scene::OnUpdateRuntimeFixed (#484/#452); this builds interpolation on top of it.

How

  • Scene::OnUpdateRuntimeFixed snapshots each entity's local TransformComponent into m_InterpPrev before each fixed step and into m_InterpCurr after the last step, and publishes m_RenderInterpAlpha = accumulator / fixedDt.
  • Scene::RenderRuntime overwrites live local transforms with the blended pose (glm::mix translation/scale, glm::slerp rotation), re-propagates world matrices, renders, then restores the exact fixed-tick poses. Overwrite-then-restore makes every render read — GetWorldTransform() world matrices and the direct TransformComponent::GetTransform() reads in RenderScene3D (terrain/water/foliage/decals) — see the blended pose without threading alpha through every draw call.
  • Restore copies the whole component (the private Rotation/RotationEuler would drift if restored via SetRotation()). The primary fly-cam is excluded from the overwrite (it's display-rate; restoring from the snapshot would erase its movement) — a gameplay camera's view is blended into cameraTransform directly instead.
  • Determinism-safe: interpolation only ever runs in RenderRuntime and restores the persisted state before returning, so SaveGameManager::Tick / editor gizmos read the exact tick pose and Feature/deterministic fixed timestep #484 does not regress.

Editor toggle

EditorPreferences::RenderInterpolation (Performance tab, on by default). Persisted, applied on Play, and live via ApplyPreferences so toggling it takes effect on a running session. OloRuntime uses the default.

Testing

  • New: RenderInterpolationSmoothsMotionTest (Functional) — drives a Lua constant-velocity mover through OnUpdateRuntimeFixed at 60 Hz sim / 144 Hz render and proves (1) the interpolated render pose advances in near-constant increments (monotonic, second difference ≈ 0 → judder-free) while the raw sim pose visibly stair-steps, and (2) interpolation never leaks a fractional pose into persisted sim state (raw pose stays exactly on the integer tick count).
  • No regression: existing FixedTimestepDeterminismTest / DeterministicReplayProducesSameStateTest pass unchanged.
  • Full suite: 4185 passed / 0 failed (3 SKIP by design). Editor builds + renders the full pipeline correctly (verified via screenshot).

Visual-verification note

Motion smoothness is proven analytically by the functional test (constant velocity ⇒ provably linear render trajectory), since Play-mode live motion can't be driven headlessly. This change adds no new pixel-producing code (no shader/pass/material/blend) — it only changes the input transforms fed to the already-pixel-tested GetWorldTransform→renderer path, and the full GPU visual-evidence suite stays green.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a new Render Interpolation preference in the Performance settings, with a help tooltip and default enabled.
    • Enabled smoother in-game rendering by blending poses between fixed simulation ticks during Play mode.
  • Bug Fixes

    • Render interpolation now applies immediately when starting Play mode and updates live during a running session.
    • Saved preference settings now persist correctly across app restarts.
  • Tests

    • Added functional coverage to verify smoother motion and preserve exact simulation state.

…502)

Render an interpolated pose between the two most recent fixed-tick states so
motion stays smooth when the display rate isn't a multiple of the sim rate
(e.g. 60 Hz sim on a 144 Hz display). The fixed-tick accumulator already
existed in Scene::OnUpdateRuntimeFixed (#484/#452); this adds interpolation
on top.

- OnUpdateRuntimeFixed snapshots each entity's local TransformComponent into
  m_InterpPrev before each step and m_InterpCurr after the last step, and
  publishes m_RenderInterpAlpha = accumulator / fixedDt.
- RenderRuntime overwrites live locals with the blended pose (glm::mix for
  translation/scale, glm::slerp for rotation), re-propagates world matrices,
  renders, then restores the exact fixed-tick poses. Overwrite-then-restore
  makes every render read (GetWorldTransform world matrices AND direct
  TransformComponent::GetTransform() reads) see the blended pose without
  threading alpha through every draw call. Restore copies the whole component
  so the private RotationEuler can't drift; the primary fly-cam is excluded
  (a gameplay camera's view is blended into cameraTransform instead).
- Purely presentational: sim state is restored before RenderRuntime returns,
  so determinism (#484) is preserved and post-render consumers read the exact
  tick pose.

Editor toggle: EditorPreferences::RenderInterpolation (on by default),
persisted + applied on Play and live via ApplyPreferences. OloRuntime uses
the default.

Test: RenderInterpolationSmoothsMotionTest drives a Lua constant-velocity
mover through OnUpdateRuntimeFixed at 60/144 and proves the interpolated
render pose advances in near-constant increments (judder-free, second
difference ~0) while the raw sim pose stair-steps, and that interpolation
never leaks a fractional pose into persisted sim state. Full suite: 4185
pass / 0 fail; existing fixed-timestep determinism tests unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@drsnuggles8, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 50 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 49406ec0-9786-420b-b3c1-ee133364f8e8

📥 Commits

Reviewing files that changed from the base of the PR and between 19398e8 and b85218c.

📒 Files selected for processing (3)
  • OloEngine/src/OloEngine/Scene/Scene.cpp
  • OloEngine/src/OloEngine/Scene/Scene.h
  • OloEngine/tests/Functional/Scene/RenderInterpolationSmoothsMotionTest.cpp
📝 Walkthrough

Walkthrough

Adds a render interpolation system to Scene that blends rendered poses between fixed simulation ticks using snapshot capture and quaternion/vector interpolation, exposes a new RenderInterpolation editor preference wired into EditorLayer and EditorPreferencesPanel, and adds a functional test verifying interpolated motion behavior.

Changes

Render Interpolation Feature

Layer / File(s) Summary
Scene interpolation API and state
OloEngine/src/OloEngine/Scene/Scene.h
Adds InterpTransform pose struct, public methods (SetRenderInterpolationEnabled, IsRenderInterpolationEnabled, GetRenderInterpolationAlpha, GetInterpolatedLocalTransform), private capture/compute helpers, and member state for snapshots, alpha, and enable flag.
Fixed-step snapshot capture and interpolation computation
OloEngine/src/OloEngine/Scene/Scene.cpp
Clears interpolation state on OnRuntimeStart/OnSimulationStart, snapshots locals before/after fixed steps and computes blend alpha in OnUpdateRuntimeFixed, and implements helper functions for capturing transforms and computing mix/slerp-blended local poses.
RenderRuntime pose blending and restore
OloEngine/src/OloEngine/Scene/Scene.cpp
Tracks the primary camera in RenderRuntime, overwrites non-camera transforms with interpolated poses and blends the camera view for rendering, then restores authoritative poses and re-propagates world transforms post-render.
Editor preference wiring for render interpolation
OloEditor/src/Panels/EditorPreferencesPanel.h, OloEditor/src/Panels/EditorPreferencesPanel.cpp, OloEditor/src/EditorLayer.cpp
Adds RenderInterpolation field to EditorPreferences, exposes a checkbox with tooltip in the Performance tab, persists it via YAML Save/Load, and applies it to the active scene in ApplyPreferences and OnScenePlay.
Functional tests for interpolated motion
OloEngine/tests/Functional/Scene/RenderInterpolationSmoothsMotionTest.cpp, OloEngine/tests/CMakeLists.txt
Adds a Lua-driven mover test fixture and two tests validating judder-free interpolated rendering and that interpolation does not leak into persisted simulation state; wires the new test file into the test build.

Sequence Diagram(s)

sequenceDiagram
  participant FixedLoop as OnUpdateRuntimeFixed
  participant Scene
  participant RenderRuntime

  FixedLoop->>Scene: CaptureLocalTransforms into m_InterpPrev
  FixedLoop->>Scene: run fixed simulation step
  FixedLoop->>Scene: CaptureLocalTransforms into m_InterpCurr
  FixedLoop->>Scene: compute m_RenderInterpAlpha
  RenderRuntime->>Scene: GetInterpolatedLocalTransform per entity
  RenderRuntime->>RenderRuntime: overwrite transforms, PropagateWorldTransforms
  RenderRuntime->>RenderRuntime: render frame
  RenderRuntime->>Scene: restore authoritative poses
  RenderRuntime->>RenderRuntime: PropagateWorldTransforms (authoritative)
Loading

Related PRs: None identified.

Suggested labels: enhancement, scene, editor, tests

Suggested reviewers: drsnuggles8

🐰 Hopping between ticks so smooth,
Poses blend, no judder to soothe,
Snapshots kept in either hand,
Alpha slides where curr meets prev,
Then true state stands, restored, unbowed.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: render interpolation to decouple rendering from fixed simulation ticks.
Linked Issues check ✅ Passed The changes implement interpolated rendering, preserve deterministic simulation state, add an editor toggle, and include a functional test for non-multiple refresh motion.
Out of Scope Changes check ✅ Passed All code changes are directly related to render interpolation, editor preference persistence, runtime application, and validation tests.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

coderabbitai[bot]

This comment was marked as resolved.

@sonarqubecloud

sonarqubecloud Bot commented Jul 7, 2026

Copy link
Copy Markdown

…-clears-cache

Code-review follow-ups on the render-interpolation change (#579):

- Paused frame-stepping: force m_RenderInterpAlpha = 0 while paused so
  RenderRuntime renders the live (frozen or just-stepped) pose exactly. Without
  this the stale pre-pause alpha/snapshot pair blended backward toward the
  previous tick and a single-step advance was invisible.
- RenderRuntime restore buffer is now a function-local static thread_local
  (cleared, not reallocated, each frame) so the hot render path no longer
  heap-allocates a fresh vector per frame. Kept out of the Scene as a member
  because TransformComponent is incomplete in Scene.h (which deliberately omits
  the heavy Components.h); render is synchronous per game thread so a per-thread
  reused buffer is safe.
- SetRenderInterpolationEnabled(false) now clears m_HasInterpSnapshots so
  re-enabling can't blend from a stale pose before the next fresh capture.
- Use glm::mat4_cast (declared in the explicitly-included gtc/quaternion.hpp)
  instead of glm::toMat4 (gtx) — removes the transitive-include dependency.
- Fix stale doc comments (ComputeInterpolatedLocal had a nonexistent parameter;
  ShouldInterpolateThisFrame now states the real alpha>0 gate) and the test
  header comment (it described an ON/OFF comparison the test doesn't perform).
- Lua mover preserves y/z instead of zeroing them, so the fixture stays correct
  if reused with a non-zero start pose.

Full suite: 4186 pass / 0 fail. Interpolation, determinism, and pause/step
tests all green (the paused-alpha fix does not regress frame-by-frame stepping).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@drsnuggles8 drsnuggles8 merged commit 2d37920 into master Jul 7, 2026
2 of 8 checks passed
@drsnuggles8 drsnuggles8 deleted the feature/fixed-render-interpolation branch July 7, 2026 07:33
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.

Renderer: decouple update (fixed tick) from render with interpolation

1 participant