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.
This commit is contained in:
Shawrkie Williams
2026-01-25 04:06:39 -05:00
parent 1c0917c3c1
commit 37b9d85313
3 changed files with 112 additions and 17 deletions
@@ -168,6 +168,8 @@ public unsafe class AddonInventoryWindow : InventoryAddonBase
addon->UnsubscribeAtkArrayData(1, (int)NumberArrayType.Inventory); addon->UnsubscribeAtkArrayData(1, (int)NumberArrayType.Inventory);
_lootedCategoryNode?.Dispose();
IsSetupComplete = false; IsSetupComplete = false;
base.OnFinalize(addon); base.OnFinalize(addon);
} }
+77 -6
View File
@@ -52,6 +52,9 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
protected bool RefreshQueued; protected bool RefreshQueued;
protected bool RefreshAutosizeQueued; protected bool RefreshAutosizeQueued;
protected bool IsSetupComplete; protected bool IsSetupComplete;
private bool _deferredPopulationInProgress;
private bool _initialPopulationComplete;
private const int ItemsPerFrame = 70;
protected abstract InventoryStateBase InventoryState { get; } 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; float maxContentWidth = CategoriesNode.Width > 0 ? CategoriesNode.Width : ContentSize.X;
int maxItemsPerLine = CalculateOptimalItemsPerLine(maxContentWidth); int maxItemsPerLine = CalculateOptimalItemsPerLine(maxContentWidth);
bool deferItems = !_deferredPopulationInProgress && !_initialPopulationComplete;
CategoriesNode.SyncWithListDataByKey<CategorizedInventory, InventoryCategoryNode, uint>( CategoriesNode.SyncWithListDataByKey<CategorizedInventory, InventoryCategoryNode, uint>(
dataList: categories, dataList: categories,
getKeyFromData: categorizedInventory => categorizedInventory.Key, getKeyFromData: categorizedInventory => categorizedInventory.Key,
getKeyFromNode: node => node.CategorizedInventory.Key, getKeyFromNode: node => node.CategorizedInventory.Key,
updateNode: (node, data) => updateNode: (node, data) =>
{ {
node.MaxWidth = maxContentWidth; node.SetCategoryData(data, Math.Min(data.Items.Count, maxItemsPerLine), deferItemCreation: deferItems);
node.SetCategoryData(data, Math.Min(data.Items.Count, maxItemsPerLine)); if (!deferItems) node.RefreshNodeVisuals();
node.RefreshNodeVisuals();
}, },
createNodeMethod: _ => CreateCategoryNode(maxContentWidth)); createNodeMethod: _ => CreateCategoryNode());
if (HasPinning) if (HasPinning)
{ {
@@ -181,6 +185,72 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
LayoutContent(); LayoutContent();
CategoriesNode.RecalculateLayout(); 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 protected readonly struct HeaderLayout
@@ -231,12 +301,11 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
BackgroundDropTarget.AttachNode(this); BackgroundDropTarget.AttachNode(this);
} }
protected virtual InventoryCategoryNode CreateCategoryNode(float? maxWidth = null) protected virtual InventoryCategoryNode CreateCategoryNode()
{ {
return new InventoryCategoryNode return new InventoryCategoryNode
{ {
Size = ContentSize with { Y = 120 }, Size = ContentSize with { Y = 120 },
MaxWidth = maxWidth,
OnRefreshRequested = ManualRefresh, OnRefreshRequested = ManualRefresh,
OnDragEnd = () => InventoryOrchestrator.RefreshAll(updateMaps: true), OnDragEnd = () => InventoryOrchestrator.RefreshAll(updateMaps: true),
}; };
@@ -461,6 +530,8 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
HoverSubscribed.Clear(); HoverSubscribed.Clear();
RefreshQueued = false; RefreshQueued = false;
RefreshAutosizeQueued = false; RefreshAutosizeQueued = false;
_deferredPopulationInProgress = false;
_initialPopulationComplete = false;
base.OnFinalize(addon); base.OnFinalize(addon);
} }
@@ -25,7 +25,8 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase
private readonly TextNode _categoryNameTextNode; private readonly TextNode _categoryNameTextNode;
private readonly HybridDirectionalFlexNode<DragDropNode> _itemGridNode; private readonly HybridDirectionalFlexNode<DragDropNode> _itemGridNode;
private const float FallbackItemSize = 46; private const float ExpectedItemWidth = 42;
private const float ExpectedItemHeight = 46;
private const float HeaderHeight = 16; private const float HeaderHeight = 16;
private const float MinWidth = 40; private const float MinWidth = 40;
@@ -41,9 +42,11 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase
private int _lastItemCount; private int _lastItemCount;
private ulong _lastItemsHash; private ulong _lastItemsHash;
private int _lastItemsPerLine; private int _lastItemsPerLine;
private float? _lastMaxWidth; private bool _itemsNeedPopulation;
public event Action<InventoryCategoryNode, bool>? HeaderHoverChanged; public event Action<InventoryCategoryNode, bool>? HeaderHoverChanged;
public bool NeedsItemPopulation => _itemsNeedPopulation;
public Action? OnRefreshRequested { get; set; } public Action? OnRefreshRequested { get; set; }
public Action? OnDragEnd { get; set; } public Action? OnDragEnd { get; set; }
@@ -86,11 +89,10 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase
set => SetCategoryData(value, _itemGridNode.ItemsPerLine); 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 categoryChanged = data.Key != _lastCategoryKey;
bool itemsPerLineChanged = itemsPerLine != _lastItemsPerLine; bool itemsPerLineChanged = itemsPerLine != _lastItemsPerLine;
bool maxWidthChanged = _maxWidth != _lastMaxWidth;
ulong itemsHash = ComputeItemsHash(CollectionsMarshal.AsSpan(data.Items)); ulong itemsHash = ComputeItemsHash(CollectionsMarshal.AsSpan(data.Items));
bool itemsChanged = data.Items.Count != _lastItemCount || itemsHash != _lastItemsHash; bool itemsChanged = data.Items.Count != _lastItemCount || itemsHash != _lastItemsHash;
@@ -99,7 +101,6 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase
_lastItemCount = data.Items.Count; _lastItemCount = data.Items.Count;
_lastItemsHash = itemsHash; _lastItemsHash = itemsHash;
_lastItemsPerLine = itemsPerLine; _lastItemsPerLine = itemsPerLine;
_lastMaxWidth = _maxWidth;
_categorizedInventory = data; _categorizedInventory = data;
@@ -112,24 +113,45 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase
_categoryNameTextNode.TextTooltip = data.Category.Description; _categoryNameTextNode.TextTooltip = data.Category.Description;
if (itemsChanged || categoryChanged) if (itemsChanged || categoryChanged)
{
_itemGridNode.ItemsPerLine = itemsPerLine;
if (deferItemCreation)
{
_itemsNeedPopulation = true;
}
else
{ {
using (_itemGridNode.DeferRecalculateLayout()) using (_itemGridNode.DeferRecalculateLayout())
{ {
_itemGridNode.ItemsPerLine = itemsPerLine;
UpdateItemGrid(); UpdateItemGrid();
} }
_itemsNeedPopulation = false;
}
} }
else if (itemsPerLineChanged) else if (itemsPerLineChanged)
{ {
_itemGridNode.ItemsPerLine = itemsPerLine; _itemGridNode.ItemsPerLine = itemsPerLine;
} }
if (categoryChanged || itemsChanged || itemsPerLineChanged || maxWidthChanged) if (categoryChanged || itemsChanged || itemsPerLineChanged)
{ {
RecalculateSize(); RecalculateSize();
} }
} }
public void PopulateItems()
{
if (!_itemsNeedPopulation)
return;
using (_itemGridNode.DeferRecalculateLayout())
{
UpdateItemGrid();
}
_itemsNeedPopulation = false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong ComputeItemsHash(ReadOnlySpan<ItemInfo> items) private static ulong ComputeItemsHash(ReadOnlySpan<ItemInfo> items)
{ {
@@ -238,8 +260,8 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase
{ {
int itemCount = CategorizedInventory.Items.Count; int itemCount = CategorizedInventory.Items.Count;
float cellW = _itemGridNode.Nodes.Count > 0 ? _itemGridNode.Nodes[0].Width : FallbackItemSize; float cellW = ExpectedItemWidth;
float cellH = _itemGridNode.Nodes.Count > 0 ? _itemGridNode.Nodes[0].Height : FallbackItemSize; float cellH = ExpectedItemHeight;
float hPad = _itemGridNode.HorizontalPadding; float hPad = _itemGridNode.HorizontalPadding;
float vPad = _itemGridNode.VerticalPadding; float vPad = _itemGridNode.VerticalPadding;