From adcb81e7fc82f934c119656d883ebb84e251cd82 Mon Sep 17 00:00:00 2001 From: Cyd Date: Mon, 10 Aug 2026 23:36:44 -0500 Subject: [PATCH] Replicants interpolate too The first cut hung the snapshot off Mover::BeginStep, which is inside Entity::PerformAndWatch's fixed-step interleave - and that interleave sits entirely inside "if (GetInstance() != ReplicantInstance)". A replicant never runs it; it reaches the step loop through Simulation::PerformAndWatch instead. Every remote pod is a replicant, so on a Live Cam the camera was being interpolated while the car it was watching still stepped. Smoother, and most of the way to nowhere - which is exactly what "still some hitching" was. So the hooks move to Simulation::PerformTo, where both paths meet: SnapshotRenderOrigin before each Perform, SetRenderStepFraction after the loop, two virtuals that do nothing by default and are overridden by Entity because Entity owns the origin. Mover::BeginStep goes back to what it was, so there is now one mechanism instead of two. Taking the snapshot inside the step loop is also strictly better placed than BeginStep was: it lands immediately before the integration, and still after any BeginStep teleport, so a VTV's scheduled respawn stays a cut. Entity::PerformAndWatch keeps computing the fraction itself after its interleave, because there PerformTo is called once per step with a till one step ahead and so sees no leftover at all - it needs the FRAME's till, which only the interleave has. Determinism re-proved, and more thoroughly than the first time. The scripted lap at 240 fps, interpolation on and off, on both the old build and this one: all four runs agree to the last decimal at the same simulation time - pos -15.06739 3.01541 388.02603 at t=15.260. The one "differing" sample in the raw comparison was the trace sampling at t=1.260 in one run and t=1.280 in the other and then realigning, not divergence. Co-Authored-By: Claude Opus 5 (1M context) --- MUNGA/ENTITY.cpp | 14 ++++++++++++++ MUNGA/ENTITY.h | 9 +++++++++ MUNGA/MOVER.cpp | 13 ------------- MUNGA/SIMULATE.cpp | 32 ++++++++++++++++++++++++++++++++ MUNGA/SIMULATE.h | 17 +++++++++++++++++ 5 files changed, 72 insertions(+), 13 deletions(-) diff --git a/MUNGA/ENTITY.cpp b/MUNGA/ENTITY.cpp index 1631a0a..af7c005 100644 --- a/MUNGA/ENTITY.cpp +++ b/MUNGA/ENTITY.cpp @@ -1425,6 +1425,20 @@ static Logical return enabled ? True : False; } +void + Entity::SnapshotRenderOrigin() +{ + Check(this); + renderPreviousOrigin = localOrigin; +} + +void + Entity::SetRenderStepFraction(Scalar fraction) +{ + Check(this); + renderStepFraction = fraction; +} + void Entity::GetRenderToWorld(LinearMatrix *out) { diff --git a/MUNGA/ENTITY.h b/MUNGA/ENTITY.h index e6a1f34..ea7787d 100644 --- a/MUNGA/ENTITY.h +++ b/MUNGA/ENTITY.h @@ -186,6 +186,15 @@ public: void GetRenderToWorld(LinearMatrix *out); + // + // Filled by Simulation::PerformTo around each fixed step, for locally + // simulated entities and replicants alike. + // + void + SnapshotRenderOrigin(); + void + SetRenderStepFraction(Scalar fraction); + int GetDamageZoneIndex(const CString &damage_zone_name) const; diff --git a/MUNGA/MOVER.cpp b/MUNGA/MOVER.cpp index 2060331..2b0095b 100644 --- a/MUNGA/MOVER.cpp +++ b/MUNGA/MOVER.cpp @@ -699,19 +699,6 @@ void localAcceleration = Motion::Identity; previousOrigin = localOrigin; - // - // The same snapshot again, for DRAWING. Deliberately not previousOrigin - // itself: that one belongs to the physics, and rendering must not be - // aliased to a member the simulation is free to repurpose. - // - // This runs AFTER a VTV's scheduled respawn has teleported localOrigin, - // because VTV::BeginStep applies the respawn and then calls us. So a - // respawn leaves the two equal and the blend has nothing to travel - a - // teleport stays a cut instead of becoming a 300-metre slide across the - // map. See entity.h. - // - renderPreviousOrigin = localOrigin; - if (++normalizeCount >= 20) { localOrigin.angularPosition.Normalize(); diff --git a/MUNGA/SIMULATE.cpp b/MUNGA/SIMULATE.cpp index b86a1f1..d07bc3c 100644 --- a/MUNGA/SIMULATE.cpp +++ b/MUNGA/SIMULATE.cpp @@ -774,6 +774,13 @@ void while (behind >= step && taken < max_steps) { + // + // Where this step STARTED, for drawing. Taken here rather than + // in BeginStep so it covers replicants too, and taken after any + // BeginStep teleport (a VTV's scheduled respawn) so a jump + // stays a cut instead of becoming a slide. + // + SnapshotRenderOrigin(); Perform(step); ++gPhysicsStepsTaken; lastPerformance += step; @@ -781,6 +788,19 @@ void ++taken; } + // + // How far past the last completed step the frame being drawn falls. + // Entity::PerformAndWatch computes this again from the FRAME's till + // after its interleave, because there this function is called once + // per step and sees no leftover at all. + // + { + Scalar fraction = behind / step; + if (fraction < (Scalar) 0) fraction = (Scalar) 0; + if (fraction > (Scalar) 1) fraction = (Scalar) 1; + SetRenderStepFraction(fraction); + } + // // A machine that cannot keep up must not try to buy back the whole // backlog next frame - that costs more time, which makes a bigger @@ -808,6 +828,18 @@ void // nothing by default - see the header } +void + Simulation::SnapshotRenderOrigin() +{ + // nothing by default - only an Entity has an origin to snapshot +} + +void + Simulation::SetRenderStepFraction(Scalar) +{ + // nothing by default - see the header +} + void Simulation::WatchAndWrite(MemoryStream *update_stream) { diff --git a/MUNGA/SIMULATE.h b/MUNGA/SIMULATE.h index 0c844b1..69fe855 100644 --- a/MUNGA/SIMULATE.h +++ b/MUNGA/SIMULATE.h @@ -170,6 +170,23 @@ public: virtual void BeginStep(); + // + // Render interpolation hooks, called by PerformTo around the fixed + // step. They live HERE rather than on the entity interleave because a + // REPLICANT never runs that interleave - it reaches PerformTo through + // Simulation::PerformAndWatch instead - and a replicant is exactly what + // every remote pod is. Hanging the snapshot off BeginStep left the + // watched car uninterpolated while the camera watching it was smooth, + // which is most of the way to nowhere. + // + // Defaults do nothing; Entity overrides them because it owns the + // origin. See Entity::GetRenderToWorld. + // + virtual void + SnapshotRenderOrigin(); + virtual void + SetRenderStepFraction(Scalar fraction); + // // The fixed step in seconds, 0 when frame-coupled. Global on purpose: // a mixed-rate simulation would be a worse bug than either mode.