From 37b9d85313c3a7b8f47bd305a8468481dd710e74 Mon Sep 17 00:00:00 2001 From: Shawrkie Williams Date: Sun, 25 Jan 2026 04:06:39 -0500 Subject: [PATCH] Improves inventory loading performance Implements deferred item population for inventory categories, which significantly improves the initial loading time of inventory windows, especially with a large number of items. This change introduces a mechanism to populate inventory items in batches over multiple frames, preventing the UI from blocking during the initial load. It also correctly disposes of the looted category node when the addon is finalized, preventing potential memory leaks. --- AetherBags/Addons/AddonInventoryWindow.cs | 2 + AetherBags/Addons/InventoryAddonBase.cs | 83 +++++++++++++++++-- .../Nodes/Inventory/InventoryCategoryNode.cs | 44 +++++++--- 3 files changed, 112 insertions(+), 17 deletions(-) diff --git a/AetherBags/Addons/AddonInventoryWindow.cs b/AetherBags/Addons/AddonInventoryWindow.cs index af9eff0..5d1b426 100644 --- a/AetherBags/Addons/AddonInventoryWindow.cs +++ b/AetherBags/Addons/AddonInventoryWindow.cs @@ -168,6 +168,8 @@ public unsafe class AddonInventoryWindow : InventoryAddonBase addon->UnsubscribeAtkArrayData(1, (int)NumberArrayType.Inventory); + _lootedCategoryNode?.Dispose(); + IsSetupComplete = false; base.OnFinalize(addon); } diff --git a/AetherBags/Addons/InventoryAddonBase.cs b/AetherBags/Addons/InventoryAddonBase.cs index df7ed7a..7296f7a 100644 --- a/AetherBags/Addons/InventoryAddonBase.cs +++ b/AetherBags/Addons/InventoryAddonBase.cs @@ -52,6 +52,9 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow protected bool RefreshQueued; protected bool RefreshAutosizeQueued; protected bool IsSetupComplete; + private bool _deferredPopulationInProgress; + private bool _initialPopulationComplete; + private const int ItemsPerFrame = 70; protected abstract InventoryStateBase InventoryState { get; } @@ -154,17 +157,18 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow float maxContentWidth = CategoriesNode.Width > 0 ? CategoriesNode.Width : ContentSize.X; int maxItemsPerLine = CalculateOptimalItemsPerLine(maxContentWidth); + bool deferItems = !_deferredPopulationInProgress && !_initialPopulationComplete; + CategoriesNode.SyncWithListDataByKey( dataList: categories, getKeyFromData: categorizedInventory => categorizedInventory.Key, getKeyFromNode: node => node.CategorizedInventory.Key, updateNode: (node, data) => { - node.MaxWidth = maxContentWidth; - node.SetCategoryData(data, Math.Min(data.Items.Count, maxItemsPerLine)); - node.RefreshNodeVisuals(); + node.SetCategoryData(data, Math.Min(data.Items.Count, maxItemsPerLine), deferItemCreation: deferItems); + if (!deferItems) node.RefreshNodeVisuals(); }, - createNodeMethod: _ => CreateCategoryNode(maxContentWidth)); + createNodeMethod: _ => CreateCategoryNode()); if (HasPinning) { @@ -181,6 +185,72 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow LayoutContent(); CategoriesNode.RecalculateLayout(); } + + if (deferItems && !_deferredPopulationInProgress) + { + StartDeferredItemPopulation(); + } + else if (!deferItems && !_initialPopulationComplete) + { + _initialPopulationComplete = true; + } + } + + private void StartDeferredItemPopulation() + { + _deferredPopulationInProgress = true; + Services.Framework.RunOnTick(PopulateCategoryBatch, delayTicks: 1); + } + + private void PopulateCategoryBatch() + { + if (!IsOpen) + { + _deferredPopulationInProgress = false; + return; + } + + int itemsPopulated = 0; + using (CategoriesNode.DeferRecalculateLayout()) + { + foreach (var node in CategoriesNode.Nodes) + { + if (node is InventoryCategoryNode categoryNode && categoryNode.NeedsItemPopulation) + { + int categoryItemCount = categoryNode.CategorizedInventory.Items.Count; + + if (itemsPopulated > 0 && itemsPopulated + categoryItemCount > ItemsPerFrame) + break; + + categoryNode.PopulateItems(); + categoryNode.RefreshNodeVisuals(); + itemsPopulated += categoryItemCount; + + if (itemsPopulated >= ItemsPerFrame) + break; + } + } + } + + bool hasMore = false; + foreach (var node in CategoriesNode.Nodes) + { + if (node is InventoryCategoryNode categoryNode && categoryNode.NeedsItemPopulation) + { + hasMore = true; + break; + } + } + + if (hasMore) + { + Services.Framework.RunOnTick(PopulateCategoryBatch); + } + else + { + _deferredPopulationInProgress = false; + _initialPopulationComplete = true; + } } protected readonly struct HeaderLayout @@ -231,12 +301,11 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow BackgroundDropTarget.AttachNode(this); } - protected virtual InventoryCategoryNode CreateCategoryNode(float? maxWidth = null) + protected virtual InventoryCategoryNode CreateCategoryNode() { return new InventoryCategoryNode { Size = ContentSize with { Y = 120 }, - MaxWidth = maxWidth, OnRefreshRequested = ManualRefresh, OnDragEnd = () => InventoryOrchestrator.RefreshAll(updateMaps: true), }; @@ -461,6 +530,8 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow HoverSubscribed.Clear(); RefreshQueued = false; RefreshAutosizeQueued = false; + _deferredPopulationInProgress = false; + _initialPopulationComplete = false; base.OnFinalize(addon); } diff --git a/AetherBags/Nodes/Inventory/InventoryCategoryNode.cs b/AetherBags/Nodes/Inventory/InventoryCategoryNode.cs index 0a26458..b5f3340 100644 --- a/AetherBags/Nodes/Inventory/InventoryCategoryNode.cs +++ b/AetherBags/Nodes/Inventory/InventoryCategoryNode.cs @@ -25,7 +25,8 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase private readonly TextNode _categoryNameTextNode; private readonly HybridDirectionalFlexNode _itemGridNode; - private const float FallbackItemSize = 46; + private const float ExpectedItemWidth = 42; + private const float ExpectedItemHeight = 46; private const float HeaderHeight = 16; private const float MinWidth = 40; @@ -41,9 +42,11 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase private int _lastItemCount; private ulong _lastItemsHash; private int _lastItemsPerLine; - private float? _lastMaxWidth; + private bool _itemsNeedPopulation; public event Action? HeaderHoverChanged; + + public bool NeedsItemPopulation => _itemsNeedPopulation; public Action? OnRefreshRequested { get; set; } public Action? OnDragEnd { get; set; } @@ -86,11 +89,10 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase set => SetCategoryData(value, _itemGridNode.ItemsPerLine); } - public void SetCategoryData(CategorizedInventory data, int itemsPerLine) + public void SetCategoryData(CategorizedInventory data, int itemsPerLine, bool deferItemCreation = false) { bool categoryChanged = data.Key != _lastCategoryKey; bool itemsPerLineChanged = itemsPerLine != _lastItemsPerLine; - bool maxWidthChanged = _maxWidth != _lastMaxWidth; ulong itemsHash = ComputeItemsHash(CollectionsMarshal.AsSpan(data.Items)); bool itemsChanged = data.Items.Count != _lastItemCount || itemsHash != _lastItemsHash; @@ -99,7 +101,6 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase _lastItemCount = data.Items.Count; _lastItemsHash = itemsHash; _lastItemsPerLine = itemsPerLine; - _lastMaxWidth = _maxWidth; _categorizedInventory = data; @@ -113,10 +114,19 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase if (itemsChanged || categoryChanged) { - using (_itemGridNode.DeferRecalculateLayout()) + _itemGridNode.ItemsPerLine = itemsPerLine; + + if (deferItemCreation) { - _itemGridNode.ItemsPerLine = itemsPerLine; - UpdateItemGrid(); + _itemsNeedPopulation = true; + } + else + { + using (_itemGridNode.DeferRecalculateLayout()) + { + UpdateItemGrid(); + } + _itemsNeedPopulation = false; } } else if (itemsPerLineChanged) @@ -124,12 +134,24 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase _itemGridNode.ItemsPerLine = itemsPerLine; } - if (categoryChanged || itemsChanged || itemsPerLineChanged || maxWidthChanged) + if (categoryChanged || itemsChanged || itemsPerLineChanged) { RecalculateSize(); } } + public void PopulateItems() + { + if (!_itemsNeedPopulation) + return; + + using (_itemGridNode.DeferRecalculateLayout()) + { + UpdateItemGrid(); + } + _itemsNeedPopulation = false; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static ulong ComputeItemsHash(ReadOnlySpan items) { @@ -238,8 +260,8 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase { int itemCount = CategorizedInventory.Items.Count; - float cellW = _itemGridNode.Nodes.Count > 0 ? _itemGridNode.Nodes[0].Width : FallbackItemSize; - float cellH = _itemGridNode.Nodes.Count > 0 ? _itemGridNode.Nodes[0].Height : FallbackItemSize; + float cellW = ExpectedItemWidth; + float cellH = ExpectedItemHeight; float hPad = _itemGridNode.HorizontalPadding; float vPad = _itemGridNode.VerticalPadding;