using System.Numerics; using System.Runtime.InteropServices; using AcDream.App.Rendering.Gpu; using AcDream.App.Rendering.Wb; namespace AcDream.App.Rendering; internal readonly record struct DirectionalShadowTransformPublishStats( bool TopologyUploaded, int DynamicMatricesUpdated, int DynamicRangesUpdated, long BytesWritten, int CurrentChangedMatrices = 0, int PendingReplayMatrices = 0, bool UsedFullDynamicFallback = false, bool DenseDirectUpload = false, bool DenseFlightReplay = false); /// /// Pack-owned transform storage indexed by the RHI frame-flight slot. A slot is /// handed back only after its prior GPU submission retires, so stable topology /// can keep every static matrix in mapped storage and update only the exact /// dynamic indices already refreshed by . /// No animation, scene lookup, or pose derivation happens here. /// internal sealed class DirectionalShadowTransformBufferSet : IDisposable { private readonly IGpuDevice _device; private SlotState[] _slots = []; private ulong _denseTopologyBuildSequence; private ulong _denseRevision = 1; private bool _disposed; internal DirectionalShadowTransformBufferSet(IGpuDevice device) { _device = device ?? throw new ArgumentNullException(nameof(device)); if (!device.Capabilities.SupportsPersistentlyMappedRings) { throw new NotSupportedException( "Directional-shadow retained transforms require persistently mapped host-writable buffers."); } } internal long RetainedGpuBytes { get { long total = 0; for (int i = 0; i < _slots.Length; i++) total = checked(total + (_slots[i].Buffer?.SizeBytes ?? 0L)); return total; } } internal int BufferCount { get { int count = 0; for (int i = 0; i < _slots.Length; i++) { if (_slots[i].Buffer is not null) count++; } return count; } } internal long RetainedScratchBytes { get { long bytes = checked((long)_slots.Length * System.Runtime.CompilerServices.Unsafe.SizeOf()); for (int index = 0; index < _slots.Length; index++) bytes = checked(bytes + (_slots[index].Pending?.RetainedBytes ?? 0L)); return bytes; } } internal DirectionalShadowTransformPublishStats LastStats { get; private set; } internal WorldTransformFrameSlice Publish( IGpuFrame frame, ulong topologyBuildSequence, ReadOnlySpan transforms, ReadOnlySpan dynamicTransformSlots) { return Publish( frame, topologyBuildSequence, transforms, dynamicTransformSlots, dynamicTransformSlots, denseRefresh: false); } internal WorldTransformFrameSlice Publish( IGpuFrame frame, ulong topologyBuildSequence, ReadOnlySpan transforms, ReadOnlySpan dynamicTransformSlots, ReadOnlySpan allDynamicTransformSlots) { return Publish( frame, topologyBuildSequence, transforms, dynamicTransformSlots, allDynamicTransformSlots, denseRefresh: false); } internal WorldTransformFrameSlice Publish( IGpuFrame frame, ulong topologyBuildSequence, ReadOnlySpan transforms, ReadOnlySpan dynamicTransformSlots, ReadOnlySpan allDynamicTransformSlots, bool denseRefresh, uint bindingSizeBytes = 0) { ObjectDisposedException.ThrowIf(_disposed, this); ArgumentNullException.ThrowIfNull(frame); if (topologyBuildSequence == 0) throw new ArgumentOutOfRangeException(nameof(topologyBuildSequence)); uint requiredInstances = checked((uint)transforms.Length); if (bindingSizeBytes == 0) { bindingSizeBytes = WorldTransformCapacityPolicy.ResolveBindingSizeBytes( requiredInstances, _device.Capabilities.MaxStorageBufferRangeBytes); } WorldTransformCapacityPolicy.ValidateBindingSizeBytes( bindingSizeBytes, requiredInstances, _device.Capabilities.MaxStorageBufferRangeBytes); if (!denseRefresh) ValidateDynamicSlots(dynamicTransformSlots, transforms.Length); ResetDenseRevisionForTopology(topologyBuildSequence); if (denseRefresh) { ValidateDynamicSlots(allDynamicTransformSlots, transforms.Length); if (_denseRevision == ulong.MaxValue) { throw new InvalidOperationException( "Directional-shadow dense transform revision was exhausted."); } _denseRevision++; for (int index = 0; index < _slots.Length; index++) _slots[index].Pending?.Clear(); } EnsureSlotCapacity(frame.SlotIndex); ref SlotState slot = ref _slots[frame.SlotIndex]; bool matchingSlot = slot.Buffer is not null && slot.TopologyBuildSequence == topologyBuildSequence && slot.TransformCount == transforms.Length && slot.Buffer.SizeBytes >= bindingSizeBytes; bool denseFlightReplay = matchingSlot && slot.ConsumedDenseRevision != _denseRevision; int pendingReplayMatrices = matchingSlot ? slot.Pending?.Count ?? 0 : 0; if (!denseRefresh) { MarkPendingChanges( topologyBuildSequence, transforms.Length, dynamicTransformSlots); } int contentBytes = checked(transforms.Length * 64); int allocationBytes = checked((int)bindingSizeBytes); if (slot.Buffer is null || slot.TopologyBuildSequence != topologyBuildSequence || slot.TransformCount != transforms.Length || slot.Buffer.SizeBytes < allocationBytes) { IGpuBuffer? candidate = null; try { candidate = _device.CreateBuffer(new GpuBufferDescription( $"directional-shadow-transforms-slot-{frame.SlotIndex}-build-{topologyBuildSequence}", allocationBytes, GpuBufferUsage.Storage | GpuBufferUsage.TransferDestination, GpuMemoryResidency.HostWritable)); if (!candidate.HostWritesAreCoherent) { throw new NotSupportedException( "Directional-shadow retained transforms require coherent " + "host-writable Vulkan memory. The pack will fail safe " + "on this adapter rather than expose unflushed pose data."); } if (!transforms.IsEmpty) { candidate.Upload(0, MemoryMarshal.AsBytes(transforms)); frame.PublishHostStorageWrites(candidate); } } catch { candidate?.Dispose(); throw; } IGpuBuffer? previous = slot.Buffer; PendingTransformSet pending = slot.Pending ?? new PendingTransformSet(transforms.Length); pending.EnsureCapacity(transforms.Length); pending.Clear(); slot = new SlotState( candidate, topologyBuildSequence, transforms.Length, pending, _denseRevision); previous?.Dispose(); LastStats = new DirectionalShadowTransformPublishStats( TopologyUploaded: true, DynamicMatricesUpdated: 0, DynamicRangesUpdated: 0, BytesWritten: contentBytes, CurrentChangedMatrices: dynamicTransformSlots.Length, PendingReplayMatrices: 0, DenseDirectUpload: denseRefresh); } else { PendingTransformSet pending = slot.Pending ?? throw new InvalidOperationException( "A retained directional-shadow flight slot has no pending-change owner."); bool directDenseUpload = denseRefresh || denseFlightReplay; ReadOnlySpan slotsToUpload = directDenseUpload ? allDynamicTransformSlots : pending.GetSorted(); if (directDenseUpload && !denseRefresh) ValidateDynamicSlots(allDynamicTransformSlots, transforms.Length); int ranges = UploadDynamicRanges( slot.Buffer, transforms, slotsToUpload, out long bytesWritten); if (ranges != 0) frame.PublishHostStorageWrites(slot.Buffer); LastStats = new DirectionalShadowTransformPublishStats( TopologyUploaded: false, DynamicMatricesUpdated: slotsToUpload.Length, DynamicRangesUpdated: ranges, BytesWritten: bytesWritten, CurrentChangedMatrices: dynamicTransformSlots.Length, PendingReplayMatrices: directDenseUpload ? 0 : pendingReplayMatrices, DenseDirectUpload: denseRefresh, DenseFlightReplay: denseFlightReplay && !denseRefresh); pending.Clear(); slot = slot with { ConsumedDenseRevision = _denseRevision }; } IGpuBuffer buffer = slot.Buffer ?? throw new InvalidOperationException( "The retained directional-shadow transform buffer was not published."); return new WorldTransformFrameSlice( frame.Serial, buffer, BaseOffsetBytes: 0, checked((uint)buffer.SizeBytes), FirstInstance: 0, checked((uint)transforms.Length)); } private void ResetDenseRevisionForTopology(ulong topologyBuildSequence) { if (_denseTopologyBuildSequence == topologyBuildSequence) return; _denseTopologyBuildSequence = topologyBuildSequence; _denseRevision = 1; for (int index = 0; index < _slots.Length; index++) _slots[index].Pending?.Clear(); } private void MarkPendingChanges( ulong topologyBuildSequence, int transformCount, ReadOnlySpan dynamicTransformSlots) { if (dynamicTransformSlots.IsEmpty) return; for (int index = 0; index < _slots.Length; index++) { ref SlotState candidate = ref _slots[index]; if (candidate.Buffer is null || candidate.TopologyBuildSequence != topologyBuildSequence || candidate.TransformCount != transformCount) { continue; } PendingTransformSet pending = candidate.Pending ??= new PendingTransformSet(transformCount); pending.EnsureCapacity(transformCount); pending.Mark(dynamicTransformSlots); } } private static int UploadDynamicRanges( IGpuBuffer buffer, ReadOnlySpan transforms, ReadOnlySpan slots, out long bytesWritten) { bytesWritten = 0; int ranges = 0; int cursor = 0; while (cursor < slots.Length) { int start = slots[cursor]; int end = start + 1; cursor++; while (cursor < slots.Length && slots[cursor] == end) { end++; cursor++; } ReadOnlySpan values = transforms.Slice(start, end - start); ReadOnlySpan bytes = MemoryMarshal.AsBytes(values); buffer.Upload(checked((long)start * 64L), bytes); bytesWritten = checked(bytesWritten + bytes.Length); ranges++; } return ranges; } private static void ValidateDynamicSlots( ReadOnlySpan slots, int transformCount) { int previous = -1; for (int i = 0; i < slots.Length; i++) { int current = slots[i]; if ((uint)current >= (uint)transformCount) { throw new InvalidOperationException( $"Dynamic shadow transform slot {current} is outside the " + $"{transformCount}-matrix retained product."); } if (current <= previous) { throw new InvalidOperationException( "Dynamic shadow transform slots must be strictly increasing."); } previous = current; } } private void EnsureSlotCapacity(int slotIndex) { ArgumentOutOfRangeException.ThrowIfNegative(slotIndex); if (_slots.Length > slotIndex) return; int capacity = _slots.Length == 0 ? 2 : _slots.Length; while (capacity <= slotIndex) capacity = checked(capacity * 2); Array.Resize(ref _slots, capacity); } public void Dispose() { if (_disposed) return; _disposed = true; for (int i = 0; i < _slots.Length; i++) { _slots[i].Buffer?.Dispose(); _slots[i] = default; } LastStats = default; _denseTopologyBuildSequence = 0; _denseRevision = 0; } private record struct SlotState( IGpuBuffer? Buffer, ulong TopologyBuildSequence, int TransformCount, PendingTransformSet? Pending, ulong ConsumedDenseRevision); private sealed class PendingTransformSet { private int[] _slots; private bool[] _marked; internal PendingTransformSet(int capacity) { ArgumentOutOfRangeException.ThrowIfNegative(capacity); _slots = new int[capacity]; _marked = new bool[capacity]; } internal int Count { get; private set; } internal long RetainedBytes => checked( (long)_slots.Length * sizeof(int) + _marked.Length); internal void EnsureCapacity(int capacity) { ArgumentOutOfRangeException.ThrowIfNegative(capacity); if (_slots.Length >= capacity) return; Array.Resize(ref _slots, capacity); Array.Resize(ref _marked, capacity); } internal void Mark(ReadOnlySpan slots) { for (int index = 0; index < slots.Length; index++) { int slot = slots[index]; if (_marked[slot]) continue; _marked[slot] = true; _slots[Count++] = slot; } } internal ReadOnlySpan GetSorted() { Array.Sort(_slots, 0, Count); return _slots.AsSpan(0, Count); } internal void Clear() { for (int index = 0; index < Count; index++) _marked[_slots[index]] = false; Count = 0; } } }