feat: Step 1 SDL3 window + Vulkan context + clear screen

This commit is contained in:
emil28092005
2026-06-16 17:21:43 +03:00
parent 2c4b47538d
commit 6397488c61
12 changed files with 1200 additions and 0 deletions
+183
View File
@@ -0,0 +1,183 @@
using System;
using Vortice.Vulkan;
namespace Engine.Graphics;
/// <summary>
/// Minimal renderer that clears the swapchain image to a solid color.
/// Serves as the foundational Step 1 rendering proof-of-concept.
/// </summary>
public sealed unsafe class ClearRenderer : IDisposable
{
private readonly VulkanContext _context;
private readonly Swapchain _swapchain;
private VkCommandPool _commandPool;
private VkCommandBuffer[] _commandBuffers;
private VkSemaphore[] _imageAvailableSemaphores;
private VkSemaphore[] _renderFinishedSemaphores;
private VkFence[] _inFlightFences;
private int _currentFrame;
public ClearRenderer(VulkanContext context, Swapchain swapchain)
{
_context = context;
_swapchain = swapchain;
CreateCommandPool();
CreateCommandBuffers();
CreateSyncObjects();
}
private void CreateCommandPool()
{
var createInfo = new VkCommandPoolCreateInfo
{
sType = VkStructureType.CommandPoolCreateInfo,
queueFamilyIndex = _context.GraphicsFamilyIndex,
flags = VkCommandPoolCreateFlags.ResetCommandBuffer
};
var result = _context.DeviceApi.vkCreateCommandPool(&createInfo, null, out _commandPool);
if (result != VkResult.Success)
throw new InvalidOperationException($"vkCreateCommandPool failed: {result}");
}
private void CreateCommandBuffers()
{
_commandBuffers = new VkCommandBuffer[2];
for (var i = 0; i < _commandBuffers.Length; i++)
{
var allocInfo = new VkCommandBufferAllocateInfo
{
sType = VkStructureType.CommandBufferAllocateInfo,
commandPool = _commandPool,
level = VkCommandBufferLevel.Primary,
commandBufferCount = 1
};
var result = _context.DeviceApi.vkAllocateCommandBuffer(&allocInfo, out _commandBuffers[i]);
if (result != VkResult.Success)
throw new InvalidOperationException($"vkAllocateCommandBuffers failed: {result}");
}
}
private void CreateSyncObjects()
{
_imageAvailableSemaphores = new VkSemaphore[2];
_renderFinishedSemaphores = new VkSemaphore[2];
_inFlightFences = new VkFence[2];
var semaphoreInfo = new VkSemaphoreCreateInfo { sType = VkStructureType.SemaphoreCreateInfo };
var fenceInfo = new VkFenceCreateInfo
{
sType = VkStructureType.FenceCreateInfo,
flags = VkFenceCreateFlags.Signaled
};
for (var i = 0; i < 2; i++)
{
_context.DeviceApi.vkCreateSemaphore(&semaphoreInfo, null, out _imageAvailableSemaphores[i]);
_context.DeviceApi.vkCreateSemaphore(&semaphoreInfo, null, out _renderFinishedSemaphores[i]);
_context.DeviceApi.vkCreateFence(&fenceInfo, null, out _inFlightFences[i]);
}
}
public void RenderFrame(float r, float g, float b)
{
var frame = _currentFrame % 2;
_context.DeviceApi.vkWaitForFences(_inFlightFences[frame], true, ulong.MaxValue);
_context.DeviceApi.vkResetFences(_inFlightFences[frame]);
var result = _context.DeviceApi.vkAcquireNextImageKHR(
_swapchain.Handle,
ulong.MaxValue,
_imageAvailableSemaphores[frame],
VkFence.Null,
out var imageIndex);
if (result == VkResult.ErrorOutOfDateKHR)
{
return;
}
var cmd = _commandBuffers[frame];
_context.DeviceApi.vkResetCommandBuffer(cmd, VkCommandBufferResetFlags.None);
var beginInfo = new VkCommandBufferBeginInfo
{
sType = VkStructureType.CommandBufferBeginInfo,
flags = VkCommandBufferUsageFlags.OneTimeSubmit
};
_context.DeviceApi.vkBeginCommandBuffer(cmd, &beginInfo);
var clearColor = new VkClearValue(r, g, b, 1.0f);
var renderPassInfo = new VkRenderPassBeginInfo
{
sType = VkStructureType.RenderPassBeginInfo,
renderPass = _swapchain.RenderPass,
framebuffer = _swapchain.Framebuffers[imageIndex],
renderArea = new VkRect2D(0, 0, _swapchain.Extent.width, _swapchain.Extent.height),
clearValueCount = 1,
pClearValues = &clearColor
};
_context.DeviceApi.vkCmdBeginRenderPass(cmd, &renderPassInfo, VkSubpassContents.Inline);
_context.DeviceApi.vkCmdEndRenderPass(cmd);
_context.DeviceApi.vkEndCommandBuffer(cmd);
var waitSemaphore = _imageAvailableSemaphores[frame];
var signalSemaphore = _renderFinishedSemaphores[frame];
var stageMask = VkPipelineStageFlags.ColorAttachmentOutput;
var submitInfo = new VkSubmitInfo
{
sType = VkStructureType.SubmitInfo,
waitSemaphoreCount = 1,
pWaitSemaphores = &waitSemaphore,
pWaitDstStageMask = &stageMask,
commandBufferCount = 1,
pCommandBuffers = &cmd,
signalSemaphoreCount = 1,
pSignalSemaphores = &signalSemaphore
};
_context.DeviceApi.vkQueueSubmit(_context.GraphicsQueue, 1, &submitInfo, _inFlightFences[frame]);
var swapchain = _swapchain.Handle;
var presentInfo = new VkPresentInfoKHR
{
sType = VkStructureType.PresentInfoKHR,
waitSemaphoreCount = 1,
pWaitSemaphores = &signalSemaphore,
swapchainCount = 1,
pSwapchains = &swapchain,
pImageIndices = &imageIndex
};
var presentResult = _context.DeviceApi.vkQueuePresentKHR(_context.PresentQueue, &presentInfo);
if (presentResult == VkResult.ErrorOutOfDateKHR || presentResult == VkResult.SuboptimalKHR)
{
// Recreate handled externally.
}
_currentFrame++;
}
public void Dispose()
{
_context.DeviceApi.vkDeviceWaitIdle();
for (var i = 0; i < 2; i++)
{
if (_renderFinishedSemaphores[i] != VkSemaphore.Null)
_context.DeviceApi.vkDestroySemaphore(_renderFinishedSemaphores[i]);
if (_imageAvailableSemaphores[i] != VkSemaphore.Null)
_context.DeviceApi.vkDestroySemaphore(_imageAvailableSemaphores[i]);
if (_inFlightFences[i] != VkFence.Null)
_context.DeviceApi.vkDestroyFence(_inFlightFences[i]);
}
if (_commandPool != VkCommandPool.Null)
_context.DeviceApi.vkDestroyCommandPool(_commandPool);
}
}
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<DefineConstants>DEV_MODE</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'ReleaseAOT'">
<DefineConstants>RELEASE_AOT</DefineConstants>
<PublishAot>true</PublishAot>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Vortice.Vulkan" Version="3.2.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Engine.Core\Engine.Core.csproj" />
</ItemGroup>
</Project>
+292
View File
@@ -0,0 +1,292 @@
using System;
using Vortice.Vulkan;
namespace Engine.Graphics;
/// <summary>
/// Manages the Vulkan swapchain, image views, render pass, and framebuffers.
/// Recreates itself automatically when the window is resized.
/// </summary>
public sealed unsafe class Swapchain : IDisposable
{
private readonly VulkanContext _context;
private VkRenderPass _renderPass;
private VkSwapchainKHR _swapchain;
private VkImage[] _images;
private VkImageView[] _imageViews;
private VkFramebuffer[] _framebuffers;
private VkSurfaceFormatKHR _surfaceFormat;
private VkPresentModeKHR _presentMode;
private VkExtent2D _extent;
public VkRenderPass RenderPass => _renderPass;
public VkFramebuffer[] Framebuffers => _framebuffers;
public VkExtent2D Extent => _extent;
public VkSwapchainKHR Handle => _swapchain;
public uint ImageCount => (uint)_images.Length;
public Swapchain(VulkanContext context)
{
_context = context;
_surfaceFormat = ChooseSurfaceFormat();
CreateRenderPass();
Recreate(1280, 720);
}
public void Recreate(int width, int height)
{
_context.DeviceApi.vkDeviceWaitIdle();
CleanupSwapchain();
var capabilities = GetSurfaceCapabilities();
_surfaceFormat = ChooseSurfaceFormat();
_presentMode = ChoosePresentMode();
_extent = ChooseExtent(capabilities, (uint)width, (uint)height);
var imageCount = capabilities.minImageCount + 1;
if (capabilities.maxImageCount > 0 && imageCount > capabilities.maxImageCount)
imageCount = capabilities.maxImageCount;
var createInfo = new VkSwapchainCreateInfoKHR
{
sType = VkStructureType.SwapchainCreateInfoKHR,
surface = _context.Surface,
minImageCount = imageCount,
imageFormat = _surfaceFormat.format,
imageColorSpace = _surfaceFormat.colorSpace,
imageExtent = _extent,
imageArrayLayers = 1,
imageUsage = VkImageUsageFlags.ColorAttachment,
imageSharingMode = VkSharingMode.Exclusive,
preTransform = capabilities.currentTransform,
compositeAlpha = VkCompositeAlphaFlagsKHR.Opaque,
presentMode = _presentMode,
clipped = true,
oldSwapchain = _swapchain
};
var result = _context.DeviceApi.vkCreateSwapchainKHR(&createInfo, null, out _swapchain);
if (result != VkResult.Success)
throw new InvalidOperationException($"vkCreateSwapchainKHR failed: {result}");
_images = GetSwapchainImages();
_imageViews = new VkImageView[_images.Length];
_framebuffers = new VkFramebuffer[_images.Length];
for (var i = 0; i < _images.Length; i++)
{
_imageViews[i] = CreateImageView(_images[i], _surfaceFormat.format);
_framebuffers[i] = CreateFramebuffer(_imageViews[i]);
}
}
private VkSurfaceCapabilitiesKHR GetSurfaceCapabilities()
{
var result = _context.InstanceApi.vkGetPhysicalDeviceSurfaceCapabilitiesKHR(_context.PhysicalDevice, _context.Surface, out var capabilities);
if (result != VkResult.Success)
throw new InvalidOperationException($"vkGetPhysicalDeviceSurfaceCapabilitiesKHR failed: {result}");
return capabilities;
}
private VkImage[] GetSwapchainImages()
{
uint count = 0;
_context.DeviceApi.vkGetSwapchainImagesKHR(_swapchain, &count, null);
var images = new VkImage[count];
fixed (VkImage* p = images)
{
var result = _context.DeviceApi.vkGetSwapchainImagesKHR(_swapchain, &count, p);
if (result != VkResult.Success)
throw new InvalidOperationException($"vkGetSwapchainImagesKHR failed: {result}");
}
return images;
}
private void CreateRenderPass()
{
var colorAttachment = new VkAttachmentDescription
{
format = _surfaceFormat.format != VkFormat.Undefined ? _surfaceFormat.format : VkFormat.B8G8R8A8Unorm,
samples = VkSampleCountFlags.Count1,
loadOp = VkAttachmentLoadOp.Clear,
storeOp = VkAttachmentStoreOp.Store,
stencilLoadOp = VkAttachmentLoadOp.DontCare,
stencilStoreOp = VkAttachmentStoreOp.DontCare,
initialLayout = VkImageLayout.Undefined,
finalLayout = VkImageLayout.PresentSrcKHR
};
var colorAttachmentRef = new VkAttachmentReference
{
attachment = 0,
layout = VkImageLayout.ColorAttachmentOptimal
};
var subpass = new VkSubpassDescription
{
pipelineBindPoint = VkPipelineBindPoint.Graphics,
colorAttachmentCount = 1,
pColorAttachments = &colorAttachmentRef
};
var dependency = new VkSubpassDependency
{
srcSubpass = Vulkan.VK_SUBPASS_EXTERNAL,
dstSubpass = 0,
srcStageMask = VkPipelineStageFlags.ColorAttachmentOutput,
dstStageMask = VkPipelineStageFlags.ColorAttachmentOutput,
srcAccessMask = VkAccessFlags.None,
dstAccessMask = VkAccessFlags.ColorAttachmentWrite
};
var createInfo = new VkRenderPassCreateInfo
{
sType = VkStructureType.RenderPassCreateInfo,
attachmentCount = 1,
pAttachments = &colorAttachment,
subpassCount = 1,
pSubpasses = &subpass,
dependencyCount = 1,
pDependencies = &dependency
};
var result = _context.DeviceApi.vkCreateRenderPass(&createInfo, null, out _renderPass);
if (result != VkResult.Success)
throw new InvalidOperationException($"vkCreateRenderPass failed: {result}");
}
private VkImageView CreateImageView(VkImage image, VkFormat format)
{
var createInfo = new VkImageViewCreateInfo
{
sType = VkStructureType.ImageViewCreateInfo,
image = image,
viewType = VkImageViewType.Image2D,
format = format,
components = new VkComponentMapping(VkComponentSwizzle.R, VkComponentSwizzle.G, VkComponentSwizzle.B, VkComponentSwizzle.A),
subresourceRange = new VkImageSubresourceRange(VkImageAspectFlags.Color, 0, 1, 0, 1)
};
var result = _context.DeviceApi.vkCreateImageView(&createInfo, null, out var imageView);
if (result != VkResult.Success)
throw new InvalidOperationException($"vkCreateImageView failed: {result}");
return imageView;
}
private VkFramebuffer CreateFramebuffer(VkImageView imageView)
{
var createInfo = new VkFramebufferCreateInfo
{
sType = VkStructureType.FramebufferCreateInfo,
renderPass = _renderPass,
attachmentCount = 1,
pAttachments = &imageView,
width = _extent.width,
height = _extent.height,
layers = 1
};
var result = _context.DeviceApi.vkCreateFramebuffer(&createInfo, null, out var framebuffer);
if (result != VkResult.Success)
throw new InvalidOperationException($"vkCreateFramebuffer failed: {result}");
return framebuffer;
}
private VkSurfaceFormatKHR ChooseSurfaceFormat()
{
var formats = GetSurfaceFormats();
foreach (var format in formats)
{
if (format.format == VkFormat.B8G8R8A8Unorm && format.colorSpace == VkColorSpaceKHR.SrgbNonLinear)
return format;
}
return formats[0];
}
private VkSurfaceFormatKHR[] GetSurfaceFormats()
{
uint count = 0;
_context.InstanceApi.vkGetPhysicalDeviceSurfaceFormatsKHR(_context.PhysicalDevice, _context.Surface, &count, null);
var formats = new VkSurfaceFormatKHR[count];
fixed (VkSurfaceFormatKHR* p = formats)
{
var result = _context.InstanceApi.vkGetPhysicalDeviceSurfaceFormatsKHR(_context.PhysicalDevice, _context.Surface, &count, p);
if (result != VkResult.Success)
throw new InvalidOperationException($"vkGetPhysicalDeviceSurfaceFormatsKHR failed: {result}");
}
return formats;
}
private VkPresentModeKHR ChoosePresentMode()
{
var modes = GetSurfacePresentModes();
if (Array.Exists(modes, m => m == VkPresentModeKHR.Mailbox))
return VkPresentModeKHR.Mailbox;
return VkPresentModeKHR.Fifo;
}
private VkPresentModeKHR[] GetSurfacePresentModes()
{
uint count = 0;
_context.InstanceApi.vkGetPhysicalDeviceSurfacePresentModesKHR(_context.PhysicalDevice, _context.Surface, &count, null);
var modes = new VkPresentModeKHR[count];
fixed (VkPresentModeKHR* p = modes)
{
var result = _context.InstanceApi.vkGetPhysicalDeviceSurfacePresentModesKHR(_context.PhysicalDevice, _context.Surface, &count, p);
if (result != VkResult.Success)
throw new InvalidOperationException($"vkGetPhysicalDeviceSurfacePresentModesKHR failed: {result}");
}
return modes;
}
private VkExtent2D ChooseExtent(VkSurfaceCapabilitiesKHR capabilities, uint width, uint height)
{
if (capabilities.currentExtent.width != uint.MaxValue)
return capabilities.currentExtent;
var extent = new VkExtent2D
{
width = Math.Clamp(width, capabilities.minImageExtent.width, capabilities.maxImageExtent.width),
height = Math.Clamp(height, capabilities.minImageExtent.height, capabilities.maxImageExtent.height)
};
return extent;
}
private void CleanupSwapchain()
{
if (_context.Device == VkDevice.Null)
return;
if (_framebuffers != null)
{
foreach (var fb in _framebuffers)
{
if (fb != VkFramebuffer.Null)
_context.DeviceApi.vkDestroyFramebuffer(fb);
}
}
if (_imageViews != null)
{
foreach (var view in _imageViews)
{
if (view != VkImageView.Null)
_context.DeviceApi.vkDestroyImageView(view);
}
}
if (_swapchain != VkSwapchainKHR.Null)
_context.DeviceApi.vkDestroySwapchainKHR(_swapchain);
}
public void Dispose()
{
_context.DeviceApi.vkDeviceWaitIdle();
CleanupSwapchain();
if (_renderPass != VkRenderPass.Null)
_context.DeviceApi.vkDestroyRenderPass(_renderPass);
}
}
+308
View File
@@ -0,0 +1,308 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using Engine.Core;
using SDL;
using Vortice.Vulkan;
namespace Engine.Graphics;
/// <summary>
/// Owns the Vulkan instance, physical device, logical device, queues, and API handles.
/// Created once per application lifetime.
/// </summary>
public sealed unsafe class VulkanContext : IDisposable
{
private bool _disposed;
public VkInstance Instance { get; private set; }
public VkInstanceApi InstanceApi { get; private set; }
public VkPhysicalDevice PhysicalDevice { get; private set; }
public VkDevice Device { get; private set; }
public VkDeviceApi DeviceApi { get; private set; }
public VkQueue GraphicsQueue { get; private set; }
public VkQueue PresentQueue { get; private set; }
public uint GraphicsFamilyIndex { get; private set; }
public uint PresentFamilyIndex { get; private set; }
public VkSurfaceKHR Surface { get; private set; }
public VulkanContext(Sdl3Window window, bool enableValidation = true)
{
CreateInstance(window, enableValidation);
InstanceApi = Vulkan.GetApi(Instance);
CreateSurface(window);
PickPhysicalDevice();
CreateLogicalDevice();
DeviceApi = Vulkan.GetApi(Instance, Device);
GetQueues();
}
private void CreateInstance(Sdl3Window window, bool enableValidation)
{
var requiredExtensions = new List<string>(window.GetRequiredInstanceExtensions());
if (enableValidation)
{
requiredExtensions.Add("VK_EXT_debug_utils");
}
var layerNames = enableValidation
? new[] { "VK_LAYER_KHRONOS_validation" }
: Array.Empty<string>();
var appName = VkStringInterop.ConvertToUnmanaged("Cortex Engine");
var engineName = VkStringInterop.ConvertToUnmanaged("CortexEngine");
var appInfo = new VkApplicationInfo
{
sType = VkStructureType.ApplicationInfo,
pApplicationName = appName,
pEngineName = engineName,
apiVersion = VkVersion.Version_1_3
};
using var extensionPin = new StringArrayPin(requiredExtensions);
using var layerPin = new StringArrayPin(layerNames);
{
var createInfo = new VkInstanceCreateInfo
{
sType = VkStructureType.InstanceCreateInfo,
pApplicationInfo = &appInfo,
enabledExtensionCount = (uint)requiredExtensions.Count,
ppEnabledExtensionNames = extensionPin.Pointers,
enabledLayerCount = (uint)layerNames.Length,
ppEnabledLayerNames = layerPin.Pointers
};
var result = Vulkan.vkCreateInstance(&createInfo, null, out var instance);
if (result != VkResult.Success)
throw new InvalidOperationException($"vkCreateInstance failed: {result}");
Instance = instance;
}
VkStringInterop.Free(appName);
VkStringInterop.Free(engineName);
}
private void CreateSurface(Sdl3Window window)
{
var sdlInstance = (SDL.VkInstance_T*)Instance.Handle;
var sdlSurface = (SDL.VkSurfaceKHR_T*)null;
var result = SDL3.SDL_Vulkan_CreateSurface(
(SDL_Window*)window.Handle,
sdlInstance,
null,
&sdlSurface);
if (result != true)
throw new InvalidOperationException($"SDL_Vulkan_CreateSurface failed: {SDL3.SDL_GetError()}");
Surface = new VkSurfaceKHR((ulong)sdlSurface);
}
private void PickPhysicalDevice()
{
var devices = EnumeratePhysicalDevices();
if (devices.Length == 0)
throw new InvalidOperationException("No Vulkan physical devices found.");
foreach (var device in devices)
{
var properties = InstanceApi.vkGetPhysicalDeviceProperties(device);
var queueFamilies = GetPhysicalDeviceQueueFamilyProperties(device);
var hasGraphics = false;
var hasPresent = false;
for (var i = 0; i < queueFamilies.Length; i++)
{
if (queueFamilies[i].queueFlags.HasFlag(VkQueueFlags.Graphics))
hasGraphics = true;
var supportResult = InstanceApi.vkGetPhysicalDeviceSurfaceSupportKHR(device, (uint)i, Surface, out VkBool32 supported);
if (supportResult == VkResult.Success && supported)
hasPresent = true;
}
if (hasGraphics && hasPresent)
{
PhysicalDevice = device;
if (properties.deviceType == VkPhysicalDeviceType.DiscreteGpu)
break;
}
}
if (PhysicalDevice == VkPhysicalDevice.Null)
throw new InvalidOperationException("No suitable Vulkan physical device found.");
}
private VkPhysicalDevice[] EnumeratePhysicalDevices()
{
uint count = 0;
InstanceApi.vkEnumeratePhysicalDevices(&count, null);
if (count == 0)
return Array.Empty<VkPhysicalDevice>();
var devices = new VkPhysicalDevice[count];
fixed (VkPhysicalDevice* p = devices)
{
var result = InstanceApi.vkEnumeratePhysicalDevices(&count, p);
if (result != VkResult.Success)
throw new InvalidOperationException($"vkEnumeratePhysicalDevices failed: {result}");
}
return devices;
}
private void CreateLogicalDevice()
{
var queueFamilies = GetPhysicalDeviceQueueFamilyProperties(PhysicalDevice);
GraphicsFamilyIndex = FindQueueFamilyIndex(queueFamilies, VkQueueFlags.Graphics);
PresentFamilyIndex = FindPresentQueueFamilyIndex(queueFamilies);
var uniqueFamilies = new HashSet<uint> { GraphicsFamilyIndex, PresentFamilyIndex };
var queueCreateInfos = uniqueFamilies.Select(family => new VkDeviceQueueCreateInfo
{
sType = VkStructureType.DeviceQueueCreateInfo,
queueFamilyIndex = family,
queueCount = 1
}).ToArray();
var priorityHandles = new GCHandle[queueCreateInfos.Length];
var extensionNames = new[] { "VK_KHR_swapchain" };
using var extensionPin = new StringArrayPin(extensionNames);
try
{
var deviceFeatures = new VkPhysicalDeviceFeatures();
for (var i = 0; i < queueCreateInfos.Length; i++)
{
var priority = new[] { 1.0f };
var handle = GCHandle.Alloc(priority, GCHandleType.Pinned);
priorityHandles[i] = handle;
queueCreateInfos[i].pQueuePriorities = (float*)handle.AddrOfPinnedObject();
}
fixed (VkDeviceQueueCreateInfo* pQueue = queueCreateInfos)
{
var createInfo = new VkDeviceCreateInfo
{
sType = VkStructureType.DeviceCreateInfo,
queueCreateInfoCount = (uint)queueCreateInfos.Length,
pQueueCreateInfos = pQueue,
pEnabledFeatures = &deviceFeatures,
enabledExtensionCount = 1,
ppEnabledExtensionNames = extensionPin.Pointers
};
var result = InstanceApi.vkCreateDevice(PhysicalDevice, &createInfo, null, out var device);
if (result != VkResult.Success)
throw new InvalidOperationException($"vkCreateDevice failed: {result}");
Device = device;
}
}
finally
{
foreach (var handle in priorityHandles)
{
if (handle.IsAllocated)
handle.Free();
}
}
}
private void GetQueues()
{
DeviceApi.vkGetDeviceQueue(GraphicsFamilyIndex, 0, out var graphicsQueue);
DeviceApi.vkGetDeviceQueue(PresentFamilyIndex, 0, out var presentQueue);
GraphicsQueue = graphicsQueue;
PresentQueue = presentQueue;
}
private uint FindQueueFamilyIndex(VkQueueFamilyProperties[] properties, VkQueueFlags flags)
{
for (var i = 0; i < properties.Length; i++)
{
if (properties[i].queueFlags.HasFlag(flags))
return (uint)i;
}
throw new InvalidOperationException($"No queue family with flags {flags} found.");
}
private uint FindPresentQueueFamilyIndex(VkQueueFamilyProperties[] properties)
{
for (var i = 0; i < properties.Length; i++)
{
var supportResult = InstanceApi.vkGetPhysicalDeviceSurfaceSupportKHR(PhysicalDevice, (uint)i, Surface, out VkBool32 supported);
if (supportResult == VkResult.Success && supported)
return (uint)i;
}
throw new InvalidOperationException("No present queue family found.");
}
private VkQueueFamilyProperties[] GetPhysicalDeviceQueueFamilyProperties(VkPhysicalDevice device)
{
uint count = 0;
InstanceApi.vkGetPhysicalDeviceQueueFamilyProperties(device, &count, null);
var properties = new VkQueueFamilyProperties[count];
fixed (VkQueueFamilyProperties* p = properties)
{
InstanceApi.vkGetPhysicalDeviceQueueFamilyProperties(device, &count, p);
}
return properties;
}
private sealed unsafe class StringArrayPin : IDisposable
{
public byte** Pointers;
private readonly GCHandle[] _handles;
public StringArrayPin(IReadOnlyList<string> strings)
{
if (strings.Count == 0)
{
Pointers = null;
_handles = Array.Empty<GCHandle>();
return;
}
Pointers = (byte**)Marshal.AllocHGlobal(strings.Count * sizeof(byte*));
_handles = new GCHandle[strings.Count];
for (var i = 0; i < strings.Count; i++)
{
var bytes = Encoding.UTF8.GetBytes(strings[i] + '\0');
_handles[i] = GCHandle.Alloc(bytes, GCHandleType.Pinned);
Pointers[i] = (byte*)_handles[i].AddrOfPinnedObject();
}
}
public void Dispose()
{
if (Pointers == null)
return;
foreach (var handle in _handles)
{
if (handle.IsAllocated)
handle.Free();
}
Marshal.FreeHGlobal((nint)Pointers);
Pointers = null;
}
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
if (Device != VkDevice.Null)
DeviceApi.vkDestroyDevice();
if (Instance != VkInstance.Null && Surface != VkSurfaceKHR.Null)
InstanceApi.vkDestroySurfaceKHR(Surface);
if (Instance != VkInstance.Null)
InstanceApi.vkDestroyInstance();
}
}