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(); for (int i = 0; i < 100; i++) Hit(cache, generation, id, identity); long before = GC.GetAllocatedBytesForCurrentThread(); for (int i = 0; i < 1_000; i++) Hit(cache, generation, id, identity); long allocated = GC.GetAllocatedBytesForCurrentThread() - before; Assert.Equal(0, allocated); } 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)); }