fix(render): harden portal exit handoff
Some checks failed
CI / linux-portable (push) Successful in 3m19s
CI / windows-gate (push) Failing after 6m43s
CI / release (push) Has been skipped

This commit is contained in:
Erik 2026-08-25 17:39:44 +02:00
parent ddbd7e4096
commit 82e4b4cb6d
17 changed files with 896 additions and 119 deletions

View file

@ -555,7 +555,20 @@ internal sealed class LocalPlayerTeleportPresentation
var (snapshot, events) = _animation.Tick(
deltaSeconds,
worldReady,
CurrentTunnelFrame);
CurrentTunnelFrame,
holdInTunnel: StreamingDiagnostics.TunnelFreezeFrame.HasValue);
// Retail hides portal space before publishing the WorldFadeIn view
// plane (gmSmartBoxUI::UseTime @ 0x004D73D3). Keep that viewport
// swap inside the presentation update: the controller's exit event
// performs host work, and allowing the terminal projection to escape
// while the tunnel remains visible exposes the finite portal mesh as
// a one-frame faceted disk. Hiding first is also safe if a render
// boundary lands between these two writes: the destination world is
// already materialized and receives the old outgoing projection.
if (!snapshot.ShowTunnel && _tunnel.IsVisible)
_tunnel.Exit();
_viewPlane.Update(snapshot);
return (snapshot, events);
}
@ -1228,16 +1241,19 @@ internal sealed class LocalPlayerTeleportController
return;
break;
case TeleportAnimEvent.PlayExitSound:
// gmSmartBoxUI::UseTime @ 0x004D6E30 releases destination
// cell blocking at the exact portal/world viewport swap.
// LoginComplete remains one WorldFadeIn second later.
// Retail first hides portal space, then shows the world,
// and only afterwards plays Sound_UI_ExitPortal
// (gmSmartBoxUI::UseTime @ 0x004D73D3..0x004D7405).
// ExitTunnel is idempotent: the production presentation
// normally retired it before publishing this snapshot,
// while this call enforces the same order for every host.
_presentation.ExitTunnel();
if (!IsCurrentLifetime(generation, sequence))
return;
_worldReveal.RevealWorldViewport();
if (!IsCurrentLifetime(generation, sequence))
return;
_presentation.PlayExitCue();
if (!IsCurrentLifetime(generation, sequence))
return;
_presentation.ExitTunnel();
if (!IsCurrentLifetime(generation, sequence))
return;
break;
@ -1707,17 +1723,16 @@ internal sealed class LocalPlayerTeleportController
return;
break;
case TeleportAnimEvent.PlayExitSound:
// Release destination cell blocking at the exact
// portal/world viewport swap — same edge as the teleport
// pump (gmSmartBoxUI::UseTime @ 0x004D6E30), with
// Sound_UI_ExitPortal @ 0x004D7405.
// Identical retail viewport order to the teleport pump:
// hide portal, show/release the destination world, then
// play Sound_UI_ExitPortal @ 0x004D7405.
_presentation.ExitTunnel();
if (!IsCurrentLoginLifetime(generation, revealGeneration))
return;
_worldReveal.RevealWorldViewport();
if (!IsCurrentLoginLifetime(generation, revealGeneration))
return;
_presentation.PlayExitCue();
if (!IsCurrentLoginLifetime(generation, revealGeneration))
return;
_presentation.ExitTunnel();
if (!IsCurrentLoginLifetime(generation, revealGeneration))
return;
break;