acdream/src/AcDream.App/Rendering/Wb/PackedProjectionClassificationCache.cs
Erik 54d17eb446 perf(rendering): retain packed classification
Retain appearance classification per projection while refreshing transforms, clip slots, lights, selection lighting, and group instance payloads each frame. Incomplete resources and active animation remain on the live classification path, and only an exact full packed comparison acknowledges scene dirtiness.
2026-07-25 02:46:40 +02:00

251 lines
7.6 KiB
C#

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;
}
}