Resolve exact SetSurface blend, alpha-test, and fog state once during extraction and preserve it through recipe-10 prepared payloads, Wb/EnvCell command data, Vulkan pipelines, push constants, and both ordinary/atmospheric one-pass shaders. Preserve AP-240 Wb pure-Clip immediate opaque/A2C while EnvCell uses retail premultiplied Clip; detail-off routing remains unchanged. Correct AP-232 and register the remaining detail-off state divergence. Mutation first failures (all restored): - raw Add->SRCALPHA/ONE: WalkStaticStreamPopulatorTests.ImmediateBuildingDetail_UsesExactResolvedSetSurfaceState line 1278, expected wb-mesh-raw-additive-1x. - inverse-add->alpha-add: same test line 1278, expected wb-mesh-inverse-additive-1x. - remove Env inverse: EnvCellAlphaDrawSourceTests.DetailOn_EveryEnvCellFamilyDrawsOnceInPlaceWithAuthoredOpacity line 132, expected envcell-inverse. - raw IsAdditive precedence: same test line 132, Translucent|Clip|Additive expected envcell-alpha. - Wb paletted ParamB=0: OrderPreservingSubmitterTests line 333, expected 0.392156869. - Wb DDS ParamB=0.05: same test line 333, expected 0.784313738. - disable Alpha+Clip test: WalkStaticStreamPopulatorTests line 1286, expected 0.784313738. - always fog raw Add: same test line 1287, expected no-fog true. - disable fog non-Add: same test line 1287, expected no-fog false. - X=a*qA: RetailDetailTextureContractTests line 261, shared squared-alpha substring absent. - X includes base alpha: same test line 262, forbidden baseTexel.a present. - CLIP uses 0.05: EnvCellAlphaDrawSourceTests.ClipShaders_UseGreaterEqualForThePerRangeReference line 367. - second detail draw: EnvCell detail-on line 131, collection contained 2 draws. - straight-alpha substitute: Wb immediate line 1278, expected wb-mesh-additive-1x. - omit ordered detail arm: OrderPreservingSubmitterTests line 318, expected (77,3.5), got (0,0). - omit atmospheric combine: RetailDetailTextureContractTests line 260, shared include absent. - drop serialized opacity: ObjectMeshDataSerializerTests line 292, expected opacity bits, got 1.0. - stale detail arm: atmospheric adjacency line 402, expected slot 0, got 77. - per-frame surface map: EnvCell warmed allocation line 285, expected 0 B, got 204800 B.
251 lines
11 KiB
C#
251 lines
11 KiB
C#
using AcDream.App.Rendering.Wb;
|
|
using AcDream.Core.Meshing;
|
|
|
|
namespace AcDream.App.Tests.Rendering.Wb;
|
|
|
|
/// <summary>
|
|
/// Campaign VM VM6: <see cref="FoliageWindClassification"/> — the pure
|
|
/// function <c>WbDrawDispatcher.ClassifyBatches</c> (the world receiver) and
|
|
/// <c>WbDrawDispatcher.AddDirectionalShadowBatches</c> (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.
|
|
/// </summary>
|
|
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));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// <see cref="FoliageWindClassification.Classify"/> — 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 <c>DirectionalShadowPreparedDrawTests
|
|
/// .SameSubsetDifferentFoliageClassificationNeverCoalescesIntoOneCasterBatch</c>
|
|
/// proves the caster keeps separate.
|
|
/// </summary>
|
|
[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,
|
|
MaterialState: RetailSetSurfaceMaterialState.Opaque,
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[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<bool>(),
|
|
context: 0,
|
|
static (_, value) => value));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
}
|