refactor(render): delete the packed dispatcher

This commit is contained in:
Erik 2026-08-31 04:44:22 +02:00
parent 8d8aa715ee
commit 73ba1d3c32
7 changed files with 115 additions and 1659 deletions

View file

@ -117,7 +117,6 @@ internal interface IRenderFrameEntityPassExecutor
/// </summary>
internal sealed partial class RetailPViewPassExecutor :
IRetailPViewPassExecutor,
IRenderFrameEntityPassExecutor,
IOutdoorSceneParticleOwnerSource
{
private readonly IWorldPassSurface _surface;
@ -214,30 +213,6 @@ internal sealed partial class RetailPViewPassExecutor :
internal (int Width, int Height)? WalkAttachmentExtent =>
_entities.WalkAttachmentExtent;
public void BeginEntityFrame(in RenderFrameView view) =>
_entities.BeginPackedProductionFrame(in view);
public bool DrawEntityRoute(
ICamera camera,
in RenderFrameView view,
RenderFrameCandidateRoute route,
int routeIndex,
uint cellId,
uint tupleLandblockId) =>
_entities.DrawPackedProductionRoute(
camera,
in view,
route,
routeIndex,
cellId,
tupleLandblockId);
public void CompleteEntityFrame(in RenderFrameView view) =>
_entities.CompletePackedProductionFrame(in view);
public void AbortEntityFrame() =>
_entities.AbortPackedProductionFrame();
public void AbortFrame()
{
List<Exception>? failures = null;
@ -406,18 +381,7 @@ internal sealed partial class RetailPViewPassExecutor :
_terrainDiagnostics.Complete();
DisableClipDistances();
if (context.EntityDraw is RenderFrameEntityDrawRequest request)
{
RenderFrameView drawView = request.View;
_entities.DrawPackedProductionRoute(
frame.Camera,
in drawView,
request.Route,
request.RouteIndex,
request.CellId,
request.TupleLandblockId);
}
else if (context.OutdoorEntities.Count > 0)
if (context.OutdoorEntities.Count > 0)
{
var sceneryEntry = (
frame.PlayerLandblockId ?? 0u,
@ -449,18 +413,7 @@ internal sealed partial class RetailPViewPassExecutor :
_surface.BindTerrainClip();
DisableClipDistances();
if (context.EntityDraw is RenderFrameEntityDrawRequest request)
{
RenderFrameView drawView = request.View;
_entities.DrawPackedProductionRoute(
frame.Camera,
in drawView,
request.Route,
request.RouteIndex,
request.CellId,
request.TupleLandblockId);
}
else if (context.Dynamics.Count > 0)
if (context.Dynamics.Count > 0)
{
var dynamicsEntry = (
frame.PlayerLandblockId ?? 0u,
@ -558,18 +511,7 @@ internal sealed partial class RetailPViewPassExecutor :
_surface.BindTerrainClip();
DisableClipDistances();
if (context.EntityDraw is RenderFrameEntityDrawRequest request)
{
RenderFrameView drawView = request.View;
_entities.DrawPackedProductionRoute(
frame.Camera,
in drawView,
request.Route,
request.RouteIndex,
request.CellId,
request.TupleLandblockId);
}
else if (context.BuildingShells.Count > 0)
if (context.BuildingShells.Count > 0)
{
var buildingEntry = (
frame.PlayerLandblockId ?? 0u,

View file

@ -1,251 +0,0 @@
using System.Numerics;
using System.Runtime.CompilerServices;
using AcDream.App.Rendering.Scene;
namespace AcDream.App.Rendering.Wb;
/// <summary>
/// The appearance-only identity of one retained scene projection. Root
/// transform, clip slot, light selection, and selection lighting deliberately
/// stay outside this key: G3 updates those instance fields without rebuilding
/// mesh/material classification.
/// </summary>
internal readonly record struct PackedClassificationIdentity(
RenderOwnerIncarnation Incarnation,
RenderMeshSet MeshSet,
RenderMaterialVariant Material,
RenderDegradeState DegradeState,
RenderSceneHash128 Geometry,
RenderSceneHash128 Appearance)
{
public static PackedClassificationIdentity From(
in RenderProjectionRecord projection) =>
new(
projection.OwnerIncarnation,
projection.MeshSet,
projection.Material,
projection.DegradeState,
projection.Source.GeometryFingerprint,
projection.Source.AppearanceFingerprint);
}
internal readonly record struct PackedClassifiedBatch(
GroupKey Key,
Matrix4x4 RestPose,
Vector3 LocalSortCenter);
internal readonly record struct PackedClassifiedSelectionPart(
int PartIndex,
uint GfxObjId,
Matrix4x4 RestPose);
/// <summary>
/// Reusable variable-size payload for one projection. Lists grow only when an
/// appearance first exposes a higher batch/part count and are cleared in
/// place on the next appearance rebuild.
/// </summary>
internal sealed class PackedProjectionClassificationEntry
{
public PackedClassificationIdentity Identity;
public readonly List<PackedClassifiedBatch> Batches = [];
public readonly List<PackedClassifiedSelectionPart> SelectionParts = [];
public long LastBuiltFrame;
public long LastSeenFrame;
public bool ReusableAcrossFrames;
public long AccountedPayloadBytes;
public void BeginRebuild(
in PackedClassificationIdentity identity,
long frame)
{
Identity = identity;
Batches.Clear();
SelectionParts.Clear();
LastBuiltFrame = frame;
LastSeenFrame = frame;
ReusableAcrossFrames = false;
}
public void ReleaseStorage()
{
Batches.Clear();
SelectionParts.Clear();
Batches.TrimExcess();
SelectionParts.TrimExcess();
}
}
internal readonly record struct PackedClassificationCacheSnapshot(
int ProjectionCount,
int StaticRebuildCount,
int SameFrameReuseCount,
int CrossFrameReuseCount,
int AnimatedClassificationCount,
int RetiredProjectionCount,
long RetainedPayloadBytes);
/// <summary>
/// G3 retained classification owner. Structural classification is rebuilt
/// only for first sight, incarnation/appearance changes, incomplete resource
/// readiness, or active animation. Warm unchanged projections replay their
/// retained batch templates while current transforms, lights, clip slots, and
/// selection lighting are refreshed.
/// </summary>
internal sealed class PackedProjectionClassificationCache
{
private const RenderDirtyMask ClassificationDirtyMask =
RenderDirtyMask.Appearance;
private readonly Dictionary<
RenderProjectionId,
PackedProjectionClassificationEntry> _entries = [];
private readonly List<RenderProjectionId> _retired = [];
private RenderSceneGeneration _generation;
private bool _generationSet;
private long _frame;
private int _staticRebuildCount;
private int _sameFrameReuseCount;
private int _crossFrameReuseCount;
private int _animatedClassificationCount;
private int _retiredProjectionCount;
private long _retainedPayloadBytes;
public long Frame => _frame;
public PackedClassificationCacheSnapshot Snapshot =>
new(
_entries.Count,
_staticRebuildCount,
_sameFrameReuseCount,
_crossFrameReuseCount,
_animatedClassificationCount,
_retiredProjectionCount,
_retainedPayloadBytes);
public void BeginFrame(RenderSceneGeneration generation)
{
if (_frame == long.MaxValue)
{
throw new InvalidOperationException(
"Packed classification frame identity was exhausted.");
}
if (!_generationSet || generation != _generation)
{
Clear();
_generation = generation;
_generationSet = true;
}
_frame++;
_staticRebuildCount = 0;
_sameFrameReuseCount = 0;
_crossFrameReuseCount = 0;
_animatedClassificationCount = 0;
_retiredProjectionCount = 0;
}
public bool TryGetReusable(
RenderProjectionId id,
in PackedClassificationIdentity identity,
RenderDirtyMask dirtyMask,
out PackedProjectionClassificationEntry? entry)
{
if (!_entries.TryGetValue(id, out entry)
|| entry.Identity != identity)
{
return false;
}
entry.LastSeenFrame = _frame;
if (entry.LastBuiltFrame == _frame)
{
_sameFrameReuseCount++;
return true;
}
if (!entry.ReusableAcrossFrames
|| (dirtyMask & ClassificationDirtyMask) != 0)
{
return false;
}
_crossFrameReuseCount++;
return true;
}
public PackedProjectionClassificationEntry BeginRebuild(
RenderProjectionId id,
in PackedClassificationIdentity identity)
{
if (!_entries.TryGetValue(id, out var entry))
{
entry = new PackedProjectionClassificationEntry();
_entries.Add(id, entry);
}
entry.BeginRebuild(in identity, _frame);
_staticRebuildCount++;
return entry;
}
public void CompleteRebuild(
PackedProjectionClassificationEntry entry,
bool reusableAcrossFrames)
{
ArgumentNullException.ThrowIfNull(entry);
if (entry.LastBuiltFrame != _frame)
{
throw new InvalidOperationException(
"Only an entry rebuilt in the active frame can be completed.");
}
entry.ReusableAcrossFrames = reusableAcrossFrames;
long payloadBytes = checked(
(long)entry.Batches.Capacity
* Unsafe.SizeOf<PackedClassifiedBatch>()
+ (long)entry.SelectionParts.Capacity
* Unsafe.SizeOf<PackedClassifiedSelectionPart>());
_retainedPayloadBytes = checked(
_retainedPayloadBytes
- entry.AccountedPayloadBytes
+ payloadBytes);
entry.AccountedPayloadBytes = payloadBytes;
}
public void RecordAnimatedClassification() =>
_animatedClassificationCount++;
public void EndFrame()
{
_retired.Clear();
foreach ((RenderProjectionId id, var entry) in _entries)
{
if (entry.LastSeenFrame != _frame)
{
_retainedPayloadBytes -= entry.AccountedPayloadBytes;
entry.AccountedPayloadBytes = 0;
entry.ReleaseStorage();
_retired.Add(id);
}
}
for (int i = 0; i < _retired.Count; i++)
_entries.Remove(_retired[i]);
_retiredProjectionCount = _retired.Count;
_retired.Clear();
}
public void Clear()
{
foreach (PackedProjectionClassificationEntry entry
in _entries.Values)
{
entry.ReleaseStorage();
}
_entries.Clear();
_retired.Clear();
_frame = 0;
_retainedPayloadBytes = 0;
}
}

File diff suppressed because it is too large Load diff

View file

@ -2,6 +2,7 @@ using System.Numerics;
using AcDream.App.Rendering.Scene;
using AcDream.App.Rendering.Selection;
using AcDream.App.Rendering.Walk;
using AcDream.Core.Lighting;
using AcDream.Core.Meshing;
using AcDream.Core.World;
using DatReaderWriter.Enums;
@ -166,7 +167,7 @@ public sealed partial class WbDrawDispatcher
/// Classifies one static entity for the walk populator: resolves its clip
/// slot / light set / selection lighting exactly as
/// <c>ClassifyPackedEntity</c> does (via the shared
/// <see cref="ResolveSlotForFrame"/> / <c>ResolvePackedLightSet</c>
/// <see cref="ResolveSlotForFrame"/> / <c>ResolveWalkLightSet</c>
/// helpers), walks its Setup parts or single mesh (mirroring
/// <c>ClassifyPackedEntity</c>'s own shape), and appends one
/// <see cref="WalkClassifiedBatch"/> per surviving batch to
@ -204,7 +205,7 @@ public sealed partial class WbDrawDispatcher
if (culled)
return;
ResolvePackedLightSet(in entity, out InstanceLightSet lights, out bool indoor);
ResolveWalkLightSet(in entity, out InstanceLightSet lights, out bool indoor);
Vector2 selectionLighting =
_selectionLighting?.TryGetLighting(
entity.ServerGuid, entity.LocalEntityId, out RetailSelectionLighting lighting) == true
@ -261,7 +262,7 @@ public sealed partial class WbDrawDispatcher
}
float opacity = liveDynamic
? PackedPartOpacity(
? WalkPartOpacity(
entity.ServerGuid,
entity.LocalEntityId,
(uint)setupPartIndex)
@ -298,7 +299,7 @@ public sealed partial class WbDrawDispatcher
else
{
float opacity = liveDynamic
? PackedPartOpacity(
? WalkPartOpacity(
entity.ServerGuid,
entity.LocalEntityId,
(uint)partIndex)
@ -330,6 +331,110 @@ public sealed partial class WbDrawDispatcher
}
}
private float WalkPartOpacity(
uint serverGuid,
uint localEntityId,
uint setupPartIndex)
{
float opacity = EntityOpacity(serverGuid);
if (opacity <= 0f)
return 0f;
if (!_translucencyFades.TryGetCurrentValue(
localEntityId,
setupPartIndex,
out float translucency))
{
return opacity;
}
return translucency >= 1f
? 0f
: opacity * (1f - translucency);
}
private void ResolveWalkLightSet(
in RenderInstanceCandidate entity,
out InstanceLightSet lights,
out bool indoor)
{
indoor = IndoorObjectReceivesTorches(entity.ParentCell);
lights = InstanceLightSet.Disabled;
IReadOnlyList<LightSource>? snapshot = _pointSnapshot;
if (!indoor || snapshot is null || snapshot.Count == 0)
return;
Vector3 center =
(entity.Bounds.Minimum + entity.Bounds.Maximum) * 0.5f;
float radius =
(entity.Bounds.Maximum - entity.Bounds.Minimum)
.Length() * 0.5f;
Span<int> selected =
stackalloc int[LightManager.MaxLightsPerObject];
selected.Fill(-1);
LightManager.SelectForObject(
snapshot,
center,
radius,
selected);
lights = InstanceLightSet.From(selected);
}
private static bool PartVisibleInLookInTurn(
IWalkLookInViewSource? lookInViews,
int routeIndex,
uint cellId,
in RenderInstanceCandidate entity,
int partIndex,
uint gfxObjId,
ObjectRenderData renderData,
Matrix4x4 localToWorld)
{
if (lookInViews is null)
return true;
if (renderData.SelectionSphere is not { Radius: > 0f } sphere)
return true;
return LookInDrawingSphereVisible(
lookInViews,
routeIndex,
sphere,
localToWorld,
out _,
out _);
}
internal static bool LookInDrawingSphereVisible(
IWalkLookInViewSource lookInViews,
int routeIndex,
DatReaderWriter.Types.Sphere sphere,
Matrix4x4 localToWorld,
out Vector3 center,
out float radius)
{
ArgumentNullException.ThrowIfNull(lookInViews);
ArgumentNullException.ThrowIfNull(sphere);
center = Vector3.Transform(sphere.Origin, localToWorld);
float scaleX = new Vector3(
localToWorld.M11,
localToWorld.M12,
localToWorld.M13).Length();
float scaleY = new Vector3(
localToWorld.M21,
localToWorld.M22,
localToWorld.M23).Length();
float scaleZ = new Vector3(
localToWorld.M31,
localToWorld.M32,
localToWorld.M33).Length();
radius = sphere.Radius
* MathF.Max(scaleX, MathF.Max(scaleY, scaleZ));
return lookInViews.SphereVisibleInLookInTurn(
routeIndex,
in center,
radius);
}
/// <summary>
/// Walks one resolved mesh's batches through <see cref="TryClassifyBatch"/>
/// and appends every surviving one to <paramref name="sink"/> at

View file

@ -2305,18 +2305,9 @@ public sealed partial class WbDrawDispatcher : IDisposable
/// <summary>
/// Campaign VM VM6 review fix round 3 (N2a): the ONE shared
/// InstanceGroup-from-key construction seam. Before this there were two
/// near-identical <c>new InstanceGroup { ... }</c> initializers — the
/// classic route's <see cref="GetOrCreateInstanceGroup"/> and the packed
/// route's <c>GetOrCreatePackedGroup</c> — that had already drifted once
/// (the packed route's F1 bug: FoliageFlags copied on one side, dropped
/// on the other). <paramref name="registration"/> and
/// <paramref name="frame"/> are the only two values that legitimately
/// differ per route (each owns its own registration counter and frame
/// serial); every other field comes from <paramref name="key"/>, so a
/// field neither site remembers to set can no longer exist. Same
/// precedent as <see cref="AppendPackedInstance"/> — one writer, tested
/// once, called from both routes.
/// InstanceGroup-from-key construction seam retained by the classic
/// diagnostic route. The production walk writes its ordered stream
/// directly and the former packed twin was deleted at Campaign FW4.
/// </summary>
internal static InstanceGroup CreateGroupFromKey(
GroupKey key,

View file

@ -1,115 +0,0 @@
using AcDream.App.Rendering.Wb;
using AcDream.App.Rendering.Gpu;
using AcDream.Core.Meshing;
using System.Numerics;
namespace AcDream.App.Tests.Rendering.Wb;
public sealed class PackedDispatcherOracleTests
{
/// <summary>
/// Campaign VM VM6 review fix round 3 (N1): this test does NOT guard
/// the packed classifier itself — CreateGroupFromKey's own test
/// (InstanceGroupClearTests.CreateGroupFromKey_CopiesFoliageFlagsFromTheKey)
/// plus GroupKey.FoliageFlags being a required constructor argument are
/// what actually make F1 (the round-2 blocker: the packed classifier's
/// InstanceGroup silently carried FoliageFlags == 0) impossible to
/// reintroduce. What THIS test proves is the SHARED production step
/// downstream of classification: FoliageWindClassification.Classify,
/// called with the same argument shape ClassifyPackedBatches uses
/// (entity.LocalEntityId, exclusion membership, batch.Translucency,
/// entity-scoped HasCutoutSubset), resolves a real procedural-scenery
/// entity id (0x8…) to CutoutFoliageFlag (0x2); and a group carrying
/// that flags value reaches the literal BatchData.flags word through
/// BuildIndirectArrays — the same already-tested production step both
/// the classic and packed group lists feed into
/// (WbDrawDispatcherIndirectBuilderTests pins bit 0; this pins bits 1/2
/// landing alongside it for a real scenery id).
/// </summary>
[Fact]
public void SceneryCutoutEntityClassificationReachesTheBatchDataFlagsWordAsBit0x2()
{
const uint proceduralSceneryTreeId = 0x80010203u; // top nibble 0x8
uint foliageFlags = FoliageWindClassification.Classify(
proceduralSceneryTreeId,
isExcluded: false,
TranslucencyKind.ClipMap,
meshHasCutoutSubset: true);
Assert.Equal(FoliageWindClassification.CutoutFoliageFlag, foliageFlags);
Assert.Equal(0x2u, foliageFlags);
var groups = new List<WbDrawDispatcher.IndirectGroupInput>
{
new(
IndexCount: 12,
FirstIndex: 0,
BaseVertex: 0,
InstanceCount: 1,
FirstInstance: 0,
TextureIndex: 0x5,
TextureLayer: 0,
Translucency: TranslucencyKind.ClipMap,
FoliageFlags: foliageFlags),
};
var indirect = new DrawElementsIndirectCommand[4];
var batches = new WbDrawDispatcher.BatchDataPublic[4];
WbDrawDispatcher.BuildIndirectArrays(groups, indirect, batches);
// Bit 0 (the #226 built-mesh marker) | bit 1 (cutout foliage) = 0x3;
// isolating bit 1 with a mask proves the foliage classification
// specifically reached the word, not merely that SOME nonzero value
// did.
Assert.Equal(0x2u, batches[0].Flags & 0x2u);
}
[Theory]
[InlineData(false, 0u)]
[InlineData(true, 1u)]
public void PackedInstanceWriter_AppendsDetailCategoryInParallel(
bool buildingDetail,
uint expectedCategory)
{
var group = new WbDrawDispatcher.InstanceGroup();
WbDrawDispatcher.AppendPackedInstance(
group,
Matrix4x4.Identity,
Vector3.One,
submissionOrder: 7,
slot: 3u,
lights: WbDrawDispatcher.InstanceLightSet.Disabled,
indoor: true,
buildingDetail: buildingDetail,
opacity: 0.5f,
selectionLighting: new Vector2(0.25f, 0.75f));
Assert.Single(group.Matrices);
Assert.Single(group.LocalSortCenters);
Assert.Single(group.SubmissionOrders);
Assert.Single(group.Slots);
Assert.Single(group.LightSets);
Assert.Single(group.IndoorFlags);
Assert.Equal(expectedCategory, Assert.Single(group.DetailCategories));
Assert.Single(group.Opacities);
Assert.Single(group.SelectionLighting);
}
[Theory]
[InlineData(0u, false, false)]
[InlineData(0u, true, false)]
[InlineData(7u, false, false)]
[InlineData(7u, true, true)]
public void TransparentDeferral_MatchesProductionNoVaoEarlyReturn(
uint anyVao,
bool alphaQueueCollecting,
bool expected)
{
Assert.Equal(
expected,
WbDrawDispatcher.ShouldDeferPackedTransparent(
anyVao,
alphaQueueCollecting));
}
}

View file

@ -1,215 +0,0 @@
using AcDream.App.Rendering.Scene;
using AcDream.App.Rendering.Wb;
using Xunit;
namespace AcDream.App.Tests.Rendering.Wb;
public sealed class PackedProjectionClassificationCacheTests
{
[Fact]
public void UnchangedProjection_ReusesClassificationAcrossFrames()
{
var cache = new PackedProjectionClassificationCache();
RenderSceneGeneration generation =
RenderSceneGeneration.FromRaw(1);
RenderProjectionId id = RenderProjectionId.FromRaw(0x1234);
PackedClassificationIdentity identity = Identity(7);
cache.BeginFrame(generation);
PackedProjectionClassificationEntry entry =
cache.BeginRebuild(id, in identity);
cache.CompleteRebuild(entry, reusableAcrossFrames: true);
cache.EndFrame();
cache.BeginFrame(generation);
Assert.True(cache.TryGetReusable(
id,
in identity,
RenderDirtyMask.Transform,
out PackedProjectionClassificationEntry? reused));
cache.EndFrame();
Assert.Same(entry, reused);
Assert.Equal(0, cache.Snapshot.StaticRebuildCount);
Assert.Equal(1, cache.Snapshot.CrossFrameReuseCount);
Assert.Equal(1, cache.Snapshot.ProjectionCount);
}
[Fact]
public void AppearanceDirty_RequiresRebuildButTransformDirtyDoesNot()
{
var cache = new PackedProjectionClassificationCache();
RenderSceneGeneration generation =
RenderSceneGeneration.FromRaw(1);
RenderProjectionId id = RenderProjectionId.FromRaw(0x1234);
PackedClassificationIdentity identity = Identity(7);
cache.BeginFrame(generation);
PackedProjectionClassificationEntry entry =
cache.BeginRebuild(id, in identity);
cache.CompleteRebuild(entry, reusableAcrossFrames: true);
cache.EndFrame();
cache.BeginFrame(generation);
Assert.False(cache.TryGetReusable(
id,
in identity,
RenderDirtyMask.Appearance,
out _));
PackedProjectionClassificationEntry rebuilt =
cache.BeginRebuild(id, in identity);
cache.CompleteRebuild(rebuilt, reusableAcrossFrames: true);
Assert.Same(entry, rebuilt);
Assert.Equal(1, cache.Snapshot.StaticRebuildCount);
}
[Fact]
public void IncompleteBuild_CanReplayDuplicateRouteOnlyInSameFrame()
{
var cache = new PackedProjectionClassificationCache();
RenderSceneGeneration generation =
RenderSceneGeneration.FromRaw(1);
RenderProjectionId id = RenderProjectionId.FromRaw(0x1234);
PackedClassificationIdentity identity = Identity(7);
cache.BeginFrame(generation);
PackedProjectionClassificationEntry entry =
cache.BeginRebuild(id, in identity);
cache.CompleteRebuild(entry, reusableAcrossFrames: false);
Assert.True(cache.TryGetReusable(
id,
in identity,
RenderDirtyMask.All,
out _));
Assert.Equal(1, cache.Snapshot.SameFrameReuseCount);
cache.EndFrame();
cache.BeginFrame(generation);
Assert.False(cache.TryGetReusable(
id,
in identity,
RenderDirtyMask.None,
out _));
}
[Fact]
public void IncarnationOrAppearanceChange_CannotReuseRetainedPayload()
{
var cache = new PackedProjectionClassificationCache();
RenderSceneGeneration generation =
RenderSceneGeneration.FromRaw(1);
RenderProjectionId id = RenderProjectionId.FromRaw(0x1234);
PackedClassificationIdentity original = Identity(7);
cache.BeginFrame(generation);
PackedProjectionClassificationEntry entry =
cache.BeginRebuild(id, in original);
cache.CompleteRebuild(entry, reusableAcrossFrames: true);
cache.EndFrame();
cache.BeginFrame(generation);
PackedClassificationIdentity replacement = Identity(17);
Assert.False(cache.TryGetReusable(
id,
in replacement,
RenderDirtyMask.None,
out _));
PackedProjectionClassificationEntry rebuilt =
cache.BeginRebuild(id, in replacement);
cache.CompleteRebuild(rebuilt, reusableAcrossFrames: true);
cache.EndFrame();
Assert.Same(entry, rebuilt);
Assert.Equal(replacement, rebuilt.Identity);
Assert.Equal(1, cache.Snapshot.StaticRebuildCount);
Assert.Equal(0, cache.Snapshot.CrossFrameReuseCount);
}
[Fact]
public void GenerationReplacementAndUnseenProjectionReleaseRetainedEntries()
{
var cache = new PackedProjectionClassificationCache();
RenderProjectionId id = RenderProjectionId.FromRaw(0x1234);
PackedClassificationIdentity identity = Identity(7);
cache.BeginFrame(RenderSceneGeneration.FromRaw(1));
PackedProjectionClassificationEntry entry =
cache.BeginRebuild(id, in identity);
entry.Batches.Capacity = 32;
entry.SelectionParts.Capacity = 16;
cache.CompleteRebuild(entry, reusableAcrossFrames: true);
cache.EndFrame();
Assert.True(cache.Snapshot.RetainedPayloadBytes > 0);
cache.BeginFrame(RenderSceneGeneration.FromRaw(1));
cache.EndFrame();
Assert.Equal(0, cache.Snapshot.ProjectionCount);
Assert.Equal(1, cache.Snapshot.RetiredProjectionCount);
Assert.Equal(0, entry.Batches.Capacity);
Assert.Equal(0, entry.SelectionParts.Capacity);
Assert.Equal(0, cache.Snapshot.RetainedPayloadBytes);
cache.BeginFrame(RenderSceneGeneration.FromRaw(2));
Assert.Equal(0, cache.Snapshot.ProjectionCount);
}
[Fact]
public void WarmUnchangedProjectionCache_AllocatesZeroBytes()
{
var cache = new PackedProjectionClassificationCache();
RenderSceneGeneration generation =
RenderSceneGeneration.FromRaw(1);
RenderProjectionId id = RenderProjectionId.FromRaw(0x1234);
PackedClassificationIdentity identity = Identity(7);
cache.BeginFrame(generation);
PackedProjectionClassificationEntry entry =
cache.BeginRebuild(id, in identity);
cache.CompleteRebuild(entry, reusableAcrossFrames: true);
cache.EndFrame();
// #250: the measured window was a 1,000-iteration loop written inline.
ZeroAllocationProbe.AssertAllocatesNothing(
"PackedProjectionClassificationCache warm cache hit",
() => Hit(cache, generation, id, identity));
}
private static void Hit(
PackedProjectionClassificationCache cache,
RenderSceneGeneration generation,
RenderProjectionId id,
PackedClassificationIdentity identity)
{
cache.BeginFrame(generation);
if (!cache.TryGetReusable(
id,
in identity,
RenderDirtyMask.None,
out _))
{
throw new InvalidOperationException(
"The warmed projection classification was not reusable.");
}
cache.EndFrame();
}
private static PackedClassificationIdentity Identity(ulong seed) =>
new(
RenderOwnerIncarnation.FromRaw(seed),
new RenderMeshSet(
RenderAssetHandle.FromRaw(seed + 1),
MeshCount: 2,
Revision: seed + 2),
new RenderMaterialVariant(
PaletteKey: seed + 3,
TextureReplacementKey: seed + 4,
Opacity: 1f),
new RenderDegradeState(
Level: 0,
Revision: (uint)(seed + 5)),
new RenderSceneHash128(seed + 6, seed + 7),
new RenderSceneHash128(seed + 8, seed + 9));
}