refactor(streaming): own landblock publication transactions
This commit is contained in:
parent
4001251064
commit
4f965d0699
5 changed files with 1301 additions and 122 deletions
|
|
@ -0,0 +1,711 @@
|
|||
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 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();
|
||||
LandblockBuild source = BuildWithShell(landblockId, Entity(9));
|
||||
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.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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue