diff --git a/LinguaEngine.sln b/LinguaEngine.sln
index 83d01ab..10f9391 100644
--- a/LinguaEngine.sln
+++ b/LinguaEngine.sln
@@ -49,6 +49,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Engine.Assets", "plugins\en
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Engine.Assets.Tests", "tests\Engine.Assets.Tests\Engine.Assets.Tests.csproj", "{AFAB0A32-2A70-4BCA-8561-5F94B195DFAB}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "engine.editor", "engine.editor", "{8067C66D-9D49-192B-E4C1-1C024401ACE3}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Engine.Editor.Contracts", "plugins\engine.editor\Engine.Editor.Contracts\Engine.Editor.Contracts.csproj", "{6A2943F1-1984-470D-BD74-C6E16BB77241}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Engine.Editor", "plugins\engine.editor\Engine.Editor\Engine.Editor.csproj", "{B60A3933-923C-4339-98D1-E4C87A76E80B}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -239,6 +245,30 @@ Global
{AFAB0A32-2A70-4BCA-8561-5F94B195DFAB}.Release|x64.Build.0 = Release|Any CPU
{AFAB0A32-2A70-4BCA-8561-5F94B195DFAB}.Release|x86.ActiveCfg = Release|Any CPU
{AFAB0A32-2A70-4BCA-8561-5F94B195DFAB}.Release|x86.Build.0 = Release|Any CPU
+ {6A2943F1-1984-470D-BD74-C6E16BB77241}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6A2943F1-1984-470D-BD74-C6E16BB77241}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6A2943F1-1984-470D-BD74-C6E16BB77241}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {6A2943F1-1984-470D-BD74-C6E16BB77241}.Debug|x64.Build.0 = Debug|Any CPU
+ {6A2943F1-1984-470D-BD74-C6E16BB77241}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {6A2943F1-1984-470D-BD74-C6E16BB77241}.Debug|x86.Build.0 = Debug|Any CPU
+ {6A2943F1-1984-470D-BD74-C6E16BB77241}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6A2943F1-1984-470D-BD74-C6E16BB77241}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6A2943F1-1984-470D-BD74-C6E16BB77241}.Release|x64.ActiveCfg = Release|Any CPU
+ {6A2943F1-1984-470D-BD74-C6E16BB77241}.Release|x64.Build.0 = Release|Any CPU
+ {6A2943F1-1984-470D-BD74-C6E16BB77241}.Release|x86.ActiveCfg = Release|Any CPU
+ {6A2943F1-1984-470D-BD74-C6E16BB77241}.Release|x86.Build.0 = Release|Any CPU
+ {B60A3933-923C-4339-98D1-E4C87A76E80B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B60A3933-923C-4339-98D1-E4C87A76E80B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B60A3933-923C-4339-98D1-E4C87A76E80B}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {B60A3933-923C-4339-98D1-E4C87A76E80B}.Debug|x64.Build.0 = Debug|Any CPU
+ {B60A3933-923C-4339-98D1-E4C87A76E80B}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {B60A3933-923C-4339-98D1-E4C87A76E80B}.Debug|x86.Build.0 = Debug|Any CPU
+ {B60A3933-923C-4339-98D1-E4C87A76E80B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B60A3933-923C-4339-98D1-E4C87A76E80B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B60A3933-923C-4339-98D1-E4C87A76E80B}.Release|x64.ActiveCfg = Release|Any CPU
+ {B60A3933-923C-4339-98D1-E4C87A76E80B}.Release|x64.Build.0 = Release|Any CPU
+ {B60A3933-923C-4339-98D1-E4C87A76E80B}.Release|x86.ActiveCfg = Release|Any CPU
+ {B60A3933-923C-4339-98D1-E4C87A76E80B}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -264,5 +294,8 @@ Global
{1F6F4B27-5624-42F8-87A7-A4729FB198E3} = {BBD4296A-8D59-75AB-1261-7E1302A19746}
{8C2DE8CB-3207-4990-8F6D-87CF31D6CF15} = {BBD4296A-8D59-75AB-1261-7E1302A19746}
{AFAB0A32-2A70-4BCA-8561-5F94B195DFAB} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
+ {8067C66D-9D49-192B-E4C1-1C024401ACE3} = {07D57EEB-2F50-60C4-C011-FE4FA775C9A8}
+ {6A2943F1-1984-470D-BD74-C6E16BB77241} = {8067C66D-9D49-192B-E4C1-1C024401ACE3}
+ {B60A3933-923C-4339-98D1-E4C87A76E80B} = {8067C66D-9D49-192B-E4C1-1C024401ACE3}
EndGlobalSection
EndGlobal
diff --git a/plugins/engine.editor/Engine.Editor.Contracts/Engine.Editor.Contracts.csproj b/plugins/engine.editor/Engine.Editor.Contracts/Engine.Editor.Contracts.csproj
new file mode 100644
index 0000000..bb93c00
--- /dev/null
+++ b/plugins/engine.editor/Engine.Editor.Contracts/Engine.Editor.Contracts.csproj
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/plugins/engine.editor/Engine.Editor/EditorPlugin.cs b/plugins/engine.editor/Engine.Editor/EditorPlugin.cs
new file mode 100644
index 0000000..5312b5d
--- /dev/null
+++ b/plugins/engine.editor/Engine.Editor/EditorPlugin.cs
@@ -0,0 +1,69 @@
+using Engine.Input.Contracts;
+using Engine.Kernel.Diagnostics;
+using Engine.Kernel.Plugins;
+using Engine.Kernel.Scheduling;
+using Engine.Kernel.World;
+using Engine.Windowing.Contracts;
+using ImGuiNET;
+using Silk.NET.OpenGL;
+using Silk.NET.OpenGL.Extensions.ImGui;
+
+namespace Engine.Editor;
+
+///
+/// M3's editor shell: an ImGui overlay drawn on top of the running scene.
+/// Registers on Stage.Render, after engine.render's own Draw (per
+/// project.json's plugin order) so it paints into the same back buffer the
+/// scene just drew into — and before Stage.Present's SwapBuffers, so the
+/// UI isn't delayed a frame. See Stage.Present's doc comment for why that
+/// split exists at all.
+///
+/// Nothing here reads or writes a Component through GetComponent/Query —
+/// the Hierarchy and Inspector panels (not built yet) will walk
+/// IWorld.Roots and GameObject.Components directly instead, which is why
+/// this plugin never needs to declare Reads/Writes on its system: those
+/// checks only guard the typed accessors, not plain property reads. See
+/// GameObject.AddComponent's own doc comment on the same gap.
+///
+public sealed class EditorPlugin : IPlugin
+{
+ private GL? _gl;
+ private ImGuiController? _controller;
+ private ITime? _time;
+
+ public void Configure(IPluginContext ctx)
+ {
+ var window = ctx.Services.Require();
+ var input = ctx.Services.Require();
+ _time = ctx.Time;
+
+ window.Native.GLContext!.MakeCurrent();
+ _gl = window.Native.CreateOpenGL();
+ _controller = new ImGuiController(_gl, window.Native, input.Native);
+
+ ctx.Schedule.Add(Stage.Render, DrawUi);
+ ctx.Log.Info("editor UI ready (ImGui)");
+ }
+
+ public void Shutdown(IPluginContext ctx)
+ {
+ ctx.Schedule.RemoveAllFrom("engine.editor");
+ _controller?.Dispose();
+ _gl?.Dispose();
+ _controller = null;
+ _gl = null;
+ _time = null;
+ }
+
+ private void DrawUi(IWorld world)
+ {
+ _controller!.Update(_time!.DeltaTime);
+
+ ImGui.Begin("Lingua Editor");
+ ImGui.Text($"FPS: {1f / MathF.Max(_time.DeltaTime, 0.0001f):F0}");
+ ImGui.Text($"Frame: {_time.FrameCount}");
+ ImGui.End();
+
+ _controller.Render();
+ }
+}
diff --git a/plugins/engine.editor/Engine.Editor/Engine.Editor.csproj b/plugins/engine.editor/Engine.Editor/Engine.Editor.csproj
new file mode 100644
index 0000000..749efee
--- /dev/null
+++ b/plugins/engine.editor/Engine.Editor/Engine.Editor.csproj
@@ -0,0 +1,23 @@
+
+
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/plugins/engine.editor/plugin.json b/plugins/engine.editor/plugin.json
new file mode 100644
index 0000000..c964ec1
--- /dev/null
+++ b/plugins/engine.editor/plugin.json
@@ -0,0 +1,12 @@
+{
+ "id": "engine.editor",
+ "version": "0.1.0",
+ "contracts": "Engine.Editor.Contracts.dll",
+ "assembly": "Engine.Editor.dll",
+ "dependsOn": {
+ "engine.windowing": "^0.1",
+ "engine.input": "^0.1",
+ "engine.render": "^0.1"
+ },
+ "reloadable": true
+}
diff --git a/plugins/engine.input/Engine.Input.Contracts/IEngineInput.cs b/plugins/engine.input/Engine.Input.Contracts/IEngineInput.cs
index 711471b..847d405 100644
--- a/plugins/engine.input/Engine.Input.Contracts/IEngineInput.cs
+++ b/plugins/engine.input/Engine.Input.Contracts/IEngineInput.cs
@@ -12,4 +12,12 @@ namespace Engine.Input.Contracts;
public interface IEngineInput
{
bool IsKeyDown(Key key);
+
+ ///
+ /// The raw Silk.NET input context, same escape hatch as
+ /// IEngineWindow.Native — needed here specifically so engine.editor can
+ /// construct Silk.NET.OpenGL.Extensions.ImGui's ImGuiController, which
+ /// takes an IInputContext directly rather than anything of ours.
+ ///
+ IInputContext Native { get; }
}
diff --git a/plugins/engine.input/Engine.Input/SilkEngineInput.cs b/plugins/engine.input/Engine.Input/SilkEngineInput.cs
index f57b160..eab9d09 100644
--- a/plugins/engine.input/Engine.Input/SilkEngineInput.cs
+++ b/plugins/engine.input/Engine.Input/SilkEngineInput.cs
@@ -6,4 +6,6 @@ namespace Engine.Input;
internal sealed class SilkEngineInput(IInputContext context) : IEngineInput
{
public bool IsKeyDown(Key key) => context.Keyboards.Any(k => k.IsKeyPressed(key));
+
+ public IInputContext Native => context;
}
diff --git a/plugins/engine.render/Engine.Render/RenderPlugin.cs b/plugins/engine.render/Engine.Render/RenderPlugin.cs
index 2a92888..4ae757f 100644
--- a/plugins/engine.render/Engine.Render/RenderPlugin.cs
+++ b/plugins/engine.render/Engine.Render/RenderPlugin.cs
@@ -160,6 +160,7 @@ public sealed class RenderPlugin : IPlugin
ctx.Services.Provide(new GlScreenCapture(_gl, _window));
ctx.Schedule.Add(Stage.Render, Draw).Reads();
+ ctx.Schedule.Add(Stage.Present, Present);
ctx.Log.Info("GL context created, 3D quad pipeline ready");
}
@@ -236,10 +237,14 @@ public sealed class RenderPlugin : IPlugin
SetMatrix(_modelLocation, go.WorldMatrix);
_gl.DrawArrays(PrimitiveType.Triangles, 0, 6);
}
-
- _window!.Native.GLContext!.SwapBuffers();
}
+ // Split from Draw() into its own Stage.Present system so engine.editor
+ // can draw ImGui on top of the scene from a Stage.Render system of its
+ // own (registered after this plugin's, per project.json order) without
+ // racing the swap — see Stage.Present's doc comment.
+ private void Present(IWorld world) => _window!.Native.GLContext!.SwapBuffers();
+
private unsafe void SetMatrix(int location, Matrix4x4 matrix) =>
_gl!.UniformMatrix4(location, 1, true, (float*)&matrix);
diff --git a/samples/WindowDemo/project.json b/samples/WindowDemo/project.json
index 2e298c2..cd4cb16 100644
--- a/samples/WindowDemo/project.json
+++ b/samples/WindowDemo/project.json
@@ -4,7 +4,8 @@
{ "id": "engine.windowing" },
{ "id": "engine.assets" },
{ "id": "engine.render" },
- { "id": "engine.input" }
+ { "id": "engine.input" },
+ { "id": "engine.editor" }
],
"pluginPaths": []
}
diff --git a/src/Engine.Host/Program.cs b/src/Engine.Host/Program.cs
index c69b529..edbe551 100644
--- a/src/Engine.Host/Program.cs
+++ b/src/Engine.Host/Program.cs
@@ -244,6 +244,7 @@ if (windowed)
schedule.RunStage(Stage.Update, world);
schedule.RunStage(Stage.Render, world);
+ schedule.RunStage(Stage.Present, world);
foreach (var path in pendingScreenshots)
{
diff --git a/src/Engine.Kernel/Scheduling/Stage.cs b/src/Engine.Kernel/Scheduling/Stage.cs
index 0f54625..bbe54b2 100644
--- a/src/Engine.Kernel/Scheduling/Stage.cs
+++ b/src/Engine.Kernel/Scheduling/Stage.cs
@@ -1,12 +1,19 @@
namespace Engine.Kernel.Scheduling;
///
-/// Open question (see the footer of docs/kernel-contract.md): whether the
-/// set of stages is fixed or plugin-extensible. This is only enough to
-/// make the §3 example compile — not a decision.
+/// Fixed, kernel-defined, not plugin-extensible — see "Frame stages" in
+/// docs/kernel-contract.md §8. is the second addition
+/// to the original {Update, Render} set (after FixedUpdate was scoped for
+/// M4), added for M3's editor: engine.render splits its old single
+/// Clear+Draw+SwapBuffers system into a Render-stage draw and a
+/// Present-stage swap specifically so engine.editor's ImGui overlay — which
+/// has to run after the scene is drawn but before the buffers swap — has
+/// somewhere to register that isn't a race against load order. Not a stage
+/// added speculatively: SwapBuffers already needed to move somewhere real.
///
public enum Stage
{
Update,
Render,
+ Present,
}