Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/dev/high-framerate-rendering-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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
Expand Down
Loading