Decouple render from fixed sim tick with interpolation (#502)#579
Conversation
…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>
|
Warning Review limit reached
Next review available in: 50 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Repository UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughAdds a render interpolation system to ChangesRender Interpolation Feature
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)
Related PRs: None identified. Suggested labels: enhancement, scene, editor, tests Suggested reviewers: drsnuggles8 🐰 Hopping between ticks so smooth, 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
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. Comment |
|
…-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>



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::OnUpdateRuntimeFixedsnapshots each entity's localTransformComponentintom_InterpPrevbefore each fixed step and intom_InterpCurrafter the last step, and publishesm_RenderInterpAlpha = accumulator / fixedDt.Scene::RenderRuntimeoverwrites live local transforms with the blended pose (glm::mixtranslation/scale,glm::slerprotation), re-propagates world matrices, renders, then restores the exact fixed-tick poses. Overwrite-then-restore makes every render read —GetWorldTransform()world matrices and the directTransformComponent::GetTransform()reads inRenderScene3D(terrain/water/foliage/decals) — see the blended pose without threadingalphathrough every draw call.Rotation/RotationEulerwould drift if restored viaSetRotation()). 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 intocameraTransformdirectly instead.RenderRuntimeand restores the persisted state before returning, soSaveGameManager::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 viaApplyPreferencesso toggling it takes effect on a running session. OloRuntime uses the default.Testing
RenderInterpolationSmoothsMotionTest(Functional) — drives a Lua constant-velocity mover throughOnUpdateRuntimeFixedat 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).FixedTimestepDeterminismTest/DeterministicReplayProducesSameStateTestpass unchanged.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
Bug Fixes
Tests