From 331bc10365c3af4d122599dd3f9603861de42945 Mon Sep 17 00:00:00 2001 From: Cyd Date: Tue, 11 Aug 2026 11:01:17 -0500 Subject: [PATCH] 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) --- MUNGA/MOVER.cpp | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/MUNGA/MOVER.cpp b/MUNGA/MOVER.cpp index 64d3d8e..e54cbba 100644 --- a/MUNGA/MOVER.cpp +++ b/MUNGA/MOVER.cpp @@ -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