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:
Erik 2026-07-24 19:39:23 +02:00
parent 98f1ac8934
commit 2ff8f844b0
33 changed files with 870 additions and 230 deletions

View file

@ -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();

View file

@ -339,7 +339,8 @@ internal sealed class SessionPlayerCompositionPhase
live.DrawDispatcher.InvalidateCompositeWarmupReadiness,
spawnClaimClassifier.IsUnhydratable,
d.Log,
worldQuiescence);
worldQuiescence,
streaming);
Fault(SessionPlayerCompositionPoint.WorldRevealCreated);
return CompleteSessionPlayer(

View file

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

View file

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

View file

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

View file

@ -27,6 +27,7 @@ internal readonly record struct StreamingQueuedCompletion(
LandblockStreamResult Result,
LandblockStreamCostEstimate Estimate,
StreamingCompletionPriority Priority,
long RevealGeneration,
ulong Generation,
long Sequence,
long EnqueuedTimestamp);

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

@ -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,