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>
773 lines
28 KiB
C#
773 lines
28 KiB
C#
using System.Collections.Immutable;
|
|
using System.Numerics;
|
|
using AcDream.App.Rendering;
|
|
using AcDream.App.Rendering.Wb;
|
|
using AcDream.App.Streaming;
|
|
using AcDream.Core.Terrain;
|
|
using AcDream.Core.World;
|
|
using DatReaderWriter.DBObjs;
|
|
using Xunit;
|
|
|
|
namespace AcDream.App.Tests.Streaming;
|
|
|
|
public sealed class LandblockPresentationPipelineTests
|
|
{
|
|
[Fact]
|
|
public void ControllerRejectsPresentationPipelineFromForeignWorldState()
|
|
{
|
|
var controllerState = new GpuWorldState();
|
|
var foreignState = new GpuWorldState();
|
|
var foreignPipeline = new LandblockPresentationPipeline(
|
|
static (_, _) => { },
|
|
foreignState);
|
|
|
|
Assert.Throws<ArgumentException>(() => new StreamingController(
|
|
enqueueLoad: static (_, _, _) => { },
|
|
enqueueUnload: static (_, _) => { },
|
|
drainCompletions: static _ => Array.Empty<LandblockStreamResult>(),
|
|
controllerState,
|
|
nearRadius: 0,
|
|
farRadius: 0,
|
|
presentationPipeline: foreignPipeline));
|
|
}
|
|
|
|
[Fact]
|
|
public void LegacyPipelineRejectsRetirementCoordinatorFromForeignWorldState()
|
|
{
|
|
var pipelineState = new GpuWorldState();
|
|
var foreignCoordinator = new LandblockRetirementCoordinator(
|
|
new GpuWorldState(),
|
|
static _ => { });
|
|
|
|
Assert.Throws<ArgumentException>(() =>
|
|
new LandblockPresentationPipeline(
|
|
static (_, _) => { },
|
|
pipelineState,
|
|
retirementCoordinator: foreignCoordinator));
|
|
}
|
|
|
|
[Fact]
|
|
public void ControllerConcreteConstructorHasNoLegacyPresentationParameters()
|
|
{
|
|
string[] legacyNames =
|
|
[
|
|
"applyTerrain",
|
|
"removeTerrain",
|
|
"demoteNearLayer",
|
|
"onLandblockLoaded",
|
|
"ensureEnvCellMeshes",
|
|
"retirementCoordinator",
|
|
];
|
|
|
|
System.Reflection.ConstructorInfo concrete = Assert.Single(
|
|
typeof(StreamingController).GetConstructors(),
|
|
constructor => constructor.GetParameters().Any(
|
|
parameter => parameter.Name == "presentationPipeline"));
|
|
string[] names = concrete.GetParameters()
|
|
.Select(parameter => parameter.Name!)
|
|
.ToArray();
|
|
Assert.DoesNotContain(names, legacyNames.Contains);
|
|
}
|
|
|
|
[Fact]
|
|
public void Loaded_PreservesPresentationSpatialPinReplayAndLiveRecoveryOrder()
|
|
{
|
|
const uint landblockId = 0x2020FFFFu;
|
|
var calls = new List<string>();
|
|
var meshes = new RecordingMeshAdapter(calls);
|
|
var state = new GpuWorldState(new LandblockSpawnAdapter(meshes));
|
|
LandblockBuild build = BuildWithShell(landblockId);
|
|
var pipeline = new LandblockPresentationPipeline(
|
|
publishBeforeSpatialCommit: (accepted, _) =>
|
|
{
|
|
Assert.Same(build, accepted);
|
|
Assert.False(state.IsLoaded(landblockId));
|
|
calls.Add("presentation");
|
|
},
|
|
state,
|
|
onLandblockLoaded: id =>
|
|
{
|
|
Assert.Equal(landblockId, id);
|
|
Assert.True(state.IsLoaded(id));
|
|
calls.Add("live-recovery");
|
|
},
|
|
ensureEnvCellMeshes: envCells =>
|
|
{
|
|
Assert.Same(build.EnvCells, envCells);
|
|
Assert.True(state.IsLoaded(landblockId));
|
|
calls.Add("mesh-replay-after-pin");
|
|
});
|
|
|
|
pipeline.PublishLoaded(new LandblockStreamResult.Loaded(
|
|
landblockId,
|
|
LandblockStreamTier.Near,
|
|
build,
|
|
EmptyMesh()));
|
|
|
|
Assert.Equal(
|
|
["presentation", "pin", "mesh-replay-after-pin", "live-recovery"],
|
|
calls);
|
|
Assert.True(state.IsNearTier(landblockId));
|
|
}
|
|
|
|
[Fact]
|
|
public void Promoted_ExistingFar_MergesNearLayerBeforeReplayAndRecovery()
|
|
{
|
|
const uint landblockId = 0x3030FFFFu;
|
|
var calls = new List<string>();
|
|
var meshes = new RecordingMeshAdapter(calls);
|
|
var state = new GpuWorldState(new LandblockSpawnAdapter(meshes));
|
|
state.AddLandblock(
|
|
new LoadedLandblock(
|
|
landblockId,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>(),
|
|
PhysicsDatBundle.Empty),
|
|
tier: LandblockStreamTier.Far);
|
|
LandblockBuild build = BuildWithShell(landblockId, Entity(7));
|
|
var pipeline = new LandblockPresentationPipeline(
|
|
publishBeforeSpatialCommit: (_, _) => calls.Add("presentation"),
|
|
state,
|
|
onLandblockLoaded: _ =>
|
|
{
|
|
Assert.True(state.IsNearTier(landblockId));
|
|
Assert.Contains(state.Entities, entity => entity.Id == 7);
|
|
calls.Add("live-recovery");
|
|
},
|
|
ensureEnvCellMeshes: _ =>
|
|
{
|
|
Assert.True(state.IsNearTier(landblockId));
|
|
Assert.Contains(state.Entities, entity => entity.Id == 7);
|
|
calls.Add("mesh-replay-after-pin");
|
|
});
|
|
|
|
pipeline.PublishPromoted(new LandblockStreamResult.Promoted(
|
|
landblockId,
|
|
build,
|
|
EmptyMesh()),
|
|
mergeIntoExistingLandblock: true);
|
|
|
|
Assert.Equal(
|
|
["presentation", "pin", "mesh-replay-after-pin", "live-recovery"],
|
|
calls);
|
|
}
|
|
|
|
[Fact]
|
|
public void PublishAsFar_StripsNearPayloadAndSkipsEnvCellReplay()
|
|
{
|
|
const uint landblockId = 0x4040FFFFu;
|
|
var state = new GpuWorldState();
|
|
var origin = new LandblockBuildOrigin(0x40, 0x41);
|
|
LandblockBuild source = BuildWithShell(landblockId, Entity(9)) with
|
|
{
|
|
Origin = origin,
|
|
};
|
|
LandblockBuild? published = null;
|
|
int replayCount = 0;
|
|
int recoveryCount = 0;
|
|
var pipeline = new LandblockPresentationPipeline(
|
|
publishBeforeSpatialCommit: (build, _) => published = build,
|
|
state,
|
|
onLandblockLoaded: _ => recoveryCount++,
|
|
ensureEnvCellMeshes: _ => replayCount++);
|
|
|
|
var accepted = new LandblockStreamResult.Loaded(
|
|
landblockId,
|
|
LandblockStreamTier.Near,
|
|
source,
|
|
EmptyMesh());
|
|
pipeline.PublishAsFar(accepted, source, accepted.MeshData);
|
|
|
|
Assert.NotNull(published);
|
|
Assert.Empty(published.Landblock.Entities);
|
|
Assert.Same(PhysicsDatBundle.Empty, published.Landblock.PhysicsDats);
|
|
Assert.Null(published.EnvCells);
|
|
Assert.Equal(origin, published.Origin);
|
|
Assert.True(state.IsLoaded(landblockId));
|
|
Assert.False(state.IsNearTier(landblockId));
|
|
Assert.Equal(0, replayCount);
|
|
Assert.Equal(1, recoveryCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void EnvCellReplayFailure_ResumesSuffixWithoutReplayingCommittedPrefix()
|
|
{
|
|
const uint landblockId = 0x5151FFFFu;
|
|
var calls = new List<string>();
|
|
var state = new GpuWorldState(new LandblockSpawnAdapter(
|
|
new RecordingMeshAdapter(calls)));
|
|
bool failReplayOnce = true;
|
|
var pipeline = new LandblockPresentationPipeline(
|
|
publishBeforeSpatialCommit: (_, _) => calls.Add("presentation"),
|
|
state,
|
|
onLandblockLoaded: _ => calls.Add("live-recovery"),
|
|
ensureEnvCellMeshes: _ =>
|
|
{
|
|
calls.Add("mesh-replay");
|
|
if (failReplayOnce)
|
|
{
|
|
failReplayOnce = false;
|
|
throw new InvalidOperationException("injected replay failure");
|
|
}
|
|
});
|
|
LandblockBuild build = BuildWithShell(landblockId);
|
|
var result = new LandblockStreamResult.Loaded(
|
|
landblockId,
|
|
LandblockStreamTier.Near,
|
|
build,
|
|
EmptyMesh());
|
|
|
|
Assert.Throws<InvalidOperationException>(() => pipeline.PublishLoaded(result));
|
|
Assert.True(state.IsNearTier(landblockId));
|
|
Assert.True(pipeline.HasPendingPublication(result));
|
|
|
|
pipeline.PublishLoaded(result);
|
|
|
|
Assert.Equal(
|
|
["presentation", "pin", "mesh-replay", "mesh-replay", "live-recovery"],
|
|
calls);
|
|
Assert.False(pipeline.HasPendingPublication(result));
|
|
}
|
|
|
|
[Fact]
|
|
public void PromotionReplayFailure_RetainsOriginalSelfContainedModeAcrossRetry()
|
|
{
|
|
const uint landblockId = 0x5252FFFFu;
|
|
var calls = new List<string>();
|
|
var state = new GpuWorldState(new LandblockSpawnAdapter(
|
|
new RecordingMeshAdapter(calls)));
|
|
bool failReplayOnce = true;
|
|
var pipeline = new LandblockPresentationPipeline(
|
|
publishBeforeSpatialCommit: (_, _) => calls.Add("presentation"),
|
|
state,
|
|
onLandblockLoaded: _ => calls.Add("live-recovery"),
|
|
ensureEnvCellMeshes: _ =>
|
|
{
|
|
calls.Add("mesh-replay");
|
|
if (failReplayOnce)
|
|
{
|
|
failReplayOnce = false;
|
|
throw new InvalidOperationException("injected replay failure");
|
|
}
|
|
});
|
|
LandblockBuild build = BuildWithShell(landblockId, Entity(17));
|
|
var result = new LandblockStreamResult.Promoted(
|
|
landblockId,
|
|
build,
|
|
EmptyMesh());
|
|
|
|
Assert.Throws<InvalidOperationException>(() => pipeline.PublishPromoted(
|
|
result,
|
|
mergeIntoExistingLandblock: false));
|
|
Assert.True(state.IsNearTier(landblockId));
|
|
|
|
// Current residency changed after the first attempt. The transaction
|
|
// must retain the original self-contained publication command rather
|
|
// than switching policy mid-retry.
|
|
pipeline.PublishPromoted(result, mergeIntoExistingLandblock: true);
|
|
|
|
Assert.Equal(
|
|
["presentation", "pin", "mesh-replay", "mesh-replay", "live-recovery"],
|
|
calls);
|
|
Assert.Single(state.Entities, entity => entity.Id == 17);
|
|
}
|
|
|
|
[Fact]
|
|
public void SpatialPinFailure_ControllerRetriesExactCompletionWithoutReplayingPresentation()
|
|
{
|
|
const uint landblockId = 0x6060FFFFu;
|
|
var meshes = new FailFirstPreparedPinAdapter();
|
|
var state = new GpuWorldState(new LandblockSpawnAdapter(meshes));
|
|
var outbox = new Queue<LandblockStreamResult>();
|
|
LandblockBuild build = BuildWithShell(landblockId);
|
|
outbox.Enqueue(new LandblockStreamResult.Loaded(
|
|
landblockId,
|
|
LandblockStreamTier.Near,
|
|
build,
|
|
EmptyMesh()));
|
|
int presentationCalls = 0;
|
|
int replayCalls = 0;
|
|
int liveRecoveryCalls = 0;
|
|
var controller = new StreamingController(
|
|
enqueueLoad: (_, _) => { },
|
|
enqueueUnload: _ => { },
|
|
drainCompletions: max => Drain(outbox, max),
|
|
applyTerrain: (_, _) => presentationCalls++,
|
|
state,
|
|
nearRadius: 0,
|
|
farRadius: 0,
|
|
onLandblockLoaded: _ => liveRecoveryCalls++,
|
|
ensureEnvCellMeshes: _ => replayCalls++);
|
|
|
|
Assert.Throws<InvalidOperationException>(() => controller.Tick(0x60, 0x60));
|
|
Assert.True(state.IsNearTier(landblockId));
|
|
Assert.Equal(1, presentationCalls);
|
|
Assert.Equal(0, replayCalls);
|
|
Assert.Equal(0, liveRecoveryCalls);
|
|
|
|
controller.Tick(0x60, 0x60);
|
|
|
|
Assert.Equal(1, presentationCalls);
|
|
Assert.Equal(2, meshes.PinAttempts);
|
|
Assert.Equal(1, replayCalls);
|
|
Assert.Equal(1, liveRecoveryCalls);
|
|
}
|
|
|
|
[Fact]
|
|
public void PromotionPinFailure_RetriesActivationWithoutAppendingEntitiesAgain()
|
|
{
|
|
const uint landblockId = 0x6161FFFFu;
|
|
var meshes = new FailFirstPreparedPinAdapter();
|
|
var state = new GpuWorldState(new LandblockSpawnAdapter(meshes));
|
|
state.AddLandblock(
|
|
new LoadedLandblock(
|
|
landblockId,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()),
|
|
tier: LandblockStreamTier.Far);
|
|
int presentationCalls = 0;
|
|
var pipeline = new LandblockPresentationPipeline(
|
|
publishBeforeSpatialCommit: (_, _) => presentationCalls++,
|
|
state);
|
|
WorldEntity promotedEntity = Entity(31);
|
|
LandblockBuild build = BuildWithShell(landblockId, promotedEntity);
|
|
var result = new LandblockStreamResult.Promoted(
|
|
landblockId,
|
|
build,
|
|
EmptyMesh());
|
|
|
|
Assert.Throws<InvalidOperationException>(() => pipeline.PublishPromoted(
|
|
result,
|
|
mergeIntoExistingLandblock: true));
|
|
Assert.Single(state.Entities, entity => ReferenceEquals(entity, promotedEntity));
|
|
|
|
pipeline.PublishPromoted(result, mergeIntoExistingLandblock: true);
|
|
|
|
Assert.Equal(1, presentationCalls);
|
|
Assert.Equal(2, meshes.PinAttempts);
|
|
Assert.Single(state.Entities, entity => ReferenceEquals(entity, promotedEntity));
|
|
Assert.False(pipeline.HasPendingPublication(result));
|
|
}
|
|
|
|
[Fact]
|
|
public void LoadedPinFailure_RetryPreservesPendingLiveProjectionMergedAtSpatialCommit()
|
|
{
|
|
const uint landblockId = 0x6262FFFFu;
|
|
var meshes = new FailFirstPreparedPinAdapter();
|
|
var state = new GpuWorldState(new LandblockSpawnAdapter(meshes));
|
|
var pendingLive = new WorldEntity
|
|
{
|
|
Id = 77,
|
|
ServerGuid = 0x80000077u,
|
|
SourceGfxObjOrSetupId = 0x01000077u,
|
|
Position = Vector3.Zero,
|
|
Rotation = Quaternion.Identity,
|
|
MeshRefs = [new MeshRef(0x01000077u, Matrix4x4.Identity)],
|
|
};
|
|
state.PlaceLiveEntityProjection(0x62620100u, pendingLive);
|
|
LandblockBuild build = BuildWithShell(landblockId, Entity(32));
|
|
var result = new LandblockStreamResult.Loaded(
|
|
landblockId,
|
|
LandblockStreamTier.Near,
|
|
build,
|
|
EmptyMesh());
|
|
var pipeline = new LandblockPresentationPipeline(
|
|
static (_, _) => { },
|
|
state);
|
|
|
|
Assert.Throws<InvalidOperationException>(() => pipeline.PublishLoaded(result));
|
|
Assert.Contains(state.Entities, entity => ReferenceEquals(entity, pendingLive));
|
|
Assert.Equal(0, state.PendingLiveEntityCount);
|
|
|
|
pipeline.PublishLoaded(result);
|
|
|
|
Assert.Contains(state.Entities, entity => ReferenceEquals(entity, pendingLive));
|
|
Assert.Single(state.Entities, entity => ReferenceEquals(entity, pendingLive));
|
|
Assert.Equal(0, state.PendingLiveEntityCount);
|
|
Assert.Equal(2, meshes.PinAttempts);
|
|
}
|
|
|
|
[Fact]
|
|
public void PriorityFailure_PreservesFailingResultAndUntouchedDrainedSuffix()
|
|
{
|
|
const uint priorityId = 0x7070FFFFu;
|
|
const uint laterId = 0x7071FFFFu;
|
|
const uint unloadId = 0x1010FFFFu;
|
|
var meshes = new FailFirstPreparedPinAdapter();
|
|
var state = new GpuWorldState(new LandblockSpawnAdapter(meshes));
|
|
state.AddLandblock(new LoadedLandblock(
|
|
unloadId,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()));
|
|
var outbox = new Queue<LandblockStreamResult>(
|
|
[
|
|
new LandblockStreamResult.Loaded(
|
|
priorityId,
|
|
LandblockStreamTier.Near,
|
|
BuildWithShell(priorityId),
|
|
EmptyMesh()),
|
|
new LandblockStreamResult.Loaded(
|
|
laterId,
|
|
LandblockStreamTier.Near,
|
|
BuildWithShell(laterId),
|
|
EmptyMesh()),
|
|
new LandblockStreamResult.Unloaded(unloadId),
|
|
]);
|
|
var calls = new List<string>();
|
|
var controller = new StreamingController(
|
|
enqueueLoad: (_, _) => { },
|
|
enqueueUnload: _ => { },
|
|
drainCompletions: max => Drain(outbox, max),
|
|
applyTerrain: (build, _) => calls.Add($"publish:{build.LandblockId:X8}"),
|
|
state,
|
|
nearRadius: 0,
|
|
farRadius: 2,
|
|
removeTerrain: id => calls.Add($"unload:{id:X8}"));
|
|
controller.PriorityLandblockId = priorityId;
|
|
|
|
Assert.Throws<InvalidOperationException>(() => controller.Tick(0x70, 0x70));
|
|
Assert.Empty(outbox);
|
|
Assert.False(state.IsLoaded(laterId));
|
|
Assert.True(state.IsLoaded(unloadId));
|
|
|
|
controller.PriorityLandblockId = 0;
|
|
controller.Tick(0x70, 0x70);
|
|
|
|
Assert.True(state.IsLoaded(priorityId));
|
|
Assert.True(state.IsLoaded(laterId));
|
|
Assert.False(state.IsLoaded(unloadId));
|
|
Assert.Equal(
|
|
[
|
|
$"publish:{priorityId:X8}",
|
|
$"publish:{laterId:X8}",
|
|
$"unload:{unloadId:X8}",
|
|
],
|
|
calls);
|
|
}
|
|
|
|
[Fact]
|
|
public void HardRecenter_ConvergesPendingSpatialPublicationBeforeRetiringIt()
|
|
{
|
|
const uint landblockId = 0x8080FFFFu;
|
|
var meshes = new FailFirstPreparedPinAdapter();
|
|
var state = new GpuWorldState(new LandblockSpawnAdapter(meshes));
|
|
var outbox = new Queue<LandblockStreamResult>();
|
|
var result = new LandblockStreamResult.Loaded(
|
|
landblockId,
|
|
LandblockStreamTier.Near,
|
|
BuildWithShell(landblockId),
|
|
EmptyMesh());
|
|
outbox.Enqueue(result);
|
|
int presentationCalls = 0;
|
|
int replayCalls = 0;
|
|
int recoveryCalls = 0;
|
|
var controller = new StreamingController(
|
|
enqueueLoad: (_, _) => { },
|
|
enqueueUnload: _ => { },
|
|
drainCompletions: max => Drain(outbox, max),
|
|
applyTerrain: (_, _) => presentationCalls++,
|
|
state,
|
|
nearRadius: 0,
|
|
farRadius: 0,
|
|
onLandblockLoaded: _ => recoveryCalls++,
|
|
ensureEnvCellMeshes: _ => replayCalls++);
|
|
|
|
Assert.Throws<InvalidOperationException>(() => controller.Tick(0x80, 0x80));
|
|
Assert.True(state.IsNearTier(landblockId));
|
|
|
|
controller.ForceReloadWindow();
|
|
|
|
Assert.False(state.IsLoaded(landblockId));
|
|
Assert.Equal(1, presentationCalls);
|
|
Assert.Equal(1, replayCalls);
|
|
Assert.Equal(1, recoveryCalls);
|
|
Assert.Equal(2, meshes.PinAttempts);
|
|
}
|
|
|
|
[Fact]
|
|
public void DesiredNearToFarChurn_ResumesOriginalPublicationThenDemotes()
|
|
{
|
|
const uint landblockId = 0x9090FFFFu;
|
|
var calls = new List<string>();
|
|
var state = new GpuWorldState(new LandblockSpawnAdapter(
|
|
new RecordingMeshAdapter(calls)));
|
|
var outbox = new Queue<LandblockStreamResult>();
|
|
var result = new LandblockStreamResult.Loaded(
|
|
landblockId,
|
|
LandblockStreamTier.Near,
|
|
BuildWithShell(landblockId, Entity(23)),
|
|
EmptyMesh());
|
|
outbox.Enqueue(result);
|
|
bool failReplayOnce = true;
|
|
var controller = new StreamingController(
|
|
enqueueLoad: (_, _) => { },
|
|
enqueueUnload: _ => { },
|
|
drainCompletions: max => Drain(outbox, max),
|
|
applyTerrain: (_, _) => calls.Add("presentation"),
|
|
state,
|
|
nearRadius: 0,
|
|
farRadius: 2,
|
|
demoteNearLayer: _ => calls.Add("demote"),
|
|
ensureEnvCellMeshes: _ =>
|
|
{
|
|
calls.Add("mesh-replay");
|
|
if (failReplayOnce)
|
|
{
|
|
failReplayOnce = false;
|
|
throw new InvalidOperationException("injected replay failure");
|
|
}
|
|
});
|
|
|
|
Assert.Throws<InvalidOperationException>(() => controller.Tick(0x90, 0x90));
|
|
Assert.True(state.IsNearTier(landblockId));
|
|
|
|
controller.Tick(0x90, 0x93);
|
|
|
|
Assert.True(state.IsLoaded(landblockId));
|
|
Assert.False(state.IsNearTier(landblockId));
|
|
Assert.DoesNotContain(state.Entities, entity => entity.Id == 23);
|
|
Assert.Equal(1, calls.Count(call => call == "presentation"));
|
|
Assert.Equal(2, calls.Count(call => call == "mesh-replay"));
|
|
Assert.Contains("demote", calls);
|
|
}
|
|
|
|
[Fact]
|
|
public void CoarsePresentationFailure_IsFailFastAndNotRetainedForAutomaticReplay()
|
|
{
|
|
const uint landblockId = 0xA0A0FFFFu;
|
|
var state = new GpuWorldState();
|
|
int presentationCalls = 0;
|
|
var pipeline = new LandblockPresentationPipeline(
|
|
publishBeforeSpatialCommit: (_, _) =>
|
|
{
|
|
presentationCalls++;
|
|
throw new StreamingMutationException(
|
|
"injected committed coarse failure",
|
|
mutationCommitted: true);
|
|
},
|
|
state);
|
|
var result = new LandblockStreamResult.Loaded(
|
|
landblockId,
|
|
LandblockStreamTier.Near,
|
|
BuildWithShell(landblockId),
|
|
EmptyMesh());
|
|
|
|
Assert.Throws<StreamingMutationException>(() => pipeline.PublishLoaded(result));
|
|
|
|
Assert.Equal(1, presentationCalls);
|
|
Assert.False(pipeline.HasPendingPublication(result));
|
|
Assert.Equal(0, pipeline.PendingPublicationCount);
|
|
Assert.False(state.IsLoaded(landblockId));
|
|
}
|
|
|
|
[Fact]
|
|
public void DeferredCommittedPresentationFailure_DropsOnlyFailedResultAndPreservesTail()
|
|
{
|
|
const uint failedId = 0xB0B0FFFFu;
|
|
const uint tailId = 0xB0B1FFFFu;
|
|
var outbox = new Queue<LandblockStreamResult>(
|
|
[
|
|
new LandblockStreamResult.Loaded(
|
|
failedId,
|
|
LandblockStreamTier.Near,
|
|
new LandblockBuild(new LoadedLandblock(
|
|
failedId,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>())),
|
|
EmptyMesh()),
|
|
new LandblockStreamResult.Loaded(
|
|
tailId,
|
|
LandblockStreamTier.Near,
|
|
new LandblockBuild(new LoadedLandblock(
|
|
tailId,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>())),
|
|
EmptyMesh()),
|
|
]);
|
|
int failedCalls = 0;
|
|
int tailCalls = 0;
|
|
var state = new GpuWorldState();
|
|
var controller = new StreamingController(
|
|
enqueueLoad: (_, _) => { },
|
|
enqueueUnload: _ => { },
|
|
drainCompletions: max => Drain(outbox, max),
|
|
applyTerrain: (build, _) =>
|
|
{
|
|
if (build.LandblockId == failedId)
|
|
{
|
|
failedCalls++;
|
|
throw new StreamingMutationException(
|
|
"injected committed presentation failure",
|
|
mutationCommitted: true);
|
|
}
|
|
tailCalls++;
|
|
},
|
|
state,
|
|
nearRadius: 0,
|
|
farRadius: 2);
|
|
|
|
Assert.Throws<StreamingMutationException>(() => controller.Tick(0xB0, 0xB0));
|
|
Assert.Equal(1, failedCalls);
|
|
Assert.Equal(0, tailCalls);
|
|
Assert.Equal(1, controller.DeferredApplyBacklog);
|
|
|
|
controller.Tick(0xB0, 0xB0);
|
|
|
|
Assert.Equal(1, failedCalls);
|
|
Assert.Equal(1, tailCalls);
|
|
Assert.False(state.IsLoaded(failedId));
|
|
Assert.True(state.IsLoaded(tailId));
|
|
Assert.Equal(0, controller.DeferredApplyBacklog);
|
|
}
|
|
|
|
[Fact]
|
|
public void RetirementFacade_DetachesFirstAndReportsPendingLedger()
|
|
{
|
|
const uint landblockId = 0x5050FFFFu;
|
|
var state = new GpuWorldState();
|
|
state.AddLandblock(new LoadedLandblock(
|
|
landblockId,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()));
|
|
bool failOnce = true;
|
|
var retirements = new LandblockRetirementCoordinator(
|
|
state,
|
|
ticket =>
|
|
{
|
|
ticket.RunOnce(
|
|
LandblockRetirementStage.EntityLighting,
|
|
() =>
|
|
{
|
|
if (failOnce)
|
|
{
|
|
failOnce = false;
|
|
throw new InvalidOperationException("injected failure");
|
|
}
|
|
});
|
|
},
|
|
_ => LandblockRetirementStage.EntityLighting);
|
|
var pipeline = new LandblockPresentationPipeline(
|
|
static (_, _) => { },
|
|
state,
|
|
retirementCoordinator: retirements);
|
|
|
|
pipeline.BeginFullRetirement(landblockId);
|
|
|
|
Assert.False(state.IsLoaded(landblockId));
|
|
Assert.True(pipeline.IsRetirementPending(landblockId));
|
|
Assert.Equal(1, pipeline.PendingRetirementCount);
|
|
|
|
pipeline.AdvanceRetirements();
|
|
|
|
Assert.False(pipeline.IsRetirementPending(landblockId));
|
|
Assert.Equal(0, pipeline.PendingRetirementCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void Pipeline_DoesNotOwnStreamingPolicyOrLiveIdentityMaps()
|
|
{
|
|
string[] forbiddenFragments =
|
|
[
|
|
"generation",
|
|
"desiredtier",
|
|
"streamingregion",
|
|
"serverguid",
|
|
"liveentity",
|
|
];
|
|
|
|
string[] fields = typeof(LandblockPresentationPipeline)
|
|
.GetFields(System.Reflection.BindingFlags.Instance
|
|
| System.Reflection.BindingFlags.NonPublic)
|
|
.Select(field => $"{field.Name}:{field.FieldType.FullName}")
|
|
.ToArray();
|
|
|
|
foreach (string field in fields)
|
|
foreach (string forbidden in forbiddenFragments)
|
|
Assert.DoesNotContain(forbidden, field, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
private static LandblockBuild BuildWithShell(
|
|
uint landblockId,
|
|
params WorldEntity[] entities)
|
|
{
|
|
uint cellId = (landblockId & 0xFFFF0000u) | 0x0100u;
|
|
var shell = new EnvCellShellPlacement(
|
|
cellId,
|
|
0x2_0000_0001UL,
|
|
0x0D000001u,
|
|
1,
|
|
ImmutableArray<ushort>.Empty,
|
|
Vector3.Zero,
|
|
Quaternion.Identity,
|
|
Matrix4x4.Identity,
|
|
new WbBoundingBox(Vector3.Zero, Vector3.One),
|
|
new WbBoundingBox(Vector3.Zero, Vector3.One));
|
|
var envCells = new EnvCellLandblockBuild(
|
|
landblockId,
|
|
Array.Empty<LoadedCell>(),
|
|
[shell]);
|
|
return new LandblockBuild(
|
|
new LoadedLandblock(
|
|
landblockId,
|
|
new LandBlock(),
|
|
entities,
|
|
PhysicsDatBundle.Empty),
|
|
envCells);
|
|
}
|
|
|
|
private static WorldEntity Entity(uint id) => new()
|
|
{
|
|
Id = id,
|
|
SourceGfxObjOrSetupId = 0x01000000u + id,
|
|
Position = Vector3.Zero,
|
|
Rotation = Quaternion.Identity,
|
|
MeshRefs = Array.Empty<MeshRef>(),
|
|
};
|
|
|
|
private static LandblockMeshData EmptyMesh() => new(
|
|
Array.Empty<TerrainVertex>(),
|
|
Array.Empty<uint>());
|
|
|
|
private static IReadOnlyList<LandblockStreamResult> Drain(
|
|
Queue<LandblockStreamResult> queue,
|
|
int max)
|
|
{
|
|
var drained = new List<LandblockStreamResult>(max);
|
|
while (drained.Count < max && queue.TryDequeue(out LandblockStreamResult? result))
|
|
drained.Add(result);
|
|
return drained;
|
|
}
|
|
|
|
private sealed class RecordingMeshAdapter(List<string> calls) : IWbMeshAdapter
|
|
{
|
|
public void IncrementRefCount(ulong id)
|
|
{
|
|
}
|
|
|
|
public void PinPreparedRenderData(ulong id) => calls.Add("pin");
|
|
|
|
public void DecrementRefCount(ulong id)
|
|
{
|
|
}
|
|
}
|
|
|
|
private sealed class FailFirstPreparedPinAdapter : IWbMeshAdapter
|
|
{
|
|
public int PinAttempts { get; private set; }
|
|
|
|
public void IncrementRefCount(ulong id)
|
|
{
|
|
}
|
|
|
|
public void PinPreparedRenderData(ulong id)
|
|
{
|
|
PinAttempts++;
|
|
if (PinAttempts == 1)
|
|
throw new InvalidOperationException("injected pin failure");
|
|
}
|
|
|
|
public void DecrementRefCount(ulong id)
|
|
{
|
|
}
|
|
}
|
|
}
|