using System.Runtime.CompilerServices; using AcDream.App.Rendering.Wb; using AcDream.App.Rendering.Gpu; namespace AcDream.App.Rendering; internal readonly record struct DirectionalShadowTerrainRange( uint FirstIndex, int IndexCount); internal readonly record struct DirectionalShadowTerrainGeometry( IGpuBuffer VertexBuffer, IGpuBuffer IndexBuffer); /// /// The complete resident terrain arena expressed once as indirect commands. /// It deliberately has no camera, PView, portal, or cascade input. /// internal sealed class DirectionalShadowTerrainPreparedDraws { private DrawElementsIndirectCommand[] _commands = []; private int _count; private bool _building; public long SourceFrameSequence { get; private set; } public ulong BuildSequence { get; private set; } public ReadOnlySpan Commands => _commands.AsSpan(0, _count); public long RetainedScratchBytes => checked((long)_commands.Length * Unsafe.SizeOf()); public bool TryBegin(long frameSequence, int estimatedCommands) { if (_building) throw new InvalidOperationException( "A terrain shadow draw build is already active."); if (frameSequence <= 0) throw new ArgumentOutOfRangeException(nameof(frameSequence)); ArgumentOutOfRangeException.ThrowIfNegative(estimatedCommands); if (SourceFrameSequence == frameSequence) return false; EnsureCapacity(estimatedCommands); _count = 0; _building = true; return true; } public void Add(in DirectionalShadowTerrainRange range) { if (!_building) throw new InvalidOperationException( "Begin a terrain shadow draw build before adding ranges."); if (range.IndexCount <= 0) throw new ArgumentOutOfRangeException(nameof(range)); EnsureCapacity(checked(_count + 1)); _commands[_count++] = new DrawElementsIndirectCommand { Count = checked((uint)range.IndexCount), InstanceCount = 1, FirstIndex = range.FirstIndex, BaseVertex = 0, BaseInstance = 0, }; } public void Complete(long frameSequence) { if (!_building) throw new InvalidOperationException( "No terrain shadow draw build is active."); if (frameSequence <= 0) throw new ArgumentOutOfRangeException(nameof(frameSequence)); SourceFrameSequence = frameSequence; BuildSequence = checked(BuildSequence + 1); _building = false; } public void Abort() { _count = 0; _building = false; } private void EnsureCapacity(int required) { if (_commands.Length >= required) return; int capacity = _commands.Length == 0 ? 16 : _commands.Length; while (capacity < required) capacity = checked(capacity * 2); Array.Resize(ref _commands, capacity); } } public sealed partial class TerrainModernRenderer { private readonly DirectionalShadowTerrainPreparedDraws _directionalShadowTerrainDraws = new(); private long _directionalShadowFrameSequence; internal DirectionalShadowTerrainGeometry GetDirectionalShadowGeometry() => new( _vertexStore ?? throw new InvalidOperationException("Terrain has no vertex store."), _indexStore ?? throw new InvalidOperationException("Terrain has no index store.")); /// /// Builds one all-resident indirect list for the current frame. Repeated /// calls by individual cascades return the same retained product. /// internal DirectionalShadowTerrainPreparedDraws PrepareDirectionalShadowDraws() { if (!_directionalShadowTerrainDraws.TryBegin( _directionalShadowFrameSequence, _alloc.LoadedCount)) { return _directionalShadowTerrainDraws; } try { for (int slot = 0; slot < _slots.Length; slot++) { SlotData? data = _slots[slot]; if (data is null) continue; var range = new DirectionalShadowTerrainRange( data.FirstIndex, data.IndexCount); _directionalShadowTerrainDraws.Add(in range); } _directionalShadowTerrainDraws.Complete( _directionalShadowFrameSequence); return _directionalShadowTerrainDraws; } catch { _directionalShadowTerrainDraws.Abort(); throw; } } }