feat: pure Vulkan P/Invoke renderer — no wrappers, SDL3 window, SPIR-V shaders

- Engine.Graphics: restored interfaces (IRenderContext, IRenderer, RenderBackendFactory),
  loaders (ObjLoader, GltfLoader), ProceduralMesh, MeshMath, SceneSerializer
- Engine.Graphics.Vulkan: pure P/Invoke to libvulkan.so.1/vulkan-1.dll
  - VulkanNative: library loading, function pointer loading via vkGetInstanceProcAddr
  - Vk: static function cache for ~80 Vulkan functions, all delegate types
  - VulkanTypes: ~50 structs, ~20 enums matching Vulkan C headers
  - VulkanContext: instance, physical device, logical device, surface, memory types
  - VulkanSwapchain: swapchain, image views, depth image, render pass, framebuffers
  - VulkanPipeline: graphics pipeline, descriptor set layout, shader modules
  - VulkanBuffer: vertex/index/uniform buffers, staging, memory allocation
  - VulkanRenderer: command buffers, sync (semaphores/fences), render loop, 2 frames in flight
  - GLSL 450 shaders: PBR lighting (Fresnel, ACES, gamma), directional+point lights
  - Compiled to SPIR-V via @webgpu/glslang WASM
- CortexEngine.App: switched to vulkan backend, removed Raylib/OpenTK/ImGui refs
- Tests: all 66 pass (ObjLoader, MeshMath, ProceduralMesh, SceneSerializer, etc.)
- Camera tour: 16 poses, screenshots captured, clean shutdown
This commit is contained in:
emil28092005
2026-06-17 23:03:24 +03:00
parent dd105ea4af
commit 80238b08f5
29 changed files with 4424 additions and 290 deletions
+334
View File
@@ -0,0 +1,334 @@
using System.Runtime.InteropServices;
using System.Text;
using Engine.Core;
using SDL;
namespace Engine.Graphics.Vulkan;
internal sealed unsafe class VulkanContext : IDisposable
{
public VkInstance Instance;
public VkPhysicalDevice PhysicalDevice;
public VkDevice Device;
public VkSurfaceKHR Surface;
public VkQueue GraphicsQueue;
public VkQueue PresentQueue;
public uint GraphicsFamily;
public uint PresentFamily;
public VkPhysicalDeviceMemoryProperties MemoryProperties;
private readonly bool _validation;
private bool _disposed;
public VulkanContext(Sdl3Window window, bool enableValidation)
{
_validation = enableValidation;
Vk.LoadGlobalFunctions();
CreateInstance(window.GetRequiredVulkanExtensions());
CreateSurface(window);
PickPhysicalDevice();
CreateLogicalDevice();
}
private unsafe void CreateInstance(string[] requiredExtensions)
{
var layers = Array.Empty<string>();
if (_validation)
{
uint layerCount = 0;
VulkanNative.vkEnumerateInstanceLayerProperties(&layerCount, null);
if (layerCount > 0)
{
var availableLayers = new VkLayerProperties[layerCount];
fixed (VkLayerProperties* pLayers = availableLayers)
{
VulkanNative.vkEnumerateInstanceLayerProperties(&layerCount, pLayers);
}
for (var i = 0; i < layerCount; i++)
{
fixed (VkLayerProperties* pLayer = &availableLayers[i])
{
var nameLen = 0;
while (nameLen < 256 && pLayer->layerName[nameLen] != 0) nameLen++;
var layerName = Encoding.UTF8.GetString(pLayer->layerName, nameLen);
if (layerName == "VK_LAYER_KHRONOS_validation")
{
layers = new[] { "VK_LAYER_KHRONOS_validation" };
Console.WriteLine("[Vulkan] Validation layers enabled.");
break;
}
}
}
}
if (layers.Length == 0)
Console.WriteLine("[Vulkan] Validation layers requested but not available.");
}
var extensions = requiredExtensions;
var appNameBytes = Encoding.UTF8.GetBytes("Cortex Engine\0");
var engineNameBytes = Encoding.UTF8.GetBytes("CortexEngine\0");
VkApplicationInfo appInfo;
appInfo.sType = VkStructureType.ApplicationInfo;
appInfo.pNext = null;
fixed (byte* pAppName = appNameBytes, pEngineName = engineNameBytes)
{
appInfo.pApplicationName = pAppName;
appInfo.applicationVersion = 0;
appInfo.pEngineName = pEngineName;
appInfo.engineVersion = 0;
appInfo.apiVersion = (1 << 22) | (3 << 12);
var extPtrs = Vk.AllocStringArray(extensions);
var layerPtrs = Vk.AllocStringArray(layers);
VkInstanceCreateInfo createInfo;
createInfo.sType = VkStructureType.InstanceCreateInfo;
createInfo.pNext = null;
createInfo.flags = 0;
createInfo.pApplicationInfo = &appInfo;
createInfo.enabledLayerCount = (uint)layers.Length;
createInfo.ppEnabledLayerNames = layerPtrs;
createInfo.enabledExtensionCount = (uint)extensions.Length;
createInfo.ppEnabledExtensionNames = extPtrs;
VkResult result;
fixed (VkInstance* pInstance = &Instance)
{
result = Vk.vkCreateInstance(&createInfo, null, pInstance);
}
Vk.FreeStringArray(extPtrs, extensions.Length);
Vk.FreeStringArray(layerPtrs, layers.Length);
if (result != VkResult.Success)
throw new InvalidOperationException($"vkCreateInstance failed: {result}. " +
$"Extensions: [{string.Join(", ", extensions)}], Layers: [{string.Join(", ", layers)}]");
}
Vk.LoadInstanceFunctions(Instance);
Console.WriteLine("[Vulkan] Instance created.");
}
private unsafe void CreateSurface(Sdl3Window window)
{
var sdlWindow = (SDL_Window*)window.Handle;
var instancePtr = (SDL.VkInstance_T*)Instance.Value;
SDL.VkSurfaceKHR_T* surfacePtr;
if (!SDL3.SDL_Vulkan_CreateSurface(sdlWindow, instancePtr, null, &surfacePtr))
throw new InvalidOperationException($"SDL_Vulkan_CreateSurface failed: {SDL3.SDL_GetError()}");
Surface = new VkSurfaceKHR { Value = (ulong)surfacePtr };
Console.WriteLine("[Vulkan] Surface created.");
}
private unsafe void PickPhysicalDevice()
{
uint deviceCount = 0;
Vk.vkEnumeratePhysicalDevices(Instance, &deviceCount, null);
if (deviceCount == 0)
throw new InvalidOperationException("No GPU with Vulkan support found.");
var devices = new VkPhysicalDevice[deviceCount];
fixed (VkPhysicalDevice* pDevices = devices)
{
Vk.vkEnumeratePhysicalDevices(Instance, &deviceCount, pDevices);
}
VkPhysicalDevice bestDevice = default;
uint bestGraphicsFamily = uint.MaxValue;
uint bestPresentFamily = uint.MaxValue;
int bestScore = -1;
for (uint i = 0; i < deviceCount; i++)
{
VkPhysicalDeviceProperties props;
Vk.vkGetPhysicalDeviceProperties(devices[i], &props);
byte* pName = props.deviceName;
var nameLen = 0;
while (nameLen < 256 && pName[nameLen] != 0) nameLen++;
var deviceName = Encoding.UTF8.GetString(pName, nameLen);
var score = (int)props.deviceType;
if (props.deviceType == VkPhysicalDeviceType.DiscreteGpu) score = 1000;
else if (props.deviceType == VkPhysicalDeviceType.IntegratedGpu) score = 500;
if (!FindQueueFamilies(devices[i], out var graphicsFamily, out var presentFamily))
continue;
if (score > bestScore)
{
bestScore = score;
bestDevice = devices[i];
bestGraphicsFamily = graphicsFamily;
bestPresentFamily = presentFamily;
Console.WriteLine($"[Vulkan] Selected GPU: {deviceName} (score {score})");
}
}
if (bestScore < 0)
throw new InvalidOperationException("No suitable GPU found with graphics + present queues.");
PhysicalDevice = bestDevice;
GraphicsFamily = bestGraphicsFamily;
PresentFamily = bestPresentFamily;
VkPhysicalDeviceMemoryProperties memProps;
Vk.vkGetPhysicalDeviceMemoryProperties(PhysicalDevice, &memProps);
MemoryProperties = memProps;
}
private unsafe bool FindQueueFamilies(VkPhysicalDevice device, out uint graphicsFamily, out uint presentFamily)
{
graphicsFamily = uint.MaxValue;
presentFamily = uint.MaxValue;
uint count = 0;
Vk.vkGetPhysicalDeviceQueueFamilyProperties(device, &count, null);
if (count == 0) return false;
var props = new VkQueueFamilyProperties[count];
fixed (VkQueueFamilyProperties* pProps = props)
{
Vk.vkGetPhysicalDeviceQueueFamilyProperties(device, &count, pProps);
}
for (uint i = 0; i < count; i++)
{
if ((props[i].queueFlags & VkQueueFlags.Graphics) != 0)
graphicsFamily = i;
uint supported = 0;
Vk.vkGetPhysicalDeviceSurfaceSupportKHR(device, i, Surface, &supported);
if (supported != 0)
presentFamily = i;
if (graphicsFamily != uint.MaxValue && presentFamily != uint.MaxValue)
return true;
}
return false;
}
private unsafe void CreateLogicalDevice()
{
var queueIndices = new HashSet<uint> { GraphicsFamily, PresentFamily };
var queueCreateInfos = new VkDeviceQueueCreateInfo[queueIndices.Count];
var priorities = new float[] { 1.0f };
fixed (float* pPrio = priorities)
{
var idx = 0;
foreach (var qfi in queueIndices)
{
queueCreateInfos[idx] = new VkDeviceQueueCreateInfo
{
sType = VkStructureType.DeviceQueueCreateInfo,
pNext = null,
flags = 0,
queueFamilyIndex = qfi,
queueCount = 1,
pQueuePriorities = pPrio
};
idx++;
}
var extNameBytes = System.Text.Encoding.UTF8.GetBytes("VK_KHR_swapchain\0");
var extNamePtr = (byte*)Marshal.AllocHGlobal(extNameBytes.Length);
Marshal.Copy(extNameBytes, 0, (nint)extNamePtr, extNameBytes.Length);
fixed (VkDeviceQueueCreateInfo* pQueueCreateInfos = queueCreateInfos)
{
byte* features = stackalloc byte[228];
VkDeviceCreateInfo createInfo;
createInfo.sType = VkStructureType.DeviceCreateInfo;
createInfo.pNext = null;
createInfo.flags = 0;
createInfo.queueCreateInfoCount = (uint)queueCreateInfos.Length;
createInfo.pQueueCreateInfos = pQueueCreateInfos;
createInfo.enabledLayerCount = 0;
createInfo.ppEnabledLayerNames = null;
createInfo.enabledExtensionCount = 1;
createInfo.ppEnabledExtensionNames = &extNamePtr;
createInfo.pEnabledFeatures = features;
VkDevice device;
var result = Vk.vkCreateDevice(PhysicalDevice, &createInfo, null, &device);
Vk.CheckResult(result, "vkCreateDevice");
Device = device;
}
Marshal.FreeHGlobal((nint)extNamePtr);
}
Vk.LoadDeviceFunctions(Device);
fixed (VkQueue* pGfxQueue = &GraphicsQueue)
{
Vk.vkGetDeviceQueue(Device, GraphicsFamily, 0, pGfxQueue);
}
fixed (VkQueue* pPresentQueue = &PresentQueue)
{
Vk.vkGetDeviceQueue(Device, PresentFamily, 0, pPresentQueue);
}
Console.WriteLine("[Vulkan] Logical device created.");
}
public unsafe uint FindMemoryType(uint typeFilter, VkMemoryPropertyFlags properties)
{
for (uint i = 0; i < MemoryProperties.memoryTypeCount; i++)
{
var memType = GetMemoryType(i);
if ((typeFilter & (1u << (int)i)) != 0 && (memType.propertyFlags & properties) == properties)
return i;
}
throw new InvalidOperationException($"Failed to find memory type with filter={typeFilter:X} props={properties}");
}
private VkMemoryType GetMemoryType(uint index)
{
return index switch
{
0 => MemoryProperties.memoryTypes0,
1 => MemoryProperties.memoryTypes1,
2 => MemoryProperties.memoryTypes2,
3 => MemoryProperties.memoryTypes3,
4 => MemoryProperties.memoryTypes4,
5 => MemoryProperties.memoryTypes5,
6 => MemoryProperties.memoryTypes6,
7 => MemoryProperties.memoryTypes7,
8 => MemoryProperties.memoryTypes8,
9 => MemoryProperties.memoryTypes9,
10 => MemoryProperties.memoryTypes10,
11 => MemoryProperties.memoryTypes11,
12 => MemoryProperties.memoryTypes12,
13 => MemoryProperties.memoryTypes13,
14 => MemoryProperties.memoryTypes14,
15 => MemoryProperties.memoryTypes15,
_ => throw new IndexOutOfRangeException()
};
}
public unsafe void Dispose()
{
if (_disposed) return;
_disposed = true;
if (Device.Value != 0)
{
Vk.vkQueueWaitIdle(GraphicsQueue);
Vk.vkDestroyDevice(Device, null);
}
if (Surface.Value != 0)
Vk.vkDestroySurfaceKHR(Instance, Surface, null);
if (Instance.Value != 0)
Vk.vkDestroyInstance(Instance, null);
}
}