performance night: the interface splices and the walks go filtered

- a single animating entry — the resting blink, a sweeping cooldown — now re-converts alone and stitches into last frame's interface quads instead of dragging the whole batch through conversion, and the staleness sweep stops allocating
- two physics levers measured and flipped: entity clusters and the pre-step transform propagation both default off, −0.28 and −0.40 ms at the Stormwind gates
- the per-frame hygiene basket: env lookups cached once, both mouseover walks filtered by archetype instead of touching every streamed object, ground-shade cards resolve to their nearest shaded root, the nameplate steady frame stops allocating and stops dirtying transforms
- new meters for what remains: an animation memo-sizing counter and a lever that prices the physics stack's rate

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-08-17 16:56:48 +07:00
co-authored by Claude Fable 5
parent 49ba9d599b
commit 4ff2a8ebcf
24 changed files with 1467 additions and 457 deletions
+21 -3
View File
@@ -16,6 +16,8 @@
//! caught (mlua's `Function::call` is a protected call) and returned to the caller, which records
//! them in [`super::Model::errors`] — never a panic, never a print.
use std::borrow::Cow;
use mlua::{Function, Lua, MultiValue, Table, Value};
use super::{Model, ScriptValue, REG_SCRIPTS};
@@ -203,6 +205,22 @@ fn fire(
invoke_with_globals(lua, wrapper, &func, event_name, extra)
}
/// The legacy `argN` global names, pre-spelled through the arities that occur in practice so the
/// per-arg save/set/restore below allocates nothing.
const ARG_NAMES: [&str; 16] = [
"arg1", "arg2", "arg3", "arg4", "arg5", "arg6", "arg7", "arg8", "arg9", "arg10", "arg11",
"arg12", "arg13", "arg14", "arg15", "arg16",
];
/// The global name for arg `i` — **1-based**, matching the globals themselves (`arg1..`). Past
/// [`ARG_NAMES`] it falls back to allocating.
fn arg_name(i: usize) -> Cow<'static, str> {
match ARG_NAMES.get(i - 1) {
Some(&name) => Cow::Borrowed(name),
None => Cow::Owned(format!("arg{i}")),
}
}
/// The RF-0025 calling convention itself — the single home for it, shared by [`fire`] (registry
/// handlers: events, OnUpdate, OnShow/OnHide) and the loader's bottom-up `OnLoad` (which holds the
/// compiled `Function` directly). Sets the legacy `this`/`event`/`arg1..argN` globals and passes the
@@ -222,7 +240,7 @@ pub(crate) fn invoke_with_globals(
let n = extra.len();
let mut saved_args: Vec<Value> = Vec::with_capacity(n);
for i in 1..=n {
saved_args.push(g.get::<Value>(format!("arg{i}"))?);
saved_args.push(g.get::<Value>(arg_name(i).as_ref())?);
}
g.set("this", wrapper.clone())?;
@@ -230,7 +248,7 @@ pub(crate) fn invoke_with_globals(
g.set("event", lua.create_string(ev)?)?;
}
for (i, v) in extra.iter().enumerate() {
g.set(format!("arg{}", i + 1), v.clone())?;
g.set(arg_name(i + 1).as_ref(), v.clone())?;
}
let mut modern: Vec<Value> = Vec::with_capacity(2 + n);
@@ -246,7 +264,7 @@ pub(crate) fn invoke_with_globals(
g.set("this", saved_this)?;
g.set("event", saved_event)?;
for (i, v) in saved_args.into_iter().enumerate() {
g.set(format!("arg{}", i + 1), v)?;
g.set(arg_name(i + 1).as_ref(), v)?;
}
outcome
+44 -26
View File
@@ -1061,20 +1061,24 @@ impl UiScript {
/// alone left those rects zero-width and their anchored neighbours overlapping.
pub fn fontstrings_needing_measure(&mut self) -> Vec<MeasureRequest> {
let mut model = self.model_mut();
let mut out = Vec::new();
let handles: Vec<RegionHandle> = model
// ANY FontString with text — not only the auto-sized ones. A region with both axes
// declared needs no measure for its *layout*, but `GetStringWidth` still has to answer
// with the string's natural width, and only a measure can supply it (decision 0997: a
// kit that reads that number and then SETS a width on the string would otherwise stop
// receiving measures the instant it did so, and start reading its own box back). The
// key cache is what keeps this from costing a re-measure per frame — and the staleness
// check runs allocation-free ([`stale_measure_key`]) so the whole-roster sweep stays
// cheap: the request build (text/font clones, the id mint) is paid only by rows that
// actually need a measure, which on a quiet frame is none.
let needy: Vec<RegionHandle> = model
.region_data
.iter()
// ANY FontString with text — not only the auto-sized ones. A region with both axes
// declared needs no measure for its *layout*, but `GetStringWidth` still has to answer
// with the string's natural width, and only a measure can supply it (decision 0997: a
// kit that reads that number and then SETS a width on the string would otherwise stop
// receiving measures the instant it did so, and start reading its own box back). The
// key cache below is what keeps this from costing a re-measure per frame.
.filter(|(_, d)| d.text.as_deref().is_some_and(|t| !t.is_empty()))
.map(|(&rh, _)| rh)
.filter(|&rh| stale_measure_key(&model, rh).is_some())
.collect();
for rh in handles {
let mut out = Vec::with_capacity(needy.len());
for rh in needy {
if let Some(req) = measure_request_for(&mut model, rh) {
out.push(req);
}
@@ -1149,17 +1153,7 @@ impl UiScript {
/// agree byte for byte on the cache key, or a same-tick measure and the batch pass would each think
/// the other's answer was stale and re-measure forever.
pub(super) fn measure_request_for(model: &mut Model, rh: RegionHandle) -> Option<MeasureRequest> {
let region = model.arena.region(rh)?;
if !matches!(region.kind, crate::widget::RegionKind::FontString) {
return None;
}
// The owner's effective_scale: the host measures at the drawn raster size
// ([`MeasureRequest::scale`]), and the key carries it so a SetScale re-measures.
let scale = model
.arena
.frame(region.owner)
.map(|f| f.effective_scale)
.unwrap_or(1.0);
let (key, scale) = stale_measure_key(model, rh)?;
let d = model.region_data.get(&rh)?;
let text = d.text.clone().filter(|t| !t.is_empty())?;
let wrap_width = d.size.map(|s| s.0).filter(|w| *w > 0.0);
@@ -1167,12 +1161,6 @@ pub(super) fn measure_request_for(model: &mut Model, rh: RegionHandle) -> Option
let height = d.font_height;
let text_height = d.text_height;
let outline = d.outline;
// The shared key recipe ([`RegionData::measure_key`]) — the metric reads (region.rs) check the
// stored measure against the same hash.
let key = d.measure_key(scale);
if d.measured.map(|m| m.key) == Some(key) {
return None;
}
let id = model.region_id(rh);
Some(MeasureRequest {
id,
@@ -1186,3 +1174,33 @@ pub(super) fn measure_request_for(model: &mut Model, rh: RegionHandle) -> Option
key,
})
}
/// The staleness half of [`measure_request_for`], allocation-free: `Some((key, scale))` iff `rh`
/// is a FontString holding text whose stored measure no longer matches its current key. This is
/// the ONE place the "does this region need a measure?" question is answered — the per-frame
/// sweep ([`super::UiScript::fontstrings_needing_measure`]) asks it for every FontString, so it
/// must not clone; the request build above pays the clones only after this says stale.
fn stale_measure_key(model: &Model, rh: RegionHandle) -> Option<(u64, f32)> {
let region = model.arena.region(rh)?;
if !matches!(region.kind, crate::widget::RegionKind::FontString) {
return None;
}
// The owner's effective_scale: the host measures at the drawn raster size
// ([`MeasureRequest::scale`]), and the key carries it so a SetScale re-measures.
let scale = model
.arena
.frame(region.owner)
.map(|f| f.effective_scale)
.unwrap_or(1.0);
let d = model.region_data.get(&rh)?;
if d.text.as_deref().is_none_or(|t| t.is_empty()) {
return None;
}
// The shared key recipe ([`RegionData::measure_key`]) — the metric reads (region.rs) check the
// stored measure against the same hash.
let key = d.measure_key(scale);
if d.measured.map(|m| m.key) == Some(key) {
return None;
}
Some((key, scale))
}
+10 -1
View File
@@ -690,7 +690,10 @@ impl UiScript {
/// Push the player's WMO-containment state onto every Minimap widget (the client's `0xceaa60`).
/// It selects which of the two persisted zoom indices `GetZoom`/`SetZoom` act on, so the zoom
/// buttons drive the indoor level while indoors and the outdoor level while outside. The app
/// owns the containment test, so it owns this push — call it before the script tick.
/// owns the containment test, so it owns this push — call it before the script tick. The
/// caller pushes on the inside↔outside edge and whenever [`Self::minimap_widgets_created`]
/// moved (a widget born after the last transition still gets told, without paying the arena
/// walk every frame).
pub fn set_minimap_inside(&mut self, inside: bool) {
for (_, frame) in self.model_mut().arena.iter_frames_mut() {
if let KindState::Minimap(m) = &mut frame.kind_state {
@@ -699,6 +702,12 @@ impl UiScript {
}
}
/// Monotonic count of Minimap widgets ever created in this VM — the O(1) signal that a new
/// one exists and needs the containment state pushed.
pub fn minimap_widgets_created(&self) -> u64 {
self.model_ref().arena.minimap_created()
}
/// Seed every Minimap widget's two zoom indices from the persisted levels — the client's
/// minimap reset path copying each CVar object's parsed int into its live index
/// (`[0x86f698] ← [[0xb4b410]+0x28]`, `[0x86f69c] ← [[0xb4d90c]+0x28]`; wow-re
+4 -1
View File
@@ -582,7 +582,10 @@ impl RegionData {
pub(crate) fn measure_key(&self, scale: f32) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
self.text.clone().unwrap_or_default().hash(&mut hasher);
// By reference: `String` hashes exactly as `str` does, and this runs for every
// FontString on every frame's staleness sweep — a clone here was ~1.5k heap allocs a
// frame at a city pin, all discarded at the key compare.
self.text.as_deref().unwrap_or("").hash(&mut hasher);
self.font_path.hash(&mut hasher);
self.font_height.map(f32::to_bits).hash(&mut hasher);
self.text_height.map(f32::to_bits).hash(&mut hasher);