perf(streaming): reserve destination reveal capacity
Join destination scheduling to the canonical reveal generation, protect its share across every typed frame-budget dimension, and prevent stale work from clearing a replacement reservation. Remove forced incomplete materialization and project retail's centered portal wait cue while the authored tunnel remains active. Tests: Release build clean; 91 focused reservation/reveal tests; full solution 8,158 passed, 5 skipped. Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
parent
98f1ac8934
commit
2ff8f844b0
33 changed files with 870 additions and 230 deletions
|
|
@ -829,6 +829,20 @@ internal sealed class LivePresentationCompositionPhase
|
|||
"portal depth mask",
|
||||
() => new PortalDepthMaskRenderer(d.Gl),
|
||||
static value => value.Dispose());
|
||||
CompositionAcquisitionScope.CompositionAcquisitionLease<
|
||||
PortalWaitNoticeController>? portalWaitNoticeLease = null;
|
||||
if (interaction.RetainedUi is { } portalRetainedUi)
|
||||
{
|
||||
portalWaitNoticeLease = scope.Acquire(
|
||||
"portal wait notice",
|
||||
() => new PortalWaitNoticeController(
|
||||
portalRetainedUi.Host.Root),
|
||||
static value => value.Dispose());
|
||||
}
|
||||
Action<string?>? displayPortalWaitNotice =
|
||||
portalWaitNoticeLease is { } waitNoticeLease
|
||||
? waitNoticeLease.Resource.Set
|
||||
: null;
|
||||
PortalTunnelPresentation portalTunnel;
|
||||
try
|
||||
{
|
||||
|
|
@ -840,7 +854,9 @@ internal sealed class LivePresentationCompositionPhase
|
|||
d.HookRouter,
|
||||
dispatcherLease.Resource,
|
||||
foundation.SceneLighting,
|
||||
foundation.MeshAdapter),
|
||||
foundation.MeshAdapter,
|
||||
displayPortalWaitNotice,
|
||||
portalWaitNoticeLease?.Resource),
|
||||
static tunnel => tunnel.PrepareResources());
|
||||
}
|
||||
catch (Exception acquisitionFailure)
|
||||
|
|
@ -1006,6 +1022,7 @@ internal sealed class LivePresentationCompositionPhase
|
|||
clipFrameLease.Transfer();
|
||||
portalDepthLease.Transfer();
|
||||
portalTunnelLease.Transfer();
|
||||
portalWaitNoticeLease?.Transfer();
|
||||
skyLease.Transfer();
|
||||
skyShaderLease.Transfer();
|
||||
particleLease.Transfer();
|
||||
|
|
|
|||
|
|
@ -339,7 +339,8 @@ internal sealed class SessionPlayerCompositionPhase
|
|||
live.DrawDispatcher.InvalidateCompositeWarmupReadiness,
|
||||
spawnClaimClassifier.IsUnhydratable,
|
||||
d.Log,
|
||||
worldQuiescence);
|
||||
worldQuiescence,
|
||||
streaming);
|
||||
Fault(SessionPlayerCompositionPoint.WorldRevealCreated);
|
||||
|
||||
return CompleteSessionPlayer(
|
||||
|
|
|
|||
|
|
@ -94,7 +94,11 @@ internal sealed class LivePlayerModeAutoEntryContext
|
|||
|
||||
public bool IsPlayerModeActive => _mode.IsPlayerMode;
|
||||
|
||||
public void EnterPlayerMode() => _playerMode.EnterFromAutoEntry();
|
||||
public void EnterPlayerMode()
|
||||
{
|
||||
_playerMode.EnterFromAutoEntry();
|
||||
_worldReveal.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class PlayerModeAutoEntry
|
||||
|
|
|
|||
|
|
@ -53,7 +53,8 @@ public sealed class PortalTunnelPresentation : IDisposable
|
|||
private readonly WorldEntity _entity;
|
||||
private readonly PortalTunnelCamera _camera = new();
|
||||
private readonly Random _random;
|
||||
private readonly Action<string>? _displayNotice;
|
||||
private readonly Action<string?>? _displayNotice;
|
||||
private IDisposable? _displayNoticeLifetime;
|
||||
private readonly PortalMeshReferenceOwner _meshReferences;
|
||||
|
||||
private bool _visible;
|
||||
|
|
@ -62,6 +63,7 @@ public sealed class PortalTunnelPresentation : IDisposable
|
|||
private float _rotationStartAngle;
|
||||
private float _rotationEndAngle;
|
||||
private float _rotationCurrentAngle;
|
||||
private bool _waitCueVisible;
|
||||
private bool _disposeRequested;
|
||||
private bool _disposing;
|
||||
private bool _disposed;
|
||||
|
|
@ -77,7 +79,8 @@ public sealed class PortalTunnelPresentation : IDisposable
|
|||
IAnimationLoader animationLoader,
|
||||
IAnimationHookSink hookSink,
|
||||
Random random,
|
||||
Action<string>? displayNotice)
|
||||
Action<string?>? displayNotice,
|
||||
IDisposable? displayNoticeLifetime)
|
||||
{
|
||||
_gl = gl;
|
||||
_dispatcher = dispatcher;
|
||||
|
|
@ -90,6 +93,7 @@ public sealed class PortalTunnelPresentation : IDisposable
|
|||
_sequence.HookObj = _animationHooks;
|
||||
_random = random;
|
||||
_displayNotice = displayNotice;
|
||||
_displayNoticeLifetime = displayNoticeLifetime;
|
||||
|
||||
_entity = new WorldEntity
|
||||
{
|
||||
|
|
@ -119,7 +123,8 @@ public sealed class PortalTunnelPresentation : IDisposable
|
|||
WbDrawDispatcher dispatcher,
|
||||
SceneLightingUboBinding lightUbo,
|
||||
IWbMeshAdapter meshAdapter,
|
||||
Action<string>? displayNotice = null,
|
||||
Action<string?>? displayNotice = null,
|
||||
IDisposable? displayNoticeLifetime = null,
|
||||
Random? random = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(gl);
|
||||
|
|
@ -154,7 +159,8 @@ public sealed class PortalTunnelPresentation : IDisposable
|
|||
animationLoader,
|
||||
hookSink,
|
||||
random ?? Random.Shared,
|
||||
displayNotice);
|
||||
displayNotice,
|
||||
displayNoticeLifetime);
|
||||
}
|
||||
|
||||
internal static void EnsureRequiredAssets(
|
||||
|
|
@ -211,6 +217,7 @@ public sealed class PortalTunnelPresentation : IDisposable
|
|||
_rotationEndAngle = 0f;
|
||||
_rotationCurrentAngle = 0f;
|
||||
_camera.DirectionDegrees = 0f;
|
||||
SetWaitCue(false);
|
||||
_visible = true;
|
||||
RebuildPose();
|
||||
}
|
||||
|
|
@ -221,6 +228,7 @@ public sealed class PortalTunnelPresentation : IDisposable
|
|||
if (_disposed)
|
||||
return;
|
||||
_visible = false;
|
||||
SetWaitCue(false);
|
||||
_animationHooks.Clear();
|
||||
_sequence.ClearAnimations();
|
||||
}
|
||||
|
|
@ -236,6 +244,16 @@ public sealed class PortalTunnelPresentation : IDisposable
|
|||
TickRotation(dt);
|
||||
}
|
||||
|
||||
public void SetWaitCue(bool visible)
|
||||
{
|
||||
if (_waitCueVisible == visible)
|
||||
return;
|
||||
|
||||
_waitCueVisible = visible;
|
||||
_displayNotice?.Invoke(
|
||||
visible ? "In Portal Space - Please Wait..." : null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draw retail portal space into the active viewport. The caller suppresses
|
||||
/// the normal world viewport while this scene is visible, then draws the
|
||||
|
|
@ -298,7 +316,8 @@ public sealed class PortalTunnelPresentation : IDisposable
|
|||
_rotationDuration = NextDouble(RotationDurationMin, RotationDurationMax);
|
||||
_rotationStartAngle = _rotationCurrentAngle;
|
||||
_rotationEndAngle = (float)NextDouble(0.0, 360.0);
|
||||
_displayNotice?.Invoke("In Portal Space - Please Wait...");
|
||||
if (_waitCueVisible)
|
||||
_displayNotice?.Invoke("In Portal Space - Please Wait...");
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -389,11 +408,16 @@ public sealed class PortalTunnelPresentation : IDisposable
|
|||
try
|
||||
{
|
||||
_visible = false;
|
||||
SetWaitCue(false);
|
||||
_animationHooks.Clear();
|
||||
_sequence.ClearAnimations();
|
||||
_meshReferences.Dispose();
|
||||
if (_meshReferences.IsDisposed)
|
||||
{
|
||||
_displayNoticeLifetime?.Dispose();
|
||||
_displayNoticeLifetime = null;
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
|
|||
|
|
@ -127,8 +127,6 @@ internal interface ILocalPlayerTeleportStreamingOperations
|
|||
bool BeginRecenter(int x, int y, bool isSealedDungeon);
|
||||
bool ResetRecenter(bool sessionEnding);
|
||||
bool IsSealedDungeon(uint cellId);
|
||||
void SetPriority(uint landblockId, int radius);
|
||||
void ClearPriority();
|
||||
}
|
||||
|
||||
internal sealed class LocalPlayerTeleportStreamingOperations
|
||||
|
|
@ -165,22 +163,11 @@ internal sealed class LocalPlayerTeleportStreamingOperations
|
|||
public bool IsSealedDungeon(uint cellId) =>
|
||||
_sealedDungeonCells.IsSealedDungeon(cellId);
|
||||
|
||||
public void SetPriority(uint landblockId, int radius)
|
||||
{
|
||||
_streaming.PriorityLandblockId = landblockId;
|
||||
_streaming.PriorityRadius = radius;
|
||||
}
|
||||
|
||||
public void ClearPriority()
|
||||
{
|
||||
_streaming.PriorityLandblockId = 0u;
|
||||
_streaming.PriorityRadius = 0;
|
||||
}
|
||||
}
|
||||
|
||||
internal interface ILocalPlayerTeleportPlacement
|
||||
{
|
||||
void Place(Vector3 position, uint cellId, Quaternion rotation, bool forced);
|
||||
void Place(Vector3 position, uint cellId, Quaternion rotation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -218,7 +205,7 @@ internal sealed class LocalPlayerTeleportPlacement : ILocalPlayerTeleportPlaceme
|
|||
_spatial = spatial ?? throw new ArgumentNullException(nameof(spatial));
|
||||
}
|
||||
|
||||
public void Place(Vector3 position, uint cellId, Quaternion rotation, bool forced)
|
||||
public void Place(Vector3 position, uint cellId, Quaternion rotation)
|
||||
{
|
||||
PlayerMovementController controller = _controller.Controller
|
||||
?? throw new InvalidOperationException(
|
||||
|
|
@ -233,13 +220,6 @@ internal sealed class LocalPlayerTeleportPlacement : ILocalPlayerTeleportPlaceme
|
|||
resolved.Position.Y,
|
||||
resolved.Position.Z);
|
||||
|
||||
if (forced)
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"live: teleport HOLD gave up (impossible/timeout) - force-snapping "
|
||||
+ $"cell=0x{cellId:X8} pos={position} -> 0x{resolved.CellId:X8} {snapped}");
|
||||
}
|
||||
|
||||
uint playerGuid = _identity.ServerGuid;
|
||||
controller.SetPosition(
|
||||
snapped,
|
||||
|
|
@ -270,7 +250,7 @@ internal sealed class LocalPlayerTeleportPlacement : ILocalPlayerTeleportPlaceme
|
|||
PhysicsDiagnostics.LogTeleport(
|
||||
"PLACED",
|
||||
controller.CellId,
|
||||
$"forced={forced}");
|
||||
"readiness=complete");
|
||||
Console.WriteLine(
|
||||
$"live: teleport materialized - snapped to {controller.Position} "
|
||||
+ $"cell=0x{controller.CellId:X8}");
|
||||
|
|
@ -315,6 +295,7 @@ internal interface ILocalPlayerTeleportPresentation : IDisposable
|
|||
void TickTunnel(float deltaSeconds);
|
||||
void EnterTunnel();
|
||||
void ExitTunnel();
|
||||
void SetWaitCue(bool visible);
|
||||
void Reset();
|
||||
Matrix4x4 ApplyViewPlane(Matrix4x4 projection);
|
||||
ICamera ApplyViewPlane(ICamera camera);
|
||||
|
|
@ -354,6 +335,7 @@ internal sealed class LocalPlayerTeleportPresentation
|
|||
public void TickTunnel(float deltaSeconds) => _tunnel.Tick(deltaSeconds);
|
||||
public void EnterTunnel() => _tunnel.Enter();
|
||||
public void ExitTunnel() => _tunnel.Exit();
|
||||
public void SetWaitCue(bool visible) => _tunnel.SetWaitCue(visible);
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
|
|
@ -384,10 +366,6 @@ internal sealed class LocalPlayerTeleportController
|
|||
AcDream.App.Interaction.ISelectionViewPlaneSource,
|
||||
IDisposable
|
||||
{
|
||||
internal const float MaximumWorldHoldSeconds = 10f;
|
||||
internal const int NearRingRadius =
|
||||
WorldRevealReadinessBarrier.OutdoorNeighborhoodRadius;
|
||||
|
||||
private readonly TeleportTransitCoordinator<WorldSession.EntityPositionUpdate> _transit = new();
|
||||
private readonly ILocalPlayerTeleportAuthority _authority;
|
||||
private readonly ILocalPlayerTeleportInputLifetime _input;
|
||||
|
|
@ -404,7 +382,6 @@ internal sealed class LocalPlayerTeleportController
|
|||
private bool _hasAcceptedDestination;
|
||||
private WorldSession.EntityPositionUpdate _acceptedDestination;
|
||||
private float _holdSeconds;
|
||||
private bool _forced;
|
||||
private long _lifetimeGeneration;
|
||||
private bool _disposed;
|
||||
|
||||
|
|
@ -496,15 +473,13 @@ internal sealed class LocalPlayerTeleportController
|
|||
if (!IsCurrentLifetime(generation, sequence))
|
||||
return;
|
||||
|
||||
if (haveDestination && originReady && !ready)
|
||||
{
|
||||
if (haveDestination && !ready)
|
||||
_holdSeconds += deltaSeconds;
|
||||
if (_holdSeconds >= MaximumWorldHoldSeconds)
|
||||
{
|
||||
ready = true;
|
||||
_forced = true;
|
||||
}
|
||||
}
|
||||
_presentation.SetWaitCue(
|
||||
haveDestination
|
||||
&& !ready
|
||||
&& _worldReveal.ObserveWait(
|
||||
TimeSpan.FromSeconds(_holdSeconds)));
|
||||
|
||||
var (_, events) = _presentation.Tick(deltaSeconds, ready);
|
||||
if (!IsCurrentLifetime(generation, sequence))
|
||||
|
|
@ -518,14 +493,10 @@ internal sealed class LocalPlayerTeleportController
|
|||
_placement.Place(
|
||||
_pendingPosition,
|
||||
_pendingCell,
|
||||
_pendingRotation,
|
||||
_forced);
|
||||
_pendingRotation);
|
||||
if (!IsCurrentLifetime(generation, sequence))
|
||||
return;
|
||||
_worldReveal.ObserveMaterialized();
|
||||
if (!IsCurrentLifetime(generation, sequence))
|
||||
return;
|
||||
_streaming.ClearPriority();
|
||||
if (!IsCurrentLifetime(generation, sequence))
|
||||
return;
|
||||
break;
|
||||
|
|
@ -590,7 +561,6 @@ internal sealed class LocalPlayerTeleportController
|
|||
}
|
||||
|
||||
_holdSeconds = 0f;
|
||||
_forced = false;
|
||||
_presentation.Begin(_mode.Projection);
|
||||
if (_lifetimeGeneration != generation || !_transit.HasPendingStart)
|
||||
return;
|
||||
|
|
@ -677,7 +647,7 @@ internal sealed class LocalPlayerTeleportController
|
|||
// Retail SmartBox enters blocking_for_cells before old-world object,
|
||||
// physics, landscape, and ambient owners may advance again. Begin the
|
||||
// reveal generation before a recenter can start detaching that world.
|
||||
_worldReveal.Begin(WorldRevealKind.Portal);
|
||||
_worldReveal.Begin(WorldRevealKind.Portal, position.LandblockId);
|
||||
if (!IsCurrentLifetime(generation, sequence))
|
||||
return false;
|
||||
|
||||
|
|
@ -713,13 +683,6 @@ internal sealed class LocalPlayerTeleportController
|
|||
_pendingPosition = worldPosition;
|
||||
_pendingCell = position.LandblockId;
|
||||
_holdSeconds = 0f;
|
||||
_forced = false;
|
||||
_streaming.SetPriority(
|
||||
StreamingRegion.EncodeLandblockId(landblockX, landblockY),
|
||||
NearRingRadius);
|
||||
if (!IsCurrentLifetime(generation, sequence))
|
||||
return false;
|
||||
|
||||
PhysicsDiagnostics.LogTeleport(
|
||||
"AIM",
|
||||
position.LandblockId,
|
||||
|
|
@ -748,25 +711,19 @@ internal sealed class LocalPlayerTeleportController
|
|||
_hasAcceptedDestination = false;
|
||||
_acceptedDestination = default;
|
||||
_holdSeconds = 0f;
|
||||
_forced = false;
|
||||
|
||||
_streaming.ResetRecenter(clearSession);
|
||||
if (_lifetimeGeneration != generation)
|
||||
return generation;
|
||||
|
||||
if (clearSession)
|
||||
{
|
||||
_worldReveal.Cancel();
|
||||
if (_lifetimeGeneration != generation)
|
||||
return generation;
|
||||
}
|
||||
_worldReveal.Cancel();
|
||||
if (_lifetimeGeneration != generation)
|
||||
return generation;
|
||||
|
||||
_presentation.Reset();
|
||||
if (_lifetimeGeneration != generation)
|
||||
return generation;
|
||||
|
||||
_streaming.ClearPriority();
|
||||
|
||||
return generation;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ internal readonly record struct StreamingQueuedCompletion(
|
|||
LandblockStreamResult Result,
|
||||
LandblockStreamCostEstimate Estimate,
|
||||
StreamingCompletionPriority Priority,
|
||||
long RevealGeneration,
|
||||
ulong Generation,
|
||||
long Sequence,
|
||||
long EnqueuedTimestamp);
|
||||
|
|
|
|||
|
|
@ -17,8 +17,15 @@ namespace AcDream.App.Streaming;
|
|||
/// Threading: not thread-safe. All calls must happen on the render thread.
|
||||
/// </remarks>
|
||||
/// </summary>
|
||||
public sealed class StreamingController : IStreamingFrameBackend
|
||||
public sealed class StreamingController
|
||||
: IStreamingFrameBackend,
|
||||
IWorldRevealStreamingScheduler
|
||||
{
|
||||
private readonly record struct DestinationReservation(
|
||||
long RevealGeneration,
|
||||
uint LandblockId,
|
||||
int Radius);
|
||||
|
||||
private sealed class OriginRecenterRetirement
|
||||
{
|
||||
public bool RadiiConverged;
|
||||
|
|
@ -61,6 +68,7 @@ public sealed class StreamingController : IStreamingFrameBackend
|
|||
private readonly StreamingCompletionQueue _completionQueue = new();
|
||||
private long _nextCompletionSequence;
|
||||
private int _legacyCompletionProfile = 4;
|
||||
private DestinationReservation? _destinationReservation;
|
||||
|
||||
private readonly GpuWorldState _state;
|
||||
private StreamingRegion? _region;
|
||||
|
|
@ -120,24 +128,54 @@ public sealed class StreamingController : IStreamingFrameBackend
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #138: the teleport destination landblock id. Loaded or promoted results
|
||||
/// inside its priority ring enter the destination FIFO, ahead of ordinary
|
||||
/// Near/Far work, but still consume the same typed frame budget.
|
||||
///
|
||||
/// <para>Set by <c>GameWindow</c> when a teleport starts; reset to 0
|
||||
/// once the destination landblock has been applied (or when the
|
||||
/// teleport destination changes).</para>
|
||||
/// </summary>
|
||||
public uint PriorityLandblockId { get; set; }
|
||||
internal long ActiveRevealGeneration =>
|
||||
_destinationReservation?.RevealGeneration ?? 0L;
|
||||
|
||||
/// <summary>
|
||||
/// Radius in landblocks (Chebyshev distance) around
|
||||
/// <see cref="PriorityLandblockId"/> classified as destination work.
|
||||
/// Zero means only the center. This changes queue priority, never budget
|
||||
/// enforcement or atomic publication semantics.
|
||||
/// </summary>
|
||||
public int PriorityRadius { get; set; }
|
||||
internal uint DestinationLandblockId =>
|
||||
_destinationReservation?.LandblockId ?? 0u;
|
||||
|
||||
internal int DestinationRadius =>
|
||||
_destinationReservation?.Radius ?? 0;
|
||||
|
||||
internal void BeginDestinationReservation(
|
||||
long revealGeneration,
|
||||
uint destinationCell,
|
||||
int requiredRenderRadius)
|
||||
{
|
||||
if (revealGeneration <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(revealGeneration));
|
||||
if (destinationCell == 0u)
|
||||
throw new ArgumentOutOfRangeException(nameof(destinationCell));
|
||||
if (requiredRenderRadius < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(requiredRenderRadius));
|
||||
|
||||
_destinationReservation = new DestinationReservation(
|
||||
revealGeneration,
|
||||
(destinationCell & 0xFFFF0000u) | 0xFFFFu,
|
||||
requiredRenderRadius);
|
||||
}
|
||||
|
||||
internal void EndDestinationReservation(long revealGeneration)
|
||||
{
|
||||
if (_destinationReservation is { } active
|
||||
&& active.RevealGeneration == revealGeneration)
|
||||
{
|
||||
_destinationReservation = null;
|
||||
}
|
||||
}
|
||||
|
||||
void IWorldRevealStreamingScheduler.BeginDestinationReservation(
|
||||
long revealGeneration,
|
||||
uint destinationCell,
|
||||
int requiredRenderRadius) =>
|
||||
BeginDestinationReservation(
|
||||
revealGeneration,
|
||||
destinationCell,
|
||||
requiredRenderRadius);
|
||||
|
||||
void IWorldRevealStreamingScheduler.EndDestinationReservation(
|
||||
long revealGeneration) =>
|
||||
EndDestinationReservation(revealGeneration);
|
||||
|
||||
// [FRAME-DIAG] (read by GameWindow's ACDREAM_WB_DIAG rollup): the standing
|
||||
// accepted completion backlog. A non-zero value during/after a teleport is
|
||||
|
|
@ -507,6 +545,11 @@ public sealed class StreamingController : IStreamingFrameBackend
|
|||
for (int i = 0; i < pending.Count; i++)
|
||||
{
|
||||
LandblockStreamResult result = pending[i];
|
||||
using StreamingWorkMeter.LaneScope lane =
|
||||
_activeWorkMeter.EnterLane(
|
||||
IsDestinationWork(result.LandblockId)
|
||||
? StreamingWorkLane.Destination
|
||||
: StreamingWorkLane.NonDestination);
|
||||
LandblockPublicationAdvance advance =
|
||||
_presentation.ResumePublication(
|
||||
result,
|
||||
|
|
@ -549,7 +592,9 @@ public sealed class StreamingController : IStreamingFrameBackend
|
|||
throw new InvalidOperationException(
|
||||
"StreamingController.Tick cannot be reentered.");
|
||||
|
||||
var meter = new StreamingWorkMeter(_workBudget);
|
||||
var meter = new StreamingWorkMeter(
|
||||
_workBudget,
|
||||
destinationReservationActive: _destinationReservation is not null);
|
||||
_activeWorkMeter = meter;
|
||||
try
|
||||
{
|
||||
|
|
@ -901,15 +946,13 @@ public sealed class StreamingController : IStreamingFrameBackend
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// True when <paramref name="id"/> is the priority center or within
|
||||
/// <see cref="PriorityRadius"/> landblocks of it (Chebyshev) — i.e. inside the teleport
|
||||
/// near ring that <see cref="DrainAndApply"/> prioritizes. With the default radius 0
|
||||
/// this reduces to an exact match on <see cref="PriorityLandblockId"/> (the original
|
||||
/// single-landblock priority behaviour).
|
||||
/// True when <paramref name="id"/> belongs to the canonical reveal
|
||||
/// generation's destination neighborhood.
|
||||
/// </summary>
|
||||
private bool IsWithinPriorityRing(uint id)
|
||||
=> PriorityLandblockId != 0u
|
||||
&& ChebyshevLandblocks(id, PriorityLandblockId) <= PriorityRadius;
|
||||
private bool IsDestinationWork(uint id)
|
||||
=> _destinationReservation is { } reservation
|
||||
&& ChebyshevLandblocks(id, reservation.LandblockId)
|
||||
<= reservation.Radius;
|
||||
|
||||
/// <summary>
|
||||
/// Dungeon-exit edge (portal to outdoors / teleport): rebuild the full
|
||||
|
|
@ -1498,6 +1541,11 @@ public sealed class StreamingController : IStreamingFrameBackend
|
|||
"The completion queue returned a null head.");
|
||||
try
|
||||
{
|
||||
using StreamingWorkMeter.LaneScope lane = meter.EnterLane(
|
||||
work.Priority == StreamingCompletionPriority.Destination
|
||||
&& work.RevealGeneration == ActiveRevealGeneration
|
||||
? StreamingWorkLane.Destination
|
||||
: StreamingWorkLane.NonDestination);
|
||||
LandblockPublicationAdvance advance = ApplyResult(
|
||||
work,
|
||||
meter,
|
||||
|
|
@ -1554,6 +1602,9 @@ public sealed class StreamingController : IStreamingFrameBackend
|
|||
"The completion source returned a null peek.");
|
||||
bool stale = IsStaleGeneration(result)
|
||||
&& !_presentation.HasPendingPublication(result);
|
||||
StreamingCompletionPriority priority = stale
|
||||
? StreamingCompletionPriority.Control
|
||||
: ClassifyCompletion(result);
|
||||
LandblockStreamCostEstimate estimate =
|
||||
LandblockStreamResultCost.Estimate(result);
|
||||
StreamingWorkCost admissionCost = stale
|
||||
|
|
@ -1563,6 +1614,10 @@ public sealed class StreamingController : IStreamingFrameBackend
|
|||
estimate.Work.CompletionAdmissions,
|
||||
AdoptedCpuBytes:
|
||||
estimate.Work.AdoptedCpuBytes);
|
||||
using StreamingWorkMeter.LaneScope lane = meter.EnterLane(
|
||||
priority == StreamingCompletionPriority.Destination
|
||||
? StreamingWorkLane.Destination
|
||||
: StreamingWorkLane.NonDestination);
|
||||
StreamingWorkAdmission admission = meter.TryReserve(
|
||||
admissionCost,
|
||||
"completion-admission");
|
||||
|
|
@ -1584,7 +1639,10 @@ public sealed class StreamingController : IStreamingFrameBackend
|
|||
_completionQueue.Enqueue(new StreamingQueuedCompletion(
|
||||
result,
|
||||
estimate,
|
||||
ClassifyCompletion(result),
|
||||
priority,
|
||||
priority == StreamingCompletionPriority.Destination
|
||||
? ActiveRevealGeneration
|
||||
: 0L,
|
||||
result.Generation,
|
||||
checked(_nextCompletionSequence++),
|
||||
Stopwatch.GetTimestamp()));
|
||||
|
|
@ -1604,7 +1662,7 @@ public sealed class StreamingController : IStreamingFrameBackend
|
|||
{
|
||||
if (result is LandblockStreamResult.Loaded
|
||||
or LandblockStreamResult.Promoted
|
||||
&& IsWithinPriorityRing(result.LandblockId))
|
||||
&& IsDestinationWork(result.LandblockId))
|
||||
{
|
||||
return StreamingCompletionPriority.Destination;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -135,11 +135,19 @@ public enum StreamingWorkLimit : byte
|
|||
GlRetireOperations,
|
||||
}
|
||||
|
||||
internal enum StreamingWorkLane : byte
|
||||
{
|
||||
NonDestination,
|
||||
Destination,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Immutable observation of one frame's streaming work.
|
||||
/// </summary>
|
||||
public readonly record struct StreamingWorkMeterSnapshot(
|
||||
StreamingWorkCost Used,
|
||||
StreamingWorkCost DestinationUsed,
|
||||
StreamingWorkCost NonDestinationUsed,
|
||||
int Operations,
|
||||
int CompletedOperations,
|
||||
int YieldCount,
|
||||
|
|
@ -177,7 +185,14 @@ public sealed class StreamingWorkMeter
|
|||
private readonly Func<long> _timestamp;
|
||||
private readonly long _frequency;
|
||||
private readonly long _start;
|
||||
private readonly bool _destinationReservationActive;
|
||||
private StreamingWorkCost _used;
|
||||
private StreamingWorkCost _destinationUsed;
|
||||
private StreamingWorkCost _nonDestinationUsed;
|
||||
private StreamingWorkLane _lane;
|
||||
private long _destinationElapsedTicks;
|
||||
private long _nonDestinationElapsedTicks;
|
||||
private long _activeOperationStart;
|
||||
private int _operations;
|
||||
private int _completed;
|
||||
private int _yields;
|
||||
|
|
@ -191,15 +206,22 @@ public sealed class StreamingWorkMeter
|
|||
private string? _lastStage;
|
||||
private StreamingWorkLimit _lastLimit;
|
||||
|
||||
public StreamingWorkMeter(StreamingWorkBudget budget)
|
||||
: this(budget, Stopwatch.GetTimestamp, Stopwatch.Frequency)
|
||||
public StreamingWorkMeter(
|
||||
StreamingWorkBudget budget,
|
||||
bool destinationReservationActive = false)
|
||||
: this(
|
||||
budget,
|
||||
Stopwatch.GetTimestamp,
|
||||
Stopwatch.Frequency,
|
||||
destinationReservationActive)
|
||||
{
|
||||
}
|
||||
|
||||
public StreamingWorkMeter(
|
||||
StreamingWorkBudget budget,
|
||||
Func<long> timestamp,
|
||||
long timestampFrequency)
|
||||
long timestampFrequency,
|
||||
bool destinationReservationActive = false)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(timestamp);
|
||||
if (timestampFrequency <= 0)
|
||||
|
|
@ -210,6 +232,18 @@ public sealed class StreamingWorkMeter
|
|||
_timestamp = timestamp;
|
||||
_frequency = timestampFrequency;
|
||||
_start = timestamp();
|
||||
_destinationReservationActive = destinationReservationActive;
|
||||
}
|
||||
|
||||
internal LaneScope EnterLane(StreamingWorkLane lane)
|
||||
{
|
||||
if (_reservationActive)
|
||||
throw new InvalidOperationException(
|
||||
"Cannot change streaming lane during an active operation.");
|
||||
|
||||
StreamingWorkLane previous = _lane;
|
||||
_lane = lane;
|
||||
return new LaneScope(this, previous);
|
||||
}
|
||||
|
||||
public StreamingWorkAdmission TryReserve(
|
||||
|
|
@ -237,9 +271,14 @@ public sealed class StreamingWorkMeter
|
|||
}
|
||||
|
||||
_used = _used.Add(cost);
|
||||
if (_lane == StreamingWorkLane.Destination)
|
||||
_destinationUsed = _destinationUsed.Add(cost);
|
||||
else
|
||||
_nonDestinationUsed = _nonDestinationUsed.Add(cost);
|
||||
_operations++;
|
||||
_reservationActive = true;
|
||||
_activeStage = stage;
|
||||
_activeOperationStart = _timestamp();
|
||||
if (ensureProgress)
|
||||
_ensuredProgressGranted = true;
|
||||
if (limit != StreamingWorkLimit.None)
|
||||
|
|
@ -258,31 +297,10 @@ public sealed class StreamingWorkMeter
|
|||
return StreamingWorkAdmission.Admitted;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// E1 observation seam: records work the legacy scheduler already chose to
|
||||
/// execute, while naming the budget decision that would have yielded. This
|
||||
/// preserves execution until the explicit scheduler takes ownership in E2.
|
||||
/// </summary>
|
||||
public void ForceReserveObserved(
|
||||
StreamingWorkCost cost,
|
||||
string stage)
|
||||
{
|
||||
cost.Validate();
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(stage);
|
||||
if (_reservationActive)
|
||||
throw new InvalidOperationException(
|
||||
"The prior streaming operation has not completed.");
|
||||
|
||||
_used = _used.Add(cost);
|
||||
_operations++;
|
||||
_reservationActive = true;
|
||||
_activeStage = stage;
|
||||
_lastStage = stage;
|
||||
}
|
||||
|
||||
public void Complete()
|
||||
{
|
||||
EnsureReservation();
|
||||
CompleteLaneTiming();
|
||||
_completed++;
|
||||
_reservationActive = false;
|
||||
_activeStage = null;
|
||||
|
|
@ -292,6 +310,7 @@ public sealed class StreamingWorkMeter
|
|||
public void Fail()
|
||||
{
|
||||
EnsureReservation();
|
||||
CompleteLaneTiming();
|
||||
_failures++;
|
||||
_reservationActive = false;
|
||||
_activeStage = null;
|
||||
|
|
@ -313,6 +332,8 @@ public sealed class StreamingWorkMeter
|
|||
long now = _timestamp();
|
||||
return new StreamingWorkMeterSnapshot(
|
||||
_used,
|
||||
_destinationUsed,
|
||||
_nonDestinationUsed,
|
||||
_operations,
|
||||
_completed,
|
||||
_yields,
|
||||
|
|
@ -348,9 +369,105 @@ public sealed class StreamingWorkMeter
|
|||
{
|
||||
return StreamingWorkLimit.GlRetireOperations;
|
||||
}
|
||||
|
||||
if (_destinationReservationActive
|
||||
&& _lane == StreamingWorkLane.NonDestination)
|
||||
{
|
||||
double unreservedFraction =
|
||||
1.0 - _budget.DestinationReserveFraction;
|
||||
long maxTimeTicks = ReservedLimit(
|
||||
_budget.MaxUpdateTime.Ticks,
|
||||
unreservedFraction);
|
||||
if (_nonDestinationElapsedTicks >= maxTimeTicks)
|
||||
return StreamingWorkLimit.Time;
|
||||
if ((long)_nonDestinationUsed.CompletionAdmissions
|
||||
+ cost.CompletionAdmissions
|
||||
> ReservedLimit(
|
||||
_budget.MaxCompletionAdmissions,
|
||||
unreservedFraction))
|
||||
{
|
||||
return StreamingWorkLimit.CompletionAdmissions;
|
||||
}
|
||||
if (_nonDestinationUsed.AdoptedCpuBytes
|
||||
> ReservedLimit(
|
||||
_budget.MaxAdoptedCpuBytes,
|
||||
unreservedFraction) - cost.AdoptedCpuBytes)
|
||||
{
|
||||
return StreamingWorkLimit.AdoptedCpuBytes;
|
||||
}
|
||||
if ((long)_nonDestinationUsed.EntityOperations
|
||||
+ cost.EntityOperations
|
||||
> ReservedLimit(
|
||||
_budget.MaxEntityOperations,
|
||||
unreservedFraction))
|
||||
{
|
||||
return StreamingWorkLimit.EntityOperations;
|
||||
}
|
||||
if (_nonDestinationUsed.GpuUploadBytes
|
||||
> ReservedLimit(
|
||||
_budget.MaxGpuUploadBytes,
|
||||
unreservedFraction) - cost.GpuUploadBytes)
|
||||
{
|
||||
return StreamingWorkLimit.GpuUploadBytes;
|
||||
}
|
||||
if ((long)_nonDestinationUsed.GlRetireOperations
|
||||
+ cost.GlRetireOperations
|
||||
> ReservedLimit(
|
||||
_budget.MaxGlRetireOperations,
|
||||
unreservedFraction))
|
||||
{
|
||||
return StreamingWorkLimit.GlRetireOperations;
|
||||
}
|
||||
}
|
||||
return StreamingWorkLimit.None;
|
||||
}
|
||||
|
||||
private static long ReservedLimit(long total, double fraction) =>
|
||||
Math.Max(1L, (long)Math.Floor(total * fraction));
|
||||
|
||||
private void CompleteLaneTiming()
|
||||
{
|
||||
long raw = Math.Max(0L, _timestamp() - _activeOperationStart);
|
||||
long elapsed = raw > long.MaxValue / TimeSpan.TicksPerSecond
|
||||
? long.MaxValue
|
||||
: raw * TimeSpan.TicksPerSecond / _frequency;
|
||||
if (_lane == StreamingWorkLane.Destination)
|
||||
_destinationElapsedTicks = SaturatingAdd(
|
||||
_destinationElapsedTicks,
|
||||
elapsed);
|
||||
else
|
||||
_nonDestinationElapsedTicks = SaturatingAdd(
|
||||
_nonDestinationElapsedTicks,
|
||||
elapsed);
|
||||
}
|
||||
|
||||
private static long SaturatingAdd(long left, long right) =>
|
||||
left > long.MaxValue - right ? long.MaxValue : left + right;
|
||||
|
||||
private void RestoreLane(StreamingWorkLane lane)
|
||||
{
|
||||
if (_reservationActive)
|
||||
throw new InvalidOperationException(
|
||||
"Cannot restore streaming lane during an active operation.");
|
||||
_lane = lane;
|
||||
}
|
||||
|
||||
internal readonly struct LaneScope : IDisposable
|
||||
{
|
||||
private readonly StreamingWorkMeter _meter;
|
||||
private readonly StreamingWorkLane _previous;
|
||||
|
||||
internal LaneScope(
|
||||
StreamingWorkMeter meter,
|
||||
StreamingWorkLane previous)
|
||||
{
|
||||
_meter = meter;
|
||||
_previous = previous;
|
||||
}
|
||||
|
||||
public void Dispose() => _meter.RestoreLane(_previous);
|
||||
}
|
||||
|
||||
private void EnsureReservation()
|
||||
{
|
||||
if (!_reservationActive)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,20 @@
|
|||
namespace AcDream.App.Streaming;
|
||||
|
||||
/// <summary>
|
||||
/// Generation-scoped destination-work seam. The reveal coordinator is the
|
||||
/// canonical owner of login/portal lifetime; the streaming scheduler only
|
||||
/// reserves capacity for the exact generation and destination it is given.
|
||||
/// </summary>
|
||||
internal interface IWorldRevealStreamingScheduler
|
||||
{
|
||||
void BeginDestinationReservation(
|
||||
long revealGeneration,
|
||||
uint destinationCell,
|
||||
int requiredRenderRadius);
|
||||
|
||||
void EndDestinationReservation(long revealGeneration);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Owns one login/portal reveal lifetime across readiness and lifecycle
|
||||
/// diagnostics. This keeps the destination barrier and its observations on a
|
||||
|
|
@ -11,6 +26,7 @@ internal sealed class WorldRevealCoordinator
|
|||
private readonly WorldRevealReadinessBarrier _readiness;
|
||||
private readonly WorldRevealLifecycleTelemetry _lifecycle;
|
||||
private readonly WorldGenerationQuiescence? _quiescence;
|
||||
private readonly IWorldRevealStreamingScheduler? _streaming;
|
||||
private long _activeGeneration;
|
||||
|
||||
public WorldRevealCoordinator(
|
||||
|
|
@ -22,7 +38,8 @@ internal sealed class WorldRevealCoordinator
|
|||
Action invalidateCompositeTextures,
|
||||
Func<uint, bool> isSpawnClaimUnhydratable,
|
||||
Action<string>? log = null,
|
||||
WorldGenerationQuiescence? quiescence = null)
|
||||
WorldGenerationQuiescence? quiescence = null,
|
||||
IWorldRevealStreamingScheduler? streaming = null)
|
||||
{
|
||||
_readiness = new WorldRevealReadinessBarrier(
|
||||
isRenderNeighborhoodReady,
|
||||
|
|
@ -34,17 +51,26 @@ internal sealed class WorldRevealCoordinator
|
|||
isSpawnClaimUnhydratable);
|
||||
_lifecycle = new WorldRevealLifecycleTelemetry(log);
|
||||
_quiescence = quiescence;
|
||||
_streaming = streaming;
|
||||
}
|
||||
|
||||
public WorldRevealLifecycleSnapshot Snapshot => _lifecycle.Snapshot;
|
||||
public int PortalMaterializationCount => _lifecycle.PortalMaterializationCount;
|
||||
public bool WaitCueShown => _lifecycle.WaitCueShown;
|
||||
|
||||
public long Begin(WorldRevealKind kind)
|
||||
public long Begin(WorldRevealKind kind, uint destinationCell)
|
||||
{
|
||||
if (destinationCell == 0u)
|
||||
throw new ArgumentOutOfRangeException(nameof(destinationCell));
|
||||
|
||||
_readiness.Begin();
|
||||
long generation = _lifecycle.Begin(kind);
|
||||
_activeGeneration = generation;
|
||||
_quiescence?.Begin(generation);
|
||||
_streaming?.BeginDestinationReservation(
|
||||
generation,
|
||||
destinationCell,
|
||||
WorldRevealReadinessBarrier.RequiredRenderRadius(destinationCell));
|
||||
return generation;
|
||||
}
|
||||
|
||||
|
|
@ -70,6 +96,9 @@ internal sealed class WorldRevealCoordinator
|
|||
public void ObserveWorldViewportVisible() =>
|
||||
_lifecycle.ObserveWorldViewportVisible();
|
||||
|
||||
public bool ObserveWait(TimeSpan elapsed) =>
|
||||
_lifecycle.ObserveWait(elapsed);
|
||||
|
||||
public void Complete()
|
||||
{
|
||||
_lifecycle.Complete();
|
||||
|
|
@ -89,6 +118,7 @@ internal sealed class WorldRevealCoordinator
|
|||
return;
|
||||
|
||||
_activeGeneration = 0;
|
||||
_streaming?.EndDestinationReservation(generation);
|
||||
_quiescence?.End(generation);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,10 +30,14 @@ internal readonly record struct WorldRevealLifecycleSnapshot(
|
|||
/// </summary>
|
||||
internal sealed class WorldRevealLifecycleTelemetry
|
||||
{
|
||||
internal static readonly TimeSpan RetailWaitCueDelay =
|
||||
TimeSpan.FromSeconds(5);
|
||||
|
||||
private readonly Action<string> _log;
|
||||
private WorldRevealLifecycleSnapshot _snapshot;
|
||||
private long _nextGeneration;
|
||||
private int _portalMaterializationCount;
|
||||
private bool _waitCueShown;
|
||||
|
||||
public WorldRevealLifecycleTelemetry(Action<string>? log = null)
|
||||
{
|
||||
|
|
@ -42,6 +46,7 @@ internal sealed class WorldRevealLifecycleTelemetry
|
|||
|
||||
public WorldRevealLifecycleSnapshot Snapshot => _snapshot;
|
||||
public int PortalMaterializationCount => _portalMaterializationCount;
|
||||
public bool WaitCueShown => _waitCueShown;
|
||||
|
||||
public long Begin(WorldRevealKind kind)
|
||||
{
|
||||
|
|
@ -53,6 +58,7 @@ internal sealed class WorldRevealLifecycleTelemetry
|
|||
}
|
||||
|
||||
long generation = checked(++_nextGeneration);
|
||||
_waitCueShown = false;
|
||||
_snapshot = new WorldRevealLifecycleSnapshot(
|
||||
generation,
|
||||
kind,
|
||||
|
|
@ -66,6 +72,27 @@ internal sealed class WorldRevealLifecycleTelemetry
|
|||
return generation;
|
||||
}
|
||||
|
||||
public bool ObserveWait(TimeSpan elapsed)
|
||||
{
|
||||
if (_snapshot.Generation == 0
|
||||
|| _snapshot.Cancelled
|
||||
|| _snapshot.Completed
|
||||
|| elapsed < RetailWaitCueDelay)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_waitCueShown)
|
||||
{
|
||||
_waitCueShown = true;
|
||||
_log(
|
||||
$"[world-reveal] event=wait-cue elapsedMs={elapsed.TotalMilliseconds:F0} "
|
||||
+ Describe(_snapshot));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void ObserveReadiness(WorldRevealReadinessSnapshot readiness)
|
||||
{
|
||||
if (_snapshot.Generation == 0 || _snapshot.Cancelled)
|
||||
|
|
@ -121,7 +148,9 @@ internal sealed class WorldRevealLifecycleTelemetry
|
|||
|
||||
public void Cancel()
|
||||
{
|
||||
if (_snapshot.Generation == 0 || _snapshot.Cancelled)
|
||||
if (_snapshot.Generation == 0
|
||||
|| _snapshot.Cancelled
|
||||
|| _snapshot.Completed)
|
||||
return;
|
||||
|
||||
_snapshot = _snapshot with { Cancelled = true };
|
||||
|
|
|
|||
|
|
@ -94,7 +94,8 @@ internal sealed class WorldRevealReadinessBarrier
|
|||
/// <summary>
|
||||
/// True only when the same destination is drawable and collidable. An
|
||||
/// impossible indoor claim intentionally crosses the barrier so the
|
||||
/// existing loud forced-placement path can expose the invalid server data.
|
||||
/// existing loud unhydratable-placement path can expose invalid server
|
||||
/// data. A merely slow hydratable claim never crosses early.
|
||||
/// </summary>
|
||||
public bool IsReady(uint destinationCell)
|
||||
=> Evaluate(destinationCell).IsReady;
|
||||
|
|
@ -139,7 +140,7 @@ internal sealed class WorldRevealReadinessBarrier
|
|||
collisionReady);
|
||||
}
|
||||
|
||||
private static int RequiredRenderRadius(uint destinationCell) =>
|
||||
internal static int RequiredRenderRadius(uint destinationCell) =>
|
||||
IsIndoor(destinationCell) ? 0 : OutdoorNeighborhoodRadius;
|
||||
|
||||
private static bool IsIndoor(uint cellId) => (cellId & 0xFFFFu) >= 0x0100u;
|
||||
|
|
|
|||
64
src/AcDream.App/UI/PortalWaitNoticeController.cs
Normal file
64
src/AcDream.App/UI/PortalWaitNoticeController.cs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
using System.Numerics;
|
||||
|
||||
namespace AcDream.App.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Retained presentation of retail's ECM_UI display-string notice emitted by
|
||||
/// <c>gmSmartBoxUI::UseTime @ 0x004D6E30</c> while portal-space cell blocking
|
||||
/// continues. It is a centered overlay, not a chat message.
|
||||
/// </summary>
|
||||
internal sealed class PortalWaitNoticeController : IDisposable
|
||||
{
|
||||
private readonly UiRoot _root;
|
||||
private readonly UiText _text;
|
||||
private UiText.Line[] _lines = [];
|
||||
private bool _disposed;
|
||||
|
||||
public PortalWaitNoticeController(UiRoot root)
|
||||
{
|
||||
_root = root ?? throw new ArgumentNullException(nameof(root));
|
||||
_text = new UiText
|
||||
{
|
||||
Name = "PortalSpaceWaitNotice",
|
||||
Left = 0f,
|
||||
Top = 0f,
|
||||
Width = root.Width,
|
||||
Height = root.Height,
|
||||
Anchors = AnchorEdges.Left
|
||||
| AnchorEdges.Top
|
||||
| AnchorEdges.Right
|
||||
| AnchorEdges.Bottom,
|
||||
Centered = true,
|
||||
OneLine = true,
|
||||
ClickThrough = true,
|
||||
ZOrder = int.MaxValue,
|
||||
DefaultColor = Vector4.One,
|
||||
Visible = false,
|
||||
};
|
||||
_text.LinesProvider = () => _lines;
|
||||
_root.AddChild(_text);
|
||||
}
|
||||
|
||||
public void Set(string? message)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (string.IsNullOrEmpty(message))
|
||||
{
|
||||
_text.Visible = false;
|
||||
_lines = [];
|
||||
return;
|
||||
}
|
||||
|
||||
_lines = [new UiText.Line(message, Vector4.One)];
|
||||
_text.Visible = true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
_root.RemoveChild(_text);
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -224,7 +224,7 @@ internal sealed class LiveEntityWorldOriginCoordinator : ILiveEntityWorldOriginC
|
|||
lbY,
|
||||
isSealedDungeon: _sealedDungeonCells.IsSealedDungeon(
|
||||
position.LandblockId));
|
||||
_worldReveal.Begin(WorldRevealKind.Login);
|
||||
_worldReveal.Begin(WorldRevealKind.Login, position.LandblockId);
|
||||
|
||||
return new(
|
||||
true,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue