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:
parent
dc39772bf9
commit
ea7ffbc186
10 changed files with 860 additions and 140 deletions
|
|
@ -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>());
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue