using AcDream.App.Rendering.Wb; using AcDream.Core.Meshing; namespace AcDream.App.Tests.Rendering.Wb; /// /// Campaign VM VM6: — the pure /// function WbDrawDispatcher.ClassifyBatches (the world receiver) and /// WbDrawDispatcher.AddDirectionalShadowBatches (the directional- /// shadow caster) both call with the same four inputs (entity id, exclusion /// check, subset translucency, mesh-level HasCutoutSubset), which is what /// makes the two agree by construction — see foliage_wind.glsl. /// public sealed class FoliageWindClassificationTests { private const uint ProceduralSceneryEntityId = 0x80010203u; // bit 31 set private const uint OrdinaryEntityId = 0x00010203u; // bit 31 clear [Fact] public void ProceduralSceneryCutoutSubsetGetsTheCutoutFlag() { uint flags = FoliageWindClassification.Classify( ProceduralSceneryEntityId, isExcluded: false, TranslucencyKind.ClipMap, meshHasCutoutSubset: true); Assert.Equal(FoliageWindClassification.CutoutFoliageFlag, flags); } [Fact] public void ProceduralSceneryOpaqueSubsetGetsTheTrunkFlagOnlyWhenTheMeshOwnsACutoutSubset() { uint withCutoutSibling = FoliageWindClassification.Classify( ProceduralSceneryEntityId, isExcluded: false, TranslucencyKind.Opaque, meshHasCutoutSubset: true); uint withoutCutoutSibling = FoliageWindClassification.Classify( ProceduralSceneryEntityId, isExcluded: false, TranslucencyKind.Opaque, meshHasCutoutSubset: false); Assert.Equal(FoliageWindClassification.TrunkFlag, withCutoutSibling); Assert.Equal(0u, withoutCutoutSibling); // a rock, not a tree trunk } [Fact] public void NonProceduralSceneryEntityGetsNeitherBitRegardlessOfMaterial() { uint cutout = FoliageWindClassification.Classify( OrdinaryEntityId, isExcluded: false, TranslucencyKind.ClipMap, meshHasCutoutSubset: true); uint opaqueWithCutoutSibling = FoliageWindClassification.Classify( OrdinaryEntityId, isExcluded: false, TranslucencyKind.Opaque, meshHasCutoutSubset: true); Assert.Equal(0u, cutout); Assert.Equal(0u, opaqueWithCutoutSibling); } [Fact] public void AnExcludedObjectIdGetsNeitherBitEvenWhenOtherwiseQualifying() { uint cutout = FoliageWindClassification.Classify( ProceduralSceneryEntityId, isExcluded: true, TranslucencyKind.ClipMap, meshHasCutoutSubset: true); uint trunk = FoliageWindClassification.Classify( ProceduralSceneryEntityId, isExcluded: true, TranslucencyKind.Opaque, meshHasCutoutSubset: true); Assert.Equal(0u, cutout); Assert.Equal(0u, trunk); } [Theory] [InlineData(TranslucencyKind.AlphaBlend)] [InlineData(TranslucencyKind.Additive)] [InlineData(TranslucencyKind.InvAlpha)] public void OtherMaterialKindsOnProceduralSceneryGetNeitherBit(TranslucencyKind translucency) { uint flags = FoliageWindClassification.Classify( ProceduralSceneryEntityId, isExcluded: false, translucency, meshHasCutoutSubset: true); Assert.Equal(0u, flags); } // Review fix round: the classifier tests the full top-nibble 0x8000_0000, // NOT bit 31 alone — bit 31 is also set by LandblockStaticEntityIdAllocator's // 0xC... namespace (top nibble 1100) and by the synthetic render ids // 0xDA11_D0xx (paperdoll) and 0xFFFF_FF01 (portal tunnel). The prior // [InlineData(0xFFFFFFFFu, true)] case pinned exactly this bug (0xFFFFFFFFu // has bit 31 set but is NOT a procedural-scenery id under the top-nibble // rule) — it is corrected to false below. [Theory] [InlineData(0x80000000u, true)] // top nibble 0x8 exactly [InlineData(0x8FFFFFFFu, true)] // top nibble 0x8, every other bit set [InlineData(0x7FFFFFFFu, false)] // top nibble 0x7 — bit 31 clear [InlineData(0x00000000u, false)] [InlineData(0xFFFFFFFFu, false)] // top nibble 0xF — bit 31 set, NOT procedural scenery [InlineData(0xC0010203u, false)] // LandblockStaticEntityIdAllocator (top nibble 0xC) — fence/gate/building shell [InlineData(0xDA11D012u, false)] // ChargenPreviewEntityBuilder/CreatureAppraisalPresentation synthetic doll id [InlineData(0xFFFFFF01u, false)] // PortalTunnelPresentation synthetic id public void IsProceduralSceneryDecodesTheFullTopNibbleNotJustBit31(uint entityId, bool expected) { Assert.Equal(expected, FoliageWindClassification.IsProceduralScenery(entityId)); } /// /// Campaign VM VM6 review fix round (A2): GroupKey now carries /// FoliageFlags, so two otherwise-identical mesh-subset keys — same /// geometry/texture/translucency/cull mode — with different /// classification results are UNEQUAL keys, which is what makes a /// scenery instance and a non-scenery instance of the same GfxObj land /// in two separate WbDrawDispatcher groups instead of coalescing into /// one group whose flags depended on whichever entity classified it /// last. Flags are derived from real entity ids through /// — a procedural- /// scenery id (0x8…, a tree's cutout leaf subset) versus a landblock- /// static id (0xC…, e.g. a fence with a cutout mesh subset) — matching /// the review's exact pairing and its 0x2/0x0 expected words, and the /// same pairing DirectionalShadowPreparedDrawTests /// .SameSubsetDifferentFoliageClassificationNeverCoalescesIntoOneCasterBatch /// proves the caster keeps separate. /// [Fact] public void GroupKeysWithDifferentFoliageFlagsAreNeverEqualEvenWithIdenticalGeometry() { const uint proceduralSceneryTreeId = 0x80010203u; // top nibble 0x8 const uint landblockStaticFenceId = 0xC0010203u; // top nibble 0xC uint sceneryFlags = FoliageWindClassification.Classify( proceduralSceneryTreeId, isExcluded: false, TranslucencyKind.ClipMap, meshHasCutoutSubset: true); uint landblockStaticFlags = FoliageWindClassification.Classify( landblockStaticFenceId, isExcluded: false, TranslucencyKind.ClipMap, meshHasCutoutSubset: true); Assert.Equal(FoliageWindClassification.CutoutFoliageFlag, sceneryFlags); // 0x2 Assert.Equal(0u, landblockStaticFlags); // 0x0 var sceneryKey = new GroupKey( FirstIndex: 100, BaseVertex: 5, IndexCount: 12, TextureSlot: new AcDream.App.Rendering.Gpu.GpuTextureSlot(42), TextureLayer: 0, Translucency: TranslucencyKind.ClipMap, CullMode: DatReaderWriter.Enums.CullMode.CounterClockwise, FoliageFlags: sceneryFlags); var landblockStaticKey = sceneryKey with { FoliageFlags = landblockStaticFlags }; // Two otherwise-identical mesh-subset keys, differing only by the // entity-driven classification, are unequal — this is what makes // WbDrawDispatcher.GetOrCreateInstanceGroup place the two instances // in two separate InstanceGroups instead of coalescing them. Assert.NotEqual(sceneryKey, landblockStaticKey); } /// /// Campaign VM VM6 review fix round (A4): ComputeEntityHasCutoutSubset /// ORs across every part regardless of WHICH part carries the cutout /// subset — this pins the "two-part Setup, part B has cutout" scenario /// at the algorithm level, decoupled from ObjectRenderData/mesh-adapter /// plumbing. /// [Theory] [InlineData(new[] { false, false }, false)] [InlineData(new[] { true, false }, true)] [InlineData(new[] { false, true }, true)] // part B (index 1) has the cutout [InlineData(new[] { true, true }, true)] public void ComputeEntityHasCutoutSubsetOrsAcrossEveryPart(bool[] partHasCutout, bool expected) { bool result = FoliageWindClassification.ComputeEntityHasCutoutSubset( partHasCutout, context: 0, static (_, value) => value); Assert.Equal(expected, result); } [Fact] public void ComputeEntityHasCutoutSubsetIsFalseForAnEmptyPartList() { Assert.False( FoliageWindClassification.ComputeEntityHasCutoutSubset( Array.Empty(), context: 0, static (_, value) => value)); } /// /// Campaign VM VM6 review fix round (A4), end to end at the /// classification level: a two-part Setup — part A is an opaque trunk /// with no cutout batches of its own, part B is the leaves (has a /// cutout subset). Classifying part A's opaque subset from its OWN /// (false) HasCutoutSubset never yields the trunk flag (the pre-fix /// bug); classifying it from the entity-scoped OR that /// ComputeEntityHasCutoutSubset computes across BOTH parts correctly /// yields TrunkFlag. Both WbDrawDispatcher.ClassifyBatches (receiver) /// and AddDirectionalShadowBatches (caster) now pass the entity-scoped /// value here instead of the part's own. /// [Fact] public void TwoPartSetupGivesTheOpaqueTrunkPartTheTrunkFlagWhenAnotherPartHasCutout() { const uint sceneryEntityId = 0x80010203u; bool[] partAHasCutoutSubset = [false]; // the trunk part's OWN mesh has no cutout batches bool[] setupPartsHasCutoutSubset = [false, true]; // part A (trunk), part B (leaves) bool partAOwnHasCutoutSubset = FoliageWindClassification.ComputeEntityHasCutoutSubset( partAHasCutoutSubset, context: 0, static (_, value) => value); bool entityScopedHasCutoutSubset = FoliageWindClassification.ComputeEntityHasCutoutSubset( setupPartsHasCutoutSubset, context: 0, static (_, value) => value); uint flagsFromPartOwnValue = FoliageWindClassification.Classify( sceneryEntityId, isExcluded: false, TranslucencyKind.Opaque, partAOwnHasCutoutSubset); uint flagsFromEntityScopedValue = FoliageWindClassification.Classify( sceneryEntityId, isExcluded: false, TranslucencyKind.Opaque, entityScopedHasCutoutSubset); Assert.Equal(0u, flagsFromPartOwnValue); // the pre-fix bug: no flag at all Assert.Equal(FoliageWindClassification.TrunkFlag, flagsFromEntityScopedValue); } }