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:
Sam
2026-08-17 17:00:19 +07:00
co-authored by Claude Fable 5
parent 81b5ed59cb
commit b3e2f68c11
28 changed files with 1654 additions and 562 deletions
+147 -22
View File
@@ -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).