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; /// /// Owns the Vulkan instance, physical device, logical device, queues, and API handles. /// Created once per application lifetime. /// 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(window.GetRequiredInstanceExtensions()); if (enableValidation) { requiredExtensions.Add("VK_EXT_debug_utils"); } var layerNames = enableValidation ? new[] { "VK_LAYER_KHRONOS_validation" } : Array.Empty(); 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(); 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 { 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 strings) { if (strings.Count == 0) { Pointers = null; _handles = Array.Empty(); 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(); } }