feat: push constants + UBO descriptor sets — rotating triangle with VP matrix

- Push constants: rotation angle (float) sent to vertex shader via vkCmdPushConstants
- UBO + descriptor sets: VP matrix (mat4) in uniform buffer, bound via VkDescriptorSet
- New Vulkan functions: vkCreateDescriptorSetLayout, vkCreateDescriptorPool,
  vkAllocateDescriptorSets, vkUpdateDescriptorSets, vkCmdBindDescriptorSets,
  vkCmdPushConstants
- New structs: VkPushConstantRange, VkDescriptorSetLayoutBinding,
  VkDescriptorSetLayoutCreateInfo, VkDescriptorPoolCreateInfo,
  VkDescriptorPoolSize, VkDescriptorSetAllocateInfo, VkWriteDescriptorSet,
  VkDescriptorBufferInfo
- Per-frame UBO buffers (HOST_VISIBLE|HOST_COHERENT, 64 bytes each)
- Perspective projection + look-at camera (static, no ECS yet)
- Cleaned up debug logging in VulkanContext
- Zero validation errors
This commit is contained in:
emil28092005
2026-06-18 01:59:41 +03:00
parent 2e0970e769
commit 4f3810010a
9 changed files with 328 additions and 16 deletions
@@ -1,8 +1,11 @@
using System.Runtime.InteropServices;
namespace Engine.Graphics.Vulkan;
internal sealed unsafe class VulkanFrameResources : IDisposable
{
public const int MaxFramesInFlight = 2;
public const ulong UboSize = 64;
public VkCommandPool CommandPool;
public VkCommandBuffer[] CommandBuffers = new VkCommandBuffer[MaxFramesInFlight];
@@ -10,10 +13,17 @@ internal sealed unsafe class VulkanFrameResources : IDisposable
public VkSemaphore[] AcquireSemaphores = new VkSemaphore[MaxFramesInFlight];
public VkSemaphore[] SubmitSemaphores = Array.Empty<VkSemaphore>();
public VkBuffer[] UboBuffers = new VkBuffer[MaxFramesInFlight];
public VkDeviceMemory[] UboMemories = new VkDeviceMemory[MaxFramesInFlight];
public VkDescriptorSet[] DescriptorSets = new VkDescriptorSet[MaxFramesInFlight];
public VkDescriptorPool DescriptorPool;
private readonly VkDevice _device;
private bool _disposed;
public VulkanFrameResources(VkDevice device, uint queueFamilyIndex, uint swapchainImageCount)
public VulkanFrameResources(VkDevice device, uint queueFamilyIndex, uint swapchainImageCount,
VulkanContext ctx, VkDescriptorSetLayout descriptorSetLayout)
{
_device = device;
@@ -24,12 +34,11 @@ internal sealed unsafe class VulkanFrameResources : IDisposable
queueFamilyIndex = queueFamilyIndex,
};
fixed (VkCommandPool* poolPtr = &CommandPool)
{
var result = Vk.vkCreateCommandPool(_device, &poolInfo, 0, poolPtr);
if (result != VkResult.Success)
throw new InvalidOperationException($"vkCreateCommandPool failed: {result}");
}
var cmdPool = VkCommandPool.Null;
var result = Vk.vkCreateCommandPool(_device, &poolInfo, 0, &cmdPool);
if (result != VkResult.Success)
throw new InvalidOperationException($"vkCreateCommandPool failed: {result}");
CommandPool = cmdPool;
var allocInfo = new VkCommandBufferAllocateInfo
{
@@ -41,7 +50,7 @@ internal sealed unsafe class VulkanFrameResources : IDisposable
fixed (VkCommandBuffer* cmdPtr = CommandBuffers)
{
var result = Vk.vkAllocateCommandBuffers(_device, &allocInfo, cmdPtr);
result = Vk.vkAllocateCommandBuffers(_device, &allocInfo, cmdPtr);
if (result != VkResult.Success)
throw new InvalidOperationException($"vkAllocateCommandBuffers failed: {result}");
}
@@ -60,7 +69,7 @@ internal sealed unsafe class VulkanFrameResources : IDisposable
for (int i = 0; i < MaxFramesInFlight; i++)
{
var fence = VkFence.Null;
var result = Vk.vkCreateFence(_device, &fenceInfo, 0, &fence);
result = Vk.vkCreateFence(_device, &fenceInfo, 0, &fence);
if (result != VkResult.Success)
throw new InvalidOperationException($"vkCreateFence failed: {result}");
FrameFences[i] = fence;
@@ -76,15 +85,133 @@ internal sealed unsafe class VulkanFrameResources : IDisposable
for (int i = 0; i < swapchainImageCount; i++)
{
var sem = VkSemaphore.Null;
var result = Vk.vkCreateSemaphore(_device, &semInfo, 0, &sem);
result = Vk.vkCreateSemaphore(_device, &semInfo, 0, &sem);
if (result != VkResult.Success)
throw new InvalidOperationException($"vkCreateSemaphore (submit) failed: {result}");
SubmitSemaphores[i] = sem;
}
CreateUniformBuffers(ctx);
CreateDescriptorPoolAndSets(descriptorSetLayout);
Console.WriteLine($"[Vulkan] Frame resources: {MaxFramesInFlight} frames in flight, {swapchainImageCount} submit semaphores");
}
private void CreateUniformBuffers(VulkanContext ctx)
{
for (int i = 0; i < MaxFramesInFlight; i++)
{
var info = new VkBufferCreateInfo
{
sType = VkStructureType.BufferCreateInfo,
size = UboSize,
usage = VkBufferUsageFlags.UniformBuffer,
sharingMode = VkSharingMode.Exclusive,
};
var buf = VkBuffer.Null;
var result = Vk.vkCreateBuffer(_device, &info, 0, &buf);
if (result != VkResult.Success)
throw new InvalidOperationException($"vkCreateBuffer (UBO) failed: {result}");
UboBuffers[i] = buf;
var reqs = new VkMemoryRequirements();
Vk.vkGetBufferMemoryRequirements(_device, buf, &reqs);
var memTypeIndex = ctx.FindMemoryType(reqs.memoryTypeBits,
VkMemoryPropertyFlags.HostVisible | VkMemoryPropertyFlags.HostCoherent);
var allocInfo = new VkMemoryAllocateInfo
{
sType = VkStructureType.MemoryAllocateInfo,
allocationSize = reqs.size,
memoryTypeIndex = memTypeIndex,
};
var mem = VkDeviceMemory.Null;
result = Vk.vkAllocateMemory(_device, &allocInfo, 0, &mem);
if (result != VkResult.Success)
throw new InvalidOperationException($"vkAllocateMemory (UBO) failed: {result}");
UboMemories[i] = mem;
Vk.vkBindBufferMemory(_device, buf, mem, 0);
}
}
private void CreateDescriptorPoolAndSets(VkDescriptorSetLayout layout)
{
var poolSize = new VkDescriptorPoolSize
{
type = VkDescriptorType.UniformBuffer,
descriptorCount = MaxFramesInFlight,
};
var poolInfo = new VkDescriptorPoolCreateInfo
{
sType = VkStructureType.DescriptorPoolCreateInfo,
flags = VkDescriptorPoolCreateFlags.FreeDescriptorSet,
maxSets = MaxFramesInFlight,
poolSizeCount = 1,
pPoolSizes = &poolSize,
};
var descPool = VkDescriptorPool.Null;
var result = Vk.vkCreateDescriptorPool(_device, &poolInfo, 0, &descPool);
if (result != VkResult.Success)
throw new InvalidOperationException($"vkCreateDescriptorPool failed: {result}");
DescriptorPool = descPool;
var layouts = stackalloc VkDescriptorSetLayout[MaxFramesInFlight];
for (int i = 0; i < MaxFramesInFlight; i++)
layouts[i] = layout;
var allocInfo = new VkDescriptorSetAllocateInfo
{
sType = VkStructureType.DescriptorSetAllocateInfo,
descriptorPool = DescriptorPool,
descriptorSetCount = MaxFramesInFlight,
pSetLayouts = layouts,
};
fixed (VkDescriptorSet* setPtr = DescriptorSets)
{
result = Vk.vkAllocateDescriptorSets(_device, &allocInfo, setPtr);
if (result != VkResult.Success)
throw new InvalidOperationException($"vkAllocateDescriptorSets failed: {result}");
}
for (int i = 0; i < MaxFramesInFlight; i++)
{
var bufferInfo = new VkDescriptorBufferInfo
{
buffer = UboBuffers[i],
offset = 0,
range = UboSize,
};
var write = new VkWriteDescriptorSet
{
sType = VkStructureType.WriteDescriptorSet,
dstSet = DescriptorSets[i],
dstBinding = 0,
dstArrayElement = 0,
descriptorCount = 1,
descriptorType = VkDescriptorType.UniformBuffer,
pBufferInfo = &bufferInfo,
};
Vk.vkUpdateDescriptorSets(_device, 1, &write, 0, 0);
}
}
public void UpdateUbo(int frameIndex, void* data, ulong size)
{
void* pData = null;
Vk.vkMapMemory(_device, UboMemories[frameIndex], 0, size, 0, &pData);
System.Buffer.MemoryCopy(data, pData, (long)size, (long)size);
Vk.vkUnmapMemory(_device, UboMemories[frameIndex]);
}
public void WaitFrame(int frameIndex)
{
fixed (VkFence* fencePtr = &FrameFences[frameIndex])
@@ -105,6 +232,8 @@ internal sealed unsafe class VulkanFrameResources : IDisposable
{
if (FrameFences[i].Handle != 0) Vk.vkDestroyFence(_device, FrameFences[i], 0);
if (AcquireSemaphores[i].Handle != 0) Vk.vkDestroySemaphore(_device, AcquireSemaphores[i], 0);
if (UboBuffers[i].Handle != 0) Vk.vkDestroyBuffer(_device, UboBuffers[i], 0);
if (UboMemories[i].Handle != 0) Vk.vkFreeMemory(_device, UboMemories[i], 0);
}
for (int i = 0; i < SubmitSemaphores.Length; i++)
@@ -112,6 +241,7 @@ internal sealed unsafe class VulkanFrameResources : IDisposable
if (SubmitSemaphores[i].Handle != 0) Vk.vkDestroySemaphore(_device, SubmitSemaphores[i], 0);
}
if (DescriptorPool.Handle != 0) Vk.vkDestroyDescriptorPool(_device, DescriptorPool, 0);
if (CommandPool.Handle != 0) Vk.vkDestroyCommandPool(_device, CommandPool, 0);
}
}