using BepInEx.Configuration; using Elements.Core; using ResoniteImGuiLib.SDL3; using SDL3; using System.Collections.Concurrent; namespace ResoniteImGuiLib; public class ImGuiInstance { private ImGuiInstance(string name) { _name = name; _entry = Plugin.Config.Bind("Windows", "Rect_" + Name, new int4(200, 200, 400, 300)); } public string Name => _name; /// /// Called whenever ImGui is being rendered. You can use ImGui functions inside here. /// public event Action? Layout; /// /// Called whenever the window is processing an event. Returning false means that the event will not be processed further. /// public event Func? SdlEvent; private string _name; private ConfigEntry _entry; private static bool IsSDL3Running => _sdl3Thread is { IsAlive: true }; private static Thread? _sdl3Thread; private static Dictionary GuiInstances = new(); internal static ConcurrentQueue newInstances = new(); internal static ConcurrentQueue<(string, Action)> windowRequests = new(); /// /// Tries to open the window. If it's already open, nothing happens. This function may be expensive to call rapidly. /// public void OpenWindow() { newInstances.Enqueue(this); } /// /// Gets a reference to the SDL3Window inside of a callback. /// The callback will be called inside the SDL3 Thread. /// /// public void GetImGui(Action callback) { windowRequests.Enqueue((Name, callback)); } /// /// Creates a new ImGuiInstance with the "global" name or returns it if it already exists. /// /// Callback for when the ImGui instance is ready. You can put initialization logic here. /// public static ImGuiInstance GetOrCreate(ImGuiReady onReady) => GetOrCreate("global", onReady); /// /// Creates a new ImGuiInstance or returns one if it already exists. /// /// Callback for when the ImGui instance is ready. You can put initialization logic here. /// public static ImGuiInstance GetOrCreate(string name = "global", ImGuiReady? onReady = null) { if (GuiInstances.TryGetValue(name, out var instance)) { if (onReady != null) instance.GetImGui((gui) => onReady(gui, false)); return instance; } instance = new ImGuiInstance(name); GuiInstances.Add(name, instance); if (onReady != null) instance.GetImGui((gui) => onReady(gui, true)); newInstances.Enqueue(instance); TryStartSDL3Thread(); return instance; } private static void TryStartSDL3Thread() { if (IsSDL3Running) return; _sdl3Thread = new Thread(RunSDL3) { Name = $"Resonite ImGui SDL3", Priority = ThreadPriority.Highest, IsBackground = false }; _sdl3Thread.Start(); } static Dictionary windows = new(); static Dictionary windowsById = new(); private static void RunSDL3() { try { while (true) { while (newInstances.TryDequeue(out var request)) { if (windows.ContainsKey(request.Name)) continue; SDL3Window app = new SDL3Window("ImGuiContext: " + request.Name, request._entry.Value.x, request._entry.Value.y, request._entry.Value.z, request._entry.Value.w); app.WindowRectModified += (rect) => request._entry.Value = rect; // TODO: maybe listen to config changes to resize window // request._entry.SettingChanged += (_, _) => { }; app.RenderCallback = request.Layout; app.SdlEventReceived = request.SdlEvent; if (!windows.TryAdd(request.Name, app)) { Plugin.Log.LogError($"Failed to add window with name {request.Name}!"); } if (!windowsById.TryAdd(app.SdlWindowId, app)) { Plugin.Log.LogError($"Failed to add window with id {app.SdlWindowId}, name {request.Name}!"); } } for (int i = 0; i < windowRequests.Count; i++) { if (windowRequests.TryDequeue(out var callback)) { if (windows.TryGetValue(callback.Item1, out var window)) { callback.Item2?.Invoke(window); } else { windowRequests.Enqueue(callback); } } } while (SDL.PollEvent(out SDL.Event ev)) { if (windowsById.TryGetValue(ev.Window.WindowID, out var window)) { window.EventQueue.Enqueue(ev); } } var removed = new HashSet<(string, uint)>(); foreach (var (key, window) in windows) { if(window.Disposed) { removed.Add((key, window.SdlWindowId)); continue; } window.SetContext(); if (Plugin.CancellationToken.IsCancellationRequested) { window.ForceDispose(); continue; } if (window.ShouldClose) { removed.Add((key, window.SdlWindowId)); window.Dispose(); continue; } window.RunOneFrame(); } foreach (var (k, k2) in removed) { var b = windows.Remove(k); var b2 = windowsById.Remove(k2); Plugin.Log.LogDebug($"Removed window {k}, success codes: {b}|{b2}"); } if (Plugin.CancellationToken.IsCancellationRequested) { break; } // Prevent busy looping when nothing is happening. if (windows.Count == 0) { Thread.Sleep(1000); } } } catch (Exception e) { Plugin.Log.LogError($"SDL3 Thread crashed: {e}"); } _sdl3Thread?.Interrupt(); _sdl3Thread = null; } } public delegate void ImGuiReady(SDL3Window imGui, bool isNewInstance);