# Issue #38 render interpolation pseudocode ## Problem Phase L.5 correctly restored retail's `CPhysicsObj::update_object` physics gate: the body only integrates when accumulated frame time reaches `PhysicsBody.MinQuantum` (1/30 second). That keeps collision behavior aligned with retail, but the renderer currently reads `_body.Position` directly. At 60+ FPS that means several rendered frames can show the same physics position, then jump to the next 30 Hz position. Chase camera makes the stepping obvious because both the player mesh and camera target follow that discrete physics sample. ## Evidence - Named retail: `CPhysicsObj::update_object` at `0x00515d10` (`docs/research/named-retail/acclient_2013_pseudo_c.txt:283950`) skips or consumes time outside the valid quantum window and calls `CPhysicsObj::UpdateObjectInternal` only for accepted physics quanta. - ACE mirrors the same constants in `PhysicsGlobals`: `MinQuantum = 1/30`, `MaxQuantum = 0.1`, `HugeQuantum = 2.0`. - ACE's `InterpolationManager` / `PositionManager` prove client-style smoothing is a known AC-family concept, but their queue chases network target positions. Issue #38 is narrower: local render-only interpolation between the last two authoritative local physics ticks. - Glenn Fiedler's "Fix Your Timestep!" describes the canonical fixed-timestep accumulator plus render interpolation pattern: https://gafferongames.com/post/fix_your_timestep/ ## Intentional divergence Retail did not need a separate high-FPS render interpolation layer because the 2013 client effectively presented near the same cadence as its physics gate. acdream renders at modern display rates, so we preserve retail's 30 Hz physics truth but draw a blended visual position between the last two physics truths. ## Pseudocode State stored by `PlayerMovementController`: ```text physicsAccum: leftover render time not yet consumed by physics prevPhysicsPos: body position at the start of the most recently completed tick currPhysicsPos: body position at the end of the most recently completed tick ``` On spawn, teleport, or any authoritative SetPosition: ```text body.Position = newPosition prevPhysicsPos = newPosition currPhysicsPos = newPosition physicsAccum = 0 ``` Each Update(dt): ```text physicsAccum += dt if physicsAccum > HugeQuantum: physicsAccum = 0 prevPhysicsPos = body.Position currPhysicsPos = body.Position return/render body.Position if physicsAccum >= MinQuantum: oldTickEnd = currPhysicsPos preIntegratePos = body.Position tickDt = min(physicsAccum, MaxQuantum) calc_acceleration() UpdatePhysicsInternal(tickDt) postIntegratePos = body.Position resolve collision from preIntegratePos to postIntegratePos body.Position = resolved physics position update contact/cell/velocity exactly as before prevPhysicsPos = oldTickEnd currPhysicsPos = body.Position physicsAccum -= tickDt alpha = clamp(physicsAccum / MinQuantum, 0, 1) renderPosition = lerp(prevPhysicsPos, currPhysicsPos, alpha) ``` Important constraints: - Never write `renderPosition` back to `_body.Position`. - `MovementResult.Position` remains the authoritative physics/collision/network position. - Add `MovementResult.RenderPosition` for mesh and camera drawing only. - Outbound `MoveToState` / `AutonomousPosition` keep using `MovementResult.Position`. - If no physics tick has completed yet, `prevPhysicsPos == currPhysicsPos`, so interpolation is a no-op.