The warmup path was the one that skipped the clamp

Live trace of the median predictor: in steady state the tick is gone -
zero spikes over five seconds where there were seven, every step blending
instead of 182 in 250, the blend fraction floored at 0.294 instead of
0.014, and the prediction within 13 to 20 ms of the gap that followed it.

But the log also read "predicting 2.054s", above the 1.0s clamp, which
should not have been reachable. It was: the fewer-than-three-samples path
returned the raw gap without clamping it. The arithmetic identifies it
exactly - 0.0201 / (2.054 + 0.0201) is 0.009685, against a logged blend
fraction of 0.00968523.

So the old near-stall survived, confined to the first three updates after
an entity appears. That is every respawn, and a pod is being watched
closely at exactly that moment.

Route every path through one ClampPredictedInterval, and tighten the
bounds now that the real send rate is known to be about 30ms: nothing
slower than half a second enters the sample window, and no prediction
reaches beyond 250ms. The second of those puts a floor under the blend
fraction itself - at a 20ms step the worst case is 0.02/(0.25+0.02),
roughly 7% of the gap per step, so a pod converges on its projection in a
dozen steps rather than crawling toward it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-08-11 08:46:31 -05:00
co-authored by Claude Opus 5
parent ec815b6216
commit 178714198a
+41 -16
View File
@@ -32,16 +32,47 @@ static Scalar gLastPercent = 0.0f;
// The second pair are a backstop on the answer, set deliberately wide so
// that in every sane case the median decides it and these never bind.
//
// Measured send rate on a live connection is about 30ms, so half a second
// is already sixteen times slower than anything healthy.
//
static const Scalar kMinimumUpdateInterval = 0.001f;
static const Scalar kOutlierUpdateInterval = 2.0f;
static const Scalar kOutlierUpdateInterval = 0.5f;
static const Scalar kMinimumPredictedInterval = 0.010f;
static const Scalar kMaximumPredictedInterval = 1.0f;
//
// Never predict further ahead than this, which puts a floor under the
// dead reckoner's blend fraction: at a 20ms step the worst case becomes
// 0.02/(0.25+0.02), near enough 7% of the gap per step, so a pod still
// converges on its projection in a dozen steps instead of crawling.
//
static const Scalar kMaximumPredictedInterval = 0.25f;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// RP412NETPREDICT=0 restores the original single-sample prediction, so the
// two can be compared on the same build and the same connection.
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// Every path out of PredictUpdateInterval goes through here. It used not
// to, and the one that skipped it was the bug.
//
static Scalar
ClampPredictedInterval(Scalar interval)
{
if (interval < kMinimumPredictedInterval)
{
return kMinimumPredictedInterval;
}
if (interval > kMaximumPredictedInterval)
{
return kMaximumPredictedInterval;
}
return interval;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
static Logical
UseMedianPrediction()
{
@@ -918,12 +949,16 @@ Scalar
}
//
// Too few samples to hold an opinion. Fall back to the old behaviour
// rather than inventing a rate we have no evidence for.
// Too few samples to hold an opinion. Fall back to the gap we just saw
// rather than inventing a rate we have no evidence for - but clamp it
// like any other answer. Leaving this path unclamped let a 2.05s gap
// through in the first updates after an entity appeared, which drove
// the blend fraction to 0.0097 and stalled the step. That is every
// respawn, and it is exactly when the pod is being watched.
//
if (updateIntervalCount < 3)
{
return latest;
return ClampPredictedInterval(latest);
}
//
@@ -950,17 +985,7 @@ Scalar
sorted[j + 1] = value;
}
Scalar median = sorted[updateIntervalCount / 2];
if (median < kMinimumPredictedInterval)
{
median = kMinimumPredictedInterval;
}
if (median > kMaximumPredictedInterval)
{
median = kMaximumPredictedInterval;
}
return median;
return ClampPredictedInterval(sorted[updateIntervalCount / 2]);
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~