acdream/src/AcDream.App/Streaming/WorldRevealCoordinator.cs
Erik a9a822f206 refactor(runtime): unify generation reset for direct hosts
Move canonical per-session teardown into one retryable Runtime transaction, reduce App reset to projection acknowledgements, and prove the same GameRuntime graph through deterministic no-window lifecycle, gameplay, portal, fault, reconnect, and isolation gates.\n\nCo-authored-by: Codex <noreply@openai.com>
2026-07-27 00:43:26 +02:00

574 lines
18 KiB
C#

using AcDream.Runtime;
using AcDream.Runtime.World;
namespace AcDream.App.Streaming;
/// <summary>
/// Generation-scoped destination-work seam. The reveal coordinator is the
/// graphical host for Runtime's canonical 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>
/// Generation-scoped renderer-resource profile used only while the normal
/// world viewport is withheld for a login or portal destination.
/// </summary>
internal interface IWorldRevealRenderResourceScheduler
{
void BeginDestinationReveal(long revealGeneration);
void EndDestinationReveal(long revealGeneration);
}
/// <summary>
/// Graphical-host adapter for one canonical Runtime login/portal reveal
/// lifetime. App owns render/collision readiness and destination resource
/// reservations; Runtime owns generation, destination, materialization,
/// simulation availability, completion, and cancellation.
/// </summary>
internal sealed class WorldRevealCoordinator
{
private sealed class HostProjection(
RuntimeWorldHostProjectionToken token,
int requiredRenderRadius,
in WorldGenerationQuiescenceEdge quiescenceEdge)
{
public RuntimeWorldHostProjectionToken Token { get; } = token;
public int RequiredRenderRadius { get; } = requiredRenderRadius;
public WorldGenerationQuiescenceEdge QuiescenceEdge { get; } =
quiescenceEdge;
public bool QuiescenceCommitted { get; set; }
public bool StreamingRegistered { get; set; }
public bool RenderResourcesRegistered { get; set; }
public bool SimulationReleaseProjected { get; set; }
public bool StreamingReleased { get; set; }
public bool RenderResourcesReleased { get; set; }
}
private readonly RuntimeWorldTransitState _transit;
private readonly WorldRevealReadinessBarrier _readiness;
private readonly WorldGenerationQuiescence? _quiescence;
private readonly IWorldRevealStreamingScheduler? _streaming;
private readonly IWorldRevealRenderResourceScheduler? _renderResources;
private readonly List<HostProjection> _hostProjections = [];
private bool _hostRetryActive;
private bool _hostRetryRequested;
public WorldRevealCoordinator(
RuntimeWorldTransitState transit,
Func<uint, int, bool> isRenderNeighborhoodReady,
Func<uint, bool> isSpawnCellReady,
Func<uint, int, bool> isTerrainNeighborhoodReady,
Func<bool> areCompositeTexturesReady,
Action<uint, int> prepareCompositeTextures,
Action invalidateCompositeTextures,
Func<uint, bool> isSpawnClaimUnhydratable,
WorldGenerationQuiescence? quiescence = null,
IWorldRevealStreamingScheduler? streaming = null,
IWorldRevealRenderResourceScheduler? renderResources = null)
{
_transit = transit ?? throw new ArgumentNullException(nameof(transit));
_readiness = new WorldRevealReadinessBarrier(
isRenderNeighborhoodReady,
isSpawnCellReady,
isTerrainNeighborhoodReady,
areCompositeTexturesReady,
prepareCompositeTextures,
invalidateCompositeTextures,
isSpawnClaimUnhydratable);
_quiescence = quiescence;
_streaming = streaming;
_renderResources = renderResources;
}
public RuntimePortalSnapshot Snapshot => _transit.Snapshot;
public int PortalMaterializationCount =>
_transit.Snapshot.PortalMaterializationCount;
public bool WaitCueShown => _transit.Snapshot.WaitCueShown;
public RuntimeWorldTransitOwnershipSnapshot Ownership =>
_transit.Ownership;
public long BeginLogin(uint destinationCell)
{
if (destinationCell == 0u)
throw new ArgumentOutOfRangeException(nameof(destinationCell));
WithdrawHostForReplacement();
WorldGenerationQuiescenceEdge quiescenceEdge =
_quiescence?.CaptureBegin() ?? default;
_readiness.Begin();
long generation = _transit.BeginLoginReveal(destinationCell);
BeginHostLifetime(
generation,
destinationCell,
quiescenceEdge);
return generation;
}
public bool TryBeginPortal(
ushort teleportSequence,
uint destinationCell,
out long generation)
{
if (destinationCell == 0u)
throw new ArgumentOutOfRangeException(nameof(destinationCell));
if (!_transit.CanBeginPortalReveal(
teleportSequence,
destinationCell))
{
generation = 0;
return false;
}
WithdrawHostForReplacement();
WorldGenerationQuiescenceEdge quiescenceEdge =
_quiescence?.CaptureBegin() ?? default;
if (!_transit.TryBeginPortalReveal(
teleportSequence,
destinationCell,
out generation))
{
return false;
}
_readiness.Begin();
BeginHostLifetime(
generation,
destinationCell,
quiescenceEdge);
return true;
}
private void BeginHostLifetime(
long generation,
uint destinationCell,
in WorldGenerationQuiescenceEdge quiescenceEdge)
{
if (!_transit.TryRegisterHostProjection(
generation,
destinationCell,
out RuntimeWorldHostProjectionToken token))
{
throw new InvalidOperationException(
"Runtime rejected the exact reveal host projection.");
}
_hostProjections.Add(new HostProjection(
token,
WorldRevealReadinessBarrier.RequiredRenderRadius(
destinationCell),
quiescenceEdge));
RetryPendingHostWork();
}
/// <summary>
/// Advances render-thread preparation and publishes the resulting
/// canonical readiness snapshot to lifecycle diagnostics.
/// </summary>
public WorldRevealReadinessSnapshot PrepareAndEvaluate(uint destinationCell)
{
_readiness.Prepare(destinationCell);
return Evaluate(destinationCell);
}
public WorldRevealReadinessSnapshot Evaluate(uint destinationCell)
{
RetryPendingHostWork();
WorldRevealReadinessSnapshot snapshot = _readiness.Evaluate(destinationCell);
RuntimePortalSnapshot portal = _transit.Snapshot;
if (portal.Generation != 0)
{
_transit.AcknowledgeDestinationReadiness(
new RuntimeDestinationReadiness(
portal.Generation,
snapshot.DestinationCell,
snapshot.IsIndoor,
snapshot.IsUnhydratable,
snapshot.RequiredRenderRadius,
snapshot.IsRenderNeighborhoodReady,
snapshot.AreCompositeTexturesReady,
snapshot.IsCollisionReady));
}
return snapshot;
}
/// <summary>
/// Commits the accepted destination and resumes its hidden world
/// simulation while the private portal viewport remains in front.
/// </summary>
/// <remarks>
/// Retail <c>SmartBox::UseTime @ 0x00455410</c> resumes
/// <c>CObjectMaint</c>/<c>CPhysics</c> before
/// <c>gmSmartBoxUI::EndTeleportAnimation @ 0x004D65A0</c> enters
/// <c>TAS_TUNNEL_CONTINUE</c>. The normal world viewport is not restored
/// until the later <c>TUNNEL_FADE_OUT -&gt; WORLD_FADE_IN</c> edge.
/// </remarks>
public bool CanPlacePortalDestination(
long generation,
ushort teleportSequence,
uint destinationCell) =>
_transit.CanPlacePortalDestination(
generation,
teleportSequence,
destinationCell);
public bool ObserveMaterialized(
long generation,
ushort teleportSequence,
uint destinationCell)
{
bool acknowledged = _transit.AcknowledgePortalMaterialized(
generation,
teleportSequence,
destinationCell);
RetryPendingHostWork();
return acknowledged;
}
public void ObserveWorldViewportVisible()
{
RetryPendingHostWork();
long generation = _transit.Snapshot.Generation;
if (generation != 0)
_transit.AcknowledgeWorldViewportVisible(generation);
}
public bool ObserveWait(TimeSpan elapsed)
{
long generation = _transit.Snapshot.Generation;
return generation != 0
&& _transit.ObserveWait(generation, elapsed);
}
/// <summary>
/// Releases the destination streaming/resource reservation at retail's
/// TunnelFadeOut -&gt; WorldFadeIn viewport swap without completing the
/// one-second WorldFadeIn/LoginComplete tail. World simulation already
/// resumed when the accepted destination materialized.
/// </summary>
public void RevealWorldViewport()
{
HostProjection? host = FindCurrentHostProjection();
if (host is null)
return;
_transit.RequireDestinationReservationRelease(host.Token);
RetryPendingHostWork();
}
public void Complete()
{
long generation = _transit.Snapshot.Generation;
if (generation == 0)
return;
_transit.Complete(generation);
RetryPendingHostWork();
}
public void Cancel()
{
long generation = _transit.Snapshot.Generation;
if (generation == 0)
return;
_transit.Cancel(generation);
RetryPendingHostWork();
}
public void ResetSession()
{
ResetHostSession();
_transit.ResetSession();
}
/// <summary>
/// Retires only App streaming/presentation acknowledgements. The shared
/// Runtime generation-reset transaction clears canonical transit state
/// after this host boundary has converged.
/// </summary>
internal void ResetHostSession()
{
long generation = _transit.Snapshot.Generation;
if (generation != 0)
_transit.Cancel(generation);
RetryPendingHostWork();
}
/// <summary>
/// Resumes the exact unfinished graphical-host suffix. Completed callback
/// stages never replay; a thrown callback leaves its Runtime
/// acknowledgement pending for the next call.
/// </summary>
internal void RetryPendingHostWork()
{
_hostRetryRequested = true;
if (_hostRetryActive)
return;
_hostRetryActive = true;
try
{
do
{
_hostRetryRequested = false;
int index = 0;
while (index < _hostProjections.Count)
{
HostProjection host = _hostProjections[index];
DrainHostProjection(host);
if (index < _hostProjections.Count
&& ReferenceEquals(
_hostProjections[index],
host))
{
index++;
}
}
}
while (_hostRetryRequested);
}
finally
{
_hostRetryActive = false;
}
}
private void DrainHostProjection(HostProjection host)
{
if (!TryGetHostProjection(host, out var projection))
return;
RuntimeWorldHostAcknowledgementStage pending =
projection.PendingAcknowledgements;
if ((pending & RuntimeWorldHostAcknowledgementStage
.ProjectionRegistered) != 0)
{
CompleteHostRegistration(host);
}
if (!TryGetHostProjection(host, out projection))
return;
pending = projection.PendingAcknowledgements;
if ((pending & RuntimeWorldHostAcknowledgementStage
.SimulationReleaseProjected) != 0)
{
CompleteSimulationRelease(host);
}
if (!TryGetHostProjection(host, out projection))
return;
pending = projection.PendingAcknowledgements;
if ((pending & RuntimeWorldHostAcknowledgementStage
.DestinationReservationReleased) != 0)
{
CompleteDestinationReservationRelease(host);
}
if (!TryGetHostProjection(host, out projection))
return;
if ((projection.PendingAcknowledgements
& RuntimeWorldHostAcknowledgementStage.TerminalProjected)
== 0)
{
return;
}
Acknowledge(
host,
RuntimeWorldHostAcknowledgementStage.TerminalProjected);
RemoveHostProjection(host);
}
private void CompleteHostRegistration(HostProjection host)
{
if (!host.QuiescenceCommitted)
{
_quiescence?.CommitBegin(host.QuiescenceEdge);
host.QuiescenceCommitted = true;
}
if (!IsStagePending(
host,
RuntimeWorldHostAcknowledgementStage
.ProjectionRegistered))
{
return;
}
if (!host.StreamingRegistered)
{
_streaming?.BeginDestinationReservation(
host.Token.Generation,
host.Token.DestinationCell,
host.RequiredRenderRadius);
host.StreamingRegistered = true;
}
if (!IsStagePending(
host,
RuntimeWorldHostAcknowledgementStage
.ProjectionRegistered))
{
return;
}
if (!host.RenderResourcesRegistered)
{
_renderResources?.BeginDestinationReveal(
host.Token.Generation);
host.RenderResourcesRegistered = true;
}
if (!IsStagePending(
host,
RuntimeWorldHostAcknowledgementStage
.ProjectionRegistered))
{
return;
}
Acknowledge(
host,
RuntimeWorldHostAcknowledgementStage.ProjectionRegistered);
}
private void CompleteSimulationRelease(HostProjection host)
{
if (!host.SimulationReleaseProjected)
{
_quiescence?.ObserveReleased();
host.SimulationReleaseProjected = true;
}
if (!IsStagePending(
host,
RuntimeWorldHostAcknowledgementStage
.SimulationReleaseProjected))
{
return;
}
Acknowledge(
host,
RuntimeWorldHostAcknowledgementStage
.SimulationReleaseProjected);
}
private void CompleteDestinationReservationRelease(HostProjection host)
{
if (!host.StreamingReleased)
{
if (host.StreamingRegistered)
{
_streaming?.EndDestinationReservation(
host.Token.Generation);
}
host.StreamingReleased = true;
}
if (!IsStagePending(
host,
RuntimeWorldHostAcknowledgementStage
.DestinationReservationReleased))
{
return;
}
if (!host.RenderResourcesReleased)
{
if (host.RenderResourcesRegistered)
{
_renderResources?.EndDestinationReveal(
host.Token.Generation);
}
host.RenderResourcesReleased = true;
}
if (!IsStagePending(
host,
RuntimeWorldHostAcknowledgementStage
.DestinationReservationReleased))
{
return;
}
Acknowledge(
host,
RuntimeWorldHostAcknowledgementStage
.DestinationReservationReleased);
}
private void WithdrawHostForReplacement()
{
HostProjection? host = FindCurrentHostProjection();
if (host is null)
return;
if (!_transit.BeginHostProjectionSupersession(host.Token))
{
throw new InvalidOperationException(
"Runtime rejected reveal-host supersession.");
}
RetryPendingHostWork();
}
private HostProjection? FindCurrentHostProjection()
{
long generation = _transit.Snapshot.Generation;
for (int i = 0; i < _hostProjections.Count; i++)
{
HostProjection candidate = _hostProjections[i];
if (candidate.Token.Generation == generation)
return candidate;
}
return null;
}
private bool TryGetHostProjection(
HostProjection host,
out RuntimeWorldHostProjectionSnapshot projection)
{
if (_transit.TryGetHostProjection(host.Token, out projection))
return true;
RemoveHostProjection(host);
return false;
}
private bool IsStagePending(
HostProjection host,
RuntimeWorldHostAcknowledgementStage stage) =>
_transit.TryGetHostProjection(
host.Token,
out RuntimeWorldHostProjectionSnapshot projection)
&& (projection.PendingAcknowledgements & stage) != 0;
private void RemoveHostProjection(HostProjection host)
{
for (int i = 0; i < _hostProjections.Count; i++)
{
if (!ReferenceEquals(_hostProjections[i], host))
continue;
_hostProjections.RemoveAt(i);
return;
}
}
private void Acknowledge(
HostProjection host,
RuntimeWorldHostAcknowledgementStage stage)
{
if (!_transit.AcknowledgeHostProjection(
new RuntimeWorldHostAcknowledgement(
host.Token,
stage)))
{
throw new InvalidOperationException(
$"Runtime rejected reveal host acknowledgement {stage} "
+ $"for generation {host.Token.Generation}.");
}
}
}