From 6ba86cceb4749096eb238fbfc46e9ab1900bde6f Mon Sep 17 00:00:00 2001 From: LostPoE Date: Sat, 1 Aug 2026 13:02:58 -0400 Subject: [PATCH] perf: stop burning frame time in the settings UI Two fixes for severe frame-time cost while the plugin's settings panel is visible. Measured with a full inventory of items at 60fps. 1. The Craft of Exile import box declared a 10,000,000 byte buffer. ImGui.NET allocates, zeroes and copies the full declared size on every frame, and the section is DefaultOpen, so this ran continuously whether or not the box was ever used. The largest real CoE export string in Template Crafts is ~118KB, so 500,000 keeps 4x headroom. The other InputTextWithHint buffers held currency base names, condition names and file names, none of which need 100,000 bytes; those are now 2,000. 2. The inventory slot filter highlight called IsItemMatchingCondition per slot per frame. Each call constructs a fresh ItemData for the item, measured at ~1.4ms, so a full bag cost roughly 84ms per frame and made the whole overlay stutter whenever Apply Filter was enabled. Results are now cached in a bool[5,12], recomputed when the query text changes or after 200ms, so slot highlighting stays responsive without re-evaluating every frame. No behaviour change: the same slots highlight, and Apply Steps still evaluates the filter directly at press time. Co-Authored-By: Claude Opus 5 (1M context) --- CraftingMenu/CraftingSequenceMenu.cs | 14 +++++----- WheresMyCraftAtSettings.cs | 38 ++++++++++++++++++++++++---- 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/CraftingMenu/CraftingSequenceMenu.cs b/CraftingMenu/CraftingSequenceMenu.cs index 13f03d3..d22708d 100644 --- a/CraftingMenu/CraftingSequenceMenu.cs +++ b/CraftingMenu/CraftingSequenceMenu.cs @@ -281,7 +281,7 @@ private static void DrawCurrencyAndAutoSuccessSection(CraftingStepInput step) var currencyItem = step.CurrencyItem; var availableWidth = ImGui.GetContentRegionAvail().X * 0.75f; ImGui.SetNextItemWidth(availableWidth); - if (ImGui.InputTextWithHint("Currency Item", "Case Sensitive Currency BaseName \"Orb of Transmutation\"...", ref currencyItem, 100_000)) + if (ImGui.InputTextWithHint("Currency Item", "Case Sensitive Currency BaseName \"Orb of Transmutation\"...", ref currencyItem, 2_000)) step.CurrencyItem = currencyItem; var autoSuccess = step.AutomaticSuccess; @@ -383,7 +383,7 @@ private static void DrawBranchEditor(CraftingStepInput step, CraftingStepBranchI { using (new ErrorStyling()) { - if (ImGui.InputTextWithHint("##conditionName", "Branch Name", ref branchName, 100_000)) + if (ImGui.InputTextWithHint("##conditionName", "Branch Name", ref branchName, 2_000)) step.Branches[branchIndex].ConditionalGroups[0].Conditionals[0].Name = branchName; } @@ -396,7 +396,7 @@ private static void DrawBranchEditor(CraftingStepInput step, CraftingStepBranchI } else { - if (ImGui.InputTextWithHint("##conditionName", "Branch Name", ref branchName, 100_000)) + if (ImGui.InputTextWithHint("##conditionName", "Branch Name", ref branchName, 2_000)) step.Branches[branchIndex].ConditionalGroups[0].Conditionals[0].Name = branchName; } @@ -640,7 +640,7 @@ private static void DrawConditionalChecks(CraftingStepInput step, int stepIndex, { var availableWidth = ImGui.GetContentRegionAvail().X * 0.75f; ImGui.SetNextItemWidth(availableWidth); - if (ImGui.InputTextWithHint("##conditionName", "Name of condition...", ref checkKey, 100_000)) + if (ImGui.InputTextWithHint("##conditionName", "Name of condition...", ref checkKey, 2_000)) step.ConditionalGroups[groupIndex].Conditionals[conditionalIndex].Name = checkKey; } @@ -655,7 +655,7 @@ private static void DrawConditionalChecks(CraftingStepInput step, int stepIndex, { var availableWidth = ImGui.GetContentRegionAvail().X * 0.75f; ImGui.SetNextItemWidth(availableWidth); - if (ImGui.InputTextWithHint("##conditionName", "Name of condition...", ref checkKey, 100_000)) + if (ImGui.InputTextWithHint("##conditionName", "Name of condition...", ref checkKey, 2_000)) step.ConditionalGroups[groupIndex].Conditionals[conditionalIndex].Name = checkKey; } @@ -1233,7 +1233,7 @@ private static void DrawFileOptions() return; ImGui.Indent(); - ImGui.InputTextWithHint("##SaveAs", "File Path...", ref _fileSaveName, 100_000); + ImGui.InputTextWithHint("##SaveAs", "File Path...", ref _fileSaveName, 2_000); ImGui.SameLine(); if (ImGui.Button("Save To File")) @@ -1342,7 +1342,7 @@ private static void DrawImportSettings() LoadCoEDataData(); } - ImGui.InputTextWithHint("##CoEImport", "Craft of Exile Export String..", ref _coeImportData, 10_000_000); + ImGui.InputTextWithHint("##CoEImport", "Craft of Exile Export String..", ref _coeImportData, 500_000); ImGui.SameLine(); if (ImGui.Button("Import CoE Data")) { diff --git a/WheresMyCraftAtSettings.cs b/WheresMyCraftAtSettings.cs index 3de9393..977c4c6 100644 --- a/WheresMyCraftAtSettings.cs +++ b/WheresMyCraftAtSettings.cs @@ -54,6 +54,18 @@ public class RunOptions public CustomNode InventorySectionSelector { get; } public int[,] InventoryCraftingSlots { get; set; } = new int[5, 12]; + // The filter highlight used to be evaluated per slot per frame, which rebuilds an + // ItemData for every item every frame (~1.4ms each, ~84ms/frame with a full bag). + // Cache the matches and refresh at most every 200ms, or when the query text changes. + [JsonIgnore] + private readonly bool[,] _filterMatchCache = new bool[5, 12]; + + [JsonIgnore] + private readonly System.Diagnostics.Stopwatch _filterCacheAge = System.Diagnostics.Stopwatch.StartNew(); + + [JsonIgnore] + private string _filterCacheKey = null; + public RunOptions(StylingDooDads Styling) { ToggleAll.OnPressed = () => @@ -109,7 +121,26 @@ public RunOptions(StylingDooDads Styling) filter = new ItemFilter([query]); } } - + + var cacheKey = ApplyFilter ? InventoryFilter.Value ?? "" : null; + if (_filterCacheKey != cacheKey || _filterCacheAge.ElapsedMilliseconds > 200) + { + _filterCacheKey = cacheKey; + _filterCacheAge.Restart(); + Array.Clear(_filterMatchCache, 0, _filterMatchCache.Length); + + if (filter != null) + { + foreach (var invItem in itemsInInventory) + { + if (invItem.PosX is >= 0 and < 12 && invItem.PosY is >= 0 and < 5) + { + _filterMatchCache[invItem.PosY, invItem.PosX] = + FilterHandler.IsItemMatchingCondition(invItem.Item, filter); + } + } + } + } var numb = 1; ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(1, 1)); @@ -122,10 +153,7 @@ public RunOptions(StylingDooDads Styling) var isValidItemInSlot = itemsInInventory.Any(item => item.PosX == col && item.PosY == row); if (isValidItemInSlot && Styling.CustomMenuStyling) { - var isFiltered = ApplyFilter && - filter != null && - InventoryHandler.TryGetInventoryItemFromSlot(new Vector2(col, row), out var item) && - FilterHandler.IsItemMatchingCondition(item.Item, filter); + var isFiltered = ApplyFilter && _filterMatchCache[row, col]; ImGui.PushStyleColor(ImGuiCol.FrameBg, isFiltered ? Styling.InventoryVisualizer.Filtered.Value.ToImgui() : Styling.InventoryVisualizer.BackgroundNormal.Value.ToImgui()); ImGui.PushStyleColor(ImGuiCol.FrameBgHovered, Styling.InventoryVisualizer.BackgroundHovered.Value.ToImgui());