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

@ -1,6 +1,7 @@
using System.Numerics;
using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Wb;
using AcDream.App.Streaming;
using AcDream.App.UI;
using AcDream.Content.Vfx;
using AcDream.Core.Lighting;
@ -96,6 +97,7 @@ public sealed class PortalTunnelPresentation : IDisposable
private float _rotationEndAngle;
private float _rotationCurrentAngle;
private bool _waitCueVisible;
private bool _probeFreezeReached;
private bool _disposeRequested;
private bool _disposing;
private bool _disposed;
@ -256,6 +258,7 @@ public sealed class PortalTunnelPresentation : IDisposable
_rotationCurrentAngle = 0f;
_camera.DirectionDegrees = 0f;
_waitCueVisible = false;
_probeFreezeReached = false;
_visible = true;
RebuildPose();
}
@ -267,6 +270,7 @@ public sealed class PortalTunnelPresentation : IDisposable
return;
_visible = false;
_waitCueVisible = false;
_probeFreezeReached = false;
_animationHooks.Clear();
_sequence.ClearAnimations();
}
@ -275,11 +279,25 @@ public sealed class PortalTunnelPresentation : IDisposable
{
if (!_visible || dt < 0f)
return;
if (_probeFreezeReached)
return;
_sequence.Update(dt, frame: null);
RebuildPose();
_animationHooks.Drain(Vector3.Zero);
TickRotation(dt);
if (StreamingDiagnostics.TunnelFreezeFrame is not { } freezeFrame
|| CurrentAnimationFrame < freezeFrame)
{
return;
}
_probeFreezeReached = true;
Console.WriteLine(
$"[tunnel-freeze] frame={CurrentAnimationFrame} "
+ $"target={freezeFrame} setup=0x{_setupDid:X8} "
+ $"animation=0x{_animationDid:X8} state=held");
}
/// <summary>

View file

@ -128,9 +128,10 @@ internal readonly record struct ProcessResourceDiagnostics(
int TrackedGpuTextures);
/// <summary>
/// Immutable resource facts captured only when explicit UI-probe dumping is enabled.
/// Grouping keeps the diagnostics controller independent from every canonical renderer,
/// VFX, mesh, texture, and process owner used to produce the values.
/// Immutable resource facts captured only by an explicit low-frequency
/// diagnostic such as UI-probe dumping or reveal timing. Grouping keeps the
/// consumers independent from every canonical renderer, VFX, mesh, texture,
/// and process owner used to produce the values.
/// </summary>
internal readonly record struct RenderFrameResourceDiagnosticsSnapshot(
VfxStreamResourceDiagnostics Vfx,

View file

@ -17,6 +17,7 @@ public sealed class TeleportViewPlaneController
public const float TransitionViewPlaneDistance = 0.001f;
private float _gameViewPlaneDistance = 1f;
private TeleportAnimState _state = TeleportAnimState.Off;
private readonly ProjectionOverrideCamera _projectionCamera = new();
public bool Enabled { get; private set; }
@ -36,6 +37,7 @@ public sealed class TeleportViewPlaneController
_gameViewPlaneDistance = distance;
CurrentViewPlaneDistance = distance;
_state = TeleportAnimState.Off;
Enabled = false;
}
@ -48,6 +50,7 @@ public sealed class TeleportViewPlaneController
/// </summary>
public void Update(TeleportAnimSnapshot snapshot)
{
_state = snapshot.State;
switch (snapshot.State)
{
case TeleportAnimState.WorldFadeOut:
@ -80,6 +83,7 @@ public sealed class TeleportViewPlaneController
public void Reset()
{
_state = TeleportAnimState.Off;
Enabled = false;
CurrentViewPlaneDistance = _gameViewPlaneDistance;
}
@ -110,13 +114,35 @@ public sealed class TeleportViewPlaneController
float distance = MathF.Max(CurrentViewPlaneDistance, TransitionViewPlaneDistance);
float fov = 2f * MathF.Atan(1f / distance);
float near = MathF.Max(0.1f, distance * 0.25f);
float near = WorldTransitionNearPlane(distance);
if (near >= far)
near = MathF.Min(0.1f, far * 0.5f);
return Matrix4x4.CreatePerspectiveFieldOfView(fov, aspect, near, far);
}
/// <summary>
/// Retail's <c>set_vdst</c> keeps <c>znear</c> at 0.1 m below a view-plane
/// distance of 0.4. The legacy landscape path still supplied a complete
/// projected screen at the singular teleport endpoint. Vulkan clips the
/// finite resident terrain geometrically: at distance 0.001, every lower-
/// viewport ground ray hits the terrain before 0.1 m and the authored sky
/// is exposed underneath it. Scale the near plane with the transition only
/// while the world viewport owns the frame. This preserves the exact retail
/// X/Y warp, the tunnel projection, and the ordinary game projection while
/// giving the modern terrain path the coverage retail visibly produced.
/// </summary>
private float WorldTransitionNearPlane(float distance)
{
if (_state is TeleportAnimState.WorldFadeOut or TeleportAnimState.WorldFadeIn
&& distance < 0.4f)
{
return MathF.Max(0.0001f, distance * 0.25f);
}
return MathF.Max(0.1f, distance * 0.25f);
}
/// <summary>
/// Decorate the active camera with the same projection returned by
/// <see cref="Apply(Matrix4x4)"/>. Retail's <c>Render::set_vdst</c> is