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

@ -168,6 +168,31 @@ public sealed class TeleportAnimSequencerTests
Assert.Contains(TeleportAnimEvent.Place, evts);
}
[Fact]
public void DiagnosticHold_KeepsReadyPortalInStableTunnelUntilReleased()
{
var seq = new TeleportAnimSequencer();
seq.Begin(TeleportEntryKind.Portal);
var (_, heldEvents) = seq.Tick(
dt: 30f,
worldReady: true,
tunnelAnimationFrame: 72,
holdInTunnel: true);
Assert.Equal(TeleportAnimState.Tunnel, seq.State);
Assert.DoesNotContain(TeleportAnimEvent.Place, heldEvents);
var (_, releasedEvents) = seq.Tick(
dt: 0f,
worldReady: true,
tunnelAnimationFrame: 72,
holdInTunnel: false);
Assert.Equal(TeleportAnimState.TunnelContinue, seq.State);
Assert.Contains(TeleportAnimEvent.Place, releasedEvents);
}
// --- TunnelContinue: MIN_CONTINUE hold then TunnelFadeOut ---
[Fact]
@ -301,6 +326,44 @@ public sealed class TeleportAnimSequencerTests
framesPerSecond);
}
[Fact]
public void TunnelFadeOut_SwapsAfterLastFullViewportCaptureLevel()
{
Assert.Equal((short)1001, TeleportAnimSequencer.LastVisibleOutgoingAnimationLevel);
var seq = new TeleportAnimSequencer();
seq.Begin(TeleportEntryKind.Portal);
seq.Tick(0f, worldReady: false);
seq.Tick(0.016f, worldReady: true);
seq.Tick(
TeleportAnimSequencer.MinContinue,
worldReady: true,
tunnelAnimationFrame: 72);
Assert.Equal(TeleportAnimState.TunnelFadeOut, seq.State);
float lastSafeTime = Enumerable.Range(0, 1001)
.Select(static i => i / 1000f)
.First(t => TeleportAnimSequencer.GetRetailAnimationLevel(t)
== TeleportAnimSequencer.LastVisibleOutgoingAnimationLevel);
float firstUnsafeTime = Enumerable.Range(0, 1001)
.Select(static i => i / 1000f)
.First(t => TeleportAnimSequencer.GetRetailAnimationLevel(t)
> TeleportAnimSequencer.LastVisibleOutgoingAnimationLevel);
var (lastSafe, _) = seq.Tick(lastSafeTime, worldReady: true);
Assert.Equal(TeleportAnimState.TunnelFadeOut, lastSafe.State);
Assert.Equal(
TeleportAnimSequencer.LastVisibleOutgoingAnimationLevel / 1024f,
lastSafe.ViewPlaneBlend);
var (swapped, events) = seq.Tick(
firstUnsafeTime - lastSafeTime,
worldReady: true);
Assert.Equal(TeleportAnimState.WorldFadeIn, swapped.State);
Assert.False(swapped.ShowTunnel);
Assert.Contains(TeleportAnimEvent.PlayExitSound, events);
}
private static void AssertOutgoingViewportRetiresBeforeTerminal(
TeleportAnimSequencer seq,
TeleportAnimState outgoingState,
@ -314,7 +377,8 @@ public sealed class TeleportAnimSequencerTests
if (snapshot.State == outgoingState)
{
Assert.True(
snapshot.ViewPlaneBlend <= 1022f / 1024f,
snapshot.ViewPlaneBlend
<= TeleportAnimSequencer.LastVisibleOutgoingAnimationLevel / 1024f,
$"Published unsafe outgoing blend {snapshot.ViewPlaneBlend}.");
continue;
}
@ -384,8 +448,13 @@ public sealed class TeleportAnimSequencerTests
seq.Begin(TeleportEntryKind.Logout);
seq.Tick(0f, worldReady: false); // consume enter-sound tick, elapsed≈0
// Drive to just BEFORE the transition (so we're still in WorldFadeOut)
DriveSeconds(seq, TeleportAnimSequencer.FadeTime - 0.03f, worldReady: false);
float lastSafeTime = Enumerable.Range(0, 1001)
.Select(static i => i / 1000f)
.First(t => TeleportAnimSequencer.GetRetailAnimationLevel(t)
== TeleportAnimSequencer.LastVisibleOutgoingAnimationLevel);
// Drive directly to the last full-viewport quantized table level.
seq.Tick(lastSafeTime, worldReady: false);
Assert.Equal(TeleportAnimState.WorldFadeOut, seq.State);
var (snap, _) = seq.Tick(0f, worldReady: false);
@ -393,7 +462,9 @@ public sealed class TeleportAnimSequencerTests
Assert.True(
snap.ViewPlaneBlend > 0.95f,
$"Expected ViewPlaneBlend near 1, got {snap.ViewPlaneBlend}");
Assert.True(snap.ViewPlaneBlend <= 1022f / 1024f);
Assert.True(
snap.ViewPlaneBlend
<= TeleportAnimSequencer.LastVisibleOutgoingAnimationLevel / 1024f);
}
[Fact]