Narrow re-review of a82959f1: APPROVE, with follow-ups. All items landed.
N2 (structural): (a) extracted the ONE shared InstanceGroup-from-key
construction seam, WbDrawDispatcher.CreateGroupFromKey(key, registration,
frame) — before this there were two near-identical `new InstanceGroup
{ ... }` initializers (GetOrCreateInstanceGroup and GetOrCreatePackedGroup)
that had already drifted once (the round-2 F1 bug). Both routes call it now;
CreateGroupFromKey's own `new()` is the only production InstanceGroup
construction site repo-wide, same precedent as AppendPackedInstance. (b)
GroupKey.FoliageFlags lost its `= 0u` default and moved before CullMode in
the declaration (CullMode keeps its default, C# requires optional params to
trail required ones), so a `new GroupKey(...)` that omits it is a compile
error. Fixed every real construction site the reorder/requirement touched:
the 2 production sites, ToKey (a reconstruction from InstanceGroup the
review didn't count but the reorder broke), and 5 test sites (one more than
the review's "4" — InstanceGroupClearTests had a second, implicit
target-typed `MakeKey` factory the original count missed). Verified by a
full solution build.
N1: added CreateGroupFromKey_CopiesFoliageFlagsFromTheKey
(InstanceGroupClearTests) — a key carrying FoliageFlags 0x2 in, the created
group's FoliageFlags 0x2 out. That test plus N2b's required field are what
actually guard the round-2 F1 blocker; reworded PackedDispatcherOracleTests'
existing test comment to say what IT proves (the classification-to-
BuildIndirectArrays-to-BatchData.flags path), not that it guards the
classifier.
N3: corrected the plan's round-2 paragraph — folding FoliageFlags into the
G2/G3 digest is correct and symmetric, but CompareClassifiedOutput only
runs from RenderScenePViewFrameProductController.BuildAndCompare, which has
no production caller anywhere in src/AcDream.App/, and both of
RenderScenePViewFrameProductTests's own callers construct the controller
without the optional dispatcher argument — so the fold catches nothing
until that oracle is wired to an actual caller.
N4: the plan's F6 note now names both classification caches — the classic
route's EntityClassificationCache.EntityCacheEntry (self-heals per entity
on its own next eviction) and the packed route's
PackedProjectionClassificationEntry/PackedClassifiedBatch.Key
(PackedProjectionClassificationCache.BeginFrame clears its entire cache in
one shot on a RenderSceneGeneration change) — and notes neither mechanism
is keyed to a pack switch specifically.
N5: deleted the now-unused single-generic ComputeEntityHasCutoutSubset<T>
overload; its 4 test call sites now use the two-generic, zero-alloc
overload with an unused int context and a static (_, value) => value
lambda, so there is exactly one ComputeEntityHasCutoutSubset to keep
correct.
A6 (reviewer-filed): ResolveFoliageWind's _windMean/_windGust started at 0
and always eased toward the weather target by clock delta, with no
distinction for a graph's first-ever advance. A pinned clock
(ACDREAM_SKY_PHASE_SECONDS, the offline pixel gate's determinism pin) never
advances between calls, so the wind reached only whatever fraction the
first (1-second-clamped) step produced and sat there forever; live, the
first 10 s after a graph is constructed (pack selection / login) spun up
from dead calm even though the weather already IS what it is. Fixed at the
root: the first advance (_windFrameSerial == -1, the constructor sentinel)
now snaps _windMean/_windGust straight to the target; every later advance
eases over WeatherSystem.TransitionSeconds exactly as before. Added a
SetWindClockSecondsOverrideForTesting seam (_windClockSecondsOverride is no
longer readonly) so a hermetic test can advance the pinned clock by an
exact amount between two resolves without a real-time Thread.Sleep; two new
tests prove the first-advance snap is exact and a second advance still
eases at the normal rate. The three existing indoor/wind-disabled/amplitude
gate tests pass unchanged.
Verify: Release build 0 warnings/0 errors. App hermetic-lane filter
6,046/0 failed. Core.Tests 4,695/0 failed. Full hermetic-filtered solution:
15,274/0 failed across 15 projects.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
442 lines
16 KiB
C#
442 lines
16 KiB
C#
using System.Numerics;
|
|
using AcDream.App.Rendering.Scene;
|
|
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, Slot(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<WbDrawDispatcher.InstanceGroup>();
|
|
var transparent = new List<WbDrawDispatcher.InstanceGroup>();
|
|
|
|
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 DispatcherFingerprint_EqualDistanceAlphaUsesOriginalSubmissionOrder()
|
|
{
|
|
WbDrawDispatcher.InstanceGroup first = MakeCompleteGroup(
|
|
textureSlot: 0xAA,
|
|
submissionOrder: 0);
|
|
WbDrawDispatcher.InstanceGroup second = MakeCompleteGroup(
|
|
textureSlot: 0xBB,
|
|
submissionOrder: 1);
|
|
var scratch = new List<WbDrawDispatcher.AlphaFingerprint>();
|
|
|
|
var forward = WbDrawDispatcher.CreateDispatcherSubmission(
|
|
visibleInstanceCount: 2,
|
|
immediateInstanceCount: 0,
|
|
deferTransparent: true,
|
|
opaque: [],
|
|
transparent: [first, second],
|
|
cameraWorldPosition: Vector3.Zero,
|
|
alphaScratch: scratch);
|
|
var reversedMaterialGroups =
|
|
WbDrawDispatcher.CreateDispatcherSubmission(
|
|
visibleInstanceCount: 2,
|
|
immediateInstanceCount: 0,
|
|
deferTransparent: true,
|
|
opaque: [],
|
|
transparent: [second, first],
|
|
cameraWorldPosition: Vector3.Zero,
|
|
alphaScratch: scratch);
|
|
|
|
Assert.Equal(
|
|
forward.TransparentDigest,
|
|
reversedMaterialGroups.TransparentDigest);
|
|
|
|
first.SubmissionOrders[0] = 1;
|
|
second.SubmissionOrders[0] = 0;
|
|
var reversedSubmission = WbDrawDispatcher.CreateDispatcherSubmission(
|
|
visibleInstanceCount: 2,
|
|
immediateInstanceCount: 0,
|
|
deferTransparent: true,
|
|
opaque: [],
|
|
transparent: [first, second],
|
|
cameraWorldPosition: Vector3.Zero,
|
|
alphaScratch: scratch);
|
|
|
|
Assert.NotEqual(
|
|
forward.TransparentDigest,
|
|
reversedSubmission.TransparentDigest);
|
|
Assert.Equal(
|
|
forward.TransparentSetDigest,
|
|
reversedSubmission.TransparentSetDigest);
|
|
}
|
|
|
|
[Fact]
|
|
public void DispatcherFingerprint_ZeroVisibleInstancesIgnoresStaleGroupStorage()
|
|
{
|
|
WbDrawDispatcher.InstanceGroup stale = MakeCompleteGroup(
|
|
textureSlot: 0xAA,
|
|
submissionOrder: 0);
|
|
var scratch = new List<WbDrawDispatcher.AlphaFingerprint>();
|
|
|
|
CurrentRenderDispatcherSubmission expected =
|
|
WbDrawDispatcher.CreateDispatcherSubmission(
|
|
visibleInstanceCount: 0,
|
|
immediateInstanceCount: 0,
|
|
deferTransparent: false,
|
|
opaque: [],
|
|
transparent: [],
|
|
cameraWorldPosition: Vector3.Zero,
|
|
alphaScratch: scratch);
|
|
CurrentRenderDispatcherSubmission actual =
|
|
WbDrawDispatcher.CreateDispatcherSubmission(
|
|
visibleInstanceCount: 0,
|
|
immediateInstanceCount: 0,
|
|
deferTransparent: false,
|
|
opaque: [stale],
|
|
transparent: [stale],
|
|
cameraWorldPosition: Vector3.Zero,
|
|
alphaScratch: scratch);
|
|
|
|
Assert.Equal(expected, actual);
|
|
}
|
|
|
|
[Fact]
|
|
public void DispatcherFingerprint_IncludesRetailDetailCategory()
|
|
{
|
|
WbDrawDispatcher.InstanceGroup ordinary = MakeCompleteGroup(
|
|
textureSlot: 0xAA,
|
|
submissionOrder: 0);
|
|
WbDrawDispatcher.InstanceGroup building = MakeCompleteGroup(
|
|
textureSlot: 0xAA,
|
|
submissionOrder: 0);
|
|
building.DetailCategories[0] = 1u;
|
|
var scratch = new List<WbDrawDispatcher.AlphaFingerprint>();
|
|
|
|
CurrentRenderDispatcherSubmission ordinarySubmission =
|
|
WbDrawDispatcher.CreateDispatcherSubmission(
|
|
visibleInstanceCount: 1,
|
|
immediateInstanceCount: 0,
|
|
deferTransparent: true,
|
|
opaque: [],
|
|
transparent: [ordinary],
|
|
cameraWorldPosition: Vector3.Zero,
|
|
alphaScratch: scratch);
|
|
CurrentRenderDispatcherSubmission buildingSubmission =
|
|
WbDrawDispatcher.CreateDispatcherSubmission(
|
|
visibleInstanceCount: 1,
|
|
immediateInstanceCount: 0,
|
|
deferTransparent: true,
|
|
opaque: [],
|
|
transparent: [building],
|
|
cameraWorldPosition: Vector3.Zero,
|
|
alphaScratch: scratch);
|
|
|
|
Assert.NotEqual(
|
|
ordinarySubmission.TransparentDigest,
|
|
buildingSubmission.TransparentDigest);
|
|
Assert.NotEqual(
|
|
ordinarySubmission.TransparentSetDigest,
|
|
buildingSubmission.TransparentSetDigest);
|
|
}
|
|
|
|
[Fact]
|
|
public void CachedGroupHandle_RequiresLiveMatchingRegistration()
|
|
{
|
|
var group = new WbDrawDispatcher.InstanceGroup { Registration = 17 };
|
|
var key = new GroupKey(
|
|
FirstIndex: 0,
|
|
BaseVertex: 0,
|
|
IndexCount: 6,
|
|
TextureSlot: Slot(0xAA),
|
|
TextureLayer: 0,
|
|
Translucency: TranslucencyKind.Opaque,
|
|
FoliageFlags: 0u);
|
|
var cached = new CachedBatch(
|
|
key,
|
|
Slot(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 _));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Campaign VM VM6 review fix round 3 (N1): this is the guard for the
|
|
/// round-2 F1 blocker (the packed classifier's InstanceGroup carried
|
|
/// FoliageFlags == 0 no matter what GroupKey.FoliageFlags said, because
|
|
/// GetOrCreatePackedGroup's own initializer never copied it). N2a
|
|
/// deleted that possibility structurally — CreateGroupFromKey is the
|
|
/// ONLY InstanceGroup-from-key construction site left in production,
|
|
/// used by BOTH the classic and packed routes — and N2b made
|
|
/// GroupKey.FoliageFlags a required constructor argument, so a future
|
|
/// caller cannot silently default it to 0 either. Together those two
|
|
/// changes are what actually guards F1; this test pins
|
|
/// CreateGroupFromKey's own copy so a regression there fails here
|
|
/// directly instead of only showing up as a visual "trees don't sway"
|
|
/// report.
|
|
/// </summary>
|
|
[Fact]
|
|
public void CreateGroupFromKey_CopiesFoliageFlagsFromTheKey()
|
|
{
|
|
var key = new GroupKey(
|
|
FirstIndex: 10,
|
|
BaseVertex: 2,
|
|
IndexCount: 18,
|
|
TextureSlot: Slot(0x77),
|
|
TextureLayer: 0,
|
|
Translucency: TranslucencyKind.ClipMap,
|
|
FoliageFlags: 0x2u,
|
|
CullMode: DatReaderWriter.Enums.CullMode.CounterClockwise);
|
|
|
|
WbDrawDispatcher.InstanceGroup group =
|
|
WbDrawDispatcher.CreateGroupFromKey(key, registration: 5, frame: 9);
|
|
|
|
Assert.Equal(0x2u, group.FoliageFlags);
|
|
Assert.Equal(5, group.Registration);
|
|
Assert.Equal(9, group.LastUsedFrame);
|
|
Assert.Equal(key.FirstIndex, group.FirstIndex);
|
|
Assert.Equal(key.BaseVertex, group.BaseVertex);
|
|
Assert.Equal(key.IndexCount, group.IndexCount);
|
|
Assert.Equal(key.TextureSlot, group.TextureSlot);
|
|
Assert.Equal(key.TextureLayer, group.TextureLayer);
|
|
Assert.Equal(key.Translucency, group.Translucency);
|
|
Assert.Equal(key.CullMode, group.CullMode);
|
|
}
|
|
|
|
[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<GroupKey, WbDrawDispatcher.InstanceGroup>
|
|
{
|
|
[liveKey] = live,
|
|
[retiredKey] = retired,
|
|
};
|
|
var retiredKeys = new List<GroupKey>();
|
|
var staleCache = new CachedBatch(
|
|
retiredKey,
|
|
retiredKey.TextureSlot,
|
|
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<GroupKey, WbDrawDispatcher.InstanceGroup>
|
|
{
|
|
[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 AcDream.App.Rendering.Gpu.GpuTextureSlot Slot(uint index) => new(index);
|
|
private static GroupKey MakeKey(uint textureSlot) => new(
|
|
FirstIndex: 0,
|
|
BaseVertex: 0,
|
|
IndexCount: 6,
|
|
TextureSlot: Slot(textureSlot),
|
|
TextureLayer: 0,
|
|
Translucency: TranslucencyKind.Opaque,
|
|
FoliageFlags: 0u);
|
|
|
|
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;
|
|
}
|
|
|
|
private static WbDrawDispatcher.InstanceGroup MakeCompleteGroup(
|
|
uint textureSlot,
|
|
int submissionOrder)
|
|
{
|
|
var group = new WbDrawDispatcher.InstanceGroup
|
|
{
|
|
FirstIndex = 0,
|
|
BaseVertex = 0,
|
|
IndexCount = 6,
|
|
TextureSlot = Slot(textureSlot),
|
|
TextureLayer = 0,
|
|
Translucency = TranslucencyKind.AlphaBlend,
|
|
};
|
|
group.Matrices.Add(Matrix4x4.CreateTranslation(10f, 0f, 0f));
|
|
group.LocalSortCenters.Add(Vector3.Zero);
|
|
group.SubmissionOrders.Add(submissionOrder);
|
|
group.Slots.Add(0u);
|
|
group.LightSets.Add(WbDrawDispatcher.InstanceLightSet.Disabled);
|
|
group.IndoorFlags.Add(0u);
|
|
group.DetailCategories.Add(0u);
|
|
group.Opacities.Add(1f);
|
|
group.SelectionLighting.Add(new Vector2(0f, 1f));
|
|
return group;
|
|
}
|
|
|
|
// #193 (regression from #188, 2026-07-09): WbDrawDispatcher's InstanceGroup holds
|
|
// nine per-instance parallel lists — Matrices, LocalSortCenters,
|
|
// SubmissionOrders, Slots, LightSets, IndoorFlags, Opacities, and
|
|
// DetailCategories, 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<float>'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.SubmissionOrders.Add(0);
|
|
grp.Slots.Add(1u);
|
|
grp.LightSets.Add(WbDrawDispatcher.InstanceLightSet.Disabled);
|
|
grp.IndoorFlags.Add(0u);
|
|
grp.DetailCategories.Add(1u);
|
|
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.SubmissionOrders);
|
|
Assert.Empty(grp.Slots);
|
|
Assert.Empty(grp.LightSets);
|
|
Assert.Empty(grp.IndoorFlags);
|
|
Assert.Empty(grp.DetailCategories);
|
|
Assert.Empty(grp.Opacities); // #193 — the list that leaked
|
|
Assert.Empty(grp.SelectionLighting);
|
|
}
|
|
}
|