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
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -1,75 +0,0 @@
|
|||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// TEMPORARY #429 apparatus. One CSV row per rendered frame:
|
||||
/// <c>seconds,camX,camY,camZ,playerX,playerY,playerZ</c>, where seconds is a
|
||||
/// Stopwatch-derived monotonic time and player is the frame's
|
||||
/// <c>PlayerViewPosition</c> — the presented local-player position driving
|
||||
/// lighting/visibility this frame. Off unless
|
||||
/// <c>ACDREAM_PROBE_PLAYER_PRESENT=<path></c> is set (one null check per
|
||||
/// frame). Analysis: a hitch frame shows the player's per-frame delta
|
||||
/// collapsing to ~0 or doubling; whether the SAME frame's dt is smooth
|
||||
/// separates a presentation-phase bug (pack frame graph sampling a stale
|
||||
/// snapshot) from genuine frame-pacing spikes. Strip with the #429 fix.
|
||||
/// </summary>
|
||||
internal sealed class PlayerPresentationProbe : IDisposable
|
||||
{
|
||||
private readonly StreamWriter _writer;
|
||||
private readonly long _startTimestamp = Stopwatch.GetTimestamp();
|
||||
private int _linesSinceFlush;
|
||||
|
||||
private PlayerPresentationProbe(StreamWriter writer)
|
||||
{
|
||||
_writer = writer;
|
||||
_writer.WriteLine("seconds,camX,camY,camZ,playerX,playerY,playerZ");
|
||||
}
|
||||
|
||||
internal static PlayerPresentationProbe? CreateFromEnvironment()
|
||||
{
|
||||
string? path = Environment.GetEnvironmentVariable("ACDREAM_PROBE_PLAYER_PRESENT");
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
return new PlayerPresentationProbe(new StreamWriter(path, append: false));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[player-present] probe file '{path}' could not be opened: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
internal void Observe(in Vector3 cameraPosition, in Vector3 playerViewPosition)
|
||||
{
|
||||
double seconds = (Stopwatch.GetTimestamp() - _startTimestamp)
|
||||
/ (double)Stopwatch.Frequency;
|
||||
_writer.WriteLine(string.Create(
|
||||
CultureInfo.InvariantCulture,
|
||||
$"{seconds:F6},{cameraPosition.X:F4},{cameraPosition.Y:F4},{cameraPosition.Z:F4},{playerViewPosition.X:F4},{playerViewPosition.Y:F4},{playerViewPosition.Z:F4}"));
|
||||
if (++_linesSinceFlush >= 240)
|
||||
{
|
||||
_linesSinceFlush = 0;
|
||||
_writer.Flush();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try
|
||||
{
|
||||
_writer.Flush();
|
||||
_writer.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// A probe must never turn teardown fallible.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -450,10 +450,6 @@ internal sealed class RuntimeWorldFrameEnvironmentPreparation
|
|||
private readonly HashSet<uint> _visibleCells = [];
|
||||
private bool _visibleCellsValid;
|
||||
|
||||
/// <summary>TEMPORARY #429 — see <see cref="PlayerPresentationProbe"/>.</summary>
|
||||
private readonly PlayerPresentationProbe? _playerPresentProbe =
|
||||
PlayerPresentationProbe.CreateFromEnvironment();
|
||||
|
||||
public RuntimeWorldFrameEnvironmentPreparation(
|
||||
RuntimeOptions options,
|
||||
WorldTimeService worldTime,
|
||||
|
|
@ -489,10 +485,6 @@ internal sealed class RuntimeWorldFrameEnvironmentPreparation
|
|||
activeDayGroup,
|
||||
camera.Position);
|
||||
|
||||
// TEMPORARY #429 apparatus — one CSV row per frame; off unless
|
||||
// ACDREAM_PROBE_PLAYER_PRESENT names a file. Strip with the fix.
|
||||
_playerPresentProbe?.Observe(camera.Position, roots.PlayerViewPosition);
|
||||
|
||||
UpdateSunFromSky(foundation.Sky, roots.PlayerInsideCell);
|
||||
_lighting.UpdateViewerLight(roots.PlayerViewPosition);
|
||||
_lighting.Tick(camera.Position);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue