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; namespace AcDream.App.Tests.Streaming; public sealed class StreamingWorkBudgetTests { private static StreamingWorkBudget Budget( double milliseconds = 2, int completions = 4, long cpuBytes = 100, int entityOperations = 8, long gpuBytes = 100, int retireOperations = 4) => new( TimeSpan.FromMilliseconds(milliseconds), completions, cpuBytes, entityOperations, gpuBytes, retireOperations, 0.75f); [Fact] public void MeterYieldsBeforeSecondOperationThatExceedsAnyDimension() { long now = 0; var meter = new StreamingWorkMeter( Budget(), () => now, timestampFrequency: 1_000); Assert.Equal( StreamingWorkAdmission.Admitted, meter.TryReserve( new StreamingWorkCost( CompletionAdmissions: 2, AdoptedCpuBytes: 60, EntityOperations: 3, GpuUploadBytes: 40, GlRetireOperations: 1), "first")); meter.Complete(); Assert.Equal( StreamingWorkAdmission.Yielded, meter.TryReserve( new StreamingWorkCost(AdoptedCpuBytes: 41), "second")); StreamingWorkMeterSnapshot snapshot = meter.Snapshot; Assert.Equal(1, snapshot.Operations); Assert.Equal(1, snapshot.CompletedOperations); Assert.Equal(1, snapshot.YieldCount); Assert.Equal(StreamingWorkLimit.AdoptedCpuBytes, snapshot.LastLimit); Assert.Equal("second", snapshot.LastStage); } [Fact] public void FirstOversizedOperationProgressesAndNamesTheOversize() { var meter = new StreamingWorkMeter( Budget(completions: 1), static () => 0, timestampFrequency: 1_000); Assert.Equal( StreamingWorkAdmission.OversizedProgress, meter.TryReserve( new StreamingWorkCost(CompletionAdmissions: 2), "oversized")); meter.Complete(); StreamingWorkMeterSnapshot snapshot = meter.Snapshot; Assert.Equal(1, snapshot.OversizedProgressCount); Assert.Equal( StreamingWorkLimit.CompletionAdmissions, snapshot.LastLimit); } [Fact] public void EnsureProgressCanCrossTheLimitOnlyOncePerFrame() { var meter = new StreamingWorkMeter( Budget(cpuBytes: 1), static () => 0, timestampFrequency: 1_000); Assert.Equal( StreamingWorkAdmission.Admitted, meter.TryReserve( new StreamingWorkCost(CompletionAdmissions: 1), "admission")); meter.Complete(); Assert.Equal( StreamingWorkAdmission.Admitted, meter.TryReserve( new StreamingWorkCost(AdoptedCpuBytes: 1), "first-execution", ensureProgress: true)); meter.Complete(); Assert.Equal( StreamingWorkAdmission.Yielded, meter.TryReserve( new StreamingWorkCost(AdoptedCpuBytes: 1), "second-execution", ensureProgress: true)); Assert.Equal(1, meter.Snapshot.YieldCount); Assert.Equal( StreamingWorkLimit.AdoptedCpuBytes, meter.Snapshot.LastLimit); } [Fact] public void ActiveReveal_ProtectsDestinationShareAcrossEveryCountAndByteDimension() { var meter = new StreamingWorkMeter( Budget( milliseconds: 100, completions: 4, cpuBytes: 100, entityOperations: 8, gpuBytes: 100, retireOperations: 4), static () => 0, timestampFrequency: 1_000, destinationReservationActive: true); StreamingWorkCost unreservedShare = new( CompletionAdmissions: 1, AdoptedCpuBytes: 25, EntityOperations: 2, GpuUploadBytes: 25, GlRetireOperations: 1); Assert.Equal( StreamingWorkAdmission.Admitted, meter.TryReserve(unreservedShare, "ordinary")); meter.Complete(); Assert.Equal( StreamingWorkAdmission.Yielded, meter.TryReserve( new StreamingWorkCost(CompletionAdmissions: 1), "ordinary-over-reserve")); using (meter.EnterLane(StreamingWorkLane.Destination)) { Assert.Equal( StreamingWorkAdmission.Admitted, meter.TryReserve( new StreamingWorkCost( CompletionAdmissions: 3, AdoptedCpuBytes: 75, EntityOperations: 6, GpuUploadBytes: 75, GlRetireOperations: 3), "destination")); meter.Complete(); } StreamingWorkMeterSnapshot snapshot = meter.Snapshot; Assert.Equal(unreservedShare, snapshot.NonDestinationUsed); Assert.Equal(3, snapshot.DestinationUsed.CompletionAdmissions); Assert.Equal(100, snapshot.Used.AdoptedCpuBytes); Assert.Equal(100, snapshot.Used.GpuUploadBytes); } [Fact] public void InactiveReveal_DoesNotApplyDestinationReservation() { var meter = new StreamingWorkMeter( Budget(completions: 4), static () => 0, timestampFrequency: 1_000, destinationReservationActive: false); Assert.Equal( StreamingWorkAdmission.Admitted, meter.TryReserve( new StreamingWorkCost(CompletionAdmissions: 4), "ordinary")); meter.Complete(); Assert.Equal(4, meter.Snapshot.NonDestinationUsed.CompletionAdmissions); Assert.Equal(0, meter.Snapshot.DestinationUsed.CompletionAdmissions); } [Fact] public void ActiveReveal_ProtectsDestinationWallClockAfterOrdinaryShareIsSpent() { long now = 0; var meter = new StreamingWorkMeter( Budget(milliseconds: 100), () => now, timestampFrequency: 1_000, destinationReservationActive: true); Assert.Equal( StreamingWorkAdmission.Admitted, meter.TryReserve(default, "ordinary")); now = 25; meter.Complete(); Assert.Equal( StreamingWorkAdmission.Yielded, meter.TryReserve(default, "ordinary-after-25ms")); using (meter.EnterLane(StreamingWorkLane.Destination)) { Assert.Equal( StreamingWorkAdmission.Admitted, meter.TryReserve(default, "destination")); now = 99; meter.Complete(); } Assert.Equal(2, meter.Snapshot.CompletedOperations); } [Fact] public void MeterUsesInjectedMonotonicClockAndRecordsOverrunAndFailure() { long now = 0; var meter = new StreamingWorkMeter( Budget(milliseconds: 2), () => now, timestampFrequency: 1_000); Assert.Equal( StreamingWorkAdmission.Admitted, meter.TryReserve(new StreamingWorkCost(), "slow")); now = 3; meter.Fail(); StreamingWorkMeterSnapshot snapshot = meter.Snapshot; Assert.Equal(3, snapshot.ElapsedMilliseconds); Assert.Equal(1, snapshot.OverrunCount); Assert.Equal(1, snapshot.FailureCount); Assert.Equal(0, snapshot.CompletedOperations); Assert.Equal(3, snapshot.MaximumOperationMilliseconds); Assert.Equal("slow", snapshot.MaximumOperationStage); Assert.Equal("slow", snapshot.LastStage); Assert.Equal(StreamingWorkLimit.Time, snapshot.LastLimit); } [Fact] public void MeterRejectsReentrantReservationAndNegativeCost() { var meter = new StreamingWorkMeter( Budget(), static () => 0, timestampFrequency: 1_000); meter.TryReserve(new StreamingWorkCost(), "first"); Assert.Throws(() => meter.TryReserve(new StreamingWorkCost(), "reentrant")); meter.Complete(); Assert.Throws(() => meter.TryReserve( new StreamingWorkCost(AdoptedCpuBytes: -1), "negative")); Assert.Throws(() => new StreamingWorkMeter( default, static () => 0, timestampFrequency: 1_000)); } [Theory] [InlineData(2, 1.0, 2, 50, 4, 50, 2)] [InlineData(4, 2.0, 4, 100, 8, 100, 4)] [InlineData(6, 3.0, 6, 150, 12, 150, 6)] public void LegacyCompletionSelectorScalesTheWholeProfile( int count, double milliseconds, int admissions, long cpuBytes, int entityOperations, long gpuBytes, int retireOperations) { var options = new StreamingWorkBudgetOptions( MaxUpdateMilliseconds: 2, MaxCompletionAdmissions: 4, MaxAdoptedCpuBytes: 100, MaxEntityOperations: 8, MaxGpuUploadBytes: 100, MaxGlRetireOperations: 4, DestinationReserveFraction: 0.75f); StreamingWorkBudgetOptions scaled = options.ScaleForLegacyCompletionCount(count); Assert.Equal(milliseconds, scaled.MaxUpdateMilliseconds); Assert.Equal(admissions, scaled.MaxCompletionAdmissions); Assert.Equal(cpuBytes, scaled.MaxAdoptedCpuBytes); Assert.Equal(entityOperations, scaled.MaxEntityOperations); Assert.Equal(gpuBytes, scaled.MaxGpuUploadBytes); Assert.Equal(retireOperations, scaled.MaxGlRetireOperations); Assert.Equal(0.75f, scaled.DestinationReserveFraction); } [Fact] public void LegacyCompletionSelectorRejectsZeroAndSaturatesHugeProfiles() { var options = new StreamingWorkBudgetOptions( MaxUpdateMilliseconds: 2, MaxCompletionAdmissions: int.MaxValue, MaxAdoptedCpuBytes: long.MaxValue, MaxEntityOperations: int.MaxValue, MaxGpuUploadBytes: long.MaxValue, MaxGlRetireOperations: int.MaxValue, DestinationReserveFraction: 0.75f); Assert.Throws(() => options.ScaleForLegacyCompletionCount(0)); StreamingWorkBudgetOptions scaled = options.ScaleForLegacyCompletionCount(int.MaxValue); Assert.Equal(int.MaxValue, scaled.MaxCompletionAdmissions); Assert.Equal(long.MaxValue, scaled.MaxAdoptedCpuBytes); Assert.Equal(int.MaxValue, scaled.MaxEntityOperations); Assert.Equal(long.MaxValue, scaled.MaxGpuUploadBytes); Assert.Equal(int.MaxValue, scaled.MaxGlRetireOperations); } [Fact] public void CompletionCostChargesExactArraysAndDeterministicLogicalEntries() { const uint landblockId = 0xA9B4FFFFu; var first = Entity(1, meshRefs: 2); var second = Entity(2, meshRefs: 1); var cell = new LoadedCell { CellId = 0xA9B40100u, Portals = [ new CellPortalInfo(1, 2, 3, 4), new CellPortalInfo(5, 6, 7, 8), ], ClipPlanes = [new PortalClipPlane(), new PortalClipPlane()], PortalPolygons = [ new Vector3[3], new Vector3[4], ], VisibleCells = new uint[3], }; var shell = new EnvCellShellPlacement( cell.CellId, 1, 2, 3, ImmutableArray.Create(4, 5, 6), Vector3.Zero, Quaternion.Identity, Matrix4x4.Identity, new WbBoundingBox(Vector3.Zero, Vector3.One), new WbBoundingBox(Vector3.Zero, Vector3.One)); var physics = new PhysicsDatBundle( new LandBlockInfo(), new Dictionary { [1] = new EnvCell() }, new Dictionary { [2] = new DatReaderWriter.DBObjs.Environment(), }, new Dictionary { [3] = new Setup() }, new Dictionary { [4] = new GfxObj() }); var build = new LandblockBuild( new LoadedLandblock( landblockId, new LandBlock(), [first, second], physics), new EnvCellLandblockBuild(landblockId, [cell], [shell])); var mesh = new LandblockMeshData( new TerrainVertex[2], new uint[3]); LandblockStreamCostEstimate estimate = LandblockStreamResultCost.Estimate(build, mesh); Assert.Equal(92, estimate.TerrainPayloadBytes); // MeshRef is 80 bytes on the supported x64 runtime: the Matrix4x4 // retains its 16-byte alignment after the 4-byte GfxObj id. Assert.Equal(586, estimate.Work.AdoptedCpuBytes); Assert.Equal(1, estimate.Work.CompletionAdmissions); Assert.Equal(2, estimate.Work.EntityOperations); Assert.Equal(92, estimate.Work.GpuUploadBytes); Assert.Equal(3, estimate.MeshReferences); Assert.Equal(1, estimate.VisibilityCells); Assert.Equal(1, estimate.EnvCellShells); Assert.Equal(2, estimate.PortalConnections); Assert.Equal(7, estimate.PortalPolygonVertices); Assert.Equal(1, estimate.PhysicsEnvCells); Assert.Equal(1, estimate.PhysicsEnvironments); Assert.Equal(1, estimate.PhysicsSetups); Assert.Equal(1, estimate.PhysicsGfxObjects); } [Fact] public void NonPayloadCompletionCostsOnlyOneAdmission() { LandblockStreamCostEstimate estimate = LandblockStreamResultCost.Estimate( new LandblockStreamResult.Failed(0x1234FFFFu, "expected")); Assert.Equal( new StreamingWorkCost(CompletionAdmissions: 1), estimate.Work); Assert.Equal(0, estimate.TerrainPayloadBytes); } [Fact] public void LegacySchedulerPublishesShadowBudgetAndRetainedBacklogFacts() { uint center = StreamingRegion.EncodeLandblockIdForTest(32, 32); uint neighbor = StreamingRegion.EncodeLandblockIdForTest(32, 33); var outbox = new Queue( [ Loaded(center), Loaded(neighbor), ]); var controller = new StreamingController( enqueueLoad: static (_, _) => { }, enqueueUnload: static _ => { }, drainCompletions: max => { var drained = new List(max); while (drained.Count < max && outbox.TryDequeue(out var result)) drained.Add(result); return drained; }, applyTerrain: static (_, _) => { }, state: new GpuWorldState(), nearRadius: 1, farRadius: 1, workBudgetOptions: new StreamingWorkBudgetOptions( MaxUpdateMilliseconds: 100, MaxCompletionAdmissions: 2, MaxAdoptedCpuBytes: 1_000, MaxEntityOperations: 100, MaxGpuUploadBytes: 44, MaxGlRetireOperations: 100, DestinationReserveFraction: 0.75f)); controller.Tick(32, 32); StreamingWorkDiagnostics first = controller.WorkDiagnostics; Assert.Equal(1, first.DeferredCompletions); Assert.Equal(44, first.DeferredAdoptedCpuBytes); Assert.True(first.OldestDeferredAgeMilliseconds >= 0); 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); controller.Tick(32, 32); StreamingWorkDiagnostics second = controller.WorkDiagnostics; Assert.Equal(0, second.DeferredCompletions); Assert.Equal(0, second.DeferredAdoptedCpuBytes); Assert.Equal(0, second.OldestDeferredAgeMilliseconds); Assert.Equal(4, second.LastFrame.CompletedOperations); } [Fact] public void ControllerPublishesLifetimeOversizeAndMaximumOperationEvidence() { uint center = StreamingRegion.EncodeLandblockIdForTest(32, 32); var source = new QueueCompletionSource(Loaded(center)); StreamingController controller = Controller( source, static () => { }, WorkOptions( admissions: 4, cpuBytes: 1_000, gpuBytes: 1)); controller.Tick(32, 32); StreamingWorkDiagnostics diagnostics = controller.WorkDiagnostics; Assert.True(diagnostics.LifetimeOversizedProgressCount >= 1); Assert.True(diagnostics.MaximumOperationMilliseconds >= 0); Assert.NotNull(diagnostics.MaximumOperationStage); Assert.True(diagnostics.MaximumFrameMilliseconds >= diagnostics.MaximumOperationMilliseconds); } [Fact] public void ControllerBoundsProductionAdmissionByCountBeforeAdoption() { uint center = StreamingRegion.EncodeLandblockIdForTest(32, 32); var source = new QueueCompletionSource( Loaded(center), Loaded(StreamingRegion.EncodeLandblockIdForTest(32, 33)), Loaded(StreamingRegion.EncodeLandblockIdForTest(32, 34))); int applied = 0; StreamingController controller = Controller( source, () => applied++, WorkOptions(admissions: 2, cpuBytes: 1_000)); controller.Tick(32, 32); Assert.Equal(2, applied); Assert.Equal(1, source.BacklogCount); Assert.Equal(1, controller.WorkDiagnostics.WorkerCompletionBacklog); Assert.Equal(2, controller.WorkDiagnostics.LastFrame.Used.CompletionAdmissions); Assert.Equal(0, controller.WorkDiagnostics.DeferredCompletions); } [Fact] public void ControllerBoundsProductionAdmissionByRetainedCpuBytes() { uint center = StreamingRegion.EncodeLandblockIdForTest(32, 32); var source = new QueueCompletionSource( Loaded(center), Loaded(StreamingRegion.EncodeLandblockIdForTest(32, 33)), Loaded(StreamingRegion.EncodeLandblockIdForTest(32, 34))); int applied = 0; StreamingController controller = Controller( source, () => applied++, WorkOptions(admissions: 64, cpuBytes: 44)); controller.Tick(32, 32); Assert.Equal(1, applied); Assert.Equal(2, source.BacklogCount); Assert.Equal(44, controller.WorkDiagnostics.LastFrame.Used.AdoptedCpuBytes); Assert.Equal( StreamingWorkLimit.AdoptedCpuBytes, controller.WorkDiagnostics.LastFrame.LastLimit); } [Fact] public void DestinationAndEmptyUnloadPriorityNeverBypassPublicationBudget() { uint center = StreamingRegion.EncodeLandblockIdForTest(32, 32); uint ordinary = StreamingRegion.EncodeLandblockIdForTest(33, 33); var source = new QueueCompletionSource( Loaded(ordinary), Loaded(center), new LandblockStreamResult.Unloaded(0x1111FFFFu), new LandblockStreamResult.Unloaded(0x2222FFFFu)); var applied = new List(); StreamingController controller = Controller( source, id => applied.Add(id), WorkOptions( admissions: 4, cpuBytes: 1_000, gpuBytes: 44, retireOperations: 1)); controller.BeginDestinationReservation(1, center, 0); controller.Tick(32, 32); Assert.Equal([center], applied); Assert.Equal(1, controller.WorkDiagnostics.DeferredCompletions); Assert.Equal(2, controller.WorkDiagnostics.LastFrame.YieldCount); Assert.Equal(44, controller.WorkDiagnostics.LastFrame.Used.GpuUploadBytes); Assert.Equal(0, controller.WorkDiagnostics.LastFrame.Used.GlRetireOperations); controller.Tick(32, 32); Assert.Equal([center, ordinary], applied); Assert.Equal(0, controller.WorkDiagnostics.LastFrame.Used.GlRetireOperations); for (int i = 0; i < 4 && (controller.WorkDiagnostics.DeferredCompletions != 0 || source.BacklogCount != 0); i++) { controller.Tick(32, 32); } Assert.Equal(0, controller.WorkDiagnostics.DeferredCompletions); Assert.Equal(0, source.BacklogCount); } [Fact] public void StaleGenerationResultsConsumeAdmissionButRetainNoPayload() { uint center = StreamingRegion.EncodeLandblockIdForTest(32, 32); var source = new QueueCompletionSource( Loaded(center, generation: 1), Loaded(StreamingRegion.EncodeLandblockIdForTest(32, 33), generation: 1), Loaded(center)); int applied = 0; StreamingController controller = Controller( source, () => applied++, WorkOptions(admissions: 2, cpuBytes: 44)); controller.Tick(32, 32); Assert.Equal(0, applied); Assert.Equal(1, source.BacklogCount); Assert.Equal(0, controller.WorkDiagnostics.DeferredCompletions); Assert.Equal(0, controller.WorkDiagnostics.DeferredAdoptedCpuBytes); Assert.Equal(2, controller.WorkDiagnostics.LastFrame.Used.CompletionAdmissions); Assert.Equal(0, controller.WorkDiagnostics.LastFrame.Used.AdoptedCpuBytes); controller.Tick(32, 32); Assert.Equal(1, applied); } private static StreamingController Controller( ILandblockCompletionSource source, Action applied, StreamingWorkBudgetOptions options) => Controller(source, _ => applied(), options); private static StreamingController Controller( ILandblockCompletionSource source, Action applied, StreamingWorkBudgetOptions options) { var state = new GpuWorldState(); var presentation = new LandblockPresentationPipeline( (build, _) => applied(build.Landblock.LandblockId), state); return new StreamingController( enqueueLoad: static (_, _, _) => { }, enqueueUnload: static (_, _) => { }, completionSource: source, state, nearRadius: 1, farRadius: 1, presentationPipeline: presentation, workBudgetOptions: options); } private static StreamingWorkBudgetOptions WorkOptions( int admissions, long cpuBytes, long gpuBytes = 1_000, int retireOperations = 100) => new( MaxUpdateMilliseconds: 100, MaxCompletionAdmissions: admissions, MaxAdoptedCpuBytes: cpuBytes, MaxEntityOperations: 100, MaxGpuUploadBytes: gpuBytes, MaxGlRetireOperations: retireOperations, DestinationReserveFraction: 0.75f); private static WorldEntity Entity(uint id, int meshRefs) { var refs = new MeshRef[meshRefs]; for (int i = 0; i < refs.Length; i++) refs[i] = new MeshRef((uint)(0x01000000 + i), Matrix4x4.Identity); return new WorldEntity { Id = id, SourceGfxObjOrSetupId = 0x01000000u + id, Position = Vector3.Zero, Rotation = Quaternion.Identity, MeshRefs = refs, }; } private static LandblockStreamResult.Loaded Loaded( uint landblockId, ulong generation = 0) => new( landblockId, LandblockStreamTier.Near, new LoadedLandblock( landblockId, new LandBlock(), Array.Empty()), new LandblockMeshData( new TerrainVertex[1], new uint[1]), generation); private sealed class QueueCompletionSource( params LandblockStreamResult[] results) : ILandblockCompletionSource { private readonly Queue _results = new(results); public int BacklogCount => _results.Count; public bool TryPeek(out LandblockStreamResult? result) { if (_results.TryPeek(out LandblockStreamResult? peeked)) { result = peeked; return true; } result = null; return false; } public bool TryRead(out LandblockStreamResult? result) { if (_results.TryDequeue(out LandblockStreamResult? consumed)) { result = consumed; return true; } result = null; return false; } } }