perf(streaming): quiesce retired generations and budget teardown
Publish the retail blocking-for-cells edge before deferred recenter work, freeze old-world presentation/simulation/audio, and advance full-window retirement from exact metered entity and owner cursors. This removes synchronous portal teardown without allowing retained owners to remain observable.
This commit is contained in:
parent
b8f6317fe1
commit
bb16f74fd4
38 changed files with 1691 additions and 170 deletions
|
|
@ -152,6 +152,13 @@ public sealed class LiveSessionResetPlanTests
|
|||
Assert.Equal(1, identityResets);
|
||||
|
||||
failRetirement = false;
|
||||
for (int frame = 0;
|
||||
frame < 64
|
||||
&& !controller.IsOriginRecenterRetirementComplete();
|
||||
frame++)
|
||||
{
|
||||
controller.Tick(0x53, 0x53);
|
||||
}
|
||||
plan.Execute();
|
||||
|
||||
Assert.Equal(2, identityResets);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using System.Reflection;
|
|||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Selection;
|
||||
using AcDream.App.Rendering.Vfx;
|
||||
using AcDream.App.Streaming;
|
||||
using AcDream.Core.Rendering;
|
||||
using AcDream.Core.World;
|
||||
|
||||
|
|
@ -21,6 +22,23 @@ public sealed class WorldSceneRendererTests
|
|||
Assert.Equal(["selection:begin", "selection:complete"], rig.Calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void QuiescedGeneration_PublishesEmptySelectionFrameAndSkipsEveryWorldOwner()
|
||||
{
|
||||
var availability = new WorldGenerationAvailabilityState();
|
||||
availability.Begin(4);
|
||||
var rig = new Rig(
|
||||
portalVisible: false,
|
||||
waitingForLogin: false,
|
||||
clipRoot: null,
|
||||
availability: availability);
|
||||
|
||||
WorldRenderFrameOutcome result = rig.Renderer.Render(default);
|
||||
|
||||
Assert.Equal(default, result);
|
||||
Assert.Equal(["selection:begin", "selection:complete"], rig.Calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoginWait_DrawsFlatSkyThenCompletesFrameWithoutWorldGeometry()
|
||||
{
|
||||
|
|
@ -332,7 +350,8 @@ public sealed class WorldSceneRendererTests
|
|||
bool waitingForLogin,
|
||||
LoadedCell? clipRoot,
|
||||
bool? playerSeenOutside = null,
|
||||
EnvironOverride environOverride = EnvironOverride.None)
|
||||
EnvironOverride environOverride = EnvironOverride.None,
|
||||
IWorldGenerationAvailability? availability = null)
|
||||
{
|
||||
Calls = [];
|
||||
DayGroup = new DayGroupData { Name = "sentinel-day-group" };
|
||||
|
|
@ -385,7 +404,8 @@ public sealed class WorldSceneRendererTests
|
|||
new PViewCells(),
|
||||
Passes,
|
||||
new WorldRenderRangeState(4, 12),
|
||||
diagnostics);
|
||||
diagnostics,
|
||||
availability);
|
||||
}
|
||||
|
||||
public List<string> Calls { get; }
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@ public sealed class LandblockPresentationPipelineTests
|
|||
var recenter = new StreamingOriginRecenterCoordinator(controller, origin);
|
||||
|
||||
Assert.False(recenter.Begin(0x22, 0x23, isSealedDungeon: false));
|
||||
controller.Tick(0x19, 0x19);
|
||||
|
||||
Assert.Equal((0x19, 0x19), (origin.CenterX, origin.CenterY));
|
||||
Assert.False(state.IsLoaded(centerId));
|
||||
|
|
@ -144,12 +145,11 @@ public sealed class LandblockPresentationPipelineTests
|
|||
Assert.Equal(1, controller.PendingRetirementCount);
|
||||
Assert.Equal(2, controller.LastFullWindowRetirementLandblockCount);
|
||||
|
||||
controller.Tick(0x19, 0x19);
|
||||
Assert.Empty(enqueued);
|
||||
Assert.True(neighborAttempts >= 2);
|
||||
Assert.Equal(1, neighborAttempts);
|
||||
|
||||
failNeighbor = false;
|
||||
Assert.True(recenter.Advance());
|
||||
Assert.True(Converge(recenter, controller, 0x19, 0x19));
|
||||
|
||||
Assert.Equal((0x22, 0x23), (origin.CenterX, origin.CenterY));
|
||||
Assert.Equal(0, controller.PendingRetirementCount);
|
||||
|
|
@ -187,12 +187,13 @@ public sealed class LandblockPresentationPipelineTests
|
|||
var recenter = new StreamingOriginRecenterCoordinator(controller, origin);
|
||||
|
||||
Assert.False(recenter.Begin(0x40, 0x41, isSealedDungeon: false));
|
||||
controller.Tick(0x30, 0x30);
|
||||
Assert.Equal((0x30, 0x30), (origin.CenterX, origin.CenterY));
|
||||
Assert.True(state.IsLoaded(landblockId));
|
||||
Assert.Equal(0, controller.PendingRetirementCount);
|
||||
|
||||
failClear = false;
|
||||
Assert.True(recenter.Advance());
|
||||
Assert.True(Converge(recenter, controller, 0x30, 0x30));
|
||||
|
||||
Assert.Equal(2, clearAttempts);
|
||||
Assert.False(state.IsLoaded(landblockId));
|
||||
|
|
@ -234,13 +235,15 @@ public sealed class LandblockPresentationPipelineTests
|
|||
var recenter = new StreamingOriginRecenterCoordinator(controller, origin);
|
||||
|
||||
Assert.False(recenter.Begin(0x43, 0x44, isSealedDungeon: false));
|
||||
for (int frame = 0; frame < 16 && observerCalls == 0; frame++)
|
||||
controller.Tick(0x42, 0x42);
|
||||
|
||||
Assert.Equal((0x42, 0x42), (origin.CenterX, origin.CenterY));
|
||||
Assert.False(state.IsLoaded(firstId));
|
||||
Assert.False(state.IsLoaded(secondId));
|
||||
Assert.Equal(1, observerCalls);
|
||||
|
||||
Assert.True(recenter.Advance());
|
||||
Assert.True(Converge(recenter, controller, 0x42, 0x42));
|
||||
Assert.Equal((0x43, 0x44), (origin.CenterX, origin.CenterY));
|
||||
Assert.Equal([firstId, secondId], removed);
|
||||
}
|
||||
|
|
@ -281,13 +284,14 @@ public sealed class LandblockPresentationPipelineTests
|
|||
var recenter = new StreamingOriginRecenterCoordinator(controller, origin);
|
||||
|
||||
Assert.False(recenter.Begin(0x46, 0x46, isSealedDungeon: false));
|
||||
controller.Tick(0x45, 0x45);
|
||||
|
||||
Assert.Same(player, Assert.Single(state.DrainRescued()));
|
||||
Assert.Equal(1, state.PendingLiveEntityCount);
|
||||
Assert.Empty(state.Entities);
|
||||
|
||||
failRetirement = false;
|
||||
Assert.True(recenter.Advance());
|
||||
Assert.True(Converge(recenter, controller, 0x45, 0x45));
|
||||
Assert.Empty(state.DrainRescued());
|
||||
state.AddLandblock(new LoadedLandblock(
|
||||
destinationId,
|
||||
|
|
@ -344,7 +348,7 @@ public sealed class LandblockPresentationPipelineTests
|
|||
Assert.True(recenter.IsPending);
|
||||
|
||||
failRetirement = false;
|
||||
Assert.True(recenter.Advance());
|
||||
Assert.True(Converge(recenter, controller, 0x50, 0x50));
|
||||
|
||||
Assert.False(recenter.IsPending);
|
||||
Assert.Equal((0x50, 0x50), (origin.CenterX, origin.CenterY));
|
||||
|
|
@ -384,7 +388,12 @@ public sealed class LandblockPresentationPipelineTests
|
|||
Assert.False(recenter.Begin(0x60, 0x60, isSealedDungeon: false));
|
||||
recenter.Reset();
|
||||
failRetirement = false;
|
||||
Assert.True(recenter.Advance());
|
||||
Assert.True(Converge(
|
||||
recenter,
|
||||
controller,
|
||||
0x52,
|
||||
0x52,
|
||||
insideDungeon: true));
|
||||
|
||||
Assert.Equal((0x52, 0x52), (origin.CenterX, origin.CenterY));
|
||||
Assert.Equal([(sourceId, LandblockStreamJobKind.LoadNear)], enqueued);
|
||||
|
|
@ -423,7 +432,7 @@ public sealed class LandblockPresentationPipelineTests
|
|||
Assert.False(recenter.Begin(0x54, 0x54, isSealedDungeon: true));
|
||||
recenter.Reset(sessionEnding: true);
|
||||
failRetirement = false;
|
||||
Assert.True(recenter.Advance());
|
||||
Assert.True(Converge(recenter, controller, 0x53, 0x53));
|
||||
|
||||
Assert.Empty(enqueued);
|
||||
Assert.Equal((0x53, 0x53), (origin.CenterX, origin.CenterY));
|
||||
|
|
@ -464,7 +473,7 @@ public sealed class LandblockPresentationPipelineTests
|
|||
Assert.True(origin.TryInitialize(0x61, 0x62));
|
||||
|
||||
failRetirement = false;
|
||||
Assert.True(recenter.Advance());
|
||||
Assert.True(Converge(recenter, controller, 0x61, 0x62));
|
||||
|
||||
Assert.Equal((0x61, 0x62), (origin.CenterX, origin.CenterY));
|
||||
}
|
||||
|
|
@ -494,7 +503,7 @@ public sealed class LandblockPresentationPipelineTests
|
|||
var recenter = new StreamingOriginRecenterCoordinator(controller, origin);
|
||||
|
||||
Assert.False(recenter.Begin(0x11, 0x11, isSealedDungeon: false));
|
||||
Assert.True(recenter.Advance());
|
||||
Assert.True(Converge(recenter, controller, 0x10, 0x10));
|
||||
|
||||
Assert.Equal(1, clearAttempts);
|
||||
Assert.Equal((0x11, 0x11), (origin.CenterX, origin.CenterY));
|
||||
|
|
@ -537,10 +546,11 @@ public sealed class LandblockPresentationPipelineTests
|
|||
removed.Add(id);
|
||||
if (id == firstId)
|
||||
Assert.False(controller!.IsOriginRecenterRetirementComplete());
|
||||
});
|
||||
});
|
||||
var recenter = new StreamingOriginRecenterCoordinator(controller, origin);
|
||||
|
||||
Assert.True(recenter.Begin(0x71, 0x71, isSealedDungeon: false));
|
||||
Assert.False(recenter.Begin(0x71, 0x71, isSealedDungeon: false));
|
||||
Assert.True(Converge(recenter, controller, 0x70, 0x70));
|
||||
|
||||
Assert.Equal(1, clearCalls);
|
||||
Assert.Equal([firstId, secondId], removed);
|
||||
|
|
@ -585,7 +595,7 @@ public sealed class LandblockPresentationPipelineTests
|
|||
Assert.Empty(enqueued);
|
||||
|
||||
failRetirement = false;
|
||||
Assert.True(recenter.Advance());
|
||||
Assert.True(Converge(recenter, controller, 0x72, 0x72));
|
||||
controller.Tick(0x73, 0x73);
|
||||
|
||||
Assert.Equal(1, controller.NearRadius);
|
||||
|
|
@ -630,6 +640,7 @@ public sealed class LandblockPresentationPipelineTests
|
|||
Assert.True(recenter.IsPending);
|
||||
controller.Tick(0x30, 0x30, insideDungeon: true);
|
||||
Assert.Empty(enqueued);
|
||||
Assert.False(recenter.Advance());
|
||||
|
||||
failDungeonEnqueue = false;
|
||||
Assert.True(recenter.Advance());
|
||||
|
|
@ -1066,6 +1077,8 @@ public sealed class LandblockPresentationPipelineTests
|
|||
Assert.True(state.IsNearTier(landblockId));
|
||||
|
||||
controller.ForceReloadWindow();
|
||||
for (int frame = 0; frame < 16 && state.IsLoaded(landblockId); frame++)
|
||||
controller.Tick(0x80, 0x80);
|
||||
|
||||
Assert.False(state.IsLoaded(landblockId));
|
||||
Assert.Equal(1, presentationCalls);
|
||||
|
|
@ -1276,6 +1289,25 @@ public sealed class LandblockPresentationPipelineTests
|
|||
Assert.DoesNotContain(forbidden, field, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool Converge(
|
||||
StreamingOriginRecenterCoordinator recenter,
|
||||
StreamingController controller,
|
||||
int observerX,
|
||||
int observerY,
|
||||
bool insideDungeon = false,
|
||||
int maximumFrames = 64)
|
||||
{
|
||||
for (int frame = 0; frame < maximumFrames; frame++)
|
||||
{
|
||||
if (recenter.Advance())
|
||||
return true;
|
||||
|
||||
controller.Tick(observerX, observerY, insideDungeon);
|
||||
}
|
||||
|
||||
return recenter.Advance();
|
||||
}
|
||||
|
||||
private static LandblockBuild BuildWithShell(
|
||||
uint landblockId,
|
||||
params WorldEntity[] entities)
|
||||
|
|
|
|||
|
|
@ -51,6 +51,101 @@ public sealed class LandblockRetirementCoordinatorTests
|
|||
Assert.Equal(1, calls[3]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BudgetedRetirement_AdvancesAtMostTheAdmittedAtomicEntityWork()
|
||||
{
|
||||
const uint landblockId = 0x2021FFFFu;
|
||||
WorldEntity[] entities =
|
||||
[
|
||||
Entity(1),
|
||||
Entity(2),
|
||||
Entity(3),
|
||||
Entity(4),
|
||||
];
|
||||
var state = StateWith(landblockId, entities);
|
||||
var lightingCalls = new Dictionary<uint, int>();
|
||||
LandblockRetirementCoordinator coordinator =
|
||||
LandblockRetirementCoordinator.CreateBudgeted(
|
||||
state,
|
||||
ticket => AdvancePresentationStep(
|
||||
ticket,
|
||||
entity => lightingCalls[entity.Id] =
|
||||
lightingCalls.GetValueOrDefault(entity.Id) + 1),
|
||||
ticket => CompletePresentation(
|
||||
ticket,
|
||||
lighting: entity => lightingCalls[entity.Id] =
|
||||
lightingCalls.GetValueOrDefault(entity.Id) + 1));
|
||||
coordinator.BeginFull(landblockId);
|
||||
|
||||
Assert.False(state.IsLoaded(landblockId));
|
||||
Assert.Equal(1, coordinator.PendingCount);
|
||||
|
||||
int frames = 0;
|
||||
while (coordinator.PendingCount != 0 && frames++ < 64)
|
||||
{
|
||||
var meter = new StreamingWorkMeter(Budget(maxEntityOperations: 2));
|
||||
coordinator.Advance(meter);
|
||||
meter.FinishFrame();
|
||||
Assert.InRange(meter.Snapshot.Used.EntityOperations, 0, 2);
|
||||
}
|
||||
|
||||
Assert.InRange(frames, 2, 64);
|
||||
Assert.Equal(0, coordinator.PendingCount);
|
||||
Assert.Equal(
|
||||
entities.Select(entity => entity.Id).Order(),
|
||||
lightingCalls.Keys.Order());
|
||||
Assert.All(lightingCalls.Values, count => Assert.Equal(1, count));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BudgetedRetirement_FailedEntityRetriesWithoutReplayingCommittedPrefix()
|
||||
{
|
||||
const uint landblockId = 0x2022FFFFu;
|
||||
var state = StateWith(
|
||||
landblockId,
|
||||
Entity(1),
|
||||
Entity(2),
|
||||
Entity(3));
|
||||
var calls = new Dictionary<uint, int>();
|
||||
bool failSecond = true;
|
||||
LandblockRetirementCoordinator coordinator =
|
||||
LandblockRetirementCoordinator.CreateBudgeted(
|
||||
state,
|
||||
ticket => AdvancePresentationStep(
|
||||
ticket,
|
||||
entity =>
|
||||
{
|
||||
calls[entity.Id] = calls.GetValueOrDefault(entity.Id) + 1;
|
||||
if (entity.Id == 2 && failSecond)
|
||||
{
|
||||
failSecond = false;
|
||||
throw new InvalidOperationException(
|
||||
"injected metered entity failure");
|
||||
}
|
||||
}),
|
||||
ticket => CompletePresentation(ticket));
|
||||
coordinator.BeginFull(landblockId);
|
||||
|
||||
var firstMeter = new StreamingWorkMeter(Budget(maxEntityOperations: 64));
|
||||
coordinator.Advance(firstMeter);
|
||||
firstMeter.FinishFrame();
|
||||
|
||||
Assert.Equal(1, coordinator.PendingCount);
|
||||
Assert.Equal(1, calls[1]);
|
||||
Assert.Equal(1, calls[2]);
|
||||
Assert.False(calls.ContainsKey(3));
|
||||
Assert.Equal(1, firstMeter.Snapshot.FailureCount);
|
||||
|
||||
var retryMeter = new StreamingWorkMeter(Budget(maxEntityOperations: 64));
|
||||
coordinator.Advance(retryMeter);
|
||||
retryMeter.FinishFrame();
|
||||
|
||||
Assert.Equal(0, coordinator.PendingCount);
|
||||
Assert.Equal(1, calls[1]);
|
||||
Assert.Equal(2, calls[2]);
|
||||
Assert.Equal(1, calls[3]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ForceReload_RetiresEveryLoadedId_WhenOneOwnerRemainsPending()
|
||||
{
|
||||
|
|
@ -59,31 +154,88 @@ public sealed class LandblockRetirementCoordinatorTests
|
|||
foreach (uint id in ids)
|
||||
state.AddLandblock(new LoadedLandblock(id, new LandBlock(), Array.Empty<WorldEntity>()));
|
||||
|
||||
bool failOnce = true;
|
||||
bool failRetirement = true;
|
||||
var coordinator = new LandblockRetirementCoordinator(
|
||||
state,
|
||||
ticket => CompletePresentation(
|
||||
ticket,
|
||||
terrain: () =>
|
||||
{
|
||||
if (ticket.LandblockId == ids[1] && failOnce)
|
||||
if (ticket.LandblockId == ids[1] && failRetirement)
|
||||
{
|
||||
failOnce = false;
|
||||
throw new InvalidOperationException("injected terrain failure");
|
||||
}
|
||||
}));
|
||||
var controller = Controller(state, coordinator);
|
||||
|
||||
controller.ForceReloadWindow();
|
||||
for (int frame = 0;
|
||||
frame < 16 && ids.Any(state.IsLoaded);
|
||||
frame++)
|
||||
{
|
||||
controller.Tick(0x20, 0x20);
|
||||
}
|
||||
|
||||
foreach (uint id in ids)
|
||||
Assert.False(state.IsLoaded(id));
|
||||
Assert.Equal(1, coordinator.PendingCount);
|
||||
|
||||
failRetirement = false;
|
||||
coordinator.Advance();
|
||||
Assert.Equal(0, coordinator.PendingCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ForceReload_CapturesAndDetachesThroughTheSharedEntityBudget()
|
||||
{
|
||||
uint[] ids =
|
||||
[
|
||||
0x2323FFFFu,
|
||||
0x2424FFFFu,
|
||||
0x2525FFFFu,
|
||||
];
|
||||
var state = new GpuWorldState();
|
||||
foreach (uint id in ids)
|
||||
{
|
||||
state.AddLandblock(new LoadedLandblock(
|
||||
id,
|
||||
new LandBlock(),
|
||||
Array.Empty<WorldEntity>()));
|
||||
}
|
||||
|
||||
var coordinator = new LandblockRetirementCoordinator(
|
||||
state,
|
||||
ticket => CompletePresentation(ticket));
|
||||
var controller = Controller(
|
||||
state,
|
||||
coordinator,
|
||||
workBudgetOptions: new StreamingWorkBudgetOptions(
|
||||
MaxUpdateMilliseconds: 1000,
|
||||
MaxCompletionAdmissions: 64,
|
||||
MaxAdoptedCpuBytes: 64 * StreamingWorkBudgetOptions.MiB,
|
||||
MaxEntityOperations: 1,
|
||||
MaxGpuUploadBytes: 64 * StreamingWorkBudgetOptions.MiB,
|
||||
MaxGlRetireOperations: 64,
|
||||
DestinationReserveFraction: 0.75f));
|
||||
controller.ForceReloadWindow();
|
||||
|
||||
int priorLoaded = ids.Length;
|
||||
for (int frame = 0; frame < 32 && priorLoaded != 0; frame++)
|
||||
{
|
||||
controller.Tick(0x23, 0x23);
|
||||
int loaded = ids.Count(state.IsLoaded);
|
||||
Assert.InRange(priorLoaded - loaded, 0, 1);
|
||||
Assert.InRange(
|
||||
controller.WorkDiagnostics.LastFrame.Used.EntityOperations,
|
||||
0,
|
||||
1);
|
||||
priorLoaded = loaded;
|
||||
}
|
||||
|
||||
Assert.Equal(0, priorLoaded);
|
||||
Assert.Equal(ids.Length, controller.LastFullWindowRetirementLandblockCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReplacementPublication_WaitsForSameIdRetirementFence()
|
||||
{
|
||||
|
|
@ -126,12 +278,18 @@ public sealed class LandblockRetirementCoordinatorTests
|
|||
EmptyMesh(),
|
||||
generation: 1));
|
||||
|
||||
controller.Tick(0x32, 0x32); // second failure; publication stays deferred
|
||||
controller.Tick(0x32, 0x32); // first failure; publication stays at worker source
|
||||
Assert.Equal(0, applyCount);
|
||||
Assert.False(state.IsLoaded(landblockId));
|
||||
Assert.True(coordinator.IsPending(landblockId));
|
||||
|
||||
controller.Tick(0x32, 0x32); // third attempt completes, then publishes once
|
||||
controller.Tick(0x32, 0x32); // second failure
|
||||
Assert.Equal(0, applyCount);
|
||||
Assert.True(coordinator.IsPending(landblockId));
|
||||
|
||||
controller.Tick(0x32, 0x32); // third attempt completes the reload barrier
|
||||
Assert.Equal(0, applyCount);
|
||||
controller.Tick(0x32, 0x32); // replacement publishes on the next frame
|
||||
Assert.Equal(1, applyCount);
|
||||
Assert.True(state.IsLoaded(landblockId));
|
||||
Assert.False(coordinator.IsPending(landblockId));
|
||||
|
|
@ -338,7 +496,8 @@ public sealed class LandblockRetirementCoordinatorTests
|
|||
GpuWorldState state,
|
||||
LandblockRetirementCoordinator coordinator,
|
||||
Func<int, IReadOnlyList<LandblockStreamResult>>? drain = null,
|
||||
Action<LandblockBuild, LandblockMeshData>? apply = null) =>
|
||||
Action<LandblockBuild, LandblockMeshData>? apply = null,
|
||||
StreamingWorkBudgetOptions? workBudgetOptions = null) =>
|
||||
new(
|
||||
enqueueLoad: (_, _, _) => { },
|
||||
enqueueUnload: (_, _) => { },
|
||||
|
|
@ -347,7 +506,8 @@ public sealed class LandblockRetirementCoordinatorTests
|
|||
state: state,
|
||||
nearRadius: 0,
|
||||
farRadius: 0,
|
||||
retirementCoordinator: coordinator);
|
||||
retirementCoordinator: coordinator,
|
||||
workBudgetOptions: workBudgetOptions);
|
||||
|
||||
private static IReadOnlyList<LandblockStreamResult> Drain(
|
||||
Queue<LandblockStreamResult> pending,
|
||||
|
|
@ -359,6 +519,60 @@ public sealed class LandblockRetirementCoordinatorTests
|
|||
return result;
|
||||
}
|
||||
|
||||
private static LandblockRetirementOperationResult AdvancePresentationStep(
|
||||
LandblockRetirementTicket ticket,
|
||||
Action<WorldEntity>? lighting = null)
|
||||
{
|
||||
return ticket.NextIncompleteStage switch
|
||||
{
|
||||
LandblockRetirementStage.EntityLighting =>
|
||||
ticket.RunEntityStep(
|
||||
LandblockRetirementStage.EntityLighting,
|
||||
static _ => true,
|
||||
lighting ?? (_ => { })),
|
||||
LandblockRetirementStage.EntityTranslucency =>
|
||||
ticket.RunEntityStep(
|
||||
LandblockRetirementStage.EntityTranslucency,
|
||||
static _ => true,
|
||||
static _ => { }),
|
||||
LandblockRetirementStage.PluginProjection =>
|
||||
ticket.RunEntityStep(
|
||||
LandblockRetirementStage.PluginProjection,
|
||||
static entity => entity.ServerGuid == 0,
|
||||
static _ => { }),
|
||||
LandblockRetirementStage.Terrain =>
|
||||
ticket.RunOnceStep(
|
||||
LandblockRetirementStage.Terrain,
|
||||
static () => { }),
|
||||
LandblockRetirementStage.Physics =>
|
||||
ticket.RunOnceStep(
|
||||
LandblockRetirementStage.Physics,
|
||||
static () => { }),
|
||||
LandblockRetirementStage.CellVisibility =>
|
||||
ticket.RunOnceStep(
|
||||
LandblockRetirementStage.CellVisibility,
|
||||
static () => { }),
|
||||
LandblockRetirementStage.BuildingRegistry =>
|
||||
ticket.RunOnceStep(
|
||||
LandblockRetirementStage.BuildingRegistry,
|
||||
static () => { }),
|
||||
LandblockRetirementStage.EnvironmentCells =>
|
||||
ticket.RunOnceStep(
|
||||
LandblockRetirementStage.EnvironmentCells,
|
||||
static () => { }),
|
||||
_ => LandblockRetirementOperationResult.NoWork,
|
||||
};
|
||||
}
|
||||
|
||||
private static StreamingWorkBudget Budget(int maxEntityOperations) => new(
|
||||
maxUpdateTime: TimeSpan.FromSeconds(1),
|
||||
maxCompletionAdmissions: 64,
|
||||
maxAdoptedCpuBytes: 64 * 1024 * 1024,
|
||||
maxEntityOperations,
|
||||
maxGpuUploadBytes: 64 * 1024 * 1024,
|
||||
maxGlRetireOperations: 64,
|
||||
destinationReserveFraction: 0.75f);
|
||||
|
||||
private static void CompletePresentation(
|
||||
LandblockRetirementTicket ticket,
|
||||
Action<WorldEntity>? lighting = null,
|
||||
|
|
|
|||
|
|
@ -275,7 +275,7 @@ public sealed class LocalPlayerTeleportControllerTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void SessionReset_ClearsTransitCancelsRevealAndRequiresRecenterConvergence()
|
||||
public void SessionReset_ClearsTransitAndLetsRecenterConvergeAcrossLaterFrames()
|
||||
{
|
||||
var harness = new Harness();
|
||||
harness.Controller.OnTeleportStarted(12);
|
||||
|
|
@ -291,7 +291,9 @@ public sealed class LocalPlayerTeleportControllerTests
|
|||
|
||||
var failed = new Harness();
|
||||
failed.Streaming.ResetResult = false;
|
||||
Assert.Throws<InvalidOperationException>(() => failed.Controller.ResetSession());
|
||||
failed.Controller.ResetSession();
|
||||
Assert.False(failed.Controller.IsActive);
|
||||
Assert.True(failed.Streaming.LastResetWasSessionEnding);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,123 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Audio;
|
||||
using AcDream.App.Streaming;
|
||||
using AcDream.Core.Selection;
|
||||
using AcDream.Core.World;
|
||||
using DatReaderWriter.DBObjs;
|
||||
|
||||
namespace AcDream.App.Tests.Streaming;
|
||||
|
||||
public sealed class WorldGenerationQuiescenceTests
|
||||
{
|
||||
private const uint LandblockId = 0x1010FFFFu;
|
||||
private const uint ServerGuid = 0x70000001u;
|
||||
|
||||
[Fact]
|
||||
public void ActiveGeneration_HidesWorldQueriesButRetainsCanonicalOwners()
|
||||
{
|
||||
var availability = new WorldGenerationAvailabilityState();
|
||||
var world = new GpuWorldState(availability: availability);
|
||||
WorldEntity entity = Entity();
|
||||
world.AddLandblock(new LoadedLandblock(
|
||||
LandblockId,
|
||||
new LandBlock(),
|
||||
Array.Empty<WorldEntity>()));
|
||||
world.PlaceLiveEntityProjection(LandblockId, entity);
|
||||
world.SetLandblockAabb(LandblockId, Vector3.Zero, Vector3.One);
|
||||
var nearby = new List<KeyValuePair<uint, WorldEntity>>();
|
||||
|
||||
Assert.True(world.IsLiveEntityVisible(ServerGuid));
|
||||
Assert.Single(world.LandblockEntries);
|
||||
Assert.Single(world.LandblockBounds);
|
||||
|
||||
availability.Begin(17);
|
||||
world.CopyLiveEntitiesNearLandblock(LandblockId, 0, nearby);
|
||||
|
||||
Assert.False(availability.IsWorldAvailable);
|
||||
Assert.False(world.IsLiveEntityVisible(ServerGuid));
|
||||
Assert.Empty(world.LandblockEntries);
|
||||
Assert.Empty(world.LandblockBounds);
|
||||
Assert.Empty(nearby);
|
||||
Assert.Same(entity, Assert.Single(world.Entities));
|
||||
Assert.True(world.IsLoaded(LandblockId));
|
||||
|
||||
Assert.False(availability.End(16));
|
||||
Assert.False(availability.IsWorldAvailable);
|
||||
Assert.True(availability.End(17));
|
||||
Assert.True(world.IsLiveEntityVisible(ServerGuid));
|
||||
Assert.Same(entity, Assert.Single(world.LandblockEntries).Entities.Single());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupersededReveal_StaysContinuouslySilentAndOnlyLatestGenerationReopens()
|
||||
{
|
||||
var availability = new WorldGenerationAvailabilityState();
|
||||
var world = new GpuWorldState(availability: availability);
|
||||
world.AddLandblock(new LoadedLandblock(
|
||||
LandblockId,
|
||||
new LandBlock(),
|
||||
Array.Empty<WorldEntity>()));
|
||||
world.PlaceLiveEntityProjection(LandblockId, Entity());
|
||||
var selection = new SelectionState();
|
||||
selection.Select(ServerGuid, SelectionChangeSource.World);
|
||||
var audio = new RecordingAudioQuiescence();
|
||||
var quiescence = new WorldGenerationQuiescence(
|
||||
availability,
|
||||
selection,
|
||||
world,
|
||||
audio);
|
||||
|
||||
quiescence.Begin(1);
|
||||
quiescence.Begin(2);
|
||||
quiescence.End(1);
|
||||
|
||||
Assert.False(availability.IsWorldAvailable);
|
||||
Assert.Equal(2, availability.QuiescedGeneration);
|
||||
Assert.Null(selection.SelectedObjectId);
|
||||
Assert.Equal(1, audio.SuspendCalls);
|
||||
Assert.Equal(0, audio.ResumeCalls);
|
||||
|
||||
quiescence.End(2);
|
||||
|
||||
Assert.True(availability.IsWorldAvailable);
|
||||
Assert.Equal(1, audio.ResumeCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Quiesce_PreservesNonWorldInventorySelection()
|
||||
{
|
||||
const uint inventoryGuid = 0x80000001u;
|
||||
var availability = new WorldGenerationAvailabilityState();
|
||||
var world = new GpuWorldState(availability: availability);
|
||||
var selection = new SelectionState();
|
||||
selection.Select(inventoryGuid, SelectionChangeSource.Inventory);
|
||||
var quiescence = new WorldGenerationQuiescence(
|
||||
availability,
|
||||
selection,
|
||||
world,
|
||||
audio: null);
|
||||
|
||||
quiescence.Begin(3);
|
||||
|
||||
Assert.Equal(inventoryGuid, selection.SelectedObjectId);
|
||||
}
|
||||
|
||||
private static WorldEntity Entity() => new()
|
||||
{
|
||||
Id = 1,
|
||||
ServerGuid = ServerGuid,
|
||||
SourceGfxObjOrSetupId = 0x02000001u,
|
||||
Position = Vector3.Zero,
|
||||
Rotation = Quaternion.Identity,
|
||||
MeshRefs = Array.Empty<MeshRef>(),
|
||||
};
|
||||
|
||||
private sealed class RecordingAudioQuiescence : IWorldAudioQuiescence
|
||||
{
|
||||
public int SuspendCalls { get; private set; }
|
||||
public int ResumeCalls { get; private set; }
|
||||
|
||||
public void SuspendWorldAudio() => SuspendCalls++;
|
||||
public void ResumeWorldAudio() => ResumeCalls++;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
using AcDream.App.Streaming;
|
||||
using AcDream.Core.Selection;
|
||||
|
||||
namespace AcDream.App.Tests.Streaming;
|
||||
|
||||
|
|
@ -114,4 +115,46 @@ public sealed class WorldRevealCoordinatorTests
|
|||
Assert.Equal(default, coordinator.Snapshot.Readiness);
|
||||
Assert.Equal(0, coordinator.PortalMaterializationCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RevealLifetime_QuiescesContinuouslyAndOnlyActiveGenerationReopens()
|
||||
{
|
||||
var availability = new WorldGenerationAvailabilityState();
|
||||
var world = new GpuWorldState(availability: availability);
|
||||
var audio = new RecordingAudioQuiescence();
|
||||
var quiescence = new WorldGenerationQuiescence(
|
||||
availability,
|
||||
new SelectionState(),
|
||||
world,
|
||||
audio);
|
||||
var coordinator = new WorldRevealCoordinator(
|
||||
isRenderNeighborhoodReady: (_, _) => true,
|
||||
isSpawnCellReady: _ => true,
|
||||
isTerrainNeighborhoodReady: (_, _) => true,
|
||||
areCompositeTexturesReady: () => true,
|
||||
prepareCompositeTextures: (_, _) => { },
|
||||
invalidateCompositeTextures: () => { },
|
||||
isSpawnClaimUnhydratable: _ => false,
|
||||
quiescence: quiescence);
|
||||
|
||||
Assert.Equal(1, coordinator.Begin(WorldRevealKind.Login));
|
||||
Assert.Equal(2, coordinator.Begin(WorldRevealKind.Portal));
|
||||
Assert.False(availability.IsWorldAvailable);
|
||||
Assert.Equal(2, availability.QuiescedGeneration);
|
||||
Assert.Equal(1, audio.SuspendCalls);
|
||||
|
||||
coordinator.Complete();
|
||||
|
||||
Assert.True(availability.IsWorldAvailable);
|
||||
Assert.Equal(1, audio.ResumeCalls);
|
||||
}
|
||||
|
||||
private sealed class RecordingAudioQuiescence : AcDream.App.Audio.IWorldAudioQuiescence
|
||||
{
|
||||
public int SuspendCalls { get; private set; }
|
||||
public int ResumeCalls { get; private set; }
|
||||
|
||||
public void SuspendWorldAudio() => SuspendCalls++;
|
||||
public void ResumeWorldAudio() => ResumeCalls++;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,25 @@ public sealed class RecallTeleportAnimationTests
|
|||
Assert.Equal(["objects", "network", "commands", "spatial"], calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LiveFrame_WhileCellsAreBlocked_DrainsNetworkAndCommandsWithoutWorldTicks()
|
||||
{
|
||||
var calls = new List<string>();
|
||||
var availability = new WorldGenerationAvailabilityState();
|
||||
availability.Begin(1);
|
||||
var frame = new RetailLiveFrameCoordinator(
|
||||
new TestLiveObjectFramePhase(_ => calls.Add("objects")),
|
||||
new GpuWorldState(availability: availability),
|
||||
new TestLiveSessionFramePhase(() => calls.Add("network")),
|
||||
new TestPostNetworkCommandFramePhase(() => calls.Add("commands")),
|
||||
new TestLiveSpatialReconcilePhase(() => calls.Add("spatial")),
|
||||
availability);
|
||||
|
||||
frame.Tick(1f / 60f);
|
||||
|
||||
Assert.Equal(["network", "commands"], calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LiveFrame_InvalidHostDeltaCannotBlockNetworkDispatch()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Reflection;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Streaming;
|
||||
using AcDream.App.Update;
|
||||
using AcDream.App.World;
|
||||
|
||||
|
|
@ -129,6 +130,33 @@ public sealed class UpdateFrameOrchestratorTests
|
|||
observed.Camera.Select(value => value.SimulationDeltaSeconds));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void QuiescedWorld_FreezesScriptClockAndLivenessWhilePortalPhasesContinue()
|
||||
{
|
||||
var calls = new List<string>();
|
||||
var observed = new FrameObservations();
|
||||
var availability = new WorldGenerationAvailabilityState();
|
||||
availability.Begin(7);
|
||||
UpdateFrameOrchestrator frame = Create(
|
||||
calls,
|
||||
observed: observed,
|
||||
availability: availability);
|
||||
|
||||
frame.Tick(new UpdateFrameInput(0.25));
|
||||
frame.Tick(new UpdateFrameInput(0.25));
|
||||
availability.End(7);
|
||||
frame.Tick(new UpdateFrameInput(0.25));
|
||||
|
||||
Assert.Equal([0.0, 0.0, 0.25], observed.PublishedTimes);
|
||||
Assert.Equal(1, calls.Count(call => call == "liveness"));
|
||||
Assert.Equal(3, calls.Count(call => call == "streaming"));
|
||||
Assert.Equal(3, calls.Count(call => call == "network"));
|
||||
Assert.Equal(3, calls.Count(call => call == "teleport"));
|
||||
Assert.Equal(3, calls.Count(call => call == "camera"));
|
||||
Assert.Equal([0.25f, 0.25f, 0.25f], observed.LiveDeltas);
|
||||
Assert.Equal([0.25f, 0.25f, 0.25f], observed.TeleportDeltas);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clock_RejectsFiniteDeltaAboveTheSinglePrecisionConsumerRange()
|
||||
{
|
||||
|
|
@ -200,6 +228,7 @@ public sealed class UpdateFrameOrchestratorTests
|
|||
typeof(ILocalPlayerTeleportFramePhase),
|
||||
typeof(IPlayerModeAutoEntryFramePhase),
|
||||
typeof(ICameraFramePhase),
|
||||
typeof(IWorldGenerationAvailability),
|
||||
];
|
||||
|
||||
FieldInfo[] fields = typeof(UpdateFrameOrchestrator).GetFields(
|
||||
|
|
@ -534,7 +563,8 @@ public sealed class UpdateFrameOrchestratorTests
|
|||
"new LiveEntityLivenessFramePhase(",
|
||||
"session.LocalTeleport,",
|
||||
"new PlayerModeAutoEntryFramePhase(",
|
||||
"cameraFrame);");
|
||||
"cameraFrame,",
|
||||
"live.WorldAvailability);");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -860,7 +890,8 @@ public sealed class UpdateFrameOrchestratorTests
|
|||
RecordingFailureSink? failureSink = null,
|
||||
FrameObservations? observed = null,
|
||||
bool teleportPlace = false,
|
||||
bool inboundCreatedPlayer = false)
|
||||
bool inboundCreatedPlayer = false,
|
||||
IWorldGenerationAvailability? availability = null)
|
||||
{
|
||||
teardown ??= new RecordingTeardown(calls);
|
||||
failureSink ??= new RecordingFailureSink();
|
||||
|
|
@ -875,7 +906,8 @@ public sealed class UpdateFrameOrchestratorTests
|
|||
new RecordingLiveness(calls),
|
||||
new RecordingTeleport(calls, observed, teleportPlace),
|
||||
new RecordingAutoEntry(calls),
|
||||
new RecordingCamera(calls, observed, inboundCreatedPlayer));
|
||||
new RecordingCamera(calls, observed, inboundCreatedPlayer),
|
||||
availability);
|
||||
}
|
||||
|
||||
private sealed class RecordingTeardown(List<string> calls)
|
||||
|
|
|
|||
|
|
@ -543,7 +543,8 @@ public class StreamingControllerPriorityApplyTests
|
|||
|
||||
ctrl.ForceReloadWindow();
|
||||
loads.Clear();
|
||||
ctrl.Tick(11, 10); // overlap remains desired in the replacement window.
|
||||
for (int i = 0; i < 32 && !loads.Any(load => load.Id == overlap); i++)
|
||||
ctrl.Tick(11, 10);
|
||||
ulong newGeneration = loads.Single(load => load.Id == overlap).Generation;
|
||||
Assert.NotEqual(oldGeneration, newGeneration);
|
||||
|
||||
|
|
@ -551,7 +552,8 @@ public class StreamingControllerPriorityApplyTests
|
|||
outbox.Enqueue(LoadedOf(overlap, newGeneration));
|
||||
outbox.Enqueue(new LandblockStreamResult.Unloaded(overlap, oldGeneration));
|
||||
|
||||
ctrl.Tick(11, 10);
|
||||
for (int i = 0; i < 8 && applied.Count == 0; i++)
|
||||
ctrl.Tick(11, 10);
|
||||
|
||||
Assert.Equal(new[] { overlap }, applied);
|
||||
Assert.True(state.IsLoaded(overlap));
|
||||
|
|
@ -559,7 +561,7 @@ public class StreamingControllerPriorityApplyTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void HardRecenter_DropsStaleOutboxBeforeDeferredApplyBudget()
|
||||
public void HardRecenter_DropsStaleOutboxThroughBoundedAdmissionBeforeCurrentResult()
|
||||
{
|
||||
uint current = StreamingRegion.EncodeLandblockIdForTest(11, 10);
|
||||
var outbox = new Queue<LandblockStreamResult>();
|
||||
|
|
@ -584,7 +586,8 @@ public class StreamingControllerPriorityApplyTests
|
|||
ulong oldGeneration = loads[0].Generation;
|
||||
ctrl.ForceReloadWindow();
|
||||
loads.Clear();
|
||||
ctrl.Tick(11, 10);
|
||||
for (int i = 0; i < 32 && !loads.Any(load => load.Id == current); i++)
|
||||
ctrl.Tick(11, 10);
|
||||
ulong newGeneration = loads.Single(load => load.Id == current).Generation;
|
||||
|
||||
for (int i = 0; i < 12; i++)
|
||||
|
|
@ -593,7 +596,8 @@ public class StreamingControllerPriorityApplyTests
|
|||
oldGeneration));
|
||||
outbox.Enqueue(LoadedOf(current, newGeneration));
|
||||
|
||||
ctrl.Tick(11, 10);
|
||||
for (int i = 0; i < 32 && !applied.Contains(current); i++)
|
||||
ctrl.Tick(11, 10);
|
||||
|
||||
Assert.Equal(new[] { current }, applied);
|
||||
Assert.Equal(0, ctrl.DeferredApplyBacklog);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue