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>
574 lines
18 KiB
C#
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 -> 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 -> 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}.");
|
|
}
|
|
}
|
|
}
|