acdream/tests/AcDream.App.Tests/Streaming/WorldRevealCoordinatorTests.cs

766 lines
26 KiB
C#

using System.Numerics;
using AcDream.App.Streaming;
using AcDream.App.World;
using AcDream.Core.Physics;
using AcDream.Core.Selection;
using AcDream.Runtime;
using AcDream.Runtime.World;
namespace AcDream.App.Tests.Streaming;
public sealed class WorldRevealCoordinatorTests
{
private sealed class State
{
public RuntimeWorldTransitState Transit { get; private set; } = null!;
public bool RenderReady { get; set; }
public bool CollisionReady { get; set; }
public bool CompositesReady { get; set; }
public int InvalidateCount { get; private set; }
public int PrepareCount { get; private set; }
public WorldRevealCoordinator Build(
List<string>? logs = null,
IWorldRevealStreamingScheduler? streaming = null,
IWorldRevealRenderResourceScheduler? renderResources = null,
RuntimeWorldTransitState? transit = null)
{
transit ??= new RuntimeWorldTransitState(
logs is null ? null : new Action<string>(logs.Add));
Transit = transit;
return new WorldRevealCoordinator(
transit,
isRenderNeighborhoodReady: (_, _) => RenderReady,
isSpawnCellReady: _ => CollisionReady,
isTerrainNeighborhoodReady: (_, _) => CollisionReady,
areCompositeTexturesReady: () => CompositesReady,
prepareCompositeTextures: (_, _) => PrepareCount++,
invalidateCompositeTextures: () =>
{
InvalidateCount++;
CompositesReady = false;
},
isSpawnClaimUnhydratable: _ => false,
streaming: streaming,
renderResources: renderResources);
}
}
[Fact]
public void Begin_InvalidatesPriorDestinationAndStartsOneGeneration()
{
var state = new State { CompositesReady = true };
WorldRevealCoordinator coordinator = state.Build();
long generation = coordinator.BeginLogin(0x11340021u);
Assert.Equal(1, generation);
Assert.Equal(1, state.InvalidateCount);
Assert.False(state.CompositesReady);
Assert.Equal(RuntimePortalKind.Login, coordinator.Snapshot.Kind);
}
[Fact]
public void PrepareAndEvaluate_UsesOneCanonicalSnapshotForLifecycle()
{
const uint outdoorCell = 0x11340021u;
var state = new State
{
RenderReady = true,
CollisionReady = true,
};
WorldRevealCoordinator coordinator = state.Build();
coordinator.BeginLogin(outdoorCell);
WorldRevealReadinessSnapshot first = coordinator.PrepareAndEvaluate(outdoorCell);
state.CompositesReady = true;
WorldRevealReadinessSnapshot ready = coordinator.PrepareAndEvaluate(outdoorCell);
Assert.False(first.IsReady);
Assert.True(ready.IsReady);
Assert.Equal(2, state.PrepareCount);
Assert.Equal(ready.DestinationCell, coordinator.Snapshot.DestinationCell);
Assert.Equal(ready.IsReady, coordinator.Snapshot.IsReady);
}
[Fact]
public void PortalLifecycle_CountsMaterializationAndCompletesVisibleGeneration()
{
const uint outdoorCell = 0x3032001Cu;
var logs = new List<string>();
var state = new State
{
RenderReady = true,
CollisionReady = true,
CompositesReady = true,
};
WorldRevealCoordinator coordinator = state.Build(logs);
long generation = BeginPortal(
coordinator,
state.Transit,
outdoorCell,
sequence: 1);
state.CompositesReady = true;
Assert.True(coordinator.Evaluate(outdoorCell).IsReady);
coordinator.ObserveMaterialized(generation, 1, outdoorCell);
coordinator.ObserveMaterialized(generation, 1, outdoorCell);
coordinator.ObserveWorldViewportVisible();
coordinator.Complete();
Assert.Equal(1, coordinator.PortalMaterializationCount);
Assert.True(coordinator.Snapshot.Materialized);
Assert.True(coordinator.Snapshot.WorldViewportObserved);
Assert.True(coordinator.Snapshot.Completed);
Assert.Equal(0, coordinator.Snapshot.InvariantFailureCount);
Assert.Single(logs, line => line.Contains("event=materialized", StringComparison.Ordinal));
}
[Fact]
public void PortalBegin_WrongSequenceOrCellCannotAcquireHostResources()
{
const uint destinationCell = 0x3032001Cu;
var state = new State { CompositesReady = true };
var scheduler = new RecordingDestinationScheduler();
WorldRevealCoordinator coordinator = state.Build(streaming: scheduler);
RuntimeWorldTransitTestDriver.AcceptPortalDestination(
state.Transit,
destinationCell,
sequence: 7);
Assert.False(coordinator.TryBeginPortal(
8,
destinationCell,
out _));
Assert.False(coordinator.TryBeginPortal(
7,
0x11340021u,
out _));
Assert.Empty(scheduler.Begins);
Assert.Equal(0, state.InvalidateCount);
Assert.Equal(0, coordinator.Snapshot.Generation);
Assert.True(coordinator.TryBeginPortal(
7,
destinationCell,
out long generation));
Assert.Equal(
(generation, destinationCell, 1),
Assert.Single(scheduler.Begins));
Assert.Equal(1, state.InvalidateCount);
}
[Fact]
public void Cancel_PreventsLateDestinationWorkFromMutatingLifecycle()
{
var state = new State
{
RenderReady = true,
CollisionReady = true,
CompositesReady = true,
};
WorldRevealCoordinator coordinator = state.Build();
long generation = BeginPortal(
coordinator,
state.Transit,
0x11340021u,
sequence: 1);
coordinator.Cancel();
coordinator.PrepareAndEvaluate(0x11340021u);
coordinator.ObserveMaterialized(
generation,
1,
0x11340021u);
coordinator.ObserveWorldViewportVisible();
coordinator.Complete();
Assert.True(coordinator.Snapshot.Cancelled);
Assert.Equal(0x11340021u, coordinator.Snapshot.DestinationCell);
Assert.False(coordinator.Snapshot.IsReady);
Assert.Equal(0, coordinator.PortalMaterializationCount);
}
[Fact]
public void RevealLifetime_QuiescesContinuouslyAndOnlyActiveGenerationReopens()
{
var transit = new RuntimeWorldTransitState();
var availability = new WorldGenerationAvailabilityState(transit);
var world = new GpuWorldState(availability: availability);
var audio = new RecordingAudioQuiescence();
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);
Assert.Equal(1, coordinator.BeginLogin(0x11340021u));
long generation = BeginPortal(
coordinator,
transit,
0x11340021u,
sequence: 1);
Assert.Equal(2, generation);
Assert.False(availability.IsWorldAvailable);
Assert.Equal(2, availability.QuiescedGeneration);
Assert.Equal(1, audio.SuspendCalls);
Assert.True(coordinator.Evaluate(0x11340021u).IsReady);
coordinator.ObserveMaterialized(
generation,
1,
0x11340021u);
coordinator.Complete();
Assert.True(availability.IsWorldAvailable);
Assert.Equal(1, audio.ResumeCalls);
}
[Fact]
public void Materialization_ReopensWorldBeforePortalViewportRelease()
{
var transit = new RuntimeWorldTransitState();
var availability = new WorldGenerationAvailabilityState(transit);
var world = new GpuWorldState(availability: availability);
var audio = new RecordingAudioQuiescence();
var quiescence = new WorldGenerationQuiescence(
new SelectionState(),
world,
_ => null,
audio);
var scheduler = new RecordingDestinationScheduler();
var coordinator = new WorldRevealCoordinator(
transit,
isRenderNeighborhoodReady: (_, _) => true,
isSpawnCellReady: _ => true,
isTerrainNeighborhoodReady: (_, _) => true,
areCompositeTexturesReady: () => true,
prepareCompositeTextures: (_, _) => { },
invalidateCompositeTextures: () => { },
isSpawnClaimUnhydratable: _ => false,
quiescence: quiescence,
streaming: scheduler);
const uint destinationCell = 0x3032001Cu;
const ushort teleportSequence = 1;
long generation = BeginPortal(
coordinator,
transit,
destinationCell,
teleportSequence);
Assert.False(availability.IsWorldAvailable);
Assert.True(coordinator.Evaluate(destinationCell).IsReady);
coordinator.ObserveMaterialized(
generation,
teleportSequence,
destinationCell);
Assert.True(availability.IsWorldAvailable);
Assert.False(coordinator.Snapshot.WorldViewportObserved);
Assert.False(coordinator.Snapshot.Completed);
Assert.Equal(1, audio.ResumeCalls);
Assert.Empty(scheduler.Ends);
coordinator.RevealWorldViewport();
coordinator.RevealWorldViewport();
Assert.True(availability.IsWorldAvailable);
Assert.False(coordinator.Snapshot.Completed);
Assert.Equal(1, audio.ResumeCalls);
Assert.Equal([generation], scheduler.Ends);
coordinator.Complete();
Assert.True(coordinator.Snapshot.Completed);
Assert.Equal(1, audio.ResumeCalls);
Assert.Equal([generation], scheduler.Ends);
}
[Fact]
public void TunnelContinue_TicksDestinationObjectsBehindPortalViewport()
{
var transit = new RuntimeWorldTransitState();
var availability = new WorldGenerationAvailabilityState(transit);
var world = new GpuWorldState(availability: availability);
var quiescence = new WorldGenerationQuiescence(
new SelectionState(),
world,
_ => null,
audio: null);
var scheduler = new RecordingDestinationScheduler();
var coordinator = new WorldRevealCoordinator(
transit,
isRenderNeighborhoodReady: (_, _) => true,
isSpawnCellReady: _ => true,
isTerrainNeighborhoodReady: (_, _) => true,
areCompositeTexturesReady: () => true,
prepareCompositeTextures: (_, _) => { },
invalidateCompositeTextures: () => { },
isSpawnClaimUnhydratable: _ => false,
quiescence: quiescence,
streaming: scheduler);
var calls = new List<string>();
var frame = new RetailLiveFrameCoordinator(
new global::AcDream.App.Tests.TestLiveObjectFramePhase(
_ => calls.Add("objects")),
world,
new global::AcDream.App.Tests.TestLiveSessionFramePhase(
() => calls.Add("network")),
new global::AcDream.App.Tests.TestPostNetworkCommandFramePhase(
() => calls.Add("commands")),
new global::AcDream.App.Tests.TestLiveSpatialReconcilePhase(
() => calls.Add("spatial")),
availability);
const uint destinationCell = 0x3032001Cu;
long generation = BeginPortal(
coordinator,
transit,
destinationCell,
sequence: 1);
frame.Tick(1f / 60f);
Assert.Equal(["network", "commands"], calls);
calls.Clear();
Assert.True(coordinator.Evaluate(destinationCell).IsReady);
coordinator.ObserveMaterialized(
generation,
1,
destinationCell);
frame.Tick(1f / 60f);
Assert.Equal(["objects", "network", "commands", "spatial"], calls);
Assert.Empty(scheduler.Ends);
Assert.False(coordinator.Snapshot.WorldViewportObserved);
}
[Fact]
public void SessionReset_ReleasesHostEdgesAndClearsRuntimeSnapshot()
{
var transit = new RuntimeWorldTransitState();
var availability = new WorldGenerationAvailabilityState(transit);
var world = new GpuWorldState(availability: availability);
var audio = new RecordingAudioQuiescence();
var quiescence = new WorldGenerationQuiescence(
new SelectionState(),
world,
_ => null,
audio);
var scheduler = new RecordingDestinationScheduler();
var coordinator = new WorldRevealCoordinator(
transit,
isRenderNeighborhoodReady: (_, _) => false,
isSpawnCellReady: _ => false,
isTerrainNeighborhoodReady: (_, _) => false,
areCompositeTexturesReady: () => false,
prepareCompositeTextures: (_, _) => { },
invalidateCompositeTextures: () => { },
isSpawnClaimUnhydratable: _ => false,
quiescence: quiescence,
streaming: scheduler);
long generation = BeginPortal(
coordinator,
transit,
0x3032001Cu,
sequence: 1);
coordinator.ResetSession();
Assert.Equal(RuntimePortalSnapshot.Idle, coordinator.Snapshot);
Assert.True(availability.IsWorldAvailable);
Assert.Equal(1, audio.SuspendCalls);
Assert.Equal(1, audio.ResumeCalls);
Assert.Equal([generation], scheduler.Ends);
}
[Fact]
public void RevealGeneration_OwnsExactDestinationReservationUntilCompletion()
{
var state = new State();
var scheduler = new RecordingDestinationScheduler();
WorldRevealCoordinator coordinator = state.Build(streaming: scheduler);
long first = BeginPortal(
coordinator,
state.Transit,
0x11340021u,
sequence: 1);
state.Transit.EndTeleport();
long second = BeginPortal(
coordinator,
state.Transit,
0x20210123u,
sequence: 2);
Assert.Equal(
[
(first, 0x11340021u, 1),
(second, 0x20210123u, 0),
],
scheduler.Begins);
Assert.Equal([first], scheduler.Ends);
coordinator.Cancel();
Assert.Equal([first, second], scheduler.Ends);
}
[Fact]
public void RevealGeneration_OwnsRenderUploadProfileUntilViewportRelease()
{
var state = new State();
var resources = new RecordingRenderResourceScheduler();
WorldRevealCoordinator coordinator =
state.Build(renderResources: resources);
long first = BeginPortal(
coordinator,
state.Transit,
0x11340021u,
sequence: 1);
state.Transit.EndTeleport();
long second = BeginPortal(
coordinator,
state.Transit,
0x20210123u,
sequence: 2);
Assert.Equal([first, second], resources.Begins);
Assert.Equal([first], resources.Ends);
coordinator.RevealWorldViewport();
coordinator.Complete();
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(
WorldRevealCoordinator coordinator,
RuntimeWorldTransitState transit,
uint destinationCell,
ushort sequence)
{
Assert.True(transit.TryQueueTeleportStart(sequence));
Assert.True(transit.ActivateQueuedTeleport());
Assert.True(transit.OfferTeleportDestination(
new RuntimeTeleportDestination(
EntityGuid: 0x50000001u,
InstanceSequence: 1,
PositionSequence: 1,
TeleportSequence: sequence,
ForcePositionSequence: 1,
Position: new Position(
destinationCell,
Vector3.Zero,
Quaternion.Identity)),
teleportTimestampAdvanced: true));
Assert.True(coordinator.TryBeginPortal(
sequence,
destinationCell,
out long generation));
return generation;
}
private sealed class RecordingAudioQuiescence : AcDream.App.Audio.IWorldAudioQuiescence
{
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()
{
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
: IWorldRevealStreamingScheduler
{
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)
{
BeginAttempts++;
if (ThrowBeginCount > 0)
{
ThrowBeginCount--;
throw new InvalidOperationException("stream begin failed");
}
Begins.Add(
(revealGeneration, destinationCell, requiredRenderRadius));
OnBegin?.Invoke();
}
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
: IWorldRevealRenderResourceScheduler
{
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)
{
BeginAttempts++;
if (ThrowBeginCount > 0)
{
ThrowBeginCount--;
throw new InvalidOperationException("render begin failed");
}
Begins.Add(revealGeneration);
}
public void EndDestinationReveal(long revealGeneration)
{
EndAttempts++;
if (ThrowEndCount > 0)
{
ThrowEndCount--;
throw new InvalidOperationException("render end failed");
}
Ends.Add(revealGeneration);
}
}
}