refactor(streaming): own landblock publication transactions

This commit is contained in:
Erik 2026-07-21 19:55:24 +02:00
parent 4001251064
commit 4f965d0699
5 changed files with 1301 additions and 122 deletions

View file

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