perf(style): make GlobalStyleScope.StackHandle GC-free via counter scope

This commit is contained in:
2026-06-16 19:23:15 +02:00
parent 430c8f235a
commit 32013babaf
+45 -11
View File
@@ -1,5 +1,4 @@
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Utility.Raii;
using HellionChat.Themes; using HellionChat.Themes;
using HellionChat.Util; using HellionChat.Util;
@@ -33,6 +32,13 @@ internal static class GlobalStyleScope
var childBgWithAlpha = ResolveChildBgAlpha(c.ChildBg, windowOpacity); var childBgWithAlpha = ResolveChildBgAlpha(c.ChildBg, windowOpacity);
var stack = new StackHandle(); var stack = new StackHandle();
// Hard contract: every push must be balanced by a pop. The pushes below
// are pure, practically non-throwing ImGui calls, but if one ever threw
// mid-stack we must still pop what we pushed before the exception escapes,
// or the global style stack stays corrupt for every other window this
// frame. Dispose then rethrow.
try
{
stack.PushStyleVar(ImGuiStyleVar.WindowRounding, l.WindowRounding); stack.PushStyleVar(ImGuiStyleVar.WindowRounding, l.WindowRounding);
stack.PushStyleVar(ImGuiStyleVar.ChildRounding, l.ChildRounding); stack.PushStyleVar(ImGuiStyleVar.ChildRounding, l.ChildRounding);
stack.PushStyleVar(ImGuiStyleVar.PopupRounding, l.PopupRounding); stack.PushStyleVar(ImGuiStyleVar.PopupRounding, l.PopupRounding);
@@ -87,6 +93,12 @@ internal static class GlobalStyleScope
stack.PushColorAbgr(ImGuiCol.Separator, a.Border); stack.PushColorAbgr(ImGuiCol.Separator, a.Border);
stack.PushColorAbgr(ImGuiCol.SeparatorHovered, a.PrimaryLight); stack.PushColorAbgr(ImGuiCol.SeparatorHovered, a.PrimaryLight);
stack.PushColorAbgr(ImGuiCol.SeparatorActive, a.Primary); stack.PushColorAbgr(ImGuiCol.SeparatorActive, a.Primary);
}
catch
{
stack.Dispose();
throw;
}
return stack; return stack;
} }
@@ -101,24 +113,46 @@ internal static class GlobalStyleScope
return (themeChildBgRgba & 0xFFFFFF00u) | childBgAlpha; return (themeChildBgRgba & 0xFFFFFF00u) | childBgAlpha;
} }
// Counter-based scope: pushes go straight onto the global ImGui style
// stack and we only remember how many of each kind we pushed. Dispose
// pops them in one batched PopStyleColor(count)/PopStyleVar(count) call.
// This replaces the old List<IDisposable> + per-push boxed ImRaii structs
// (44 allocations/frame) with two ints — zero per-frame GC. Every style
// var pushed here is a single-float var, so PopStyleVar(count) is valid;
// both colour paths funnel into one PushStyleColor, so one colour counter
// covers them. Symmetry is the whole contract: the pop counts must equal
// the push counts or the global stack corrupts for every other window.
private sealed class StackHandle : IDisposable private sealed class StackHandle : IDisposable
{ {
private readonly List<IDisposable> _items = new(64); private int _colorCount;
private int _styleVarCount;
internal void PushColor(ImGuiCol slot, uint rgba) => internal void PushColor(ImGuiCol slot, uint rgba)
_items.Add(ImRaii.PushColor(slot, ColourUtil.RgbaToAbgr(rgba))); {
ImGui.PushStyleColor(slot, ColourUtil.RgbaToAbgr(rgba));
_colorCount++;
}
internal void PushColorAbgr(ImGuiCol slot, uint abgr) => internal void PushColorAbgr(ImGuiCol slot, uint abgr)
_items.Add(ImRaii.PushColor(slot, abgr)); {
ImGui.PushStyleColor(slot, abgr);
_colorCount++;
}
internal void PushStyleVar(ImGuiStyleVar var, float value) => internal void PushStyleVar(ImGuiStyleVar var, float value)
_items.Add(ImRaii.PushStyle(var, value)); {
ImGui.PushStyleVar(var, value);
_styleVarCount++;
}
public void Dispose() public void Dispose()
{ {
for (var i = _items.Count - 1; i >= 0; i--) if (_styleVarCount > 0)
_items[i].Dispose(); ImGui.PopStyleVar(_styleVarCount);
_items.Clear(); if (_colorCount > 0)
ImGui.PopStyleColor(_colorCount);
_styleVarCount = 0;
_colorCount = 0;
} }
} }
} }