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:
+121
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user