diff --git a/CORTEX_ENGINE_ARCHITECTURE.md b/CORTEX_ENGINE_ARCHITECTURE.md
index 45268ea..cd41212 100644
--- a/CORTEX_ENGINE_ARCHITECTURE.md
+++ b/CORTEX_ENGINE_ARCHITECTURE.md
@@ -15,7 +15,7 @@ Cortex Engine is a 3D game engine built from scratch to provide a Unity-like dev
- **Read** the complete ECS world state through native JSON serialization.
- **Modify** the running engine via declarative JSON commands and, in Development Mode, via hot-reloaded C# scripts.
-The architecture prioritizes **production maturity** over experimental technologies: Vulkan (via Vortice.Vulkan), Flecs.NET (C# bindings for the C-based Flecs ECS), SDL3-cs (ppy.SDL3-CS), and ImGui.NET/Hexa.NET.ImGui with a native Vulkan backend.
+The architecture prioritizes **production maturity** over experimental technologies: Vulkan (via Silk.NET.Vulkan), Flecs.NET (C# bindings for the C-based Flecs ECS), SDL3-cs (ppy.SDL3-CS), and ImGui.NET/Hexa.NET.ImGui with a native Vulkan backend.
---
@@ -137,7 +137,7 @@ All Roslyn and `AssemblyLoadContext` code is wrapped in `#if DEV_MODE`.
**Flecs.NET**
-- NuGet: `Flecs.NET.Release` 4.0.3
+- NuGet: `Flecs.NET.Debug` (Debug) / `Flecs.NET.Release` (Release) 4.0.4-build.546
- High-level C# wrapper over the C-based Flecs ECS
- Supports .NET Standard 2.1, .NET 5/6/7/8
- NativeAOT-compatible via static linking: `true`
diff --git a/src/CortexEngine.App/CortexEngine.App.csproj b/src/CortexEngine.App/CortexEngine.App.csproj
index 65c8399..ad83476 100644
--- a/src/CortexEngine.App/CortexEngine.App.csproj
+++ b/src/CortexEngine.App/CortexEngine.App.csproj
@@ -11,6 +11,7 @@
DEV_MODE
+ linux-x64
diff --git a/src/CortexEngine.App/Program.cs b/src/CortexEngine.App/Program.cs
index 3efa5f1..760689b 100644
--- a/src/CortexEngine.App/Program.cs
+++ b/src/CortexEngine.App/Program.cs
@@ -1,6 +1,9 @@
using System;
+using System.Numerics;
using Engine.Core;
+using Engine.Core.Components;
using Engine.Graphics;
+using Flecs.NET.Core;
namespace CortexEngine.App;
@@ -8,17 +11,21 @@ class Program
{
static void Main(string[] args)
{
- Console.WriteLine("Cortex Engine Step 2 — Starting up...");
+ Console.WriteLine("Cortex Engine Step 3 — Starting up...");
try
{
- using var window = new Sdl3Window("Cortex Engine — Step 2", 1280, 720);
+ using var world = World.Create();
+ using var window = new Sdl3Window("Cortex Engine — Step 3", 1280, 720);
var timing = new Timing();
var input = new InputMapping();
using var vulkan = new VulkanContext(window, enableValidation: false);
using var swapchain = new Swapchain(vulkan);
using var renderer = new TriangleRenderer(vulkan, swapchain);
+ var triangle = world.Entity("Triangle")
+ .Set(new Transform(new Vector3(0.0f, 0.0f, 0.0f)));
+
var frames = 0;
var lastFpsTime = 0.0;
var lastWidth = window.Width;
@@ -39,7 +46,16 @@ class Program
swapchain.Recreate(lastWidth, lastHeight);
}
- renderer.RenderFrame();
+ // Animate the entity transform.
+ ref var transform = ref triangle.Ensure();
+ transform.Position = new Vector3(
+ MathF.Sin((float)timing.TotalTime) * 0.5f,
+ 0.0f,
+ 0.0f);
+ transform.Rotation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, (float)timing.TotalTime);
+ transform.Scale = Vector3.One * (1.0f + MathF.Sin((float)timing.TotalTime * 2.0f) * 0.2f);
+
+ renderer.RenderWorld(world);
frames++;
if (timing.TotalTime - lastFpsTime >= 1.0)
diff --git a/src/Engine.Core/Components/Transform.cs b/src/Engine.Core/Components/Transform.cs
new file mode 100644
index 0000000..5e8d5d9
--- /dev/null
+++ b/src/Engine.Core/Components/Transform.cs
@@ -0,0 +1,30 @@
+using System.Numerics;
+
+namespace Engine.Core.Components;
+
+///
+/// Basic 3D transform component for the ECS.
+/// Uses SIMD-friendly System.Numerics types.
+///
+public record struct Transform
+{
+ public Vector3 Position;
+ public Quaternion Rotation;
+ public Vector3 Scale;
+
+ public Transform(Vector3? position = null, Quaternion? rotation = null, Vector3? scale = null)
+ {
+ Position = position ?? Vector3.Zero;
+ Rotation = rotation ?? Quaternion.Identity;
+ Scale = scale ?? Vector3.One;
+ }
+
+ public static Transform Identity => new(Vector3.Zero, Quaternion.Identity, Vector3.One);
+
+ public Matrix4x4 GetMatrix()
+ {
+ return Matrix4x4.CreateScale(Scale)
+ * Matrix4x4.CreateFromQuaternion(Rotation)
+ * Matrix4x4.CreateTranslation(Position);
+ }
+}
diff --git a/src/Engine.Core/Engine.Core.csproj b/src/Engine.Core/Engine.Core.csproj
index 839b615..e76c22d 100644
--- a/src/Engine.Core/Engine.Core.csproj
+++ b/src/Engine.Core/Engine.Core.csproj
@@ -19,6 +19,8 @@
+
+
diff --git a/src/Engine.Core/InputMapping.cs b/src/Engine.Core/InputMapping.cs
index eaaef52..70d0a21 100644
--- a/src/Engine.Core/InputMapping.cs
+++ b/src/Engine.Core/InputMapping.cs
@@ -27,7 +27,7 @@ public sealed class InputMapping
public void ProcessEvent(SDL_Event evt)
{
- switch ((SDL_EventType)evt.Type)
+ switch ((SDL_EventType)evt.type)
{
case SDL_EventType.SDL_EVENT_KEY_DOWN:
if (!_keysDown.Contains((SDL_Keycode)evt.key.key))
diff --git a/src/Engine.Core/Sdl3Window.cs b/src/Engine.Core/Sdl3Window.cs
index 5ecf90a..78c271e 100644
--- a/src/Engine.Core/Sdl3Window.cs
+++ b/src/Engine.Core/Sdl3Window.cs
@@ -50,7 +50,7 @@ public sealed unsafe class Sdl3Window : IDisposable
SDL_Event evt;
while (SDL3.SDL_PollEvent(&evt))
{
- switch ((SDL_EventType)evt.Type)
+ switch ((SDL_EventType)evt.type)
{
case SDL_EventType.SDL_EVENT_QUIT:
ShouldClose = true;
diff --git a/src/Engine.Graphics/Engine.Graphics.csproj b/src/Engine.Graphics/Engine.Graphics.csproj
index 3f59fa0..53ce07f 100644
--- a/src/Engine.Graphics/Engine.Graphics.csproj
+++ b/src/Engine.Graphics/Engine.Graphics.csproj
@@ -20,6 +20,8 @@
+
+
diff --git a/src/Engine.Graphics/TriangleRenderer.cs b/src/Engine.Graphics/TriangleRenderer.cs
index 1437dae..6cbcd4b 100644
--- a/src/Engine.Graphics/TriangleRenderer.cs
+++ b/src/Engine.Graphics/TriangleRenderer.cs
@@ -1,12 +1,15 @@
using System;
+using System.Numerics;
+using Flecs.NET.Core;
using Silk.NET.Core;
using Silk.NET.Vulkan;
+using Engine.Core.Components;
namespace Engine.Graphics;
///
/// Renders a colored triangle using a vertex buffer and a simple graphics pipeline.
-/// Uses Silk.NET.Vulkan.
+/// Uses Silk.NET.Vulkan and reads entity transforms from the ECS world.
///
public sealed unsafe class TriangleRenderer : IDisposable
{
@@ -115,7 +118,7 @@ public sealed unsafe class TriangleRenderer : IDisposable
}
}
- public void RenderFrame()
+ public void RenderWorld(World world)
{
var frame = _currentFrame % 2;
@@ -160,7 +163,14 @@ public sealed unsafe class TriangleRenderer : IDisposable
var vertexBuffer = _vertexBuffer.Buffer;
var offset = 0ul;
_context.Vk.CmdBindVertexBuffers(cmd, 0, 1, &vertexBuffer, &offset);
- _context.Vk.CmdDraw(cmd, 3, 1, 0, 0);
+
+ var drawCmd = cmd;
+ world.Each((Entity e, ref Transform transform) =>
+ {
+ var bytes = BuildTriangleVertices(transform);
+ _vertexBuffer.Update(bytes);
+ _context.Vk.CmdDraw(drawCmd, 3, 1, 0, 0);
+ });
_context.Vk.CmdEndRenderPass(cmd);
_context.Vk.EndCommandBuffer(cmd);
@@ -197,6 +207,39 @@ public sealed unsafe class TriangleRenderer : IDisposable
_currentFrame++;
}
+ private byte[] BuildTriangleVertices(Transform transform)
+ {
+ var matrix = transform.GetMatrix();
+ var positions = new Vector3[]
+ {
+ new Vector3(0.0f, -0.5f, 0.0f),
+ new Vector3(0.5f, 0.5f, 0.0f),
+ new Vector3(-0.5f, 0.5f, 0.0f)
+ };
+ var colors = new[]
+ {
+ new Vector3(1.0f, 0.0f, 0.0f),
+ new Vector3(0.0f, 1.0f, 0.0f),
+ new Vector3(0.0f, 0.0f, 1.0f)
+ };
+
+ var bytes = new byte[3 * 5 * sizeof(float)];
+ fixed (byte* p = bytes)
+ {
+ var dst = (float*)p;
+ for (var i = 0; i < 3; i++)
+ {
+ var transformed = Vector3.Transform(positions[i], matrix);
+ dst[i * 5 + 0] = transformed.X;
+ dst[i * 5 + 1] = transformed.Y;
+ dst[i * 5 + 2] = colors[i].X;
+ dst[i * 5 + 3] = colors[i].Y;
+ dst[i * 5 + 4] = colors[i].Z;
+ }
+ }
+ return bytes;
+ }
+
public void Dispose()
{
_context.Vk.DeviceWaitIdle(_context.Device);
diff --git a/src/Engine.Graphics/VertexBuffer.cs b/src/Engine.Graphics/VertexBuffer.cs
index a840cef..b58d353 100644
--- a/src/Engine.Graphics/VertexBuffer.cs
+++ b/src/Engine.Graphics/VertexBuffer.cs
@@ -102,6 +102,14 @@ public sealed unsafe class VertexBuffer : IDisposable
_context.Vk.UnmapMemory(_context.Device, Memory);
}
+ public void Update(ReadOnlySpan data)
+ {
+ if ((ulong)data.Length != Size)
+ throw new ArgumentException($"Vertex buffer update size mismatch: {data.Length} != {Size}");
+
+ CopyData(data);
+ }
+
public void Dispose()
{
_context.Vk.DeviceWaitIdle(_context.Device);