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 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(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)); } [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) { MaxCompletionsPerFrame = 1, }; 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(1, first.LastFrame.Operations); Assert.Equal(1, first.LastFrame.CompletedOperations); Assert.Equal(44, 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(1, second.LastFrame.CompletedOperations); } 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) => new( landblockId, LandblockStreamTier.Near, new LoadedLandblock( landblockId, new LandBlock(), Array.Empty()), new LandblockMeshData( new TerrainVertex[1], new uint[1])); }