fix(ui): gate — no void frames around the login wormhole; vitals icons centered
The login tunnel now covers from the first world-facing frame (the sky-void backdrop can never present pre-tunnel) and holds through an atomic tunnel-to-world swap at reveal completion — the void is structurally unreachable on both edges, pinned by frame-sequence tests across WorldSceneRenderer/WorldRevealCoordinator/LocalPlayerTeleport- Controller/RuntimeWorldTransitState. Vitals detail icons draw at their authored centered offsets in both stacked and side-by-side layouts. Implemented and live-probed by the fix agent; finalized by the lead after the agent parked post-verification (gates re-run green: App 5512/3, Runtime 1747/0). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
2f8c046aba
commit
fdc4fd496d
17 changed files with 649 additions and 66 deletions
95
src/AcDream.App/Rendering/LoginPresentationFrameProbe.cs
Normal file
95
src/AcDream.App/Rendering/LoginPresentationFrameProbe.cs
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
using System;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Diagnostic owner for the render-presentation probe family (CLAUDE.md Code
|
||||
/// Structure Rules §5 — one static class per subsystem, typed properties read
|
||||
/// from the environment once at startup, never per-call-site
|
||||
/// <c>GetEnvironmentVariable</c> reads).
|
||||
/// </summary>
|
||||
internal static class RenderPresentationDiagnostics
|
||||
{
|
||||
/// <summary>
|
||||
/// Enter-world gate round (2026-08-17): per-frame presentation
|
||||
/// classification for the login wormhole edges. When set, every completed
|
||||
/// render frame is classified by WHAT PRESENTED — <c>world</c> /
|
||||
/// <c>tunnel</c> / <c>black</c> / <c>void</c> — and a <c>[login-frames]</c>
|
||||
/// line is written on every classification transition. The gate contract
|
||||
/// is retail's: the sequence over a login must contain NO <c>void</c>
|
||||
/// entry on either edge (black → tunnel → world, each swap atomic).
|
||||
/// Not a user setting; not in <c>RuntimeOptions</c>; not persisted.
|
||||
/// </summary>
|
||||
public static bool ProbeLoginFrames { get; } =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_PROBE_LOGIN_FRAMES") == "1";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The frame-level truth for the login wormhole gate: classifies each
|
||||
/// completed render frame from the same outcome facts the orchestrator
|
||||
/// publishes, plus the two raw presentation inputs (actual tunnel-scene
|
||||
/// visibility and the live waiting-for-login latch), and logs one line per
|
||||
/// TRANSITION so the exact frame sequence at both wormhole edges is
|
||||
/// auditable.
|
||||
///
|
||||
/// <list type="bullet">
|
||||
/// <item><description><c>world</c> — the normal world viewport drew
|
||||
/// (<see cref="WorldRenderFrameOutcome.NormalWorldDrawn"/>).</description></item>
|
||||
/// <item><description><c>tunnel</c> — the portal-space scene was visible and
|
||||
/// drew over the frame's opaque black.</description></item>
|
||||
/// <item><description><c>black</c> — the frame presented the portal-viewport
|
||||
/// black clear with NO tunnel scene (retail's empty pre-player SmartBox:
|
||||
/// the gameplay screen before CreatePlayer draws no world and no
|
||||
/// tunnel).</description></item>
|
||||
/// <item><description><c>void</c> — the world path ran but drew nothing
|
||||
/// (the sky-only "waiting for login" backdrop, or an unavailable world
|
||||
/// generation). Retail NEVER presents this during a login — any void entry
|
||||
/// in a login sequence is the gate defect.</description></item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
internal sealed class LoginPresentationFrameProbe : IRenderFramePostDiagnosticsPhase
|
||||
{
|
||||
private readonly Func<bool> _tunnelSceneVisible;
|
||||
private readonly IRenderLoginStateSource _login;
|
||||
private readonly Action<string> _log;
|
||||
private long _frame;
|
||||
private double _elapsedSeconds;
|
||||
private string? _lastClass;
|
||||
|
||||
public LoginPresentationFrameProbe(
|
||||
Func<bool> tunnelSceneVisible,
|
||||
IRenderLoginStateSource login,
|
||||
Action<string> log)
|
||||
{
|
||||
_tunnelSceneVisible = tunnelSceneVisible
|
||||
?? throw new ArgumentNullException(nameof(tunnelSceneVisible));
|
||||
_login = login ?? throw new ArgumentNullException(nameof(login));
|
||||
_log = log ?? throw new ArgumentNullException(nameof(log));
|
||||
}
|
||||
|
||||
public void Process(RenderFrameInput input, RenderFrameOutcome outcome)
|
||||
{
|
||||
_frame++;
|
||||
_elapsedSeconds += input.DeltaSeconds;
|
||||
|
||||
bool world = outcome.World.NormalWorldDrawn;
|
||||
bool tunnel = _tunnelSceneVisible();
|
||||
bool cover = outcome.Presentation.PortalViewportDrawn;
|
||||
bool waiting = _login.IsWaitingForLogin;
|
||||
|
||||
string presentClass =
|
||||
world ? "world"
|
||||
: tunnel ? "tunnel"
|
||||
: cover ? "black"
|
||||
: "void";
|
||||
|
||||
if (presentClass == _lastClass)
|
||||
return;
|
||||
_lastClass = presentClass;
|
||||
_log(
|
||||
$"[login-frames] frame={_frame} t={_elapsedSeconds:F3}s "
|
||||
+ $"present={presentClass} waiting={(waiting ? 1 : 0)} "
|
||||
+ $"cover={(cover ? 1 : 0)} tunnel={(tunnel ? 1 : 0)} "
|
||||
+ $"world={(world ? 1 : 0)}");
|
||||
}
|
||||
}
|
||||
|
|
@ -136,13 +136,35 @@ internal sealed class LocalPlayerTeleportRenderStateSource
|
|||
: IRenderFramePortalStateSource
|
||||
{
|
||||
private readonly LocalPlayerTeleportController _teleport;
|
||||
private readonly IRenderLoginStateSource _login;
|
||||
|
||||
public LocalPlayerTeleportRenderStateSource(LocalPlayerTeleportController teleport)
|
||||
public LocalPlayerTeleportRenderStateSource(
|
||||
LocalPlayerTeleportController teleport,
|
||||
IRenderLoginStateSource login)
|
||||
{
|
||||
_teleport = teleport ?? throw new ArgumentNullException(nameof(teleport));
|
||||
_login = login ?? throw new ArgumentNullException(nameof(login));
|
||||
}
|
||||
|
||||
public bool IsPortalViewportVisible => _teleport.IsPortalViewportVisible;
|
||||
/// <summary>
|
||||
/// The frame presents the portal-viewport shape (opaque black clear, no
|
||||
/// world draw, retained UI on top) when the tunnel scene is visible OR
|
||||
/// while a live login is still pre-world. The second arm is retail's
|
||||
/// pre-player gameplay screen: after char-select Enter queues UI mode
|
||||
/// 0x10000008 (<c>CM_Login::SendNotice_BeginEnterWorld @ 0x006AD810</c>
|
||||
/// from <c>CPlayerSystem::LogOnCharacter @ 0x0055F890</c>), the SmartBox
|
||||
/// has no player and draws NO world — the screen behind the UI is black
|
||||
/// until <c>SmartBox::teleport_in_progress @ 0x00451C20</c> goes high at
|
||||
/// CreatePlayer and <c>gmSmartBoxUI::UseTime @ 0x004D6EAB</c> begins the
|
||||
/// tunnel in the same tick. The former sky-only "waiting for login"
|
||||
/// backdrop had no retail counterpart and presented as the gate's
|
||||
/// entry-edge VOID (2026-08-17). Both flags flip on the update thread
|
||||
/// (the login activation tick flips ChaseModeEverEntered AND makes the
|
||||
/// tunnel visible before the next render), so the black → tunnel → world
|
||||
/// sequence swaps atomically per frame.
|
||||
/// </summary>
|
||||
public bool IsPortalViewportVisible =>
|
||||
_teleport.IsPortalViewportVisible || _login.IsWaitingForLogin;
|
||||
|
||||
public uint ActiveDestinationCell => _teleport.ActiveDestinationCell;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -159,11 +159,13 @@ internal sealed class WorldSceneRenderer : IWorldSceneFramePhase
|
|||
_sky.DayFraction);
|
||||
}
|
||||
|
||||
// Retail keeps the live sky during EnterWorld while suppressing
|
||||
// terrain and object geometry until chase mode has engaged.
|
||||
if (_login.IsWaitingForLogin)
|
||||
return CompleteSkippedWorld();
|
||||
|
||||
// The former sky-only "waiting for login" skip lived here.
|
||||
// It is unreachable now: LocalPlayerTeleportRenderStateSource
|
||||
// folds IsWaitingForLogin into PortalViewportVisible (retail's
|
||||
// pre-player gameplay screen draws NO world — black, not sky),
|
||||
// so the portal-visible return above already covers every
|
||||
// waiting frame. One gate computes the frame's visibility;
|
||||
// this phase only enforces it.
|
||||
_passes.DrawFlatTerrain(in camera, roots.PlayerLandblockId);
|
||||
terrainDrawn = true;
|
||||
}
|
||||
|
|
@ -291,16 +293,6 @@ internal sealed class WorldSceneRenderer : IWorldSceneFramePhase
|
|||
diagnostic.VisibleLandblocks,
|
||||
diagnostic.TotalLandblocks,
|
||||
NormalWorldDrawn: true);
|
||||
|
||||
WorldRenderFrameOutcome CompleteSkippedWorld()
|
||||
{
|
||||
CompleteWorldFrame();
|
||||
worldFrameStarted = false;
|
||||
pviewFrameStarted = false;
|
||||
_selection?.CompleteFrame();
|
||||
selectionFrameStarted = false;
|
||||
return default;
|
||||
}
|
||||
}
|
||||
catch (Exception renderFailure)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue