Score the sender against itself

The tick is now fully accounted for. Corrections arrive about 26 times a
second per pod, mean 0.25 to 0.66m against a step of roughly a metre.
localOrigin is never snapped during a mission - only updateOrigin is
replaced - so the pod position stays continuous and its TARGET jumps. The
lerp then translates a target discontinuity into a step-size one: fifteen
steps smooth to a tenth of a percent, then a single step at 20-33%, then
recovery. Every capture has that shape and the ratios match the correction
size arithmetically.

Position is C0-continuous, so no amount of position interpolation can hide
it; the discontinuity is in the rate.

Before smoothing anything, ask whether the correction is even real. Half a
metre at 52 m/s is ten milliseconds of travel, and a dead reckoner tracking
constant velocity across a 38ms gap should be right to within centimetres.
That smells like evaluating the projection at the wrong instant rather
than like a prediction that genuinely failed.

Settle it without involving any clock we do not trust. Take the position
and velocity the sender reported last time, carry them forward by the gap
between the two SENDER timestamps, and compare against the position the
sender reports now. Both stamps come from one machine, so latency, clock
offset and RP412NETCLOCK play no part whatsoever.

Split the error along the path and across it. Along is time: divided by
speed it IS the milliseconds the window is out by, and its sign says which
way. Across cannot be a timing fault at all - that is a pod turning, and
no clock fix would touch it.

TIMING says fix the extrapolation window and the tick shrinks at the
source with no smoothing and no lag. MANOEUVRE says the corrections are
honest, the pods really are cornering, and smoothing is the only remaining
answer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-08-11 10:50:53 -05:00
co-authored by Claude Opus 5
parent da7c35cac1
commit d147c093b2
2 changed files with 136 additions and 0 deletions
+121
View File
@@ -36,6 +36,20 @@ EntityID
return gTracedEntity;
}
//
// Prediction-error totals for the RP412CAMLOG trace. Shared across
// replicants deliberately: the question - does constant-velocity
// extrapolation hold over one send interval - is about the model, not
// about any one pod, so a whole grid contributing samples is a better
// answer rather than a muddled one.
//
static int gPredictSamples = 0;
static Scalar gPredictAlong = 0.0f;
static Scalar gPredictAlongAbs = 0.0f;
static Scalar gPredictAcross = 0.0f;
static Scalar gPredictMilliseconds = 0.0f;
static Scalar gPredictNextSay = 0.0f;
//
// Bounds on the replication interval estimate, in seconds.
//
@@ -1177,6 +1191,107 @@ void
ResetUpdateIntervals();
}
//
// RP412CAMLOG: is constant-velocity extrapolation actually
// accurate over one interval, or is the pod manoeuvring?
//
// The corrections measured 0.25 to 0.66m against a step of
// about a metre, which is what collapses one step to a third
// and shows as the tick. At 52 m/s half a metre is ten
// milliseconds of travel, so the question is whether we are
// evaluating the projection at the wrong INSTANT or whether the
// pod simply is not going in a straight line.
//
// This settles it without involving any clock we do not trust:
// take the position and velocity the sender reported last time,
// carry them forward by the difference between the two SENDER
// timestamps, and compare against the position the sender
// reports now. Both stamps come from the same machine, so
// latency, clock offset and RP412NETCLOCK play no part - it
// measures the prediction and nothing else.
//
// Split the error along the direction of travel and across it.
// Error ALONG the path is time: divided by speed it IS the
// number of milliseconds the window is out by, and its sign
// says which way. Error ACROSS the path cannot be a timing
// problem at all - that is a pod turning, and no clock fix
// would touch it.
//
if (RPCameraLog())
{
UpdateRecord *sample = (UpdateRecord*)record;
if (haveSenderSample)
{
Scalar dt = sample->timeStamp - senderStamp;
Scalar speed = senderVelocity.Length();
if (dt > 0.001f && dt < 1.0f && speed > 1.0f)
{
Vector3D error;
error.x = sample->localOrigin.linearPosition.x
- (senderPosition.x + senderVelocity.x * dt);
error.y = sample->localOrigin.linearPosition.y
- (senderPosition.y + senderVelocity.y * dt);
error.z = sample->localOrigin.linearPosition.z
- (senderPosition.z + senderVelocity.z * dt);
Scalar along =
(error.x * senderVelocity.x
+ error.y * senderVelocity.y
+ error.z * senderVelocity.z) / speed;
Vector3D across;
across.x = error.x - (senderVelocity.x / speed) * along;
across.y = error.y - (senderVelocity.y / speed) * along;
across.z = error.z - (senderVelocity.z / speed) * along;
gPredictSamples++;
gPredictAlong += along;
gPredictAlongAbs += (along < 0.0f) ? -along : along;
gPredictAcross += across.Length();
gPredictMilliseconds += (along / speed) * 1000.0f;
Scalar now_say = (Scalar) Now();
if (now_say >= gPredictNextSay)
{
if (gPredictNextSay > 0.0f && gPredictSamples > 0)
{
Scalar mean_along = gPredictAlong / gPredictSamples;
Scalar mean_across = gPredictAcross / gPredictSamples;
Scalar mean_ms =
gPredictMilliseconds / gPredictSamples;
DEBUG_STREAM << "CamLog: prediction error - "
<< gPredictSamples << " intervals, along "
<< mean_along << "m (" << mean_ms
<< "ms of travel), across " << mean_across
<< "m, verdict "
<< (((mean_along < 0.0f ? -mean_along : mean_along)
> mean_across * 2.0f)
? "TIMING - the window is off"
: ((mean_across
> (mean_along < 0.0f ? -mean_along : mean_along) * 2.0f)
? "MANOEUVRE - the pod is turning"
: "mixed"))
<< "\n" << std::flush;
}
gPredictNextSay = now_say + 5.0f;
gPredictSamples = 0;
gPredictAlong = 0.0f;
gPredictAlongAbs = 0.0f;
gPredictAcross = 0.0f;
gPredictMilliseconds = 0.0f;
}
}
}
senderStamp = sample->timeStamp;
senderPosition = sample->localOrigin.linearPosition;
senderVelocity = sample->worldLinearVelocity;
haveSenderSample = True;
}
//
//---------------------------------------
// Handle updating the entity information
@@ -2199,6 +2314,12 @@ Mover::Mover(
widestGap = 0.0f;
longGapCount = 0;
queuedGapCount = 0;
haveSenderSample = False;
senderStamp = lastUpdate;
senderPosition = localOrigin.linearPosition;
senderVelocity.x = 0.0f;
senderVelocity.y = 0.0f;
senderVelocity.z = 0.0f;
normalizeCount = 0;
if (IsInitialStasis())
+15
View File
@@ -324,6 +324,21 @@ protected:
longGapCount,
queuedGapCount;
//
// The last position, velocity and timestamp the SENDER reported, kept
// so an arriving update can be scored against what the one before it
// predicted. All three come from the same machine, so the comparison
// owes nothing to latency or to clock alignment.
//
Time
senderStamp;
Point3D
senderPosition;
Vector3D
senderVelocity;
Logical
haveSenderSample;
Scalar
PredictUpdateInterval(Scalar latest);
void