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
+6 -427
View File
@@ -1,20 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Numerics;
using Engine.AI;
#if !RELEASE_AOT
using Engine.AI.Mcp;
using Microsoft.AspNetCore.Builder;
#endif
using Engine.Core;
using Engine.Core.Components;
using Engine.Graphics;
using Engine.Graphics.Loaders;
using Engine.Graphics.Vulkan;
using Engine.Physics;
using Flecs.NET.Core;
using ImGuiNET;
namespace CortexEngine.App;
@@ -22,280 +9,40 @@ class Program
{
static async Task Main(string[] args)
{
Console.WriteLine("Cortex Engine — Vulkan Backend, Pure P/Invoke...");
Console.WriteLine("Cortex Engine — Vulkan Triangle (pure P/Invoke)...");
try
{
if (args.Contains("--mcp-stdio"))
{
RunMcpStdioServer();
return;
}
var cameraTour = args.Contains("--camera-tour");
var testScene = args.Contains("--test-scene");
if (testScene)
cameraTour = true;
using var world = World.Create();
var timing = new Timing();
using var physicsWorld = new PhysicsWorld();
VulkanBackendRegistrar.EnsureRegistered();
using var renderContext = RenderBackendFactory.Create("vulkan", 1280, 720, enableValidation: true);
var window = renderContext.Window;
var input = window.Input;
using var renderer = renderContext.CreateRenderer();
VulkanImGui? imGuiLayer = null;
if (!cameraTour && renderer is VulkanRenderer vkRenderer)
{
ImGui.CreateContext();
imGuiLayer = new VulkanImGui(vkRenderer._ctx, vkRenderer._swapchain);
vkRenderer.ImGuiLayer = imGuiLayer;
ImGui.GetIO().DisplaySize = new System.Numerics.Vector2(window.Width, window.Height);
Console.WriteLine("[App] ImGui initialized.");
}
using var world = World.Create();
var (modelPath, mcpPort) = ParseArgs(args);
var mesh = LoadModel(modelPath);
var processor = new AiCommandProcessor(world, LoadModel, path => renderer.RequestScreenshot(path));
var queue = new AiCommandQueue(processor, renderer.ScreenshotProvider);
var cameraEntity = world.Entity("Camera")
.Set(new Transform(new Vector3(0.0f, 0.75f, -30.0f), Quaternion.Identity, Vector3.One))
.Set(new Camera(
new Vector3(0.0f, 0.75f, -30.0f),
new Vector3(0.0f, 0.5f, 0.0f),
Vector3.UnitY,
MathF.PI / 12.0f,
1280.0f / 720.0f,
0.1f,
100.0f));
world.Entity("MainLight")
.Set(new Transform(new Vector3(0.0f, 0.0f, 0.0f), Quaternion.Identity, Vector3.One))
.Set(Light.Directional(new Vector3(0.4f, -1.0f, -0.3f), new Vector3(1.0f, 0.95f, 0.85f), 2.0f));
ICameraController[] cameraControllers =
{
new FreeFlyCameraController(cameraEntity),
new OrbitCameraController(cameraEntity, new Vector3(0.0f, 0.5f, 0.0f))
};
var activeControllerIndex = 0;
var cameraController = cameraControllers[activeControllerIndex];
Console.WriteLine($"Active camera controller: {cameraController.Name} (press F to toggle)");
if (testScene)
{
Console.WriteLine("Calibration test scene enabled.");
CreateCalibrationScene(world, mesh);
}
else
{
CreateDemoScene(world, mesh);
}
#if !RELEASE_AOT
WebApplication? mcpApp = null;
Task? mcpTask = null;
if (mcpPort > 0)
{
mcpApp = McpEngineServerHost.Create(args, queue, port: mcpPort);
mcpTask = mcpApp.RunAsync();
_ = mcpTask.ContinueWith(t =>
{
if (t.IsFaulted)
Console.WriteLine($"MCP server error: {t.Exception?.GetBaseException().Message}");
else if (t.IsCanceled)
Console.WriteLine("MCP server canceled.");
else
Console.WriteLine("MCP server stopped.");
}, TaskScheduler.Default);
Console.WriteLine($"MCP HTTP server listening on http://localhost:{mcpPort}/ (SSE)");
}
else
{
Console.WriteLine("MCP server disabled (--mcp-port 0).");
}
#endif
var frames = 0;
var lastFpsTime = 0.0;
var lastWidth = window.Width;
var lastHeight = window.Height;
var demoScreenshotRequested = false;
var currentFps = 0;
var tourPoses = testScene
? new CameraPose[]
{
new("test_front", new Vector3(0.0f, 0.75f, -30.0f), new Vector3(0.0f, 0.5f, 0.0f), Vector3.UnitY),
new("test_back", new Vector3(0.0f, 0.75f, 30.0f), new Vector3(0.0f, 0.5f, 0.0f), Vector3.UnitY),
new("test_left", new Vector3(-30.0f, 0.75f, 0.0f), new Vector3(0.0f, 0.5f, 0.0f), Vector3.UnitY),
new("test_right", new Vector3(30.0f, 0.75f, 0.0f), new Vector3(0.0f, 0.5f, 0.0f), Vector3.UnitY),
new("test_top", new Vector3(0.0f, 30.0f, 0.0f), new Vector3(0.0f, 0.0f, 0.0f), -Vector3.UnitZ),
new("test_shifted", new Vector3(15.0f, 0.75f, -22.5f), new Vector3(0.0f, 0.5f, 0.0f), Vector3.UnitY),
new("test_rotated", new Vector3(0.0f, 0.75f, -30.0f), new Vector3(2.0f, 0.5f, 0.0f), Vector3.UnitY),
new("test_yaw_15", new Vector3(7.76f, 0.75f, -28.98f), new Vector3(0.0f, 0.5f, 0.0f), Vector3.UnitY),
new("test_yaw_30", new Vector3(15.0f, 0.75f, -25.98f), new Vector3(0.0f, 0.5f, 0.0f), Vector3.UnitY),
new("test_yaw_45", new Vector3(21.21f, 0.75f, -21.21f), new Vector3(0.0f, 0.5f, 0.0f), Vector3.UnitY),
new("test_yaw_90", new Vector3(30.0f, 0.75f, 0.0f), new Vector3(0.0f, 0.5f, 0.0f), Vector3.UnitY),
new("test_pitch_45", new Vector3(0.0f, 21.96f, -21.21f), new Vector3(0.0f, 0.5f, 0.0f), Vector3.UnitY),
new("test_close", new Vector3(0.0f, 0.75f, -15.0f), new Vector3(0.0f, 0.5f, 0.0f), Vector3.UnitY),
new("test_far", new Vector3(0.0f, 0.75f, -60.0f), new Vector3(0.0f, 0.5f, 0.0f), Vector3.UnitY),
new("test_farther", new Vector3(0.0f, 0.75f, -120.0f), new Vector3(0.0f, 0.5f, 0.0f), Vector3.UnitY),
new("test_toward", new Vector3(0.0f, 0.75f, -20.0f), new Vector3(0.0f, 0.5f, 0.0f), Vector3.UnitY)
}
: new CameraPose[]
{
new("front", new Vector3(0.0f, 0.75f, -30.0f), new Vector3(0.0f, 0.5f, 0.0f), Vector3.UnitY),
new("top", new Vector3(0.0f, 30.0f, 0.0f), new Vector3(0.0f, 0.0f, 0.0f), -Vector3.UnitZ),
new("side", new Vector3(30.0f, 0.75f, 4.0f), new Vector3(0.0f, 0.5f, 0.0f), Vector3.UnitY),
new("close", new Vector3(1.0f, 0.75f, -5.0f), new Vector3(0.5f, 0.5f, 0.0f), Vector3.UnitY),
new("low", new Vector3(0.0f, 0.25f, -6.0f), new Vector3(0.0f, 0.5f, 0.0f), Vector3.UnitY),
new("back", new Vector3(0.0f, 0.75f, 30.0f), new Vector3(0.0f, 0.5f, 0.0f), Vector3.UnitY)
};
var tourIndex = -1;
var tourSettleFrames = 0;
var tourScreenshotPending = false;
var tourDone = false;
var frames = 0;
var lastFpsTime = 0.0;
var timing = new Timing();
while (!window.ShouldClose)
{
timing.Tick();
window.PumpEvents();
input.BeginFrame();
var processed = queue.ProcessPending();
if (processed > 0)
Console.WriteLine($"Processed {processed} AI command(s)");
if (window.Width != lastWidth || window.Height != lastHeight)
{
lastWidth = window.Width;
lastHeight = window.Height;
renderContext.Resize(lastWidth, lastHeight);
ref var camera = ref cameraEntity.Ensure<Camera>();
camera.AspectRatio = (float)lastWidth / lastHeight;
}
if (input.IsKeyPressed(Key.F))
{
activeControllerIndex = (activeControllerIndex + 1) % cameraControllers.Length;
cameraController = cameraControllers[activeControllerIndex];
Console.WriteLine($"Active camera controller: {cameraController.Name}");
}
if (!cameraTour)
cameraController.Update(input, (float)timing.DeltaTime);
if (cameraTour && !tourDone)
{
if (tourIndex < 0)
{
tourIndex = 0;
SetCameraPose(cameraEntity, tourPoses[tourIndex]);
tourSettleFrames = 0;
tourScreenshotPending = true;
}
if (tourScreenshotPending)
{
tourSettleFrames++;
if (tourSettleFrames >= 5)
{
var path = $"Screenshots/tour_{tourPoses[tourIndex].Name}.png";
renderer.RequestScreenshot(path);
Console.WriteLine($"Tour screenshot: {path}");
tourScreenshotPending = false;
}
}
if (!tourScreenshotPending && !renderer.IsScreenshotRequested)
{
tourIndex++;
if (tourIndex >= tourPoses.Length)
{
tourDone = true;
Console.WriteLine("Camera tour complete.");
window.Close();
}
else
{
SetCameraPose(cameraEntity, tourPoses[tourIndex]);
tourSettleFrames = 0;
tourScreenshotPending = true;
}
}
}
if (!cameraTour)
{
var toInit = new List<(Entity, RigidBody, Transform)>();
world.Each((Entity e, ref RigidBody rb, ref Transform t) =>
{
if (!rb.IsInitialized)
toInit.Add((e, rb, t));
});
foreach (var (e, rbData, t) in toInit)
{
physicsWorld.CreateBody(e, rbData, t);
var rb = rbData;
rb.IsInitialized = true;
e.Set(rb);
}
physicsWorld.Update((float)timing.DeltaTime);
physicsWorld.SyncTransforms(world, null);
}
if (!demoScreenshotRequested && !cameraTour && frames >= 15)
{
renderer.RequestScreenshot("Screenshots/demo.png");
demoScreenshotRequested = true;
}
if (imGuiLayer != null)
{
ImGui.NewFrame();
ImGui.Begin("Cortex Engine Debug");
ImGui.Text($"FPS: {currentFps}");
ImGui.Text($"Delta: {timing.DeltaTime * 1000.0:F2} ms");
ImGui.Text($"Camera: {cameraController.Name}");
ImGui.Separator();
var cam = cameraEntity.Get<Camera>();
ImGui.Text($"Pos: ({cam.Position.X:F2}, {cam.Position.Y:F2}, {cam.Position.Z:F2})");
ImGui.Text($"Target: ({cam.Target.X:F2}, {cam.Target.Y:F2}, {cam.Target.Z:F2})");
ImGui.Separator();
var entityCount = 0;
world.Each((Entity e, ref Transform _) => entityCount++);
ImGui.Text($"Entities: {entityCount}");
ImGui.Text($"Press F to toggle camera");
ImGui.Separator();
ImGui.Text("Lights:");
world.Each((Entity e, ref Light light) =>
{
ImGui.Text($" {e.Name()}: {light.Type} I={light.Intensity:F1}");
});
ImGui.End();
ImGui.Render();
}
renderer.RenderWorld(world);
queue.CompletePendingScreenshots();
frames++;
if (timing.TotalTime - lastFpsTime >= 1.0)
{
currentFps = frames;
Console.WriteLine($"FPS: {frames}, Delta: {timing.DeltaTime * 1000.0:F2} ms");
frames = 0;
lastFpsTime = timing.TotalTime;
@@ -303,181 +50,13 @@ class Program
}
Console.WriteLine("Shutting down...");
imGuiLayer?.Dispose();
#if !RELEASE_AOT
if (mcpApp != null)
await mcpApp.StopAsync();
if (mcpTask != null)
await mcpTask;
#endif
}
catch (Exception ex)
{
Console.WriteLine($"Fatal error: {ex}");
Environment.Exit(1);
}
}
private static void CreateDemoScene(World world, Mesh mesh)
{
var sphere = ProceduralMesh.CreateSphere(0.5f, 32, 16, new Vector3(0.8f, 0.8f, 0.8f));
var torusKnot = ObjLoader.Load("Content/torusknot.obj", new Vector3(0.8f, 0.8f, 0.8f));
var cubes = new (string name, Vector3 pos, Vector3 color, float scale, float rough, float metal)[]
{
("CubeCenter", new Vector3(0, 5f, 0), new Vector3(0.9f, 0.6f, 0.3f), 0.5f, 0.3f, 0.1f),
("CubeRed", new Vector3(0.3f, 7f, 0.3f), new Vector3(0.85f, 0.15f, 0.15f), 0.5f, 0.4f, 0.2f),
("CubeGreen", new Vector3(-0.3f, 9f, -0.3f), new Vector3(0.2f, 0.8f, 0.3f), 0.5f, 0.5f, 0.0f),
("CubeBlue", new Vector3(0.1f, 11f, 0.1f), new Vector3(0.2f, 0.4f, 0.9f), 0.6f, 0.2f, 0.3f),
("CubeYellow", new Vector3(-0.2f, 13f, 0.2f), new Vector3(0.95f, 0.85f, 0.2f), 0.5f, 0.6f, 0.0f),
("CubeOrange", new Vector3(0.15f, 15f, -0.1f), new Vector3(0.95f, 0.5f, 0.1f), 0.45f, 0.5f, 0.1f),
};
foreach (var (name, pos, color, scale, rough, metal) in cubes)
{
world.Entity(name)
.Set(new Transform(pos, Quaternion.Identity, new Vector3(scale)))
.Set(mesh)
.Set(new Material(color, roughness: rough, metallic: metal))
.Set(RigidBody.DynamicBox(new Vector3(scale * 0.5f), mass: scale * 2f));
}
var spheres = new (string name, Vector3 pos, Vector3 color, float scale, float rough, float metal)[]
{
("SphereGold", new Vector3(3, 6f, -2), new Vector3(1.0f, 0.85f, 0.4f), 1.0f, 0.1f, 1.0f),
("SphereChrome", new Vector3(-3, 8f, 0), new Vector3(0.9f, 0.9f, 0.95f), 1.0f, 0.05f, 1.0f),
("SphereRed", new Vector3(3, 10f, 2), new Vector3(0.9f, 0.1f, 0.1f), 1.0f, 0.4f, 0.0f),
};
foreach (var (name, pos, color, scale, rough, metal) in spheres)
{
world.Entity(name)
.Set(new Transform(pos, Quaternion.Identity, new Vector3(scale)))
.Set(sphere)
.Set(new Material(color, roughness: rough, metallic: metal))
.Set(RigidBody.DynamicSphere(scale * 0.5f, mass: scale));
}
world.Entity("TorusKnot")
.Set(new Transform(new Vector3(0, 0.5f, -6), Quaternion.Identity, new Vector3(1.5f)))
.Set(torusKnot)
.Set(new Material(new Vector3(0.9f, 0.9f, 0.9f), roughness: 0.25f, metallic: 0.6f));
world.Entity("Floor")
.Set(new Transform(new Vector3(0, -0.5f, 0), Quaternion.Identity, new Vector3(20, 0.5f, 20)))
.Set(mesh)
.Set(new Material(new Vector3(0.45f, 0.45f, 0.5f), roughness: 0.8f, metallic: 0.0f))
.Set(RigidBody.StaticPlane(20f));
world.Entity("Grid")
.Set(new Transform(new Vector3(0.0f, 0.0f, 0.0f), Quaternion.Identity, Vector3.One))
.Set(ProceduralMesh.CreateGrid(20, 1.0f, new Vector3(0.5f, 0.5f, 0.55f)))
.Set(new Material(new Vector3(0.5f, 0.5f, 0.55f), roughness: 0.9f, metallic: 0.0f));
}
private static void CreateCalibrationScene(World world, Mesh mesh)
{
var positions = new (string name, Vector3 pos, Vector3 color)[]
{
("CubeOrigin", new Vector3(0.0f, 0.5f, 0.0f), new Vector3(1.0f, 1.0f, 1.0f)),
("CubeRight", new Vector3(2.0f, 0.5f, 0.0f), new Vector3(1.0f, 0.0f, 0.0f)),
("CubeLeft", new Vector3(-2.0f, 0.5f, 0.0f), new Vector3(0.0f, 1.0f, 0.0f)),
("CubeFront", new Vector3(0.0f, 0.5f, 2.0f), new Vector3(0.0f, 0.0f, 1.0f)),
("CubeBack", new Vector3(0.0f, 0.5f, -2.0f), new Vector3(1.0f, 1.0f, 0.0f)),
("CubeUp", new Vector3(0.0f, 2.5f, 0.0f), new Vector3(1.0f, 0.0f, 1.0f)),
("CubeFar", new Vector3(0.0f, 0.5f, 8.0f), new Vector3(0.0f, 1.0f, 1.0f)),
("CubeFarLeft", new Vector3(-5.0f, 0.5f, 5.0f), new Vector3(0.5f, 0.5f, 1.0f))
};
foreach (var (name, pos, color) in positions)
{
world.Entity(name)
.Set(new Transform(pos, Quaternion.Identity, new Vector3(0.5f)))
.Set(mesh)
.Set(new Material(color, roughness: 0.5f, metallic: 0.1f));
}
world.Entity("Grid")
.Set(new Transform(new Vector3(0.0f, 0.0f, 0.0f), Quaternion.Identity, Vector3.One))
.Set(ProceduralMesh.CreateGrid(20, 1.0f, new Vector3(0.5f, 0.5f, 0.55f)))
.Set(new Material(new Vector3(0.5f, 0.5f, 0.55f), roughness: 0.9f, metallic: 0.0f));
}
private static Mesh LoadModel(string path)
{
return path.EndsWith(".gltf", StringComparison.OrdinalIgnoreCase)
|| path.EndsWith(".glb", StringComparison.OrdinalIgnoreCase)
? GltfLoader.Load(path, new Vector3(0.7f, 0.6f, 0.5f))
: ObjLoader.Load(path, new Vector3(0.7f, 0.6f, 0.5f));
}
private static (string modelPath, int mcpPort) ParseArgs(string[] args)
{
var modelPath = FindModelPath(args);
var mcpPort = 5000;
for (var i = 0; i < args.Length; i++)
{
if (args[i] == "--mcp-port" && i + 1 < args.Length && int.TryParse(args[i + 1], out var port))
{
mcpPort = port;
break;
}
}
return (modelPath, mcpPort);
}
private static string FindModelPath(string[] args)
{
for (var i = 0; i < args.Length; i++)
{
var arg = args[i];
if (arg == "--mcp-port")
{
i++;
continue;
}
if (File.Exists(arg))
return arg;
}
var candidates = new[]
{
"Content/cube.obj",
"Models/cube.obj",
"cube.obj"
};
foreach (var candidate in candidates)
{
if (File.Exists(candidate))
return candidate;
}
throw new FileNotFoundException("No model file found. Pass a .obj/.gltf/.glb path as argument or place Content/cube.obj next to the executable.");
}
private static void RunMcpStdioServer()
{
Console.WriteLine("Starting headless stdio MCP server...");
using var world = World.Create();
var processor = new AiCommandProcessor(world, LoadModel, _ => { });
var server = new Engine.AI.Stdio.McpStdioServer(processor);
server.Run();
}
private readonly record struct CameraPose(string Name, Vector3 Position, Vector3 Target, Vector3 Up, float Fov = MathF.PI / 12.0f);
private static void SetCameraPose(Entity cameraEntity, CameraPose pose)
{
ref var camera = ref cameraEntity.Ensure<Camera>();
camera.Position = pose.Position;
camera.Target = pose.Target;
camera.Up = pose.Up;
camera.FieldOfView = pose.Fov;
cameraEntity.Set(camera);
Console.WriteLine($"Camera pose '{pose.Name}': pos={pose.Position}, target={pose.Target}, up={pose.Up}, fov={pose.Fov * 180f / MathF.PI:F0}°");
await Task.CompletedTask;
}
}