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, }