refactor(runtime): acknowledge exact world host projections

This commit is contained in:
Erik 2026-07-26 18:27:41 +02:00
parent 73d0b54e38
commit 18d17d8bb1
17 changed files with 1677 additions and 72 deletions

View file

@ -453,6 +453,8 @@ internal sealed class FrameRootCompositionPhase
lifecycleAutomation =
new WorldLifecycleAutomationController(
() => session.WorldReveal.Snapshot,
() => d.WorldEnvironment.Runtime.Ownership,
() => live.WorldTransit.Ownership,
() => session.WorldReveal.PortalMaterializationCount,
resourceSnapshots.Capture,
screenshots,

View file

@ -6,6 +6,7 @@ using AcDream.App.Streaming;
using AcDream.App.UI.Testing;
using AcDream.Core.Physics;
using AcDream.Runtime;
using AcDream.Runtime.World;
namespace AcDream.App.Diagnostics;
@ -103,6 +104,8 @@ internal sealed record WorldLifecycleCheckpoint(
DateTime TimestampUtc,
int ProcessId,
RuntimePortalSnapshot Reveal,
RuntimeWorldEnvironmentOwnershipSnapshot EnvironmentOwnership,
RuntimeWorldTransitOwnershipSnapshot TransitOwnership,
RenderFrameOutcome Render,
WorldLifecycleResourceSnapshot Resources);
@ -177,6 +180,10 @@ internal sealed class WorldLifecycleAutomationController :
};
private readonly Func<RuntimePortalSnapshot> _getReveal;
private readonly Func<RuntimeWorldEnvironmentOwnershipSnapshot>
_getEnvironmentOwnership;
private readonly Func<RuntimeWorldTransitOwnershipSnapshot>
_getTransitOwnership;
private readonly Func<int> _getPortalMaterializationCount;
private readonly Func<RenderFrameOutcome, WorldLifecycleResourceSnapshot>
_captureResources;
@ -191,6 +198,9 @@ internal sealed class WorldLifecycleAutomationController :
public WorldLifecycleAutomationController(
Func<RuntimePortalSnapshot> getReveal,
Func<RuntimeWorldEnvironmentOwnershipSnapshot>
getEnvironmentOwnership,
Func<RuntimeWorldTransitOwnershipSnapshot> getTransitOwnership,
Func<int> getPortalMaterializationCount,
Func<RenderFrameOutcome, WorldLifecycleResourceSnapshot> captureResources,
FrameScreenshotController screenshots,
@ -198,6 +208,11 @@ internal sealed class WorldLifecycleAutomationController :
Action<string>? log = null)
{
_getReveal = getReveal ?? throw new ArgumentNullException(nameof(getReveal));
_getEnvironmentOwnership = getEnvironmentOwnership
?? throw new ArgumentNullException(
nameof(getEnvironmentOwnership));
_getTransitOwnership = getTransitOwnership
?? throw new ArgumentNullException(nameof(getTransitOwnership));
_getPortalMaterializationCount = getPortalMaterializationCount
?? throw new ArgumentNullException(nameof(getPortalMaterializationCount));
_captureResources = captureResources ?? throw new ArgumentNullException(nameof(captureResources));
@ -289,6 +304,8 @@ internal sealed class WorldLifecycleAutomationController :
TimestampUtc: DateTime.UtcNow,
ProcessId: Environment.ProcessId,
Reveal: _getReveal(),
EnvironmentOwnership: _getEnvironmentOwnership(),
TransitOwnership: _getTransitOwnership(),
Render: outcome,
Resources: _captureResources(outcome));
string json = JsonSerializer.Serialize(checkpoint, JsonOptions);

View file

@ -126,7 +126,9 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView
_actionView.Snapshot,
_movementView.Snapshot,
_environmentView.Snapshot,
_portalView.Snapshot);
_environmentView.Ownership,
_portalView.Snapshot,
_portalView.Ownership);
internal void Deactivate() => _active = false;
}

View file

@ -63,6 +63,9 @@ internal sealed class WorldGenerationQuiescence
private readonly Func<uint, RuntimeEntityKey?> _resolveProjectionKey;
private readonly IWorldAudioQuiescence? _audio;
private bool _effectsQuiesced;
private bool _selectionEdgeCommitted;
private bool _audioSuspended;
private bool _audioTransitionActive;
public WorldGenerationQuiescence(
SelectionState selection,
@ -99,26 +102,62 @@ internal sealed class WorldGenerationQuiescence
public void CommitBegin(in WorldGenerationQuiescenceEdge edge)
{
if (!edge.ShouldApply || _effectsQuiesced)
if (!edge.ShouldApply)
return;
_effectsQuiesced = true;
if (edge.ClearWorldSelection)
if (!_selectionEdgeCommitted)
{
_selection.Clear(
SelectionChangeSource.System,
SelectionChangeReason.Cleared);
// SelectionState commits its value before notifying observers.
// Record the edge first so a re-entrant reveal cannot replay it.
_selectionEdgeCommitted = true;
if (edge.ClearWorldSelection)
{
_selection.Clear(
SelectionChangeSource.System,
SelectionChangeReason.Cleared);
}
}
_audio?.SuspendWorldAudio();
ReconcileAudio();
}
public void ObserveReleased()
{
if (!_effectsQuiesced)
if (!_effectsQuiesced
&& !_selectionEdgeCommitted
&& !_audioSuspended)
{
return;
}
_effectsQuiesced = false;
_audio?.ResumeWorldAudio();
ReconcileAudio();
if (!_effectsQuiesced && !_audioSuspended)
_selectionEdgeCommitted = false;
}
private void ReconcileAudio()
{
if (_audio is null || _audioTransitionActive)
return;
_audioTransitionActive = true;
try
{
while (_audioSuspended != _effectsQuiesced)
{
bool suspend = _effectsQuiesced;
if (suspend)
_audio.SuspendWorldAudio();
else
_audio.ResumeWorldAudio();
_audioSuspended = suspend;
}
}
finally
{
_audioTransitionActive = false;
}
}
}

View file

@ -37,13 +37,31 @@ internal interface IWorldRevealRenderResourceScheduler
/// </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 long _reservationGeneration;
private bool _worldViewportReleased;
private readonly List<HostProjection> _hostProjections = [];
private bool _hostRetryActive;
private bool _hostRetryRequested;
public WorldRevealCoordinator(
RuntimeWorldTransitState transit,
@ -76,12 +94,15 @@ internal sealed class WorldRevealCoordinator
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();
@ -100,7 +121,15 @@ internal sealed class WorldRevealCoordinator
{
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(
@ -124,14 +153,21 @@ internal sealed class WorldRevealCoordinator
uint destinationCell,
in WorldGenerationQuiescenceEdge quiescenceEdge)
{
_quiescence?.CommitBegin(quiescenceEdge);
_reservationGeneration = generation;
_worldViewportReleased = false;
_streaming?.BeginDestinationReservation(
generation,
destinationCell,
WorldRevealReadinessBarrier.RequiredRenderRadius(destinationCell));
_renderResources?.BeginDestinationReveal(generation);
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>
@ -146,6 +182,7 @@ internal sealed class WorldRevealCoordinator
public WorldRevealReadinessSnapshot Evaluate(uint destinationCell)
{
RetryPendingHostWork();
WorldRevealReadinessSnapshot snapshot = _readiness.Evaluate(destinationCell);
RuntimePortalSnapshot portal = _transit.Snapshot;
if (portal.Generation != 0)
@ -189,17 +226,17 @@ internal sealed class WorldRevealCoordinator
ushort teleportSequence,
uint destinationCell)
{
bool wasAvailable = _transit.IsWorldSimulationAvailable;
bool acknowledged = _transit.AcknowledgePortalMaterialized(
generation,
teleportSequence,
destinationCell);
ObserveSimulationRelease(wasAvailable);
RetryPendingHostWork();
return acknowledged;
}
public void ObserveWorldViewportVisible()
{
RetryPendingHostWork();
long generation = _transit.Snapshot.Generation;
if (generation != 0)
_transit.AcknowledgeWorldViewportVisible(generation);
@ -220,13 +257,12 @@ internal sealed class WorldRevealCoordinator
/// </summary>
public void RevealWorldViewport()
{
long generation = _reservationGeneration;
if (generation == 0 || _worldViewportReleased)
HostProjection? host = FindCurrentHostProjection();
if (host is null)
return;
_streaming?.EndDestinationReservation(generation);
_renderResources?.EndDestinationReveal(generation);
_worldViewportReleased = true;
_transit.RequireDestinationReservationRelease(host.Token);
RetryPendingHostWork();
}
public void Complete()
@ -235,10 +271,8 @@ internal sealed class WorldRevealCoordinator
if (generation == 0)
return;
bool wasAvailable = _transit.IsWorldSimulationAvailable;
_transit.Complete(generation);
ObserveSimulationRelease(wasAvailable);
EndHostLifetime();
RetryPendingHostWork();
}
public void Cancel()
@ -247,37 +281,284 @@ internal sealed class WorldRevealCoordinator
if (generation == 0)
return;
bool wasAvailable = _transit.IsWorldSimulationAvailable;
_transit.Cancel(generation);
ObserveSimulationRelease(wasAvailable);
EndHostLifetime();
RetryPendingHostWork();
}
public void ResetSession()
{
bool wasAvailable = _transit.IsWorldSimulationAvailable;
long generation = _transit.Snapshot.Generation;
if (generation != 0)
_transit.Cancel(generation);
ObserveSimulationRelease(wasAvailable);
EndHostLifetime();
RetryPendingHostWork();
_transit.ResetSession();
}
private void EndHostLifetime()
/// <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()
{
long generation = _reservationGeneration;
if (generation == 0)
_hostRetryRequested = true;
if (_hostRetryActive)
return;
RevealWorldViewport();
_reservationGeneration = 0;
_worldViewportReleased = false;
_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 ObserveSimulationRelease(bool wasAvailable)
private void DrainHostProjection(HostProjection host)
{
if (!wasAvailable && _transit.IsWorldSimulationAvailable)
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}.");
}
}
}

View file

@ -192,8 +192,19 @@ public sealed class RuntimeTraceRecorder : IRuntimeEventObserver
$"{checkpoint.Environment.ActiveDayGroupIndex}:" +
$"{(int)checkpoint.Environment.Weather}:" +
$"{(int)checkpoint.Environment.EnvironOverride};" +
$"environment-owner={checkpoint.EnvironmentOwnership.IsInitialized}:" +
$"{checkpoint.EnvironmentOwnership.DayGroupDefinitionCount}:" +
$"{checkpoint.EnvironmentOwnership.ActiveDayGroupCount};" +
$"portal={checkpoint.Portal.Generation}:{(int)checkpoint.Portal.Kind}:" +
$"{checkpoint.Portal.DestinationCell:X8}:{checkpoint.Portal.IsReady}")));
$"{checkpoint.Portal.DestinationCell:X8}:{checkpoint.Portal.IsReady};" +
$"transit-owner={checkpoint.TransitOwnership.BufferedTeleportDestinationCount}:" +
$"{checkpoint.TransitOwnership.PendingTeleportStartCount}:" +
$"{checkpoint.TransitOwnership.ActiveTeleportCount}:" +
$"{checkpoint.TransitOwnership.AcceptedTeleportDestinationCount}:" +
$"{checkpoint.TransitOwnership.ActiveRevealCount}:" +
$"{checkpoint.TransitOwnership.PendingDestinationReadinessCount}:" +
$"{checkpoint.TransitOwnership.HostProjectionCount}:" +
$"{checkpoint.TransitOwnership.PendingHostAcknowledgementCount}")));
}
public void OnLifecycle(in RuntimeLifecycleDelta delta) =>

View file

@ -128,6 +128,55 @@ public readonly record struct RuntimeDestinationReadiness(
&& IsCollisionReady));
}
[Flags]
public enum RuntimeWorldHostAcknowledgementStage
{
None = 0,
ProjectionRegistered = 1 << 0,
SimulationReleaseProjected = 1 << 1,
DestinationReservationReleased = 1 << 2,
TerminalProjected = 1 << 3,
}
/// <summary>
/// Exact identity of one graphical or direct host projection attached to a
/// Runtime reveal generation. It contains no presentation object.
/// </summary>
public readonly record struct RuntimeWorldHostProjectionToken(
long Generation,
uint DestinationCell)
{
public bool IsValid => Generation != 0 && DestinationCell != 0u;
}
public readonly record struct RuntimeWorldHostAcknowledgement(
RuntimeWorldHostProjectionToken Projection,
RuntimeWorldHostAcknowledgementStage Stage);
public readonly record struct RuntimeWorldHostProjectionSnapshot(
RuntimeWorldHostProjectionToken Token,
RuntimeWorldHostAcknowledgementStage PendingAcknowledgements,
bool ProjectionRegistered,
bool SimulationReleaseProjected,
bool DestinationReservationReleased,
bool IsSuperseding)
{
public int PendingAcknowledgementCount =>
Count(PendingAcknowledgements);
private static int Count(RuntimeWorldHostAcknowledgementStage stages)
{
int value = (int)stages;
int count = 0;
while (value != 0)
{
value &= value - 1;
count++;
}
return count;
}
}
public readonly record struct RuntimePortalSnapshot(
long Generation,
RuntimePortalKind Kind,
@ -166,6 +215,7 @@ public readonly record struct RuntimePortalSnapshot(
public interface IRuntimePortalView
{
RuntimePortalSnapshot Snapshot { get; }
RuntimeWorldTransitOwnershipSnapshot Ownership { get; }
}
public readonly record struct RuntimeStateCheckpoint(
@ -184,7 +234,9 @@ public readonly record struct RuntimeStateCheckpoint(
RuntimeActionSnapshot Actions,
RuntimeMovementSnapshot Movement,
RuntimeWorldEnvironmentSnapshot Environment,
RuntimePortalSnapshot Portal);
RuntimeWorldEnvironmentOwnershipSnapshot EnvironmentOwnership,
RuntimePortalSnapshot Portal,
RuntimeWorldTransitOwnershipSnapshot TransitOwnership);
public interface IGameRuntimeView
{

View file

@ -96,9 +96,15 @@ public readonly record struct RuntimeWorldEnvironmentSnapshot(
WeatherKind Weather,
EnvironOverride EnvironOverride);
public readonly record struct RuntimeWorldEnvironmentOwnershipSnapshot(
bool IsInitialized,
int DayGroupDefinitionCount,
int ActiveDayGroupCount);
public interface IRuntimeWorldEnvironmentView
{
RuntimeWorldEnvironmentSnapshot Snapshot { get; }
RuntimeWorldEnvironmentOwnershipSnapshot Ownership { get; }
}
/// <summary>
@ -152,6 +158,15 @@ public sealed class RuntimeWorldEnvironmentState
Weather.Kind,
Weather.Override);
public RuntimeWorldEnvironmentOwnershipSnapshot Ownership =>
CaptureOwnership();
public RuntimeWorldEnvironmentOwnershipSnapshot CaptureOwnership() =>
new(
IsInitialized,
_definition?.DayGroups.Count ?? 0,
ActiveDayGroupIndex >= 0 ? 1 : 0);
public void Initialize(RuntimeWorldEnvironmentDefinition definition)
{
ArgumentNullException.ThrowIfNull(definition);

View file

@ -2,6 +2,27 @@ using System.Globalization;
namespace AcDream.Runtime.World;
public readonly record struct RuntimeWorldTransitOwnershipSnapshot(
int BufferedTeleportDestinationCount,
int PendingTeleportStartCount,
int ActiveTeleportCount,
int AcceptedTeleportDestinationCount,
int ActiveRevealCount,
int PendingDestinationReadinessCount,
int HostProjectionCount,
int PendingHostAcknowledgementCount)
{
public bool IsSessionIdle =>
BufferedTeleportDestinationCount == 0
&& PendingTeleportStartCount == 0
&& ActiveTeleportCount == 0
&& AcceptedTeleportDestinationCount == 0
&& ActiveRevealCount == 0
&& PendingDestinationReadinessCount == 0
&& HostProjectionCount == 0
&& PendingHostAcknowledgementCount == 0;
}
/// <summary>
/// Canonical presentation-independent owner of local F751/Position
/// correlation and one login/portal reveal generation. The graphical or
@ -21,12 +42,34 @@ namespace AcDream.Runtime.World;
public sealed class RuntimeWorldTransitState
: IRuntimePortalView
{
private sealed class HostProjectionRecord(
RuntimeWorldHostProjectionToken token)
{
public RuntimeWorldHostProjectionToken Token { get; } = token;
public RuntimeWorldHostAcknowledgementStage Pending { get; set; } =
RuntimeWorldHostAcknowledgementStage.ProjectionRegistered;
public bool ProjectionRegistered { get; set; }
public bool SimulationReleaseProjected { get; set; }
public bool DestinationReservationReleased { get; set; }
public bool IsSuperseding { get; set; }
public RuntimeWorldHostProjectionSnapshot Snapshot => new(
Token,
Pending,
ProjectionRegistered,
SimulationReleaseProjected,
DestinationReservationReleased,
IsSuperseding);
}
public static readonly TimeSpan RetailWaitCueDelay =
TimeSpan.FromSeconds(5);
private readonly Action<string> _log;
private readonly Dictionary<ushort, RuntimeTeleportDestination>
_bufferedDestinations = [];
private readonly Dictionary<long, HostProjectionRecord>
_hostProjections = [];
private RuntimePortalSnapshot _snapshot = RuntimePortalSnapshot.Idle;
private long _nextGeneration;
private bool _hasLastTeleportStart;
@ -45,6 +88,8 @@ public sealed class RuntimeWorldTransitState
}
public RuntimePortalSnapshot Snapshot => _snapshot;
public RuntimeWorldTransitOwnershipSnapshot Ownership =>
CaptureOwnership();
public bool IsWorldSimulationAvailable =>
_snapshot.WorldSimulationAvailable;
public bool IsTeleportActive => _teleportActive;
@ -57,6 +102,30 @@ public sealed class RuntimeWorldTransitState
public int DiagnosticFailureCount { get; private set; }
public Exception? LastDiagnosticFailure { get; private set; }
public RuntimeWorldTransitOwnershipSnapshot CaptureOwnership()
{
int pendingHostAcknowledgements = 0;
foreach (HostProjectionRecord host in _hostProjections.Values)
{
pendingHostAcknowledgements = checked(
pendingHostAcknowledgements
+ host.Snapshot.PendingAcknowledgementCount);
}
bool revealActive = _snapshot.Generation != 0
&& !_snapshot.Completed
&& !_snapshot.Cancelled;
return new RuntimeWorldTransitOwnershipSnapshot(
_bufferedDestinations.Count,
_hasPendingTeleportStart ? 1 : 0,
_teleportActive ? 1 : 0,
_hasAcceptedDestination ? 1 : 0,
revealActive ? 1 : 0,
revealActive && !_snapshot.IsReady ? 1 : 0,
_hostProjections.Count,
pendingHostAcknowledgements);
}
/// <summary>
/// Starts initial-login reveal. Portal reveals must instead claim the
/// exact F751-correlated destination through
@ -69,15 +138,30 @@ public sealed class RuntimeWorldTransitState
/// Atomically claims the one accepted Position paired with the active F751
/// sequence and starts its reveal generation.
/// </summary>
public bool CanBeginPortalReveal(
ushort teleportSequence,
uint destinationCell)
{
if (!_teleportActive
|| teleportSequence != _activeTeleportSequence
|| !_hasAcceptedDestination)
{
return false;
}
RuntimeTeleportDestination destination = _acceptedDestination;
return destination.TeleportSequence == teleportSequence
&& destination.CellId != 0u
&& destination.CellId == destinationCell;
}
public bool TryBeginPortalReveal(
ushort teleportSequence,
uint destinationCell,
out long generation)
{
generation = 0;
if (!_teleportActive
|| teleportSequence != _activeTeleportSequence
|| !_hasAcceptedDestination)
if (!CanBeginPortalReveal(teleportSequence, destinationCell))
{
LogRejected(
"portal-begin-without-destination",
@ -89,19 +173,6 @@ public sealed class RuntimeWorldTransitState
}
RuntimeTeleportDestination destination = _acceptedDestination;
if (destination.TeleportSequence != teleportSequence
|| destination.CellId == 0u
|| destination.CellId != destinationCell)
{
FailInvariant(
"portal-begin-destination-mismatch",
$"sequence={teleportSequence} "
+ $"acceptedSequence={destination.TeleportSequence} "
+ $"expectedCell=0x{destination.CellId:X8} "
+ $"actualCell=0x{destinationCell:X8}");
return false;
}
generation = BeginRevealCore(
RuntimePortalKind.Portal,
destinationCell);
@ -110,6 +181,159 @@ public sealed class RuntimeWorldTransitState
return true;
}
/// <summary>
/// Registers one presentation-independent host projection against the
/// exact active reveal. Graphical and direct hosts receive the same token.
/// </summary>
public bool TryRegisterHostProjection(
long generation,
uint destinationCell,
out RuntimeWorldHostProjectionToken token)
{
token = new RuntimeWorldHostProjectionToken(
generation,
destinationCell);
if (!token.IsValid
|| generation != _snapshot.Generation
|| destinationCell != _snapshot.DestinationCell
|| _snapshot.Cancelled
|| _snapshot.Completed)
{
LogRejected(
"host-register-mismatch",
$"generation={generation} "
+ $"cell=0x{destinationCell:X8}");
token = default;
return false;
}
if (_hostProjections.TryGetValue(
generation,
out HostProjectionRecord? existing))
{
if (existing.Token == token)
return true;
LogRejected(
"host-register-conflict",
$"generation={generation} "
+ $"expectedCell=0x{existing.Token.DestinationCell:X8} "
+ $"actualCell=0x{destinationCell:X8}");
token = default;
return false;
}
_hostProjections.Add(
generation,
new HostProjectionRecord(token));
return true;
}
public bool TryGetHostProjection(
in RuntimeWorldHostProjectionToken token,
out RuntimeWorldHostProjectionSnapshot projection)
{
if (token.IsValid
&& _hostProjections.TryGetValue(
token.Generation,
out HostProjectionRecord? host)
&& host.Token == token)
{
projection = host.Snapshot;
return true;
}
projection = default;
return false;
}
/// <summary>
/// Marks the graphical reservation-release callback as due before the
/// host executes it. A thrown callback therefore leaves an exact pending
/// acknowledgement instead of losing the unfinished suffix.
/// </summary>
public bool RequireDestinationReservationRelease(
in RuntimeWorldHostProjectionToken token)
{
if (!TryGetHostRecord(token, out HostProjectionRecord? host))
return false;
if (host.DestinationReservationReleased)
return false;
host.Pending |= RuntimeWorldHostAcknowledgementStage
.DestinationReservationReleased;
return true;
}
/// <summary>
/// Converts an older host projection into an exact withdrawal transaction
/// while a replacement reveal keeps world simulation quiesced.
/// </summary>
public bool BeginHostProjectionSupersession(
in RuntimeWorldHostProjectionToken token)
{
if (!TryGetHostRecord(token, out HostProjectionRecord? host))
return false;
BeginHostProjectionSupersession(host);
return true;
}
public bool AcknowledgeHostProjection(
in RuntimeWorldHostAcknowledgement acknowledgement)
{
RuntimeWorldHostAcknowledgementStage stage =
acknowledgement.Stage;
if (!IsSingleStage(stage)
|| !TryGetHostRecord(
acknowledgement.Projection,
out HostProjectionRecord? host)
|| (host.Pending & stage) == 0)
{
return false;
}
switch (stage)
{
case RuntimeWorldHostAcknowledgementStage.ProjectionRegistered:
if (host.IsSuperseding)
return false;
host.ProjectionRegistered = true;
break;
case RuntimeWorldHostAcknowledgementStage
.SimulationReleaseProjected:
if (host.Token.Generation == _snapshot.Generation
&& !_snapshot.WorldSimulationAvailable)
{
return false;
}
host.SimulationReleaseProjected = true;
break;
case RuntimeWorldHostAcknowledgementStage
.DestinationReservationReleased:
host.DestinationReservationReleased = true;
break;
case RuntimeWorldHostAcknowledgementStage.TerminalProjected:
if (!host.IsSuperseding
&& (host.Token.Generation != _snapshot.Generation
|| (!_snapshot.Completed
&& !_snapshot.Cancelled)))
{
return false;
}
if ((host.Pending & ~stage) != 0)
return false;
host.Pending &= ~stage;
_hostProjections.Remove(host.Token.Generation);
return true;
default:
return false;
}
host.Pending &= ~stage;
return true;
}
private long BeginRevealCore(
RuntimePortalKind kind,
uint destinationCell)
@ -125,6 +349,12 @@ public sealed class RuntimeWorldTransitState
{
Log("superseded", _snapshot);
}
if (_hostProjections.TryGetValue(
_snapshot.Generation,
out HostProjectionRecord? superseded))
{
BeginHostProjectionSupersession(superseded);
}
long generation = checked(++_nextGeneration);
int failures = _snapshot.InvariantFailureCount;
@ -372,6 +602,10 @@ public sealed class RuntimeWorldTransitState
WorldSimulationAvailable = true,
PortalMaterializationCount = count,
};
RequireHostStage(
generation,
RuntimeWorldHostAcknowledgementStage
.SimulationReleaseProjected);
Log("materialized", _snapshot);
return true;
}
@ -444,11 +678,16 @@ public sealed class RuntimeWorldTransitState
return false;
}
bool simulationWasAvailable =
_snapshot.WorldSimulationAvailable;
_snapshot = _snapshot with
{
Completed = true,
WorldSimulationAvailable = true,
};
RequireTerminalHostProjection(
generation,
requireSimulationRelease: !simulationWasAvailable);
Log("complete", _snapshot);
return true;
}
@ -463,11 +702,16 @@ public sealed class RuntimeWorldTransitState
return false;
}
bool simulationWasAvailable =
_snapshot.WorldSimulationAvailable;
_snapshot = _snapshot with
{
Cancelled = true,
WorldSimulationAvailable = true,
};
RequireTerminalHostProjection(
generation,
requireSimulationRelease: !simulationWasAvailable);
Log("cancel", _snapshot);
return true;
}
@ -479,6 +723,17 @@ public sealed class RuntimeWorldTransitState
/// </summary>
public void ResetSession()
{
if (_hostProjections.Count != 0)
{
RuntimeWorldTransitOwnershipSnapshot ownership =
CaptureOwnership();
throw new InvalidOperationException(
"World transit cannot reset while a host projection "
+ "still owns an acknowledgement suffix "
+ $"(hosts={ownership.HostProjectionCount}, "
+ $"pending={ownership.PendingHostAcknowledgementCount}).");
}
_snapshot = RuntimePortalSnapshot.Idle;
EndTeleport();
_hasLastTeleportStart = false;
@ -486,6 +741,105 @@ public sealed class RuntimeWorldTransitState
_bufferedDestinations.Clear();
}
private bool TryGetHostRecord(
in RuntimeWorldHostProjectionToken token,
out HostProjectionRecord host)
{
if (token.IsValid
&& _hostProjections.TryGetValue(
token.Generation,
out HostProjectionRecord? candidate)
&& candidate.Token == token)
{
host = candidate;
return true;
}
host = null!;
return false;
}
private void RequireHostStage(
long generation,
RuntimeWorldHostAcknowledgementStage stage)
{
if (!_hostProjections.TryGetValue(
generation,
out HostProjectionRecord? host))
{
return;
}
bool alreadyAcknowledged = stage switch
{
RuntimeWorldHostAcknowledgementStage.ProjectionRegistered =>
host.ProjectionRegistered,
RuntimeWorldHostAcknowledgementStage
.SimulationReleaseProjected =>
host.SimulationReleaseProjected,
RuntimeWorldHostAcknowledgementStage
.DestinationReservationReleased =>
host.DestinationReservationReleased,
_ => false,
};
if (!alreadyAcknowledged)
host.Pending |= stage;
}
private void RequireTerminalHostProjection(
long generation,
bool requireSimulationRelease)
{
if (!_hostProjections.TryGetValue(
generation,
out HostProjectionRecord? host))
{
return;
}
// A host that is terminating no longer has to finish acquiring a
// projection. It must instead release every resource it did acquire.
host.Pending &=
~RuntimeWorldHostAcknowledgementStage.ProjectionRegistered;
if (requireSimulationRelease
&& !host.SimulationReleaseProjected)
{
host.Pending |= RuntimeWorldHostAcknowledgementStage
.SimulationReleaseProjected;
}
if (!host.DestinationReservationReleased)
{
host.Pending |= RuntimeWorldHostAcknowledgementStage
.DestinationReservationReleased;
}
host.Pending |=
RuntimeWorldHostAcknowledgementStage.TerminalProjected;
}
private static void BeginHostProjectionSupersession(
HostProjectionRecord host)
{
host.IsSuperseding = true;
host.Pending &=
~(RuntimeWorldHostAcknowledgementStage.ProjectionRegistered
| RuntimeWorldHostAcknowledgementStage
.SimulationReleaseProjected);
if (!host.DestinationReservationReleased)
{
host.Pending |= RuntimeWorldHostAcknowledgementStage
.DestinationReservationReleased;
}
host.Pending |=
RuntimeWorldHostAcknowledgementStage.TerminalProjected;
}
private static bool IsSingleStage(
RuntimeWorldHostAcknowledgementStage stage)
{
int value = (int)stage;
return value != 0 && (value & (value - 1)) == 0;
}
private bool IsCurrentPortalDestination(
long generation,
ushort teleportSequence,

View file

@ -6,6 +6,7 @@ using AcDream.App.Rendering.Scene;
using AcDream.App.Streaming;
using AcDream.App.UI.Testing;
using AcDream.Runtime;
using AcDream.Runtime.World;
using SixLabors.ImageSharp;
namespace AcDream.App.Tests.Diagnostics;
@ -179,6 +180,19 @@ public sealed class WorldLifecycleAutomationControllerTests
Path.Combine(directory, "screenshots"));
var controller = new WorldLifecycleAutomationController(
() => reveal,
() => new RuntimeWorldEnvironmentOwnershipSnapshot(
IsInitialized: true,
DayGroupDefinitionCount: 4,
ActiveDayGroupCount: 1),
() => new RuntimeWorldTransitOwnershipSnapshot(
BufferedTeleportDestinationCount: 0,
PendingTeleportStartCount: 0,
ActiveTeleportCount: 0,
AcceptedTeleportDestinationCount: 0,
ActiveRevealCount: 0,
PendingDestinationReadinessCount: 0,
HostProjectionCount: 0,
PendingHostAcknowledgementCount: 0),
() => 3,
_ => resources,
screenshots,
@ -205,6 +219,15 @@ public sealed class WorldLifecycleAutomationControllerTests
using JsonDocument json = JsonDocument.Parse(line);
Assert.Equal("dungeon", json.RootElement.GetProperty("name").GetString());
Assert.Equal(7, json.RootElement.GetProperty("reveal").GetProperty("generation").GetInt64());
Assert.Equal(
4,
json.RootElement.GetProperty("environmentOwnership")
.GetProperty("dayGroupDefinitionCount").GetInt32());
Assert.Equal(
0,
json.RootElement.GetProperty("transitOwnership")
.GetProperty("pendingHostAcknowledgementCount")
.GetInt32());
Assert.Equal(4, json.RootElement.GetProperty("render").GetProperty("world")
.GetProperty("visibleLandblocks").GetInt32());
Assert.True(json.RootElement.GetProperty("render").GetProperty("presentation")
@ -373,6 +396,8 @@ public sealed class WorldLifecycleAutomationControllerTests
string directory,
Func<RenderFrameOutcome, WorldLifecycleResourceSnapshot> capture) =>
new(
() => default,
() => default,
() => default,
() => 0,
capture,

View file

@ -81,7 +81,34 @@ public sealed class RuntimeWorldTransitOwnershipTests
coordinatorFields,
field => field.Name is "_activeGeneration"
or "_worldSimulationReleased"
or "_lifecycle");
or "_lifecycle"
or "_reservationGeneration"
or "_worldViewportReleased"
or "_hostProjection");
Assert.DoesNotContain(
coordinatorFields,
field => field.FieldType == typeof(RuntimePortalSnapshot)
|| field.FieldType == typeof(RuntimeDestinationReadiness)
|| field.FieldType == typeof(long)
|| field.FieldType == typeof(uint));
Type hostProjection = Assert.Single(
typeof(WorldRevealCoordinator).GetNestedTypes(
System.Reflection.BindingFlags.NonPublic),
type => type.Name == "HostProjection");
var hostFields = hostProjection.GetFields(
System.Reflection.BindingFlags.Instance
| System.Reflection.BindingFlags.NonPublic);
Assert.Single(
hostFields,
field => field.FieldType
== typeof(RuntimeWorldHostProjectionToken));
Assert.DoesNotContain(
hostFields,
field => field.FieldType == typeof(RuntimePortalSnapshot)
|| field.FieldType == typeof(RuntimeDestinationReadiness)
|| field.FieldType == typeof(long)
|| field.FieldType == typeof(uint));
var teleportFields = typeof(LocalPlayerTeleportController)
.GetFields(

View file

@ -144,6 +144,39 @@ public sealed class WorldGenerationQuiescenceTests
Assert.Equal(inventoryGuid, selection.SelectedObjectId);
}
[Fact]
public void ReentrantBeginDuringAudioResume_ReconcilesToNewestEdge()
{
var transit = new RuntimeWorldTransitState();
var world = new GpuWorldState(
availability: new WorldGenerationAvailabilityState(transit));
var audio = new ReentrantAudioQuiescence();
var quiescence = new WorldGenerationQuiescence(
new SelectionState(),
world,
_ => null,
audio);
var edge = new WorldGenerationQuiescenceEdge(
ShouldApply: true,
ClearWorldSelection: false);
quiescence.CommitBegin(edge);
audio.OnResume = () =>
{
audio.OnResume = null;
quiescence.CommitBegin(edge);
};
quiescence.ObserveReleased();
Assert.Equal(2, audio.SuspendCalls);
Assert.Equal(1, audio.ResumeCalls);
quiescence.ObserveReleased();
Assert.Equal(2, audio.SuspendCalls);
Assert.Equal(2, audio.ResumeCalls);
}
private static RuntimeDestinationReadiness Ready(
long generation,
uint destinationCell) =>
@ -178,4 +211,19 @@ public sealed class WorldGenerationQuiescenceTests
public void SuspendWorldAudio() => SuspendCalls++;
public void ResumeWorldAudio() => ResumeCalls++;
}
private sealed class ReentrantAudioQuiescence : IWorldAudioQuiescence
{
public int SuspendCalls { get; private set; }
public int ResumeCalls { get; private set; }
public Action? OnResume { get; set; }
public void SuspendWorldAudio() => SuspendCalls++;
public void ResumeWorldAudio()
{
ResumeCalls++;
OnResume?.Invoke();
}
}
}

View file

@ -409,8 +409,9 @@ public sealed class WorldRevealCoordinatorTests
(second, 0x20210123u, 0),
],
scheduler.Begins);
coordinator.Complete();
Assert.Equal([second], scheduler.Ends);
Assert.Equal([first], scheduler.Ends);
coordinator.Cancel();
Assert.Equal([first, second], scheduler.Ends);
}
[Fact]
@ -434,10 +435,200 @@ public sealed class WorldRevealCoordinatorTests
sequence: 2);
Assert.Equal([first, second], resources.Begins);
Assert.Equal([first], resources.Ends);
coordinator.RevealWorldViewport();
coordinator.Complete();
Assert.Equal([second], resources.Ends);
Assert.Equal([first, second], resources.Ends);
}
[Fact]
public void RegistrationCallback_ReentrantReplacementRetiresExactOldSuffix()
{
var state = new State();
var scheduler = new RecordingDestinationScheduler();
WorldRevealCoordinator coordinator = null!;
long replacement = 0;
scheduler.OnBegin = () =>
{
scheduler.OnBegin = null;
replacement = coordinator.BeginLogin(0x20210123u);
};
coordinator = state.Build(streaming: scheduler);
long first = coordinator.BeginLogin(0x11340021u);
Assert.NotEqual(first, replacement);
Assert.Equal(replacement, coordinator.Snapshot.Generation);
Assert.Equal(
[
(first, 0x11340021u, 1),
(replacement, 0x20210123u, 0),
],
scheduler.Begins);
Assert.Equal([first], scheduler.Ends);
Assert.Equal(1, coordinator.Ownership.HostProjectionCount);
Assert.Equal(
0,
coordinator.Ownership.PendingHostAcknowledgementCount);
coordinator.Cancel();
Assert.Equal([first, replacement], scheduler.Ends);
Assert.Equal(0, coordinator.Ownership.HostProjectionCount);
}
[Fact]
public void RegistrationFailure_RetriesExactSuffixWithoutReplayingBegin()
{
var state = new State();
var scheduler = new RecordingDestinationScheduler();
var resources = new RecordingRenderResourceScheduler
{
ThrowBeginCount = 1,
};
WorldRevealCoordinator coordinator = state.Build(
streaming: scheduler,
renderResources: resources);
Assert.Throws<InvalidOperationException>(
() => coordinator.BeginLogin(0x11340021u));
Assert.Single(scheduler.Begins);
Assert.Equal(1, resources.BeginAttempts);
Assert.Empty(resources.Begins);
Assert.Equal(1, coordinator.Ownership.HostProjectionCount);
Assert.Equal(
1,
coordinator.Ownership.PendingHostAcknowledgementCount);
coordinator.RetryPendingHostWork();
Assert.Single(scheduler.Begins);
Assert.Equal(2, resources.BeginAttempts);
Assert.Single(resources.Begins);
Assert.Equal(
0,
coordinator.Ownership.PendingHostAcknowledgementCount);
coordinator.Cancel();
Assert.Equal(0, coordinator.Ownership.HostProjectionCount);
}
[Fact]
public void ReservationReleaseFailure_RetriesOnlyUnfinishedCallback()
{
var state = new State();
var scheduler = new RecordingDestinationScheduler();
var resources = new RecordingRenderResourceScheduler
{
ThrowEndCount = 1,
};
WorldRevealCoordinator coordinator = state.Build(
streaming: scheduler,
renderResources: resources);
long generation = coordinator.BeginLogin(0x11340021u);
Assert.Throws<InvalidOperationException>(
coordinator.RevealWorldViewport);
Assert.Equal([generation], scheduler.Ends);
Assert.Equal(1, resources.EndAttempts);
Assert.Empty(resources.Ends);
Assert.Equal(
1,
coordinator.Ownership.PendingHostAcknowledgementCount);
coordinator.RetryPendingHostWork();
Assert.Equal([generation], scheduler.Ends);
Assert.Equal(2, resources.EndAttempts);
Assert.Equal([generation], resources.Ends);
Assert.Equal(
0,
coordinator.Ownership.PendingHostAcknowledgementCount);
coordinator.Cancel();
}
[Fact]
public void ResetFailure_RetainsWithdrawalAndConvergesOnRetry()
{
var state = new State();
var scheduler = new RecordingDestinationScheduler
{
ThrowEndCount = 1,
};
WorldRevealCoordinator coordinator =
state.Build(streaming: scheduler);
coordinator.BeginLogin(0x11340021u);
Assert.Throws<InvalidOperationException>(coordinator.ResetSession);
Assert.True(coordinator.Snapshot.Cancelled);
Assert.Equal(1, coordinator.Ownership.HostProjectionCount);
Assert.True(
coordinator.Ownership.PendingHostAcknowledgementCount > 0);
coordinator.ResetSession();
Assert.Equal(RuntimePortalSnapshot.Idle, coordinator.Snapshot);
Assert.True(coordinator.Ownership.IsSessionIdle);
Assert.Equal(2, scheduler.EndAttempts);
Assert.Single(scheduler.Ends);
}
[Fact]
public void SimulationReleaseFailure_RetriesAudioWithoutReplayingMaterialization()
{
var transit = new RuntimeWorldTransitState();
var availability = new WorldGenerationAvailabilityState(transit);
var world = new GpuWorldState(availability: availability);
var audio = new RecordingAudioQuiescence
{
ThrowResumeCount = 1,
};
var quiescence = new WorldGenerationQuiescence(
new SelectionState(),
world,
_ => null,
audio);
var coordinator = new WorldRevealCoordinator(
transit,
isRenderNeighborhoodReady: (_, _) => true,
isSpawnCellReady: _ => true,
isTerrainNeighborhoodReady: (_, _) => true,
areCompositeTexturesReady: () => true,
prepareCompositeTextures: (_, _) => { },
invalidateCompositeTextures: () => { },
isSpawnClaimUnhydratable: _ => false,
quiescence: quiescence);
const uint cell = 0x3032001Cu;
long generation = BeginPortal(
coordinator,
transit,
cell,
sequence: 1);
Assert.True(coordinator.Evaluate(cell).IsReady);
Assert.Throws<InvalidOperationException>(() =>
coordinator.ObserveMaterialized(
generation,
1,
cell));
Assert.True(coordinator.Snapshot.Materialized);
Assert.Equal(1, coordinator.PortalMaterializationCount);
Assert.Equal(1, audio.ResumeAttempts);
Assert.Equal(0, audio.ResumeCalls);
Assert.Equal(
1,
coordinator.Ownership.PendingHostAcknowledgementCount);
coordinator.RetryPendingHostWork();
Assert.Equal(2, audio.ResumeAttempts);
Assert.Equal(1, audio.ResumeCalls);
Assert.Equal(1, coordinator.PortalMaterializationCount);
Assert.Equal(
0,
coordinator.Ownership.PendingHostAcknowledgementCount);
coordinator.Cancel();
}
private static long BeginPortal(
@ -471,9 +662,32 @@ public sealed class WorldRevealCoordinatorTests
{
public int SuspendCalls { get; private set; }
public int ResumeCalls { get; private set; }
public int SuspendAttempts { get; private set; }
public int ResumeAttempts { get; private set; }
public int ThrowSuspendCount { get; set; }
public int ThrowResumeCount { get; set; }
public void SuspendWorldAudio() => SuspendCalls++;
public void ResumeWorldAudio() => ResumeCalls++;
public void SuspendWorldAudio()
{
SuspendAttempts++;
if (ThrowSuspendCount > 0)
{
ThrowSuspendCount--;
throw new InvalidOperationException("suspend failed");
}
SuspendCalls++;
}
public void ResumeWorldAudio()
{
ResumeAttempts++;
if (ThrowResumeCount > 0)
{
ThrowResumeCount--;
throw new InvalidOperationException("resume failed");
}
ResumeCalls++;
}
}
private sealed class RecordingDestinationScheduler
@ -481,16 +695,40 @@ public sealed class WorldRevealCoordinatorTests
{
public List<(long Generation, uint Cell, int Radius)> Begins { get; } = [];
public List<long> Ends { get; } = [];
public int BeginAttempts { get; private set; }
public int EndAttempts { get; private set; }
public int ThrowBeginCount { get; set; }
public int ThrowEndCount { get; set; }
public Action? OnBegin { get; set; }
public Action? OnEnd { get; set; }
public void BeginDestinationReservation(
long revealGeneration,
uint destinationCell,
int requiredRenderRadius) =>
int requiredRenderRadius)
{
BeginAttempts++;
if (ThrowBeginCount > 0)
{
ThrowBeginCount--;
throw new InvalidOperationException("stream begin failed");
}
Begins.Add(
(revealGeneration, destinationCell, requiredRenderRadius));
OnBegin?.Invoke();
}
public void EndDestinationReservation(long revealGeneration) =>
public void EndDestinationReservation(long revealGeneration)
{
EndAttempts++;
if (ThrowEndCount > 0)
{
ThrowEndCount--;
throw new InvalidOperationException("stream end failed");
}
Ends.Add(revealGeneration);
OnEnd?.Invoke();
}
}
private sealed class RecordingRenderResourceScheduler
@ -498,11 +736,31 @@ public sealed class WorldRevealCoordinatorTests
{
public List<long> Begins { get; } = [];
public List<long> Ends { get; } = [];
public int BeginAttempts { get; private set; }
public int EndAttempts { get; private set; }
public int ThrowBeginCount { get; set; }
public int ThrowEndCount { get; set; }
public void BeginDestinationReveal(long revealGeneration) =>
public void BeginDestinationReveal(long revealGeneration)
{
BeginAttempts++;
if (ThrowBeginCount > 0)
{
ThrowBeginCount--;
throw new InvalidOperationException("render begin failed");
}
Begins.Add(revealGeneration);
}
public void EndDestinationReveal(long revealGeneration) =>
public void EndDestinationReveal(long revealGeneration)
{
EndAttempts++;
if (ThrowEndCount > 0)
{
ThrowEndCount--;
throw new InvalidOperationException("render end failed");
}
Ends.Add(revealGeneration);
}
}
}

View file

@ -1,5 +1,6 @@
using System.Reflection;
using AcDream.Runtime.Gameplay;
using AcDream.Runtime.World;
namespace AcDream.Runtime.Tests;
@ -183,7 +184,20 @@ public sealed class GameRuntimeContractTests
InteractionTransactions: default),
default,
default,
default);
new RuntimeWorldEnvironmentOwnershipSnapshot(
IsInitialized: true,
DayGroupDefinitionCount: 4,
ActiveDayGroupCount: 1),
default,
new RuntimeWorldTransitOwnershipSnapshot(
BufferedTeleportDestinationCount: 0,
PendingTeleportStartCount: 0,
ActiveTeleportCount: 1,
AcceptedTeleportDestinationCount: 0,
ActiveRevealCount: 1,
PendingDestinationReadinessCount: 1,
HostProjectionCount: 1,
PendingHostAcknowledgementCount: 2));
recorder.AddCheckpoint(stamp, checkpoint);
@ -191,6 +205,8 @@ public sealed class GameRuntimeContractTests
Assert.Contains("inventory-state=2:4:1:3", entry.Text);
Assert.Contains("character=5:6:7:8:0:0", entry.Text);
Assert.Contains("social=9:10:11", entry.Text);
Assert.Contains("environment-owner=True:4:1", entry.Text);
Assert.Contains("transit-owner=0:0:1:0:1:1:1:2", entry.Text);
Assert.Contains(
"actions=14:50000001:15:4:1:16:1:00000000:0:00000000:" +
"00000000:00000000:00000000:0:0:0:0:00000000:00000000:" +

View file

@ -0,0 +1,204 @@
using System.Numerics;
using AcDream.Core.Physics;
using AcDream.Runtime.World;
namespace AcDream.Runtime.Tests.World;
public sealed class RuntimeWorldTransitDirectHostTests
{
private static readonly string[] ForbiddenHostAssemblyPrefixes =
[
"AcDream.App",
"AcDream.UI.",
"Silk.NET",
"OpenAL",
"Arch",
"ImGui",
];
private const uint OutdoorCell = 0x11340021u;
private const uint IndoorCell = 0x8A020164u;
[Fact]
public void DirectHost_CompletesLoginPortalCancellationAndReset()
{
string[] loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies()
.Select(static assembly =>
assembly.GetName().Name ?? string.Empty)
.ToArray();
Assert.DoesNotContain(
loadedAssemblies,
name => ForbiddenHostAssemblyPrefixes.Any(
prefix => name.StartsWith(
prefix,
StringComparison.OrdinalIgnoreCase)));
var state = new RuntimeWorldTransitState();
var host = new DirectHost(state);
host.CompleteLogin(OutdoorCell);
Assert.True(state.Snapshot.Completed);
Assert.True(state.Snapshot.WorldViewportObserved);
Assert.Equal(0, state.Ownership.HostProjectionCount);
state.ResetSession();
host.CompletePortal(IndoorCell, sequence: 1);
Assert.True(state.Snapshot.Completed);
Assert.True(state.Snapshot.Materialized);
Assert.Equal(1, state.Snapshot.PortalMaterializationCount);
Assert.Equal(0, state.Ownership.PendingHostAcknowledgementCount);
state.EndTeleport();
state.ResetSession();
host.CancelLogin(OutdoorCell);
Assert.True(state.Snapshot.Cancelled);
Assert.Equal(0, state.Ownership.HostProjectionCount);
state.ResetSession();
Assert.Equal(RuntimePortalSnapshot.Idle, state.Snapshot);
Assert.True(state.Ownership.IsSessionIdle);
}
private sealed class DirectHost(RuntimeWorldTransitState state)
{
public void CompleteLogin(uint cell)
{
long generation = state.BeginLoginReveal(cell);
RuntimeWorldHostProjectionToken host =
Register(generation, cell);
Acknowledge(
host,
RuntimeWorldHostAcknowledgementStage.ProjectionRegistered);
Assert.True(state.AcknowledgeDestinationReadiness(
Ready(generation, cell)));
Assert.True(state.Complete(generation));
DrainPending(host);
Assert.True(state.AcknowledgeWorldViewportVisible(generation));
}
public void CompletePortal(uint cell, ushort sequence)
{
Assert.True(state.TryQueueTeleportStart(sequence));
Assert.True(state.ActivateQueuedTeleport());
Assert.True(state.OfferTeleportDestination(
Destination(cell, sequence),
teleportTimestampAdvanced: true));
Assert.True(state.TryBeginPortalReveal(
sequence,
cell,
out long generation));
RuntimeWorldHostProjectionToken host =
Register(generation, cell);
Acknowledge(
host,
RuntimeWorldHostAcknowledgementStage.ProjectionRegistered);
Assert.True(state.AcknowledgeDestinationReadiness(
Ready(generation, cell)));
Assert.True(state.AcknowledgePortalMaterialized(
generation,
sequence,
cell));
DrainPending(host);
Assert.True(state.RequireDestinationReservationRelease(host));
DrainPending(host);
Assert.True(state.AcknowledgeWorldViewportVisible(generation));
Assert.True(state.Complete(generation));
DrainPending(host);
}
public void CancelLogin(uint cell)
{
long generation = state.BeginLoginReveal(cell);
RuntimeWorldHostProjectionToken host =
Register(generation, cell);
Acknowledge(
host,
RuntimeWorldHostAcknowledgementStage.ProjectionRegistered);
Assert.True(state.Cancel(generation));
DrainPending(host);
}
private RuntimeWorldHostProjectionToken Register(
long generation,
uint cell)
{
Assert.True(state.TryRegisterHostProjection(
generation,
cell,
out RuntimeWorldHostProjectionToken host));
return host;
}
private void DrainPending(RuntimeWorldHostProjectionToken host)
{
while (state.TryGetHostProjection(
host,
out RuntimeWorldHostProjectionSnapshot projection))
{
RuntimeWorldHostAcknowledgementStage pending =
projection.PendingAcknowledgements;
if ((pending & RuntimeWorldHostAcknowledgementStage
.SimulationReleaseProjected) != 0)
{
Acknowledge(
host,
RuntimeWorldHostAcknowledgementStage
.SimulationReleaseProjected);
continue;
}
if ((pending & RuntimeWorldHostAcknowledgementStage
.DestinationReservationReleased) != 0)
{
Acknowledge(
host,
RuntimeWorldHostAcknowledgementStage
.DestinationReservationReleased);
continue;
}
if ((pending & RuntimeWorldHostAcknowledgementStage
.TerminalProjected) != 0)
{
Acknowledge(
host,
RuntimeWorldHostAcknowledgementStage
.TerminalProjected);
continue;
}
break;
}
}
private void Acknowledge(
RuntimeWorldHostProjectionToken host,
RuntimeWorldHostAcknowledgementStage stage) =>
Assert.True(state.AcknowledgeHostProjection(
new RuntimeWorldHostAcknowledgement(host, stage)));
}
private static RuntimeDestinationReadiness Ready(
long generation,
uint cell) =>
new(
generation,
cell,
IsIndoor: (cell & 0xFFFFu) >= 0x0100u,
IsUnhydratable: false,
RequiredRenderRadius: (cell & 0xFFFFu) >= 0x0100u ? 0 : 1,
IsRenderNeighborhoodReady: true,
AreCompositeTexturesReady: true,
IsCollisionReady: true);
private static RuntimeTeleportDestination Destination(
uint cell,
ushort sequence) =>
new(
EntityGuid: 0x50000001u,
InstanceSequence: 1,
PositionSequence: 1,
TeleportSequence: sequence,
ForcePositionSequence: 1,
Position: new Position(
cell,
Vector3.Zero,
Quaternion.Identity));
}

View file

@ -338,6 +338,18 @@ public sealed class RuntimeWorldTransitStateTests
BeginPortal(first, OutdoorCell);
long secondGeneration =
second.BeginLoginReveal(OtherCell);
RuntimeWorldHostProjectionToken firstHost =
RegisterHost(first, firstGeneration, OutdoorCell);
RuntimeWorldHostProjectionToken secondHost =
RegisterHost(second, secondGeneration, OtherCell);
Assert.True(Acknowledge(
first,
firstHost,
RuntimeWorldHostAcknowledgementStage.ProjectionRegistered));
Assert.True(Acknowledge(
second,
secondHost,
RuntimeWorldHostAcknowledgementStage.ProjectionRegistered));
first.AcknowledgeDestinationReadiness(
Ready(firstGeneration, OutdoorCell));
first.AcknowledgePortalMaterialized(
@ -351,6 +363,13 @@ public sealed class RuntimeWorldTransitStateTests
Assert.False(second.IsWorldSimulationAvailable);
Assert.Equal(OtherCell, second.Snapshot.DestinationCell);
Assert.Equal(1, secondGeneration);
Assert.False(Acknowledge(
second,
firstHost,
RuntimeWorldHostAcknowledgementStage
.SimulationReleaseProjected));
Assert.Equal(1, first.Ownership.PendingHostAcknowledgementCount);
Assert.Equal(0, second.Ownership.PendingHostAcknowledgementCount);
}
[Fact]
@ -616,6 +635,195 @@ public sealed class RuntimeWorldTransitStateTests
Assert.True(state.TryQueueTeleportStart(8));
}
[Fact]
public void HostProjection_TracksExactGenerationScopedAcknowledgementSuffix()
{
var state = new RuntimeWorldTransitState();
long generation = BeginPortal(state, OutdoorCell);
RuntimeWorldHostProjectionToken host =
RegisterHost(state, generation, OutdoorCell);
Assert.Equal(
new RuntimeWorldTransitOwnershipSnapshot(
BufferedTeleportDestinationCount: 0,
PendingTeleportStartCount: 0,
ActiveTeleportCount: 1,
AcceptedTeleportDestinationCount: 0,
ActiveRevealCount: 1,
PendingDestinationReadinessCount: 1,
HostProjectionCount: 1,
PendingHostAcknowledgementCount: 1),
state.Ownership);
Assert.False(Acknowledge(
state,
host with { Generation = generation + 1 },
RuntimeWorldHostAcknowledgementStage.ProjectionRegistered));
Assert.False(Acknowledge(
state,
host with { DestinationCell = OtherCell },
RuntimeWorldHostAcknowledgementStage.ProjectionRegistered));
Assert.False(Acknowledge(
state,
host,
RuntimeWorldHostAcknowledgementStage.None));
Assert.False(Acknowledge(
state,
host,
RuntimeWorldHostAcknowledgementStage.ProjectionRegistered
| RuntimeWorldHostAcknowledgementStage
.DestinationReservationReleased));
Assert.True(Acknowledge(
state,
host,
RuntimeWorldHostAcknowledgementStage.ProjectionRegistered));
Assert.True(state.AcknowledgeDestinationReadiness(
Ready(generation, OutdoorCell)));
Assert.True(state.AcknowledgePortalMaterialized(
generation,
1,
OutdoorCell));
Assert.Equal(1, state.Ownership.PendingHostAcknowledgementCount);
Assert.True(Acknowledge(
state,
host,
RuntimeWorldHostAcknowledgementStage
.SimulationReleaseProjected));
Assert.True(state.RequireDestinationReservationRelease(host));
Assert.True(Acknowledge(
state,
host,
RuntimeWorldHostAcknowledgementStage
.DestinationReservationReleased));
Assert.True(state.AcknowledgeWorldViewportVisible(generation));
Assert.True(state.Complete(generation));
Assert.Equal(1, state.Ownership.PendingHostAcknowledgementCount);
Assert.True(Acknowledge(
state,
host,
RuntimeWorldHostAcknowledgementStage.TerminalProjected));
Assert.Equal(0, state.Ownership.HostProjectionCount);
Assert.Equal(0, state.Ownership.PendingHostAcknowledgementCount);
state.EndTeleport();
state.ResetSession();
Assert.True(state.Ownership.IsSessionIdle);
}
[Fact]
public void Cancel_ReplacesIncompleteRegistrationWithRetryableWithdrawal()
{
var state = new RuntimeWorldTransitState();
long generation = state.BeginLoginReveal(OutdoorCell);
RuntimeWorldHostProjectionToken host =
RegisterHost(state, generation, OutdoorCell);
Assert.True(state.Cancel(generation));
Assert.True(state.TryGetHostProjection(
host,
out RuntimeWorldHostProjectionSnapshot projection));
Assert.Equal(
RuntimeWorldHostAcknowledgementStage
.SimulationReleaseProjected
| RuntimeWorldHostAcknowledgementStage
.DestinationReservationReleased
| RuntimeWorldHostAcknowledgementStage.TerminalProjected,
projection.PendingAcknowledgements);
Assert.False(Acknowledge(
state,
host,
RuntimeWorldHostAcknowledgementStage.TerminalProjected));
Assert.True(Acknowledge(
state,
host,
RuntimeWorldHostAcknowledgementStage
.SimulationReleaseProjected));
Assert.True(Acknowledge(
state,
host,
RuntimeWorldHostAcknowledgementStage
.DestinationReservationReleased));
Assert.True(Acknowledge(
state,
host,
RuntimeWorldHostAcknowledgementStage.TerminalProjected));
state.ResetSession();
Assert.True(state.Ownership.IsSessionIdle);
}
[Fact]
public void Supersession_PreservesOldHostWithdrawalByExactGeneration()
{
var state = new RuntimeWorldTransitState();
long first = state.BeginLoginReveal(OutdoorCell);
RuntimeWorldHostProjectionToken firstHost =
RegisterHost(state, first, OutdoorCell);
Assert.True(Acknowledge(
state,
firstHost,
RuntimeWorldHostAcknowledgementStage.ProjectionRegistered));
long second = BeginPortal(state, OtherCell, sequence: 2);
Assert.True(state.TryGetHostProjection(
firstHost,
out RuntimeWorldHostProjectionSnapshot superseded));
Assert.True(superseded.IsSuperseding);
Assert.Equal(
RuntimeWorldHostAcknowledgementStage
.DestinationReservationReleased
| RuntimeWorldHostAcknowledgementStage.TerminalProjected,
superseded.PendingAcknowledgements);
Assert.False(Acknowledge(
state,
firstHost,
RuntimeWorldHostAcknowledgementStage.TerminalProjected));
Assert.True(Acknowledge(
state,
firstHost,
RuntimeWorldHostAcknowledgementStage
.DestinationReservationReleased));
Assert.True(Acknowledge(
state,
firstHost,
RuntimeWorldHostAcknowledgementStage.TerminalProjected));
Assert.Equal(second, state.Snapshot.Generation);
Assert.Equal(0, state.Ownership.HostProjectionCount);
}
[Fact]
public void ResetSession_RejectsUnacknowledgedHostProjection()
{
var state = new RuntimeWorldTransitState();
long generation = state.BeginLoginReveal(OutdoorCell);
RuntimeWorldHostProjectionToken host =
RegisterHost(state, generation, OutdoorCell);
InvalidOperationException error =
Assert.Throws<InvalidOperationException>(state.ResetSession);
Assert.Contains("hosts=1", error.Message);
Assert.Equal(generation, state.Snapshot.Generation);
Assert.True(state.Cancel(generation));
Assert.True(Acknowledge(
state,
host,
RuntimeWorldHostAcknowledgementStage
.SimulationReleaseProjected));
Assert.True(Acknowledge(
state,
host,
RuntimeWorldHostAcknowledgementStage
.DestinationReservationReleased));
Assert.True(Acknowledge(
state,
host,
RuntimeWorldHostAcknowledgementStage.TerminalProjected));
state.ResetSession();
}
private static long BeginPortal(
RuntimeWorldTransitState state,
uint cell,
@ -633,6 +841,25 @@ public sealed class RuntimeWorldTransitStateTests
return generation;
}
private static RuntimeWorldHostProjectionToken RegisterHost(
RuntimeWorldTransitState state,
long generation,
uint cell)
{
Assert.True(state.TryRegisterHostProjection(
generation,
cell,
out RuntimeWorldHostProjectionToken host));
return host;
}
private static bool Acknowledge(
RuntimeWorldTransitState state,
RuntimeWorldHostProjectionToken host,
RuntimeWorldHostAcknowledgementStage stage) =>
state.AcknowledgeHostProjection(
new RuntimeWorldHostAcknowledgement(host, stage));
private static RuntimeTeleportDestination Destination(
uint cell,
ushort sequence,

View file

@ -140,6 +140,8 @@ function Read-Checkpoints([string]$Path) {
function Validate-Checkpoint([string]$SessionLabel, [object]$Checkpoint) {
$name = $Checkpoint.name
$reveal = $Checkpoint.reveal
$environmentOwnership = $Checkpoint.environmentOwnership
$transitOwnership = $Checkpoint.transitOwnership
$resources = $Checkpoint.resources
if (-not $reveal.readiness.isReady) {
$failures.Add("${SessionLabel}/${name}: reveal was not ready")
@ -161,6 +163,31 @@ function Validate-Checkpoint([string]$SessionLabel, [object]$Checkpoint) {
$failures.Add("${SessionLabel}/${name}: collision was not ready")
}
}
if (-not $environmentOwnership.isInitialized) {
$failures.Add("${SessionLabel}/${name}: Runtime world environment is not initialized")
}
if (($environmentOwnership.dayGroupDefinitionCount -le 0) -or
($environmentOwnership.activeDayGroupCount -ne 1)) {
$failures.Add((
"${SessionLabel}/${name}: Runtime environment ownership is {0}/{1}, expected definitions with one active group" -f
$environmentOwnership.dayGroupDefinitionCount,
$environmentOwnership.activeDayGroupCount))
}
foreach ($field in @(
'bufferedTeleportDestinationCount',
'pendingTeleportStartCount',
'activeTeleportCount',
'acceptedTeleportDestinationCount',
'activeRevealCount',
'pendingDestinationReadinessCount',
'hostProjectionCount',
'pendingHostAcknowledgementCount')) {
if ([int]$transitOwnership.$field -ne 0) {
$failures.Add((
"${SessionLabel}/${name}: transitOwnership.$field={0}, expected zero at a stable checkpoint" -f
$transitOwnership.$field))
}
}
if ($resources.pendingLiveTeardowns -ne 0) {
$failures.Add("${SessionLabel}/${name}: $($resources.pendingLiveTeardowns) live teardown(s) pending")
}