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:
|
||||
[`../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
|
||||
item.
|
||||
|
|
@ -59,6 +59,9 @@ Gate: hidden inventory, equipment, external-container, vendor, and spell-bar
|
|||
lists are not scanned; reopening immediately displays the authoritative
|
||||
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
|
||||
|
||||
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()
|
||||
{
|
||||
double now = _currentTime();
|
||||
_stepByItemId.Clear();
|
||||
if (!_spellbook.HasCooldownEnchantments)
|
||||
return;
|
||||
|
||||
double now = _currentTime();
|
||||
|
||||
// Retail updates cooldown visibility from UIElement_UIItem::DoHeartbeat,
|
||||
// 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)
|
||||
{
|
||||
if (!IsEffectivelyVisible(list))
|
||||
continue;
|
||||
|
||||
for (int index = 0; index < list.GetNumUIItems(); 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)
|
||||
=> _stepByItemId.TryGetValue(itemId, out int step) ? step : 0;
|
||||
|
||||
|
|
|
|||
|
|
@ -156,6 +156,11 @@ public sealed class Spellbook
|
|||
|
||||
public int LearnedCount => _learnedSpells.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);
|
||||
|
||||
|
|
|
|||
|
|
@ -59,4 +59,82 @@ public sealed class ItemCooldownUiControllerTests
|
|||
Assert.Equal(0u, list.Cell.ActiveCooldownSprite());
|
||||
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