The deadline belongs on the sender's clock

The prediction test answered clearly, though not through the verdict
label - that compared two noise floors with no absolute threshold and so
cried TIMING over errors of a millimetre. Read the magnitudes instead:
extrapolating the sender's own position and velocity across the gap
between two of the sender's own timestamps lands within 0.0005 to 0.011m.
Constant velocity holds to MILLIMETRES over one interval.

Against corrections of 0.25 to 0.66m that is a factor of five hundred, so
the two cannot be the same quantity. The corrections are not prediction
failure at all - they are the latency offset, which is what a dead
reckoner is supposed to carry.

That leaves the target, and the fault is mine. The dead reckoner projects
to updateOrigin + velocity * (nextUpdate - lastUpdate), so that difference
becomes a DISTANCE once multiplied by speed. lastUpdate is the sampling
moment RP412NETCLOCK computes, on the sender's clock. The median predictor
I added set nextUpdate from Now(), ours - so the subtraction spanned two
different timelines and yielded the interval plus however late that
particular packet ran.

At 52 m/s each millisecond of that is 52mm. Fifteen milliseconds of
ordinary jitter is three quarters of a metre of target error, enough to
collapse a one metre step to a third, and only on the packets that ran
late. An intermittent tick, worst when a pod is close and fast - which is
the symptom as it was reported.

Anchor nextUpdate to lastUpdate and the difference is the predicted
interval exactly. The target then depends on what the sender said and how
fast it is going, and not at all on the route the packet took.

NetClock confirmed live in the log, offset 52735ms, which is these two
machines' launch times differing now that the clock counts from launch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-08-11 11:01:17 -05:00
co-authored by Claude Opus 5
parent d147c093b2
commit 331bc10365
+37 -2
View File
@@ -1148,12 +1148,20 @@ void
//
nextUpdate = Now();
Scalar diff = nextUpdate - lastUpdate;
Scalar anchorInterval = (Scalar) 0;
if (diff < 10.0f)
{
if (UseMedianPrediction())
{
Scalar predicted = PredictUpdateInterval(diff);
//
// Anchor the projection to the SENDER's timeline, below,
// once Entity::ReadUpdateRecord has moved lastUpdate to
// the sampling moment RP412NETCLOCK worked out.
//
anchorInterval = predicted;
//
// Score the previous prediction against the gap that
// actually just elapsed - a true one-step-ahead error,
@@ -1173,8 +1181,6 @@ void
if (diff > widestGap) { widestGap = diff; }
if (diff > kLongGapThreshold) { longGapCount++; }
if (diff < kQueuedGapThreshold) { queuedGapCount++; }
nextUpdate += predicted;
}
else
{
@@ -1299,6 +1305,35 @@ void
//
Entity::ReadUpdateRecord(record);
//
// Put the projection deadline on the SENDER's timeline.
//
// The dead reckoner projects to updateOrigin + velocity *
// (nextUpdate - lastUpdate), so that difference is a DISTANCE
// once multiplied by speed - and a pod at 52 m/s turns every
// millisecond in it into 52mm of target.
//
// lastUpdate is the sampling moment RP412NETCLOCK computed, on
// the sender's clock. Setting nextUpdate from Now() measured the
// gap between two different timelines, so it came out as the
// interval PLUS however late this particular packet happened to
// be. Fifteen milliseconds of ordinary jitter became three
// quarters of a metre of target error, which is enough to
// collapse a one metre step to a third - and only on the packets
// that ran late, which is exactly the intermittent tick that was
// reported.
//
// Anchored to lastUpdate the difference is the predicted
// interval exactly, so the target depends on what the sender
// said and how fast it is going, and not at all on the route the
// packet took to reach us.
//
if (anchorInterval > (Scalar) 0)
{
nextUpdate = lastUpdate;
nextUpdate += anchorInterval;
}
//
//-----------------------
// Update the motion data