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); } /// /// 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. /// 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 Transforms => _transforms.AsSpan(0, _sourceCount); /// /// 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. /// public ReadOnlySpan DynamicTransformSlots => _dynamicTransformSlots.AsSpan(0, _dynamicTransformSlotCount); /// /// 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. /// public ReadOnlySpan AllDynamicTransformSlots => _allDynamicTransformSlots.AsSpan(0, _allDynamicTransformSlotCount); public ReadOnlySpan Commands => _commands.AsSpan(0, _commandCount); public ReadOnlySpan OpaqueCommands => _commands.AsSpan(0, OpaqueCommandCount); public ReadOnlySpan AlphaCutoutCommands => _commands.AsSpan(OpaqueCommandCount, AlphaCutoutCommandCount); public ReadOnlySpan Batches => _batches.AsSpan(0, _commandCount); public ReadOnlySpan OpaqueBatches => _batches.AsSpan(0, OpaqueCommandCount); public ReadOnlySpan AlphaCutoutBatches => _batches.AsSpan(OpaqueCommandCount, AlphaCutoutCommandCount); public ReadOnlySpan Runs => _runs.AsSpan(0, _runCount); public ReadOnlySpan OpaqueRuns => _runs.AsSpan(0, OpaqueRunCount); public ReadOnlySpan AlphaCutoutRuns => _runs.AsSpan(OpaqueRunCount, _runCount - OpaqueRunCount); public DirectionalShadowPreparationStats Stats { get; private set; } public long RetainedScratchBytes => checked( (long)_source.Length * Unsafe.SizeOf() + (long)_transforms.Length * Unsafe.SizeOf() + (long)_transformSources.Length * Unsafe.SizeOf() + (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() + (long)_mappedCasterClasses.Length * Unsafe.SizeOf() + _mappedCasterIdentityPresent.Length + (long)_commands.Length * Unsafe.SizeOf() + (long)_batches.Length * Unsafe.SizeOf() + (long)_runs.Length * Unsafe.SizeOf()); /// /// 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. /// 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 currentCasters) { RefreshAllDynamicTransforms(currentCasters); LastDynamicTransformRefreshWasDense = false; } internal void RefreshDenseDynamicTransforms( ReadOnlySpan currentCasters) { RefreshAllDynamicTransforms(currentCasters); LastDynamicTransformRefreshWasDense = true; } private void RefreshAllDynamicTransforms( ReadOnlySpan 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 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 currentCasters, ReadOnlySpan 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 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; } /// /// 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. /// private void RefreshDenseDynamicTransforms( ReadOnlySpan 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 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 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 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(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 { 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.")); } /// /// 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. /// internal DirectionalShadowPreparedDraws PrepareDirectionalShadowDraws( DirectionalShadowCasterFrame casters) { ArgumentNullException.ThrowIfNull(casters); ReadOnlySpan 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 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); } } }