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

@ -53,7 +53,13 @@ public sealed class TeleportAnimSequencer
public const int TunnelEndFrame = 120;
public const float ExitWindowLow = FadeTime + 0.1f;
public const float ExitWindowHigh = FadeTime + 0.3f;
private const short LastVisibleOutgoingAnimationLevel = 1022;
// The retail viewport swap happens before the finite portal mesh reaches
// the final near-180-degree samples. Full-width 1280x720 captures establish
// level 1001 as the last radial field that covers the viewport; 1006 starts
// exposing the mesh perimeter and 1013 can leave its complete faceted rim
// on screen. Keep the cutoff in the quantized retail table domain so
// refresh rate cannot move the swap.
internal const short LastVisibleOutgoingAnimationLevel = 1001;
// UIGlobals::Init @ 0x004EE470. Retail integrates 100 integer sine
// samples into a 0..1024 easing table.
@ -102,10 +108,18 @@ public sealed class TeleportAnimSequencer
/// Advance the machine by <paramref name="dt"/> seconds.
/// <paramref name="worldReady"/> = the complete destination-load barrier is
/// satisfied (Near-tier render meshes/textures plus collision residency).
/// <paramref name="holdInTunnel"/> is diagnostic apparatus: when true the
/// machine observes readiness but withholds the <c>Place</c> edge and stays
/// in the stable tunnel state. The default is false and retail behavior is
/// unchanged.
/// Returns the current snapshot + edge-triggered events fired THIS tick.
/// </summary>
public (TeleportAnimSnapshot snapshot, IReadOnlyList<TeleportAnimEvent> events)
Tick(float dt, bool worldReady, int tunnelAnimationFrame = 72)
Tick(
float dt,
bool worldReady,
int tunnelAnimationFrame = 72,
bool holdInTunnel = false)
{
var evts = new List<TeleportAnimEvent>();
@ -133,7 +147,7 @@ public sealed class TeleportAnimSequencer
case TeleportAnimState.Tunnel:
// Hold here until worldReady (EndTeleportAnimation analogue).
if (worldReady)
if (worldReady && !holdInTunnel)
{
evts.Add(TeleportAnimEvent.Place);
Advance(TeleportAnimState.TunnelContinue, enterTunnel: false);
@ -183,10 +197,9 @@ public sealed class TeleportAnimSequencer
/// <summary>
/// Retire an outgoing viewport before the first quantized projection that
/// exposes the finite tunnel boundary. Retail's whole-frame black clear and
/// frame-paced D3D presentation show level 1022 as the last tunnel sample;
/// paired captures show no 1023/1024 portal frame before the world swap.
/// An uncapped modern loop can otherwise publish those sub-20 ms samples
/// exposes the finite tunnel boundary. Full-width captures show 1001 as
/// the last covered sample and 1006 as the first divergent faceted edge.
/// An uncapped modern loop can otherwise publish those terminal samples
/// and let the desktop compositor hold one for a complete display refresh.
/// </summary>
private bool OutgoingViewportReachedTerminalProjection()