ZeusPlayerProxy: fix jitter por fome de deltas (mitigacao + anti-stale) [Change-Set: JITTER-STARVATION-2026-06-13]

Player remoto parado nao gera DELTA_BATCH (ComputeDelta suprime no server) -> o
proxy fica "mudo": NewestMs congela, renderLagMs cresce 1:1 com o wall-clock ->
extrapola sempre -> "anda no lugar" (moonwalk) e salta quando o player volta.
Provado em log (secsSinceSnap subindo enquanto o remoto esta moving=0). NAO e'
clock errado nem self-proxy (red-herrings descartados); o seed do relogio esta' OK.

- Tick: quando faminto (extrap alem de MaxExtrapolationSeconds+0.15s), zera a vel
  visual -> proxy vai pra Idle em vez de moonwalk.
- ApplyEntitySnapshot: anti-stale -- snapshot > 1000ms a frente do topo do buffer
  descarta o buffer + re-bootstrapa o ServerClockOffsetMs -> snap limpo (sem span
  gigante na interpolacao). Loga "buffer STALE gap=Xms -> reset+reseed".
- Logs DIAG PERMANENTES (Warning, mantidos de proposito -- ver memoria
  project_proxy_delta_starvation_jitter): SEED clock + renderLagMs/newestMs/
  secsSinceSnap/speed no Tick. Sonda anti-regressao.

Cobre tambem o interserver: shadow proxies parados sofrem a mesma fome.
Validado em jogo (user: "sem jitter nenhum"). Par server: PubV2-DIAG (mesmo Change-Set).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-13 01:26:33 -03:00
parent 341e6895c9
commit 4d737829f5
2 changed files with 61 additions and 10 deletions

View File

@@ -132,11 +132,18 @@ void AZeusPlayerProxy::Tick(const float DeltaSeconds)
// conhecida, clampada por `MaxExtrapolationSeconds` para nao deixar
// o proxy fugir se a rede ficar muda.
const FZeusProxySnapshot& Last = SnapshotBuffer.Last();
const float ExtrapSec = FMath::Clamp(
static_cast<float>(RenderMs - Last.ServerTimeMs) / 1000.0f,
0.0f, MaxExtrapolationSeconds);
const float RawExtrapSec =
static_cast<float>(RenderMs - Last.ServerTimeMs) / 1000.0f;
const float ExtrapSec = FMath::Clamp(RawExtrapSec, 0.0f, MaxExtrapolationSeconds);
NewPos = Last.PosCm + Last.VelCmS * ExtrapSec;
NewVel = Last.VelCmS;
// STARVATION (buffer=1 por muito tempo -- ex: 1o login sem DELTA_BATCH por
// segundos): a extrapolacao satura no clamp e a POSE congela, mas manter a
// velocidade faz o AnimBP rodar locomocao PARADO ("andar no lugar"/moonwalk
// = o "jitter" reportado). Se ja passamos bem do clamp, zera a vel visual
// pra o proxy ir pra Idle ate' os deltas voltarem (em vez de animar
// andando travado). Quando os deltas chegam, o ramo de interpolacao retoma.
const bool bStarved = RawExtrapSec > (MaxExtrapolationSeconds + 0.15f);
NewVel = bStarved ? FVector::ZeroVector : Last.VelCmS;
bExtrapolating = ExtrapSec > KINDA_SMALL_NUMBER;
}
else
@@ -207,13 +214,25 @@ void AZeusPlayerProxy::Tick(const float DeltaSeconds)
LastDiagLogSec = NowSec;
const int64 NewestMs = SnapshotBuffer.Last().ServerTimeMs;
const int64 RenderLagMs = ServerNowMs - NewestMs;
// DEBUG-PROXY (2026-06-12): Warning temporario p/ diagnosticar jitter-andando.
// extrap=1 frequente => buffer faminto (subir InterpolationDelayMs/rate).
// Rebaixar p/ Verbose depois de validar. speed em cm/s da velocidade visual.
// === DIAG PERMANENTE -- jitter por FOME DE DELTAS ========================
// Bug investigado/resolvido 2026-06-13 (ver memoria
// project_proxy_delta_starvation_jitter). Este Warning FICA DE PROPOSITO
// (decisao do dono: NAO rebaixar p/ Verbose nem remover) -- e' a sonda pra
// cacar regressao do jitter de proxy. Como ler:
// secsSinceSnap = ha quanto tempo o proxy NAO recebe snapshot. Sobe = FOME
// (player remoto parado -> ComputeDelta suprime no server -> sem DELTA_BATCH).
// newestMs = serverTimeMs do ultimo snapshot; CONGELA na fome (deveria avancar).
// renderLagMs = ServerNowMs - newestMs; cresce 1:1 com o wall-clock na fome.
// speed=0 na fome = a mitigacao (zera a vel visual -> Idle, sem "moonwalk") agindo.
// Ao o player voltar a se mover sai "buffer STALE ... reset+reseed" (anti-stale)
// e renderLagMs/secsSinceSnap voltam a ~0 (snap limpo, sem salto/jitter).
// =========================================================================
const double SecsSinceSnap =
(LastSnapshotRecvSec > 0.0) ? (NowSec - LastSnapshotRecvSec) : -1.0;
UE_LOG(LogZMMO, Warning,
TEXT("ZeusPlayerProxy[%lld] buffer=%d renderLagMs=%lld interpAlpha=%.2f extrap=%d delayMs=%.0f speed=%.0f"),
EntityId, SnapshotBuffer.Num(), RenderLagMs, InterpAlpha,
bExtrapolating ? 1 : 0, InterpolationDelayMs,
TEXT("ZeusPlayerProxy[%lld] buffer=%d renderLagMs=%lld newestMs=%lld secsSinceSnap=%.1f interpAlpha=%.2f extrap=%d speed=%.0f"),
EntityId, SnapshotBuffer.Num(), RenderLagMs, NewestMs, SecsSinceSnap, InterpAlpha,
bExtrapolating ? 1 : 0,
FVector(VisualVelocity.X, VisualVelocity.Y, 0.0f).Size());
}
}
@@ -226,6 +245,8 @@ void AZeusPlayerProxy::SetZeusIdentity(const int64 InEntityId, const EZeusEntity
void AZeusPlayerProxy::ApplyEntitySnapshot(const FZeusEntitySnapshot& Snapshot)
{
LastSnapshotRecvSec = FPlatformTime::Seconds(); // DIAG: marca chegada (mede a fome no Tick)
// 2026-06-06 H3 fix — disciplina o ServerClockOffsetMs via EMA com clamp.
// Antes: offset congelado na 1a amostra. Em cross-server handoff a fonte
// do ServerTimeMs muda (publisher antigo -> novo) e o offset fica
@@ -235,10 +256,34 @@ void AZeusPlayerProxy::ApplyEntitySnapshot(const FZeusEntitySnapshot& Snapshot)
// pra evitar salto visual durante transicao.
const int64 LocalNowMs = static_cast<int64>(FPlatformTime::Seconds() * 1000.0);
const int64 OffsetCandidate = LocalNowMs - Snapshot.ServerTimeMs;
// Anti-stale (2026-06-13): se este snapshot esta MUITO a frente do mais novo
// do buffer, o proxy ficou "mudo" por segundos -- player remoto parado ->
// ComputeDelta suprime -> sem DELTA_BATCH novo -> renderLagMs disparou pra
// dezenas de segundos. Manter os snapshots velhos faz a interpolacao usar um
// span gigante (pose de ~100s atras -> agora) quando o player volta a se mover,
// causando rasteio/salto (o jitter reportado). Descarta o buffer obsoleto +
// re-bootstrapa o relogio pra a interpolacao recomecar limpa neste keyframe.
if (SnapshotBuffer.Num() > 0
&& (Snapshot.ServerTimeMs - SnapshotBuffer.Last().ServerTimeMs) > 1000)
{
UE_LOG(LogZMMO, Warning,
TEXT("ZeusPlayerProxy[%lld] buffer STALE gap=%lldms -> reset+reseed"),
EntityId, Snapshot.ServerTimeMs - SnapshotBuffer.Last().ServerTimeMs);
SnapshotBuffer.Reset();
ServerClockOffsetMs = OffsetCandidate;
}
if (SnapshotBuffer.Num() == 0)
{
// Primeira amostra: bootstrap direto (nao tem baseline pra suavizar).
ServerClockOffsetMs = OffsetCandidate;
// DIAG (jitter 1o login): confirma o seed do relogio. serverTimeMs deve ser
// != 0 e o offset razoavel; se vier 0 aqui, o keyframe/catch-up corrompeu o
// bootstrap (hipotese descartada, mas o log fecha a questao em campo).
UE_LOG(LogZMMO, Warning,
TEXT("ZeusPlayerProxy[%lld] SEED clock serverTimeMs=%lld localNowMs=%lld offset=%lld"),
EntityId, Snapshot.ServerTimeMs, LocalNowMs, ServerClockOffsetMs);
}
else
{

View File

@@ -140,6 +140,12 @@ protected:
*/
int64 ServerClockOffsetMs = 0;
/** DIAG (2026-06-13): instante (s, monotonic) do ultimo snapshot recebido em
* ApplyEntitySnapshot. Usado no log do Tick pra medir "secsSinceSnap" = quanto
* tempo o proxy ficou MUDO (sem delta). secsSinceSnap grande == fome (player
* remoto parado -> ComputeDelta suprime). 0 = nenhum snapshot ainda. */
double LastSnapshotRecvSec = 0.0;
/** Aceleracao derivada (com smoothing) escrita no CMC custom para alimentar
* o AnimBP (`ShouldMove`/`Jump_Start` exigem Acceleration!=0). */
FVector LastDerivedAccelerationCmS2 = FVector::ZeroVector;