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