fix(runtime/camera) #429: presented player and chase camera share the object clock

Two halves of the felt run-hitch (the visible one-frame player lurch):

- The presentation lerp normalized the pending object-clock time by the
  fixed 30 Hz MinQuantum, but retail's object clock simulates
  VARIABLE-length quanta (CPhysicsObj::update_object 0x00515D10: capped
  at MaxQuantum, everything above MinQuantum runs as ONE step). After a
  long frame the view froze for the quantum and then fast-replayed it.
  ComputeRenderPosition now spans the ACTUAL last quantum
  (_lastQuantumSeconds), and PresentedDeltaSeconds accounts continuous
  presented time across quantum boundaries.

- The chase camera damped toward the presented player using wall dt
  while the player presents on the object clock, so a long frame
  stepped the camera far past the under-advanced player — measured up
  to ~1 m of camera/player decoherence in a single frame. Retail ties
  camera update to the physics-update callback
  (SmartBox::PlayerPhysicsUpdatedCallback 0x00452d60), i.e. the same
  clock as the body; both chase cameras now integrate
  PresentedDeltaSeconds. Manual zoom/pitch adjustment stays on wall dt
  (a user-input rate, not target chasing).

Owner gate: camera-vs-player boom-length change fell from ~1 m spikes
to 0.2-1.2 cm median on long frames; teleports settle clean. Two
Runtime tests updated to pin the continuous-rate contract. The
temporary PlayerPresentationProbe apparatus that measured this is
retired with the fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-24 09:16:41 +02:00
parent 0330fcd0d1
commit 4873c10673
5 changed files with 110 additions and 100 deletions

View file

@ -94,11 +94,23 @@ internal sealed class CameraFrameController : ICameraFramePhase
_spatialReconciler.Reconcile();
MovementResult result = playerFrame.Movement;
// #429 defect 2: the chase camera smooths toward the PRESENTED player
// position, which lives on the retail 30 Hz object clock (see
// PlayerMovementController.PresentedDeltaSeconds). Integrating the
// damping with wall dt made the camera step full wall time on long
// frames while the presented player under-advanced against the
// quantum — measured ~1 m of camera/player decoherence in one frame,
// the felt run-hitch. Retail ties the camera to the physics-update
// callback (SmartBox::PlayerPhysicsUpdatedCallback 0x00452d60), i.e.
// the same clock as the body; the presented delta restores that.
// Manual zoom/pitch adjustment above stays on wall dt — it is a
// user-input rate, not target chasing.
float cameraDt = controller.PresentedDeltaSeconds;
legacy.Update(
result.RenderPosition,
controller.Yaw,
isOnGround: result.IsOnGround,
dt: timing.SimulationDeltaSecondsSingle);
dt: cameraDt);
retail?.Update(
result.RenderPosition,
@ -106,7 +118,7 @@ internal sealed class CameraFrameController : ICameraFramePhase
playerVelocity: controller.BodyVelocity,
isOnGround: result.IsOnGround,
contactPlaneNormal: controller.ContactPlane.Normal,
dt: timing.SimulationDeltaSecondsSingle,
dt: cameraDt,
cellId: controller.CellId,
selfEntityId: controller.LocalEntityId,
trackedTargetPoint: _combatTarget.GetTrackedTargetPoint());