acdream/src/AcDream.App/Streaming/LandblockStreamResultCost.cs
Erik ac45cb1bd7 feat(streaming): establish cost budget ledger
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)
2026-07-24 17:26:34 +02:00

187 lines
6.4 KiB
C#

using System.Runtime.CompilerServices;
using AcDream.App.Rendering.Wb;
using AcDream.Core.Terrain;
using AcDream.Core.World;
namespace AcDream.App.Streaming;
/// <summary>
/// Deterministic scheduling charge derived from one immutable worker result.
/// It is not a claim about CLR object-header or allocator-bucket size: those
/// implementation details are neither stable nor observable without walking
/// the managed heap. Exact array payloads and logical retained entries are
/// charged consistently so admission has a deterministic unit.
/// </summary>
public readonly record struct LandblockStreamCostEstimate(
StreamingWorkCost Work,
long TerrainPayloadBytes,
int Entities,
int MeshReferences,
int VisibilityCells,
int EnvCellShells,
int PortalConnections,
int PortalPolygonVertices,
int PhysicsEnvCells,
int PhysicsEnvironments,
int PhysicsSetups,
int PhysicsGfxObjects);
public static class LandblockStreamResultCost
{
private const int ReferenceChargeBytes = 8;
private const int DictionaryEntryChargeBytes = 16;
public static LandblockStreamCostEstimate Estimate(
LandblockStreamResult result)
{
ArgumentNullException.ThrowIfNull(result);
return result switch
{
LandblockStreamResult.Loaded loaded =>
Estimate(loaded.Build, loaded.MeshData),
LandblockStreamResult.Promoted promoted =>
Estimate(promoted.Build, promoted.MeshData),
_ => new LandblockStreamCostEstimate(
new StreamingWorkCost(CompletionAdmissions: 1),
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0),
};
}
public static LandblockStreamCostEstimate Estimate(
LandblockBuild build,
LandblockMeshData meshData)
{
ArgumentNullException.ThrowIfNull(build);
ArgumentNullException.ThrowIfNull(meshData);
long terrainBytes = Add(
Multiply(meshData.Vertices.LongLength, Unsafe.SizeOf<TerrainVertex>()),
Multiply(meshData.Indices.LongLength, sizeof(uint)));
long retainedBytes = terrainBytes;
IReadOnlyList<WorldEntity> entities = build.Landblock.Entities;
int meshReferences = 0;
retainedBytes = Add(
retainedBytes,
Multiply(entities.Count, ReferenceChargeBytes));
for (int i = 0; i < entities.Count; i++)
{
int count = entities[i].MeshRefs.Count;
meshReferences = SaturatingAdd(meshReferences, count);
retainedBytes = Add(
retainedBytes,
Multiply(count, Unsafe.SizeOf<MeshRef>()));
}
int visibilityCells = 0;
int shells = 0;
int portalConnections = 0;
int portalPolygonVertices = 0;
if (build.EnvCells is { } envCells)
{
visibilityCells = envCells.VisibilityCells.Length;
shells = envCells.Shells.Length;
retainedBytes = Add(
retainedBytes,
Multiply(
(long)visibilityCells + shells,
ReferenceChargeBytes));
foreach (var cell in envCells.VisibilityCells)
{
portalConnections = SaturatingAdd(
portalConnections,
cell.Portals.Count);
retainedBytes = Add(
retainedBytes,
Multiply(
cell.Portals.Count,
Unsafe.SizeOf<Rendering.CellPortalInfo>()));
retainedBytes = Add(
retainedBytes,
Multiply(
cell.ClipPlanes.Count,
Unsafe.SizeOf<Rendering.PortalClipPlane>()));
retainedBytes = Add(
retainedBytes,
Multiply(
cell.VisibleCells.Count,
sizeof(uint)));
foreach (System.Numerics.Vector3[] polygon in cell.PortalPolygons)
{
portalPolygonVertices = SaturatingAdd(
portalPolygonVertices,
polygon.Length);
retainedBytes = Add(
retainedBytes,
Multiply(
polygon.LongLength,
Unsafe.SizeOf<System.Numerics.Vector3>()));
}
}
foreach (EnvCellShellPlacement shell in envCells.Shells)
{
retainedBytes = Add(
retainedBytes,
Multiply(shell.Surfaces.Length, sizeof(ushort)));
}
}
PhysicsDatBundle physics =
build.Landblock.PhysicsDats ?? PhysicsDatBundle.Empty;
int physicsEnvCells = physics.EnvCells.Count;
int physicsEnvironments = physics.Environments.Count;
int physicsSetups = physics.Setups.Count;
int physicsGfxObjects = physics.GfxObjs.Count;
long physicsEntries = (long)physicsEnvCells
+ physicsEnvironments
+ physicsSetups
+ physicsGfxObjects;
retainedBytes = Add(
retainedBytes,
Multiply(physicsEntries, DictionaryEntryChargeBytes));
return new LandblockStreamCostEstimate(
new StreamingWorkCost(
CompletionAdmissions: 1,
AdoptedCpuBytes: retainedBytes,
EntityOperations: entities.Count,
GpuUploadBytes: terrainBytes),
terrainBytes,
entities.Count,
meshReferences,
visibilityCells,
shells,
portalConnections,
portalPolygonVertices,
physicsEnvCells,
physicsEnvironments,
physicsSetups,
physicsGfxObjects);
}
private static long Multiply(long count, int elementBytes) =>
count <= 0
? 0
: count > long.MaxValue / elementBytes
? long.MaxValue
: count * elementBytes;
private static long Add(long left, long right) =>
left > long.MaxValue - right ? long.MaxValue : left + right;
private static int SaturatingAdd(int left, int right) =>
left > int.MaxValue - right ? int.MaxValue : left + right;
}