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:
parent
0330fcd0d1
commit
4873c10673
5 changed files with 110 additions and 100 deletions
|
|
@ -344,6 +344,60 @@ public sealed class PlayerMovementController
|
|||
/// </summary>
|
||||
internal bool AdvancedObjectQuantumLastTick { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// #429 defect 2: how far the PRESENTED position's own clock advanced in
|
||||
/// the last tick, in seconds. The presented position
|
||||
/// (<see cref="RenderPosition"/>) lives on the retail 30 Hz object clock —
|
||||
/// a lerp between the last two simulated quanta whose alpha clamps at 1 —
|
||||
/// so near-quantum-length host frames alias against the 33.3 ms quantum
|
||||
/// and the presented position under-advances relative to wall time. Any
|
||||
/// consumer that smooths toward the presented position (the chase camera)
|
||||
/// must integrate THIS delta, not the host frame's wall dt, or the two
|
||||
/// visibly decohere on long frames (measured at ~1 m in one frame, the
|
||||
/// felt run-hitch). Retail ties its camera to the physics-update callback
|
||||
/// (<c>SmartBox::PlayerPhysicsUpdatedCallback</c> 0x00452d60) — the same
|
||||
/// clock — which is the behavior this delta restores.
|
||||
/// </summary>
|
||||
public float PresentedDeltaSeconds { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Seconds spanned by the prev→curr snapshot pair the presentation lerp
|
||||
/// interpolates across — the length of the LAST simulated quantum, which
|
||||
/// the retail clock makes variable (MinQuantum..MaxQuantum). Maintained by
|
||||
/// <see cref="ComputePresentedDelta"/>, the single per-tick chokepoint.
|
||||
/// </summary>
|
||||
private float _lastQuantumSeconds = PhysicsBody.MinQuantum;
|
||||
|
||||
private float ComputePresentedDelta(
|
||||
float wallDt,
|
||||
double pendingBeforeSeconds,
|
||||
in RetailObjectQuantumBatch batch)
|
||||
{
|
||||
if (batch.Discarded)
|
||||
{
|
||||
// The lerp base was reset to the current body position — the
|
||||
// presented position snapped. Hand the camera the wall step so it
|
||||
// snaps along rather than freezing mid-teleport.
|
||||
_lastQuantumSeconds = PhysicsBody.MinQuantum;
|
||||
return wallDt;
|
||||
}
|
||||
// Presented time = t(curr) − lastInterval + min(pending, lastInterval):
|
||||
// continuous across quantum fires, long-run rate 1 with bounded jitter.
|
||||
// The per-tick advance is the simulated seconds plus the phase change.
|
||||
float previousInterval = _lastQuantumSeconds;
|
||||
float simulated = 0f;
|
||||
if (batch.Count > 0)
|
||||
{
|
||||
simulated = batch.FullSteps * PhysicsBody.MaxQuantum + batch.Remainder;
|
||||
_lastQuantumSeconds = batch.GetQuantum(batch.Count - 1);
|
||||
}
|
||||
float interval = _lastQuantumSeconds;
|
||||
float before = Math.Min((float)pendingBeforeSeconds, previousInterval);
|
||||
float after = Math.Min((float)_objectClock.PendingSeconds, interval);
|
||||
float delta = simulated + (after - interval) - (before - previousInterval);
|
||||
return Math.Max(delta, 0f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns retail's canonical outbound <c>Position</c>: the physics body's
|
||||
/// carried cell id plus its landblock-local frame origin. Retail
|
||||
|
|
@ -1043,6 +1097,7 @@ public sealed class PlayerMovementController
|
|||
{
|
||||
EnsurePublishedForRuntimeOperation();
|
||||
AdvancedObjectQuantumLastTick = false;
|
||||
PresentedDeltaSeconds = 0f;
|
||||
if (float.IsFinite(elapsedSeconds) && elapsedSeconds > 0f)
|
||||
_simTimeSeconds += elapsedSeconds;
|
||||
_objectClock.Deactivate();
|
||||
|
|
@ -2188,8 +2243,16 @@ public sealed class PlayerMovementController
|
|||
|
||||
private Vector3 ComputeRenderPosition()
|
||||
{
|
||||
// #429 defect 2 (residual): the prev→curr snapshot pair spans the LAST
|
||||
// SIMULATED QUANTUM, whose length is variable — the retail clock
|
||||
// simulates everything above MinQuantum in one step, so a long host
|
||||
// frame produces a 34-100 ms quantum. Normalizing the interpolation by
|
||||
// the fixed MinQuantum made the presented position freeze the frame a
|
||||
// long quantum fired (alpha reset to 0 across a bigger gap) and then
|
||||
// replay at gap/MinQuantum speed — the residual whole-view hiccup
|
||||
// after the camera-clock fix. Normalize by the actual interval.
|
||||
float alpha = Math.Clamp(
|
||||
(float)(_objectClock.PendingSeconds / PhysicsBody.MinQuantum),
|
||||
(float)(_objectClock.PendingSeconds / _lastQuantumSeconds),
|
||||
0f,
|
||||
1f);
|
||||
return Vector3.Lerp(_prevPhysicsPos, _currPhysicsPos, alpha);
|
||||
|
|
@ -2206,6 +2269,7 @@ public sealed class PlayerMovementController
|
|||
{
|
||||
EnsurePublishedForRuntimeOperation();
|
||||
AdvancedObjectQuantumLastTick = false;
|
||||
PresentedDeltaSeconds = 0f;
|
||||
if (!float.IsFinite(dt) || dt <= 0f)
|
||||
{
|
||||
return CapturePresentationResult() with
|
||||
|
|
@ -2216,12 +2280,14 @@ public sealed class PlayerMovementController
|
|||
}
|
||||
|
||||
_simTimeSeconds += dt;
|
||||
double pendingBeforeSeconds = _objectClock.PendingSeconds;
|
||||
bool reactivated = _objectClock.Activate();
|
||||
_body.TransientState |= TransientStateFlags.Active;
|
||||
RetailObjectQuantumBatch batch = reactivated
|
||||
? default
|
||||
: _objectClock.Advance(dt);
|
||||
AdvancedObjectQuantumLastTick = batch.Count > 0;
|
||||
PresentedDeltaSeconds = ComputePresentedDelta(dt, pendingBeforeSeconds, in batch);
|
||||
if (batch.Discarded)
|
||||
{
|
||||
_prevPhysicsPos = _body.Position;
|
||||
|
|
@ -2326,6 +2392,7 @@ public sealed class PlayerMovementController
|
|||
{
|
||||
EnsurePublishedForRuntimeOperation();
|
||||
AdvancedObjectQuantumLastTick = false;
|
||||
PresentedDeltaSeconds = 0f;
|
||||
// Reject a malformed host-frame duration at the controller boundary.
|
||||
// The retail object clock cannot sanitize state that input/jump/yaw
|
||||
// code already mutated; in particular Infinity would never converge
|
||||
|
|
@ -2641,12 +2708,15 @@ public sealed class PlayerMovementController
|
|||
// stale gaps above HugeQuantum. Every admitted quantum executes the
|
||||
// whole object update below; animation never runs on a render-only
|
||||
// fragment.
|
||||
double pendingBeforeSeconds = _objectClock.PendingSeconds;
|
||||
bool reactivated = _objectClock.Activate();
|
||||
_body.TransientState |= TransientStateFlags.Active;
|
||||
RetailObjectQuantumBatch quantumBatch = reactivated
|
||||
? default
|
||||
: _objectClock.Advance(dt);
|
||||
AdvancedObjectQuantumLastTick = quantumBatch.Count > 0;
|
||||
PresentedDeltaSeconds =
|
||||
ComputePresentedDelta(dt, pendingBeforeSeconds, in quantumBatch);
|
||||
bool justLanded = false;
|
||||
if (quantumBatch.Discarded)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue