perf(streaming): cursor publication across frame budgets
This commit is contained in:
parent
bb16f74fd4
commit
98f1ac8934
32 changed files with 2215 additions and 823 deletions
|
|
@ -1,5 +1,6 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Types;
|
||||
|
|
@ -86,6 +87,43 @@ public class BuildingLoaderTests
|
|||
Assert.Equal(new SortedSet<uint> { 1, 2 }, ids); // sequential 1, 2
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RetainedPublication_AdvancesOneBuildingAndCommitsCellStampsAtomically()
|
||||
{
|
||||
LandBlockInfo info = MakeInfo(
|
||||
(0x02000001u, new[] { 0x0150u }),
|
||||
(0x02000002u, new[] { 0x0160u }));
|
||||
LoadedCell cell150 = MinimalCell(0xA9B40150u);
|
||||
LoadedCell cell160 = MinimalCell(0xA9B40160u);
|
||||
var cells = new Dictionary<uint, LoadedCell>
|
||||
{
|
||||
[cell150.CellId] = cell150,
|
||||
[cell160.CellId] = cell160,
|
||||
};
|
||||
|
||||
BuildingRegistryPublication publication =
|
||||
BuildingLoader.PreparePublication(info, 0xA9B40000u, cells);
|
||||
|
||||
Assert.False(publication.PreparationCommitted);
|
||||
Assert.False(BuildingLoader.AdvancePreparationOne(publication));
|
||||
Assert.Equal(1, publication.BuildingCursor);
|
||||
Assert.Equal(1, publication.Registry.Count);
|
||||
Assert.Null(cell150.BuildingId);
|
||||
Assert.Null(cell160.BuildingId);
|
||||
|
||||
Assert.True(BuildingLoader.AdvancePreparationOne(publication));
|
||||
Assert.Equal(2, publication.BuildingCursor);
|
||||
Assert.Equal(2, publication.Registry.Count);
|
||||
Assert.Null(cell150.BuildingId);
|
||||
Assert.Null(cell160.BuildingId);
|
||||
|
||||
BuildingLoader.CommitPublication(publication);
|
||||
|
||||
Assert.Equal(1u, cell150.BuildingId);
|
||||
Assert.Equal(2u, cell160.BuildingId);
|
||||
Assert.True(publication.PublicationCommitted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_StampsLoadedCellBuildingId()
|
||||
{
|
||||
|
|
@ -166,4 +204,16 @@ public class BuildingLoaderTests
|
|||
Assert.Equal(new Vector3(9, 12, 33), b.PortalBounds.Min);
|
||||
Assert.Equal(new Vector3(17, 25, 39), b.PortalBounds.Max);
|
||||
}
|
||||
|
||||
private static LoadedCell MinimalCell(uint cellId) => new()
|
||||
{
|
||||
CellId = cellId,
|
||||
Portals = new List<AcDream.App.Rendering.CellPortalInfo>(),
|
||||
PortalPolygons = new List<Vector3[]>(),
|
||||
WorldTransform = Matrix4x4.Identity,
|
||||
InverseWorldTransform = Matrix4x4.Identity,
|
||||
LocalBoundsMin = new Vector3(-5, -5, -5),
|
||||
LocalBoundsMax = new Vector3(5, 5, 5),
|
||||
ClipPlanes = new List<AcDream.App.Rendering.PortalClipPlane>(),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,73 @@ public sealed class LandblockConcretePresentationPipelineTests
|
|||
Assert.Equal(1, fixture.Static.Diagnostics.CompleteCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MeteredLoaded_RetainsExactHeadAndPublishesSpatiallyOnlyAfterOwnerSuffix()
|
||||
{
|
||||
var calls = new List<string>();
|
||||
ConcreteFixture fixture = Fixture(
|
||||
calls,
|
||||
commitEnvCells: _ => calls.Add("envcell"));
|
||||
fixture.Events.EntitySpawned += _ => calls.Add("plugin");
|
||||
var pipeline = new LandblockPresentationPipeline(
|
||||
fixture.Render,
|
||||
fixture.Physics,
|
||||
fixture.Static,
|
||||
fixture.State,
|
||||
fixture.RetirementOwner,
|
||||
onLandblockLoaded: _ => calls.Add("live-recovery"));
|
||||
LandblockStreamResult.Loaded result =
|
||||
Result(Build(Entity(0x80A9B401u), Entity(0x80A9B402u)));
|
||||
LandblockStreamCostEstimate estimate =
|
||||
LandblockStreamResultCost.Estimate(result);
|
||||
var budget = new StreamingWorkBudget(
|
||||
TimeSpan.FromSeconds(1),
|
||||
maxCompletionAdmissions: 64,
|
||||
maxAdoptedCpuBytes: 1_000_000,
|
||||
maxEntityOperations: 1,
|
||||
maxGpuUploadBytes: 1_000_000,
|
||||
maxGlRetireOperations: 64,
|
||||
destinationReserveFraction: 0.75f);
|
||||
|
||||
LandblockPublicationAdvance advance = default;
|
||||
for (int frame = 0; frame < 64; frame++)
|
||||
{
|
||||
var meter = new StreamingWorkMeter(budget);
|
||||
advance = frame == 0
|
||||
? pipeline.PublishLoaded(
|
||||
result,
|
||||
estimate,
|
||||
meter,
|
||||
ensureProgress: false)
|
||||
: pipeline.ResumePublication(
|
||||
result,
|
||||
meter,
|
||||
ensureProgress: false);
|
||||
meter.FinishFrame();
|
||||
|
||||
Assert.True(
|
||||
meter.Snapshot.Used.EntityOperations <= 1,
|
||||
$"stage={meter.Snapshot.LastStage}, " +
|
||||
$"entities={meter.Snapshot.Used.EntityOperations}, " +
|
||||
$"oversized={meter.Snapshot.OversizedProgressCount}");
|
||||
if (fixture.Static.Diagnostics.CompleteCount == 0)
|
||||
Assert.False(fixture.State.IsLoaded(LandblockId));
|
||||
if (advance.Completed)
|
||||
break;
|
||||
}
|
||||
|
||||
Assert.True(advance.Completed);
|
||||
Assert.False(pipeline.HasPendingPublication(result));
|
||||
Assert.True(fixture.State.IsNearTier(LandblockId));
|
||||
Assert.Equal(
|
||||
["terrain", "envcell", "plugin", "plugin", "pin", "live-recovery"],
|
||||
calls);
|
||||
Assert.Equal(1, fixture.Render.Diagnostics.BeginCount);
|
||||
Assert.Equal(1, fixture.Physics.Diagnostics.BeginCount);
|
||||
Assert.Equal(1, fixture.Physics.Diagnostics.CompleteCount);
|
||||
Assert.Equal(1, fixture.Static.Diagnostics.CompleteCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RenderSuffixFailure_ResumesConcreteReceiptsWithoutReplayingPrefixes()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1119,7 +1119,15 @@ public sealed class LandblockPresentationPipelineTests
|
|||
failReplayOnce = false;
|
||||
throw new InvalidOperationException("injected replay failure");
|
||||
}
|
||||
});
|
||||
},
|
||||
workBudgetOptions: new StreamingWorkBudgetOptions(
|
||||
MaxUpdateMilliseconds: 100,
|
||||
MaxCompletionAdmissions: 64,
|
||||
MaxAdoptedCpuBytes: 1_000_000,
|
||||
MaxEntityOperations: 1_000,
|
||||
MaxGpuUploadBytes: 1_000_000,
|
||||
MaxGlRetireOperations: 1_000,
|
||||
DestinationReserveFraction: 0.75f));
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => controller.Tick(0x90, 0x90));
|
||||
Assert.True(state.IsNearTier(landblockId));
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Numerics;
|
||||
using System.Reflection;
|
||||
using System.Collections.Immutable;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.App.Streaming;
|
||||
|
|
@ -194,6 +195,47 @@ public sealed class LandblockRenderPublisherTests
|
|||
Assert.Equal(1, first.Diagnostics.CompleteCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RetainedEnvCellPublisher_AdvancesOneShellBeforeAtomicCommit()
|
||||
{
|
||||
var envPublisher = new RecordingEnvCellPublisher();
|
||||
EnvCellLandblockBuild envCells = new(
|
||||
LandblockId,
|
||||
Array.Empty<LoadedCell>(),
|
||||
[
|
||||
Shell(0xA9B40100u, 1),
|
||||
Shell(0xA9B40101u, 2),
|
||||
Shell(0xA9B40102u, 3),
|
||||
]);
|
||||
var publisher = new LandblockRenderPublisher(
|
||||
publishTerrain: static (_, _, _) => { },
|
||||
removeTerrain: static _ => { },
|
||||
cellVisibility: new CellVisibility(),
|
||||
worldState: new GpuWorldState(),
|
||||
envCellPublisher: envPublisher);
|
||||
LandblockRenderPublication receipt = publisher.PreparePublication(
|
||||
Build(envCells),
|
||||
EmptyMesh());
|
||||
publisher.BeginPublication(receipt);
|
||||
|
||||
Assert.False(publisher.AdvanceCompleteOne(receipt));
|
||||
Assert.Equal(0, envPublisher.AdvancedShells);
|
||||
Assert.Equal(0, envPublisher.CommitCount);
|
||||
for (int shell = 1; shell <= 3; shell++)
|
||||
{
|
||||
Assert.False(publisher.AdvanceCompleteOne(receipt));
|
||||
Assert.Equal(shell, envPublisher.AdvancedShells);
|
||||
Assert.Equal(0, envPublisher.CommitCount);
|
||||
}
|
||||
|
||||
Assert.False(publisher.AdvanceCompleteOne(receipt));
|
||||
Assert.Equal(0, envPublisher.CommitCount);
|
||||
Assert.False(publisher.AdvanceCompleteOne(receipt));
|
||||
Assert.Equal(1, envPublisher.CommitCount);
|
||||
Assert.True(publisher.AdvanceCompleteOne(receipt));
|
||||
Assert.Equal(1, publisher.Diagnostics.CompleteCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PrepareAndRetirementOperationsStayBalancedAtTheirOwnerBoundary()
|
||||
{
|
||||
|
|
@ -341,6 +383,19 @@ public sealed class LandblockRenderPublisherTests
|
|||
InverseWorldTransform = Matrix4x4.Identity,
|
||||
};
|
||||
|
||||
private static EnvCellShellPlacement Shell(uint cellId, ulong geometryId) =>
|
||||
new(
|
||||
cellId,
|
||||
geometryId,
|
||||
EnvironmentId: 1u,
|
||||
CellStructure: 0,
|
||||
Surfaces: ImmutableArray<ushort>.Empty,
|
||||
WorldPosition: Vector3.Zero,
|
||||
Rotation: Quaternion.Identity,
|
||||
Transform: Matrix4x4.Identity,
|
||||
LocalBounds: new WbBoundingBox(Vector3.Zero, Vector3.One),
|
||||
WorldBounds: new WbBoundingBox(Vector3.Zero, Vector3.One));
|
||||
|
||||
private static LandblockMeshData MeshAtHeights(float first, float second) => new(
|
||||
[
|
||||
new TerrainVertex(new Vector3(0f, 0f, first), Vector3.UnitZ, 0, 0, 0, 0),
|
||||
|
|
@ -362,4 +417,42 @@ public sealed class LandblockRenderPublisherTests
|
|||
}
|
||||
throw new DirectoryNotFoundException("Could not locate repository root.");
|
||||
}
|
||||
|
||||
private sealed class RecordingEnvCellPublisher :
|
||||
IEnvCellLandblockPublisher
|
||||
{
|
||||
private readonly object _owner = new();
|
||||
|
||||
public int AdvancedShells { get; private set; }
|
||||
public int CommitCount { get; private set; }
|
||||
|
||||
public EnvCellLandblockPublication PreparePublication(
|
||||
EnvCellLandblockBuild build) =>
|
||||
new(_owner, build);
|
||||
|
||||
public bool AdvancePreparationOne(
|
||||
EnvCellLandblockPublication publication)
|
||||
{
|
||||
Assert.Same(_owner, publication.Owner);
|
||||
if (publication.PreparationCommitted)
|
||||
return true;
|
||||
if (publication.ShellCursor < publication.Build.Shells.Length)
|
||||
{
|
||||
publication.ShellCursor++;
|
||||
AdvancedShells++;
|
||||
return false;
|
||||
}
|
||||
|
||||
publication.PreparationCommitted = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void CommitPublication(
|
||||
EnvCellLandblockPublication publication)
|
||||
{
|
||||
Assert.True(publication.PreparationCommitted);
|
||||
publication.PublicationCommitted = true;
|
||||
CommitCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -507,7 +507,16 @@ public sealed class LandblockRetirementCoordinatorTests
|
|||
nearRadius: 0,
|
||||
farRadius: 0,
|
||||
retirementCoordinator: coordinator,
|
||||
workBudgetOptions: workBudgetOptions);
|
||||
workBudgetOptions: workBudgetOptions ?? GenerousWorkBudget());
|
||||
|
||||
private static StreamingWorkBudgetOptions GenerousWorkBudget() => new(
|
||||
MaxUpdateMilliseconds: 100,
|
||||
MaxCompletionAdmissions: 64,
|
||||
MaxAdoptedCpuBytes: 64 * StreamingWorkBudgetOptions.MiB,
|
||||
MaxEntityOperations: 1_024,
|
||||
MaxGpuUploadBytes: 64 * StreamingWorkBudgetOptions.MiB,
|
||||
MaxGlRetireOperations: 256,
|
||||
DestinationReserveFraction: 0.75f);
|
||||
|
||||
private static IReadOnlyList<LandblockStreamResult> Drain(
|
||||
Queue<LandblockStreamResult> pending,
|
||||
|
|
|
|||
|
|
@ -238,7 +238,8 @@ public sealed class StreamingControllerReadinessTests
|
|||
ensureCalls++;
|
||||
});
|
||||
|
||||
controller.Tick(0x12, 0x36);
|
||||
for (int frame = 0; frame < 32 && ensureCalls == 0; frame++)
|
||||
controller.Tick(0x12, 0x36);
|
||||
|
||||
Assert.Equal(1, ensureCalls);
|
||||
}
|
||||
|
|
@ -294,7 +295,8 @@ public sealed class StreamingControllerReadinessTests
|
|||
ensureCalls++;
|
||||
});
|
||||
|
||||
controller.Tick(0x12, 0x36);
|
||||
for (int frame = 0; frame < 32 && ensureCalls == 0; frame++)
|
||||
controller.Tick(0x12, 0x36);
|
||||
|
||||
Assert.Equal(1, ensureCalls);
|
||||
Assert.Equal(1, meshes.ReferenceCounts[geometryId]);
|
||||
|
|
|
|||
|
|
@ -337,7 +337,7 @@ public sealed class StreamingFrameControllerTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void StreamingPublicationMakesSameFrameInboundProjectionResident()
|
||||
public void CompletedStreamingPublicationMakesInboundProjectionResident()
|
||||
{
|
||||
const uint landblock = 0x0A14FFFFu;
|
||||
const uint cell = 0x0A140001u;
|
||||
|
|
@ -359,7 +359,8 @@ public sealed class StreamingFrameControllerTests
|
|||
farRadius: 0);
|
||||
var fixture = new Fixture(streamingBackend: streaming);
|
||||
|
||||
fixture.Controller.Tick();
|
||||
for (int frame = 0; frame < 32 && !state.IsLoaded(landblock); frame++)
|
||||
fixture.Controller.Tick();
|
||||
Assert.True(state.IsLoaded(landblock));
|
||||
|
||||
var runtime = Runtime(state);
|
||||
|
|
|
|||
|
|
@ -347,8 +347,8 @@ public sealed class StreamingWorkBudgetTests
|
|||
Assert.Equal(1, first.DeferredCompletions);
|
||||
Assert.Equal(44, first.DeferredAdoptedCpuBytes);
|
||||
Assert.True(first.OldestDeferredAgeMilliseconds >= 0);
|
||||
Assert.Equal(3, first.LastFrame.Operations);
|
||||
Assert.Equal(3, first.LastFrame.CompletedOperations);
|
||||
Assert.Equal(6, first.LastFrame.Operations);
|
||||
Assert.Equal(6, first.LastFrame.CompletedOperations);
|
||||
Assert.Equal(1, first.LastFrame.YieldCount);
|
||||
Assert.Equal(88, first.LastFrame.Used.AdoptedCpuBytes);
|
||||
|
||||
|
|
@ -358,7 +358,7 @@ public sealed class StreamingWorkBudgetTests
|
|||
Assert.Equal(0, second.DeferredCompletions);
|
||||
Assert.Equal(0, second.DeferredAdoptedCpuBytes);
|
||||
Assert.Equal(0, second.OldestDeferredAgeMilliseconds);
|
||||
Assert.Equal(1, second.LastFrame.CompletedOperations);
|
||||
Assert.Equal(4, second.LastFrame.CompletedOperations);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -409,7 +409,7 @@ public sealed class StreamingWorkBudgetTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void DestinationAndUnloadPriorityNeverBypassExecutionBudgets()
|
||||
public void DestinationAndEmptyUnloadPriorityNeverBypassPublicationBudget()
|
||||
{
|
||||
uint center = StreamingRegion.EncodeLandblockIdForTest(32, 32);
|
||||
uint ordinary = StreamingRegion.EncodeLandblockIdForTest(33, 33);
|
||||
|
|
@ -432,16 +432,16 @@ public sealed class StreamingWorkBudgetTests
|
|||
controller.Tick(32, 32);
|
||||
|
||||
Assert.Equal([center], applied);
|
||||
Assert.Equal(2, controller.WorkDiagnostics.DeferredCompletions);
|
||||
Assert.Equal(1, controller.WorkDiagnostics.DeferredCompletions);
|
||||
Assert.Equal(1, controller.WorkDiagnostics.LastFrame.YieldCount);
|
||||
Assert.Equal(44, controller.WorkDiagnostics.LastFrame.Used.GpuUploadBytes);
|
||||
Assert.Equal(1, controller.WorkDiagnostics.LastFrame.Used.GlRetireOperations);
|
||||
Assert.Equal(0, controller.WorkDiagnostics.LastFrame.Used.GlRetireOperations);
|
||||
|
||||
controller.Tick(32, 32);
|
||||
|
||||
Assert.Equal([center, ordinary], applied);
|
||||
Assert.Equal(0, controller.WorkDiagnostics.DeferredCompletions);
|
||||
Assert.Equal(1, controller.WorkDiagnostics.LastFrame.Used.GlRetireOperations);
|
||||
Assert.Equal(0, controller.WorkDiagnostics.LastFrame.Used.GlRetireOperations);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -330,6 +330,65 @@ public class ShadowObjectRegistryTests
|
|||
entry => entry.EntityId == entityId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StreamingReceipts_AreStableOrderedAndAdvanceOneStaticOwner()
|
||||
{
|
||||
var reg = new ShadowObjectRegistry();
|
||||
reg.Register(
|
||||
9u,
|
||||
0x01000009u,
|
||||
new Vector3(12f, 12f, 50f),
|
||||
Quaternion.Identity,
|
||||
1f,
|
||||
OffX,
|
||||
OffY,
|
||||
LbId,
|
||||
seedCellId: LbId | 1u,
|
||||
isStatic: true);
|
||||
reg.Register(
|
||||
2u,
|
||||
0x01000002u,
|
||||
new Vector3(14f, 12f, 50f),
|
||||
Quaternion.Identity,
|
||||
1f,
|
||||
OffX,
|
||||
OffY,
|
||||
LbId,
|
||||
seedCellId: LbId | 1u,
|
||||
isStatic: true);
|
||||
reg.Register(
|
||||
5u,
|
||||
0x01000005u,
|
||||
new Vector3(16f, 12f, 50f),
|
||||
Quaternion.Identity,
|
||||
1f,
|
||||
OffX,
|
||||
OffY,
|
||||
LbId,
|
||||
seedCellId: LbId | 1u,
|
||||
isStatic: false);
|
||||
|
||||
Assert.Equal(
|
||||
[2u, 9u],
|
||||
reg.CaptureStaticOwnersForLandblock(LbId));
|
||||
Assert.Equal(
|
||||
[2u, 5u, 9u],
|
||||
reg.CaptureRefloodOwnersForLandblock(LbId));
|
||||
|
||||
reg.DeregisterStaticOwnerForLandblock(2u, LbId);
|
||||
|
||||
Assert.Equal(2, reg.RetainedRegistrationCount);
|
||||
Assert.DoesNotContain(
|
||||
reg.GetObjectsInCell(LbId | 1u),
|
||||
entry => entry.EntityId == 2u);
|
||||
Assert.Contains(
|
||||
reg.GetObjectsInCell(LbId | 1u),
|
||||
entry => entry.EntityId == 5u);
|
||||
Assert.Contains(
|
||||
reg.GetObjectsInCell(LbId | 1u),
|
||||
entry => entry.EntityId == 9u);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RefloodLandblock_RestoresAdjacentOwnedFootprintWithdrawnByUnload()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -372,7 +372,8 @@ public class StreamingControllerTests
|
|||
System.Array.Empty<uint>());
|
||||
fake.Pending.Enqueue(new LandblockStreamResult.Loaded(landblockId, LandblockStreamTier.Near, lb, stubMesh));
|
||||
|
||||
controller.Tick(50, 50);
|
||||
for (int frame = 0; frame < 32 && !state.IsLoaded(landblockId); frame++)
|
||||
controller.Tick(50, 50);
|
||||
|
||||
Assert.Single(applied);
|
||||
Assert.True(state.IsLoaded(landblockId));
|
||||
|
|
|
|||
|
|
@ -136,7 +136,15 @@ public class StreamingControllerTwoTierTests
|
|||
},
|
||||
state: state,
|
||||
nearRadius: 2,
|
||||
farRadius: 2);
|
||||
farRadius: 2,
|
||||
workBudgetOptions: new StreamingWorkBudgetOptions(
|
||||
MaxUpdateMilliseconds: 100,
|
||||
MaxCompletionAdmissions: 64,
|
||||
MaxAdoptedCpuBytes: 16 * StreamingWorkBudgetOptions.MiB,
|
||||
MaxEntityOperations: 512,
|
||||
MaxGpuUploadBytes: 16 * StreamingWorkBudgetOptions.MiB,
|
||||
MaxGlRetireOperations: 128,
|
||||
DestinationReserveFraction: 0.75f));
|
||||
|
||||
ctrl.Tick(50, 50); // drains the Promoted result
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue