using Silk.NET.Vulkan; namespace AcDream.App.Rendering.Gpu.Vk; /// /// Campaign V, plan §3.4 and §4.4: the three descriptor set layouts and the ONE /// pipeline layout every acdream pipeline shares. /// /// Extracted from slice V5's active capability probe at V6b so the probe /// and the live backend build the same objects from the same code. The probe's /// whole value is that it proves the production layouts can be created on this /// device; a second, similar-looking definition would quietly destroy that /// property the first time one of them changed. /// /// One pipeline layout is a decision, not an economy. Because every /// pipeline shares it, switching pipelines mid-pass does not invalidate bound /// descriptor sets or push constants — which is what lets the world dispatcher /// bind the texture table once per frame and then change pipeline per bucket. /// The single 96-byte push-constant block exists for the same reason. /// internal static unsafe class VulkanPipelineLayouts { /// The three sets plus the shared layout, owned together and destroyed together. internal sealed class Created( DescriptorSetLayout storage, DescriptorSetLayout uniform, DescriptorSetLayout textureTable, PipelineLayout pipelineLayout) : IDisposable { private bool _disposed; internal DescriptorSetLayout Storage { get; } = storage; internal DescriptorSetLayout Uniform { get; } = uniform; internal DescriptorSetLayout TextureTable { get; } = textureTable; internal PipelineLayout PipelineLayout { get; } = pipelineLayout; internal void Destroy(Silk.NET.Vulkan.Vk vk, Device device) { if (_disposed) return; _disposed = true; if (PipelineLayout.Handle != 0) vk.DestroyPipelineLayout(device, PipelineLayout, null); if (TextureTable.Handle != 0) vk.DestroyDescriptorSetLayout(device, TextureTable, null); if (Uniform.Handle != 0) vk.DestroyDescriptorSetLayout(device, Uniform, null); if (Storage.Handle != 0) vk.DestroyDescriptorSetLayout(device, Storage, null); } /// Destruction needs the device, so is the real disposer. public void Dispose() => _disposed = true; } /// Creates all four objects, cleaning up whatever succeeded if a later one fails. internal static Created Create(Silk.NET.Vulkan.Vk vk, Device device) { ArgumentNullException.ThrowIfNull(vk); DescriptorSetLayout storage = default; DescriptorSetLayout uniform = default; DescriptorSetLayout table = default; try { storage = CreateStorageSetLayout(vk, device); uniform = CreateUniformSetLayout(vk, device); table = CreateTextureTableSetLayout(vk, device); PipelineLayout layout = CreatePipelineLayout(vk, device, storage, uniform, table); return new Created(storage, uniform, table, layout); } catch { if (table.Handle != 0) vk.DestroyDescriptorSetLayout(device, table, null); if (uniform.Handle != 0) vk.DestroyDescriptorSetLayout(device, uniform, null); if (storage.Handle != 0) vk.DestroyDescriptorSetLayout(device, storage, null); throw; } } /// /// Campaign V slice V6g: which of the ten storage bindings gets a DYNAMIC /// descriptor, and why not all of them. /// /// V6b declared all ten STORAGE_BUFFER_DYNAMIC, on the reasoning /// that the contract lets a renderer bind an arbitrary range per draw. That /// met a real device limit the first time a validation layer looked at it: /// maxDescriptorSetStorageBuffersDynamic is 8 on the RX 9070 XT and /// only 4 at Vulkan's guaranteed minimum, so ten was never portable — /// see plan §5.5.7 defect 1. /// /// The rule. A dynamic descriptor buys exactly one thing: the /// ability to address the SAME buffer at a DIFFERENT offset without a /// descriptor write. That is the shape of a per-frame ring allocation, so /// the bindings a renderer feeds from the ring stay dynamic and the offset /// travels in vkCmdBindDescriptorSets for free. Bindings that point at /// a long-lived, renderer-owned buffer written whole and bound once per pass /// buy nothing from it, and each one costs a scarce device resource. /// /// Four dynamic descriptors is not merely under the RX 9070 XT's 8 — it /// is exactly Vulkan's guaranteed minimum, so no device that can run acdream /// at all can fail this layout. That matters for slice V9's lavapipe row and /// for whatever Linux driver the deferred physical row eventually uses. /// /// Binding 9 is the clearest case. The texture table is the /// GL-only uvec2 handle-buffer emulation; the Vulkan backend binds set /// 2 instead and never touches binding 9 at all, so a dynamic descriptor for /// it would be a device resource spent on a binding that is provably never /// bound. /// /// What to do if V4c disagrees. Bindings 6, 7 and 8 are /// per-instance arrays grouped here with the frame-global tables because /// their owner writes them whole once per frame. If the Vulkan world path /// turns out to re-point one of them at a moving ring offset per draw, /// promoting it back is one line here plus one in /// — and there are four unused dynamic /// slots to promote into before the guaranteed minimum is exceeded. /// internal static bool IsDynamicStorageBinding(uint binding) => binding switch { // Per-frame ring uploads: the instance transform array, the per-draw // batch table, and the two arrays the world dispatcher chunks alongside // instances. GpuBindingModel.StorageInstances => true, GpuBindingModel.StorageBatches => true, GpuBindingModel.StorageClipSlots => true, GpuBindingModel.StorageInstanceLightSets => true, _ => false, }; /// /// How many of set 0's bindings are dynamic. Asserted against /// maxDescriptorSetStorageBuffersDynamic by the capability gate, so a /// device that cannot serve the layout is rejected at startup with the /// exit-code-4 contract rather than at vkCreatePipelineLayout. /// internal static uint DynamicStorageBindingCount { get; } = CountDynamicStorageBindings(); private static uint CountDynamicStorageBindings() { uint count = 0; for (uint binding = 0; binding < GpuBindingModel.StorageBindingCount; binding++) { if (IsDynamicStorageBinding(binding)) count++; } return count; } /// /// Set 0 — the ten storage bindings pins, split /// between dynamic and plain by . /// internal static DescriptorSetLayout CreateStorageSetLayout(Silk.NET.Vulkan.Vk vk, Device device) { int count = (int)GpuBindingModel.StorageBindingCount; DescriptorSetLayoutBinding* bindings = stackalloc DescriptorSetLayoutBinding[count]; for (int i = 0; i < count; i++) { bindings[i] = new DescriptorSetLayoutBinding { Binding = (uint)i, DescriptorType = IsDynamicStorageBinding((uint)i) ? DescriptorType.StorageBufferDynamic : DescriptorType.StorageBuffer, DescriptorCount = 1, StageFlags = ShaderStageFlags.VertexBit | ShaderStageFlags.FragmentBit, }; } var create = new DescriptorSetLayoutCreateInfo { SType = StructureType.DescriptorSetLayoutCreateInfo, BindingCount = (uint)count, PBindings = bindings, }; VulkanInterop.Check( vk.CreateDescriptorSetLayout(device, &create, null, out DescriptorSetLayout layout), "vkCreateDescriptorSetLayout (set 0, storage)"); return layout; } /// Set 1 — the SceneLighting and terrain-tiling uniform blocks. internal static DescriptorSetLayout CreateUniformSetLayout(Silk.NET.Vulkan.Vk vk, Device device) { DescriptorSetLayoutBinding* bindings = stackalloc DescriptorSetLayoutBinding[2]; bindings[0] = new DescriptorSetLayoutBinding { Binding = GpuBindingModel.UniformSceneLighting, DescriptorType = DescriptorType.UniformBufferDynamic, DescriptorCount = 1, StageFlags = ShaderStageFlags.VertexBit | ShaderStageFlags.FragmentBit, }; bindings[1] = new DescriptorSetLayoutBinding { Binding = GpuBindingModel.UniformTerrainTiling, DescriptorType = DescriptorType.UniformBufferDynamic, DescriptorCount = 1, StageFlags = ShaderStageFlags.VertexBit | ShaderStageFlags.FragmentBit, }; var create = new DescriptorSetLayoutCreateInfo { SType = StructureType.DescriptorSetLayoutCreateInfo, BindingCount = 2, PBindings = bindings, }; VulkanInterop.Check( vk.CreateDescriptorSetLayout(device, &create, null, out DescriptorSetLayout layout), "vkCreateDescriptorSetLayout (set 1, uniform)"); return layout; } /// /// Set 2 — the production texture table exactly as §4.4 specifies it: one /// combined-image-sampler binding of /// , partially bound, /// update-after-bind, update-unused-while-pending, variable count. /// internal static DescriptorSetLayout CreateTextureTableSetLayout(Silk.NET.Vulkan.Vk vk, Device device) { var binding = new DescriptorSetLayoutBinding { Binding = GpuBindingModel.TextureTableBinding, DescriptorType = DescriptorType.CombinedImageSampler, DescriptorCount = GpuBindingModel.TextureTableCapacity, StageFlags = ShaderStageFlags.FragmentBit, }; DescriptorBindingFlags flags = DescriptorBindingFlags.PartiallyBoundBit | DescriptorBindingFlags.UpdateAfterBindBit | DescriptorBindingFlags.UpdateUnusedWhilePendingBit | DescriptorBindingFlags.VariableDescriptorCountBit; var bindingFlags = new DescriptorSetLayoutBindingFlagsCreateInfo { SType = StructureType.DescriptorSetLayoutBindingFlagsCreateInfo, BindingCount = 1, PBindingFlags = &flags, }; var create = new DescriptorSetLayoutCreateInfo { SType = StructureType.DescriptorSetLayoutCreateInfo, PNext = &bindingFlags, Flags = DescriptorSetLayoutCreateFlags.UpdateAfterBindPoolBit, BindingCount = 1, PBindings = &binding, }; VulkanInterop.Check( vk.CreateDescriptorSetLayout(device, &create, null, out DescriptorSetLayout layout), "vkCreateDescriptorSetLayout (set 2, texture table)"); return layout; } /// /// One shared pipeline layout: three sets plus the single 96-byte /// push-constant block. Creating it proves maxBoundDescriptorSets and /// maxPushConstantsSize for real rather than by reading a limit. /// internal static PipelineLayout CreatePipelineLayout( Silk.NET.Vulkan.Vk vk, Device device, DescriptorSetLayout storage, DescriptorSetLayout uniform, DescriptorSetLayout table) { DescriptorSetLayout* sets = stackalloc DescriptorSetLayout[3]; sets[0] = storage; sets[1] = uniform; sets[2] = table; var pushConstants = new PushConstantRange { StageFlags = ShaderStageFlags.VertexBit | ShaderStageFlags.FragmentBit, Offset = 0, Size = GpuBindingModel.PushConstantBytes, }; var create = new PipelineLayoutCreateInfo { SType = StructureType.PipelineLayoutCreateInfo, SetLayoutCount = 3, PSetLayouts = sets, PushConstantRangeCount = 1, PPushConstantRanges = &pushConstants, }; VulkanInterop.Check( vk.CreatePipelineLayout(device, &create, null, out PipelineLayout layout), "vkCreatePipelineLayout"); return layout; } }