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