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;
}
}