feat: rebuild Vulkan renderer from scratch — pure P/Invoke triangle (Vulkan 1.3)

- Complete rewrite of Engine.Graphics.Vulkan with pure P/Invoke (no wrapper libs)
- Vulkan 1.3: dynamic rendering (vkCmdBeginRendering/vkCmdEndRendering),
  synchronization2 (vkQueueSubmit2, vkCmdPipelineBarrier2)
- Split types into VulkanHandles.cs, VulkanEnums.cs, VulkanStructs.cs
- Staging buffer → device-local vertex buffer pattern
- Correct swapchain semaphore indexing (per-image, not per-frame-in-flight)
- VK_EXT_debug_utils debug messenger with validation layer fallback
- Dynamic viewport/scissor (no pipeline recreation on resize)
- Simplified Program.cs to triangle-only rendering
- Removed old Silk.NET renderer, ImGui, PBR shaders, screenshot code
- Updated VULKAN_IMPLEMENTATION_PLAN.md with full architecture decisions
This commit is contained in:
emil28092005
2026-06-18 01:49:25 +03:00
parent ee98e4ad08
commit 2e0970e769
44 changed files with 3882 additions and 5273 deletions
+653 -181
View File
@@ -3,9 +3,11 @@
## Project State (June 2026)
### What Exists
- **Engine.Core** — Sdl3Window (SDL3, Vulkan surface ready), IWindow, IInputState, Key enum, InputMapping,
- **Engine.Core** — Sdl3Window (SDL3, Vulkan surface ready), IWindow, IInputState, Key enum, InputMapping,
camera controllers (FreeFly, Orbit), components (Transform, Mesh, Material, Light, Camera, RigidBody),
Vertex struct (Position, Color, Normal — 9 floats), Timing, IScreenshotProvider
- **Engine.Graphics** — Restored minimal interfaces: IRenderContext, IRenderer, RenderBackendFactory,
IScreenshotProvider, SceneSerializer, MeshMath, ProceduralMesh, Loaders/ObjLoader
- **Engine.Physics** — JoltPhysicsSharp 2.21.0, PhysicsWorld wrapper, RigidBody component
- **Engine.AI** — AiCommandProcessor (7 commands), MCP HTTP + stdio servers, AiCommandQueue
- **CortexEngine.App** — main loop (broken, references deleted graphics projects)
@@ -13,245 +15,715 @@
- **Content/** — cube.obj, torusknot.obj, checker.png
### What Was Deleted
- Engine.Graphics (interfaces + loaders + factory)
- Engine.Graphics.Raylib
- Engine.Graphics.OpenTK
- Engine.Graphics.Vulkan (Silk.NET version)
- Engine.Graphics.OpenTK
- Engine.Graphics.Vulkan (Silk.NET version — all previous PBR/ImGui/mesh/screenshot code gone)
### Environment
- .NET 9 SDK at $HOME/.dotnet
- .NET 9 SDK at `$HOME/.dotnet`
- Vulkan 1.4.329, NVIDIA RTX 2080 Ti, validation layers available
- SDL3 (ppy.SDL3-CS 2026.520.0) — window + Vulkan surface
- glslangValidator NOT installed (need: sudo apt install glslang-tools)
- Linux (X11), cross-platform target (Windows: vulkan-1.dll, Linux: libvulkan.so.1)
- glslangValidator: check availability (`glslangValidator --version`); fallback: `glslc`
- Linux (X11), cross-platform target (Windows: `vulkan-1.dll`, Linux: `libvulkan.so.1`)
### Key Architecture Decisions
- NO wrapper libraries (no Silk.NET, no Vortice, no OpenTK for Vulkan)
- Pure P/Invoke to libvulkan.so.1 / vulkan-1.dll
- SDL3 for windowing (Sdl3Window already works, creates Vulkan surface)
- ImGui planned (later phase)
- Shadow mapping planned (Vulkan gives full control)
- Validation layers for debugging
---
## Implementation Plan
## Key Architecture Decisions
### Phase 1: Restore Engine.Graphics (interfaces + loaders)
| Decision | Choice | Rationale |
|---|---|---|
| Vulkan version | **1.3** | Dynamic rendering (no VkRenderPass/VkFramebuffer), synchronization2, extended dynamic state. All modern GPUs (2022+) support it. |
| Wrapper libraries | **None** | Pure P/Invoke to `libvulkan.so.1` / `vulkan-1.dll`. No Silk.NET, Vortice, OpenTK. |
| Windowing | **SDL3** (ppy.SDL3-CS) | Already integrated, Vulkan surface support built in. |
| Type organisation | **Multiple files** | `VulkanHandles.cs`, `VulkanEnums.cs`, `VulkanStructs.cs` — easier to maintain. |
| Debug | **Full debug messenger** | `VK_EXT_debug_utils` with callback printing validation messages to console (Debug only). |
| Memory | **Staging buffer from start** | Staging buffer (HOST_VISIBLE) → command buffer copy → device-local vertex buffer. Correct pattern from day one. |
| Frame loop | **Re-record every frame** | Vulkan Guide recommends fresh command buffers per frame over reuse. Simpler, no cache invalidation logic. |
| Semaphore indexing | **Per-swapchain-image for submit** | Critical: submit semaphores indexed by swapchain image index, NOT frame-in-flight index. (Vulkan Guide §swapchain_semaphore_reuse) |
| Render pass | **Dynamic rendering** | `vkCmdBeginRendering` / `vkCmdEndRendering` (Vulkan 1.3). No VkRenderPass or VkFramebuffer objects. |
| Synchronisation API | **synchronization2** | `VkImageMemoryBarrier2`, `vkCmdPipelineBarrier2` — cleaner, 64-bit flags. (Vulkan 1.3) |
Create `src/Engine.Graphics/` with:
- `IRenderContext.cs` — interface: Window, CreateRenderer(), Resize(), Dispose
- `IRenderer.cs` — interface: RenderWorld(World), RequestScreenshot, IsScreenshotRequested, ScreenshotProvider, Dispose
- `RenderBackendFactory.cs` — static registry: Register(name, factory), Create(name, w, h, validation)
- `IScreenshotProvider.cs` already in Engine.Core
- `Loaders/ObjLoader.cs` — parse .obj files → Mesh component
- `Loaders/GltfLoader.cs` — parse .gltf/.glb → Mesh component
- `MeshMath.cs` — ComputeFaceNormal(a, b, c)
- `ProceduralMesh.cs` — CreateSphere(), CreateGrid()
- `SceneSerializer.cs` — save/load ECS world to JSON
---
csproj: references Engine.Core, Flecs.NET, SharpGLTF.Core
## Implementation Phases
### Phase 2: Vulkan P/Invoke Layer
### Phase 1: Vulkan P/Invoke Foundation
Create `src/Engine.Graphics.Vulkan/` with:
- `VulkanNative.cs` — all P/Invoke declarations:
- Library loading: `const string VulkanLib = OperatingSystem.IsWindows() ? "vulkan-1.dll" : "libvulkan.so.1"`
- ~80 Vulkan functions (vkCreateInstance through vkQueuePresentKHR)
- ~50 structs (InstanceCreateInfo, DeviceCreateInfo, SwapchainCreateInfoKHR, etc.)
- ~20 enums (Result, Format, ImageLayout, PipelineStageFlags, etc.)
- Extension function loading via vkGetInstanceProcAddr/vkGetDeviceProcAddr
- SDL_Vulkan_CreateSurface via SDL3 (already in Sdl3Window)
Create `src/Engine.Graphics.Vulkan/` with the following files:
- `VulkanContext.cs` — Instance + PhysicalDevice + Device + Queues + Surface:
- CreateInstance with SDL3 extensions + validation layers
- PickPhysicalDevice (prefer discrete GPU)
- CreateLogicalDevice with VK_KHR_swapchain
- CreateSurface via SDL_Vulkan_CreateSurface
- Get graphics + present queues
#### 1.1 `VulkanNative.cs`
- Load `libvulkan.so.1` (Linux) / `vulkan-1.dll` (Windows) via `NativeLibrary.Load()`
- Export `vkGetInstanceProcAddr` delegate — the only directly-loaded function
- Helper: `GetExport<T>(string name)` for static exports
- Helper: `ToUtf8Terminated(string)` for passing string names to Vulkan
- `VulkanSwapchain.cs` — Swapchain + image views + depth + render pass + framebuffers:
- Query surface capabilities
- Create swapchain (format, extent, present mode)
- Create image views
- Create depth image + view (D32_SFLOAT)
- Create render pass (color + depth attachments)
- Create framebuffers
#### 1.2 `VulkanHandles.cs`
Opaque pointer handles (all are `nint` / `ulong`):
```
VkInstance, VkPhysicalDevice, VkDevice, VkQueue,
VkCommandPool, VkCommandBuffer,
VkSwapchainKHR, VkSurfaceKHR,
VkImage, VkImageView,
VkBuffer, VkDeviceMemory,
VkShaderModule, VkPipelineLayout, VkPipeline,
VkSemaphore, VkFence,
VkDebugUtilsMessengerEXT,
VkDescriptorSetLayout, VkDescriptorPool, VkDescriptorSet
```
Each defined as `struct VkXxx { public nint Handle; }` or `using VkXxx = System.IntPtr;`
- `VulkanPipeline.cs` — Graphics pipeline:
- Load SPIR-V shader modules (vertex + fragment)
- Vertex input description (Position vec3, Normal vec3, Color vec4)
- Descriptor set layouts (frame UBO + texture sampler)
- Pipeline layout + graphics pipeline
- Push constants for MVP matrix + material params
#### 1.3 `VulkanEnums.cs`
All enums needed for triangle + future expansion:
- `VkResult` — Success=0, NotReady, Timeout, Incomplete, ErrorOutOfDateKHR, SuboptimalKHR, ErrorSurfaceLostKHR, ...
- `VkStructureType` — ApplicationInfo=0, InstanceCreateInfo=1, DeviceQueueCreateInfo=2, DeviceCreateInfo=3, ...
- `VkFormat` — Undefined=0, R8G8B8A8Unorm=37, B8G8R8A8Unorm=44, R8G8B8A8Srgb=43, B8G8R8A8Srgb=50, R32G32Sfloat=103, R32G32B32Sfloat=106, R32G32B32A32Sfloat=109, D32Sfloat=126, ...
- `VkColorSpaceKHR` — SrgbNonlinear=0
- `VkPresentModeKHR` — Immediate=0, Mailbox=1, Fifo=2, FifoRelaxed=3
- `VkImageUsageFlags` — TransferSrc, TransferDst, ColorAttachment, ...
- `VkImageLayout` — Undefined=0, General=1, ColorAttachmentOptimal=2, TransferSrcOptimal=6, TransferDstOptimal=7, PresentSrcKHR=1000001002, ...
- `VkImageAspectFlags` — Color=1, Depth=2
- `VkAttachmentLoadOp` — Load=0, Clear=1, DontCare=2
- `VkAttachmentStoreOp` — Store=0, DontCare=1
- `VkSharingMode` — Exclusive=0, Concurrent=1
- `VkCompositeAlphaFlagsKHR` — Opaque=1, ...
- `VkSurfaceTransformFlagsKHR` — Identity=1, ...
- `VkPrimitiveTopology` — PointList=0, LineList=1, TriangleList=3, ...
- `VkPolygonMode` — Fill=0, Line=1, Point=2
- `VkCullModeFlags` — None=0, Front=1, Back=2, FrontAndBack=3
- `VkFrontFace` — CounterClockwise=0, Clockwise=1
- `VkBlendFactor` — Zero=0, One=1, SrcAlpha=6, OneMinusSrcAlpha=7, ...
- `VkBlendOp` — Add=0, ...
- `VkColorComponentFlags` — R=1, G=2, B=4, A=8
- `VkShaderStageFlags` — Vertex=1, Fragment=0x10, AllGraphics=0x1F
- `VkPipelineStageFlags2` — None=0, TopOfPipe=1, ColorAttachmentOutput=0x400, AllGraphics=0x8000, Transfer=0x10000, ...
- `VkAccessFlags2` — None=0, ColorAttachmentWrite=0x400, TransferWrite=0x1000, ...
- `VkDynamicState` — Viewport=0, Scissor=1, ...
- `VkCommandBufferLevel` — Primary=0, Secondary=1
- `VkCommandBufferUsageFlags` — OneTimeSubmit=1, ...
- `VkFenceCreateFlags` — Signaled=1
- `VkMemoryPropertyFlags` — DeviceLocal=1, HostVisible=2, HostCoherent=4, HostCached=8
- `VkBufferUsageFlags` — TransferSrc=1, TransferDst=2, VertexBuffer=0x80, IndexBuffer=0x40, UniformBuffer=0x10
- `VkQueueFlags` — Graphics=1, Compute=2, Transfer=4
- `VkPhysicalDeviceType` — Other=0, IntegratedGpu=1, DiscreteGpu=2, ...
- `VkSampleCountFlags` — Count1=1
- `VkImageViewType` — Type2D=1
- `VkComponentSwizzle` — Identity=0, ...
- `VkBool32` — False=0, True=1
- `VkRenderingFlags` — None=0, ContentsSecondaryCommandBuffers=1
- `VkPipelineBindPoint` — Graphics=0, Compute=1
- `VkDescriptorType` — UniformBuffer=6, StorageBuffer=7, CombinedImageSampler=0, ...
- `VkDescriptorPoolCreateFlags` — FreeDescriptorSet=1, ...
- `VulkanBuffer.cs` — Buffer management:
- CreateBuffer (vertex/index/uniform)
- AllocateMemory + bind
- Map/unmap for writing
- FindMemoryType
- Staging buffer for copy
#### 1.4 `VulkanStructs.cs`
All structs with `LayoutKind.Sequential`:
- `VkApplicationInfo` — sType, pNext, pApplicationName, applicationVersion, pEngineName, engineVersion, apiVersion
- `VkInstanceCreateInfo` — sType, pNext, flags, pApplicationInfo, enabledLayerCount, ppEnabledLayerNames, enabledExtensionCount, ppEnabledExtensionNames
- `VkDebugUtilsMessengerCreateInfoEXT` — sType, pNext, flags, messageSeverity, messageType, pfnUserCallback, pUserData
- `VkDeviceQueueCreateInfo` — sType, pNext, flags, queueFamilyIndex, queueCount, pQueuePriorities
- `VkDeviceCreateInfo` — sType, pNext, flags, queueCreateInfoCount, pQueueCreateInfos, enabledLayerCount, ppEnabledLayerNames, enabledExtensionCount, ppEnabledExtensionNames, pEnabledFeatures
- `VkPhysicalDeviceFeatures` — all VkBool32 (can be zeroed for triangle)
- `VkPhysicalDeviceDynamicRenderingFeatures` — sType, pNext, dynamicRendering (VkBool32) — needed to enable dynamic rendering
- `VkSwapchainCreateInfoKHR` — sType, pNext, flags, surface, minImageCount, imageFormat, imageColorSpace, imageExtent, imageArrayLayers, imageUsage, imageSharingMode, queueFamilyIndexCount, pQueueFamilyIndices, preTransform, compositeAlpha, presentMode, clipped, oldSwapchain
- `VkImageViewCreateInfo` — sType, pNext, flags, image, viewType, format, components, subresourceRange
- `VkComponentMapping` — r, g, b, a (VkComponentSwizzle)
- `VkImageSubresourceRange` — aspectMask, baseMipLevel, levelCount, baseArrayLayer, layerCount
- `VkExtent2D` — width, height
- `VkExtent3D` — width, height, depth
- `VkOffset2D` — x, y
- `VkOffset3D` — x, y, z
- `VkRect2D` — offset, extent
- `VkViewport` — x, y, width, height, minDepth, maxDepth
- `VkShaderModuleCreateInfo` — sType, pNext, flags, codeSize, pCode
- `VkPipelineShaderStageCreateInfo` — sType, pNext, flags, stage, module, pName, pSpecializationInfo
- `VkPipelineVertexInputStateCreateInfo` — sType, pNext, flags, vertexBindingDescriptionCount, pVertexBindingDescriptions, vertexAttributeDescriptionCount, pVertexAttributeDescriptions
- `VkVertexInputBindingDescription` — binding, stride, inputRate
- `VkVertexInputAttributeDescription` — location, binding, format, offset
- `VkPipelineInputAssemblyStateCreateInfo` — sType, pNext, flags, topology, primitiveRestartEnable
- `VkPipelineViewportStateCreateInfo` — sType, pNext, flags, viewportCount, pViewports, scissorCount, pScissors
- `VkPipelineRasterizationStateCreateInfo` — sType, pNext, flags, depthClampEnable, rasterizerDiscardEnable, polygonMode, cullMode, frontFace, depthBiasEnable, depthBiasConstantFactor, depthBiasClamp, depthBiasSlopeFactor, lineWidth
- `VkPipelineMultisampleStateCreateInfo` — sType, pNext, flags, rasterizationSamples, sampleShadingEnable, minSampleShading, pSampleMask, alphaToCoverageEnable, alphaToOneEnable
- `VkPipelineColorBlendAttachmentState` — blendEnable, srcColorBlendFactor, dstColorBlendFactor, colorBlendOp, srcAlphaBlendFactor, dstAlphaBlendFactor, alphaBlendOp, colorWriteMask
- `VkPipelineColorBlendStateCreateInfo` — sType, pNext, flags, logicOpEnable, logicOp, attachmentCount, pAttachments, blendConstants[4]
- `VkPipelineDynamicStateCreateInfo` — sType, pNext, flags, dynamicStateCount, pDynamicStates
- `VkPipelineLayoutCreateInfo` — sType, pNext, flags, setLayoutCount, pSetLayouts, pushConstantRangeCount, pPushConstantRanges
- `VkGraphicsPipelineCreateInfo` — sType, pNext, flags, stageCount, pStages, pVertexInputState, pInputAssemblyState, pViewportState, pRasterizationState, pMultisampleState, pDepthStencilState, pColorBlendState, pDynamicState, layout, renderPass, subpass, basePipelineHandle, basePipelineIndex
- `VkCommandPoolCreateInfo` — sType, pNext, flags, queueFamilyIndex
- `VkCommandBufferAllocateInfo` — sType, pNext, commandPool, level, commandBufferCount
- `VkCommandBufferBeginInfo` — sType, pNext, flags, pInheritanceInfo
- `VkSemaphoreCreateInfo` — sType, pNext, flags
- `VkFenceCreateInfo` — sType, pNext, flags
- `VkBufferCreateInfo` — sType, pNext, flags, size, usage, sharingMode, queueFamilyIndexCount, pQueueFamilyIndices
- `VkMemoryAllocateInfo` — sType, pNext, allocationSize, memoryTypeIndex
- `VkMemoryRequirements` — size, alignment, memoryTypeBits
- `VkPhysicalDeviceMemoryProperties` — memoryTypeCount, memoryTypes[32], memoryHeapCount, memoryHeaps[16]
- `VkMemoryType` — propertyFlags, heapIndex
- `VkMemoryHeap` — size, flags
- `VkQueueFamilyProperties` — queueFlags, queueCount, timestampValidBits, minImageTransferGranularity
- `VkSurfaceCapabilitiesKHR` — minImageCount, maxImageCount, currentExtent, minImageExtent, maxImageExtent, maxImageArrayLayers, supportedTransforms, currentTransform, supportedCompositeAlpha, supportedUsageFlags
- `VkSurfaceFormatKHR` — format, colorSpace
- `VkPhysicalDeviceProperties` — apiVersion, driverVersion, vendorID, deviceID, deviceType, deviceName[256], ...
- `VkSubmitInfo` — sType, pNext, waitSemaphoreCount, pWaitSemaphores, pWaitDstStageMask, commandBufferCount, pCommandBuffers, signalSemaphoreCount, pSignalSemaphores
- `VkSubmitInfo2` — sType, pNext, flags, waitSemaphoreInfoCount, pWaitSemaphoreInfos, commandBufferInfoCount, pCommandBufferInfos, signalSemaphoreInfoCount, pSignalSemaphoreInfos (sync2)
- `VkSemaphoreSubmitInfo` — sType, pNext, semaphore, value, stageMask, deviceIndex (sync2)
- `VkCommandBufferSubmitInfo` — sType, pNext, commandBuffer, deviceMask (sync2)
- `VkPresentInfoKHR` — sType, pNext, waitSemaphoreCount, pWaitSemaphores, swapchainCount, pSwapchains, pImageIndices, pResults
- `VkClearValue` — union: VkClearColorValue color / VkClearDepthStencilValue depthStencil
- `VkClearColorValue` — union: float[4] / int[4] / uint[4]
- `VkClearDepthStencilValue` — depth, stencil
- `VkRenderingAttachmentInfo` — sType, pNext, imageView, imageLayout, resolveMode, resolveImageView, resolveImageLayout, loadOp, storeOp, clearValue
- `VkRenderingInfo` — sType, pNext, flags, renderArea, layerCount, viewMask, colorAttachmentCount, pColorAttachments, pDepthAttachment, pStencilAttachment
- `VkImageMemoryBarrier2` — sType, pNext, srcStageMask, srcAccessMask, dstStageMask, dstAccessMask, oldLayout, newLayout, srcQueueFamilyIndex, dstQueueFamilyIndex, image, subresourceRange
- `VkBufferMemoryBarrier2` — sType, pNext, srcStageMask, srcAccessMask, dstStageMask, dstAccessMask, srcQueueFamilyIndex, dstQueueFamilyIndex, buffer, offset, size
- `VkDependencyInfo` — sType, pNext, dependencyFlags, memoryBarrierCount, pMemoryBarriers, bufferMemoryBarrierCount, pBufferMemoryBarriers, imageMemoryBarrierCount, pImageMemoryBarriers
- `VkBufferCopy` — srcOffset, dstOffset, size
- `VkDebugUtilsMessengerCallbackDataEXT` — sType, pNext, messageId, pMessageIdName, messageSeverity, messageType, pMessage, queueLabelCount, pQueueLabels, cmdBufLabelCount, pCmdBufLabels, objectCount, pObjects
- `VkDebugUtilsObjectNameInfoEXT` — sType, pNext, objectType, objectHandle, pObjectName
- `VulkanRenderer.cs` — Render loop:
- Command pool + command buffers (2 frames in flight)
- Semaphores + fences for sync
- Descriptor pools + sets
- RenderWorld(World):
- Get camera from ECS
- Collect lights from ECS
- Update frame UBO (camera pos, lights)
- For each Mesh+Transform entity: upload/cache vertex+index buffers,
set push constants (MVP + material), draw indexed
- Screenshot capture (copy image to staging buffer → PNG)
- Present
#### 1.5 `Vk.cs`
Function delegate types + loaded function pointers:
- `VulkanBackendRegistrar.cs` — Register("vulkan", factory)
**Instance-level functions** (loaded via `vkGetInstanceProcAddr`):
- `vkCreateInstance`, `vkDestroyInstance`
- `vkEnumeratePhysicalDevices`, `vkGetPhysicalDeviceProperties`, `vkGetPhysicalDeviceMemoryProperties`
- `vkGetPhysicalDeviceQueueFamilyProperties`
- `vkGetPhysicalDeviceSurfaceSupportKHR`
- `vkGetPhysicalDeviceSurfaceCapabilitiesKHR`, `vkGetPhysicalDeviceSurfaceFormatsKHR`, `vkGetPhysicalDeviceSurfacePresentModesKHR`
- `vkCreateDevice`, `vkDestroyDevice`
- `vkDestroySurfaceKHR`
- `vkCreateDebugUtilsMessengerEXT`, `vkDestroyDebugUtilsMessengerEXT` (extension — via getInstanceProcAddr)
- `vkGetDeviceProcAddr`
csproj: references Engine.Core, Engine.Graphics, Flecs.NET
NO external Vulkan packages — pure P/Invoke
**Device-level functions** (loaded via `vkGetDeviceProcAddr` for best performance):
- `vkGetDeviceQueue`
- `vkCreateSwapchainKHR`, `vkDestroySwapchainKHR`, `vkGetSwapchainImagesKHR`
- `vkCreateImageView`, `vkDestroyImageView`
- `vkCreateShaderModule`, `vkDestroyShaderModule`
- `vkCreatePipelineLayout`, `vkDestroyPipelineLayout`
- `vkCreateGraphicsPipelines`, `vkDestroyPipeline`
- `vkCreateCommandPool`, `vkDestroyCommandPool`
- `vkAllocateCommandBuffers`, `vkFreeCommandBuffers`
- `vkBeginCommandBuffer`, `vkEndCommandBuffer`, `vkResetCommandBuffer`
- `vkCreateSemaphore`, `vkDestroySemaphore`
- `vkCreateFence`, `vkDestroyFence`, `vkResetFences`, `vkWaitForFences`, `vkGetFenceStatus`
- `vkCreateBuffer`, `vkDestroyBuffer`
- `vkAllocateMemory`, `vkFreeMemory`
- `vkBindBufferMemory`
- `vkGetBufferMemoryRequirements`
- `vkMapMemory`, `vkUnmapMemory`
- `vkCmdBindPipeline`
- `vkCmdSetViewport`, `vkCmdSetScissor`
- `vkCmdBindVertexBuffers`
- `vkCmdDraw`
- `vkCmdBeginRendering`, `vkCmdEndRendering` (Vulkan 1.3 dynamic rendering)
- `vkCmdPipelineBarrier2` (sync2)
- `vkCmdCopyBuffer`
- `vkCmdBindIndexBuffer`, `vkCmdDrawIndexed` (for future)
- `vkAcquireNextImageKHR`
- `vkQueueSubmit2` (sync2)
- `vkQueuePresentKHR`
- `vkDeviceWaitIdle`
- `vkQueueWaitIdle`
### Phase 3: Shaders
### Phase 2: Vulkan Context
GLSL → SPIR-V shaders (compiled with glslangValidator):
- `Shaders/vertex.vert`#version 450, position/normal/color inputs, MVP+model uniforms, outputs
- `Shaders/fragment.frag`#version 450, PBR lighting (Fresnel, ACES, gamma), directional + point lights
- Compile: `glslangValidator -V vertex.vert -o vertex.spv && glslangValidator -V fragment.frag -o fragment.spv`
- Embed .spv files as project resources or copy to output directory
#### 2.1 `VulkanContext.cs`
- **CreateInstance:**
- `VkApplicationInfo` with `apiVersion = VK_API_VERSION_1_3`
- Instance extensions from SDL3: `SDL_GetVulkanInstanceExtensions()`
- Add `VK_EXT_debug_utils` in Debug
- Layers: `VK_LAYER_KHRONOS_validation` in Debug
- Chain `VkDebugUtilsMessengerCreateInfoEXT` in `pNext` for early validation
- Debug callback: prints `pMessage` to stderr/console
### Phase 4: App Integration
- **PickPhysicalDevice:**
- Enumerate all physical devices
- Prefer `VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU`
- Find queue family with `VK_QUEUE_GRAPHICS_BIT` + surface support (`vkGetPhysicalDeviceSurfaceSupportKHR`)
- Fix `CortexEngine.App.csproj` — remove deleted project refs, add Engine.Graphics + Engine.Graphics.Vulkan
- **CreateLogicalDevice:**
- Enable `VK_KHR_swapchain` device extension
- Chain `VkPhysicalDeviceDynamicRenderingFeatures` in `pNext` with `dynamicRendering = VK_TRUE`
- Single queue from selected family, priority 1.0
- **CreateSurface:**
- Call SDL3 `SDL_Vulkan_CreateSurface(window, instance, ...)` via Sdl3Window
- Store `VkSurfaceKHR`
- **Debug Messenger:**
- `vkCreateDebugUtilsMessengerEXT` with callback
- Severity: Verbose | Warning | Error
- Type: General | Validation | Performance
### Phase 3: Swapchain
#### 3.1 `VulkanSwapchain.cs`
- **Query surface:**
- `vkGetPhysicalDeviceSurfaceCapabilitiesKHR` → min/max image count, current extent
- `vkGetPhysicalDeviceSurfaceFormatsKHR` → prefer `B8G8R8A8_UNORM` + `SrgbNonlinear`, fallback first format
- `vkGetPhysicalDeviceSurfacePresentModesKHR` → prefer `MAILBOX`, fallback `FIFO` (guaranteed)
- **Create swapchain:**
- `minImageCount = max(minImageCount + 1, maxImageCount)` (clamped)
- `imageUsage = COLOR_ATTACHMENT_BIT | TRANSFER_DST_BIT` (for future screenshots)
- `preTransform = currentTransform` (no pre-rotation on desktop)
- `compositeAlpha = OPAQUE_BIT`
- `clipped = VK_TRUE`
- `oldSwapchain = VK_NULL_HANDLE` (on first create)
- **Get swapchain images:**
- `vkGetSwapchainImagesKHR` → array of `VkImage`
- Create `VkImageView` for each (`TYPE_2D`, same format, `COLOR_BIT` aspect)
- **Recreate:**
- `vkDeviceWaitIdle`
- Destroy old image views + swapchain
- Create new swapchain with `oldSwapchain` = old handle
- Create new image views
### Phase 4: Pipeline
#### 4.1 `VulkanPipeline.cs`
- **Shader modules:**
- Load `triangle.vert.spv` and `triangle.frag.spv` from embedded resources or filesystem
- `vkCreateShaderModule` for each
- **Vertex input:**
- Binding 0: stride = sizeof(Vertex) = 36 bytes, `VERTEX_INPUT_RATE_VERTEX`
- Attribute 0: `R32G32B32_SFLOAT` @ offset 0 (Position, location 0)
- Attribute 1: `R32G32B32_SFLOAT` @ offset 12 (Color, location 1)
- Attribute 2: `R32G32B32_SFLOAT` @ offset 24 (Normal, location 2)
- **Pipeline state:**
- Input assembly: `TRIANGLE_LIST`
- Viewport state: viewportCount=1, scissorCount=1 (dynamic values)
- Rasterization: `FILL`, cull `NONE`, `COUNTER_CLOCKWISE`, lineWidth=1.0
- Multisample: `COUNT_1_BIT`, no sample shading
- Color blend: 1 attachment, blend disabled, write RGBA
- Dynamic state: `VIEWPORT`, `SCISSOR`
- Pipeline layout: no descriptor sets, no push constants (triangle only)
- **Dynamic rendering integration:**
- `VkGraphicsPipelineCreateInfo::renderPass = VK_NULL_HANDLE` (Vulkan 1.3 dynamic rendering)
- Set `pNext` to `VkPipelineRenderingCreateInfo` with `colorAttachmentCount=1`, `pColorAttachmentFormats = {swapchainFormat}`
### Phase 5: Frame Resources
#### 5.1 `VulkanFrameResources.cs`
- **Constants:**
- `MAX_FRAMES_IN_FLIGHT = 2`
- **Per-frame-in-flight resources** (indexed 0..MAX_FRAMES_IN_FLIGHT-1):
- `VkCommandBuffer` — primary, from shared command pool
- `VkFence` — signaled on submit, waited at frame start (created with `SIGNALED` flag)
- `VkSemaphore` — acquire semaphore (signaled by `vkAcquireNextImageKHR`)
- **Per-swapchain-image resources** (indexed 0..swapchainImageCount-1):
- `VkSemaphore` — submit/render-finished semaphore (signaled by `vkQueueSubmit2`, waited by `vkQueuePresentKHR`)
- **CRITICAL:** These are indexed by swapchain image index, NOT frame-in-flight index.
This is the correct pattern from the Vulkan Guide (§swapchain_semaphore_reuse).
Waiting on the acquire semaphore/fence for a given image index guarantees the previous
present operation using that image has completed, making the submit semaphore safe to reuse.
- **Command pool:**
- `vkCreateCommandPool` with `RESET_COMMAND_BUFFER_BIT` flag
- Allocate `MAX_FRAMES_IN_FLIGHT` primary command buffers
### Phase 6: Vertex Buffer
#### 6.1 `VulkanVertexBuffer.cs`
- **Staging buffer pattern (correct from start):**
1. Create staging buffer: `usage = TRANSFER_SRC_BIT`, memory = `HOST_VISIBLE | HOST_COHERENT`
2. `vkMapMemory``memcpy` vertex data → `vkUnmapMemory`
3. Create vertex buffer: `usage = TRANSFER_DST_BIT | VERTEX_BUFFER_BIT`, memory = `DEVICE_LOCAL`
4. Allocate + record one-time command buffer
5. `vkCmdCopyBuffer(staging, vertex, size)`
6. Submit + wait on fence
7. Destroy staging buffer + free its memory + free one-time command buffer
- **Triangle data:**
```
Vertex[3] = {
{ Position: ( 0.0, -0.5, 0.0), Color: (1, 0, 0), Normal: (0, 0, 1) },
{ Position: ( 0.5, 0.5, 0.0), Color: (0, 1, 0), Normal: (0, 0, 1) },
{ Position: (-0.5, 0.5, 0.0), Color: (0, 0, 1), Normal: (0, 0, 1) },
}
```
- **Memory type selection:**
- `vkGetPhysicalDeviceMemoryProperties` → iterate `memoryTypes[]`
- Find type where `(memoryTypeBits >> i) & 1` and `propertyFlags` matches desired flags
- Helper: `FindMemoryType(memoryTypeBits, desiredFlags)`
### Phase 7: Renderer
#### 7.1 `VulkanRenderer.cs` (implements `IRenderer`)
- **Constructor:**
- Create swapchain, pipeline, frame resources, vertex buffer
- Store reference to `VulkanContext` (instance, device, queue, surface)
- **Frame loop (`Render()` method):**
```
1. vkWaitForFences(frameFences[frameIndex])
2. vkResetFences(frameFences[frameIndex])
3. vkAcquireNextImageKHR(swapchain, acquireSemaphores[frameIndex], imageIndex)
4. vkResetCommandBuffer(commandBuffers[frameIndex])
5. vkBeginCommandBuffer(commandBuffers[frameIndex], ONE_TIME_SUBMIT)
6. Image layout transition (sync2 barrier):
UNDEFINED → COLOR_ATTACHMENT_OPTIMAL
(srcStageMask: NONE, dstStageMask: COLOR_ATTACHMENT_OUTPUT)
7. vkCmdBeginRendering(renderingInfo):
- colorAttachment: swapchainImageViews[imageIndex], COLOR_ATTACHMENT_OPTIMAL
- loadOp: CLEAR (black), storeOp: STORE
- renderArea: full extent
8. vkCmdBindPipeline(GRAPHICS, pipeline)
9. vkCmdSetViewport(0, 1, {0, 0, extent.width, extent.height, 0, 1})
10. vkCmdSetScissor(0, 1, {{0,0}, extent})
11. vkCmdBindVertexBuffers(0, 1, {vertexBuffer}, {0})
12. vkCmdDraw(3, 1, 0, 0)
13. vkCmdEndRendering()
14. Image layout transition (sync2 barrier):
COLOR_ATTACHMENT_OPTIMAL → PRESENT_SRC_KHR
(srcStageMask: COLOR_ATTACHMENT_OUTPUT, dstStageMask: ALL_GRAPHICS)
15. vkEndCommandBuffer()
16. vkQueueSubmit2(queue, submitInfo2):
- wait: acquireSemaphores[frameIndex] @ COLOR_ATTACHMENT_OUTPUT
- commandBuffer: commandBuffers[frameIndex]
- signal: submitSemaphores[imageIndex]
- fence: frameFences[frameIndex]
17. vkQueuePresentKHR(presentInfo):
- wait: submitSemaphores[imageIndex]
- swapchain, imageIndex
18. frameIndex = (frameIndex + 1) % MAX_FRAMES_IN_FLIGHT
```
- **Resize handling:**
- If `vkAcquireNextImageKHR` returns `ERROR_OUT_OF_DATE_KHR` or `SuboptimalKHR`:
- `vkDeviceWaitIdle`
- Recreate swapchain
- Continue frame
- **Dispose:**
- `vkDeviceWaitIdle`
- Destroy vertex buffer + memory
- Destroy semaphores (acquire + submit), fences
- Destroy command pool
- Destroy pipeline, pipeline layout, shader modules
- Destroy swapchain + image views
- Destroy debug messenger
- Destroy device, surface, instance
#### 7.2 `VulkanRenderContext.cs` (implements `IRenderContext`)
- Exposes `Window` (from Sdl3Window)
- `CreateRenderer()` → returns `VulkanRenderer`
- `Resize()` → triggers swapchain recreation
- `Dispose()` → destroys context
#### 7.3 `VulkanBackendRegistrar.cs`
- Static constructor registers `"vulkan"` in `RenderBackendFactory`
- Factory creates `VulkanRenderContext` with `Sdl3Window`
### Phase 8: Shaders
#### 8.1 `Shaders/triangle.vert`
```glsl
#version 450
layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec3 inColor;
layout(location = 2) in vec3 inNormal;
layout(location = 0) out vec3 fragColor;
void main() {
gl_Position = vec4(inPosition, 1.0);
fragColor = inColor;
}
```
#### 8.2 `Shaders/triangle.frag`
```glsl
#version 450
layout(location = 0) in vec3 fragColor;
layout(location = 0) out vec4 outColor;
void main() {
outColor = vec4(fragColor, 1.0);
}
```
#### 8.3 Compilation
```bash
glslangValidator -V triangle.vert -o triangle.vert.spv
glslangValidator -V triangle.frag -o triangle.frag.spv
```
- Embed `.spv` files as embedded resources in csproj, or copy to output directory
- Load at runtime via `Assembly.GetManifestResourceStream()` or `File.ReadAllBytes()`
### Phase 9: App Integration
- Fix `CortexEngine.App.csproj`:
- Remove deleted project references
- Add `Engine.Graphics` + `Engine.Graphics.Vulkan`
- Fix `Program.cs`:
- Remove all Raylib/OpenTK/old OpenGL imports
- Remove ImGuiLayer, ObjectManipulator (Raylib-specific, will reimplement later)
- Use `RenderBackendFactory.Create("vulkan", 1280, 720, enableValidation: true)`
- Keep: physics, camera controllers, AI commands, tour mode, scene setup
- Sdl3Window creates Vulkan surface automatically (vulkanSurface: true)
- Simplify to triangle-only rendering
- `RenderBackendFactory.Create("vulkan", 1280, 720, validation: true)`
- Main loop: poll events → render → present
- Keep: Sdl3Window, basic event handling
- Remove: ECS scene, physics, AI, camera tour (add back later)
### Phase 5: Fix Tests
### Phase 10: Fix Tests
- Update `Engine.Tests.csproj` — reference restored Engine.Graphics
- Tests that reference Engine.Graphics: ObjLoaderTests, RenderBackendFactoryTests,
- Update `Engine.Tests.csproj` — reference restored `Engine.Graphics`
- Tests referencing Engine.Graphics: ObjLoaderTests, RenderBackendFactoryTests,
SceneSerializerTests, MeshMathAndProceduralTests
- All 66 tests should pass after Engine.Graphics is restored
- All tests should pass after Engine.Graphics is restored
### Phase 6: ImGui (later)
- ImGui.NET NuGet + Vulkan ImGui backend
- ImGui_ImplVulkan for rendering
- Entity inspector, hierarchy, debug overlay
### Phase 7: Shadow Mapping (later)
- Depth-only render pass from light's POV
- Shadow image (depth texture, 2048x2048)
- Shadow matrix (lightViewProj) in push constants
- PCF sampling in fragment shader
## Cross-Platform Notes
- Vulkan P/Invoke: only difference is library name (vulkan-1.dll vs libvulkan.so.1)
- SDL3: already cross-platform (ppy.SDL3-CS)
- SPIR-V: binary format, works everywhere
- .NET 9: NativeLibrary.Load for dynamic resolution if needed
---
## File Layout
```
src/
├── Engine.Core/ (exists, unchanged)
├── Engine.Graphics/ (new — interfaces + loaders)
├── Engine.Core/ (exists, unchanged)
├── Engine.Graphics/ (exists, restored minimal interfaces)
│ ├── Engine.Graphics.csproj
│ ├── IRenderContext.cs
│ ├── IRenderer.cs
│ ├── IScreenshotProvider.cs
│ ├── RenderBackendFactory.cs
│ ├── MeshMath.cs
│ ├── ProceduralMesh.cs
│ ├── SceneSerializer.cs
│ └── Loaders/
── ObjLoader.cs
│ └── GltfLoader.cs
├── Engine.Graphics.Vulkan/ (new — pure Vulkan P/Invoke)
── ObjLoader.cs
├── Engine.Graphics.Vulkan/ (new — pure P/Invoke, Vulkan 1.3)
│ ├── Engine.Graphics.Vulkan.csproj
│ ├── VulkanNative.cs (~800 lines)
│ ├── VulkanContext.cs (~300 lines)
│ ├── VulkanSwapchain.cs (~250 lines)
│ ├── VulkanPipeline.cs (~200 lines)
│ ├── VulkanBuffer.cs (~150 lines)
│ ├── VulkanRenderer.cs (~400 lines)
│ ├── VulkanBackendRegistrar.cs
│ ├── VulkanNative.cs — library loading, vkGetInstanceProcAddr
│ ├── VulkanHandles.cs — opaque pointer types
│ ├── VulkanEnums.cs — all Vulkan enums/flags
│ ├── VulkanStructs.cs — all Vulkan structs (LayoutKind.Sequential)
│ ├── Vk.cs — function delegates + loaded pointers
│ ├── VulkanContext.cs — instance, device, queue, surface, debug
│ ├── VulkanSwapchain.cs — swapchain, image views, recreate
│ ├── VulkanPipeline.cs — shader modules, pipeline layout, graphics pipeline
│ ├── VulkanFrameResources.cs — command buffers, fences, semaphores (correct indexing)
│ ├── VulkanVertexBuffer.cs — staging buffer → device-local vertex buffer
│ ├── VulkanRenderer.cs — IRenderer: frame loop with dynamic rendering
│ ├── VulkanRenderContext.cs — IRenderContext implementation
│ ├── VulkanBackendRegistrar.cs— registration in RenderBackendFactory
│ └── Shaders/
│ ├── vertex.vert
│ ├── fragment.frag
│ ├── vertex.spv
│ └── fragment.spv
├── Engine.Physics/ (exists, unchanged)
├── Engine.AI/ (exists, unchanged)
└── CortexEngine.App/ (fix references)
│ ├── triangle.vert
│ ├── triangle.frag
│ ├── triangle.vert.spv
│ └── triangle.frag.spv
├── Engine.Physics/ (exists, unchanged)
├── Engine.AI/ (exists, unchanged)
└── CortexEngine.App/ (fix references, simplify to triangle)
```
## Solution Update
---
Remove from solution:
- Engine.Graphics.Raylib (deleted)
- Engine.Graphics.OpenTK (deleted)
- Engine.Graphics.Vulkan (old Silk.NET, deleted)
## csproj: Engine.Graphics.Vulkan
Add to solution:
- Engine.Graphics (new)
- Engine.Graphics.Vulkan (new, pure P/Invoke)
```xml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Engine.Core\Engine.Core.csproj" />
<ProjectReference Include="..\Engine.Graphics\Engine.Graphics.csproj" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Shaders\*.spv" />
</ItemGroup>
</Project>
```
No NuGet packages for Vulkan. Pure P/Invoke.
---
## Cross-Platform Notes
- **Library name:** `vulkan-1.dll` (Windows) vs `libvulkan.so.1` (Linux) — handled in `VulkanNative.cs`
- **Surface creation:** SDL3 abstracts platform differences (`SDL_Vulkan_CreateSurface`)
- **SPIR-V:** Binary format, identical on all platforms
- **.NET 9:** `NativeLibrary.Load()` for dynamic resolution
---
## Key Technical Details
### Matrix Layout
- System.Numerics.Matrix4x4 is row-major
- Vulkan expects column-major in shaders (layout(row_major) or transpose)
- Solution: use `layout(row_major) uniform mat4` in GLSL → no transpose needed
- OR transpose in C# before writing to uniform buffer
### Semaphore Indexing (CRITICAL)
```
Indexed by frame-in-flight (0..1) Indexed by swapchain image (0..N-1)
───────────────────────────────── ──────────────────────────────────
Acquire semaphore ✓
Command buffer ✓
Frame fence ✓
Submit semaphore ✓
```
Rationale: `vkQueuePresentKHR` cannot signal a fence/semaphore. The only way to know
a submit semaphore is safe to reuse is to acquire the same swapchain image index again
(which guarantees the previous present using that image has completed).
Indexing submit semaphores by frame-in-flight is a common bug that violates the spec.
### Dynamic Rendering (Vulkan 1.3)
No `VkRenderPass` or `VkFramebuffer` objects needed:
```csharp
// Instead of vkCmdBeginRenderPass:
VkRenderingAttachmentInfo colorAttachment = new() {
sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO,
imageView = swapchainImageViews[imageIndex],
imageLayout = COLOR_ATTACHMENT_OPTIMAL,
loadOp = CLEAR,
storeOp = STORE,
clearValue = new() { color = { 0, 0, 0, 1 } }
};
VkRenderingInfo renderingInfo = new() {
sType = VK_STRUCTURE_TYPE_RENDERING_INFO,
renderArea = { {0,0}, extent },
layerCount = 1,
colorAttachmentCount = 1,
pColorAttachments = &colorAttachment
};
vkCmdBeginRendering(commandBuffer, &renderingInfo);
// draw commands...
vkCmdEndRendering(commandBuffer);
```
Pipeline must include `VkPipelineRenderingCreateInfo` in `pNext`:
```csharp
VkPipelineRenderingCreateInfo renderingInfo = new() {
sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO,
colorAttachmentCount = 1,
pColorAttachmentFormats = &swapchainFormat
};
// Chain in VkGraphicsPipelineCreateInfo.pNext
```
### Sync2 Image Layout Transitions
Using `vkCmdPipelineBarrier2` with `VkImageMemoryBarrier2`:
```csharp
// UNDEFINED → COLOR_ATTACHMENT_OPTIMAL (before rendering)
VkImageMemoryBarrier2 toColor = new() {
sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2,
srcStageMask = PIPELINE_STAGE_2_NONE,
srcAccessMask = ACCESS_2_NONE,
dstStageMask = PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT,
dstAccessMask = ACCESS_2_COLOR_ATTACHMENT_WRITE,
oldLayout = UNDEFINED,
newLayout = COLOR_ATTACHMENT_OPTIMAL,
image = swapchainImages[imageIndex],
subresourceRange = { COLOR_BIT, 0, 1, 0, 1 }
};
// COLOR_ATTACHMENT_OPTIMAL → PRESENT_SRC_KHR (after rendering)
VkImageMemoryBarrier2 toPresent = new() {
sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2,
srcStageMask = PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT,
srcAccessMask = ACCESS_2_COLOR_ATTACHMENT_WRITE,
dstStageMask = PIPELINE_STAGE_2_ALL_GRAPHICS,
dstAccessMask = ACCESS_2_NONE,
oldLayout = COLOR_ATTACHMENT_OPTIMAL,
newLayout = PRESENT_SRC_KHR,
image = swapchainImages[imageIndex],
subresourceRange = { COLOR_BIT, 0, 1, 0, 1 }
};
VkDependencyInfo depInfo = new() {
sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO,
imageMemoryBarrierCount = 1,
pImageMemoryBarriers = &barrier
};
vkCmdPipelineBarrier2(commandBuffer, &depInfo);
```
### Queue Submit (Sync2)
Using `vkQueueSubmit2` with `VkSubmitInfo2`:
```csharp
VkSemaphoreSubmitInfo waitInfo = new() {
sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO,
semaphore = acquireSemaphores[frameIndex],
stageMask = PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT
};
VkCommandBufferSubmitInfo cmdInfo = new() {
sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO,
commandBuffer = commandBuffers[frameIndex]
};
VkSemaphoreSubmitInfo signalInfo = new() {
sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO,
semaphore = submitSemaphores[imageIndex],
stageMask = PIPELINE_STAGE_2_ALL_GRAPHICS
};
VkSubmitInfo2 submitInfo = new() {
sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2,
waitSemaphoreInfoCount = 1,
pWaitSemaphoreInfos = &waitInfo,
commandBufferInfoCount = 1,
pCommandBufferInfos = &cmdInfo,
signalSemaphoreInfoCount = 1,
pSignalSemaphoreInfos = &signalInfo
};
vkQueueSubmit2(queue, 1, &submitInfo, frameFences[frameIndex]);
```
### Vertex Layout
```
Vertex struct (9 floats, 36 bytes):
Position: vec3 (offset 0)
Color: vec3 (offset 12)
Normal: vec3 (offset 24)
```
### Push Constants (96 bytes max)
```
offset 0: mat4 MVP (64 bytes)
offset 64: vec3 materialAlbedo + float roughness (16 bytes)
offset 80: float metallic + uint useTexture + uint pad + uint pad (16 bytes)
```
### Frame UBO (224 bytes)
```
offset 0: vec3 cameraPosition + uint lightCount (16 bytes)
offset 16: vec3 ambientColor + float pad (16 bytes)
offset 32: Light[4] — each 48 bytes (vec3 direction + float intensity + vec3 color + float pad)
```
### Light Struct (48 bytes)
```
vec3 direction (12 bytes)
float intensity (4 bytes)
vec3 color (12 bytes)
float padding (4 bytes)
Position: vec3 (offset 0, format R32G32B32_SFLOAT, location 0)
Color: vec3 (offset 12, format R32G32B32_SFLOAT, location 1)
Normal: vec3 (offset 24, format R32G32B32_SFLOAT, location 2)
```
### Validation Layers
```csharp
string[] layers = enableValidation
? new[] { "VK_LAYER_KHRONOS_validation" }
string[] layers = enableValidation
? new[] { "VK_LAYER_KHRONOS_validation" }
: Array.Empty<string>();
string[] instanceExtensions = enableValidation
? [.. sdlExtensions, "VK_EXT_debug_utils"]
: sdlExtensions;
```
Validation errors print to stderr — use for debugging.
### Memory Allocation
Simple approach (no VMA):
1. vkGetPhysicalDeviceMemoryProperties
2. Find memory type with VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | HOST_COHERENT_BIT
3. vkAllocateMemory + vkBindBufferMemory
4. vkMapMemory for writing, vkUnmapMemory
Debug callback (C#):
```csharp
static uint DebugCallback(
nint instance, uint messageSeverity, uint messageTypes,
nint pCallbackData, nint pUserData)
{
var data = Marshal.PtrToStructure<VkDebugUtilsMessengerCallbackDataEXT>(pCallbackData);
Console.Error.WriteLine($"[Vulkan] {data.pMessage}");
return 0; // VK_FALSE — don't abort
}
```
For GPU-only buffers (vertex/index):
1. Find memory type with DEVICE_LOCAL_BIT
2. Use staging buffer (host visible) + vkCmdCopyBuffer
---
## Future Phases (Not in This Plan)
- **Phase 11:** ImGui integration (ImGui.NET + Vulkan backend)
- **Phase 12:** Mesh rendering (OBJ loading, index buffers, descriptor sets, UBO for camera)
- **Phase 13:** PBR shading (Fresnel, ACES tonemap, gamma correction, directional + point lights)
- **Phase 14:** Shadow mapping (depth-only render pass from light POV, PCF sampling)
- **Phase 15:** Screenshot capture (copy swapchain image to staging buffer → PNG)
- **Phase 16:** VMA (Vulkan Memory Allocator) for sub-allocation
- **Phase 17:** Multi-threaded command buffer recording