using System.Numerics; using AcDream.App.Rendering.Scene; using AcDream.App.Rendering.Scene.Arch; namespace AcDream.App.Tests.Rendering; public sealed class ArchRenderSceneTests { [Fact] public void Apply_RegisterChannelUpdatesAndUnregister_PreservesTypedRecord() { RenderSceneGeneration generation = Generation(1); using var scene = new ArchRenderScene(generation); RenderProjectionRecord original = Record( id: 10, incarnation: 3, RenderProjectionClass.LiveDynamicRoot, x: 1, bucket: 0xAABB); RenderDeltaApplyResult register = scene.Apply( [RenderProjectionDelta.Register(generation, 1, original)]); Assert.Equal(1, register.Applied); Assert.Equal(1, register.Registered); Assert.Equal( new RenderProjectionCounts(1, 0, 0, 1, 0, 0), scene.Counts); RenderProjectionRecord transformed = original with { PreviousTransform = new PreviousRenderTransform( original.Transform.LocalToWorld), Transform = new RenderTransform( Matrix4x4.CreateTranslation(4, 5, 6)), Bounds = new RenderWorldBounds( new Vector3(3, 4, 5), new Vector3(5, 6, 7)), SortKey = new RenderSortKey(99), }; RenderProjectionRecord appearance = transformed with { MeshSet = new RenderMeshSet(Asset(80), 4, 7), Material = new RenderMaterialVariant(12, 13, 0.5f), DegradeState = new RenderDegradeState(2, 8), }; RenderProjectionRecord flags = appearance with { Flags = RenderProjectionFlags.Draw | RenderProjectionFlags.Translucent, }; RenderProjectionRecord rebucketed = flags with { Residency = new RenderSpatialResidency( Bucket(0xCCDD), 0xCCDD0000, 0xCCDD0102), }; RenderDeltaApplyResult updates = scene.Apply( [ RenderProjectionDelta.Update( RenderProjectionDeltaKind.UpdateTransform, generation, 2, transformed), RenderProjectionDelta.Update( RenderProjectionDeltaKind.UpdateAppearance, generation, 3, appearance), RenderProjectionDelta.Update( RenderProjectionDeltaKind.UpdateFlags, generation, 4, flags), RenderProjectionDelta.Update( RenderProjectionDeltaKind.Rebucket, generation, 5, rebucketed), ]); Assert.Equal(4, updates.Applied); Assert.Equal(4, updates.Updated); Assert.True(scene.OpenQuery().TryGet(original.Id, out var stored)); Assert.Equal(rebucketed.Transform, stored.Transform); Assert.Equal(rebucketed.Bounds, stored.Bounds); Assert.Equal(rebucketed.MeshSet, stored.MeshSet); Assert.Equal(rebucketed.Material, stored.Material); Assert.Equal(rebucketed.Flags, stored.Flags); Assert.Equal(rebucketed.Residency, stored.Residency); Assert.Equal( RenderDirtyMask.All, stored.DirtyMask); RenderDeltaApplyResult unregister = scene.Apply( [ RenderProjectionDelta.Unregister( generation, 6, original.Id, original.OwnerIncarnation), ]); Assert.Equal(1, unregister.Applied); Assert.Equal(1, unregister.Unregistered); Assert.Equal(default, scene.Counts); Assert.False(scene.OpenQuery().TryGet(original.Id, out _)); } [Fact] public void Register_NewerIncarnationReplacesAndOldCallbacksCannotRemoveIt() { RenderSceneGeneration generation = Generation(4); using var scene = new ArchRenderScene(generation); RenderProjectionRecord first = Record( 44, 10, RenderProjectionClass.OutdoorStatic); RenderProjectionRecord replacement = Record( 44, 11, RenderProjectionClass.ActiveAnimatedStatic, x: 9); scene.Apply([RenderProjectionDelta.Register(generation, 1, first)]); RenderDeltaApplyResult replaced = scene.Apply( [RenderProjectionDelta.Register(generation, 2, replacement)]); RenderDeltaApplyResult staleRegister = scene.Apply( [RenderProjectionDelta.Register(generation, 3, first)]); RenderDeltaApplyResult staleDelete = scene.Apply( [ RenderProjectionDelta.Unregister( generation, 4, first.Id, first.OwnerIncarnation), ]); Assert.Equal(1, replaced.Replaced); Assert.Equal(1, replaced.Registered); Assert.Equal(1, staleRegister.RejectedStaleIncarnation); Assert.Equal(1, staleDelete.RejectedStaleIncarnation); Assert.Equal( new RenderProjectionCounts(1, 0, 0, 0, 1, 0), scene.Counts); Assert.True(scene.OpenQuery().TryGet(first.Id, out var current)); Assert.Equal(replacement.OwnerIncarnation, current.OwnerIncarnation); Assert.Equal(replacement.Transform, current.Transform); } [Fact] public void Apply_RejectsWrongGenerationOutOfOrderAndMissingDeltas() { RenderSceneGeneration generation = Generation(7); using var scene = new ArchRenderScene(generation); RenderProjectionRecord record = Record( 70, 1, RenderProjectionClass.IndoorCellStatic); RenderDeltaApplyResult result = scene.Apply( [ RenderProjectionDelta.Register(Generation(6), 1, record), RenderProjectionDelta.Register(generation, 2, record), RenderProjectionDelta.Update( RenderProjectionDeltaKind.UpdateFlags, generation, 2, record), RenderProjectionDelta.Unregister( generation, 3, Projection(999), Incarnation(1)), ]); Assert.Equal(1, result.Applied); Assert.Equal(1, result.RejectedGeneration); Assert.Equal(1, result.RejectedOutOfOrderSequence); Assert.Equal(1, result.RejectedMissing); Assert.Equal(1, scene.Counts.Total); } [Fact] public void Clear_AdvancesGenerationReclaimsRecordsAndInvalidatesBorrowedQuery() { RenderSceneGeneration firstGeneration = Generation(1); using var scene = new ArchRenderScene(firstGeneration); RenderProjectionRecord record = Record( 1, 1, RenderProjectionClass.EquippedChild); scene.Apply( [RenderProjectionDelta.Register(firstGeneration, 1, record)]); RenderSceneQuery borrowed = scene.OpenQuery(); scene.Clear(Generation(2)); Assert.Equal(Generation(2), scene.Generation); Assert.Equal(default, scene.Counts); Assert.Equal(0, scene.Memory.EntityCount); Assert.Throws(() => _ = borrowed.Counts); Assert.Throws( () => scene.Clear(Generation(2))); RenderProjectionRecord next = record with { OwnerIncarnation = Incarnation(2), }; RenderDeltaApplyResult applied = scene.Apply( [RenderProjectionDelta.Register(Generation(2), 1, next)]); Assert.Equal(1, applied.Registered); } [Fact] public void FiveProjectionClasses_UseNarrowArchetypesAndReportRetainedMemory() { RenderSceneGeneration generation = Generation(9); using var scene = new ArchRenderScene(generation); RenderProjectionClass[] classes = [ RenderProjectionClass.OutdoorStatic, RenderProjectionClass.IndoorCellStatic, RenderProjectionClass.LiveDynamicRoot, RenderProjectionClass.ActiveAnimatedStatic, RenderProjectionClass.EquippedChild, ]; var deltas = new RenderProjectionDelta[classes.Length]; for (int i = 0; i < classes.Length; i++) { RenderProjectionRecord record = Record( (ulong)i + 1, 1, classes[i]); deltas[i] = RenderProjectionDelta.Register( generation, (ulong)i + 1, record); } scene.Apply(deltas); RenderSceneMemoryAccounting memory = scene.Memory; Assert.Equal(5, scene.Counts.Total); Assert.Equal(5, memory.EntityCount); Assert.Equal(5, memory.ArchetypeCount); Assert.Equal(5, memory.AllocatedChunkCount); Assert.True(memory.ArchEntityCapacity >= memory.EntityCount); Assert.True(memory.EstimatedChunkPayloadBytes > 0); Assert.True(memory.ProjectionLookupCapacity >= memory.EntityCount); Assert.True(memory.EstimatedProjectionLookupBytes > 0); Assert.Equal( memory.EstimatedChunkPayloadBytes + memory.EstimatedProjectionLookupBytes + memory.EstimatedIndexBytes, memory.TotalEstimatedBytes); } [Fact] public void DynamicSynchronization_UpdatesOnlyMatchingLiveIncarnation() { RenderSceneGeneration generation = Generation(10); using var scene = new ArchRenderScene(generation); RenderProjectionRecord record = Record( 100, 5, RenderProjectionClass.LiveDynamicRoot); scene.Apply([RenderProjectionDelta.Register(generation, 1, record)]); DynamicProjectionUpdate stale = new( record.Id, Incarnation(4), new RenderTransform(Matrix4x4.CreateTranslation(100, 0, 0)), record.Bounds); DynamicProjectionUpdate current = new( record.Id, record.OwnerIncarnation, new RenderTransform(Matrix4x4.CreateTranslation(8, 9, 10)), new RenderWorldBounds( new Vector3(7, 8, 9), new Vector3(9, 10, 11))); scene.SynchronizeDynamicSources( new DynamicProjectionSyncInput(generation, [stale, current])); Assert.True(scene.OpenQuery().TryGet(record.Id, out var stored)); Assert.Equal(record.Transform.LocalToWorld, stored.PreviousTransform.LocalToWorld); Assert.Equal(current.Transform, stored.Transform); Assert.Equal(current.Bounds, stored.Bounds); } [Fact] public void TransformUpdateBatch_ReusesRetainedStorage() { const int count = 1_000; RenderSceneGeneration generation = Generation(11); using var scene = new ArchRenderScene(generation); var registrations = new RenderProjectionDelta[count]; var updates = new RenderProjectionDelta[count]; for (int index = 0; index < count; index++) { RenderProjectionRecord record = Record( (ulong)index + 1, 1, RenderProjectionClass.ActiveAnimatedStatic, x: index); registrations[index] = RenderProjectionDelta.Register( generation, (ulong)index + 1, record); updates[index] = RenderProjectionDelta.Update( RenderProjectionDeltaKind.UpdateTransform, generation, (ulong)count + (ulong)index + 1, record with { Transform = new RenderTransform( Matrix4x4.CreateTranslation(index + 1, 0, 0)), }); } scene.Apply(registrations); long before = GC.GetAllocatedBytesForCurrentThread(); scene.Apply(updates); long allocated = GC.GetAllocatedBytesForCurrentThread() - before; Assert.True(allocated == 0, $"Allocated {allocated:N0} bytes."); } [Fact] public void Digest_IsStableAcrossRegistrationOrderAndBufferReuse() { RenderSceneGeneration generation = Generation(12); using var first = new ArchRenderScene(generation); using var second = new ArchRenderScene(generation); RenderProjectionRecord a = Record( 1, 1, RenderProjectionClass.OutdoorStatic); RenderProjectionRecord b = Record( 2, 1, RenderProjectionClass.LiveDynamicRoot); first.Apply( [ RenderProjectionDelta.Register(generation, 1, a), RenderProjectionDelta.Register(generation, 2, b), ]); second.Apply( [ RenderProjectionDelta.Register(generation, 1, b), RenderProjectionDelta.Register(generation, 2, a), ]); var buffer = new RenderSceneDigestBuffer(); RenderSceneDigest firstDigest = first.BuildDigest(buffer); int retainedCapacity = buffer.Capacity; RenderSceneDigest repeatedDigest = first.BuildDigest(buffer); RenderSceneDigest secondDigest = second.BuildDigest(new RenderSceneDigestBuffer()); Assert.Equal(firstDigest, repeatedDigest); Assert.Equal(firstDigest, secondDigest); Assert.Equal(retainedCapacity, buffer.Capacity); } [Fact] public void Mutation_FromAnotherThread_IsRejected() { RenderSceneGeneration generation = Generation(15); using var scene = new ArchRenderScene(generation); RenderProjectionRecord record = Record( 15, 1, RenderProjectionClass.LiveDynamicRoot); Exception? captured = null; var mutationThread = new Thread(() => { try { scene.Apply( [RenderProjectionDelta.Register(generation, 1, record)]); } catch (Exception error) { captured = error; } }); mutationThread.Start(); mutationThread.Join(); InvalidOperationException error = Assert.IsType(captured); Assert.Contains("owning update thread", error.Message); Assert.Equal(0, scene.Counts.Total); } [Fact] public void IncrementalIndices_PreserveOutdoorCellDynamicAndFeatureMembership() { const uint indoorCell = 0x12340100u; RenderSceneGeneration generation = Generation(16); using var scene = new ArchRenderScene(generation); RenderProjectionRecord outdoorStatic = Record( 1, 1, RenderProjectionClass.OutdoorStatic); RenderProjectionRecord indoorStatic = Record( 2, 1, RenderProjectionClass.IndoorCellStatic) with { Residency = new RenderSpatialResidency( Bucket(indoorCell), 0x1234FFFFu, indoorCell), Source = new RenderSourceMetadata() with { ParentCellId = indoorCell, }, }; RenderProjectionRecord animatedStatic = Record( 3, 1, RenderProjectionClass.ActiveAnimatedStatic); RenderProjectionRecord outdoorDynamic = Record( 4, 1, RenderProjectionClass.LiveDynamicRoot); RenderProjectionRecord cellDynamic = Record( 5, 1, RenderProjectionClass.LiveDynamicRoot) with { Residency = new RenderSpatialResidency( Bucket(indoorCell), 0x1234FFFFu, indoorCell), Source = new RenderSourceMetadata() with { ParentCellId = indoorCell, }, }; RenderProjectionRecord equipped = Record( 6, 1, RenderProjectionClass.EquippedChild) with { Flags = RenderProjectionFlags.Draw | RenderProjectionFlags.Selectable | RenderProjectionFlags.Translucent | RenderProjectionFlags.LightCandidate | RenderProjectionFlags.PortalStraddling, }; RenderProjectionRecord[] records = [ outdoorStatic, indoorStatic, animatedStatic, outdoorDynamic, cellDynamic, equipped, ]; var deltas = new RenderProjectionDelta[records.Length]; for (int i = 0; i < records.Length; i++) { deltas[i] = RenderProjectionDelta.Register( generation, (ulong)i + 1, records[i]); } scene.Apply(deltas); RenderSceneQuery query = scene.OpenQuery(); Assert.Equal( new RenderSceneIndexCounts( OutdoorStatic: 2, IndoorCellStatic: 1, Dynamic: 3, OutdoorDynamic: 2, PortalStraddlingDynamic: 1, Translucent: 1, Selectable: 6, LightCandidate: 1, Dirty: 6), query.IndexCounts); Assert.Equal(1, query.GetCellStaticCount(indoorCell)); Assert.Equal(1, query.GetCellDynamicCount(indoorCell)); var cellRecords = new RenderProjectionRecord[1]; Assert.Equal(1, query.CopyCellStaticsTo(indoorCell, cellRecords)); Assert.Equal(indoorStatic.Id, cellRecords[0].Id); Assert.Equal(1, query.CopyCellDynamicsTo(indoorCell, cellRecords)); Assert.Equal(cellDynamic.Id, cellRecords[0].Id); Assert.True(scene.Memory.EstimatedIndexBytes > 0); } [Fact] public void IncrementalIndices_UpdateRebucketReplaceRemoveAndDirtyAcknowledge() { const uint indoorCell = 0x22220100u; RenderSceneGeneration generation = Generation(17); using var scene = new ArchRenderScene(generation); RenderProjectionRecord original = Record( 17, 1, RenderProjectionClass.LiveDynamicRoot); scene.Apply( [RenderProjectionDelta.Register(generation, 1, original)]); scene.ClearDirty(); Assert.Equal(0, scene.OpenQuery().IndexCounts.Dirty); RenderProjectionRecord flagged = original with { Flags = original.Flags | RenderProjectionFlags.Translucent | RenderProjectionFlags.LightCandidate | RenderProjectionFlags.PortalStraddling, Source = original.Source with { ParentCellId = indoorCell }, }; RenderProjectionRecord rebucketed = flagged with { Residency = new RenderSpatialResidency( Bucket(indoorCell), 0x2222FFFFu, indoorCell), }; scene.Apply( [ RenderProjectionDelta.Update( RenderProjectionDeltaKind.UpdateFlags, generation, 2, flagged), RenderProjectionDelta.Update( RenderProjectionDeltaKind.Rebucket, generation, 3, rebucketed), ]); RenderSceneQuery updated = scene.OpenQuery(); Assert.Equal(0, updated.IndexCounts.OutdoorDynamic); Assert.Equal(1, updated.IndexCounts.PortalStraddlingDynamic); Assert.Equal(1, updated.IndexCounts.Translucent); Assert.Equal(1, updated.IndexCounts.LightCandidate); Assert.Equal(1, updated.IndexCounts.Dirty); Assert.Equal(1, updated.GetCellDynamicCount(indoorCell)); RenderProjectionRecord replacement = rebucketed with { ProjectionClass = RenderProjectionClass.IndoorCellStatic, OwnerIncarnation = Incarnation(2), }; scene.Apply( [RenderProjectionDelta.Register(generation, 4, replacement)]); RenderSceneQuery replaced = scene.OpenQuery(); Assert.Equal(0, replaced.IndexCounts.Dynamic); Assert.Equal(1, replaced.IndexCounts.IndoorCellStatic); Assert.Equal(1, replaced.GetCellStaticCount(indoorCell)); scene.Apply( [ RenderProjectionDelta.Unregister( generation, 5, replacement.Id, replacement.OwnerIncarnation), ]); Assert.Equal(default, scene.OpenQuery().IndexCounts); Assert.True(scene.Memory.EstimatedIndexBytes > 0); } private static RenderProjectionRecord Record( ulong id, ulong incarnation, RenderProjectionClass projectionClass, float x = 0, ulong bucket = 1) { Matrix4x4 transform = Matrix4x4.CreateTranslation(x, 2, 3); return new RenderProjectionRecord( Projection(id), projectionClass, Incarnation(incarnation), new RenderTransform(transform), new PreviousRenderTransform(transform), new RenderMeshSet(Asset(id + 100), 2, 1), new RenderMaterialVariant(id + 200, id + 300, 1), new RenderSpatialResidency( Bucket(bucket), (uint)(bucket << 16), (uint)((bucket << 16) | 0x101)), new RenderWorldBounds( new Vector3(x - 1, 1, 2), new Vector3(x + 1, 3, 4)), RenderProjectionFlags.Draw | RenderProjectionFlags.Selectable, new RenderDegradeState(0, 1), new RenderSortKey(id + 400), RenderDirtyMask.All); } private static RenderProjectionId Projection(ulong value) => RenderProjectionId.FromRaw(value); private static RenderOwnerIncarnation Incarnation(ulong value) => RenderOwnerIncarnation.FromRaw(value); private static RenderSceneGeneration Generation(ulong value) => RenderSceneGeneration.FromRaw(value); private static RenderSpatialBucket Bucket(ulong value) => RenderSpatialBucket.FromRaw(value); private static RenderAssetHandle Asset(ulong value) => RenderAssetHandle.FromRaw(value); }