From b0b40559d54ff19ca3a32e4af46e75824908a719 Mon Sep 17 00:00:00 2001 From: Cyd Date: Tue, 11 Aug 2026 11:15:53 -0500 Subject: [PATCH] A fraction of zero is a place, not an absence The drawn pod stalled about fifteen times in 293 frames while the simulation stepped perfectly smoothly through the same window - and 15 of 293 is 5%, which is exactly the count of frames the earlier trace found sitting at a render fraction of zero. That was the whole clue. GetRenderToWorld tested the fraction to decide whether to interpolate at all: if (renderStepFraction <= 0 || !RenderInterpolationEnabled()) *out = localToWorld; Drawing at fraction f means drawing at the start of the step plus f of it, so f = 0 means the START of the step. localToWorld is its END. The two are a whole step apart, about a metre at racing speed. behind is a whole number of milliseconds against a 20ms step, so it lands on exactly zero roughly one frame in twenty. On those frames the pod was drawn a full step ahead of itself and snapped back on the next one. Three times a second at 59fps, regular because the beat between frame rate and step rate is regular, and worst when a pod crosses the view quickly - which is the symptom as it was first described, and it took this long to find because every simulation trace was right. Only the drawing was wrong. Ask renderStepTaken instead, which is what the condition meant all along. Interpolating at f = 0 is continuous with its neighbours: each frame advances the drawn position by frame_time / step whether or not a step boundary falls between the two, which is the entire point. The same mistake was in DPLEyeRenderable's rebuild gate, using the fraction as a proxy for whether interpolation was running. Same fix. renderStepTaken is cleared where localOrigin is assigned outside the step loop, so a stale snapshot is never blended from. Render path only - localOrigin is untouched, so physics, collisions and determinism are unaffected. The foreign-eye rejection added in the previous build turned out not to be the cause: it rejected between zero and three samples per window against stall counts in the twenties. Keeping it, since sampling one viewpoint against another was still wrong, but it was not this. Co-Authored-By: Claude Opus 5 (1M context) --- MUNGA/ENTITY.cpp | 37 +++++++++++++++++++++++++++++++++---- MUNGA/ENTITY.h | 9 +++++++++ MUNGA_L4/L4VIDRND.cpp | 2 +- 3 files changed, 43 insertions(+), 5 deletions(-) diff --git a/MUNGA/ENTITY.cpp b/MUNGA/ENTITY.cpp index 6ce16ec..89e1ff3 100644 --- a/MUNGA/ENTITY.cpp +++ b/MUNGA/ENTITY.cpp @@ -506,6 +506,13 @@ void { localOrigin = updateOrigin; localToWorld = localOrigin; + + // + // Set outside the step loop, so renderPreviousOrigin now + // describes a step that never happened. Draw plainly until + // a real one does. + // + renderStepTaken = False; } Simulation::ReadUpdateRecord(record); } @@ -1137,6 +1144,7 @@ Entity::Entity( // renderPreviousOrigin = localOrigin; renderStepFraction = (Scalar) 0; + renderStepTaken = False; // initialize camera stuff cameraOffset = Origin::Identity; @@ -1485,6 +1493,7 @@ void { Check(this); renderPreviousOrigin = localOrigin; + renderStepTaken = True; } void @@ -1501,11 +1510,31 @@ void Check_Pointer(out); // - // Nothing to blend towards: no fraction of a step outstanding, no - // fixed step at all, or interpolation switched off. Hand back exactly - // what every caller used before this existed. + // No fixed step at all, interpolation switched off, or this entity has + // not taken a step yet. Hand back exactly what every caller used before + // this existed. // - if (renderStepFraction <= (Scalar) 0 || !RenderInterpolationEnabled()) + // The test used to be on the fraction rather than on renderStepTaken, + // and that was wrong in a way that showed. Drawing at fraction f means + // drawing at the start of the step plus f of it, so f = 0 means the + // START of the step - while this early return hands back localToWorld, + // which is its END. The two are a whole step apart, about a metre at + // racing speed. + // + // behind is a whole number of milliseconds against a 20ms step, so it + // lands on exactly zero about one frame in twenty. On those frames a + // pod was drawn a full step ahead of itself and then snapped back on + // the next one: a jump three times a second at 59fps, regular because + // the beat between frame rate and step rate is regular, and worst when + // a pod crosses the view quickly. It never appeared in any simulation + // trace because the simulation was right - only the drawing was wrong. + // + // Interpolating at f = 0 is continuous with everything either side of + // it. Each frame advances the drawn position by frame_time / step + // whether or not a step boundary falls between the two, which is the + // entire point of doing it. + // + if (!RenderInterpolationEnabled() || !renderStepTaken) { *out = localToWorld; return; diff --git a/MUNGA/ENTITY.h b/MUNGA/ENTITY.h index ea7787d..115f94b 100644 --- a/MUNGA/ENTITY.h +++ b/MUNGA/ENTITY.h @@ -177,6 +177,15 @@ public: Origin renderPreviousOrigin; Scalar renderStepFraction; + // + // Whether renderPreviousOrigin describes a step this entity actually + // took. It has to be asked separately from the fraction, because a + // fraction of zero is a perfectly ordinary place to be drawing - see + // GetRenderToWorld for the one-step jump that testing the fraction + // instead used to produce. + // + Logical renderStepTaken; + // // The transform to DRAW with. Falls back to localToWorld verbatim when // interpolation is off, when there is no fixed step to interpolate diff --git a/MUNGA_L4/L4VIDRND.cpp b/MUNGA_L4/L4VIDRND.cpp index 89e63ff..72e29d7 100644 --- a/MUNGA_L4/L4VIDRND.cpp +++ b/MUNGA_L4/L4VIDRND.cpp @@ -5973,7 +5973,7 @@ void // whenever interpolation is off or idle, so the old behaviour is // untouched. See Entity::GetRenderToWorld. // - if((myEyepointRotation && *myEyepointRotation != oldEyepointRotation) || oldLocalToWorld != myEntity->localToWorld || mForceUpdate || myEntity->renderStepFraction > (Scalar) 0) + if((myEyepointRotation && *myEyepointRotation != oldEyepointRotation) || oldLocalToWorld != myEntity->localToWorld || mForceUpdate || myEntity->renderStepTaken) { mForceUpdate = false; oldLocalToWorld = myEntity->localToWorld;