fix(teleport): dungeon-exit priority-apply footgun + wall-clock timeout + arrival facing

T6 visual gate found three issues; probe pinned the roots.

- StreamingController.DrainAndApply: the priority hunt + outbox drain were gated
  behind 'if (budget <= 0) return;' AFTER the deferred-buffer drain. During a
  dungeon-exit expand (~600 completions) the buffer is always >= budget, so the
  hunt never ran and the destination never priority-applied (APPLY stalled ~5s;
  dest built +265ms but applied +6s). Restructured: priority hunt runs FIRST and
  unconditionally. Indoor (single-LB collapse) was unaffected, which is why only
  outdoor exits broke.
- Teleport timeout was a 600-FRAME count; the empty-world exit hold renders at
  ~1000fps so it fired in ~0.6s and force-placed into the skybox before the
  expand finished. Now wall-clock (10s) — worldReady wins the race.
- Arrival facing: synced _playerController.Yaw to the server orientation via the
  exact inverse of YawToAcQuaternion (ExtractYawFromQuaternion has a 270deg offset
  vs our yaw). Previously only the render mesh got the rotation → camera/movement
  faced the stale pre-teleport direction.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-22 14:27:55 +02:00
parent 3ce1fae332
commit 6e78fcd7f6
2 changed files with 63 additions and 56 deletions

View file

@ -332,66 +332,46 @@ public sealed class StreamingController
/// </summary>
private void DrainAndApply()
{
int budget = MaxCompletionsPerFrame;
// --- Step 1: drain the deferred buffer first (items held from a prior
// priority-hunt that overshot the cap). Apply up to `budget` of them.
int i1 = 0;
while (i1 < budget && i1 < _deferredApply.Count) { ApplyResult(_deferredApply[i1]); i1++; }
if (i1 > 0) _deferredApply.RemoveRange(0, i1);
budget -= i1;
if (budget <= 0) return;
// --- Step 2: priority hunt (only when a destination is set).
// --- Step 1: priority hunt FIRST and UNCONDITIONALLY (when a destination is set).
// The teleport destination must apply the frame the worker finishes building it,
// ahead of any deferred/outbox backlog — otherwise a big dungeon-exit expand
// (hundreds of completions) buries it and the arrival times out into the skybox.
// CRITICAL: this must NOT be gated behind the per-frame budget. The prior version
// ran the deferred drain first and returned when the budget hit 0, so once
// _deferredApply held >= MaxCompletionsPerFrame items the hunt (and the outbox
// drain) were skipped entirely — the destination never priority-applied. The hunt
// drains the outbox in bounded chunks; the priority is applied on match, every
// non-priority item is buffered in _deferredApply (no loss).
if (PriorityLandblockId != 0u)
{
// Drain in chunks of MaxCompletionsPerFrame (bounded at 64 total
// iterations to avoid an unbounded loop if the outbox is huge).
const int MaxHuntIterations = 64;
const int MaxHuntIterations = 64; // cap at 64 * MaxCompletionsPerFrame drained/frame
int hunted = 0;
bool found = false;
while (hunted < MaxHuntIterations && !found)
while (hunted < MaxHuntIterations)
{
var chunk = _drainCompletions(MaxCompletionsPerFrame);
if (chunk.Count == 0) break;
foreach (var result in chunk)
{
hunted++;
if (ResultLandblockId(result) == PriorityLandblockId)
{
ApplyResult(result);
budget--; // priority counts against the budget
found = true;
// Remaining items in this chunk go to deferred — they
// were drained past the cap to find the priority; don't
// drop them.
}
ApplyResult(result); // applied immediately, ahead of the budget
else
{
_deferredApply.Add(result);
}
}
}
if (found)
{
// Now apply up to remaining budget from deferred (mix from the
// chunk we just buffered + any pre-existing deferred items).
int i2 = 0;
while (i2 < budget && i2 < _deferredApply.Count) { ApplyResult(_deferredApply[i2]); i2++; }
if (i2 > 0) _deferredApply.RemoveRange(0, i2);
return;
}
// Priority not found in the outbox this frame: fall through to
// normal drain (the deferred buffer was already partially applied
// above; budget was already decremented).
}
// --- Step 3: normal path — drain up to remaining budget from the
// worker's outbox (priority was 0, or not found this frame).
// --- Step 2: apply up to MaxCompletionsPerFrame this frame — the deferred backlog
// first (FIFO), then fresh outbox completions. Caps GPU upload per frame so a big
// diff doesn't spike. While a priority is set, Step 1 has already moved the outbox
// into _deferredApply, so this drains that backlog at the budget rate.
int budget = MaxCompletionsPerFrame;
int i = 0;
while (i < budget && i < _deferredApply.Count) { ApplyResult(_deferredApply[i]); i++; }
if (i > 0) _deferredApply.RemoveRange(0, i);
budget -= i;
if (budget <= 0) return;
var drained = _drainCompletions(budget);
foreach (var result in drained)
ApplyResult(result);