refactor(streaming): own presentation retirement in pipeline

Move full and near-tier presentation retirement out of GameWindow and into a concrete pipeline-owned stage mapper. Harden the retry ledger against reentrancy and committed visibility observer failures, enforce a single state/resource ownership graph, and split concrete versus legacy controller construction so callbacks cannot be silently ignored.

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-21 22:07:36 +02:00
parent dc39772bf9
commit ea7ffbc186
10 changed files with 860 additions and 140 deletions

View file

@ -137,6 +137,203 @@ public sealed class LandblockRetirementCoordinatorTests
Assert.False(coordinator.IsPending(landblockId));
}
[Fact]
public void ReentrantSameIdBegin_DoesNotReplaySuccessfulStage()
{
const uint landblockId = 0x4040FFFFu;
var state = StateWith(landblockId, Entity(1));
int lightingCalls = 0;
LandblockRetirementCoordinator? coordinator = null;
coordinator = new LandblockRetirementCoordinator(
state,
ticket => CompletePresentation(
ticket,
lighting: _ =>
{
lightingCalls++;
coordinator!.BeginFull(landblockId);
}));
coordinator.BeginFull(landblockId);
Assert.Equal(1, lightingCalls);
Assert.Equal(0, coordinator.PendingCount);
Assert.False(coordinator.IsPending(landblockId));
}
[Fact]
public void ReentrantAdvance_RetriesFailedStageWithoutRecursion()
{
const uint landblockId = 0x4141FFFFu;
var state = StateWith(landblockId, Entity(1));
int lightingCalls = 0;
LandblockRetirementCoordinator? coordinator = null;
coordinator = new LandblockRetirementCoordinator(
state,
ticket => CompletePresentation(
ticket,
lighting: _ =>
{
lightingCalls++;
coordinator!.Advance();
if (lightingCalls == 1)
throw new InvalidOperationException("injected first failure");
}));
coordinator.BeginFull(landblockId);
Assert.Equal(2, lightingCalls);
Assert.Equal(0, coordinator.PendingCount);
Assert.False(coordinator.IsPending(landblockId));
}
[Fact]
public void ReentrantDifferentIdBegin_IsDeferredUntilActiveAdvanceCompletes()
{
const uint firstId = 0x4242FFFFu;
const uint secondId = 0x4343FFFFu;
var state = StateWith(firstId);
state.AddLandblock(new LoadedLandblock(
secondId,
new LandBlock(),
Array.Empty<WorldEntity>()));
int firstAttempts = 0;
int secondAttempts = 0;
LandblockRetirementCoordinator? coordinator = null;
coordinator = new LandblockRetirementCoordinator(
state,
ticket => CompletePresentation(
ticket,
terrain: () =>
{
if (ticket.LandblockId == firstId)
{
firstAttempts++;
if (firstAttempts == 1)
throw new InvalidOperationException("leave first pending");
coordinator!.BeginFull(secondId);
}
else
{
secondAttempts++;
}
}));
coordinator.BeginFull(firstId);
Assert.Equal(1, coordinator.PendingCount);
coordinator.Advance();
Assert.Equal(2, firstAttempts);
Assert.Equal(1, secondAttempts);
Assert.False(state.IsLoaded(secondId));
Assert.Equal(0, coordinator.PendingCount);
}
[Fact]
public void RepeatedBegin_RemovesTicketWhenRetryCompletes()
{
const uint landblockId = 0x4444FFFFu;
var state = StateWith(landblockId);
int terrainAttempts = 0;
int requiredStageCalls = 0;
var coordinator = new LandblockRetirementCoordinator(
state,
ticket => CompletePresentation(
ticket,
terrain: () =>
{
terrainAttempts++;
if (terrainAttempts == 1)
throw new InvalidOperationException("injected first failure");
}),
_ =>
{
requiredStageCalls++;
return LandblockRetirementCoordinator.ProductionPresentationStages;
});
coordinator.BeginFull(landblockId);
Assert.Equal(1, coordinator.PendingCount);
coordinator.BeginFull(landblockId);
Assert.Equal(2, terrainAttempts);
Assert.Equal(1, requiredStageCalls);
Assert.Equal(0, coordinator.PendingCount);
Assert.False(coordinator.IsPending(landblockId));
}
[Fact]
public void VisibilityObserverFailure_CompletesReceiptThenRethrowsCommittedEdge()
{
const uint landblockId = 0x4545FFFFu;
WorldEntity live = Entity(1, serverGuid: 0x70000001u);
var state = StateWith(landblockId, live);
state.LiveProjectionVisibilityChanged += (_, _) =>
throw new InvalidOperationException("injected visibility failure");
int terrainCalls = 0;
var coordinator = new LandblockRetirementCoordinator(
state,
ticket => CompletePresentation(
ticket,
terrain: () => terrainCalls++));
Assert.Throws<AggregateException>(() =>
coordinator.BeginFull(landblockId));
Assert.False(state.IsLoaded(landblockId));
Assert.Equal(1, terrainCalls);
Assert.Equal(0, coordinator.PendingCount);
Assert.False(coordinator.IsPending(landblockId));
}
[Fact]
public void RequiredStageFailure_HappensBeforeSpatialDetachment()
{
const uint landblockId = 0x4646FFFFu;
var state = StateWith(landblockId);
var coordinator = new LandblockRetirementCoordinator(
state,
static _ => { },
static _ => throw new InvalidOperationException(
"injected required-stage failure"));
Assert.Throws<InvalidOperationException>(() =>
coordinator.BeginFull(landblockId));
Assert.True(state.IsLoaded(landblockId));
Assert.Equal(0, coordinator.PendingCount);
}
[Fact]
public void ObserverFailure_DrainsReentrantDifferentIdBeginBeforeRethrow()
{
const uint firstId = 0x4747FFFFu;
const uint secondId = 0x4848FFFFu;
WorldEntity live = Entity(1, serverGuid: 0x70000002u);
var state = StateWith(firstId, live);
state.AddLandblock(new LoadedLandblock(
secondId,
new LandBlock(),
Array.Empty<WorldEntity>()));
LandblockRetirementCoordinator? coordinator = null;
coordinator = new LandblockRetirementCoordinator(
state,
ticket => CompletePresentation(ticket));
state.LiveProjectionVisibilityChanged += (_, _) =>
{
coordinator.BeginFull(secondId);
throw new InvalidOperationException("injected visibility failure");
};
Assert.Throws<AggregateException>(() =>
coordinator.BeginFull(firstId));
Assert.False(state.IsLoaded(firstId));
Assert.False(state.IsLoaded(secondId));
Assert.Equal(0, coordinator.PendingCount);
}
private static StreamingController Controller(
GpuWorldState state,
LandblockRetirementCoordinator coordinator,
@ -187,15 +384,28 @@ public sealed class LandblockRetirementCoordinatorTests
ticket.RunOnce(LandblockRetirementStage.EnvironmentCells, static () => { });
}
private static WorldEntity Entity(uint id) => new()
private static WorldEntity Entity(uint id, uint serverGuid = 0u) => new()
{
Id = id,
ServerGuid = serverGuid,
SourceGfxObjOrSetupId = 0x01000000u + id,
Position = Vector3.Zero,
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
};
private static GpuWorldState StateWith(
uint landblockId,
params WorldEntity[] entities)
{
var state = new GpuWorldState();
state.AddLandblock(new LoadedLandblock(
landblockId,
new LandBlock(),
entities));
return state;
}
private static LandblockMeshData EmptyMesh() => new(
Array.Empty<TerrainVertex>(),
Array.Empty<uint>());