the autocast shine stops eating frames, sunken NPCs surface
- the pet bar's autocast shine cost +4.25 ms a frame for one button: its 92 script-side SetPoints forced two whole-roster anchor solves every frame — the shine now draws from a native lane, re-measured at +0.03 ms with what is drawn bit-faithful to before - an NPC that spawned under its own floor — a cellar wolf, a pet at login — now measures its ground clamp from the server's seat instead of its own last answer, and climbs out - a running cast cost ~4 ms a frame in layout gate walks; a mutated frame now takes one walk, not three, and the settled interface is pinned at zero Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -160,9 +160,16 @@ pub(super) fn drive_script(
|
||||
Option<Res<crate::run_mode::CaptureMode>>,
|
||||
Res<super::UiCostWanted>,
|
||||
),
|
||||
// This frame's `<Minimap>` widget slot, parked for `minimap::emit_minimap` (the UiQuadAppend
|
||||
// producer that fills the hole with tile/arrow quads — decision 0203 phase 1).
|
||||
mut minimap_widget: ResMut<crate::minimap::MinimapWidget>,
|
||||
// The append-lane holes this pass parks for their UiQuadAppend producers, tupled for the
|
||||
// same param-budget squeeze as `run`:
|
||||
// · the `<Minimap>` widget slot (`minimap::emit_minimap` fills it with tile/arrow quads —
|
||||
// decision 0203 phase 1);
|
||||
// · the autocast shine sites (`autocast_shine::emit_shine` draws the spark trails there —
|
||||
// decision 1383, B282).
|
||||
mut parked: (
|
||||
ResMut<crate::minimap::MinimapWidget>,
|
||||
ResMut<crate::autocast_shine::ShineSites>,
|
||||
),
|
||||
// The seam scale the engine's text-metric caches were answered under. When `s` moves (window
|
||||
// resize / fullscreen toggle / uiScale change), every cached measure is stale — see the
|
||||
// invalidation below.
|
||||
@@ -384,6 +391,9 @@ pub(super) fn drive_script(
|
||||
let mut out = Vec::new();
|
||||
let mut assets = world_assets;
|
||||
let mut minimap_slot = None;
|
||||
// This frame's autocast shine sites (decision 1383) — refilled by the loop below like the
|
||||
// minimap slot; the settled and spliced paths above keep last conversion's set.
|
||||
let mut shine_sites: Vec<crate::autocast_shine::ShineSite> = Vec::new();
|
||||
// Hyperlink spans collected while rasterizing message-line Text quads (frame-targeted only —
|
||||
// chat lines; FontString-region links are a later arc), fed back to the engine's click
|
||||
// hit-test after the loop. Rects flip back to the engine's y-up space.
|
||||
@@ -438,8 +448,9 @@ pub(super) fn drive_script(
|
||||
// Every input the conversion below reads, compared against last frame's. Equal inputs make
|
||||
// the loop a pure re-derivation of `UiQuads` the diff at the bottom would discard — at the
|
||||
// LBRS pin that was every single settled frame, ~0.3 ms/frame of glyph re-rasterization for
|
||||
// an identical `Vec`. On a skip, `quads`/`minimap_widget`/the engine's link spans all keep
|
||||
// last frame's values, which the equal inputs prove are this frame's values too.
|
||||
// an identical `Vec`. On a skip, `quads`/the parked minimap slot and shine sites/the
|
||||
// engine's link spans all keep last frame's values, which the equal inputs prove are this
|
||||
// frame's values too.
|
||||
let dims = (w.to_bits(), h.to_bits(), s.to_bits(), dpi.to_bits());
|
||||
let settled = capture.is_none()
|
||||
&& prev.dims == Some(dims)
|
||||
@@ -537,18 +548,19 @@ pub(super) fn drive_script(
|
||||
break 'splice;
|
||||
}
|
||||
// Only entries whose conversion writes quads alone are spliceable — old AND new: an
|
||||
// entry morphing into a side-channel kind must reach the full path's plumbing.
|
||||
let simple = |eq: &benilla_ui::script::ExtractedQuad| {
|
||||
matches!(
|
||||
&eq.content,
|
||||
QuadContent::Frame
|
||||
| QuadContent::Cooldown { .. }
|
||||
| QuadContent::Backdrop { .. }
|
||||
| QuadContent::Texture {
|
||||
portrait_unit: None,
|
||||
..
|
||||
}
|
||||
)
|
||||
// entry morphing into a side-channel kind must reach the full path's plumbing. The
|
||||
// autocast-shine token (decision 1383) is a side-channel kind wearing a Texture's
|
||||
// clothes, so it is excluded by path.
|
||||
let simple = |eq: &benilla_ui::script::ExtractedQuad| match &eq.content {
|
||||
QuadContent::Frame | QuadContent::Cooldown { .. } | QuadContent::Backdrop { .. } => {
|
||||
true
|
||||
}
|
||||
QuadContent::Texture {
|
||||
portrait_unit: None,
|
||||
path,
|
||||
..
|
||||
} => path.as_deref() != Some(crate::autocast_shine::SHINE_TOKEN),
|
||||
_ => false,
|
||||
};
|
||||
if !changed
|
||||
.iter()
|
||||
@@ -563,6 +575,7 @@ pub(super) fn drive_script(
|
||||
let mut scratch_ranges: Vec<std::ops::Range<usize>> = Vec::with_capacity(changed.len());
|
||||
let mut scratch_links = Vec::new();
|
||||
let mut scratch_slot = None;
|
||||
let mut scratch_shine = Vec::new();
|
||||
for &i in &changed {
|
||||
let at = scratch.len();
|
||||
convert_entry(
|
||||
@@ -578,10 +591,11 @@ pub(super) fn drive_script(
|
||||
&mut scratch,
|
||||
&mut scratch_links,
|
||||
&mut scratch_slot,
|
||||
&mut scratch_shine,
|
||||
);
|
||||
scratch_ranges.push(at..scratch.len());
|
||||
}
|
||||
if !scratch_links.is_empty() || scratch_slot.is_some() {
|
||||
if !scratch_links.is_empty() || scratch_slot.is_some() || !scratch_shine.is_empty() {
|
||||
break 'splice;
|
||||
}
|
||||
let counts_stable = changed.iter().zip(&scratch_ranges).all(|(&i, r)| {
|
||||
@@ -681,6 +695,7 @@ pub(super) fn drive_script(
|
||||
&mut out,
|
||||
&mut link_spans,
|
||||
&mut minimap_slot,
|
||||
&mut shine_sites,
|
||||
);
|
||||
spans.push(out.len() as u32);
|
||||
}
|
||||
@@ -714,9 +729,11 @@ pub(super) fn drive_script(
|
||||
// (`OnHyperlinkClick`) hit-tests against exactly what is on screen.
|
||||
script.set_link_spans(link_spans);
|
||||
|
||||
// Park this frame's Minimap widget slot (or clear it — a hidden cluster extracts nothing);
|
||||
// `minimap::emit_minimap` runs later in the frame (UiQuadAppend) and fills the hole.
|
||||
minimap_widget.0 = minimap_slot;
|
||||
// Park this frame's Minimap widget slot and autocast shine sites (or clear them — a hidden
|
||||
// cluster extracts nothing); `minimap::emit_minimap` / `autocast_shine::emit_shine` run
|
||||
// later in the frame (UiQuadAppend) and fill the holes.
|
||||
parked.0 .0 = minimap_slot;
|
||||
parked.1 .0 = shine_sites;
|
||||
drop(extract_span);
|
||||
let us_exa = lap();
|
||||
|
||||
@@ -779,7 +796,8 @@ pub(super) fn drive_script(
|
||||
|
||||
/// One extracted entry converted to its screen quads, pushed onto `out` — with the side channels
|
||||
/// some arms carry: chat link spans (Text), the minimap widget slot (Minimap), the booth pane
|
||||
/// aspects (portrait-bound Texture). The ONE conversion body: the full pass and the per-entry
|
||||
/// aspects (portrait-bound Texture), the autocast shine sites (the token-path Texture, decision
|
||||
/// 1383). The ONE conversion body: the full pass and the per-entry
|
||||
/// splice both call this, which is what makes the splice's output equal the full path's by
|
||||
/// construction. An arm is splice-eligible only if it writes nothing but `out` — keep
|
||||
/// [`splice_simple`] in agreement when an arm's side effects change.
|
||||
@@ -802,6 +820,7 @@ fn convert_entry(
|
||||
String,
|
||||
)>,
|
||||
minimap_slot: &mut Option<crate::minimap::MinimapSlot>,
|
||||
shine_sites: &mut Vec<crate::autocast_shine::ShineSite>,
|
||||
) {
|
||||
let Some(r) = eq.rect else { return };
|
||||
// WoW UI space is y-up from the bottom-left in 768-virtual units; the quad pass is
|
||||
@@ -846,6 +865,30 @@ fn convert_entry(
|
||||
rotation,
|
||||
desaturated,
|
||||
} => {
|
||||
// The autocast-shine token (decision 1383, B282): a shown marker registers WHERE the
|
||||
// shine plays — rect, paint order, clip, alpha, and the star art resolved through
|
||||
// the same resolver as any texture — and draws nothing itself;
|
||||
// `autocast_shine::emit_shine` (UiQuadAppend) animates the sparks there with zero
|
||||
// per-frame script-layout traffic. A side-channel arm, so deliberately NOT
|
||||
// splice-simple (the `simple` predicate above excludes the token by path).
|
||||
if path.as_deref() == Some(crate::autocast_shine::SHINE_TOKEN) {
|
||||
// A no-assets context (the extract test harness; a capture booted without the
|
||||
// archive) records the site with the default handle — the site's GEOMETRY is
|
||||
// the registration, and the live app always has the resolver.
|
||||
let star = assets
|
||||
.as_mut()
|
||||
.and_then(|a| a.sprite_texture(crate::autocast_shine::STAR_TEXTURE, images))
|
||||
.unwrap_or_default();
|
||||
shine_sites.push(crate::autocast_shine::ShineSite {
|
||||
rect,
|
||||
z: eq.z,
|
||||
clip,
|
||||
alpha: eq.alpha,
|
||||
scale: s,
|
||||
texture: star,
|
||||
});
|
||||
return;
|
||||
}
|
||||
// A live unit portrait (`SetPortraitTexture(region, unit)`): sample this token's
|
||||
// source ([`crate::portrait::PortraitImages`]) — the off-screen model bake, or the
|
||||
// ref's 2D TemporaryPortrait stand-in while the model streams in. Absent entry (no
|
||||
@@ -1186,6 +1229,7 @@ mod clip_plumb_tests {
|
||||
app.init_resource::<PortraitImages>();
|
||||
app.init_resource::<crate::portrait::BoothPanes>();
|
||||
app.init_resource::<crate::minimap::MinimapWidget>();
|
||||
app.init_resource::<crate::autocast_shine::ShineSites>();
|
||||
app.init_resource::<crate::ui_script::UiFrameCost>();
|
||||
app.init_resource::<crate::ui_script::UiCostWanted>();
|
||||
app.init_resource::<Time>();
|
||||
@@ -1272,6 +1316,7 @@ mod clip_plumb_tests {
|
||||
app.init_resource::<PortraitImages>();
|
||||
app.init_resource::<crate::portrait::BoothPanes>();
|
||||
app.init_resource::<crate::minimap::MinimapWidget>();
|
||||
app.init_resource::<crate::autocast_shine::ShineSites>();
|
||||
app.init_resource::<crate::ui_script::UiFrameCost>();
|
||||
app.init_resource::<crate::ui_script::UiCostWanted>();
|
||||
app.init_resource::<Time>();
|
||||
@@ -1336,6 +1381,7 @@ mod extract_gate_tests {
|
||||
app.init_resource::<PortraitImages>();
|
||||
app.init_resource::<crate::portrait::BoothPanes>();
|
||||
app.init_resource::<crate::minimap::MinimapWidget>();
|
||||
app.init_resource::<crate::autocast_shine::ShineSites>();
|
||||
app.init_resource::<crate::ui_script::UiFrameCost>();
|
||||
app.init_resource::<crate::ui_script::UiCostWanted>();
|
||||
app.init_resource::<Time>();
|
||||
@@ -1492,6 +1538,85 @@ mod extract_gate_tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// The autocast-shine token (decision 1383, B282): a shown marker converts to a parked SITE
|
||||
/// and zero quads; a spliced frame keeps last conversion's sites; hiding the marker reaches
|
||||
/// the full path and clears them. The site's rect/z/scale are the registration — the
|
||||
/// producer (`autocast_shine::emit_shine`) draws there with no script traffic at all.
|
||||
#[test]
|
||||
fn the_shine_token_parks_a_site_and_never_a_quad() {
|
||||
let mut app = app_from_script(
|
||||
r#"
|
||||
local b = CreateFrame("Button", "PetB")
|
||||
b:SetPoint("BOTTOMLEFT", 72, 652)
|
||||
b:SetSize(30, 30)
|
||||
local shine = b:CreateTexture(nil, "OVERLAY")
|
||||
shine:SetTexture("benilla:autocast-shine")
|
||||
shine:SetAllPoints()
|
||||
marker = b:CreateTexture(nil, "ARTWORK")
|
||||
marker:SetTexture(1, 0, 0)
|
||||
marker:SetAllPoints()
|
||||
"#,
|
||||
);
|
||||
app.update();
|
||||
{
|
||||
let sites = app.world().resource::<crate::autocast_shine::ShineSites>();
|
||||
assert_eq!(sites.0.len(), 1, "one shown token, one site");
|
||||
let site = &sites.0[0];
|
||||
// 1024x768 window ⇒ the 768-virtual scale is 1.0 and y flips through 768: the
|
||||
// button's y-up [652, 682] lands at y-down [86, 116].
|
||||
assert_eq!(site.rect, Rect::new(72.0, 86.0, 102.0, 116.0));
|
||||
assert_eq!(site.scale, 1.0);
|
||||
assert!(site.clip.is_none());
|
||||
assert_eq!(site.alpha, 1.0);
|
||||
assert_eq!(
|
||||
app.world().resource::<UiQuads>().quads.len(),
|
||||
1,
|
||||
"the red marker's quad alone — the token itself draws nothing"
|
||||
);
|
||||
}
|
||||
|
||||
// A paint write elsewhere splices — and the parked site survives it untouched.
|
||||
app.world_mut()
|
||||
.resource_mut::<crate::ui_script::UiCostWanted>()
|
||||
.0 = true;
|
||||
app.world_mut().resource_mut::<UiQuads>().dirty = false;
|
||||
app.world_mut()
|
||||
.non_send_resource_mut::<UiScript>()
|
||||
.run("marker:SetTexture(0, 0, 1)")
|
||||
.unwrap();
|
||||
app.update();
|
||||
assert_eq!(
|
||||
app.world()
|
||||
.resource::<crate::ui_script::UiFrameCost>()
|
||||
.spliced,
|
||||
1,
|
||||
"the marker write splices; the token entry did not change"
|
||||
);
|
||||
assert_eq!(
|
||||
app.world()
|
||||
.resource::<crate::autocast_shine::ShineSites>()
|
||||
.0
|
||||
.len(),
|
||||
1,
|
||||
"a spliced frame keeps last conversion's sites"
|
||||
);
|
||||
|
||||
// Hiding the marker changes the extracted list's length — the full path runs and the
|
||||
// site set empties: the shine goes out with the region that registered it.
|
||||
app.world_mut()
|
||||
.non_send_resource_mut::<UiScript>()
|
||||
.run("getglobal('PetB'):Hide()")
|
||||
.unwrap();
|
||||
app.update();
|
||||
assert!(
|
||||
app.world()
|
||||
.resource::<crate::autocast_shine::ShineSites>()
|
||||
.0
|
||||
.is_empty(),
|
||||
"a hidden token registers nothing"
|
||||
);
|
||||
}
|
||||
|
||||
/// The stitch half: a write that CHANGES an entry's quad count (a cleared texture emits
|
||||
/// nothing) re-derives the span table — and a later splice against the re-derived spans
|
||||
/// still matches the full conversion, in both directions (1 → 0 quads, then 0 → 1).
|
||||
|
||||
Reference in New Issue
Block a user