diff --git a/docs/dev/high-framerate-rendering-plan.md b/docs/dev/high-framerate-rendering-plan.md index dc5a34b9..1968d7a5 100644 --- a/docs/dev/high-framerate-rendering-plan.md +++ b/docs/dev/high-framerate-rendering-plan.md @@ -263,7 +263,9 @@ Implemented scope: This closes the mover-relative case. The camera reaches the current authoritative pose one tic late, so a lift or tram drawn at its authoritative pose separated from the rider's eye by a whole tic of its own travel and snapped back every tic. The result was a 60 Hz sawtooth of everything the player was riding — invisible against static geometry at any speed, and filling the entire view inside a tram car (`game/storage2`, `game/tram1b`). - Two rules keep the scene on one clock rather than trading one mismatch for another. `idPlayer::CanInterpolatePresentationView()` refuses to interpolate the eye when its carrier — bind master, or the entity under its feet — moved but cannot be interpolated this frame. `idPlayer::IsPresentationViewInterpolated()` gates the entity pass on the drawn camera actually being the interpolated first-person pose, so cinematic, private-camera and third-person frames put the whole scene back on the authoritative clock together. The view model opts out because `UpdatePresentationWeapon()` already owns it, and actors opt out in multiplayer so remote players keep being drawn where the server rewinds them to. + Two rules keep the scene on one clock rather than trading one mismatch for another. `idPlayer::CanInterpolatePresentationView()` refuses to interpolate the eye when its carrier — the vehicle being driven, the bind master, or the entity under its feet — moved but cannot be interpolated this frame. `idPlayer::IsPresentationViewInterpolated()` gates the entity pass on the drawn camera actually being the interpolated first-person pose, so cinematic, private-camera and third-person frames put the whole scene back on the authoritative clock together. The view model opts out because `UpdatePresentationWeapon()` already owns it, and actors opt out in multiplayer so remote players keep being drawn where the server rewinds them to. + + The samples are taken from `renderEntity.origin/axis` — the composed *visual* transform — not from the physics pose. That distinction is load-bearing rather than cosmetic: `rvVehicleAnimated` (the walker) keeps its entire heading in `idActor::viewAxis` and exposes it through `GetPhysicsToVisualTransform()`, while its physics axis barely turns, so interpolating physics left the vehicle body snapping once a tic underneath a smoothly turning camera. Every actor carries a `modelOffset` the same way, and AF-driven entities compose their visual transform there too. Sampling the visual transform covers all of them, and means `UpdatePresentationPose()` must not apply the physics-to-visual step a second time. Validation recorded for this slice: @@ -279,6 +281,7 @@ Explicitly deferred: - world-space BSE effects that are not client entities bound to an interpolated entity, plus trail, fracture and other bespoke visual owners that rebuild geometry per tic rather than moving a transform - alternate camera, security-camera, portal-sky and cinematic-camera interpolation. These are not vibration sources today because `IsPresentationViewInterpolated()` puts the whole scene back on the authoritative clock whenever one of them is the drawn view, but it does mean high-refresh smoothing is unavailable in those modes - animated interpolated entities still drop their cached dynamic model on every draw frame, because the renderer's `r_useRepeatedStateReuse` transform-only path requires `renderEntity.callback == NULL`. This is a CPU cost on a small set of entities, not a correctness gap; extending the reuse path to callback entities would need a way to prove the callback is time-only +- **skeletal joints are still evaluated on the simulation clock.** `idEntity::UpdateRenderEntity()` calls `animator->CreateFrame( gameLocal.time )`, so an interpolated entity moves its root smoothly while its joints step at 60 Hz. Usually invisible, because root motion dominates. It is visible where a *camera* is anchored to a joint of the thing it is looking at: `rvVehiclePosition::GetPosition()` builds the driver's eye from `GetJointTransform( eyeJoint, gameLocal.GetTime() )`, so the eye is interpolated across the vehicle's animation while the cockpit around it is drawn at the authoritative joint pose. Residual amplitude is one tic of eye-joint travel — the walk-cycle bob, not the vehicle's heading. `idAnimator::CreateFrame()` is a pure function of the requested time and keyframe-interpolates at arbitrary milliseconds, so passing the presentation clock there would close this and smooth all skeletal animation at high refresh; the open question is the cached `lastTransformTime` / `joints[]` state it shares with gameplay joint queries, which is why it has not been done as part of this pass - same-fire-frame weapon FX/tracer compensation and other presentation-time gameplay-adjacent cosmetic retraces - raw-input compensation or presentation bias/extrapolation; the engine's optional presentation-input sampler is not consumed by the current game-library slice - a successful full loopback-client capture, human SP/MP feel testing, cut-heavy cinematic coverage, Apple-platform testing, and dedicated high-refresh display/hardware qualification