using System.Numerics;
using System.Runtime.CompilerServices;
using AcDream.App.Rendering.Scene;
namespace AcDream.App.Rendering.Wb;
///
/// 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.
///
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);
///
/// 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.
///
internal sealed class PackedProjectionClassificationEntry
{
public PackedClassificationIdentity Identity;
public readonly List Batches = [];
public readonly List 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);
///
/// 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.
///
internal sealed class PackedProjectionClassificationCache
{
private const RenderDirtyMask ClassificationDirtyMask =
RenderDirtyMask.Appearance;
private readonly Dictionary<
RenderProjectionId,
PackedProjectionClassificationEntry> _entries = [];
private readonly List _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()
+ (long)entry.SelectionParts.Capacity
* Unsafe.SizeOf());
_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;
}
}