acdream/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.DirectionalShadows.cs
Erik 517d17b4b3 fix #426: extract solid-colour (NO_POS_UVS) faces; skip untextured subsets only on building shells and cells like retail
The Holtburg windmill axle (GfxObj 0x010010CE, 8 polygons, all
Stippling.NoPos + SurfaceType.Base1Solid) extracted to a 0-vertex mesh.
NoPos ("NO_POS_UVS", acclient.h:7380-7388) means "this side has no
texture coordinates" — true of every solid-colour polygon, since
nothing samples them — not "there is no positive face". Extraction read
it as the latter and dropped the polygon entirely, client-wide, for
every untextured polygon on every object.

Retail's D3DPolyRender::DrawMesh (@0x0059d4a0, named-retail decomp
~line 426048) draws an untextured subset on an ordinary object exactly
like a textured one; the only retail cases that skip an untextured
subset are a building shell (RenderDeviceD3D::DrawBuilding @0x0059f2a0
sets ObjBuildingOrBuildingPart=1) or an EnvCell interior
(RenderDeviceD3D::DrawEnvCell @0x0059f170, arg4=1). The #119
investigation's "retail's skipNoTexture never draws them either"
conclusion was itself wrong as a general rule.

- MeshExtractor.PrepareGfxObjMeshData / GfxObjMesh.Build: emit the
  positive side whenever PosSurface is a valid index, regardless of
  NoPos; the existing UV-index-0 fallback already produces zero
  texcoords for a NoPos polygon with no UVs on the wire.
- RetailUntexturedSurfacePolicy.IsUntextured(SurfaceType): the one
  place that answers "is this surface textured"
  ((type & (Base1Image|Base1ClipMap)) == 0), replacing the old
  `isSolid = NoPos || Base1Solid` (which also mis-classified a NEG-side
  batch by the POS-side's NoPos flag).
- RetailUntexturedSubsetPolicy.Draws(isBuildingShell, isUntextured):
  the shared draw-time gate wired into WbDrawDispatcher.ClassifyBatches,
  .PackedOracle.ClassifyPackedBatches, and
  .DirectionalShadows.AddDirectionalShadowBatches — one predicate so the
  three walks cannot drift (Campaign VM VM6 lesson).
- CellMesh.cs / MeshExtractor.PrepareCellStructMeshData deliberately
  KEEP their NoPos-gated skip for cell-wall geometry — retail's
  DrawEnvCell really does skip untextured subsets there; register row
  AP-234 documents the NoPos-vs-Surface.Type approximation.
- PakFormat.CurrentBakeToolVersion 4->5 (LauncherInstallRecordStore in
  lockstep): a pak baked by an older tool is missing every untextured
  face. No bake was run as part of this commit.

Also fixed: WorldBuilder's own upstream ObjectMeshManager.cs has the
identical NoPos bug (ObjectMeshManager.cs:959,984) — our port had
faithfully carried it over, and our own conformance test
(Build_NoPosFlag_OnlyEmitsNegSide) asserted the bug as correct WB
conformance. Renamed/reworded to Build_NoPosFlag_EmitsBothPosAndNegSide
with a citation for why retail decomp overrides WB here.

Issue119UpNullGfxObjDumpTests re-run against the installed DAT:
#119's own two objects (0x010002B4 9/9 polys, 0x010008A8 1/1 poly) now
gate DRAWS on every polygon instead of extracting to nothing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 11:20:24 +02:00

1255 lines
50 KiB
C#

using System.Numerics;
using System.Runtime.CompilerServices;
using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Scene;
using AcDream.Core.Meshing;
using AcDream.Core.World;
using DatReaderWriter.Enums;
namespace AcDream.App.Rendering.Wb;
internal enum DirectionalShadowCasterMaterial : byte
{
Opaque,
AlphaCutout,
}
internal readonly record struct DirectionalShadowPreparedBatch(
GpuTextureSlot TextureSlot,
uint TextureLayer,
CullMode CullMode,
DirectionalShadowCasterMaterial Material,
// Campaign VM VM6: FoliageWindClassification.CutoutFoliageFlag /
// TrunkFlag — the identical word WbDrawDispatcher.ClassifyBatches
// computed for the matching world-receiver subset, so the caster and
// receiver agree by construction (see foliage_wind.glsl).
uint FoliageFlags = 0u);
internal readonly record struct DirectionalShadowPreparedRun(
int StartCommand,
int CommandCount,
CullMode CullMode,
DirectionalShadowCasterMaterial Material);
internal readonly record struct DirectionalShadowPreparationStats(
int SourceCasters,
int SourceMeshRefs,
int SourceParts,
int SourceBatches,
int PreparedInstances,
int PreparedOpaqueCommands,
int PreparedAlphaCutoutCommands,
int RejectedTransparentBatches,
int RejectedFadedParts,
int MissingMeshes,
int UnresolvedAlphaCutoutTextures);
internal readonly record struct DirectionalShadowMeshGeometry(
IGpuBuffer VertexBuffer,
IGpuBuffer IndexBuffer);
internal readonly record struct DirectionalShadowTransformSource(
bool Refreshable,
int CasterIndex,
int MeshIndex,
bool IsSetupPart,
Matrix4x4 SetupPartTransform)
{
public static DirectionalShadowTransformSource Dynamic(
int casterIndex,
int meshIndex,
bool isSetupPart,
in Matrix4x4 setupPartTransform) =>
new(
true,
casterIndex,
meshIndex,
isSetupPart,
setupPartTransform);
}
/// <summary>
/// One reusable CPU product prepared before the first directional cascade.
/// Every cascade replays these exact command, metadata, and transform spans;
/// it never reclassifies materials or asks animation for another pose.
/// </summary>
internal sealed class DirectionalShadowPreparedDraws
{
private DirectionalShadowSourceDraw[] _source = [];
private Matrix4x4[] _transforms = [];
private DirectionalShadowTransformSource[] _transformSources = [];
private int[] _dynamicTransformSlots = [];
private int[] _allDynamicTransformSlots = [];
private int[] _firstDynamicTransformByCaster = [];
private int[] _nextDynamicTransform = [];
private int[] _denseChangedPoseByCaster = [];
private RenderProjectionId[] _mappedCasterIds = [];
private RenderProjectionClass[] _mappedCasterClasses = [];
private bool[] _mappedCasterIdentityPresent = [];
private DrawElementsIndirectCommand[] _commands = [];
private DirectionalShadowPreparedBatch[] _batches = [];
private DirectionalShadowPreparedRun[] _runs = [];
private int _sourceCount;
private int _commandCount;
private int _runCount;
private int _dynamicTransformSlotCount;
private int _allDynamicTransformSlotCount;
private int _mappedCasterCount;
private bool _building;
private bool _retryClassificationNextFrame;
public RenderSceneGeneration SourceGeneration { get; private set; }
public ulong SourceCasterBuildSequence { get; private set; }
public long SourceRenderDataAvailabilityVersion { get; private set; }
public ulong SourceTranslucencyFadeRevision { get; private set; }
public ulong BuildSequence { get; private set; }
public int LastDynamicTransformRefreshCount { get; private set; }
public bool LastDynamicTransformRefreshWasDense { get; private set; }
public int OpaqueCommandCount { get; private set; }
public int AlphaCutoutCommandCount => _commandCount - OpaqueCommandCount;
public int OpaqueRunCount { get; private set; }
public ReadOnlySpan<Matrix4x4> Transforms =>
_transforms.AsSpan(0, _sourceCount);
/// <summary>
/// Sorted transform indices whose root and/or animated part values are
/// refreshed from already-published slim pose snapshots this frame.
/// Static indices are omitted so a retained GPU transform product can leave
/// their exact bits untouched until topology changes.
/// </summary>
public ReadOnlySpan<int> DynamicTransformSlots =>
_dynamicTransformSlots.AsSpan(0, _dynamicTransformSlotCount);
/// <summary>
/// Every refreshable transform in the retained topology. The per-flight
/// GPU product uses this exact list only when its bounded replay journal
/// cannot cover all changes since a flight slot was last submitted.
/// </summary>
public ReadOnlySpan<int> AllDynamicTransformSlots =>
_allDynamicTransformSlots.AsSpan(0, _allDynamicTransformSlotCount);
public ReadOnlySpan<DrawElementsIndirectCommand> Commands =>
_commands.AsSpan(0, _commandCount);
public ReadOnlySpan<DrawElementsIndirectCommand> OpaqueCommands =>
_commands.AsSpan(0, OpaqueCommandCount);
public ReadOnlySpan<DrawElementsIndirectCommand> AlphaCutoutCommands =>
_commands.AsSpan(OpaqueCommandCount, AlphaCutoutCommandCount);
public ReadOnlySpan<DirectionalShadowPreparedBatch> Batches =>
_batches.AsSpan(0, _commandCount);
public ReadOnlySpan<DirectionalShadowPreparedBatch> OpaqueBatches =>
_batches.AsSpan(0, OpaqueCommandCount);
public ReadOnlySpan<DirectionalShadowPreparedBatch> AlphaCutoutBatches =>
_batches.AsSpan(OpaqueCommandCount, AlphaCutoutCommandCount);
public ReadOnlySpan<DirectionalShadowPreparedRun> Runs =>
_runs.AsSpan(0, _runCount);
public ReadOnlySpan<DirectionalShadowPreparedRun> OpaqueRuns =>
_runs.AsSpan(0, OpaqueRunCount);
public ReadOnlySpan<DirectionalShadowPreparedRun> AlphaCutoutRuns =>
_runs.AsSpan(OpaqueRunCount, _runCount - OpaqueRunCount);
public DirectionalShadowPreparationStats Stats { get; private set; }
public long RetainedScratchBytes => checked(
(long)_source.Length * Unsafe.SizeOf<DirectionalShadowSourceDraw>()
+ (long)_transforms.Length * Unsafe.SizeOf<Matrix4x4>()
+ (long)_transformSources.Length
* Unsafe.SizeOf<DirectionalShadowTransformSource>()
+ (long)_dynamicTransformSlots.Length * sizeof(int)
+ (long)_allDynamicTransformSlots.Length * sizeof(int)
+ (long)_firstDynamicTransformByCaster.Length * sizeof(int)
+ (long)_nextDynamicTransform.Length * sizeof(int)
+ (long)_denseChangedPoseByCaster.Length * sizeof(int)
+ (long)_mappedCasterIds.Length * Unsafe.SizeOf<RenderProjectionId>()
+ (long)_mappedCasterClasses.Length
* Unsafe.SizeOf<RenderProjectionClass>()
+ _mappedCasterIdentityPresent.Length
+ (long)_commands.Length * Unsafe.SizeOf<DrawElementsIndirectCommand>()
+ (long)_batches.Length * Unsafe.SizeOf<DirectionalShadowPreparedBatch>()
+ (long)_runs.Length * Unsafe.SizeOf<DirectionalShadowPreparedRun>());
/// <summary>
/// Returns false when this exact resident-caster build was already
/// prepared. A controller may therefore ask once per cascade without
/// accidentally performing per-cascade CPU classification.
/// </summary>
public bool RequiresTopologyBuild(
RenderSceneGeneration generation,
ulong casterBuildSequence,
long renderDataAvailabilityVersion = 0,
ulong translucencyFadeRevision = 0) =>
SourceGeneration != generation
|| SourceCasterBuildSequence != casterBuildSequence
|| SourceRenderDataAvailabilityVersion
!= renderDataAvailabilityVersion
|| SourceTranslucencyFadeRevision != translucencyFadeRevision
|| _retryClassificationNextFrame;
public bool TryBegin(
RenderSceneGeneration generation,
ulong casterBuildSequence,
int estimatedInstances,
long renderDataAvailabilityVersion = 0,
ulong translucencyFadeRevision = 0)
{
if (_building)
throw new InvalidOperationException(
"A directional-shadow draw build is already active.");
if (casterBuildSequence == 0)
throw new ArgumentOutOfRangeException(nameof(casterBuildSequence));
ArgumentOutOfRangeException.ThrowIfNegative(estimatedInstances);
if (!RequiresTopologyBuild(
generation,
casterBuildSequence,
renderDataAvailabilityVersion,
translucencyFadeRevision))
{
return false;
}
EnsureCapacity(ref _source, estimatedInstances);
_sourceCount = 0;
_commandCount = 0;
_runCount = 0;
_dynamicTransformSlotCount = 0;
_allDynamicTransformSlotCount = 0;
if (_mappedCasterCount != 0)
{
Array.Clear(
_mappedCasterIdentityPresent,
0,
_mappedCasterCount);
}
_mappedCasterCount = 0;
OpaqueCommandCount = 0;
OpaqueRunCount = 0;
Stats = default;
LastDynamicTransformRefreshCount = 0;
LastDynamicTransformRefreshWasDense = false;
_building = true;
return true;
}
public void MapCasterIdentity(
int casterIndex,
RenderProjectionId id,
RenderProjectionClass projectionClass)
{
if (!_building)
{
throw new InvalidOperationException(
"Begin a directional-shadow draw build before mapping casters.");
}
ArgumentOutOfRangeException.ThrowIfNegative(casterIndex);
int required = checked(casterIndex + 1);
EnsureCapacity(ref _mappedCasterIds, required);
EnsureCapacity(ref _mappedCasterClasses, required);
EnsureCapacity(ref _mappedCasterIdentityPresent, required);
if (_mappedCasterIdentityPresent[casterIndex]
&& (_mappedCasterIds[casterIndex] != id
|| _mappedCasterClasses[casterIndex] != projectionClass))
{
throw new InvalidOperationException(
$"Directional-shadow caster slot {casterIndex} was mapped twice "
+ "with different projection identities.");
}
_mappedCasterIds[casterIndex] = id;
_mappedCasterClasses[casterIndex] = projectionClass;
_mappedCasterIdentityPresent[casterIndex] = true;
_mappedCasterCount = Math.Max(_mappedCasterCount, required);
}
public void Add(
uint firstIndex,
int baseVertex,
int indexCount,
GpuTextureSlot textureSlot,
uint textureLayer,
CullMode cullMode,
DirectionalShadowCasterMaterial material,
in Matrix4x4 transform,
uint foliageFlags = 0u)
{
DirectionalShadowTransformSource source = default;
Add(
firstIndex,
baseVertex,
indexCount,
textureSlot,
textureLayer,
cullMode,
material,
in transform,
in source,
foliageFlags);
}
public void Add(
uint firstIndex,
int baseVertex,
int indexCount,
GpuTextureSlot textureSlot,
uint textureLayer,
CullMode cullMode,
DirectionalShadowCasterMaterial material,
in Matrix4x4 transform,
in DirectionalShadowTransformSource transformSource,
uint foliageFlags = 0u)
{
if (!_building)
throw new InvalidOperationException(
"Begin a directional-shadow draw build before adding batches.");
if (indexCount <= 0)
throw new ArgumentOutOfRangeException(nameof(indexCount));
if (material is DirectionalShadowCasterMaterial.AlphaCutout
&& !textureSlot.IsAssigned)
{
throw new ArgumentException(
"An alpha-cutout caster requires an assigned texture slot.",
nameof(textureSlot));
}
EnsureCapacity(ref _source, checked(_sourceCount + 1));
_source[_sourceCount++] = new DirectionalShadowSourceDraw(
new DirectionalShadowDrawKey(
firstIndex,
baseVertex,
indexCount,
material is DirectionalShadowCasterMaterial.AlphaCutout
? textureSlot
: GpuTextureSlot.Unassigned,
material is DirectionalShadowCasterMaterial.AlphaCutout
? textureLayer
: 0u,
cullMode,
material,
foliageFlags),
transform,
transformSource);
}
public void Complete(
RenderSceneGeneration generation,
ulong casterBuildSequence,
in DirectionalShadowPreparationStats stats,
long renderDataAvailabilityVersion = 0,
ulong translucencyFadeRevision = 0)
{
if (!_building)
throw new InvalidOperationException(
"No directional-shadow draw build is active.");
if (casterBuildSequence == 0)
throw new ArgumentOutOfRangeException(nameof(casterBuildSequence));
Array.Sort(
_source,
0,
_sourceCount,
DirectionalShadowSourceDrawComparer.Instance);
EnsureCapacity(ref _transforms, _sourceCount);
EnsureCapacity(ref _transformSources, _sourceCount);
EnsureCapacity(ref _dynamicTransformSlots, _sourceCount);
EnsureCapacity(ref _allDynamicTransformSlots, _sourceCount);
EnsureCapacity(ref _nextDynamicTransform, _sourceCount);
EnsureCapacity(ref _commands, _sourceCount);
EnsureCapacity(ref _batches, _sourceCount);
EnsureCapacity(ref _runs, _sourceCount);
int maxCasterIndex = -1;
for (int index = 0; index < _sourceCount; index++)
{
DirectionalShadowTransformSource transformSource =
_source[index].TransformSource;
if (transformSource.Refreshable)
maxCasterIndex = Math.Max(maxCasterIndex, transformSource.CasterIndex);
}
_mappedCasterCount = Math.Max(_mappedCasterCount, maxCasterIndex + 1);
EnsureCapacity(ref _firstDynamicTransformByCaster, _mappedCasterCount);
EnsureCapacity(ref _denseChangedPoseByCaster, _mappedCasterCount);
EnsureCapacity(ref _mappedCasterIds, _mappedCasterCount);
EnsureCapacity(ref _mappedCasterClasses, _mappedCasterCount);
EnsureCapacity(ref _mappedCasterIdentityPresent, _mappedCasterCount);
if (_mappedCasterCount != 0)
{
Array.Fill(
_firstDynamicTransformByCaster,
-1,
0,
_mappedCasterCount);
}
int sourceIndex = 0;
int transformIndex = 0;
int commandIndex = 0;
int opaqueCommands = 0;
while (sourceIndex < _sourceCount)
{
DirectionalShadowDrawKey key = _source[sourceIndex].Key;
int groupStart = sourceIndex;
do
{
_transforms[transformIndex++] = _source[sourceIndex].Transform;
_transformSources[transformIndex - 1] =
_source[sourceIndex].TransformSource;
if (_source[sourceIndex].TransformSource.Refreshable)
{
int dynamicTransformIndex = transformIndex - 1;
DirectionalShadowTransformSource transformSource =
_source[sourceIndex].TransformSource;
if (transformSource.CasterIndex < 0)
{
throw new InvalidOperationException(
"A refreshable directional-shadow transform has a negative caster index.");
}
_allDynamicTransformSlots[_allDynamicTransformSlotCount++] =
dynamicTransformIndex;
_nextDynamicTransform[dynamicTransformIndex] =
_firstDynamicTransformByCaster[transformSource.CasterIndex];
_firstDynamicTransformByCaster[transformSource.CasterIndex] =
dynamicTransformIndex;
}
sourceIndex++;
}
while (sourceIndex < _sourceCount && _source[sourceIndex].Key == key);
int instanceCount = sourceIndex - groupStart;
_commands[commandIndex] = new DrawElementsIndirectCommand
{
Count = checked((uint)key.IndexCount),
InstanceCount = checked((uint)instanceCount),
FirstIndex = key.FirstIndex,
BaseVertex = key.BaseVertex,
BaseInstance = checked((uint)(transformIndex - instanceCount)),
};
_batches[commandIndex] = new DirectionalShadowPreparedBatch(
key.TextureSlot,
key.TextureLayer,
key.CullMode,
key.Material,
key.FoliageFlags);
if (key.Material is DirectionalShadowCasterMaterial.Opaque)
opaqueCommands++;
commandIndex++;
}
_commandCount = commandIndex;
OpaqueCommandCount = opaqueCommands;
int runCursor = 0;
int runStart = 0;
while (runStart < commandIndex)
{
DirectionalShadowPreparedBatch first = _batches[runStart];
int runEnd = runStart + 1;
while (runEnd < commandIndex
&& _batches[runEnd].CullMode == first.CullMode
&& _batches[runEnd].Material == first.Material)
{
runEnd++;
}
_runs[runCursor++] = new DirectionalShadowPreparedRun(
runStart,
runEnd - runStart,
first.CullMode,
first.Material);
runStart = runEnd;
}
_runCount = runCursor;
while (OpaqueRunCount < runCursor
&& _runs[OpaqueRunCount].Material is DirectionalShadowCasterMaterial.Opaque)
{
OpaqueRunCount++;
}
SourceGeneration = generation;
SourceCasterBuildSequence = casterBuildSequence;
SourceRenderDataAvailabilityVersion = renderDataAvailabilityVersion;
SourceTranslucencyFadeRevision = translucencyFadeRevision;
BuildSequence = checked(BuildSequence + 1);
LastDynamicTransformRefreshCount = 0;
LastDynamicTransformRefreshWasDense = false;
_dynamicTransformSlotCount = 0;
_retryClassificationNextFrame =
stats.UnresolvedAlphaCutoutTextures != 0;
Stats = stats with
{
PreparedInstances = _sourceCount,
PreparedOpaqueCommands = opaqueCommands,
PreparedAlphaCutoutCommands = commandIndex - opaqueCommands,
};
_building = false;
}
public void RefreshDynamicTransforms(
DirectionalShadowCasterFrame casters)
{
ArgumentNullException.ThrowIfNull(casters);
if (casters.Stats.TransformJournalFullRefresh)
{
RefreshAllDynamicTransforms(casters.Casters);
LastDynamicTransformRefreshWasDense = false;
return;
}
RefreshDynamicTransforms(
casters.ChangedCasterPoses,
casters.Stats.DensityBulkRefresh);
}
internal void RefreshDynamicTransforms(
ReadOnlySpan<DirectionalShadowCaster> currentCasters)
{
RefreshAllDynamicTransforms(currentCasters);
LastDynamicTransformRefreshWasDense = false;
}
internal void RefreshDenseDynamicTransforms(
ReadOnlySpan<DirectionalShadowCaster> currentCasters)
{
RefreshAllDynamicTransforms(currentCasters);
LastDynamicTransformRefreshWasDense = true;
}
private void RefreshAllDynamicTransforms(
ReadOnlySpan<DirectionalShadowCaster> currentCasters)
{
_dynamicTransformSlotCount = _allDynamicTransformSlotCount;
_allDynamicTransformSlots.AsSpan(0, _allDynamicTransformSlotCount)
.CopyTo(_dynamicTransformSlots);
int refreshed = 0;
for (int dynamicIndex = 0;
dynamicIndex < _dynamicTransformSlotCount;
dynamicIndex++)
{
int transformIndex = _dynamicTransformSlots[dynamicIndex];
DirectionalShadowTransformSource source =
_transformSources[transformIndex];
if ((uint)source.CasterIndex >= (uint)currentCasters.Length)
{
throw new InvalidOperationException(
"Directional-shadow transform source has a stale caster index.");
}
RenderProjectionRecord projection =
currentCasters[source.CasterIndex].Projection;
IReadOnlyList<MeshRef> meshes = projection.EntityPayload.MeshRefs;
if ((uint)source.MeshIndex >= (uint)meshes.Count)
{
throw new InvalidOperationException(
"Directional-shadow transform source has a stale mesh index.");
}
MeshRef mesh = meshes[source.MeshIndex];
_transforms[transformIndex] = source.IsSetupPart
? WbDrawDispatcher.ComposePartWorldMatrix(
projection.Transform.LocalToWorld,
mesh.PartTransform,
source.SetupPartTransform)
: mesh.PartTransform * projection.Transform.LocalToWorld;
refreshed++;
}
LastDynamicTransformRefreshCount = refreshed;
}
internal void RefreshDynamicTransforms(
ReadOnlySpan<DirectionalShadowCaster> currentCasters,
ReadOnlySpan<int> changedCasterSlots)
{
_dynamicTransformSlotCount = 0;
for (int changedIndex = 0;
changedIndex < changedCasterSlots.Length;
changedIndex++)
{
int casterIndex = changedCasterSlots[changedIndex];
if ((uint)casterIndex >= (uint)currentCasters.Length)
{
throw new InvalidOperationException(
"Directional-shadow changed-caster slot is stale.");
}
if ((uint)casterIndex >= (uint)_mappedCasterCount)
continue;
for (int transformIndex = _firstDynamicTransformByCaster[casterIndex];
transformIndex >= 0;
transformIndex = _nextDynamicTransform[transformIndex])
{
RefreshTransform(currentCasters, transformIndex);
_dynamicTransformSlots[_dynamicTransformSlotCount++] = transformIndex;
}
}
_dynamicTransformSlots.AsSpan(0, _dynamicTransformSlotCount).Sort();
LastDynamicTransformRefreshCount = _dynamicTransformSlotCount;
LastDynamicTransformRefreshWasDense = false;
}
internal void RefreshDynamicTransforms(
ReadOnlySpan<DirectionalShadowChangedPose> changedPoses,
bool denseRefresh = false)
{
if (denseRefresh)
{
RefreshDenseDynamicTransforms(changedPoses);
return;
}
_dynamicTransformSlotCount = 0;
for (int changedIndex = 0;
changedIndex < changedPoses.Length;
changedIndex++)
{
ref readonly DirectionalShadowChangedPose changed =
ref changedPoses[changedIndex];
int casterIndex = changed.CasterIndex;
if ((uint)casterIndex >= (uint)_mappedCasterCount
|| !_mappedCasterIdentityPresent[casterIndex])
{
throw new InvalidOperationException(
"Directional-shadow changed pose has a stale or unmapped caster index.");
}
ref readonly DirectionalShadowTransformSnapshot pose =
ref changed.Snapshot;
if (pose.Id != _mappedCasterIds[casterIndex]
|| pose.ProjectionClass != _mappedCasterClasses[casterIndex])
{
throw new InvalidOperationException(
$"Directional-shadow changed pose {pose.Id} does not match "
+ $"retained caster {_mappedCasterIds[casterIndex]} at "
+ $"slot {casterIndex}.");
}
for (int transformIndex = _firstDynamicTransformByCaster[casterIndex];
transformIndex >= 0;
transformIndex = _nextDynamicTransform[transformIndex])
{
RefreshTransform(
in pose,
casterIndex,
transformIndex);
_dynamicTransformSlots[_dynamicTransformSlotCount++] =
transformIndex;
}
}
_dynamicTransformSlots.AsSpan(0, _dynamicTransformSlotCount).Sort();
LastDynamicTransformRefreshCount = _dynamicTransformSlotCount;
LastDynamicTransformRefreshWasDense = denseRefresh;
}
/// <summary>
/// Dense animation refreshes already upload the complete retained dynamic
/// ranges. Mark the changed caster slots, then walk the topology's strictly
/// increasing transform slots once. This produces the same exact sorted
/// changed-slot product without sorting roughly two thousand integers on
/// every foliage-animation frame.
/// </summary>
private void RefreshDenseDynamicTransforms(
ReadOnlySpan<DirectionalShadowChangedPose> changedPoses)
{
_dynamicTransformSlotCount = 0;
int marked = 0;
try
{
for (int changedIndex = 0;
changedIndex < changedPoses.Length;
changedIndex++)
{
ref readonly DirectionalShadowChangedPose changed =
ref changedPoses[changedIndex];
int casterIndex = changed.CasterIndex;
ValidateChangedPose(in changed, casterIndex);
if (_denseChangedPoseByCaster[casterIndex] != 0)
{
throw new InvalidOperationException(
"A dense directional-shadow refresh contains a duplicate caster slot.");
}
_denseChangedPoseByCaster[casterIndex] = changedIndex + 1;
marked++;
}
for (int dynamicIndex = 0;
dynamicIndex < _allDynamicTransformSlotCount;
dynamicIndex++)
{
int transformIndex = _allDynamicTransformSlots[dynamicIndex];
DirectionalShadowTransformSource source =
_transformSources[transformIndex];
int poseIndex = _denseChangedPoseByCaster[source.CasterIndex] - 1;
if (poseIndex < 0)
continue;
ref readonly DirectionalShadowChangedPose changed =
ref changedPoses[poseIndex];
RefreshTransform(
in changed.Snapshot,
changed.CasterIndex,
transformIndex);
_dynamicTransformSlots[_dynamicTransformSlotCount++] =
transformIndex;
}
}
finally
{
if (marked != 0)
{
for (int changedIndex = 0;
changedIndex < changedPoses.Length;
changedIndex++)
{
int casterIndex = changedPoses[changedIndex].CasterIndex;
if ((uint)casterIndex < (uint)_denseChangedPoseByCaster.Length)
_denseChangedPoseByCaster[casterIndex] = 0;
}
}
}
LastDynamicTransformRefreshCount = _dynamicTransformSlotCount;
LastDynamicTransformRefreshWasDense = true;
}
private void ValidateChangedPose(
in DirectionalShadowChangedPose changed,
int casterIndex)
{
if ((uint)casterIndex >= (uint)_mappedCasterCount
|| !_mappedCasterIdentityPresent[casterIndex])
{
throw new InvalidOperationException(
"Directional-shadow changed pose has a stale or unmapped caster index.");
}
ref readonly DirectionalShadowTransformSnapshot pose =
ref changed.Snapshot;
if (pose.Id != _mappedCasterIds[casterIndex]
|| pose.ProjectionClass != _mappedCasterClasses[casterIndex])
{
throw new InvalidOperationException(
$"Directional-shadow changed pose {pose.Id} does not match "
+ $"retained caster {_mappedCasterIds[casterIndex]} at "
+ $"slot {casterIndex}.");
}
}
private void RefreshTransform(
in DirectionalShadowTransformSnapshot pose,
int casterIndex,
int transformIndex)
{
DirectionalShadowTransformSource source =
_transformSources[transformIndex];
if (source.CasterIndex != casterIndex)
{
throw new InvalidOperationException(
"Directional-shadow transform source maps to a different caster.");
}
IReadOnlyList<MeshRef> currentMeshes = pose.EntityPayload.MeshRefs;
if ((uint)source.MeshIndex >= (uint)currentMeshes.Count)
{
throw new InvalidOperationException(
"Directional-shadow changed pose has a stale mesh index.");
}
MeshRef currentMesh = currentMeshes[source.MeshIndex];
_transforms[transformIndex] = source.IsSetupPart
? WbDrawDispatcher.ComposePartWorldMatrix(
pose.Transform.LocalToWorld,
currentMesh.PartTransform,
source.SetupPartTransform)
: currentMesh.PartTransform * pose.Transform.LocalToWorld;
}
private void RefreshTransform(
ReadOnlySpan<DirectionalShadowCaster> currentCasters,
int transformIndex)
{
DirectionalShadowTransformSource source =
_transformSources[transformIndex];
if ((uint)source.CasterIndex >= (uint)currentCasters.Length)
{
throw new InvalidOperationException(
"Directional-shadow transform source has a stale caster index.");
}
RenderProjectionRecord projection =
currentCasters[source.CasterIndex].Projection;
IReadOnlyList<MeshRef> meshes = projection.EntityPayload.MeshRefs;
if ((uint)source.MeshIndex >= (uint)meshes.Count)
{
throw new InvalidOperationException(
"Directional-shadow transform source has a stale mesh index.");
}
MeshRef mesh = meshes[source.MeshIndex];
_transforms[transformIndex] = source.IsSetupPart
? WbDrawDispatcher.ComposePartWorldMatrix(
projection.Transform.LocalToWorld,
mesh.PartTransform,
source.SetupPartTransform)
: mesh.PartTransform * projection.Transform.LocalToWorld;
}
public void Abort()
{
_sourceCount = 0;
_commandCount = 0;
_runCount = 0;
_dynamicTransformSlotCount = 0;
_allDynamicTransformSlotCount = 0;
if (_mappedCasterCount != 0)
{
Array.Clear(
_mappedCasterIdentityPresent,
0,
_mappedCasterCount);
}
_mappedCasterCount = 0;
OpaqueCommandCount = 0;
OpaqueRunCount = 0;
Stats = default;
SourceGeneration = default;
SourceCasterBuildSequence = 0;
SourceRenderDataAvailabilityVersion = 0;
SourceTranslucencyFadeRevision = 0;
LastDynamicTransformRefreshCount = 0;
LastDynamicTransformRefreshWasDense = false;
_retryClassificationNextFrame = false;
_building = false;
}
internal static bool TryClassifyMaterial(
TranslucencyKind translucency,
out DirectionalShadowCasterMaterial material)
{
switch (translucency)
{
case TranslucencyKind.Opaque:
material = DirectionalShadowCasterMaterial.Opaque;
return true;
case TranslucencyKind.ClipMap:
material = DirectionalShadowCasterMaterial.AlphaCutout;
return true;
default:
material = default;
return false;
}
}
internal static bool FadeExcludesCaster(float translucency) =>
!float.IsFinite(translucency) || translucency > 0f;
private static void EnsureCapacity<T>(ref T[] values, int required)
{
if (values.Length >= required)
return;
int capacity = values.Length == 0 ? 16 : values.Length;
while (capacity < required)
capacity = checked(capacity * 2);
Array.Resize(ref values, capacity);
}
private readonly record struct DirectionalShadowDrawKey(
uint FirstIndex,
int BaseVertex,
int IndexCount,
GpuTextureSlot TextureSlot,
uint TextureLayer,
CullMode CullMode,
DirectionalShadowCasterMaterial Material,
// Campaign VM VM6: part of the sort/group key (not just the prepared
// batch payload) so two subsets that would otherwise share a group
// never coalesce under different foliage classification.
uint FoliageFlags = 0u);
private readonly record struct DirectionalShadowSourceDraw(
DirectionalShadowDrawKey Key,
Matrix4x4 Transform,
DirectionalShadowTransformSource TransformSource);
private sealed class DirectionalShadowSourceDrawComparer
: IComparer<DirectionalShadowSourceDraw>
{
public static DirectionalShadowSourceDrawComparer Instance { get; } = new();
public int Compare(
DirectionalShadowSourceDraw left,
DirectionalShadowSourceDraw right)
{
DirectionalShadowDrawKey x = left.Key;
DirectionalShadowDrawKey y = right.Key;
int order = x.Material.CompareTo(y.Material);
if (order != 0) return order;
order = x.CullMode.CompareTo(y.CullMode);
if (order != 0) return order;
order = x.FirstIndex.CompareTo(y.FirstIndex);
if (order != 0) return order;
order = x.BaseVertex.CompareTo(y.BaseVertex);
if (order != 0) return order;
order = x.IndexCount.CompareTo(y.IndexCount);
if (order != 0) return order;
order = x.TextureSlot.Index.CompareTo(y.TextureSlot.Index);
if (order != 0) return order;
order = x.TextureLayer.CompareTo(y.TextureLayer);
// Campaign VM VM6: tie-break on FoliageFlags so entries sharing
// every other key field but differing only in classification
// (the rare case a mesh subset is reachable from both a
// procedural-scenery and a non-scenery placement) still sort
// into one contiguous, exact-key-matched run instead of an
// unstable-sort-dependent scatter. The grouping loop below keys
// on exact DirectionalShadowDrawKey equality regardless.
return order != 0
? order
: x.FoliageFlags.CompareTo(y.FoliageFlags);
}
}
}
public sealed partial class WbDrawDispatcher
{
private readonly DirectionalShadowPreparedDraws _directionalShadowDraws = new();
internal DirectionalShadowMeshGeometry GetDirectionalShadowGeometry()
{
GlobalMeshBuffer mesh = _meshAdapter.MeshManager?.GlobalBuffer
?? throw new InvalidOperationException("The shared mesh arena is not published.");
return new DirectionalShadowMeshGeometry(
mesh.VertexStore ?? throw new InvalidOperationException(
"The shared mesh arena has no vertex store."),
mesh.IndexStore ?? throw new InvalidOperationException(
"The shared mesh arena has no index store."));
}
/// <summary>
/// Classifies the retained resident caster product exactly once. The
/// returned owner is renderer-retained and remains valid until the next
/// distinct caster build is prepared.
/// </summary>
internal DirectionalShadowPreparedDraws PrepareDirectionalShadowDraws(
DirectionalShadowCasterFrame casters)
{
ArgumentNullException.ThrowIfNull(casters);
ReadOnlySpan<DirectionalShadowCaster> source = casters.Casters;
long renderDataAvailabilityVersion =
_meshAdapter.MeshManager?.RenderDataAvailabilityVersion ?? 0L;
ulong translucencyFadeRevision = _translucencyFades.Revision;
if (!_directionalShadowDraws.RequiresTopologyBuild(
casters.Generation,
casters.BuildSequence,
renderDataAvailabilityVersion,
translucencyFadeRevision))
{
_directionalShadowDraws.RefreshDynamicTransforms(casters);
return _directionalShadowDraws;
}
int estimatedInstances = 0;
for (int i = 0; i < source.Length; i++)
{
estimatedInstances = checked(
estimatedInstances
+ source[i].Projection.EntityPayload.MeshRefs.Count);
}
if (!_directionalShadowDraws.TryBegin(
casters.Generation,
casters.BuildSequence,
estimatedInstances,
renderDataAvailabilityVersion,
translucencyFadeRevision))
{
return _directionalShadowDraws;
}
int meshRefs = 0;
int parts = 0;
int batches = 0;
int rejectedTransparent = 0;
int rejectedFaded = 0;
int missingMeshes = 0;
int unresolvedCutoutTextures = 0;
try
{
for (int casterIndex = 0; casterIndex < source.Length; casterIndex++)
{
RenderProjectionRecord projection = source[casterIndex].Projection;
_directionalShadowDraws.MapCasterIdentity(
casterIndex,
projection.Id,
projection.ProjectionClass);
IReadOnlyList<MeshRef> projectionMeshes =
projection.EntityPayload.MeshRefs;
var frameCandidate = new RenderFrameEntityCandidate(
projection,
MeshPartOffset: 0,
MeshPartCount: projectionMeshes.Count,
Animated: source[casterIndex].UsesCurrentAnimatedTransforms);
RenderInstanceCandidate candidate =
RenderInstanceCandidate.FromFrame(
in frameCandidate,
projection.Residency.OwnerLandblockId);
PaletteCompositeIdentity paletteIdentity =
projection.EntityPayload.PaletteOverride is null
? default
: TextureCache.GetPaletteIdentity(
projection.EntityPayload.PaletteOverride);
for (int meshIndex = 0;
meshIndex < projectionMeshes.Count;
meshIndex++)
{
meshRefs++;
MeshRef meshRef = projectionMeshes[meshIndex];
ObjectRenderData? renderData =
_meshAdapter.TryGetRenderData(meshRef.GfxObjId);
if (renderData is null)
{
missingMeshes++;
_meshAdapter.EnsureLoaded(meshRef.GfxObjId);
continue;
}
if (renderData.IsSetup && renderData.SetupParts.Count > 0)
{
// Campaign VM VM6 review fix round (A4) — see
// FoliageWindClassification.ComputeEntityHasCutoutSubset's
// doc comment. Same shared helper the world-receiver
// Setup-part loop uses, so the caster's trunk-part
// classification agrees with the receiver's. Review
// fix round 2 (F3): the context-taking overload
// passes _meshAdapter explicitly to a static lambda
// instead of closing over it — zero allocation per
// entity per frame.
bool entityHasCutoutSubset = FoliageWindClassification
.ComputeEntityHasCutoutSubset(
renderData.SetupParts,
_meshAdapter,
static (adapter, part) => adapter.TryGetRenderData(part.GfxObjId)
is { HasCutoutSubset: true });
for (int setupPartIndex = 0;
setupPartIndex < renderData.SetupParts.Count;
setupPartIndex++)
{
parts++;
if (PartFadeExcludesCaster(
projection.Source.LocalEntityId,
setupPartIndex))
{
rejectedFaded++;
continue;
}
(ulong partGfxObjId, Matrix4x4 partTransform) =
renderData.SetupParts[setupPartIndex];
ObjectRenderData? partData =
_meshAdapter.TryGetRenderData(partGfxObjId);
if (partData is null)
{
missingMeshes++;
_meshAdapter.EnsureLoaded(partGfxObjId);
continue;
}
Matrix4x4 model = ComposePartWorldMatrix(
projection.Transform.LocalToWorld,
meshRef.PartTransform,
partTransform);
DirectionalShadowTransformSource transformSource =
source[casterIndex].UsesCurrentAnimatedTransforms
? DirectionalShadowTransformSource.Dynamic(
casterIndex,
meshIndex,
true,
in partTransform)
: default;
AddDirectionalShadowBatches(
partData,
in candidate,
meshRef,
paletteIdentity,
in model,
in transformSource,
ref batches,
ref rejectedTransparent,
ref unresolvedCutoutTextures,
entityHasCutoutSubset);
}
}
else
{
parts++;
if (PartFadeExcludesCaster(
projection.Source.LocalEntityId,
meshIndex))
{
rejectedFaded++;
continue;
}
Matrix4x4 model = meshRef.PartTransform
* projection.Transform.LocalToWorld;
Matrix4x4 noSetupPart = default;
DirectionalShadowTransformSource transformSource =
source[casterIndex].UsesCurrentAnimatedTransforms
? DirectionalShadowTransformSource.Dynamic(
casterIndex,
meshIndex,
false,
in noSetupPart)
: default;
AddDirectionalShadowBatches(
renderData,
in candidate,
meshRef,
paletteIdentity,
in model,
in transformSource,
ref batches,
ref rejectedTransparent,
ref unresolvedCutoutTextures);
}
}
}
var stats = new DirectionalShadowPreparationStats(
SourceCasters: source.Length,
SourceMeshRefs: meshRefs,
SourceParts: parts,
SourceBatches: batches,
PreparedInstances: 0,
PreparedOpaqueCommands: 0,
PreparedAlphaCutoutCommands: 0,
RejectedTransparentBatches: rejectedTransparent,
RejectedFadedParts: rejectedFaded,
MissingMeshes: missingMeshes,
UnresolvedAlphaCutoutTextures: unresolvedCutoutTextures);
_directionalShadowDraws.Complete(
casters.Generation,
casters.BuildSequence,
in stats,
renderDataAvailabilityVersion,
translucencyFadeRevision);
return _directionalShadowDraws;
}
catch
{
_directionalShadowDraws.Abort();
throw;
}
}
private bool PartFadeExcludesCaster(uint entityId, int partIndex) =>
_translucencyFades.TryGetCurrentValue(
entityId,
checked((uint)partIndex),
out float translucency)
&& DirectionalShadowPreparedDraws.FadeExcludesCaster(translucency);
private void AddDirectionalShadowBatches(
ObjectRenderData renderData,
in RenderInstanceCandidate candidate,
MeshRef meshRef,
PaletteCompositeIdentity paletteIdentity,
in Matrix4x4 model,
in DirectionalShadowTransformSource transformSource,
ref int sourceBatches,
ref int rejectedTransparent,
ref int unresolvedCutoutTextures,
// Campaign VM VM6 review fix round (A4): the ENTITY/Setup-scoped OR
// of every resolved part's HasCutoutSubset — see the matching
// parameter on WbDrawDispatcher.ClassifyBatches (the world receiver)
// for why a Setup composite's own per-part HasCutoutSubset is not
// enough. Null (the default) means "use renderData.HasCutoutSubset
// directly", correct for the non-Setup flat-MeshRef caller below.
bool? entityHasCutoutSubsetOverride = null)
{
bool entityHasCutoutSubset = entityHasCutoutSubsetOverride ?? renderData.HasCutoutSubset;
for (int batchIndex = 0;
batchIndex < renderData.Batches.Count;
batchIndex++)
{
ObjectRenderBatch batch = renderData.Batches[batchIndex];
// #426: a batch retail never draws for this entity casts no
// shadow either — same gate as ClassifyBatches/
// ClassifyPackedBatches (RetailUntexturedSubsetPolicy), so the
// caster and receiver agree by construction (mirrors the
// FoliageWindClassification comment below).
if (!RetailUntexturedSubsetPolicy.Draws(candidate.IsBuildingShell, batch.Key.IsSolid))
continue;
sourceBatches++;
if (!DirectionalShadowPreparedDraws.TryClassifyMaterial(
batch.Translucency,
out DirectionalShadowCasterMaterial material))
{
rejectedTransparent++;
continue;
}
GpuTextureSlot textureSlot = GpuTextureSlot.Unassigned;
uint textureLayer = 0;
if (material is DirectionalShadowCasterMaterial.AlphaCutout)
{
// ObjectRenderBatch.SurfaceId is a legacy property that the
// modern mesh uploader does not populate. Key.SurfaceId is
// the same authoritative id ResolveTexture consumes.
if (batch.Key.SurfaceId is 0 or uint.MaxValue)
{
unresolvedCutoutTextures++;
continue;
}
ResolvedTexture texture = ResolveTexture(
in candidate,
meshRef,
batch,
paletteIdentity,
out bool compositePending);
if (compositePending || !texture.Slot.IsAssigned)
{
unresolvedCutoutTextures++;
continue;
}
textureSlot = texture.Slot;
textureLayer = texture.Layer;
}
// Campaign VM VM6: the identical classification
// WbDrawDispatcher.ClassifyBatches computes for the matching
// world-receiver subset, so the caster and receiver agree by
// construction — the shadow moves with the same displaced
// vertex the receiver draws.
uint foliageFlags = FoliageWindClassification.Classify(
candidate.LocalEntityId,
FoliageWindExclusions.Contains(meshRef.GfxObjId),
batch.Translucency,
entityHasCutoutSubset);
_directionalShadowDraws.Add(
batch.FirstIndex,
checked((int)batch.BaseVertex),
batch.IndexCount,
textureSlot,
textureLayer,
batch.CullMode,
material,
in model,
in transformSource,
foliageFlags);
}
}
}