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:
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user