using Echo.Core.Common.Packed; using ImGuiNET; using SDL2; using SDL2Demo.SdlWrapper; using skyscraper8.Skyscraper.Plugins; using System; using System.Numerics; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; using System.Runtime.Intrinsics.X86; using testdrid.SdlWrapper; namespace Echo.UserInterface.Backend; using static SDL; /// /// Implementation reference: /// https://github.com/ocornut/imgui/blob/e23c5edd5fdef85ea0f5418b1368adb94bf86230/backends/imgui_impl_sdl.cpp /// https://github.com/ocornut/imgui/blob/e23c5edd5fdef85ea0f5418b1368adb94bf86230/backends/imgui_impl_sdlrenderer.cpp /// public sealed unsafe class ImGuiDevice : IDisposable, IEventConsumer { private static PluginLogger logger = PluginLogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); public ImGuiDevice(IntPtr window, IntPtr renderer) { this.window = window; this.renderer = renderer; io = ImGui.GetIO(); //Assign global names and flags AssignBackendNames(); io.BackendFlags |= ImGuiBackendFlags.HasMouseCursors; io.BackendFlags |= ImGuiBackendFlags.HasSetMousePos; io.BackendFlags |= ImGuiBackendFlags.RendererHasVtxOffset; //Link viewport data SDL_SysWMinfo info = default; SDL_VERSION(out info.version); if (SDL_GetWindowWMInfo(window, ref info) == SDL_bool.SDL_TRUE) { ImGuiViewportPtr viewport = ImGui.GetMainViewport(); viewport.PlatformHandleRaw = info.info.win.window; } //Setup SDL hint SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1"); SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "nearest"); //Can be nearest, linear, or best } public void Initialize() { //Create resources CreateMouseCursors(); CreateFontTexture(); CreateClipboardSetup(); } readonly IntPtr window; readonly IntPtr renderer; ImGuiIOPtr io; IntPtr[] mouseCursors; IntPtr fontTexture; int mouseButtonDownCount; int pendingMouseLeaveFrame; bool disposed; static readonly SetClipboardTextFn SetClipboardText = (_, text) => SDL_SetClipboardText(text); static readonly GetClipboardTextFn GetClipboardText = _ => SDL_GetClipboardText(); public void ProcessEvent(in SDL_Event sdlEvent) { switch (sdlEvent.type) { case SDL_EventType.SDL_MOUSEMOTION: { io.AddMousePosEvent(sdlEvent.motion.x, sdlEvent.motion.y); break; } case SDL_EventType.SDL_MOUSEWHEEL: { io.AddMouseWheelEvent(sdlEvent.wheel.x, sdlEvent.wheel.y); break; } case SDL_EventType.SDL_MOUSEBUTTONDOWN: case SDL_EventType.SDL_MOUSEBUTTONUP: { ProcessMouseButtonEvent(sdlEvent.button); break; } case SDL_EventType.SDL_TEXTINPUT: { fixed (byte* ptr = sdlEvent.text.text) ImGuiNative.ImGuiIO_AddInputCharactersUTF8(io.NativePtr, ptr); break; } case SDL_EventType.SDL_KEYDOWN: case SDL_EventType.SDL_KEYUP: { ProcessKeyboardEvent(sdlEvent.key); break; } case SDL_EventType.SDL_WINDOWEVENT: { ProcessWindowEvent(sdlEvent.window); break; } } } public void NewFrame(in TimeSpan deltaTime) { io.DeltaTime = (float)deltaTime.TotalSeconds; RefreshDisplaySize(); UpdateMouseData(); UpdateMouseCursor(); } public void Render(ImDrawDataPtr data) { //Setup clip information Vector2 scale = data.FramebufferScale; Float4 clipSize = Widen(scale * data.DisplaySize); if (clipSize.X <= 0f || clipSize.Y <= 0f) return; SDL_Rect zerocopy = new SDL_Rect(); //Render if (SDL_RenderSetScale(renderer, scale.X, scale.Y) != 0) throw SdlException.GenerateException(); if (SDL_RenderSetClipRect(renderer, ref zerocopy) != 0) throw SdlException.GenerateException(); Float4 clipOffset = Widen(data.DisplayPos); var lists = data.CmdListsRange; for (int i = 0; i < lists.Count; i++) { ExecuteCommandList(lists[i], clipOffset, clipSize); } // SDL_RenderPresent(renderer); static Float4 Widen(Vector2 vector) => new Float4(vector.AsVector128()).XYXY; } public IntPtr CreateTexture(Int2 size, bool streaming, bool bigEndian = false) { int access = streaming ? (int)SDL_TextureAccess.SDL_TEXTUREACCESS_STREAMING : (int)SDL_TextureAccess.SDL_TEXTUREACCESS_STATIC; uint format = bigEndian ? SDL_PIXELFORMAT_RGBA8888 : SDL_PIXELFORMAT_ABGR8888; IntPtr texture = SDL_CreateTexture(renderer, format, access, size.X, size.Y); if (texture == IntPtr.Zero) throw SdlException.GenerateException(); if (SDL_SetTextureBlendMode(texture, SDL_BlendMode.SDL_BLENDMODE_BLEND) != 0) throw SdlException.GenerateException(); return texture; } public void DestroyTexture(ref IntPtr texture) { if (texture == IntPtr.Zero) return; SDL_DestroyTexture(texture); texture = IntPtr.Zero; } public void Dispose() { if (disposed) return; disposed = true; DestroyMouseCursors(); DestroyFontTexture(); DestroyClipboardSetup(); io = null; } void AssignBackendNames() { if (SDL_GetRendererInfo(renderer, out SDL_RendererInfo info) != 0) throw SdlException.GenerateException(); var name = (Marshal.PtrToStringAnsi(info.name) ?? "unknown").ToUpper(); var size = new Int2(info.max_texture_width, info.max_texture_height); io.NativePtr->BackendPlatformName = (byte*)Marshal.StringToHGlobalAnsi("SDL2 & Dear ImGui for C#"); io.NativePtr->BackendRendererName = (byte*)Marshal.StringToHGlobalAnsi($"{name} {size.X}x{size.Y}"); } void CreateMouseCursors() { mouseCursors = new IntPtr[(int)ImGuiMouseCursor.COUNT]; mouseCursors[(int)ImGuiMouseCursor.Arrow] = SDL_CreateSystemCursor(SDL_SystemCursor.SDL_SYSTEM_CURSOR_ARROW); mouseCursors[(int)ImGuiMouseCursor.TextInput] = SDL_CreateSystemCursor(SDL_SystemCursor.SDL_SYSTEM_CURSOR_IBEAM); mouseCursors[(int)ImGuiMouseCursor.ResizeAll] = SDL_CreateSystemCursor(SDL_SystemCursor.SDL_SYSTEM_CURSOR_SIZEALL); mouseCursors[(int)ImGuiMouseCursor.ResizeNS] = SDL_CreateSystemCursor(SDL_SystemCursor.SDL_SYSTEM_CURSOR_SIZENS); mouseCursors[(int)ImGuiMouseCursor.ResizeEW] = SDL_CreateSystemCursor(SDL_SystemCursor.SDL_SYSTEM_CURSOR_SIZEWE); mouseCursors[(int)ImGuiMouseCursor.ResizeNESW] = SDL_CreateSystemCursor(SDL_SystemCursor.SDL_SYSTEM_CURSOR_SIZENESW); mouseCursors[(int)ImGuiMouseCursor.ResizeNWSE] = SDL_CreateSystemCursor(SDL_SystemCursor.SDL_SYSTEM_CURSOR_SIZENWSE); mouseCursors[(int)ImGuiMouseCursor.Hand] = SDL_CreateSystemCursor(SDL_SystemCursor.SDL_SYSTEM_CURSOR_HAND); mouseCursors[(int)ImGuiMouseCursor.NotAllowed] = SDL_CreateSystemCursor(SDL_SystemCursor.SDL_SYSTEM_CURSOR_NO); } void DestroyMouseCursors() { if (mouseCursors == null) return; foreach (IntPtr cursor in mouseCursors) { if (cursor == IntPtr.Zero) continue; SDL_FreeCursor(cursor); } mouseCursors = null; } void CreateFontTexture() { io.Fonts.GetTexDataAsRGBA32(out IntPtr pixels, out int width, out int height); fontTexture = CreateTexture(new Int2(width, height), false); if (SDL_UpdateTexture(fontTexture, IntPtr.Zero, pixels, width * sizeof(uint)) != 0) throw SdlException.GenerateException(); if (SDL_SetTextureBlendMode(fontTexture, SDL_BlendMode.SDL_BLENDMODE_BLEND) != 0) throw SdlException.GenerateException(); if (SDL_SetTextureScaleMode(fontTexture, SDL_ScaleMode.SDL_ScaleModeLinear) != 0) throw SdlException.GenerateException(); io.Fonts.SetTexID(fontTexture); } void DestroyFontTexture() { DestroyTexture(ref fontTexture); io.Fonts.SetTexID(IntPtr.Zero); } void CreateClipboardSetup() { io.SetClipboardTextFn = Marshal.GetFunctionPointerForDelegate(SetClipboardText); io.GetClipboardTextFn = Marshal.GetFunctionPointerForDelegate(GetClipboardText); } void DestroyClipboardSetup() { io.SetClipboardTextFn = IntPtr.Zero; io.GetClipboardTextFn = IntPtr.Zero; } void ProcessMouseButtonEvent(SDL_MouseButtonEvent mouseButtonEvent) { int mouseButton = (uint)mouseButtonEvent.button switch { SDL_BUTTON_LEFT => 0, SDL_BUTTON_RIGHT => 1, SDL_BUTTON_MIDDLE => 2, SDL_BUTTON_X1 => 3, SDL_BUTTON_X2 => 4, _ => -1 }; if (mouseButton < 0) return; bool down = mouseButtonEvent.type == SDL_EventType.SDL_MOUSEBUTTONDOWN; io.AddMouseButtonEvent(mouseButton, down); mouseButtonDownCount += down ? 1 : -1; } void ProcessKeyboardEvent(in SDL_KeyboardEvent keyboardEvent) { ref readonly SDL_Keysym key = ref keyboardEvent.keysym; bool down = keyboardEvent.type == SDL_EventType.SDL_KEYDOWN; io.AddKeyEvent(ImGuiKey.ModCtrl, (key.mod & SDL_Keymod.KMOD_CTRL) != 0); io.AddKeyEvent(ImGuiKey.ModShift, (key.mod & SDL_Keymod.KMOD_SHIFT) != 0); io.AddKeyEvent(ImGuiKey.ModAlt, (key.mod & SDL_Keymod.KMOD_ALT) != 0); io.AddKeyEvent(ImGuiKey.ModSuper, (key.mod & SDL_Keymod.KMOD_GUI) != 0); io.AddKeyEvent(SDL_KeycodeToImGuiKey(key.sym), down); } public static ImGuiKey SDL_KeycodeToImGuiKey(SDL_Keycode key) => key switch { SDL_Keycode.SDLK_TAB => ImGuiKey.Tab, SDL_Keycode.SDLK_LEFT => ImGuiKey.LeftArrow, SDL_Keycode.SDLK_RIGHT => ImGuiKey.RightArrow, SDL_Keycode.SDLK_UP => ImGuiKey.UpArrow, SDL_Keycode.SDLK_DOWN => ImGuiKey.DownArrow, SDL_Keycode.SDLK_PAGEUP => ImGuiKey.PageUp, SDL_Keycode.SDLK_PAGEDOWN => ImGuiKey.PageDown, SDL_Keycode.SDLK_HOME => ImGuiKey.Home, SDL_Keycode.SDLK_END => ImGuiKey.End, SDL_Keycode.SDLK_INSERT => ImGuiKey.Insert, SDL_Keycode.SDLK_DELETE => ImGuiKey.Delete, SDL_Keycode.SDLK_BACKSPACE => ImGuiKey.Backspace, SDL_Keycode.SDLK_SPACE => ImGuiKey.Space, SDL_Keycode.SDLK_RETURN => ImGuiKey.Enter, SDL_Keycode.SDLK_ESCAPE => ImGuiKey.Escape, SDL_Keycode.SDLK_QUOTE => ImGuiKey.Apostrophe, SDL_Keycode.SDLK_COMMA => ImGuiKey.Comma, SDL_Keycode.SDLK_MINUS => ImGuiKey.Minus, SDL_Keycode.SDLK_PERIOD => ImGuiKey.Period, SDL_Keycode.SDLK_SLASH => ImGuiKey.Slash, SDL_Keycode.SDLK_SEMICOLON => ImGuiKey.Semicolon, SDL_Keycode.SDLK_EQUALS => ImGuiKey.Equal, SDL_Keycode.SDLK_LEFTBRACKET => ImGuiKey.LeftBracket, SDL_Keycode.SDLK_BACKSLASH => ImGuiKey.Backslash, SDL_Keycode.SDLK_RIGHTBRACKET => ImGuiKey.RightBracket, SDL_Keycode.SDLK_BACKQUOTE => ImGuiKey.GraveAccent, SDL_Keycode.SDLK_CAPSLOCK => ImGuiKey.CapsLock, SDL_Keycode.SDLK_SCROLLLOCK => ImGuiKey.ScrollLock, SDL_Keycode.SDLK_NUMLOCKCLEAR => ImGuiKey.NumLock, SDL_Keycode.SDLK_PRINTSCREEN => ImGuiKey.PrintScreen, SDL_Keycode.SDLK_PAUSE => ImGuiKey.Pause, SDL_Keycode.SDLK_KP_0 => ImGuiKey.Keypad0, SDL_Keycode.SDLK_KP_1 => ImGuiKey.Keypad1, SDL_Keycode.SDLK_KP_2 => ImGuiKey.Keypad2, SDL_Keycode.SDLK_KP_3 => ImGuiKey.Keypad3, SDL_Keycode.SDLK_KP_4 => ImGuiKey.Keypad4, SDL_Keycode.SDLK_KP_5 => ImGuiKey.Keypad5, SDL_Keycode.SDLK_KP_6 => ImGuiKey.Keypad6, SDL_Keycode.SDLK_KP_7 => ImGuiKey.Keypad7, SDL_Keycode.SDLK_KP_8 => ImGuiKey.Keypad8, SDL_Keycode.SDLK_KP_9 => ImGuiKey.Keypad9, SDL_Keycode.SDLK_KP_PERIOD => ImGuiKey.KeypadDecimal, SDL_Keycode.SDLK_KP_DIVIDE => ImGuiKey.KeypadDivide, SDL_Keycode.SDLK_KP_MULTIPLY => ImGuiKey.KeypadMultiply, SDL_Keycode.SDLK_KP_MINUS => ImGuiKey.KeypadSubtract, SDL_Keycode.SDLK_KP_PLUS => ImGuiKey.KeypadAdd, SDL_Keycode.SDLK_KP_ENTER => ImGuiKey.KeypadEnter, SDL_Keycode.SDLK_KP_EQUALS => ImGuiKey.KeypadEqual, SDL_Keycode.SDLK_LCTRL => ImGuiKey.LeftCtrl, SDL_Keycode.SDLK_LSHIFT => ImGuiKey.LeftShift, SDL_Keycode.SDLK_LALT => ImGuiKey.LeftAlt, SDL_Keycode.SDLK_LGUI => ImGuiKey.LeftSuper, SDL_Keycode.SDLK_RCTRL => ImGuiKey.RightCtrl, SDL_Keycode.SDLK_RSHIFT => ImGuiKey.RightShift, SDL_Keycode.SDLK_RALT => ImGuiKey.RightAlt, SDL_Keycode.SDLK_RGUI => ImGuiKey.RightSuper, SDL_Keycode.SDLK_APPLICATION => ImGuiKey.Menu, SDL_Keycode.SDLK_0 => ImGuiKey._0, SDL_Keycode.SDLK_1 => ImGuiKey._1, SDL_Keycode.SDLK_2 => ImGuiKey._2, SDL_Keycode.SDLK_3 => ImGuiKey._3, SDL_Keycode.SDLK_4 => ImGuiKey._4, SDL_Keycode.SDLK_5 => ImGuiKey._5, SDL_Keycode.SDLK_6 => ImGuiKey._6, SDL_Keycode.SDLK_7 => ImGuiKey._7, SDL_Keycode.SDLK_8 => ImGuiKey._8, SDL_Keycode.SDLK_9 => ImGuiKey._9, SDL_Keycode.SDLK_a => ImGuiKey.A, SDL_Keycode.SDLK_b => ImGuiKey.B, SDL_Keycode.SDLK_c => ImGuiKey.C, SDL_Keycode.SDLK_d => ImGuiKey.D, SDL_Keycode.SDLK_e => ImGuiKey.E, SDL_Keycode.SDLK_f => ImGuiKey.F, SDL_Keycode.SDLK_g => ImGuiKey.G, SDL_Keycode.SDLK_h => ImGuiKey.H, SDL_Keycode.SDLK_i => ImGuiKey.I, SDL_Keycode.SDLK_j => ImGuiKey.J, SDL_Keycode.SDLK_k => ImGuiKey.K, SDL_Keycode.SDLK_l => ImGuiKey.L, SDL_Keycode.SDLK_m => ImGuiKey.M, SDL_Keycode.SDLK_n => ImGuiKey.N, SDL_Keycode.SDLK_o => ImGuiKey.O, SDL_Keycode.SDLK_p => ImGuiKey.P, SDL_Keycode.SDLK_q => ImGuiKey.Q, SDL_Keycode.SDLK_r => ImGuiKey.R, SDL_Keycode.SDLK_s => ImGuiKey.S, SDL_Keycode.SDLK_t => ImGuiKey.T, SDL_Keycode.SDLK_u => ImGuiKey.U, SDL_Keycode.SDLK_v => ImGuiKey.V, SDL_Keycode.SDLK_w => ImGuiKey.W, SDL_Keycode.SDLK_x => ImGuiKey.X, SDL_Keycode.SDLK_y => ImGuiKey.Y, SDL_Keycode.SDLK_z => ImGuiKey.Z, SDL_Keycode.SDLK_F1 => ImGuiKey.F1, SDL_Keycode.SDLK_F2 => ImGuiKey.F2, SDL_Keycode.SDLK_F3 => ImGuiKey.F3, SDL_Keycode.SDLK_F4 => ImGuiKey.F4, SDL_Keycode.SDLK_F5 => ImGuiKey.F5, SDL_Keycode.SDLK_F6 => ImGuiKey.F6, SDL_Keycode.SDLK_F7 => ImGuiKey.F7, SDL_Keycode.SDLK_F8 => ImGuiKey.F8, SDL_Keycode.SDLK_F9 => ImGuiKey.F9, SDL_Keycode.SDLK_F10 => ImGuiKey.F10, SDL_Keycode.SDLK_F11 => ImGuiKey.F11, SDL_Keycode.SDLK_F12 => ImGuiKey.F12, _ => ImGuiKey.None }; void ProcessWindowEvent(in SDL_WindowEvent windowEvent) { switch (windowEvent.windowEvent) { case SDL_WindowEventID.SDL_WINDOWEVENT_ENTER: { pendingMouseLeaveFrame = 0; break; } case SDL_WindowEventID.SDL_WINDOWEVENT_LEAVE: { pendingMouseLeaveFrame = ImGui.GetFrameCount() + 1; break; } case SDL_WindowEventID.SDL_WINDOWEVENT_FOCUS_GAINED: { io.AddFocusEvent(true); break; } case SDL_WindowEventID.SDL_WINDOWEVENT_FOCUS_LOST: { io.AddFocusEvent(false); break; } } } void RefreshDisplaySize() { SDL_GetWindowSize(window, out int width, out int height); var size = io.DisplaySize = new Vector2(width, height); if (SDL_GetRendererOutputSize(renderer, out int displayWidth, out int displayHeight) != 0) throw SdlException.GenerateException(); if ((SDL_GetWindowFlags(window) & (uint)SDL_WindowFlags.SDL_WINDOW_MINIMIZED) != 0) size = Vector2.Zero; if (size.X > 0f && size.Y > 0f) io.DisplayFramebufferScale = new Vector2(displayWidth, displayHeight) / size; } private long nandate = 0; void UpdateMouseData() { if (pendingMouseLeaveFrame >= ImGui.GetFrameCount() && mouseButtonDownCount == 0) { io.AddMousePosEvent(float.MinValue, float.MinValue); pendingMouseLeaveFrame = 0; } if (SDL_CaptureMouse(mouseButtonDownCount != 0 ? SDL_bool.SDL_TRUE : SDL_bool.SDL_FALSE) != 0) { nandate++; //throw SdlException.GenerateException(); } if (window != SDL_GetKeyboardFocus()) return; if (io.WantSetMousePos) SDL_WarpMouseInWindow(window, (int)io.MousePos.X, (int)io.MousePos.Y); if (mouseButtonDownCount == 0) { _ = SDL_GetGlobalMouseState(out int globalX, out int globalY); SDL_GetWindowPosition(window, out int windowX, out int windowY); io.AddMousePosEvent(globalX - windowX, globalY - windowY); } } void UpdateMouseCursor() { if ((io.ConfigFlags & ImGuiConfigFlags.NoMouseCursorChange) != 0) return; ImGuiMouseCursor cursor = ImGui.GetMouseCursor(); if (!io.MouseDrawCursor && cursor != ImGuiMouseCursor.None) { IntPtr mouseCursor = mouseCursors[(int)cursor]; const int Fallback = (int)ImGuiMouseCursor.Arrow; if (mouseCursor == IntPtr.Zero) mouseCursor = mouseCursors[Fallback]; SDL_SetCursor(mouseCursor); _ = SDL_ShowCursor((int)SDL_bool.SDL_TRUE); } else _ = SDL_ShowCursor((int)SDL_bool.SDL_FALSE); } private uint numErrors; void ExecuteCommandList(ImDrawListPtr list, in Float4 clipOffset, in Float4 clipSize) { ImPtrVector buffer = list.CmdBuffer; var vertices = (ImDrawVert*)list.VtxBuffer.Data; var indices = (ushort*)list.IdxBuffer.Data; for (int i = 0; i < buffer.Size; i++) { ImDrawCmdPtr command = buffer[i]; if (command.UserCallback == IntPtr.Zero) { var clipRect = new Float4(command.ClipRect.AsVector128()) - clipOffset; Float4 clipMin = clipRect.Max(Float4.Zero); //(minX, minY, ____, ____).Max(zero) Float4 clipMax = clipRect.Min(clipSize); //(____, ____, maxX, maxY).Min(size) if (clipMax.Z <= clipMin.X || clipMax.W <= clipMin.Y) continue; clipMin = clipMin.XYXY; //(minX, minY, minX, minY) clipMax = clipMax.__ZW; //(0000, 0000, maxX, maxY) Float4 clip = (clipMax - clipMin).Absoluted; //(-minX, -minY, maxX - minX, maxY - minY).Absoluted var rect = Sse2.ConvertToVector128Int32(clip.v); //( X, Y, width, height) ImDrawVert* vertex = vertices + command.VtxOffset; int stride = sizeof(ImDrawVert); lock (Renderer.Lockable) { if (SDL_RenderGeometryRaw ( renderer, command.TextureId, (float*)&vertex->pos, stride, (int*)&vertex->col, stride, (float*)&vertex->uv, stride, list.VtxBuffer.Size - (int)command.VtxOffset, (IntPtr)(indices + command.IdxOffset), (int)command.ElemCount, sizeof(ushort) ) != 0) { numErrors++; SdlException sdlException = SdlException.GenerateException(); //We can tolerate the Geometry Renderer going sideways a few times logger.Log(PluginLogLevel.Error, String.Format("{0}, ({1}x)",sdlException.ToString(),numErrors)); } } } else { var callback = Marshal.GetDelegateForFunctionPointer(command.UserCallback); callback(new IntPtr(list), new IntPtr(command)); //Perform user callback, not really used } } } [DllImport("SDL2", CallingConvention = CallingConvention.Cdecl)] public static extern unsafe int SDL_RenderGeometryRaw(IntPtr renderer, IntPtr texture, float* xy, int xy_stride, int* color, int color_stride, float* uv, int uv_stride, int num_vertices, IntPtr indices, int num_indices, int size_indices); delegate void SetClipboardTextFn(IntPtr _, string text); delegate string GetClipboardTextFn(IntPtr _); delegate void ImDrawCallback(IntPtr list, IntPtr cmd); public void ProcessEvent(SDL_Event evt) { ProcessEvent(in evt); } }