diff --git a/.gitmodules b/.gitmodules index 110dc50..63fa691 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,3 +13,6 @@ [submodule "Nerfed.Runtime/Libraries/dav1dfile"] path = Nerfed.Runtime/Libraries/dav1dfile url = https://github.com/MoonsideGames/dav1dfile.git +[submodule "Nerfed.Runtime/Libraries/ImGui.NET"] + path = Nerfed.Runtime/Libraries/ImGui.NET + url = https://github.com/ImGuiNET/ImGui.NET.git diff --git a/Nerfed.Runtime/Assets/Shaders/imgui-frag.spv b/Nerfed.Runtime/Assets/Shaders/imgui-frag.spv new file mode 100644 index 0000000..550f9a0 Binary files /dev/null and b/Nerfed.Runtime/Assets/Shaders/imgui-frag.spv differ diff --git a/Nerfed.Runtime/Assets/Shaders/imgui-vertex.spv b/Nerfed.Runtime/Assets/Shaders/imgui-vertex.spv new file mode 100644 index 0000000..a63373f Binary files /dev/null and b/Nerfed.Runtime/Assets/Shaders/imgui-vertex.spv differ diff --git a/Nerfed.Runtime/Engine.cs b/Nerfed.Runtime/Engine.cs index b714d2c..486b151 100644 --- a/Nerfed.Runtime/Engine.cs +++ b/Nerfed.Runtime/Engine.cs @@ -1,7 +1,7 @@ -using System.Diagnostics; using Nerfed.Runtime.Audio; using Nerfed.Runtime.Graphics; using SDL2; +using System.Diagnostics; namespace Nerfed.Runtime; @@ -39,6 +39,8 @@ public static class Engine private const string WindowTitle = "Nerfed"; //.. + private static Gui.Controller Controller; + internal static void Run(string[] args) { Timestep = TimeSpan.FromTicks(TimeSpan.TicksPerSecond / TargetTimestep); @@ -65,11 +67,17 @@ internal static void Run(string[] args) AudioDevice = new AudioDevice(); + Controller = new Gui.Controller(GraphicsDevice, MainWindow, Color.DarkOliveGreen); + Controller.OnGui += () => { + ImGuiNET.ImGui.ShowDemoWindow(); + }; + while (!quit) { Tick(); } + Controller.Dispose(); GraphicsDevice.UnclaimWindow(MainWindow); MainWindow.Dispose(); GraphicsDevice.Dispose(); @@ -145,6 +153,7 @@ private static void Tick() ProcessSDLEvents(); + Controller.Update((float)Timestep.TotalSeconds); // Tick game here... AudioDevice.WakeThread(); @@ -154,6 +163,7 @@ private static void Tick() double alpha = accumulatedUpdateTime / Timestep; // Render here.. + Controller.Render(); accumulatedDrawTime -= framerateCapTimeSpan; } diff --git a/Nerfed.Runtime/Gui/Clipboard.cs b/Nerfed.Runtime/Gui/Clipboard.cs new file mode 100644 index 0000000..ddb883f --- /dev/null +++ b/Nerfed.Runtime/Gui/Clipboard.cs @@ -0,0 +1,48 @@ +using System.Runtime.InteropServices; +using System.Text; + +namespace Nerfed.Runtime.Gui; + +public static unsafe class Clipboard +{ + private static IntPtr clipboard; + private static readonly Dictionary pinned = new(); + + private static unsafe void Set(void* userdata, byte* text) + { + int len = 0; while (text[len] != 0) len++; + string str = Encoding.UTF8.GetString(text, len); + SDL2.SDL.SDL_SetClipboardText(str); + } + + private static unsafe byte* Get(void* userdata) + { + if (clipboard != IntPtr.Zero) + { + NativeMemory.Free((void*) clipboard); + clipboard = IntPtr.Zero; + } + + string str = SDL2.SDL.SDL_GetClipboardText(); + int length = Encoding.UTF8.GetByteCount(str); + byte* bytes = (byte*)(clipboard = (nint)NativeMemory.Alloc((nuint)(length + 1))); + + Encoding.UTF8.GetBytes(str, new Span(bytes, length)); + bytes[length] = 0; + return bytes; + } + + // Stops the delegate pointer from being collected + private static IntPtr GetPointerTo(T fn) where T : Delegate + { + if (pinned.TryGetValue(fn, out nint ptr)) + return ptr; + + ptr = Marshal.GetFunctionPointerForDelegate(fn); + pinned.Add(fn, ptr); + return ptr; + } + + public static readonly IntPtr GetFnPtr = GetPointerTo(Get); + public static readonly IntPtr SetFnPtr = GetPointerTo(Set); +} \ No newline at end of file diff --git a/Nerfed.Runtime/Gui/Controller.cs b/Nerfed.Runtime/Gui/Controller.cs new file mode 100644 index 0000000..6d4990d --- /dev/null +++ b/Nerfed.Runtime/Gui/Controller.cs @@ -0,0 +1,661 @@ +// ImGuiController with docking and viewport support for MoonWorks/Refresh. +// Based on the example im ImGui.NET and MoonWorksDearImGuiScaffold. + +using ImGuiNET; +using Nerfed.Runtime.Graphics; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Nerfed.Runtime.Gui; + +internal class Controller : IDisposable +{ + public event Action OnGui; + + private readonly string shaderContentPath = Path.Combine(System.AppContext.BaseDirectory, "Assets", "Shaders"); + + private readonly GraphicsDevice graphicsDevice; + private readonly Window mainWindow; + private readonly Color clearColor; + + private readonly Platform_CreateWindow createWindow; + private readonly Platform_DestroyWindow destroyWindow; + private readonly Platform_GetWindowPos getWindowPos; + private readonly Platform_ShowWindow showWindow; + private readonly Platform_SetWindowPos setWindowPos; + private readonly Platform_SetWindowSize setWindowSize; + private readonly Platform_GetWindowSize getWindowSize; + private readonly Platform_SetWindowFocus setWindowFocus; + private readonly Platform_GetWindowFocus getWindowFocus; + private readonly Platform_GetWindowMinimized getWindowMinimized; + private readonly Platform_SetWindowTitle setWindowTitle; + + private readonly ResourceUploader resourceUploader; + private readonly GraphicsPipeline imGuiPipeline; + private readonly Shader imGuiVertexShader; + private readonly Shader imGuiFragmentShader; + private readonly Sampler imGuiSampler; + private readonly TextureStorage textureStorage = new TextureStorage(); + private readonly Dictionary windows = new Dictionary(16); + + private Texture fontTexture = null; + private uint vertexCount = 0; + private uint indexCount = 0; + private Graphics.Buffer imGuiVertexBuffer = null; + private Graphics.Buffer imGuiIndexBuffer = null; + private bool frameBegun = false; + + public Controller(GraphicsDevice graphicsDevice, Window mainWindow, Color clearColor, ImGuiConfigFlags configFlags = ImGuiConfigFlags.NavEnableKeyboard | ImGuiConfigFlags.DockingEnable | ImGuiConfigFlags.ViewportsEnable) + { + this.mainWindow = mainWindow; + this.graphicsDevice = graphicsDevice; + this.clearColor = clearColor; + + resourceUploader = new ResourceUploader(graphicsDevice); + + ImGui.CreateContext(); + + ImGuiIOPtr io = ImGui.GetIO(); + io.DisplaySize = new Vector2(mainWindow.Width, mainWindow.Height); + io.DisplayFramebufferScale = Vector2.One; + + ShaderCreateInfo vertexCreateInfo = new ShaderCreateInfo { + ShaderStage = ShaderStage.Vertex, + ShaderFormat = ShaderFormat.SPIRV, + UniformBufferCount = 1, + }; + imGuiVertexShader = new Shader(graphicsDevice, Path.Combine(shaderContentPath, "imgui-vertex.spv"), "main", in vertexCreateInfo); + ShaderCreateInfo fragCreateInfo = new ShaderCreateInfo { + ShaderStage = ShaderStage.Fragment, + ShaderFormat = ShaderFormat.SPIRV, + SamplerCount = 1, + + }; + imGuiFragmentShader = new Shader(graphicsDevice, Path.Combine(shaderContentPath, "imgui-frag.spv"), "main", in fragCreateInfo); + + imGuiSampler = new Sampler(graphicsDevice, SamplerCreateInfo.LinearClamp); + + imGuiPipeline = new GraphicsPipeline( + graphicsDevice, + new GraphicsPipelineCreateInfo + { + AttachmentInfo = new GraphicsPipelineAttachmentInfo( + new ColorAttachmentDescription( + mainWindow.SwapchainFormat, + ColorAttachmentBlendState.NonPremultiplied + ) + ), + DepthStencilState = DepthStencilState.Disable, + MultisampleState = MultisampleState.None, + PrimitiveType = PrimitiveType.TriangleList, + RasterizerState = RasterizerState.CW_CullNone, + VertexInputState = VertexInputState.CreateSingleBinding(), + VertexShader = imGuiVertexShader, + FragmentShader = imGuiFragmentShader, + } + ); + + BuildFontAtlas(); + + io.ConfigFlags = configFlags; + //io.MouseDrawCursor = true; + + if (!OperatingSystem.IsWindows()) + { + io.SetClipboardTextFn = Clipboard.SetFnPtr; + io.GetClipboardTextFn = Clipboard.GetFnPtr; + } + + ImGuiPlatformIOPtr platformIO = ImGui.GetPlatformIO(); + ImGuiViewportPtr mainViewport = platformIO.Viewports[0]; + mainViewport.PlatformHandle = mainWindow.Handle; + GCHandle handle = GCHandle.Alloc(mainWindow); + mainViewport.PlatformUserData = (IntPtr)handle; + windows.Add(mainWindow, handle); + + unsafe + { + createWindow = CreateWindow; + destroyWindow = DestroyWindow; + getWindowPos = GetWindowPos; + showWindow = ShowWindow; + setWindowPos = SetWindowPos; + setWindowSize = SetWindowSize; + getWindowSize = GetWindowSize; + setWindowFocus = SetWindowFocus; + getWindowFocus = GetWindowFocus; + getWindowMinimized = GetWindowMinimized; + setWindowTitle = SetWindowTitle; + + platformIO.Platform_CreateWindow = Marshal.GetFunctionPointerForDelegate(createWindow); + platformIO.Platform_DestroyWindow = Marshal.GetFunctionPointerForDelegate(destroyWindow); + platformIO.Platform_ShowWindow = Marshal.GetFunctionPointerForDelegate(showWindow); + platformIO.Platform_SetWindowPos = Marshal.GetFunctionPointerForDelegate(setWindowPos); + platformIO.Platform_SetWindowSize = Marshal.GetFunctionPointerForDelegate(setWindowSize); + platformIO.Platform_SetWindowFocus = Marshal.GetFunctionPointerForDelegate(setWindowFocus); + platformIO.Platform_GetWindowFocus = Marshal.GetFunctionPointerForDelegate(getWindowFocus); + platformIO.Platform_GetWindowMinimized = Marshal.GetFunctionPointerForDelegate(getWindowMinimized); + platformIO.Platform_SetWindowTitle = Marshal.GetFunctionPointerForDelegate(setWindowTitle); + + ImGuiNative.ImGuiPlatformIO_Set_Platform_GetWindowPos(platformIO.NativePtr, Marshal.GetFunctionPointerForDelegate(getWindowPos)); + ImGuiNative.ImGuiPlatformIO_Set_Platform_GetWindowSize(platformIO.NativePtr, Marshal.GetFunctionPointerForDelegate(getWindowSize)); + } + + //io.BackendFlags |= ImGuiBackendFlags.HasMouseCursors; + io.BackendFlags |= ImGuiBackendFlags.HasSetMousePos; + io.BackendFlags |= ImGuiBackendFlags.PlatformHasViewports; + io.BackendFlags |= ImGuiBackendFlags.RendererHasViewports; + } + + public void Update(float deltaTime) + { + if (frameBegun) + { + ImGui.Render(); + ImGui.UpdatePlatformWindows(); + } + + UpdatePerFrameImGuiData(deltaTime); + UpdateInput(); + UpdateMonitors(); + + frameBegun = true; + ImGui.NewFrame(); + + OnGui?.Invoke(); + + { // Debug + ImGuiIOPtr io = ImGui.GetIO(); + ImGui.Text($"mouse pos: {io.MousePos}"); + } + + ImGui.EndFrame(); + } + + private void UpdatePerFrameImGuiData(float deltaSeconds) + { + ImGuiIOPtr io = ImGui.GetIO(); + io.DisplaySize = new Vector2(mainWindow.Width, mainWindow.Height); + io.DisplayFramebufferScale = new Vector2(1, 1); + io.DeltaTime = deltaSeconds; // DeltaTime is in seconds. + } + + private void UpdateInput() + { + ImGuiIOPtr io = ImGui.GetIO(); + + if ((ImGui.GetIO().ConfigFlags & ImGuiConfigFlags.ViewportsEnable) != 0) + { + // For viewports we use the global mouse position. + _ = SDL2.SDL.SDL_GetGlobalMouseState(out int x, out int y); + io.MousePos = new Vector2(x, y); + } + else + { + // Without viewports we need to use the relative position. + //_ = SDL2.SDL.SDL_GetMouseState(out int x, out int y); + io.MousePos = Mouse.Position; + } + + io.MouseDown[0] = Mouse.IsButtonDown(MouseButton.Left); + io.MouseDown[1] = Mouse.IsButtonDown(MouseButton.Right); + io.MouseDown[2] = Mouse.IsButtonDown(MouseButton.Middle); + + io.MouseWheel = Mouse.GetWheel(); + + //io.AddKeyEvent(ImGuiKey.A, Keyboard.IsKeyDown(Key.A)); + //io.AddKeyEvent(ImGuiKey.Z, Keyboard.IsKeyDown(Key.Z)); + //io.AddKeyEvent(ImGuiKey.Y, Keyboard.IsKeyDown(Key.Y)); + //io.AddKeyEvent(ImGuiKey.X, Keyboard.IsKeyDown(Key.X)); + //io.AddKeyEvent(ImGuiKey.C, Keyboard.IsKeyDown(Key.C)); + //io.AddKeyEvent(ImGuiKey.V, Keyboard.IsKeyDown(Key.V)); + + //io.AddKeyEvent(ImGuiKey.Tab, Keyboard.IsKeyDown(Key.Tab)); + //io.AddKeyEvent(ImGuiKey.LeftArrow, Keyboard.IsKeyDown(Key.Left)); + //io.AddKeyEvent(ImGuiKey.RightArrow, Keyboard.IsKeyDown(Key.Right)); + //io.AddKeyEvent(ImGuiKey.UpArrow, Keyboard.IsKeyDown(Key.Up)); + //io.AddKeyEvent(ImGuiKey.DownArrow, Keyboard.IsKeyDown(Key.Down)); + //io.AddKeyEvent(ImGuiKey.Enter, Keyboard.IsKeyDown(Key.Enter)); + //io.AddKeyEvent(ImGuiKey.Escape, Keyboard.IsKeyDown(Key.Escape)); + //io.AddKeyEvent(ImGuiKey.Delete, Keyboard.IsKeyDown(Key.Delete)); + //io.AddKeyEvent(ImGuiKey.Backspace, Keyboard.IsKeyDown(Key.Backspace)); + //io.AddKeyEvent(ImGuiKey.Home, Keyboard.IsKeyDown(Key.Home)); + //io.AddKeyEvent(ImGuiKey.End, Keyboard.IsKeyDown(Key.End)); + //io.AddKeyEvent(ImGuiKey.PageDown, Keyboard.IsKeyDown(Key.PageDown)); + //io.AddKeyEvent(ImGuiKey.PageUp, Keyboard.IsKeyDown(Key.PageUp)); + //io.AddKeyEvent(ImGuiKey.Insert, Keyboard.IsKeyDown(Key.Insert)); + + //io.AddKeyEvent(ImGuiKey.ModCtrl, Keyboard.IsKeyDown(Key.LeftControl) || Keyboard.IsKeyDown(Key.RightControl)); + //io.AddKeyEvent(ImGuiKey.ModShift, Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift)); + //io.AddKeyEvent(ImGuiKey.ModAlt, Keyboard.IsKeyDown(Key.LeftAlt) || Keyboard.IsKeyDown(Key.RightAlt)); + //io.AddKeyEvent(ImGuiKey.ModSuper, Keyboard.IsKeyDown(Key.LeftSuper) || Keyboard.IsKeyDown(Key.RightSuper)); + + //ReadOnlySpan input = Keyboard.GetTextInput(); + //if (!input.IsEmpty) + //{ + // foreach (char c in input) + // { + // if (c == '\t') + // { + // break; + // } + + // io.AddInputCharacter(c); + // } + //} + } + + private unsafe void UpdateMonitors() + { + ImGuiPlatformIOPtr platformIO = ImGui.GetPlatformIO(); + Marshal.FreeHGlobal(platformIO.NativePtr->Monitors.Data); + int videoDisplayCount = SDL2.SDL.SDL_GetNumVideoDisplays(); + IntPtr data = Marshal.AllocHGlobal(Unsafe.SizeOf() * videoDisplayCount); + platformIO.NativePtr->Monitors = new ImVector(videoDisplayCount, videoDisplayCount, data); + + for (int i = 0; i < videoDisplayCount; i++) + { + _ = SDL2.SDL.SDL_GetDisplayUsableBounds(i, out SDL2.SDL.SDL_Rect usableBounds); + _ = SDL2.SDL.SDL_GetDisplayBounds(i, out SDL2.SDL.SDL_Rect bounds); + _ = SDL2.SDL.SDL_GetDisplayDPI(i, out float ddpi, out float hdpi, out float vdpi); + ImGuiPlatformMonitorPtr monitor = platformIO.Monitors[i]; + float standardDpi = 96f; // Standard DPI typically used + monitor.DpiScale = hdpi / standardDpi; + monitor.MainPos = new Vector2(bounds.x, bounds.y); + monitor.MainSize = new Vector2(bounds.w, bounds.h); + monitor.WorkPos = new Vector2(usableBounds.x, usableBounds.y); + monitor.WorkSize = new Vector2(usableBounds.w, usableBounds.h); + } + } + + public void Render() + { + if (!frameBegun) + { + return; + } + + frameBegun = false; + + if ((ImGui.GetIO().ConfigFlags & ImGuiConfigFlags.ViewportsEnable) != 0) + { + ImGui.Render(); + ImGui.UpdatePlatformWindows(); + ImGuiPlatformIOPtr platformIO = ImGui.GetPlatformIO(); + + for (int i = 0; i < platformIO.Viewports.Size; i++) + { + ImGuiViewportPtr vp = platformIO.Viewports[i]; + Window window = (Window)GCHandle.FromIntPtr(vp.PlatformUserData).Target; + + if (!window.Claimed) + { + continue; + } + + UpdateImGuiBuffers(vp.DrawData); + + CommandBuffer commandBuffer = graphicsDevice.AcquireCommandBuffer(); + Texture swapchainTexture = commandBuffer.AcquireSwapchainTexture(window); + + if (swapchainTexture != null) + { + RenderCommandLists(commandBuffer, swapchainTexture, vp.DrawData); + graphicsDevice.Submit(commandBuffer); + graphicsDevice.Wait(); + } + } + } + else + { + ImGui.Render(); + + if (!mainWindow.Claimed) + { + return; + } + + ImDrawDataPtr drawDataPtr = ImGui.GetDrawData(); + UpdateImGuiBuffers(drawDataPtr); + + CommandBuffer commandBuffer = graphicsDevice.AcquireCommandBuffer(); + Texture swapchainTexture = commandBuffer.AcquireSwapchainTexture(mainWindow); + + if (swapchainTexture != null) + { + RenderCommandLists(commandBuffer, swapchainTexture, drawDataPtr); + graphicsDevice.Submit(commandBuffer); + graphicsDevice.Wait(); + } + } + } + + private unsafe void UpdateImGuiBuffers(ImDrawDataPtr drawDataPtr) + { + if (drawDataPtr.TotalVtxCount == 0 || drawDataPtr.CmdListsCount == 0) + { + return; + } + + if (drawDataPtr.TotalVtxCount > vertexCount) + { + imGuiVertexBuffer?.Dispose(); + + vertexCount = (uint)(drawDataPtr.TotalVtxCount * 1.5f); + imGuiVertexBuffer = Graphics.Buffer.Create( + graphicsDevice, + BufferUsageFlags.Vertex, + vertexCount + ); + } + + if (drawDataPtr.TotalIdxCount > indexCount) + { + imGuiIndexBuffer?.Dispose(); + + indexCount = (uint)(drawDataPtr.TotalIdxCount * 1.5f); + imGuiIndexBuffer = Graphics.Buffer.Create( + graphicsDevice, + BufferUsageFlags.Index, + indexCount + ); + } + + uint vertexOffset = 0; + uint indexOffset = 0; + + for (int n = 0; n < drawDataPtr.CmdListsCount; n++) + { + ImDrawListPtr cmdList = drawDataPtr.CmdLists[n]; + + resourceUploader.SetBufferData( + imGuiVertexBuffer, + vertexOffset, + new Span(cmdList.VtxBuffer.Data.ToPointer(), cmdList.VtxBuffer.Size), + n == 0 + ); + + resourceUploader.SetBufferData( + imGuiIndexBuffer, + indexOffset, + new Span(cmdList.IdxBuffer.Data.ToPointer(), cmdList.IdxBuffer.Size), + n == 0 + ); + + vertexOffset += (uint)cmdList.VtxBuffer.Size; + indexOffset += (uint)cmdList.IdxBuffer.Size; + } + + resourceUploader.Upload(); + } + + private void RenderCommandLists(CommandBuffer commandBuffer, Texture renderTexture, ImDrawDataPtr drawDataPtr) + { + Vector2 pos = drawDataPtr.DisplayPos; + + RenderPass renderPass = commandBuffer.BeginRenderPass( + new ColorAttachmentInfo(renderTexture, false, clearColor) + ); + + renderPass.BindGraphicsPipeline(imGuiPipeline); + + // It is possible that the buffers are null (for example nothing is in our main windows viewport, then we exixt early but still clear it). + if (imGuiVertexBuffer == null || imGuiIndexBuffer == null) + { + commandBuffer.EndRenderPass(renderPass); + return; + } + + Matrix4x4 projectionMatrix = Matrix4x4.CreateOrthographicOffCenter( + pos.X, + pos.X + drawDataPtr.DisplaySize.X, + pos.Y + drawDataPtr.DisplaySize.Y, + pos.Y, + -1.0f, + 1.0f + ); + TransformVertexUniform vertexUniform = new TransformVertexUniform(projectionMatrix); + + renderPass.BindVertexBuffer(imGuiVertexBuffer); + renderPass.BindIndexBuffer(imGuiIndexBuffer, IndexElementSize.Sixteen); + + commandBuffer.PushVertexUniformData(in vertexUniform); + + uint vertexOffset = 0; + uint indexOffset = 0; + + for (int n = 0; n < drawDataPtr.CmdListsCount; n++) + { + ImDrawListPtr cmdList = drawDataPtr.CmdLists[n]; + + for (int cmdIndex = 0; cmdIndex < cmdList.CmdBuffer.Size; cmdIndex++) + { + ImDrawCmdPtr drawCmd = cmdList.CmdBuffer[cmdIndex]; + + Texture texture = textureStorage.GetTexture(drawCmd.TextureId); + + if (texture == null) + { + Log.Error("Texture or drawCmd.TextureId became null. Fit it!"); + continue; + } + + renderPass.BindFragmentSampler(new TextureSamplerBinding(texture, imGuiSampler)); + + float width = drawCmd.ClipRect.Z - (int)drawCmd.ClipRect.X; + float height = drawCmd.ClipRect.W - (int)drawCmd.ClipRect.Y; + + if (width <= 0 || height <= 0) + { + continue; + } + + renderPass.SetScissor( + new Rect( + (int)drawCmd.ClipRect.X - (int)pos.X, + (int)drawCmd.ClipRect.Y - (int)pos.Y, + (int)drawCmd.ClipRect.Z - (int)drawCmd.ClipRect.X, + (int)drawCmd.ClipRect.W - (int)drawCmd.ClipRect.Y + ) + ); + + renderPass.DrawIndexedPrimitives(vertexOffset, indexOffset, drawCmd.ElemCount / 3); + + indexOffset += drawCmd.ElemCount; + } + + vertexOffset += (uint)cmdList.VtxBuffer.Size; + } + + commandBuffer.EndRenderPass(renderPass); + } + + private unsafe void BuildFontAtlas() + { + ResourceUploader resourceUploader = new ResourceUploader(graphicsDevice); + + ImGuiIOPtr io = ImGui.GetIO(); + + io.Fonts.GetTexDataAsRGBA32( + out nint pixelData, + out int width, + out int height, + out int bytesPerPixel + ); + + Texture fontTexture = resourceUploader.CreateTexture2D( + new Span((void*)pixelData, width * height * bytesPerPixel), + (uint)width, + (uint)height + ); + + resourceUploader.Upload(); + resourceUploader.Dispose(); + + io.Fonts.SetTexID(fontTexture.Handle); + io.Fonts.ClearTexData(); + + textureStorage.Add(fontTexture); // <-- The fontTexture seems to get lost after some time (CG?). + this.fontTexture = fontTexture; // <-- So we also keep a reference to make sure it doesn't happen. + } + + #region Window + private void CreateWindow(ImGuiViewportPtr vp) + { + WindowCreateInfo info = new WindowCreateInfo("Window Title", (uint)vp.Pos.X, (uint)vp.Pos.Y, ScreenMode.Windowed); + + //SDL2.SDL.SDL_WindowFlags flags = graphicsDevice.WindowFlags; + //flags |= SDL2.SDL.SDL_WindowFlags.SDL_WINDOW_HIDDEN; + + //if ((vp.Flags & ImGuiViewportFlags.NoTaskBarIcon) != 0) + //{ + // flags |= SDL2.SDL.SDL_WindowFlags.SDL_WINDOW_SKIP_TASKBAR; + //} + + //if ((vp.Flags & ImGuiViewportFlags.NoDecoration) != 0) + //{ + // flags |= SDL2.SDL.SDL_WindowFlags.SDL_WINDOW_BORDERLESS; + // info.SystemResizable = false; + //} + //else + //{ + // flags |= SDL2.SDL.SDL_WindowFlags.SDL_WINDOW_RESIZABLE; + // info.SystemResizable = true; + //} + + //if ((vp.Flags & ImGuiViewportFlags.TopMost) != 0) + //{ + // flags |= SDL2.SDL.SDL_WindowFlags.SDL_WINDOW_ALWAYS_ON_TOP; + //} + + Window window = new Window(graphicsDevice, info); + graphicsDevice.ClaimWindow(window, SwapchainComposition.SDR, PresentMode.Immediate); + + GCHandle handle = GCHandle.Alloc(window); + vp.PlatformUserData = (IntPtr)handle; + + windows.Add(window, handle); + } + + private void DestroyWindow(ImGuiViewportPtr vp) + { + if (vp.PlatformUserData == IntPtr.Zero) return; + + Window window = (Window)GCHandle.FromIntPtr(vp.PlatformUserData).Target; + graphicsDevice.UnclaimWindow(window); + + if (windows.TryGetValue(window, out GCHandle handle)) + { + handle.Free(); + windows.Remove(window); + } + + graphicsDevice.Wait(); + window.Dispose(); + } + + private void ShowWindow(ImGuiViewportPtr vp) + { + if (vp.PlatformUserData == IntPtr.Zero) return; + + Window window = (Window)GCHandle.FromIntPtr(vp.PlatformUserData).Target; + SDL2.SDL.SDL_ShowWindow(window.Handle); + } + + private unsafe void GetWindowPos(ImGuiViewportPtr vp, Vector2* outPos) + { + if (vp.PlatformUserData == IntPtr.Zero) return; + + Window window = (Window)GCHandle.FromIntPtr(vp.PlatformUserData).Target; + SDL2.SDL.SDL_GetWindowPosition(window.Handle, out int x, out int y); + *outPos = new Vector2(x, y); + } + + private void SetWindowPos(ImGuiViewportPtr vp, Vector2 pos) + { + if (vp.PlatformUserData == IntPtr.Zero) return; + + Window window = (Window)GCHandle.FromIntPtr(vp.PlatformUserData).Target; + SDL2.SDL.SDL_SetWindowPosition(window.Handle, (int)pos.X, (int)pos.Y); + } + + private void SetWindowSize(ImGuiViewportPtr vp, Vector2 size) + { + if (vp.PlatformUserData == IntPtr.Zero) return; + + Window window = (Window)GCHandle.FromIntPtr(vp.PlatformUserData).Target; + SDL2.SDL.SDL_SetWindowSize(window.Handle, (int)size.X, (int)size.Y); + } + + private unsafe void GetWindowSize(ImGuiViewportPtr vp, Vector2* outSize) + { + if (vp.PlatformUserData == IntPtr.Zero) return; + + Window window = (Window)GCHandle.FromIntPtr(vp.PlatformUserData).Target; + SDL2.SDL.SDL_GetWindowSize(window.Handle, out int w, out int h); + *outSize = new Vector2(w, h); + } + + private void SetWindowFocus(ImGuiViewportPtr vp) + { + if (vp.PlatformUserData == IntPtr.Zero) return; + + Window window = (Window)GCHandle.FromIntPtr(vp.PlatformUserData).Target; + //SDL2.SDL.SDL_SetWindowInputFocus(window.Handle); + SDL2.SDL.SDL_RaiseWindow(window.Handle); + } + + private byte GetWindowFocus(ImGuiViewportPtr vp) + { + if (vp.PlatformUserData == IntPtr.Zero) return (byte)0; + + Window window = (Window)GCHandle.FromIntPtr(vp.PlatformUserData).Target; + SDL2.SDL.SDL_WindowFlags flags = (SDL2.SDL.SDL_WindowFlags)SDL2.SDL.SDL_GetWindowFlags(window.Handle); + return (flags & SDL2.SDL.SDL_WindowFlags.SDL_WINDOW_INPUT_FOCUS) != 0 ? (byte)1 : (byte)0; + } + + private byte GetWindowMinimized(ImGuiViewportPtr vp) + { + if (vp.PlatformUserData == IntPtr.Zero) return (byte)0; + + Window window = (Window)GCHandle.FromIntPtr(vp.PlatformUserData).Target; + SDL2.SDL.SDL_WindowFlags flags = (SDL2.SDL.SDL_WindowFlags)SDL2.SDL.SDL_GetWindowFlags(window.Handle); + return (flags & SDL2.SDL.SDL_WindowFlags.SDL_WINDOW_MINIMIZED) != 0 ? (byte)1 : (byte)0; + } + + private unsafe void SetWindowTitle(ImGuiViewportPtr vp, IntPtr title) + { + if (vp.PlatformUserData == IntPtr.Zero) return; + + Window window = (Window)GCHandle.FromIntPtr(vp.PlatformUserData).Target; + byte* titlePtr = (byte*)title; + int count = 0; + while (titlePtr[count] != 0) + { + count += 1; + } + SDL2.SDL.SDL_SetWindowTitle(window.Handle, System.Text.Encoding.ASCII.GetString(titlePtr, count)); + } + #endregion + + public void Dispose() + { + fontTexture?.Dispose(); + imGuiVertexBuffer?.Dispose(); + imGuiIndexBuffer?.Dispose(); + imGuiFragmentShader?.Dispose(); + imGuiVertexShader?.Dispose(); + imGuiPipeline?.Dispose(); + imGuiSampler?.Dispose(); + resourceUploader?.Dispose(); + + foreach (KeyValuePair window in windows) + { + graphicsDevice.UnclaimWindow(window.Key); + window.Key.Dispose(); + window.Value.Free(); + } + windows.Clear(); + } +} \ No newline at end of file diff --git a/Nerfed.Runtime/Gui/Shaders/generate-spirv.bat b/Nerfed.Runtime/Gui/Shaders/generate-spirv.bat new file mode 100644 index 0000000..62d1d99 --- /dev/null +++ b/Nerfed.Runtime/Gui/Shaders/generate-spirv.bat @@ -0,0 +1,2 @@ +glslangvalidator -V imgui-vertex.glsl -o imgui-vertex.spv -S vert +glslangvalidator -V imgui-frag.glsl -o imgui-frag.spv -S frag diff --git a/Nerfed.Runtime/Gui/Shaders/imgui-frag.glsl b/Nerfed.Runtime/Gui/Shaders/imgui-frag.glsl new file mode 100644 index 0000000..126b932 --- /dev/null +++ b/Nerfed.Runtime/Gui/Shaders/imgui-frag.glsl @@ -0,0 +1,13 @@ +#version 450 + +layout (location = 0) in vec4 color; +layout (location = 1) in vec2 texCoord; + +layout(set = 2, binding = 0) uniform sampler2D Sampler; + +layout (location = 0) out vec4 outputColor; + +void main() +{ + outputColor = color * texture(Sampler, texCoord); +} \ No newline at end of file diff --git a/Nerfed.Runtime/Gui/Shaders/imgui-frag.spv b/Nerfed.Runtime/Gui/Shaders/imgui-frag.spv new file mode 100644 index 0000000..550f9a0 Binary files /dev/null and b/Nerfed.Runtime/Gui/Shaders/imgui-frag.spv differ diff --git a/Nerfed.Runtime/Gui/Shaders/imgui-vertex.glsl b/Nerfed.Runtime/Gui/Shaders/imgui-vertex.glsl new file mode 100644 index 0000000..19f6c14 --- /dev/null +++ b/Nerfed.Runtime/Gui/Shaders/imgui-vertex.glsl @@ -0,0 +1,20 @@ +#version 450 + +layout (location = 0) in vec2 in_position; +layout (location = 1) in vec2 in_texCoord; +layout (location = 2) in vec4 in_color; + +layout (set = 1, binding = 0) uniform ProjectionMatrixBuffer +{ + mat4 projection_matrix; +}; + +layout (location = 0) out vec4 color; +layout (location = 1) out vec2 texCoord; + +void main() +{ + gl_Position = projection_matrix * vec4(in_position, 0, 1); + color = in_color; + texCoord = in_texCoord; +} diff --git a/Nerfed.Runtime/Gui/Shaders/imgui-vertex.spv b/Nerfed.Runtime/Gui/Shaders/imgui-vertex.spv new file mode 100644 index 0000000..a63373f Binary files /dev/null and b/Nerfed.Runtime/Gui/Shaders/imgui-vertex.spv differ diff --git a/Nerfed.Runtime/Gui/Structs.cs b/Nerfed.Runtime/Gui/Structs.cs new file mode 100644 index 0000000..a633ba6 --- /dev/null +++ b/Nerfed.Runtime/Gui/Structs.cs @@ -0,0 +1,49 @@ +using Nerfed.Runtime.Graphics; +using System.Numerics; +using System.Runtime.InteropServices; + +namespace Nerfed.Runtime.Gui; + +[StructLayout(LayoutKind.Sequential)] +public struct Position2DTextureColorVertex : IVertexType +{ + public Vector2 Position; + public Vector2 TexCoord; + public Color Color; + + public Position2DTextureColorVertex( + Vector2 position, + Vector2 texcoord, + Color color + ) + { + Position = position; + TexCoord = texcoord; + Color = color; + } + + public static VertexElementFormat[] Formats { get; } = new VertexElementFormat[3] + { + VertexElementFormat.Vector2, + VertexElementFormat.Vector2, + VertexElementFormat.Color + }; + + public static uint[] Offsets => new uint[3] + { + 0, + 8, + 16 + }; +} + +[StructLayout(LayoutKind.Sequential)] +public struct TransformVertexUniform +{ + public Matrix4x4 ProjectionMatrix; + + public TransformVertexUniform(Matrix4x4 projectionMatrix) + { + ProjectionMatrix = projectionMatrix; + } +} \ No newline at end of file diff --git a/Nerfed.Runtime/Gui/TextureStorage.cs b/Nerfed.Runtime/Gui/TextureStorage.cs new file mode 100644 index 0000000..e56da06 --- /dev/null +++ b/Nerfed.Runtime/Gui/TextureStorage.cs @@ -0,0 +1,35 @@ +using Nerfed.Runtime.Graphics; + +namespace Nerfed.Runtime.Gui; + +public class TextureStorage +{ + private readonly Dictionary> pointerToTexture = new Dictionary>(); + + public IntPtr Add(Texture texture) + { + if (!pointerToTexture.ContainsKey(texture.Handle)) + { + pointerToTexture.Add(texture.Handle, new WeakReference(texture)); + } + return texture.Handle; + } + + public Texture GetTexture(IntPtr pointer) + { + if (!pointerToTexture.ContainsKey(pointer)) + { + return null; + } + + WeakReference result = pointerToTexture[pointer]; + + if (!result.TryGetTarget(out Texture texture)) + { + pointerToTexture.Remove(pointer); + return null; + } + + return texture; + } +} \ No newline at end of file diff --git a/Nerfed.Runtime/Libraries/ImGui.NET b/Nerfed.Runtime/Libraries/ImGui.NET new file mode 160000 index 0000000..ae493d9 --- /dev/null +++ b/Nerfed.Runtime/Libraries/ImGui.NET @@ -0,0 +1 @@ +Subproject commit ae493d92a312810b66483af1922babe2eb434a47 diff --git a/Nerfed.Runtime/Nerfed.Runtime.csproj b/Nerfed.Runtime/Nerfed.Runtime.csproj index a27271a..d209046 100644 --- a/Nerfed.Runtime/Nerfed.Runtime.csproj +++ b/Nerfed.Runtime/Nerfed.Runtime.csproj @@ -12,6 +12,16 @@ + + + + + + + Always + + + Exe net8.0 diff --git a/Nerfed.sln b/Nerfed.sln index be1920e..30ec14e 100644 --- a/Nerfed.sln +++ b/Nerfed.sln @@ -5,6 +5,8 @@ VisualStudioVersion = 17.10.35013.160 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Nerfed.Runtime", "Nerfed.Runtime\Nerfed.Runtime.csproj", "{98E09BAF-587F-4238-89BD-7693C036C233}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImGui.NET", "Nerfed.Runtime\Libraries\ImGui.NET\src\ImGui.NET\ImGui.NET.csproj", "{4EC3C399-4E09-4A36-B11E-391F0792C1C8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,6 +17,10 @@ Global {98E09BAF-587F-4238-89BD-7693C036C233}.Debug|Any CPU.Build.0 = Debug|Any CPU {98E09BAF-587F-4238-89BD-7693C036C233}.Release|Any CPU.ActiveCfg = Release|Any CPU {98E09BAF-587F-4238-89BD-7693C036C233}.Release|Any CPU.Build.0 = Release|Any CPU + {4EC3C399-4E09-4A36-B11E-391F0792C1C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4EC3C399-4E09-4A36-B11E-391F0792C1C8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4EC3C399-4E09-4A36-B11E-391F0792C1C8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4EC3C399-4E09-4A36-B11E-391F0792C1C8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/libs/x64/cimgui.dll b/libs/x64/cimgui.dll new file mode 100644 index 0000000..94c3de9 --- /dev/null +++ b/libs/x64/cimgui.dll @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c1c7185401fdc2e008a4bf04ea6c667811f8f06b7fb3d33c18ef7ca983641e27 +size 1204224