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