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.
///
/// Slice V6i: one set pair per renderer scope. There is no longer a
/// single (set 0, set 1) pair per flight slot; there is an arena of them, and
/// decides which pair a bind belongs to and
/// whether its descriptors have to be written. That is what closes plan §5.5.8's
/// recorded one-binding-two-buffers hazard before the world arm fires it — see
/// the arena's own documentation for why the scope is derived from the
/// descriptor state rather than declared by the renderer.
///
internal sealed unsafe class VulkanFrameBindings : IDisposable
{
private readonly Silk.NET.Vulkan.Vk _vk;
private readonly Device _device;
private readonly VulkanPipelineLayouts.Created _layouts;
private readonly VulkanBindingScopeArena _arena;
private readonly List _pools = [];
private readonly List<(DescriptorSet Storage, DescriptorSet Uniform)> _sets = [];
private bool _disposed;
///
/// Set pairs one descriptor pool serves. Distinct descriptor states in a
/// frame are the world renderers plus the retained UI, so this is generous;
/// exceeding it allocates another pool rather than failing.
///
private const int PairsPerPool = 16;
///
/// 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..4 of set 1. Slice V6i-2 raised this from 4 when the layout
/// gained binding 4 (sky params); binding 0 remains unused and is counted
/// only so the bookkeeping arrays stay index-aligned with the binding number.
/// Which of them the layout DECLARES is
/// .
///
internal const int UniformBindingCount = 5;
///
/// 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 static uint DynamicUniformBindingCount { get; } =
(uint)VulkanPipelineLayouts.DeclaredUniformBindings.Length;
///
/// 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;
_layouts = layouts ?? throw new ArgumentNullException(nameof(layouts));
ArgumentNullException.ThrowIfNull(ring);
ArgumentNullException.ThrowIfNull(dummy);
Ring = ring;
Dummy = dummy;
_arena = new VulkanBindingScopeArena(
(int)GpuBindingModel.StorageBindingCount,
UniformBindingCount,
VulkanPipelineLayouts.IsDynamicStorageBinding);
uint dummyStorageRange = (uint)Math.Min(dummy.SizeBytes, MaxBindingRangeBytes);
for (uint binding = 0; binding < GpuBindingModel.StorageBindingCount; binding++)
_arena.SeedStorage(binding, dummy.Handle.Handle, offsetBytes: 0, dummyStorageRange);
uint dummyUniformRange = (uint)Math.Min(dummy.SizeBytes, 65536);
for (uint binding = 0; binding < UniformBindingCount; binding++)
_arena.SeedUniform(binding, dummy.Handle.Handle, dummyUniformRange);
}
internal VulkanGpuBuffer Ring { get; }
internal VulkanGpuBuffer Dummy { get; }
///
/// Diagnostic: distinct descriptor states this flight slot materialised. One
/// per renderer scope in a steady frame, so a number that keeps climbing is
/// a renderer pointing a binding at a fresh buffer every draw.
///
internal int ScopeCount => _arena.Count;
///
/// Recycles the arena for a new frame on this slot. Safe because the slot's
/// previous submission has retired before BeginFrame returns, which is
/// the same guarantee that lets the ring rewind.
///
internal void BeginFrame() => _arena.BeginFrame();
internal void SetStorage(uint binding, VulkanGpuBuffer buffer, uint offsetBytes, uint sizeBytes)
{
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(binding, GpuBindingModel.StorageBindingCount);
uint descriptorOffset = VulkanPipelineLayouts.IsDynamicStorageBinding(binding) ? 0 : offsetBytes;
_arena.SetStorage(
binding,
buffer.Handle.Handle,
offsetBytes,
ClampRange(buffer, sizeBytes, descriptorOffset));
}
internal void SetUniform(uint binding, VulkanGpuBuffer buffer, uint offsetBytes, uint sizeBytes)
{
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(binding, (uint)UniformBindingCount);
_arena.SetUniform(
binding,
buffer.Handle.Handle,
offsetBytes,
Math.Min(ClampRange(buffer, sizeBytes, offsetBytes: 0), 65536));
}
/// Binds all three sets with the current dynamic offsets.
internal void Bind(CommandBuffer commands, VulkanGpuDevice device)
{
(int index, int slot, bool needsWrite) = _arena.Resolve();
if (slot < 0)
{
slot = _sets.Count;
_sets.Add(AllocatePair());
_arena.AssignSlot(index, slot);
}
if (needsWrite)
WritePair(_sets[slot]);
DescriptorSet* sets = stackalloc DescriptorSet[3];
sets[0] = _sets[slot].Storage;
sets[1] = _sets[slot].Uniform;
sets[2] = device.TextureTable.Set;
uint[] declaredUniforms = VulkanPipelineLayouts.DeclaredUniformBindings;
int dynamicCount = DynamicStorageBindings.Length + declaredUniforms.Length;
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] = _arena.StorageOffset(DynamicStorageBindings[i]);
for (int i = 0; i < declaredUniforms.Length; i++)
offsets[DynamicStorageBindings.Length + i] = _arena.UniformOffset(declaredUniforms[i]);
_vk.CmdBindDescriptorSets(
commands,
PipelineBindPoint.Graphics,
device.Layouts.PipelineLayout,
0,
3,
sets,
(uint)dynamicCount,
offsets);
}
private void WritePair((DescriptorSet Storage, DescriptorSet Uniform) pair)
{
for (uint binding = 0; binding < GpuBindingModel.StorageBindingCount; binding++)
{
WriteStorage(
pair.Storage,
binding,
new Silk.NET.Vulkan.Buffer(_arena.StorageBuffer(binding)),
_arena.StorageDescriptorOffset(binding),
_arena.StorageRange(binding),
VulkanPipelineLayouts.IsDynamicStorageBinding(binding));
}
// Only the bindings the layout declares exist; the rest of the array is
// bookkeeping so the offsets stay index-aligned with the binding number.
foreach (uint binding in VulkanPipelineLayouts.DeclaredUniformBindings)
{
WriteUniform(
pair.Uniform,
binding,
new Silk.NET.Vulkan.Buffer(_arena.UniformBuffer(binding)),
_arena.UniformRange(binding));
}
}
private (DescriptorSet Storage, DescriptorSet Uniform) AllocatePair()
{
if (_sets.Count % PairsPerPool == 0)
_pools.Add(CreatePool());
DescriptorPool pool = _pools[^1];
return (Allocate(pool, _layouts.Storage), Allocate(pool, _layouts.Uniform));
}
private DescriptorPool CreatePool()
{
DescriptorPoolSize* sizes = stackalloc DescriptorPoolSize[3];
sizes[0] = new DescriptorPoolSize
{
Type = DescriptorType.StorageBufferDynamic,
DescriptorCount = VulkanPipelineLayouts.DynamicStorageBindingCount * PairsPerPool,
};
sizes[1] = new DescriptorPoolSize
{
Type = DescriptorType.StorageBuffer,
DescriptorCount =
(GpuBindingModel.StorageBindingCount - VulkanPipelineLayouts.DynamicStorageBindingCount)
* PairsPerPool,
};
sizes[2] = new DescriptorPoolSize
{
Type = DescriptorType.UniformBufferDynamic,
DescriptorCount = DynamicUniformBindingCount * PairsPerPool,
};
var poolCreate = new DescriptorPoolCreateInfo
{
SType = StructureType.DescriptorPoolCreateInfo,
MaxSets = 2 * PairsPerPool,
PoolSizeCount = 3,
PPoolSizes = sizes,
};
VulkanInterop.Check(
_vk.CreateDescriptorPool(_device, &poolCreate, null, out DescriptorPool pool),
"vkCreateDescriptorPool (frame bindings)");
return pool;
}
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(DescriptorPool pool, 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(
DescriptorSet set,
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 = set,
DstBinding = binding,
DescriptorCount = 1,
DescriptorType = dynamic
? DescriptorType.StorageBufferDynamic
: DescriptorType.StorageBuffer,
PBufferInfo = &info,
};
_vk.UpdateDescriptorSets(_device, 1, &write, 0, null);
}
private void WriteUniform(
DescriptorSet set,
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 = set,
DstBinding = binding,
DescriptorCount = 1,
DescriptorType = DescriptorType.UniformBufferDynamic,
PBufferInfo = &info,
};
_vk.UpdateDescriptorSets(_device, 1, &write, 0, null);
}
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
foreach (DescriptorPool pool in _pools)
{
if (pool.Handle != 0)
_vk.DestroyDescriptorPool(_device, pool, null);
}
_pools.Clear();
_sets.Clear();
}
}