perf(ui): scope cooldown heartbeat to visible items
This commit is contained in:
parent
f2a015be8e
commit
6b56f4bef2
5 changed files with 132 additions and 3 deletions
|
|
@ -45,7 +45,7 @@ and repeated stable provider polls allocate no report/wrap objects.
|
||||||
Landed evidence:
|
Landed evidence:
|
||||||
[`../research/2026-07-25-slice-h-a1-ui-text-cache.md`](../research/2026-07-25-slice-h-a1-ui-text-cache.md).
|
[`../research/2026-07-25-slice-h-a1-ui-text-cache.md`](../research/2026-07-25-slice-h-a1-ui-text-cache.md).
|
||||||
|
|
||||||
### H-a2 — visible cooldown participants
|
### H-a2 — visible cooldown participants — COMPLETE
|
||||||
|
|
||||||
1. Keep the shared retail heartbeat and one cooldown result per represented
|
1. Keep the shared retail heartbeat and one cooldown result per represented
|
||||||
item.
|
item.
|
||||||
|
|
@ -59,6 +59,9 @@ Gate: hidden inventory, equipment, external-container, vendor, and spell-bar
|
||||||
lists are not scanned; reopening immediately displays the authoritative
|
lists are not scanned; reopening immediately displays the authoritative
|
||||||
cooldown step.
|
cooldown step.
|
||||||
|
|
||||||
|
Landed evidence:
|
||||||
|
[`../research/2026-07-25-slice-h-a2-cooldown-scope.md`](../research/2026-07-25-slice-h-a2-cooldown-scope.md).
|
||||||
|
|
||||||
### H-a3 — equipped-child transition work
|
### H-a3 — equipped-child transition work
|
||||||
|
|
||||||
1. Preserve both frame ordering points: pre-network presentation and
|
1. Preserve both frame ordering points: pre-network presentation and
|
||||||
|
|
|
||||||
24
docs/research/2026-07-25-slice-h-a2-cooldown-scope.md
Normal file
24
docs/research/2026-07-25-slice-h-a2-cooldown-scope.md
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Slice H-a2 — retained cooldown heartbeat scope
|
||||||
|
|
||||||
|
Retail updates item cooldown children from
|
||||||
|
`UIElement_UIItem::DoHeartbeat @ 0x004E1DF0`. Hidden retained subtrees do not
|
||||||
|
receive that heartbeat.
|
||||||
|
|
||||||
|
The shared acdream controller now keeps the same one-result-per-item heartbeat,
|
||||||
|
but:
|
||||||
|
|
||||||
|
- returns before reading the clock or visiting a slot when the authoritative
|
||||||
|
enchantment registry has no cooldown entries;
|
||||||
|
- visits a mounted `UiItemList` only when the list and every ancestor are
|
||||||
|
visible;
|
||||||
|
- clears the frame's step projection first, so a hidden-only item cannot retain
|
||||||
|
a stale ring;
|
||||||
|
- recomputes the authoritative step on the first heartbeat after the panel
|
||||||
|
becomes visible;
|
||||||
|
- retains existing future-slot and shared-cooldown-group behavior.
|
||||||
|
|
||||||
|
Focused tests cover shared groups/future cells, hidden-parent suppression and
|
||||||
|
reopen, and the zero-work empty-registry path.
|
||||||
|
|
||||||
|
Release gate: 3,781 App tests passed / 3 skipped; 8,265 complete-solution
|
||||||
|
tests passed / 5 skipped.
|
||||||
|
|
@ -49,14 +49,22 @@ public sealed class ItemCooldownUiController
|
||||||
|
|
||||||
public void Tick()
|
public void Tick()
|
||||||
{
|
{
|
||||||
double now = _currentTime();
|
|
||||||
_stepByItemId.Clear();
|
_stepByItemId.Clear();
|
||||||
|
if (!_spellbook.HasCooldownEnchantments)
|
||||||
|
return;
|
||||||
|
|
||||||
|
double now = _currentTime();
|
||||||
|
|
||||||
// Retail updates cooldown visibility from UIElement_UIItem::DoHeartbeat,
|
// Retail updates cooldown visibility from UIElement_UIItem::DoHeartbeat,
|
||||||
// not while drawing. Query each represented item once so an expired
|
// not while drawing. Query each represented item once so an expired
|
||||||
// registry node can be removed safely before the render walk.
|
// registry node can be removed safely before the render walk. A hidden
|
||||||
|
// retained subtree receives no heartbeat in retail, so only lists whose
|
||||||
|
// complete ancestry is visible participate.
|
||||||
foreach (UiItemList list in _lists)
|
foreach (UiItemList list in _lists)
|
||||||
{
|
{
|
||||||
|
if (!IsEffectivelyVisible(list))
|
||||||
|
continue;
|
||||||
|
|
||||||
for (int index = 0; index < list.GetNumUIItems(); index++)
|
for (int index = 0; index < list.GetNumUIItems(); index++)
|
||||||
{
|
{
|
||||||
UiItemSlot? cell = list.GetItem(index);
|
UiItemSlot? cell = list.GetItem(index);
|
||||||
|
|
@ -83,6 +91,17 @@ public sealed class ItemCooldownUiController
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static bool IsEffectivelyVisible(UiElement element)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(element);
|
||||||
|
for (UiElement? current = element; current is not null; current = current.Parent)
|
||||||
|
{
|
||||||
|
if (!current.Visible)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
internal int GetOverlayStep(uint itemId)
|
internal int GetOverlayStep(uint itemId)
|
||||||
=> _stepByItemId.TryGetValue(itemId, out int step) ? step : 0;
|
=> _stepByItemId.TryGetValue(itemId, out int step) ? step : 0;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -156,6 +156,11 @@ public sealed class Spellbook
|
||||||
|
|
||||||
public int LearnedCount => _learnedSpells.Count;
|
public int LearnedCount => _learnedSpells.Count;
|
||||||
public int ActiveCount => _activeById.Count;
|
public int ActiveCount => _activeById.Count;
|
||||||
|
public bool HasCooldownEnchantments =>
|
||||||
|
_enchantmentOrderByBucket.TryGetValue(
|
||||||
|
CooldownBucket,
|
||||||
|
out List<uint>? order)
|
||||||
|
&& order.Count != 0;
|
||||||
|
|
||||||
public bool Knows(uint spellId) => _learnedSpells.ContainsKey(spellId);
|
public bool Knows(uint spellId) => _learnedSpells.ContainsKey(spellId);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,4 +59,82 @@ public sealed class ItemCooldownUiControllerTests
|
||||||
Assert.Equal(0u, list.Cell.ActiveCooldownSprite());
|
Assert.Equal(0u, list.Cell.ActiveCooldownSprite());
|
||||||
Assert.Equal(0, spellbook.ActiveCount);
|
Assert.Equal(0, spellbook.ActiveCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Hidden_ancestor_removes_list_from_retail_heartbeat_scope()
|
||||||
|
{
|
||||||
|
var objects = new ClientObjectTable();
|
||||||
|
objects.AddOrUpdate(new ClientObject
|
||||||
|
{
|
||||||
|
ObjectId = 0x5001u,
|
||||||
|
CooldownId = 42u,
|
||||||
|
CooldownDuration = 30d,
|
||||||
|
});
|
||||||
|
var spellbook = new Spellbook();
|
||||||
|
spellbook.OnEnchantmentAdded(new ActiveEnchantmentRecord(
|
||||||
|
SpellId: 0x802Au,
|
||||||
|
LayerId: 1u,
|
||||||
|
Duration: 30d,
|
||||||
|
CasterGuid: 0u,
|
||||||
|
Bucket: Spellbook.CooldownBucket,
|
||||||
|
StartTime: 100d));
|
||||||
|
var root = new UiPanel();
|
||||||
|
var window = new UiPanel();
|
||||||
|
var list = new UiItemList();
|
||||||
|
list.Cell.SetItem(0x5001u, 99u);
|
||||||
|
root.AddChild(window);
|
||||||
|
window.AddChild(list);
|
||||||
|
uint[] sprites = Enumerable.Range(1, 10)
|
||||||
|
.Select(index => 0x06000000u + (uint)index)
|
||||||
|
.ToArray();
|
||||||
|
ItemCooldownUiController controller = ItemCooldownUiController.Bind(
|
||||||
|
root,
|
||||||
|
spellbook,
|
||||||
|
objects,
|
||||||
|
() => 112.5d,
|
||||||
|
new ItemCooldownAssets(sprites));
|
||||||
|
Assert.Equal(sprites[5], list.Cell.ActiveCooldownSprite());
|
||||||
|
|
||||||
|
window.Visible = false;
|
||||||
|
controller.Tick();
|
||||||
|
Assert.Equal(0u, list.Cell.ActiveCooldownSprite());
|
||||||
|
|
||||||
|
window.Visible = true;
|
||||||
|
controller.Tick();
|
||||||
|
Assert.Equal(sprites[5], list.Cell.ActiveCooldownSprite());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Empty_cooldown_registry_skips_clock_and_item_walk()
|
||||||
|
{
|
||||||
|
var objects = new ClientObjectTable();
|
||||||
|
objects.AddOrUpdate(new ClientObject
|
||||||
|
{
|
||||||
|
ObjectId = 0x5001u,
|
||||||
|
CooldownId = 42u,
|
||||||
|
CooldownDuration = 30d,
|
||||||
|
});
|
||||||
|
var spellbook = new Spellbook();
|
||||||
|
var root = new UiPanel();
|
||||||
|
var list = new UiItemList();
|
||||||
|
list.Cell.SetItem(0x5001u, 99u);
|
||||||
|
root.AddChild(list);
|
||||||
|
int clockReads = 0;
|
||||||
|
ItemCooldownUiController controller = ItemCooldownUiController.Bind(
|
||||||
|
root,
|
||||||
|
spellbook,
|
||||||
|
objects,
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
clockReads++;
|
||||||
|
return 100d;
|
||||||
|
},
|
||||||
|
new ItemCooldownAssets(
|
||||||
|
Enumerable.Range(1, 10).Select(index => (uint)index).ToArray()));
|
||||||
|
|
||||||
|
controller.Tick();
|
||||||
|
|
||||||
|
Assert.Equal(0, clockReads);
|
||||||
|
Assert.Equal(0u, list.Cell.ActiveCooldownSprite());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue