acdream/tests/AcDream.App.Tests/Rendering/Wb/DirectionalShadowPreparedDrawTests.cs
Erik 43e3abed4d fix(render): correct foliage-wind classification, receiver/caster desync, and frame binding (Campaign VM VM6 review)
Opus dual-lens review of the three VM6 commits (0930c35d, 39e8408c,
6cc5e183) found two blockers and two should-fix issues; all landed here
along with the review's nits and documentation corrections.

Blockers:
- A1: the procedural-scenery classifier tested bit 31 alone instead of
  the full top nibble (0xF000_0000 == 0x8000_0000), so it also matched
  LandblockStaticEntityIdAllocator's 0xC... namespace (fences/gates/
  building shells with a cutout subset), the 0xDA11_D0xx paperdoll id,
  and the 0xFFFF_FF01 portal-tunnel id as procedural scenery — all
  three would have swayed. ProceduralSceneryIdAllocator.IsInNamespace
  now does the exact top-nibble test; FoliageWindClassification
  delegates to it.
- A2: GroupKey (the receiver's instance-batching key) did not carry
  FoliageFlags while the caster's dedup key already did, so a scenery
  instance and a non-scenery instance sharing a mesh subset coalesced
  into one receiver InstanceGroup whose flags were last-writer-wins —
  disagreeing with the correctly-keyed caster. GroupKey now carries
  FoliageFlags, computed before key construction and set exactly once
  at group creation; the imperative re-stamp is gone, and CachedBatch's
  now-redundant FoliageFlags field is removed.

Should-fix:
- A3: the world receiver pass bound UniformAtmosphericFrame only by
  accident (leftover from the caster pass, which runs first each
  frame, since Vulkan binding state isn't reset between passes).
  DirectionalShadowFrameBinding now carries the caster's exact
  AtmosphericFrameBufferBinding and BindDirectionalShadowReceiver binds
  it explicitly.
- A4: a Setup-composed tree's opaque trunk part never got the trunk
  flag because HasCutoutSubset is cached per GfxObj part, not per
  entity. FoliageWindClassification.ComputeEntityHasCutoutSubset now
  ORs HasCutoutSubset across an entity's resolved sibling parts once
  per entity, threaded into ClassifyBatches/AddDirectionalShadowBatches
  via a new optional override parameter.

Nits: A5 hashes the per-vertex flutter seed relative to the instance
origin instead of absolute world XY (fp32 sin() precision loss at far
landblock corners), mirrored in both foliage_wind.glsl and
FoliageWindModel; A7 documents the max(maxHeight, 0.5) divide-guard as
a deliberate pseudocode divergence; A8 switches FoliageWindExclusions'
construction to ToFrozenSet() and softens the "never stale" doc
comment to "no slower than one frame behind."

Tests added: top-nibble classification (0xFFFFFFFFu now correctly
false), GroupKey inequality across entity-driven scenery/landblock-
static classification, a caster-batch test proving the same pairing
never coalesces, ComputeEntityHasCutoutSubset unit + end-to-end
two-part-Setup tests, the caster→receiver AtmosphericFrame binding
carry-through, flutter-hash translation invariance relative to
instance origin, and a Storm-wind mid-height displacement floor
guarding against a "no motion" regression.

Docs: plan VM6 body corrected to the five-row WeatherKind table, "bits
1 and 2", "all four" caster shaders, and top-nibble wording throughout;
the owner gate checklist's Rain/Storm step; the stale v1-only shader-
interface compatibility entry; semantic-bindings-v1.md's v2 members
folded into the main 192-byte block; the IA-25 register row's top-
nibble wording; AtmosphericFrameInputs.cs's ABI size reference.

foliage_wind.glsl's A5 change recompiled exactly the five shaders that
include it (mesh_atmospheric.vert, the four directional_shadow_world_*
casters) plus the manifest; no other .spv changed.

Verify: Release build 0 warnings/0 errors. App hermetic-lane filter
6,041/0 failed (no environment-specific failures this run).
RenderPackValidator 30/30. Full hermetic-filtered solution: 15,269/0
failed across 15 projects.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 01:54:36 +02:00

601 lines
25 KiB
C#

using System.Numerics;
using System.Runtime.InteropServices;
using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Scene;
using AcDream.App.Rendering.Wb;
using AcDream.Core.Meshing;
using AcDream.Core.World;
using DatReaderWriter.Enums;
namespace AcDream.App.Tests.Rendering.Wb;
public sealed class DirectionalShadowPreparedDrawTests
{
[Theory]
[InlineData(TranslucencyKind.Opaque, true, DirectionalShadowCasterMaterial.Opaque)]
[InlineData(TranslucencyKind.ClipMap, true, DirectionalShadowCasterMaterial.AlphaCutout)]
[InlineData(TranslucencyKind.AlphaBlend, false, DirectionalShadowCasterMaterial.Opaque)]
[InlineData(TranslucencyKind.Additive, false, DirectionalShadowCasterMaterial.Opaque)]
[InlineData(TranslucencyKind.InvAlpha, false, DirectionalShadowCasterMaterial.Opaque)]
internal void MaterialPolicy_PreservesCutoutsAndExcludesTrueTransparency(
TranslucencyKind source,
bool expectedAccepted,
DirectionalShadowCasterMaterial expectedMaterial)
{
bool accepted = DirectionalShadowPreparedDraws.TryClassifyMaterial(
source,
out DirectionalShadowCasterMaterial material);
Assert.Equal(expectedAccepted, accepted);
if (accepted)
Assert.Equal(expectedMaterial, material);
}
[Theory]
[InlineData(0f, false)]
[InlineData(0.0001f, true)]
[InlineData(1f, true)]
[InlineData(float.NaN, true)]
internal void FadePolicy_OnlyExactOpaquePartsCast(
float translucency,
bool excluded)
{
Assert.Equal(
excluded,
DirectionalShadowPreparedDraws.FadeExcludesCaster(translucency));
}
[Fact]
public void Complete_GroupsCommandsAndRetainsExactCurrentTransforms()
{
var product = new DirectionalShadowPreparedDraws();
RenderSceneGeneration generation = RenderSceneGeneration.FromRaw(4);
Assert.True(product.TryBegin(generation, 7, estimatedInstances: 3));
Matrix4x4 first = Matrix4x4.CreateRotationZ(0.25f)
* Matrix4x4.CreateTranslation(10f, 20f, 30f);
Matrix4x4 second = Matrix4x4.CreateScale(1.25f)
* Matrix4x4.CreateTranslation(-2f, 3f, 7f);
Matrix4x4 leaves = Matrix4x4.CreateRotationX(0.5f)
* Matrix4x4.CreateTranslation(100f, 200f, 12f);
product.Add(
firstIndex: 40,
baseVertex: 3,
indexCount: 18,
GpuTextureSlot.Unassigned,
textureLayer: 0,
CullMode.CounterClockwise,
DirectionalShadowCasterMaterial.Opaque,
in first);
product.Add(
firstIndex: 40,
baseVertex: 3,
indexCount: 18,
GpuTextureSlot.Unassigned,
textureLayer: 0,
CullMode.CounterClockwise,
DirectionalShadowCasterMaterial.Opaque,
in second);
product.Add(
firstIndex: 90,
baseVertex: 11,
indexCount: 24,
new GpuTextureSlot(17),
textureLayer: 6,
CullMode.None,
DirectionalShadowCasterMaterial.AlphaCutout,
in leaves);
var inputStats = new DirectionalShadowPreparationStats(
SourceCasters: 2,
SourceMeshRefs: 3,
SourceParts: 3,
SourceBatches: 5,
PreparedInstances: 0,
PreparedOpaqueCommands: 0,
PreparedAlphaCutoutCommands: 0,
RejectedTransparentBatches: 2,
RejectedFadedParts: 1,
MissingMeshes: 0,
UnresolvedAlphaCutoutTextures: 0);
product.Complete(generation, 7, in inputStats);
Assert.Equal(3, product.Transforms.Length);
Assert.Equal(2, product.Commands.Length);
Assert.Equal(1, product.OpaqueCommandCount);
Assert.Equal(1, product.AlphaCutoutCommandCount);
Assert.Single(product.OpaqueCommands.ToArray());
Assert.Single(product.AlphaCutoutCommands.ToArray());
Assert.Single(product.OpaqueBatches.ToArray());
Assert.Single(product.AlphaCutoutBatches.ToArray());
Assert.Single(product.OpaqueRuns.ToArray());
Assert.Single(product.AlphaCutoutRuns.ToArray());
Assert.Equal(0, product.OpaqueRuns[0].StartCommand);
Assert.Equal(1, product.AlphaCutoutRuns[0].StartCommand);
Assert.Equal(2u, product.Commands[0].InstanceCount);
Assert.Equal(0u, product.Commands[0].BaseInstance);
Assert.Equal(1u, product.Commands[1].InstanceCount);
Assert.Equal(2u, product.Commands[1].BaseInstance);
Assert.Equal(DirectionalShadowCasterMaterial.Opaque, product.Batches[0].Material);
Assert.False(product.Batches[0].TextureSlot.IsAssigned);
Assert.Equal(DirectionalShadowCasterMaterial.AlphaCutout, product.Batches[1].Material);
Assert.Equal(17u, product.Batches[1].TextureSlot.Index);
Assert.Equal(6u, product.Batches[1].TextureLayer);
Assert.Contains(first, product.Transforms.ToArray());
Assert.Contains(second, product.Transforms.ToArray());
Assert.Equal(leaves, product.Transforms[2]);
Assert.Equal(3, product.Stats.PreparedInstances);
Assert.Equal(2, product.Stats.RejectedTransparentBatches);
Assert.Equal(1, product.Stats.RejectedFadedParts);
}
/// <summary>
/// Campaign VM VM6 review fix round (A2): two instances of the IDENTICAL
/// mesh subset (same index range, texture, material, cull mode) — one a
/// procedural-scenery entity (top nibble 0x8), one a
/// LandblockStaticEntityIdAllocator entity (top nibble 0xC) sharing the
/// SAME GfxObj — classify to different FoliageWindClassification flags
/// (0x2 cutout vs 0x0) via the identical Classify call the world
/// receiver uses, and must land as two SEPARATE prepared caster
/// batches/commands rather than coalescing into one. This is the
/// caster-side half of "casters and receivers agree by construction":
/// DirectionalShadowDrawKey (the caster's sort/group key, unlike the
/// pre-fix receiver GroupKey) already includes FoliageFlags, so this
/// pins that it stays correct.
/// </summary>
[Fact]
public void SameSubsetDifferentFoliageClassificationNeverCoalescesIntoOneCasterBatch()
{
const uint sceneryEntityId = 0x80010203u; // ProceduralSceneryIdAllocator
const uint landblockStaticEntityId = 0xC0010203u; // LandblockStaticEntityIdAllocator
uint sceneryFlags = FoliageWindClassification.Classify(
sceneryEntityId,
isExcluded: false,
TranslucencyKind.ClipMap,
meshHasCutoutSubset: true);
uint landblockStaticFlags = FoliageWindClassification.Classify(
landblockStaticEntityId,
isExcluded: false,
TranslucencyKind.ClipMap,
meshHasCutoutSubset: true);
Assert.Equal(FoliageWindClassification.CutoutFoliageFlag, sceneryFlags);
Assert.Equal(0u, landblockStaticFlags);
var product = new DirectionalShadowPreparedDraws();
RenderSceneGeneration generation = RenderSceneGeneration.FromRaw(1);
Assert.True(product.TryBegin(generation, 1, estimatedInstances: 2));
Matrix4x4 sceneryTransform = Matrix4x4.CreateTranslation(1f, 2f, 3f);
Matrix4x4 landblockStaticTransform = Matrix4x4.CreateTranslation(9f, 8f, 7f);
GpuTextureSlot sharedTexture = new(42);
product.Add(
firstIndex: 100,
baseVertex: 5,
indexCount: 12,
sharedTexture,
textureLayer: 0,
CullMode.CounterClockwise,
DirectionalShadowCasterMaterial.AlphaCutout,
in sceneryTransform,
sceneryFlags);
product.Add(
firstIndex: 100,
baseVertex: 5,
indexCount: 12,
sharedTexture,
textureLayer: 0,
CullMode.CounterClockwise,
DirectionalShadowCasterMaterial.AlphaCutout,
in landblockStaticTransform,
landblockStaticFlags);
product.Complete(
generation,
1,
new DirectionalShadowPreparationStats(
SourceCasters: 2,
SourceMeshRefs: 2,
SourceParts: 2,
SourceBatches: 2,
PreparedInstances: 0,
PreparedOpaqueCommands: 0,
PreparedAlphaCutoutCommands: 0,
RejectedTransparentBatches: 0,
RejectedFadedParts: 0,
MissingMeshes: 0,
UnresolvedAlphaCutoutTextures: 0));
// Two distinct commands/batches, NOT one command with InstanceCount=2
// — the identical geometry/texture/material would have coalesced
// pre-fix, since only entity-level classification (which the caster
// key did not carry before A2) tells them apart.
Assert.Equal(2, product.Commands.Length);
Assert.Equal(2, product.Batches.Length);
Assert.All(product.Commands.ToArray(), command => Assert.Equal(1u, command.InstanceCount));
int sceneryIndex = product.Batches.ToArray()
.ToList()
.FindIndex(batch => batch.FoliageFlags == FoliageWindClassification.CutoutFoliageFlag);
int landblockStaticIndex = product.Batches.ToArray()
.ToList()
.FindIndex(batch => batch.FoliageFlags == 0u);
Assert.True(sceneryIndex >= 0, "expected one prepared batch carrying the cutout foliage flag");
Assert.True(landblockStaticIndex >= 0, "expected one prepared batch carrying zero foliage flags");
Assert.NotEqual(sceneryIndex, landblockStaticIndex);
Assert.Equal(
sceneryTransform,
product.Transforms[(int)product.Commands[sceneryIndex].BaseInstance]);
Assert.Equal(
landblockStaticTransform,
product.Transforms[(int)product.Commands[landblockStaticIndex].BaseInstance]);
}
[Fact]
public void SameCasterBuild_ReplaysWithoutReclassificationOrStorageGrowth()
{
var product = new DirectionalShadowPreparedDraws();
RenderSceneGeneration generation = RenderSceneGeneration.FromRaw(8);
Matrix4x4 exactMeshRefTransform = new(
1, 2, 3, 4,
5, 6, 7, 8,
9, 10, 11, 12,
13, 14, 15, 16);
Assert.True(product.TryBegin(generation, 20, estimatedInstances: 1));
product.Add(
1,
2,
3,
GpuTextureSlot.Unassigned,
0,
CullMode.Clockwise,
DirectionalShadowCasterMaterial.Opaque,
in exactMeshRefTransform);
DirectionalShadowPreparationStats stats = default;
product.Complete(generation, 20, in stats);
long retained = product.RetainedScratchBytes;
ulong buildSequence = product.BuildSequence;
Assert.False(product.TryBegin(generation, 20, estimatedInstances: 100));
Assert.Equal(buildSequence, product.BuildSequence);
Assert.Equal(retained, product.RetainedScratchBytes);
Assert.Equal(exactMeshRefTransform, product.Transforms[0]);
Assert.Single(product.Commands.ToArray());
}
[Fact]
public void StableTopology_ComposesSlimPoseWithoutMutatingCasterOrAllocating()
{
RenderSceneGeneration generation = RenderSceneGeneration.FromRaw(10);
Matrix4x4 originalRoot = Matrix4x4.CreateTranslation(1f, 2f, 3f);
Matrix4x4 originalPart = Matrix4x4.CreateTranslation(4f, 5f, 6f);
Matrix4x4 setupPart = Matrix4x4.CreateRotationX(0.25f)
* Matrix4x4.CreateTranslation(7f, 8f, 9f);
DirectionalShadowCaster[] casters =
[
new DirectionalShadowCaster(
Projection(originalRoot, originalPart),
DirectionalShadowCasterKind.LiveDynamic),
];
var product = new DirectionalShadowPreparedDraws();
Assert.True(product.TryBegin(
generation,
casterBuildSequence: 12,
estimatedInstances: 2,
renderDataAvailabilityVersion: 5,
translucencyFadeRevision: 7));
product.MapCasterIdentity(
0,
casters[0].Projection.Id,
casters[0].Projection.ProjectionClass);
DirectionalShadowTransformSource direct =
DirectionalShadowTransformSource.Dynamic(
casterIndex: 0,
meshIndex: 0,
isSetupPart: false,
in setupPart);
DirectionalShadowTransformSource setup =
DirectionalShadowTransformSource.Dynamic(
casterIndex: 0,
meshIndex: 0,
isSetupPart: true,
in setupPart);
Matrix4x4 originalDirect = originalPart * originalRoot;
Matrix4x4 originalSetup = setupPart * originalPart * originalRoot;
product.Add(
10,
0,
3,
GpuTextureSlot.Unassigned,
0,
CullMode.Clockwise,
DirectionalShadowCasterMaterial.Opaque,
in originalDirect,
in direct);
product.Add(
20,
0,
3,
GpuTextureSlot.Unassigned,
0,
CullMode.Clockwise,
DirectionalShadowCasterMaterial.Opaque,
in originalSetup,
in setup);
DirectionalShadowPreparationStats stats = default;
product.Complete(
generation,
12,
in stats,
renderDataAvailabilityVersion: 5,
translucencyFadeRevision: 7);
ulong topologyBuild = product.BuildSequence;
float exactRootX = BitConverter.Int32BitsToSingle(0x41234567);
float exactPartY = BitConverter.Int32BitsToSingle(0x40ABCDEF);
Matrix4x4 currentRoot = Matrix4x4.CreateRotationZ(0.15f)
* Matrix4x4.CreateTranslation(exactRootX, 12f, 13f);
Matrix4x4 currentPart = Matrix4x4.CreateRotationY(0.35f)
* Matrix4x4.CreateTranslation(14f, exactPartY, 16f);
RenderProjectionRecord current = Projection(currentRoot, currentPart);
DirectionalShadowTransformSnapshot snapshot =
DirectionalShadowTransformSnapshot.Capture(in current);
DirectionalShadowChangedPose[] changed = [new(0, snapshot)];
product.RefreshDynamicTransforms(changed);
AssertMatrixBitsEqual(currentPart * currentRoot, product.Transforms[0]);
AssertMatrixBitsEqual(
setupPart * currentPart * currentRoot,
product.Transforms[1]);
Assert.Equal(topologyBuild, product.BuildSequence);
Assert.Equal(2, product.LastDynamicTransformRefreshCount);
Assert.False(product.RequiresTopologyBuild(generation, 12, 5, 7));
AssertMatrixBitsEqual(
originalRoot,
casters[0].Projection.Transform.LocalToWorld);
AssertMatrixBitsEqual(
originalPart,
casters[0].Projection.EntityPayload.MeshRefs[0].PartTransform);
product.RefreshDynamicTransforms(
ReadOnlySpan<DirectionalShadowChangedPose>.Empty);
Assert.Empty(product.DynamicTransformSlots.ToArray());
Assert.Equal(0, product.LastDynamicTransformRefreshCount);
AssertMatrixBitsEqual(currentPart * currentRoot, product.Transforms[0]);
AssertMatrixBitsEqual(
setupPart * currentPart * currentRoot,
product.Transforms[1]);
product.RefreshDynamicTransforms(changed, denseRefresh: true);
Assert.True(product.LastDynamicTransformRefreshWasDense);
Assert.Equal([0, 1], product.DynamicTransformSlots.ToArray());
product.RefreshDynamicTransforms(changed);
long before = GC.GetAllocatedBytesForCurrentThread();
for (int iteration = 0; iteration < 256; iteration++)
product.RefreshDynamicTransforms(changed);
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
Assert.Equal(0, allocated);
RenderProjectionRecord wrongId = current with
{
Id = RenderProjectionId.FromRaw(99),
};
DirectionalShadowTransformSnapshot wrongSnapshot =
DirectionalShadowTransformSnapshot.Capture(in wrongId);
DirectionalShadowChangedPose[] wrongIdentity = [new(0, wrongSnapshot)];
InvalidOperationException identityFailure = Assert.Throws<
InvalidOperationException>(
() => product.RefreshDynamicTransforms(wrongIdentity));
Assert.Contains("does not match", identityFailure.Message);
DirectionalShadowChangedPose[] staleSlot = [new(1, snapshot)];
InvalidOperationException slotFailure = Assert.Throws<
InvalidOperationException>(
() => product.RefreshDynamicTransforms(staleSlot));
Assert.Contains("stale or unmapped caster index", slotFailure.Message);
}
[Fact]
public void StableTopology_MapsChangedCasterToOnlyItsRetainedTransformSlots()
{
RenderSceneGeneration generation = RenderSceneGeneration.FromRaw(12);
Matrix4x4 firstRoot = Matrix4x4.CreateTranslation(1f, 2f, 3f);
Matrix4x4 secondRoot = Matrix4x4.CreateTranslation(4f, 5f, 6f);
Matrix4x4 firstPart = Matrix4x4.CreateTranslation(7f, 8f, 9f);
Matrix4x4 secondPart = Matrix4x4.CreateTranslation(10f, 11f, 12f);
DirectionalShadowCaster[] casters =
[
new DirectionalShadowCaster(
Projection(firstRoot, firstPart),
DirectionalShadowCasterKind.LiveDynamic),
new DirectionalShadowCaster(
Projection(secondRoot, secondPart) with
{
Id = RenderProjectionId.FromRaw(2),
},
DirectionalShadowCasterKind.EquippedChild),
];
var product = new DirectionalShadowPreparedDraws();
Assert.True(product.TryBegin(generation, 13, estimatedInstances: 2));
product.MapCasterIdentity(
0,
casters[0].Projection.Id,
casters[0].Projection.ProjectionClass);
product.MapCasterIdentity(
1,
casters[1].Projection.Id,
casters[1].Projection.ProjectionClass);
Matrix4x4 setupPart = Matrix4x4.Identity;
DirectionalShadowTransformSource firstSource =
DirectionalShadowTransformSource.Dynamic(
0,
0,
false,
in setupPart);
DirectionalShadowTransformSource secondSource =
DirectionalShadowTransformSource.Dynamic(
1,
0,
false,
in setupPart);
Matrix4x4 firstWorld = firstPart * firstRoot;
Matrix4x4 secondWorld = secondPart * secondRoot;
product.Add(
1, 0, 3, GpuTextureSlot.Unassigned, 0,
CullMode.Clockwise,
DirectionalShadowCasterMaterial.Opaque,
in firstWorld,
in firstSource);
product.Add(
2, 0, 3, GpuTextureSlot.Unassigned, 0,
CullMode.Clockwise,
DirectionalShadowCasterMaterial.Opaque,
in secondWorld,
in secondSource);
DirectionalShadowPreparationStats stats = default;
product.Complete(generation, 13, in stats);
Matrix4x4 movedRoot = Matrix4x4.CreateTranslation(40f, 50f, 60f);
Matrix4x4 movedPart = Matrix4x4.CreateTranslation(70f, 80f, 90f);
casters[1] = casters[1] with
{
Projection = Projection(movedRoot, movedPart) with
{
Id = RenderProjectionId.FromRaw(2),
},
};
product.RefreshDynamicTransforms(casters, [1]);
Assert.Equal([0, 1], product.AllDynamicTransformSlots.ToArray());
Assert.Equal([1], product.DynamicTransformSlots.ToArray());
AssertMatrixBitsEqual(firstWorld, product.Transforms[0]);
AssertMatrixBitsEqual(movedPart * movedRoot, product.Transforms[1]);
Assert.Equal(1, product.LastDynamicTransformRefreshCount);
product.RefreshDenseDynamicTransforms(casters);
Assert.True(product.LastDynamicTransformRefreshWasDense);
Assert.Equal([0, 1], product.DynamicTransformSlots.ToArray());
Assert.Equal(2, product.LastDynamicTransformRefreshCount);
AssertMatrixBitsEqual(firstWorld, product.Transforms[0]);
AssertMatrixBitsEqual(movedPart * movedRoot, product.Transforms[1]);
Matrix4x4 denseFirstRoot = Matrix4x4.CreateTranslation(100f, 101f, 102f);
Matrix4x4 denseFirstPart = Matrix4x4.CreateTranslation(103f, 104f, 105f);
casters[0] = casters[0] with
{
Projection = Projection(denseFirstRoot, denseFirstPart),
};
RenderProjectionRecord firstProjection = casters[0].Projection;
RenderProjectionRecord secondProjection = casters[1].Projection;
DirectionalShadowTransformSnapshot firstPose =
DirectionalShadowTransformSnapshot.Capture(in firstProjection);
DirectionalShadowTransformSnapshot secondPose =
DirectionalShadowTransformSnapshot.Capture(in secondProjection);
DirectionalShadowChangedPose[] reversedDenseChanges =
[
new(1, secondPose),
new(0, firstPose),
];
product.RefreshDynamicTransforms(reversedDenseChanges, denseRefresh: true);
Assert.True(product.LastDynamicTransformRefreshWasDense);
Assert.Equal([0, 1], product.DynamicTransformSlots.ToArray());
Assert.Equal(2, product.LastDynamicTransformRefreshCount);
AssertMatrixBitsEqual(
denseFirstPart * denseFirstRoot,
product.Transforms[0]);
AssertMatrixBitsEqual(movedPart * movedRoot, product.Transforms[1]);
long beforeDense = GC.GetAllocatedBytesForCurrentThread();
for (int iteration = 0; iteration < 256; iteration++)
{
product.RefreshDynamicTransforms(
reversedDenseChanges,
denseRefresh: true);
}
long denseAllocated =
GC.GetAllocatedBytesForCurrentThread() - beforeDense;
Assert.Equal(0, denseAllocated);
Assert.True(product.TryBegin(generation, 14, estimatedInstances: 0));
product.Complete(generation, 14, in stats);
product.RefreshDynamicTransforms(casters, [0]);
Assert.Empty(product.DynamicTransformSlots.ToArray());
Assert.Equal(0, product.LastDynamicTransformRefreshCount);
}
[Fact]
public void ResourceAvailabilityFadeAndPendingTextureInvalidateRetainedTopology()
{
RenderSceneGeneration generation = RenderSceneGeneration.FromRaw(11);
var product = new DirectionalShadowPreparedDraws();
Assert.True(product.TryBegin(generation, 2, 0, 20, 30));
var pending = new DirectionalShadowPreparationStats(
SourceCasters: 1,
SourceMeshRefs: 1,
SourceParts: 1,
SourceBatches: 1,
PreparedInstances: 0,
PreparedOpaqueCommands: 0,
PreparedAlphaCutoutCommands: 0,
RejectedTransparentBatches: 0,
RejectedFadedParts: 0,
MissingMeshes: 1,
UnresolvedAlphaCutoutTextures: 0);
product.Complete(generation, 2, in pending, 20, 30);
Assert.False(product.RequiresTopologyBuild(generation, 2, 20, 30));
Assert.True(product.RequiresTopologyBuild(generation, 2, 21, 30));
Assert.True(product.RequiresTopologyBuild(generation, 2, 20, 31));
Assert.True(product.TryBegin(generation, 2, 0, 21, 30));
pending = pending with { UnresolvedAlphaCutoutTextures = 1 };
product.Complete(generation, 2, in pending, 21, 30);
Assert.True(product.RequiresTopologyBuild(generation, 2, 21, 30));
}
[Fact]
public void SetupComposition_UsesPublishedMeshRefTransformWithoutAnotherPose()
{
Matrix4x4 root = Matrix4x4.CreateRotationZ(0.1f)
* Matrix4x4.CreateTranslation(50f, 60f, 70f);
Matrix4x4 currentMeshRef = Matrix4x4.CreateRotationY(0.2f)
* Matrix4x4.CreateTranslation(4f, 5f, 6f);
Matrix4x4 authoredSetupPart = Matrix4x4.CreateRotationX(0.3f)
* Matrix4x4.CreateTranslation(1f, 2f, 3f);
Matrix4x4 actual = WbDrawDispatcher.ComposePartWorldMatrix(
root,
currentMeshRef,
authoredSetupPart);
Assert.Equal(authoredSetupPart * currentMeshRef * root, actual);
}
private static RenderProjectionRecord Projection(
in Matrix4x4 root,
in Matrix4x4 part) =>
new RenderProjectionRecord() with
{
Id = RenderProjectionId.FromRaw(1),
ProjectionClass = RenderProjectionClass.LiveDynamicRoot,
Transform = new RenderTransform(root),
EntityPayload = new RenderEntityPayload(
[new MeshRef(0x01000001, part)],
PaletteOverride: null,
IsBuildingShell: false),
};
private static void AssertMatrixBitsEqual(
Matrix4x4 expected,
Matrix4x4 actual)
{
ReadOnlySpan<byte> expectedBits = MemoryMarshal.AsBytes(
MemoryMarshal.CreateReadOnlySpan(ref expected, 1));
ReadOnlySpan<byte> actualBits = MemoryMarshal.AsBytes(
MemoryMarshal.CreateReadOnlySpan(ref actual, 1));
Assert.True(expectedBits.SequenceEqual(actualBits));
}
}