From 8d372ec0670ea5d75eb98247698223be7a27763a Mon Sep 17 00:00:00 2001 From: Cyd Date: Tue, 11 Aug 2026 09:01:55 -0500 Subject: [PATCH] The tick has a period, and the period is the evidence Rejecting the sender-stall theory was right: a gap of over a second is a death pause, not a tick. The remaining symptom is described precisely - a RHYTHMIC tick as a pod moves past the camera - and rhythm is the clue. A fixed period points at a cadence in our own code, because the network has no period. There are at least four candidates and they are only distinguishable by their interval: the 20ms physics step, the 30ms update rate, the sawtooth in the dead reckoner blend (percent climbs from 0.29 to 0.87 across each update interval as lastPerformance approaches nextUpdate, then resets), and the twenty-step quaternion renormalisation in Mover::BeginStep, which falls at 0.4s - a few times a second. So measure the interval rather than guess among them. Per frame, take the angle the traced pod subtends at the eye, flag the frames whose angular step is far above the running mean, and report the time BETWEEN those events. Angle rather than distance because a pod crossing the view moves far across the screen while barely changing range, which is the geometry the tick was reported in. Both samples come from one frame, marked by the eye's own frame counter rather than assumed from draw order, and the renderer reports on the same pod the mover trace describes - MoverTracedEntity now exposes that latch. Two traces about two different pods, or two different frames, would compare nothing; that mistake has already cost this investigation three wrong answers. Co-Authored-By: Claude Opus 5 (1M context) --- MUNGA/MOVER.cpp | 26 ++++++-- MUNGA/MOVER.h | 8 +++ MUNGA_L4/L4VIDRND.cpp | 144 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 171 insertions(+), 7 deletions(-) diff --git a/MUNGA/MOVER.cpp b/MUNGA/MOVER.cpp index 7e2f809..cf06aff 100644 --- a/MUNGA/MOVER.cpp +++ b/MUNGA/MOVER.cpp @@ -21,6 +21,21 @@ static Logical gLastLerpUsed = False; static Scalar gLastPercent = 0.0f; +// +// The one replicant the RP412CAMLOG traces describe. Latched here because +// the renderer reports on the same entity from the other end - what its +// motion looks like on screen - and two traces about two different pods +// would compare nothing. +// +static EntityID gTracedEntity = EntityID::Null; +static Logical gTracedLatched = False; + +EntityID + MoverTracedEntity() +{ + return gTracedEntity; +} + // // Bounds on the replication interval estimate, in seconds. // @@ -693,15 +708,12 @@ void // if (RPCameraLog()) { - static EntityID watched = EntityID::Null; - static Logical latched = False; - - if (!latched) + if (!gTracedLatched) { - latched = True; - watched = GetEntityID(); + gTracedLatched = True; + gTracedEntity = GetEntityID(); } - if (watched == GetEntityID()) + if (gTracedEntity == GetEntityID()) { static Scalar next_say = 0.0f; static Point3D last_pos(0.0f, 0.0f, 0.0f); diff --git a/MUNGA/MOVER.h b/MUNGA/MOVER.h index 53cb685..3073d17 100644 --- a/MUNGA/MOVER.h +++ b/MUNGA/MOVER.h @@ -509,3 +509,11 @@ public: Logical TestInstance() const; }; + +// +// The replicant the RP412CAMLOG traces are describing, so the renderer can +// report on the same pod from the other end - what its motion looks like on +// screen. Null until a replicant has stepped at least once. +// +EntityID + MoverTracedEntity(); diff --git a/MUNGA_L4/L4VIDRND.cpp b/MUNGA_L4/L4VIDRND.cpp index 50e2383..bc31eb1 100644 --- a/MUNGA_L4/L4VIDRND.cpp +++ b/MUNGA_L4/L4VIDRND.cpp @@ -11,6 +11,16 @@ #include "l4app.h" #include "..\RP\VTV.h" +// +// Where the eye was this frame, and a counter of frames the eye has been +// built for. Written by DPLEyeRenderable::Execute, read by +// RootRenderable::Execute, which draws afterwards in the same frame - the +// counter is what proves that rather than assuming it. +// +static D3DXVECTOR3 gEyeWorld(0.0f, 0.0f, 0.0f); +static unsigned gEyeFrame = 0; +static unsigned gLastSampledFrame = 0; + // RB 1/14/07 //#include //#include @@ -1095,6 +1105,126 @@ void Matrix4x4 tempMatrix; tempMatrix = renderToWorld; + // + // RP412CAMLOG: what the tick actually looks like on screen. + // + // The complaint is a RHYTHMIC tick as a pod moves past the camera, + // and rhythm is the clue: a fixed period points at a cadence in our + // own code rather than at the network, which has no period. So + // measure the angle the pod subtends at the eye, frame by frame, + // and report the INTERVAL BETWEEN the lurches rather than merely + // counting them. That interval names the culprit - about 0.02s is + // the physics step, 0.03s the update rate, 0.4s the twenty-step + // quaternion renormalisation in Mover::BeginStep. + // + // Angle rather than distance because a pod crossing the view moves + // far across the screen while barely changing range, which is + // exactly the geometry the tick was reported in. + // + // Both samples are taken in one frame - the eye's, marked by + // gEyeFrame - because a pod and a camera read from different frames + // alias against each other no matter what the game is doing. + // + if (RPCameraLog() && gEyeFrame != gLastSampledFrame) + { + EntityID traced = MoverTracedEntity(); + + if (traced != EntityID::Null && traced == myEntity->GetEntityID()) + { + gLastSampledFrame = gEyeFrame; + + Vector3D to_pod; + to_pod.x = renderToWorld(3,0) - gEyeWorld.x; + to_pod.y = renderToWorld(3,1) - gEyeWorld.y; + to_pod.z = renderToWorld(3,2) - gEyeWorld.z; + + Scalar range = to_pod.Length(); + + if (range > 0.1f) + { + static Vector3D last_dir; + static Logical have_last = False; + static Scalar next_say = 0.0f; + static Scalar mean_angle = 0.0f; + static Scalar last_event = 0.0f; + static Scalar interval_sum = 0.0f; + static Scalar interval_min = 1000.0f; + static Scalar interval_max = 0.0f; + static int interval_count = 0; + static int frames = 0; + static int lurches = 0; + + Vector3D dir; + dir.Divide(to_pod, range); + ++frames; + + if (have_last) + { + Scalar dot = + dir.x*last_dir.x + dir.y*last_dir.y + dir.z*last_dir.z; + + if (dot > 1.0f) { dot = 1.0f; } + if (dot < -1.0f) { dot = -1.0f; } + + Scalar angle = (Scalar) acos((double) dot); + + // + // Only judge while the pod is actually crossing the + // view. A pod held near the centre subtends almost + // no angle and every ratio against it is noise. + // + if (mean_angle > 0.0002f) + { + if (angle > mean_angle * 2.5f) + { + ++lurches; + + Scalar now = (Scalar) Now(); + + if (last_event > 0.0f) + { + Scalar gap = now - last_event; + + interval_sum += gap; + ++interval_count; + if (gap < interval_min) { interval_min = gap; } + if (gap > interval_max) { interval_max = gap; } + } + last_event = now; + } + } + mean_angle = (mean_angle > 0.0f) + ? (mean_angle * 0.95f + angle * 0.05f) + : angle; + } + last_dir = dir; + have_last = True; + + if ((Scalar) Now() >= next_say) + { + if (next_say > 0.0f) + { + DEBUG_STREAM << "CamLog: on-screen motion - " << frames + << " frames, " << lurches << " lurch(es), period " + << ((interval_count > 0) + ? (interval_sum / interval_count) + : 0.0f) + << "s (" << interval_min << ".." << interval_max + << "), mean step " << (mean_angle * 1000.0f) + << " mrad, range " << range << "m\n" << std::flush; + } + next_say = ((Scalar) Now()) + 5.0f; + frames = 0; + lurches = 0; + interval_sum = 0.0f; + interval_min = 1000.0f; + interval_max = 0.0f; + interval_count = 0; + } + } + } + } + myRenderer->GetMatrixStack()->MultMatrix(&tempMatrix.ToD3DMatrix()); //myLocalToWorld = *myRenderer->GetMatrixStack()->GetTop(); HierarchicalDrawComponent::SetLocalToWorld(myRenderer->GetMatrixStack()->GetTop()); @@ -5695,6 +5825,20 @@ void mat = mat4; D3DXVECTOR3 pos(mat(3,0), mat(3, 1), mat(3,2)); + + // + // Where the eye ended up this frame, for the on-screen motion + // trace in RootRenderable::Execute. The eye is built once per + // frame and before the world is drawn, so it also serves as the + // frame marker that keeps the two samples in the same frame - + // which is the whole point, since sampling a pod and a camera + // from different frames is what made three earlier attempts at + // this question meaningless. + // + gEyeWorld.x = mat(3,0); + gEyeWorld.y = mat(3,1); + gEyeWorld.z = mat(3,2); + gEyeFrame++; mat(3,0) = 0; mat(3,1) = 0; mat(3,2) = 0;