using Silk.NET.Vulkan; namespace AcDream.App.Rendering.Gpu.Vk; /// /// Campaign V slice V6c, plan §4.4: sets 0 and 1 for one flight slot, bound with /// dynamic offsets so no descriptor is ever written mid-frame. /// /// The contract lets a renderer bind an arbitrary buffer range per draw, /// and ring allocations mean that range moves every frame. The obvious /// implementation — write a descriptor per bind — would put a /// vkUpdateDescriptorSets in the hot path and reintroduce the exact cost /// the texture table was designed to remove. So each ring-fed binding is a /// *_BUFFER_DYNAMIC descriptor pointing at the whole ring, and the /// per-draw offset travels in vkCmdBindDescriptorSets's dynamic-offset /// array, which is free. /// /// Not every binding is dynamic. Slice V6g split set 0 by /// , because ten /// dynamic storage descriptors exceeded the device limit (plan §5.5.7 defect 1). /// A plain binding carries its offset in the descriptor itself, so it is /// rewritten when the range moves rather than when only the buffer changes — and /// its slot in the dynamic-offset array does not exist. Getting that array's /// length or ordering wrong is a validation error, so both are derived from the /// same predicate the layout is built from rather than restated. /// /// Every binding is always bound, whether a renderer uses it or /// not. Bindings a shader does not declare still need a live descriptor, so /// unused ones point at a shared dummy range. That is what lets there be ONE /// descriptor set layout and one pipeline layout rather than a permutation per /// renderer — plan §4.4's requirement, and the thing that makes switching /// pipelines mid-pass free. /// internal sealed unsafe class VulkanFrameBindings : IDisposable { private readonly Silk.NET.Vulkan.Vk _vk; private readonly Device _device; private readonly DescriptorPool _pool; private readonly DescriptorSet _storageSet; private readonly DescriptorSet _uniformSet; private readonly uint[] _storageOffsets = new uint[GpuBindingModel.StorageBindingCount]; private readonly uint[] _uniformOffsets = new uint[UniformBindingCount]; private readonly Silk.NET.Vulkan.Buffer[] _storageBuffers = new Silk.NET.Vulkan.Buffer[GpuBindingModel.StorageBindingCount]; private readonly Silk.NET.Vulkan.Buffer[] _uniformBuffers = new Silk.NET.Vulkan.Buffer[UniformBindingCount]; private bool _disposed; /// /// Set 0's dynamic-offset slots, in binding order — the order /// vkCmdBindDescriptorSets requires. A plain binding has no slot. /// private static readonly uint[] DynamicStorageBindings = BuildDynamicStorageBindings(); private static uint[] BuildDynamicStorageBindings() { var bindings = new List((int)GpuBindingModel.StorageBindingCount); for (uint binding = 0; binding < GpuBindingModel.StorageBindingCount; binding++) { if (VulkanPipelineLayouts.IsDynamicStorageBinding(binding)) bindings.Add(binding); } return [.. bindings]; } /// Bindings 0..3 of set 1; only 1 (SceneLighting) and 3 (terrain tiling) are used. internal const int UniformBindingCount = 4; /// /// How many of set 1's bindings the layout actually declares, all dynamic. /// Asserted against maxDescriptorSetUniformBuffersDynamic by the /// capability gate; Vulkan guarantees 8, so this is comfortable. /// internal const uint DynamicUniformBindingCount = 2; /// /// Widest range any single binding may address. Dynamic descriptors take a /// static range at write time and slide it with an offset, so this bounds /// how much of the ring one binding can see at once. /// internal const uint MaxBindingRangeBytes = 4 * 1024 * 1024; internal VulkanFrameBindings( Silk.NET.Vulkan.Vk vk, Device device, VulkanPipelineLayouts.Created layouts, VulkanGpuBuffer ring, VulkanGpuBuffer dummy) { _vk = vk ?? throw new ArgumentNullException(nameof(vk)); _device = device; ArgumentNullException.ThrowIfNull(layouts); ArgumentNullException.ThrowIfNull(ring); ArgumentNullException.ThrowIfNull(dummy); DescriptorPoolSize* sizes = stackalloc DescriptorPoolSize[3]; sizes[0] = new DescriptorPoolSize { Type = DescriptorType.StorageBufferDynamic, DescriptorCount = VulkanPipelineLayouts.DynamicStorageBindingCount, }; sizes[1] = new DescriptorPoolSize { Type = DescriptorType.StorageBuffer, DescriptorCount = GpuBindingModel.StorageBindingCount - VulkanPipelineLayouts.DynamicStorageBindingCount, }; sizes[2] = new DescriptorPoolSize { Type = DescriptorType.UniformBufferDynamic, DescriptorCount = UniformBindingCount, }; var poolCreate = new DescriptorPoolCreateInfo { SType = StructureType.DescriptorPoolCreateInfo, MaxSets = 2, PoolSizeCount = 3, PPoolSizes = sizes, }; VulkanInterop.Check( _vk.CreateDescriptorPool(_device, &poolCreate, null, out _pool), "vkCreateDescriptorPool (frame bindings)"); _storageSet = Allocate(layouts.Storage); _uniformSet = Allocate(layouts.Uniform); for (uint binding = 0; binding < GpuBindingModel.StorageBindingCount; binding++) { _storageBuffers[binding] = dummy.Handle; WriteStorage( binding, dummy.Handle, offsetBytes: 0, (uint)Math.Min(dummy.SizeBytes, MaxBindingRangeBytes), VulkanPipelineLayouts.IsDynamicStorageBinding(binding)); } // Only the two bindings the layout declares exist; the rest of the // array is bookkeeping so the offsets stay index-aligned. WriteUniform(GpuBindingModel.UniformSceneLighting, dummy.Handle, (uint)Math.Min(dummy.SizeBytes, 65536)); WriteUniform(GpuBindingModel.UniformTerrainTiling, dummy.Handle, (uint)Math.Min(dummy.SizeBytes, 65536)); _uniformBuffers[GpuBindingModel.UniformSceneLighting] = dummy.Handle; _uniformBuffers[GpuBindingModel.UniformTerrainTiling] = dummy.Handle; Ring = ring; Dummy = dummy; } internal VulkanGpuBuffer Ring { get; } internal VulkanGpuBuffer Dummy { get; } /// /// Points a storage binding at a range. /// /// A DYNAMIC binding re-writes its descriptor only when the BUFFER /// changes; the offset rides the bind call. A PLAIN binding has no such /// channel, so the descriptor itself carries the offset and is re-written /// when either moves. /// internal void SetStorage(uint binding, VulkanGpuBuffer buffer, uint offsetBytes, uint sizeBytes) { ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(binding, GpuBindingModel.StorageBindingCount); bool dynamic = VulkanPipelineLayouts.IsDynamicStorageBinding(binding); bool bufferChanged = _storageBuffers[binding].Handle != buffer.Handle.Handle; bool offsetChanged = _storageOffsets[binding] != offsetBytes; if (bufferChanged || (!dynamic && offsetChanged)) { _storageBuffers[binding] = buffer.Handle; WriteStorage( binding, buffer.Handle, dynamic ? 0 : offsetBytes, ClampRange(buffer, sizeBytes, dynamic ? 0 : offsetBytes), dynamic); } _storageOffsets[binding] = offsetBytes; } internal void SetUniform(uint binding, VulkanGpuBuffer buffer, uint offsetBytes, uint sizeBytes) { ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(binding, (uint)UniformBindingCount); if (_uniformBuffers[binding].Handle != buffer.Handle.Handle) { _uniformBuffers[binding] = buffer.Handle; WriteUniform(binding, buffer.Handle, Math.Min(ClampRange(buffer, sizeBytes, offsetBytes: 0), 65536)); } _uniformOffsets[binding] = offsetBytes; } /// Binds all three sets with the current dynamic offsets. internal void Bind(CommandBuffer commands, VulkanGpuDevice device) { DescriptorSet* sets = stackalloc DescriptorSet[3]; sets[0] = _storageSet; sets[1] = _uniformSet; sets[2] = device.TextureTable.Set; int dynamicCount = DynamicStorageBindings.Length + 2; uint* offsets = stackalloc uint[dynamicCount]; // Dynamic offsets are ordered by set, then by binding number, and only // the DYNAMIC descriptors have a slot at all. for (int i = 0; i < DynamicStorageBindings.Length; i++) offsets[i] = _storageOffsets[DynamicStorageBindings[i]]; offsets[DynamicStorageBindings.Length + 0] = _uniformOffsets[GpuBindingModel.UniformSceneLighting]; offsets[DynamicStorageBindings.Length + 1] = _uniformOffsets[GpuBindingModel.UniformTerrainTiling]; _vk.CmdBindDescriptorSets( commands, PipelineBindPoint.Graphics, device.Layouts.PipelineLayout, 0, 3, sets, (uint)dynamicCount, offsets); } private static uint ClampRange(VulkanGpuBuffer buffer, uint requested, uint offsetBytes) { long remaining = buffer.SizeBytes - offsetBytes; if (remaining <= 0) { throw new ArgumentOutOfRangeException( nameof(offsetBytes), offsetBytes, $"A storage binding was pointed past the end of its {buffer.SizeBytes}-byte buffer. " + "A descriptor range of zero is not representable in Vulkan."); } uint available = (uint)Math.Min(remaining, MaxBindingRangeBytes); return requested == 0 ? available : Math.Min(Math.Max(requested, 16), available); } private DescriptorSet Allocate(DescriptorSetLayout layout) { DescriptorSetLayout handle = layout; var allocate = new DescriptorSetAllocateInfo { SType = StructureType.DescriptorSetAllocateInfo, DescriptorPool = _pool, DescriptorSetCount = 1, PSetLayouts = &handle, }; VulkanInterop.Check( _vk.AllocateDescriptorSets(_device, &allocate, out DescriptorSet set), "vkAllocateDescriptorSets (frame bindings)"); return set; } private void WriteStorage( uint binding, Silk.NET.Vulkan.Buffer buffer, uint offsetBytes, uint rangeBytes, bool dynamic) { var info = new DescriptorBufferInfo { Buffer = buffer, Offset = offsetBytes, Range = rangeBytes, }; var write = new WriteDescriptorSet { SType = StructureType.WriteDescriptorSet, DstSet = _storageSet, DstBinding = binding, DescriptorCount = 1, DescriptorType = dynamic ? DescriptorType.StorageBufferDynamic : DescriptorType.StorageBuffer, PBufferInfo = &info, }; _vk.UpdateDescriptorSets(_device, 1, &write, 0, null); } private void WriteUniform(uint binding, Silk.NET.Vulkan.Buffer buffer, uint rangeBytes) { var info = new DescriptorBufferInfo { Buffer = buffer, Offset = 0, Range = rangeBytes, }; var write = new WriteDescriptorSet { SType = StructureType.WriteDescriptorSet, DstSet = _uniformSet, DstBinding = binding, DescriptorCount = 1, DescriptorType = DescriptorType.UniformBufferDynamic, PBufferInfo = &info, }; _vk.UpdateDescriptorSets(_device, 1, &write, 0, null); } public void Dispose() { if (_disposed) return; _disposed = true; if (_pool.Handle != 0) _vk.DestroyDescriptorPool(_device, _pool, null); } }