the hover tax is pinned and cut, and a spell-less macro is a button
- a tooltip change costs ~1.3 ms — five whole-interface recomputes, each working as designed: the quad re-convert at 474 µs, the Lua tick at 287, the layout resolve at 200, bevy's `Assets<Mesh>` tax at ~270 and the mesh rebuild at 146 — pinned with new hover probes (a jitter dial, a `WOW_PROBE_HOVER_DUTY` duty cycle so run drift cancels, an `AssetChanged` tax meter), and the layout preamble's two whole-roster scans become lists maintained at the writer (52–83 µs → 1–11) - the splice compared this frame's entries against last frame's by index, so one inserted highlight shifted the 160 entries behind it and read as "everything changed"; it now aligns on `z` — an identity, not a position — and a `Text` entry that writes only quads became spliceable, so a tooltip change re-converts 22 entries instead of 520 (`convert` +439 µs → +107), with `WOW_UI_SPLICE_VERIFY` running the full conversion behind every spliced frame: 0 mismatches over 1,078 - a spell-less macro is a usable action button, not the 0.4 grey — the reference's usable compute has a dedicated leg answering usable for the bound-spell zero, cross-checked at the bytes in the reference's disassembly — and the button's Name line is written unconditionally from `GetActionText` as the reference does, so the macro's name sits under its icon and a `/cast` of a spell the character does not know now greys, which is also 1.12's behaviour (B340) - a capture bounds its run in wall-clock seconds, because a frame cap is no cap at the ~1 fps a starved `nextDrawable` leaves — 1800 frames is thirty minutes there — and the teardown backstop runs on every run as a liveness guarantee, carrying the exit code the run asked for; the Multisampling row reads Off / 2x / 4x, since "1x" contradicts the no-multisampling byte both of the reference's enumerators normalise to and the bit-depth prefix was identical on every row Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -725,11 +725,15 @@ impl UiScript {
|
||||
// Snapshotted only for frames that actually CARRY a handler (a handful, usually zero), and
|
||||
// only past the tier-1 gate above — an idle frame returns before this line, so the change
|
||||
// costs nothing on the quiet path decision 0740 exists to protect.
|
||||
//
|
||||
// Read off `SetScript`'s maintained list (1446's shape, extended in 1634), NOT by filtering
|
||||
// the whole `scripts` map: "a handful, usually zero" was true of the RESULT and never of
|
||||
// the search, which cost a string compare per frame in a 3,988-frame roster and was the
|
||||
// preamble's biggest phase — larger than everything the graph does with the answer.
|
||||
let watched: Vec<(FrameHandle, Option<Rect>)> = model
|
||||
.scripts
|
||||
.on_size_changed_frames
|
||||
.iter()
|
||||
.filter(|(_, kinds)| kinds.contains("OnSizeChanged"))
|
||||
.map(|(&h, _)| (h, model.resolved.get(&h).copied()))
|
||||
.map(|&h| (h, model.resolved.get(&h).copied()))
|
||||
.collect();
|
||||
pre.watched = pre.lap();
|
||||
// ── The graph: the ledger's cache, or a fresh derivation ─────────────────────────────
|
||||
|
||||
@@ -263,6 +263,13 @@ pub(crate) struct Model {
|
||||
/// irrelevant here: the tick sorts its visible subset by frame id every pass (the
|
||||
/// deterministic-dispatch law stays where it was).
|
||||
pub(crate) on_update_frames: Vec<FrameHandle>,
|
||||
/// The frames carrying an `OnSizeChanged` script — 1446's list, for the layout resolve's
|
||||
/// "before" snapshot instead of the tick. Same one writer, same reason: that snapshot's own
|
||||
/// comment says it covers "a handful, usually zero", and it found them by filtering the WHOLE
|
||||
/// `scripts` map with a string compare per entry, on every let-through resolve — 36–72 µs a
|
||||
/// resolve at a 3,988-frame roster, which made it the biggest phase left in the preamble
|
||||
/// (decision 1634's `[layout-pre] watched=`).
|
||||
pub(crate) on_size_changed_frames: Vec<FrameHandle>,
|
||||
/// `event name → frames registered for it` (RegisterEvent), in **registration order** — an
|
||||
/// ordered Vec, never a set: the client's `SignalEvent 0x703e50` walks a per-event listener
|
||||
/// LIST, so cross-frame dispatch order is a law, not an accident (the abbey territory-line
|
||||
@@ -1390,6 +1397,7 @@ impl Model {
|
||||
region_names: HashMap::new(),
|
||||
scripts: HashMap::new(),
|
||||
on_update_frames: Vec::new(),
|
||||
on_size_changed_frames: Vec::new(),
|
||||
event_to_frames: HashMap::new(),
|
||||
frame_events: HashMap::new(),
|
||||
focused_editbox: None,
|
||||
|
||||
@@ -360,16 +360,26 @@ fn set_script(lua: &Lua, this: &Table, name: &str, func: Option<Function>) -> ml
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
// `insert` answering true = this frame did not have the kind before — the tick's
|
||||
// OnUpdate list rides that edge (decision 1446; `scripts` has no other writer).
|
||||
if model.scripts.entry(h).or_default().insert(kind) && kind == "OnUpdate" {
|
||||
model.on_update_frames.push(h);
|
||||
if model.scripts.entry(h).or_default().insert(kind) {
|
||||
match kind {
|
||||
"OnUpdate" => model.on_update_frames.push(h),
|
||||
"OnSizeChanged" => model.on_size_changed_frames.push(h),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
per.set(kind, Value::Nil)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
if let Some(set) = model.scripts.get_mut(&h) {
|
||||
if set.remove(&kind) && kind == "OnUpdate" {
|
||||
model.on_update_frames.retain(|&x| x != h);
|
||||
if set.remove(&kind) {
|
||||
match kind {
|
||||
"OnUpdate" => model.on_update_frames.retain(|&x| x != h),
|
||||
"OnSizeChanged" => {
|
||||
model.on_size_changed_frames.retain(|&x| x != h);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,3 +159,54 @@ fn set_script_on_size_changed_is_accepted_because_the_resolve_pass_fires_it() {
|
||||
s.resolve();
|
||||
assert!(s.eval::<bool>("return ran").unwrap(), "…and it FIRED");
|
||||
}
|
||||
|
||||
/// **The watch list is exactly the frames carrying the script.** The resolve's "before" snapshot
|
||||
/// reads `SetScript`'s maintained `on_size_changed_frames` rather than filtering the whole scripts
|
||||
/// map (decision 1634), so the list itself is now load-bearing: `SetScript(…, nil)` must remove
|
||||
/// the frame, and re-registering must not enrol it twice.
|
||||
///
|
||||
/// Asserted on the LIST, not on the fire count, and that is the point. A stale entry fires nothing
|
||||
/// — `fire` looks the handler up in Lua and finds nil — so behaviour hides the bug completely and
|
||||
/// the only symptom is the snapshot silently growing forever, one dead frame at a time. (Written
|
||||
/// as a fire-count test first; it passed with the `retain` deleted.)
|
||||
#[test]
|
||||
fn the_on_size_changed_watch_list_is_exactly_the_frames_carrying_the_script() {
|
||||
use crate::script::model::Model;
|
||||
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.run(
|
||||
r#"
|
||||
fires = 0
|
||||
Watched = CreateFrame("Frame", "WatchedPanel")
|
||||
Watched:SetPoint("BOTTOMLEFT", 0, 0)
|
||||
Watched:SetSize(10, 10)
|
||||
Other = CreateFrame("Frame", "UnwatchedPanel")
|
||||
Other:SetScript("OnShow", function() end) -- another kind never enrols
|
||||
local bump = function() fires = fires + 1 end
|
||||
Watched:SetScript("OnSizeChanged", bump)
|
||||
Watched:SetScript("OnSizeChanged", bump) -- re-registering is not a second enrolment
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let watched = |s: &crate::script::UiScript| {
|
||||
s.lua()
|
||||
.app_data_ref::<Model>()
|
||||
.expect("model app_data")
|
||||
.on_size_changed_frames
|
||||
.len()
|
||||
};
|
||||
assert_eq!(watched(&s), 1, "one frame carries the script");
|
||||
|
||||
s.resolve();
|
||||
s.run("Watched:SetSize(40, 40)").unwrap();
|
||||
s.resolve();
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return fires").unwrap(),
|
||||
2,
|
||||
"one fire per resolve whose size moved — the 0×0→10×10 birth, then the resize"
|
||||
);
|
||||
|
||||
s.run(r#"Watched:SetScript("OnSizeChanged", nil)"#).unwrap();
|
||||
assert_eq!(watched(&s), 0, "clearing the script un-watches the frame");
|
||||
}
|
||||
|
||||
@@ -671,16 +671,16 @@ fn cell(model: &Model, rh: crate::widget::RegionHandle) -> Cell {
|
||||
/// frames whose lines haven't been measured yet (the XML default size holds until the measure
|
||||
/// round-trip lands, one frame later — the same convergence the Lua loop had).
|
||||
pub(super) fn layout_tooltips(model: &mut Model) {
|
||||
// The arena's tooltip registry, not the resolve's whole frame roster: this pre-pass runs at
|
||||
// the top of EVERY resolve, and finding two or three tooltips by scanning ~4000 ids was most
|
||||
// of what it cost (decision 1634). `frame_to_id` still gates each one — a tooltip outside the
|
||||
// resolve's roster would mint an id here and take the ledger's conservative branch with it.
|
||||
let tips: Vec<FrameHandle> = model
|
||||
.frame_to_id
|
||||
.keys()
|
||||
.arena
|
||||
.tooltip_kinds()
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|&h| {
|
||||
matches!(
|
||||
model.arena.frame(h).map(|f| &f.kind_state),
|
||||
Some(KindState::Tooltip(_))
|
||||
)
|
||||
})
|
||||
.filter(|h| model.frame_to_id.contains_key(h))
|
||||
.collect();
|
||||
for h in tips {
|
||||
let (num, lefts, rights, min_w, pad_w) = match model.arena.frame(h).map(|f| &f.kind_state) {
|
||||
@@ -767,7 +767,14 @@ pub(super) fn tick_fades(lua: &Lua) {
|
||||
let mut finished: Vec<FrameHandle> = Vec::new();
|
||||
{
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
for &h in model.frame_to_id.keys() {
|
||||
// The arena's tooltip registry (1634) — this runs every tick, and the roster it used to
|
||||
// walk is ~4000 entries at a corpus UI. The `frame_to_id` gate is kept: `hide_tooltip`
|
||||
// below writes layout, and a frame outside the resolve's roster takes the ledger's
|
||||
// conservative branch.
|
||||
for &h in model.arena.tooltip_kinds() {
|
||||
if !model.frame_to_id.contains_key(&h) {
|
||||
continue;
|
||||
}
|
||||
let Some(frame) = model.arena.frame(h) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
@@ -262,15 +262,19 @@ fn update_bar(lua: &Lua, h: FrameHandle, unit: Option<&UnitState>) {
|
||||
pub(super) fn on_unit_push(lua: &Lua, token: &str) {
|
||||
let hits: Vec<FrameHandle> = {
|
||||
let model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
// The arena's tooltip registry (1634), gated by the resolve's roster exactly as the walk
|
||||
// it replaces was — `update_bar` writes layout.
|
||||
model
|
||||
.frame_to_id
|
||||
.keys()
|
||||
.arena
|
||||
.tooltip_kinds()
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|&h| {
|
||||
matches!(
|
||||
model.arena.frame(h).map(|f| &f.kind_state),
|
||||
Some(KindState::Tooltip(t)) if t.unit_token.as_deref() == Some(token)
|
||||
)
|
||||
model.frame_to_id.contains_key(&h)
|
||||
&& matches!(
|
||||
model.arena.frame(h).map(|f| &f.kind_state),
|
||||
Some(KindState::Tooltip(t)) if t.unit_token.as_deref() == Some(token)
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user