action-bar hover stops re-deriving, and multisampling asks the GPU

- hovering the action bars re-derived the whole layout graph and hovering bags did not: `GameTooltip:SetOwner(owner, "ANCHOR_NONE")` dropped the tooltip's anchors through the conservative whole-graph derive, and with `UberTooltips` at its shipped default every action-bar button routes through that arm while a bag slot anchors `ANCHOR_RIGHT` and never does — the arm now retargets to the empty target set, and derive frames fall from 15.9 % of hovered frames to 0.2 %
- `gxMultisample` clamps to what the GPU actually offers: Apple GPUs stop at 4x, so a typed 8 was a wgpu validation panic on frame one — the clamp intersects the supported counts of every format the client multisamples into and steps the request down the way the reference's device-init retry loop does, with a warning that says so
- the Multisampling dropdown lands on the Graphics page with the device's own stops, built from the same enumeration that clamps a typed value on the reference's three bindings (`GetMultisampleFormats`, `GetCurrentMultisampleFormat`, `SetMultisampleFormat`) — a fixed Off/2x/4x/8x ladder would put a crash on the menu of every GPU that stops at 4
- capture hardening: a run whose `$WOW_WIN` the display clamped is refused in ~2 s instead of producing a wrong-sized shot to diff, a capture never reads the OS pointer (a cursor resting over the window arms a hover and a tooltip that a cursor an inch to the left does not), and the stall sampler writes into `benilla-config` instead of a hand-built `~/Library/Logs` path

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-08-27 15:13:27 +07:00
co-authored by Claude Fable 5
parent 17a47acd71
commit bf7201f4d3
18 changed files with 820 additions and 33 deletions
+7 -1
View File
@@ -36,6 +36,9 @@ pub(super) struct PointerFeed<'w> {
/// cursor must not fight it. Same arm as `hidden` below, for the same reason: the mouse half
/// of this pass is not ours this frame.
synthetic: Res<'w, super::SyntheticPointer>,
/// A capture owns the pointer for its whole run ([`super::CapturePointerPinned`]) — the OS
/// cursor belongs to whoever is at the keyboard and must not reach the shot.
capture_pinned: Res<'w, super::CapturePointerPinned>,
}
impl PointerFeed<'_> {
@@ -94,7 +97,10 @@ pub(super) fn feed_ui_input(
// WHOLE mouse half out — including the else-arm below, which is the half that matters: a
// `pointer_left_window` between the synthetic press and the synthetic release would disarm
// the gesture the probe is in the middle of.
let synthetic = pointer.synthetic.0;
// The two reasons the OS pointer is not ours this frame: a probe is driving a gesture through
// the real pointer path, or this is a capture (where it is never ours). Same treatment — skip
// the mouse half whole, touch nothing — so they read as one condition here.
let synthetic = pointer.synthetic.0 || pointer.capture_pinned.0;
let (hover, click_consumed, payload_held) = (
&mut pointer.hover,
&mut pointer.click_consumed,
+30
View File
@@ -78,6 +78,31 @@ pub(crate) struct PointerOverUi(pub(crate) bool);
#[derive(Resource, Default)]
pub(crate) struct SyntheticPointer(pub(crate) bool);
/// **A capture never reads the OS pointer** — set for the whole life of a `$WOW_CAPTURE` /
/// `$WOW_CAPTURE_UI` run, and never cleared.
///
/// [`input::feed_ui_input`] treats this exactly like [`SyntheticPointer`]: the mouse half of the
/// pass is skipped entirely, touching nothing. The keyboard half is untouched.
///
/// **Why it is not just `SyntheticPointer`.** They are different statements. That one means "a
/// probe is driving a gesture *right now*", and the drag probe clears it when its gesture ends
/// (`capture::probes::act`) — so borrowing it would re-expose the real cursor for the rest of the
/// run, which is the opposite of the guarantee wanted here.
///
/// **Why it exists at all.** The mouse feed reads `window.cursor_position()`, so a UI capture's
/// pixels depended on where the person at the keyboard had left their mouse: a cursor resting over
/// the window arms a hover and a tooltip that a cursor an inch to the left does not. The old
/// defence was an assumption written in a comment — *"probes park the cursor outside"* — with
/// nothing enforcing it. On 2026-08-26 a `ui-tooltip` A/B came back with an MAE of 5.275 against
/// 0.020 for every other UI scenario; that particular anomaly turned out to be a rebuilt-between-
/// legs mistake, and three attempts to reproduce a cursor-driven one all failed, because
/// `CGWarpMouseCursorPosition` does not synthesise the events winit needs. So the hazard was left
/// on the record as **suspected, unproven** — and this closes it by construction instead, which
/// costs nothing and does not require ever winning that argument. A capture that cannot read the
/// pointer cannot be perturbed by it.
#[derive(Resource, Default)]
pub(crate) struct CapturePointerPinned(pub(crate) bool);
/// The egui dev overlay's half of the pointer arbitration, written each egui pass by the debug
/// panel's `track_pointer_over_ui`. **Defined here, with the arbiter that reads it** (decision
/// 1174 finishing 0026): the type has to exist in a build with no dev overlays compiled in, and
@@ -297,6 +322,11 @@ impl Plugin for UiScriptPlugin {
.init_resource::<UiCostWanted>()
.init_resource::<PointerOverUi>()
.init_resource::<SyntheticPointer>()
// Not `init_resource`: the value IS the answer, read once from the env at build.
.insert_resource(CapturePointerPinned(
std::env::var_os("WOW_CAPTURE").is_some()
|| std::env::var_os("WOW_CAPTURE_UI").is_some(),
))
.init_resource::<EguiPointerOver>()
.init_resource::<InspectMode>()
.init_resource::<PlayerUiHover>()
@@ -1096,6 +1096,91 @@ fn the_world_detail_dropdown_writes_the_cvar_and_the_capsule_follows() {
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// The Graphics page's **Multisampling** row (decision 1631): its stops are enumerated from the
/// DEVICE, not a fixed ladder, and picking one writes the latched `gxMultisample`.
///
/// The device list is seeded Apple-shaped — 1x/2x/4x and no 8x — because that is what this
/// project's own machine reports, and because a fixed ladder offering 8x there is precisely the
/// crash 1631 exists to prevent. A machine with 8x would show four rows off the same code.
#[test]
fn the_multisampling_row_offers_exactly_what_the_device_supports() {
// No explicit `register_cvars` here: `audio_harness` registers `cvars::REGISTERED` whole, and
// the three multisample rows live in it. That is deliberate — if they are ever dropped from
// the registry this test fails rather than papering over it with its own registration.
let mut s = audio_harness();
s.set_multisample_formats(vec![
benilla_ui::script::MultisampleFormat {
color_bits: 32,
depth_bits: 32,
samples: 1,
},
benilla_ui::script::MultisampleFormat {
color_bits: 32,
depth_bits: 32,
samples: 2,
},
benilla_ui::script::MultisampleFormat {
color_bits: 32,
depth_bits: 32,
samples: 4,
},
]);
let mut s = harness_on(s);
s.run("ShowUIPanel(OptionsFrame)").unwrap();
s.run("OptionsFrameCategoryListRowGraphics:Click()")
.unwrap();
assert_eq!(
s.eval::<String>("return OptionsFrameContainerBodyGraphicsRowMultisampleLabel:GetText()")
.unwrap(),
"Multisampling",
"the reference's own MULTISAMPLE global"
);
// "1" is the registered default and means NO multisampling — the first stop, shown in the
// reference's own MULTISAMPLING_FORMAT_STRING wording.
assert_eq!(
s.eval::<String>(
"return OptionsFrameContainerBodyGraphicsRowMultisampleDropdownText:GetText()"
)
.unwrap(),
"32-bit color 32-bit depth 1x multisample"
);
assert!(
s.take_cvar_changes().is_empty(),
"reading the table on select must not write it back"
);
// Three stops, because the device offered three. This is the assertion that would fail if the
// row ever went back to a hardcoded ladder.
s.run("OptionsFrameContainerBodyGraphicsRowMultisampleDropdownButton:Click()")
.unwrap();
assert_eq!(
s.eval::<f64>("return DropDownList1.numButtons").unwrap(),
3.0
);
assert_eq!(
s.eval::<String>("return DropDownList1Button3:GetText()")
.unwrap(),
"32-bit color 32-bit depth 4x multisample"
);
// Picking 4x writes the CVar — live and persisted, even though the picture waits for the next
// launch (the 1629 latch; the camera takes its sample count at spawn).
s.run("DropDownList1Button3:Click()").unwrap();
assert_eq!(
s.take_cvar_changes(),
vec![("gxMultisample".to_string(), "4".to_string())]
);
assert_eq!(
s.eval::<String>(
"return OptionsFrameContainerBodyGraphicsRowMultisampleDropdownText:GetText()"
)
.unwrap(),
"32-bit color 32-bit depth 4x multisample"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// The Nameplates page (0992 — live at last): the three 1.12 UnitName* checkbox rows read the
/// table on select and write their flags on the interface panel's own click kit (checked →
/// CheckBoxOn; these rows carry no soundQuirk).
@@ -1925,10 +2010,10 @@ fn every_row_tooltip_key_resolves_in_the_real_global_strings() {
);
checked += 1;
}
// 26 CVar rows (the Chat page's two bubble switches are 1139's and its Detailed Loot
// 27 CVar rows (the Chat page's two bubble switches are 1139's and its Detailed Loot
// Information + Guild Member Alert are 1589's; Status Bar Text, Mouse Sensitivity
// and Max Camera Distance 1140's; Graphics' Vertical Sync is 1394's and its Windowed Mode
// 1627's; Camera Following Style
// and Max Camera Distance 1140's; Graphics' Vertical Sync is 1394's, its Windowed Mode
// 1627's and its Multisampling 1632's; Camera Following Style
// 1493's; Terrain Distance 1513's) + the Combat page's 14 saved-variable rows (1134) + the Interface page's 6 (3 from
// 1136, Buff Durations 1139, the target-of-target pair 1576), the Action Bars page's 2 (the
// lock 1136, Always Show
@@ -1938,7 +2023,7 @@ fn every_row_tooltip_key_resolves_in_the_real_global_strings() {
// bar as the other two. Camera Following Style is counted on the key it wears at rest (Smart's
// OPTION_TOOLTIP_CAMERA1) and Show When on its own (Always's OPTION_TOOLTIP_TARGETOFTARGET5);
// their other entries ride the same census as the selection moves.
assert_eq!(checked, 54, "every tipped row carries a live 1.12 key");
assert_eq!(checked, 55, "every tipped row carries a live 1.12 key");
assert_eq!(
untipped,
vec![
@@ -2034,15 +2119,16 @@ fn every_flavor_of_row_raises_its_plate_from_the_page_it_lives_on() {
s.errors()
);
}
// 26 of the 27 CVar rows (the Chat page's two bubble switches are 1139's and its Detailed
// 27 of the 28 CVar rows (the Chat page's two bubble switches are 1139's and its Detailed
// Loot Information + Guild Member Alert 1589's; Status Bar Text, Mouse Sensitivity and
// Max Camera Distance 1140's; Vertical Sync 1394's and Windowed Mode 1627's; Camera Following
// Max Camera Distance 1140's; Vertical Sync 1394's, Windowed Mode 1627's and Multisampling
// 1632's; Camera Following
// Style 1493's; Terrain Distance 1513's), plus the Combat page's 14 saved-variable rows (1134), the Interface
// page's 6 (1136, + Buff Durations 1139, + the target-of-target pair 1576), Action Bars' 2
// (the lock 1136, Always Show
// ActionBars 1500), the Chat page's 1 (Remove Chat Hover Delay, 1589) and 6 API rows (Show
// Cloak / Show Helm, 1472; the four multibar switches, 1500).
assert_eq!(raised, 54, "every row but Auto Loot has a 1.12 description");
assert_eq!(raised, 55, "every row but Auto Loot has a 1.12 description");
}
/// The **Combat page** (decision 1134) — the first rows in this window whose store is a
@@ -643,3 +643,73 @@ fn a_hover_sweep_across_owners_costs_no_graph_derivation() {
roster on every slot the cursor passes over (decisions 1388, 1625)."
);
}
/// **The action-bar hover law** (decision 1630, ledger B06): sweeping the cursor across ACTION BAR
/// buttons must cost **zero** derivations of the layout graph.
///
/// The two guards above drive `SetOwner(button, "ANCHOR_RIGHT")` — the bag slot's idiom. The bars
/// do not use it. With `UberTooltips` at its shipped default of `"1"`, every action button routes
/// through `GameTooltip_SetDefaultAnchor` (`ActionBar.xml:510`, and the stance/pet/bonus bars the
/// same), which is `SetOwner(owner, "ANCHOR_NONE")` followed by an explicit `SetPoint` — a
/// completely different arm of the same verb, and the one that DROPS the tooltip's anchors.
///
/// That drop took the conservative touch, so it re-derived the whole graph on every button the
/// cursor crossed — and on nothing else, which is why it survived two rounds of fixing and a
/// live probe: the director's own recording is what named it, every derive frame owned by a
/// `MultiBarBottomLeftButton*` or `BonusActionButton*`. The lesson worth keeping is in the guards
/// as much as the fix: a hover guard that only drives one of two anchor idioms is testing the
/// idiom, not the gesture.
#[test]
fn an_action_bar_hover_sweep_costs_no_graph_derivation() {
let mut s = settled_default_ui();
s.run(
r#"
for i = 1, 12 do
local b = CreateFrame("Button", "BarOwner" .. i)
b:SetPoint("CENTER", 0, 0); b:SetSize(36, 36)
end
bar_n = 0
-- `GameTooltip_SetDefaultAnchor`'s body, which is what every action button actually runs:
-- own the tooltip WITHOUT an anchor, then point it by hand.
function bar_hover()
bar_n = bar_n + 1
local owner = getglobal("BarOwner" .. (math.mod(bar_n, 12) + 1))
GameTooltip:SetOwner(owner, "ANCHOR_NONE")
GameTooltip:ClearAllPoints()
GameTooltip:SetPoint("BOTTOMRIGHT", "UIParent", "BOTTOMRIGHT", -70, 80)
GameTooltip:AddLine("Spell " .. bar_n, 1, 1, 1)
if math.mod(bar_n, 2) == 0 then
GameTooltip:AddLine("Blasts the enemy for 20 damage.", 1, 1, 1, 1)
else
GameTooltip:AddLine("Instant", 1, 1, 1)
end
GameTooltip:Show()
end
"#,
)
.unwrap();
// Positive control across the births and the line pool's growth — both structural.
let born_at = s.layout_derivations();
for _ in 0..40 {
s.run("bar_hover()").unwrap();
app_frame(&mut s);
}
assert!(
s.layout_derivations() > born_at,
"creating twelve buttons must derive the graph — zero here makes the assertion vacuous."
);
let derives_before = s.layout_derivations();
for _ in 0..24 {
s.run("bar_hover()").unwrap();
app_frame(&mut s);
}
let derives = s.layout_derivations() - derives_before;
assert_eq!(
derives, 0,
"24 action-bar hovers derived the layout graph {derives} times. `SetOwner`'s ANCHOR_NONE \
arm drops the tooltip's anchors, which is a retarget to the EMPTY target set and names \
its node like any other (decision 1630, extending 1625)."
);
}