Add the validated frame-work profile, deterministic completion charges, and shadow admission meter before enforcing the scheduler in Slice E2. Lifecycle artifacts now expose elapsed work, would-yield limits, backlog bytes/age, and pending owner ledgers without changing accepted execution. Tests: dotnet build AcDream.slnx -c Release --no-restore; dotnet test AcDream.slnx -c Release --no-build --no-restore (8124 passed, 5 skipped)
291 lines
9.9 KiB
C#
291 lines
9.9 KiB
C#
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<InvalidOperationException>(() =>
|
|
meter.TryReserve(new StreamingWorkCost(), "reentrant"));
|
|
meter.Complete();
|
|
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
|
meter.TryReserve(
|
|
new StreamingWorkCost(AdoptedCpuBytes: -1),
|
|
"negative"));
|
|
Assert.Throws<ArgumentException>(() =>
|
|
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<ushort>(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<uint, EnvCell> { [1] = new EnvCell() },
|
|
new Dictionary<uint, DatReaderWriter.DBObjs.Environment>
|
|
{
|
|
[2] = new DatReaderWriter.DBObjs.Environment(),
|
|
},
|
|
new Dictionary<uint, Setup> { [3] = new Setup() },
|
|
new Dictionary<uint, GfxObj> { [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<LandblockStreamResult>(
|
|
[
|
|
Loaded(center),
|
|
Loaded(neighbor),
|
|
]);
|
|
var controller = new StreamingController(
|
|
enqueueLoad: static (_, _) => { },
|
|
enqueueUnload: static _ => { },
|
|
drainCompletions: max =>
|
|
{
|
|
var drained = new List<LandblockStreamResult>(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<WorldEntity>()),
|
|
new LandblockMeshData(
|
|
new TerrainVertex[1],
|
|
new uint[1]));
|
|
}
|