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:
Erik 2026-07-24 18:29:52 +02:00
parent b8f6317fe1
commit bb16f74fd4
38 changed files with 1691 additions and 170 deletions

View file

@ -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);

View file

@ -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; }

View file

@ -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)

View file

@ -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,

View file

@ -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]

View file

@ -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++;
}
}

View file

@ -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++;
}
}

View file

@ -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()
{

View file

@ -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)

View file

@ -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);