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:
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user