using System.Numerics; using AcDream.App.Rendering.Wb; using AcDream.Core.Meshing; using Xunit; namespace AcDream.App.Tests.Rendering.Wb; public class InstanceGroupClearTests { [Fact] public void ClassificationCache_PreservesImmutableSelectionPartDescriptors() { var cache = new EntityClassificationCache(); Matrix4x4 restPose = Matrix4x4.CreateRotationZ(0.75f) * Matrix4x4.CreateTranslation(1f, 2f, 3f); CachedSelectionPart[] parts = [ new CachedSelectionPart(0x0001_0002, 0x01001234u, restPose), ]; GroupKey key = MakeKey(0xAA); cache.Populate( entityId: 100, landblockHint: 0xA9B40000u, batches: [new CachedBatch(key, 0xAA, Matrix4x4.Identity)], selectionParts: parts); Assert.True(cache.TryGet(100, 0xA9B40000u, out EntityCacheEntry? entry)); CachedSelectionPart part = Assert.Single(entry!.SelectionParts); Assert.Equal(parts[0], part); Matrix4x4 entityWorld = Matrix4x4.CreateTranslation(20f, 30f, 40f); Assert.Equal(restPose * entityWorld, part.RestPose * entityWorld); } [Fact] public void InstanceLightSet_PacksAndCopiesAllRetailSlotsInOrder() { int[] source = [3, 7, 11, 15, 19, 23, 27, 31]; var packed = WbDrawDispatcher.InstanceLightSet.From(source); int[] destination = Enumerable.Repeat(-99, 12).ToArray(); packed.CopyTo(destination, 2); Assert.Equal(source, destination[2..10]); Assert.Equal(-99, destination[1]); Assert.Equal(-99, destination[10]); } [Fact] public void PartitionInstanceGroups_DeferredAlphaIsNotStagedInImmediateBuffers() { var opaqueGroup = MakeGroup(TranslucencyKind.Opaque, 2, new Vector3(3f, 4f, 0f)); var alphaGroup = MakeGroup(TranslucencyKind.AlphaBlend, 3, new Vector3(0f, 0f, 10f)); var additiveGroup = MakeGroup(TranslucencyKind.Additive, 1, new Vector3(1f, 0f, 0f)); var emptyGroup = new WbDrawDispatcher.InstanceGroup { Translucency = TranslucencyKind.Opaque, }; var opaque = new List(); var transparent = new List(); var deferred = WbDrawDispatcher.PartitionInstanceGroups( [opaqueGroup, alphaGroup, additiveGroup, emptyGroup], deferTransparent: true, cameraWorldPosition: Vector3.Zero, opaque, transparent); Assert.Equal(6, deferred.VisibleInstances); Assert.Equal(2, deferred.ImmediateInstances); Assert.Equal([opaqueGroup], opaque); Assert.Equal([alphaGroup, additiveGroup], transparent); Assert.Equal(25f, opaqueGroup.SortDistance); Assert.Equal(100f, alphaGroup.SortDistance); var immediate = WbDrawDispatcher.PartitionInstanceGroups( [opaqueGroup, alphaGroup, additiveGroup], deferTransparent: false, cameraWorldPosition: Vector3.Zero, opaque, transparent); Assert.Equal(6, immediate.VisibleInstances); Assert.Equal(6, immediate.ImmediateInstances); } [Fact] public void CachedGroupHandle_RequiresLiveMatchingRegistration() { var group = new WbDrawDispatcher.InstanceGroup { Registration = 17 }; var key = new GroupKey( FirstIndex: 0, BaseVertex: 0, IndexCount: 6, BindlessTextureHandle: 0xAA, TextureLayer: 0, Translucency: TranslucencyKind.Opaque); var cached = new CachedBatch( key, 0xAA, Matrix4x4.Identity, Group: group, GroupRegistration: 17); Assert.True(WbDrawDispatcher.TryResolveCachedGroup(cached, out var resolved)); Assert.Same(group, resolved); group.Registration = 0; Assert.False(WbDrawDispatcher.TryResolveCachedGroup(cached, out _)); Assert.False(WbDrawDispatcher.TryResolveCachedGroup( cached with { Group = null }, out _)); } [Fact] public void FramePrune_RetiresOnlyGroupsAbsentForWholePreviousFrame() { GroupKey liveKey = MakeKey(0xAA); GroupKey retiredKey = MakeKey(0xBB); var live = new WbDrawDispatcher.InstanceGroup { Registration = 11, LastUsedFrame = 9, }; live.Matrices.Add(Matrix4x4.Identity); var retired = new WbDrawDispatcher.InstanceGroup { Registration = 12, LastUsedFrame = 8, }; retired.Matrices.Capacity = 64; var groups = new Dictionary { [liveKey] = live, [retiredKey] = retired, }; var retiredKeys = new List(); var staleCache = new CachedBatch( retiredKey, retiredKey.BindlessTextureHandle, Matrix4x4.Identity, Group: retired, GroupRegistration: retired.Registration); int retiredCount = WbDrawDispatcher.PruneInstanceGroupsUnusedBeforeFrame( groups, retiredKeys, oldestLiveFrame: 9); Assert.Equal(1, retiredCount); Assert.Single(groups); Assert.Same(live, groups[liveKey]); Assert.Single(live.Matrices); Assert.Equal(11, live.Registration); Assert.Equal(0, retired.Registration); Assert.Equal(0, retired.Matrices.Capacity); Assert.False(WbDrawDispatcher.TryResolveCachedGroup(staleCache, out _)); Assert.Empty(retiredKeys); } [Fact] public void FramePrune_KeepsGroupsUsedByDifferentDrawScopesInSameFrame() { GroupKey landscapeKey = MakeKey(0xAA); GroupKey paperdollKey = MakeKey(0xBB); var landscape = new WbDrawDispatcher.InstanceGroup { Registration = 21, LastUsedFrame = 14, }; var paperdoll = new WbDrawDispatcher.InstanceGroup { Registration = 22, LastUsedFrame = 14, }; var groups = new Dictionary { [landscapeKey] = landscape, [paperdollKey] = paperdoll, }; int retiredCount = WbDrawDispatcher.PruneInstanceGroupsUnusedBeforeFrame( groups, [], oldestLiveFrame: 14); Assert.Equal(0, retiredCount); Assert.Equal(2, groups.Count); Assert.Equal(21, landscape.Registration); Assert.Equal(22, paperdoll.Registration); } private static GroupKey MakeKey(ulong textureHandle) => new( FirstIndex: 0, BaseVertex: 0, IndexCount: 6, BindlessTextureHandle: textureHandle, TextureLayer: 0, Translucency: TranslucencyKind.Opaque); private static WbDrawDispatcher.InstanceGroup MakeGroup( TranslucencyKind translucency, int instanceCount, Vector3 firstPosition) { var group = new WbDrawDispatcher.InstanceGroup { Translucency = translucency }; for (int i = 0; i < instanceCount; i++) group.Matrices.Add(Matrix4x4.CreateTranslation(firstPosition + new Vector3(i, 0f, 0f))); return group; } // #193 (regression from #188, 2026-07-09): WbDrawDispatcher's InstanceGroup holds // seven per-instance parallel lists — Matrices, LocalSortCenters, Slots, // LightSets, IndoorFlags, Opacities, SelectionLighting — appended in lockstep // (one entry per drawn instance) every frame. The // per-frame reset must clear ALL of them. #188 added Opacities but left it out of // the inline clear loop, so it grew one float per instance per frame forever; as // List's backing array doubled it produced ~128 MB / ~512 MB LOH float[] // arrays and leaked ~1 GB/min -> OOM after ~50 min of play. This test pins that // every parallel list is reset, so a newly-added list can't silently drift again. [Fact] public void ClearPerInstanceData_ClearsEveryParallelPerInstanceList() { var grp = new WbDrawDispatcher.InstanceGroup(); grp.Matrices.Add(Matrix4x4.Identity); grp.LocalSortCenters.Add(Vector3.Zero); grp.Slots.Add(1u); grp.LightSets.Add(WbDrawDispatcher.InstanceLightSet.Disabled); grp.IndoorFlags.Add(0u); grp.Opacities.Add(1.0f); grp.SelectionLighting.Add(new Vector2(0f, 1f)); grp.ClearPerInstanceData(); Assert.Empty(grp.Matrices); Assert.Empty(grp.LocalSortCenters); Assert.Empty(grp.Slots); Assert.Empty(grp.LightSets); Assert.Empty(grp.IndoorFlags); Assert.Empty(grp.Opacities); // #193 — the list that leaked Assert.Empty(grp.SelectionLighting); } }