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:
Erik 2026-08-17 12:30:51 +02:00
parent 2f8c046aba
commit fdc4fd496d
17 changed files with 649 additions and 66 deletions

View file

@ -239,11 +239,13 @@ internal sealed class FrameRootCompositionPhase
// deleted at slice V11. The graph is the clear pass, the private-
// presentation phase, and the retained UI inside it — the client's
// own frame, drawn entirely through the RHI.
var teleportRenderState =
new LocalPlayerTeleportRenderStateSource(session.LocalTeleport);
var renderLoginState = new RenderLoginStateSource(
d.Options.LiveMode,
d.PlayerMode);
var teleportRenderState =
new LocalPlayerTeleportRenderStateSource(
session.LocalTeleport,
renderLoginState);
// Campaign V slice V6i-3: on Vulkan the frame's clear is a load op of the
// world pass rather than a pass of its own, so the two phases share this
// one value. See VulkanWorldScenePhase for why the merge is required
@ -561,6 +563,24 @@ internal sealed class FrameRootCompositionPhase
lifecycleAutomation)
: (IRenderFramePostDiagnosticsPhase?)lifecycleAutomation
?? NullRenderFramePostDiagnosticsPhase.Instance;
if (RenderPresentationDiagnostics.ProbeLoginFrames)
{
// Enter-world gate round (2026-08-17): the per-frame presentation
// classification probe (world/tunnel/black/void transitions). The
// tunnel fact is the controller's RAW portal-scene visibility, not
// the composed render-state source, so the probe can distinguish
// "covered black" from "tunnel scene drawn".
var loginFrameProbe = new LoginPresentationFrameProbe(
() => session.LocalTeleport.IsPortalViewportVisible,
renderLoginState,
d.Log);
postDiagnostics =
postDiagnostics is NullRenderFramePostDiagnosticsPhase
? loginFrameProbe
: new SerialRenderFramePostDiagnosticsPhase(
postDiagnostics,
loginFrameProbe);
}
var renderFrame = new RenderFrameOrchestrator(
host.GpuFrameLifetime,
// Campaign V slice V8: the Vulkan arm measures the frame bracket

View 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)}");
}
}

View file

@ -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;
}

View file

@ -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)
{

View file

@ -1161,11 +1161,23 @@ internal sealed class LocalPlayerTeleportController
return;
break;
case TeleportAnimEvent.Place:
// No login Place edge: the canonical initial placement is
// No login PLACEMENT: the canonical initial placement is
// the first-entry conductor's, already committed (the
// worldReady latch above requires it). Retail's login
// analogue only flips position_update_complete
// (SmartBox::UseTime @ 0x00455483).
// (SmartBox::UseTime @ 0x00455483) — but that same
// blocking-ends edge is where retail RESUMES
// CObjectMaint/CPhysics (@ 0x00455410), while the tunnel
// is still in front. Acknowledge the login
// materialization/simulation release here, the exact
// login mirror of the teleport pump's
// ObserveMaterialized — without it the world generation
// stayed unavailable until Complete, so the whole
// WorldFadeIn second presented an EMPTY world frame (the
// 2026-08-17 gate's exit-edge void).
_worldReveal.ObserveLoginMaterialized(revealGeneration);
if (!IsCurrentLoginLifetime(generation, revealGeneration))
return;
break;
case TeleportAnimEvent.PlayExitSound:
// Release destination cell blocking at the exact

View file

@ -244,6 +244,22 @@ internal sealed class WorldRevealCoordinator
return acknowledged;
}
/// <summary>
/// The login mirror of <see cref="ObserveMaterialized"/>: acknowledges
/// the login reveal's materialization/simulation-release edge (retail
/// resumes <c>CObjectMaint</c>/<c>CPhysics</c> when destination cells
/// stop blocking, <c>SmartBox::UseTime @ 0x00455483</c> — while the
/// tunnel is still in front). See
/// <c>RuntimeWorldTransitState.AcknowledgeLoginMaterialized</c>.
/// </summary>
public bool ObserveLoginMaterialized(long generation)
{
bool acknowledged =
_transit.AcknowledgeLoginMaterialized(generation);
RetryPendingHostWork();
return acknowledged;
}
public void ObserveWorldViewportVisible()
{
RetryPendingHostWork();

View file

@ -542,13 +542,14 @@ public static class DatWidgetFactory
{
bool passToChildren = info.States.Values.Any(
static s => s.Name is "HideDetail" or "ShowDetail" && s.PassToChildren);
// Each spec carries the overlay's authored edge modes and its
// container's authored size: the overlays author L3/R3 (retail
// CENTER anchors), which is what re-centers the icon when the
// vitals window resizes the meter away from its authored
// width (UiMeter.ComputeDetailOverlayRect).
m.ConfigureDetailOverlay(
backOverlay is not null ? backOverlay.StateMedia["ShowDetail"].File : 0u,
backOverlay?.X ?? 0f, backOverlay?.Y ?? 0f,
backOverlay?.Width ?? 0f, backOverlay?.Height ?? 0f,
frontOverlay is not null ? frontOverlay.StateMedia["ShowDetail"].File : 0u,
frontOverlay?.X ?? 0f, frontOverlay?.Y ?? 0f,
frontOverlay?.Width ?? 0f, frontOverlay?.Height ?? 0f,
DetailOverlaySpec(backOverlay, containers[0]),
DetailOverlaySpec(frontOverlay, containers[1]),
passToChildren);
}
}
@ -672,6 +673,23 @@ public static class DatWidgetFactory
&& c.StateMedia.TryGetValue("ShowDetail", out var media)
&& media.File != 0);
/// <summary>
/// Builds one absorbed overlay spec: the ShowDetail sprite, the authored
/// container-local rect, the authored raw edge-anchor modes, and the
/// container's authored size (the overlay's reflow parent — the container
/// itself authors L1/T1/R1/B1, so its current box always equals the
/// meter's). A null overlay yields the empty spec (Sprite 0 never draws).
/// </summary>
private static UiMeterDetailOverlaySpec DetailOverlaySpec(
ElementInfo? overlay, ElementInfo container)
=> overlay is null
? default
: new UiMeterDetailOverlaySpec(
overlay.StateMedia["ShowDetail"].File,
overlay.X, overlay.Y, overlay.Width, overlay.Height,
overlay.Left, overlay.Top, overlay.Right, overlay.Bottom,
container.Width, container.Height);
private static bool HasStatefulFill(ElementInfo container)
=> container.States.Any(pair =>
pair.Key != UiStateInfo.DirectStateId

View file

@ -148,6 +148,21 @@ public sealed class RetailUiAutomationProbe
return true;
}
/// <summary>
/// Raw synthetic drag between two canvas points (gate-fix round,
/// 2026-08-17): the vitals icon-centering verify needs the vitals window
/// RESIZED away from its authored width (the retail L3/R3 center anchors
/// only become observable on a non-authored meter width), and window
/// resize rides an edge-grip drag no element/item-addressed drag form can
/// express. Same synthetic <see cref="UiRoot"/> pointer route as
/// <see cref="DragItemToElement"/> — never the OS cursor.
/// </summary>
public bool DragAtPoint(int startX, int startY, int endX, int endY)
{
DragAt(startX, startY, endX, endY);
return true;
}
/// <summary>
/// 2026-08-17 morning gate: synthetic pointer HOVER (no click) at an
/// element's center, for rollover/tooltip verification. Deliberately

View file

@ -286,8 +286,21 @@ public sealed class RetailUiAutomationScriptRunner : IDisposable
private bool DoDrag(ScriptCommand command)
{
var p = command.Parts;
if (p.Length >= 6
&& string.Equals(p[1], "at", StringComparison.OrdinalIgnoreCase))
{
// `drag at <x1> <y1> <x2> <y2>` — raw synthetic pointer drag in
// canvas coordinates (gate-fix round 2026-08-17: window-resize
// choreography for the vitals icon-centering verify; see
// RetailUiAutomationProbe.DragAtPoint).
if (!TryParseInt(p[2], out int x1) || !TryParseInt(p[3], out int y1)
|| !TryParseInt(p[4], out int x2) || !TryParseInt(p[5], out int y2))
return Stop(command, "usage: drag at <x1> <y1> <x2> <y2>");
return _probe.DragAtPoint(x1, y1, x2, y2)
|| Stop(command, "drag at failed");
}
if (p.Length < 5 || !string.Equals(p[1], "item", StringComparison.OrdinalIgnoreCase))
return Stop(command, "usage: drag item <guid> element <datId> | drag item <guid> item <guid> | drag item <guid> outside <x> <y>");
return Stop(command, "usage: drag item <guid> element <datId> | drag item <guid> item <guid> | drag item <guid> outside <x> <y> | drag at <x1> <y1> <x2> <y2>");
if (!TryParseUInt(p[2], out uint sourceGuid)) return Stop(command, $"bad item guid '{p[2]}'");
string target = p[3].ToLowerInvariant();

View file

@ -7,6 +7,24 @@ namespace AcDream.App.UI;
/// — the same mapping ElementReader applies at element level).</summary>
public enum UiMeterLabelAlign : byte { Left = 0, Center = 1, Right = 2 }
/// <summary>
/// One absorbed vitals detail-icon overlay (the <c>0x100004A9</c> child of a
/// meter's back/front slice container): its ShowDetail sprite, authored rect
/// (local to the container, which spans the meter at 0,0), the authored raw
/// edge-anchor modes (<c>ElementDesc</c> Left/Top/Right/Bottom, values 04),
/// and the container's authored size. The overlays author <c>L3/R3</c> —
/// retail's CENTER anchors — so when the meter is resized away from its
/// authored width the icon re-centers through the exact
/// <see cref="UiLayoutPolicy"/> port of
/// <c>UIElement::UpdateForParentSizeChange @0x00462640</c> instead of staying
/// pinned at its authored X (the gate-observed left-drift).
/// </summary>
internal readonly record struct UiMeterDetailOverlaySpec(
uint Sprite,
float X, float Y, float W, float H,
uint LeftMode, uint TopMode, uint RightMode, uint BottomMode,
float ParentW, float ParentH);
/// <summary>
/// A horizontal vital bar (retail HP/Stamina/Mana style): a background rect, a
/// partial-width solid fill, and an optional centered "current/max" numeric
@ -28,19 +46,22 @@ public sealed class UiMeter : UiElement, IUiDatStateful
// Vitals ShowDetail icon overlays (see ConfigureDetailOverlay).
private bool _detailConfigured;
private bool _detailPassToChildren;
private uint _detailBackSprite;
private uint _detailFrontSprite;
private (float X, float Y, float W, float H) _detailBackRect;
private (float X, float Y, float W, float H) _detailFrontRect;
private UiMeterDetailOverlaySpec _detailBack;
private UiMeterDetailOverlaySpec _detailFront;
/// <summary>True when this meter absorbed the vitals detail-icon overlays. Exposed for tests.</summary>
internal bool HasDetailOverlay => _detailConfigured;
/// <summary>The dim back-container detail icon (ShowDetail media). Exposed for tests.</summary>
internal uint DetailBackSprite => _detailBackSprite;
internal uint DetailBackSprite => _detailBack.Sprite;
/// <summary>The bright fill-clipped front-container detail icon. Exposed for tests.</summary>
internal uint DetailFrontSprite => _detailFrontSprite;
internal uint DetailFrontSprite => _detailFront.Sprite;
/// <summary>The back overlay's authored meter-local rect. Exposed for tests.</summary>
internal (float X, float Y, float W, float H) DetailBackRect => _detailBackRect;
internal (float X, float Y, float W, float H) DetailBackRect =>
(_detailBack.X, _detailBack.Y, _detailBack.W, _detailBack.H);
/// <summary>The complete absorbed back overlay. Exposed for tests.</summary>
internal UiMeterDetailOverlaySpec DetailBack => _detailBack;
/// <summary>The complete absorbed front overlay. Exposed for tests.</summary>
internal UiMeterDetailOverlaySpec DetailFront => _detailFront;
/// <summary>Dat element id, set by the layout importer so duplicated page copies can be scoped.</summary>
public uint ElementId { get; set; }
@ -142,22 +163,69 @@ public sealed class UiMeter : UiElement, IUiDatStateful
/// <c>m_pcChildImage</c> child, element id 2, to the 0x69 fraction), so
/// the icon itself fills up with the vital. Both overlays author media
/// ONLY for <c>ShowDetail</c> (<c>HideDetail</c> authors File=0), so they
/// draw solely in that state. Rects are the overlays' authored X/Y/W/H
/// local to the meter (the containers span the meter at 0,0).
/// draw solely in that state. Each spec carries the overlay's authored
/// rect (local to its container, which spans the meter at 0,0), its raw
/// edge-anchor modes, and the container's authored size — see
/// <see cref="ComputeDetailOverlayRect"/> for how those position the icon
/// on a resized meter.
/// </summary>
internal void ConfigureDetailOverlay(
uint backSprite, float backX, float backY, float backW, float backH,
uint frontSprite, float frontX, float frontY, float frontW, float frontH,
in UiMeterDetailOverlaySpec back,
in UiMeterDetailOverlaySpec front,
bool passToChildren)
{
_detailBackSprite = backSprite;
_detailBackRect = (backX, backY, backW, backH);
_detailFrontSprite = frontSprite;
_detailFrontRect = (frontX, frontY, frontW, frontH);
_detailConfigured = backSprite != 0 || frontSprite != 0;
_detailBack = back;
_detailFront = front;
_detailConfigured = back.Sprite != 0 || front.Sprite != 0;
_detailPassToChildren = passToChildren;
}
/// <summary>
/// The overlay's effective meter-local rect at the meter's CURRENT size.
/// At the authored container size the authored rect stands verbatim —
/// retail's <c>UIElement::UpdateForParentSizeChange @0x00462640</c> runs
/// only when a parent actually resizes, and the designers hand-placed
/// rects that are not always the exact center formula (the stamina sword
/// authors X=32 where the mode-3 formula yields 33). On any other size
/// the authored edge modes are applied from the ORIGINAL child/parent
/// rects through <see cref="UiLayoutPolicy.Apply"/>, the exact port of
/// that retail routine — the overlays' authored <c>L3/R3</c> center
/// anchors are what keep the heart/sword/scepter centered when the
/// vitals window is resized (retail near mode 3 =
/// <c>curParentW/2 - origChildW/2</c> @0x004627ca; far mode 3 =
/// <c>curParentW/2 + origChildW/2 - 1</c> @0x00462827, integer
/// arithmetic, so odd authored widths lose one pixel exactly as retail
/// does).
/// </summary>
internal static (float X, float Y, float W, float H) ComputeDetailOverlayRect(
in UiMeterDetailOverlaySpec overlay, float meterW, float meterH)
{
int parentW = (int)meterW, parentH = (int)meterH;
int origParentW = (int)overlay.ParentW, origParentH = (int)overlay.ParentH;
if (origParentW <= 0 || origParentH <= 0
|| (parentW == origParentW && parentH == origParentH))
{
return (overlay.X, overlay.Y, overlay.W, overlay.H);
}
var originalChild = UiPixelRect.FromPositionAndSize(
(int)overlay.X, (int)overlay.Y, (int)overlay.W, (int)overlay.H);
var originalParent = UiPixelRect.FromPositionAndSize(
0, 0, origParentW, origParentH);
var currentParent = UiPixelRect.FromPositionAndSize(
0, 0, parentW, parentH);
UiPixelRect effective = UiLayoutPolicy.Apply(
overlay.LeftMode,
overlay.TopMode,
overlay.RightMode,
overlay.BottomMode,
originalChild,
originalParent,
originalChild,
currentParent);
return (effective.X0, effective.Y0, effective.Width, effective.Height);
}
public bool TrySetRetailState(uint stateId)
{
// Vitals detail toggle (gmVitalsUI::ListenToElementMessage @0x004BFC00
@ -271,12 +339,22 @@ public sealed class UiMeter : UiElement, IUiDatStateful
&& ActiveRetailStateId == RetailUiStateIds.ShowDetail;
DrawHBar(ctx, resolve, BackLeft, BackTile, BackRight, Width);
if (detail)
DrawDetailIcon(ctx, resolve, _detailBackSprite, _detailBackRect, Width);
{
DrawDetailIcon(
ctx, resolve, _detailBack.Sprite,
ComputeDetailOverlayRect(in _detailBack, Width, Height),
Width);
}
if (pct is not null && p > 0f)
{
DrawHBar(ctx, resolve, FrontLeft, FrontTile, FrontRight, Width * p);
if (detail)
DrawDetailIcon(ctx, resolve, _detailFrontSprite, _detailFrontRect, Width * p);
{
DrawDetailIcon(
ctx, resolve, _detailFront.Sprite,
ComputeDetailOverlayRect(in _detailFront, Width, Height),
Width * p);
}
}
}
}

View file

@ -649,6 +649,64 @@ public sealed class RuntimeWorldTransitState
return true;
}
/// <summary>
/// The LOGIN half of the materialization/simulation edge. Retail's
/// <c>SmartBox::UseTime @ 0x00455410</c> <c>blocking_for_cells</c> branch
/// resumes <c>CObjectMaint</c>/<c>CPhysics</c> the moment destination
/// cells stop blocking — while the tunnel is still in front, and
/// IDENTICALLY for the initial login and an F751 teleport (retail has one
/// <c>teleport_in_progress</c> flow). The portal route rides
/// <see cref="AcknowledgePortalMaterialized"/> at its Place edge; the
/// login route has no Place (the first-entry conductor already committed
/// the canonical placement), so this acknowledges the same simulation
/// release at the login pump's tunnel-hold-end edge. Without it the world
/// stayed unavailable until <see cref="Complete"/> — the WorldFadeIn
/// second presented an empty (void) world frame instead of retail's
/// world-under-warp (2026-08-17 gate).
/// </summary>
public bool AcknowledgeLoginMaterialized(long generation)
{
if (generation == 0
|| generation != _snapshot.Generation
|| _snapshot.Kind != RuntimePortalKind.Login)
{
LogRejected(
"materialized-login-mismatch",
$"generation={generation} kind={_snapshot.Kind}");
return false;
}
if (!ValidateActive(
generation,
_snapshot.DestinationCell,
"materialized"))
{
return false;
}
if (_snapshot.Materialized)
return false;
if (!_snapshot.Readiness.IsReady)
{
FailInvariant("materialized-before-ready", null);
return false;
}
// PortalMaterializationCount deliberately unchanged: it counts
// Kind == Portal materializations only, exactly like the portal
// acknowledgement's own conditional increment.
_snapshot = _snapshot with
{
Materialized = true,
WorldSimulationAvailable = true,
};
RequireHostStage(
generation,
RuntimeWorldHostAcknowledgementStage
.SimulationReleaseProjected);
Log("materialized", _snapshot);
return true;
}
public bool AcknowledgeWorldViewportVisible(long generation)
{
if (!ValidateGeneration(