the walking tax banded, and the minimap stops rewriting the world
- a panning UI batch rides its Transform instead of rewriting its mesh, minimap tiles emit unclipped (the mask shader already zeroes the corners), and the blip layer's origin snaps to half a logical pixel — steady UI mesh writes while moving drop from ~183/s to 10–14/s, and three minimap quads stop arming change detection over ~40k world meshes - the classify twin GC comes off the despawn edge: a part despawn schedules the full walk behind a 2 s deadline instead of running it that frame — 12–22 full walks a second while walking, gone - the walking tax measured and banded at Goldshire: parked 10.7 → moving 14.1 ms/frame of process CPU, ~+2.5 after the two fixes above - WOW_PART_CHURN and WOW_MESH_HOLDERS count part spawn/despawn frames and name which entities hold a churning mesh; the capture rig's `at:` pin learns a facing Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -54,7 +54,7 @@
|
|||||||
//! | `gear:<t>` | `.character premade gear <t>` | SEC_BASIC_ADMIN 4 | name or entry; levels up + equips |
|
//! | `gear:<t>` | `.character premade gear <t>` | SEC_BASIC_ADMIN 4 | name or entry; levels up + equips |
|
||||||
//! | `spec:<t>` | `.character premade spec <t>` | SEC_BASIC_ADMIN 4 | resets talents, learns the tree |
|
//! | `spec:<t>` | `.character premade spec <t>` | SEC_BASIC_ADMIN 4 | resets talents, learns the tree |
|
||||||
//! | `at:<name>` | `.tele <name>` | SEC_TICKETMASTER 2 | 997 named rows in `game_tele` |
|
//! | `at:<name>` | `.tele <name>` | SEC_TICKETMASTER 2 | 997 named rows in `game_tele` |
|
||||||
//! | `at:m,x,y,z` | `.go xyz x y z m` | SEC_TICKETMASTER 2 | |
|
//! | `at:m,x,y,z[,o]` | `.go xyz x y z m` (`.go xyzo` with `o`) | SEC_TICKETMASTER 2 | pin the facing when a motion probe walks |
|
||||||
//! | `gm:on\|off` | `.gm on\|off` | SEC_GAMEMASTER 3 | see 0649 on why `off` matters |
|
//! | `gm:on\|off` | `.gm on\|off` | SEC_GAMEMASTER 3 | see 0649 on why `off` matters |
|
||||||
//!
|
//!
|
||||||
//! Probe accounts are gmlevel **6**, so every one of these lands. Pass `gear:?` (or `spec:?`) to
|
//! Probe accounts are gmlevel **6**, so every one of these lands. Pass `gear:?` (or `spec:?`) to
|
||||||
@@ -506,15 +506,21 @@ fn build_steps(spec: &RigSpec, dead: bool) -> Vec<String> {
|
|||||||
}
|
}
|
||||||
if let Some(at) = &spec.at {
|
if let Some(at) = &spec.at {
|
||||||
steps.push(match parse_point(at) {
|
steps.push(match parse_point(at) {
|
||||||
Some((map, x, y, z)) => format!(".go xyz {x} {y} {z} {map}"),
|
Some((map, x, y, z, Some(o))) => format!(".go xyzo {x} {y} {z} {o} {map}"),
|
||||||
|
Some((map, x, y, z, None)) => format!(".go xyz {x} {y} {z} {map}"),
|
||||||
None => format!(".tele {at}"),
|
None => format!(".tele {at}"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
steps
|
steps
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `map,x,y,z` → the `.go xyz` argument order (`x y z map`). Anything else is a `game_tele` name.
|
/// `map,x,y,z[,o]` → `.go xyz x y z map`, or `.go xyzo x y z o map` when a facing is given
|
||||||
fn parse_point(at: &str) -> Option<(i32, f32, f32, f32)> {
|
/// (VERIFIED vmangos `Chat.cpp:408`: `xyzo`, SEC_TICKETMASTER like `xyz`). Anything else is a
|
||||||
|
/// `game_tele` name. The facing is load-bearing exactly when a motion probe walks from the
|
||||||
|
/// pin: `W` follows the BODY's facing, and each probe character keeps whatever facing it last
|
||||||
|
/// had — an unpinned facing sent probe0 and probe4 down different routes from the same point,
|
||||||
|
/// which invalidated a cross-slot A/B before 1462's sitting caught it.
|
||||||
|
fn parse_point(at: &str) -> Option<(i32, f32, f32, f32, Option<f32>)> {
|
||||||
let mut parts = at.split(',').map(str::trim);
|
let mut parts = at.split(',').map(str::trim);
|
||||||
let map = parts.next()?.parse().ok()?;
|
let map = parts.next()?.parse().ok()?;
|
||||||
let (x, y, z) = (
|
let (x, y, z) = (
|
||||||
@@ -522,7 +528,11 @@ fn parse_point(at: &str) -> Option<(i32, f32, f32, f32)> {
|
|||||||
parts.next()?.parse().ok()?,
|
parts.next()?.parse().ok()?,
|
||||||
parts.next()?.parse().ok()?,
|
parts.next()?.parse().ok()?,
|
||||||
);
|
);
|
||||||
parts.next().is_none().then_some((map, x, y, z))
|
let o = match parts.next() {
|
||||||
|
Some(o) => Some(o.parse().ok()?),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
parts.next().is_none().then_some((map, x, y, z, o))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rig_char_name(spec: &RigSpec) -> Option<String> {
|
fn rig_char_name(spec: &RigSpec) -> Option<String> {
|
||||||
@@ -743,9 +753,14 @@ mod tests {
|
|||||||
build_steps(&point, false),
|
build_steps(&point, false),
|
||||||
vec![".go xyz -1277.5 124 131.2 1"]
|
vec![".go xyz -1277.5 124 131.2 1"]
|
||||||
);
|
);
|
||||||
|
let faced = RigSpec::parse("at:0,-9399.03,10.41,59.83,0.0").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
build_steps(&faced, false),
|
||||||
|
vec![".go xyzo -9399.03 10.41 59.83 0 0"]
|
||||||
|
);
|
||||||
assert_eq!(parse_point("ThunderBluff"), None);
|
assert_eq!(parse_point("ThunderBluff"), None);
|
||||||
assert_eq!(parse_point("1,2,3"), None); // three numbers is not a map + point
|
assert_eq!(parse_point("1,2,3"), None); // three numbers is not a map + point
|
||||||
assert_eq!(parse_point("1,2,3,4,5"), None);
|
assert_eq!(parse_point("1,2,3,4,5,6"), None); // and six is past even a faced point
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -517,7 +517,11 @@ fn emit_minimap(
|
|||||||
z_key: slot.z,
|
z_key: slot.z,
|
||||||
texture: Some(handle.clone()),
|
texture: Some(handle.clone()),
|
||||||
color: [tint[0], tint[1], tint[2], slot.alpha],
|
color: [tint[0], tint[1], tint[2], slot.alpha],
|
||||||
clip: Some(slot.rect),
|
// No CPU clip (1463): the mask shader already zeroes everything outside
|
||||||
|
// `mask_rect` (`ui_quad.wgsl`'s `inside` test), and clipping a PANNING tile
|
||||||
|
// re-cut its quad every frame — constant positions, churning UVs — which is
|
||||||
|
// exactly the shape the batcher's pan gate cannot ride on a `Transform`.
|
||||||
|
// Unclipped, the tile is a pure translation and never rewrites its mesh.
|
||||||
mask: mask.clone(),
|
mask: mask.clone(),
|
||||||
..default()
|
..default()
|
||||||
});
|
});
|
||||||
@@ -552,6 +556,14 @@ fn emit_minimap(
|
|||||||
}
|
}
|
||||||
let win = window.iter().next();
|
let win = window.iter().next();
|
||||||
let cursor = win.and_then(|w| w.cursor_position());
|
let cursor = win.and_then(|w| w.cursor_position());
|
||||||
|
// The player's pan term, quantized to a half-logical-pixel grid (one device px at 2×):
|
||||||
|
// every blip offset — and the rim arrows' bearing — derives from `wx`/`wy`, so the whole
|
||||||
|
// blip layer steps together a few times a second instead of re-emitting sub-pixel-shifted
|
||||||
|
// quads every frame while walking (1463; a 0.07 px/frame slide on a 16 px icon is not a
|
||||||
|
// visible motion, but each slide rewrote the batch mesh and armed the world's
|
||||||
|
// `AssetChanged` scans). The blips' world positions stay exact — only the shared pan
|
||||||
|
// origin snaps.
|
||||||
|
let q = 0.5 / blip_px_per_yd;
|
||||||
blips::BlipCtx {
|
blips::BlipCtx {
|
||||||
center,
|
center,
|
||||||
side,
|
side,
|
||||||
@@ -559,8 +571,8 @@ fn emit_minimap(
|
|||||||
radius_yd: (side * 0.5) / blip_px_per_yd,
|
radius_yd: (side * 0.5) / blip_px_per_yd,
|
||||||
z: slot.z,
|
z: slot.z,
|
||||||
alpha: slot.alpha,
|
alpha: slot.alpha,
|
||||||
wx,
|
wx: (wx / q).round() * q,
|
||||||
wy,
|
wy: (wy / q).round() * q,
|
||||||
wz: wow[2],
|
wz: wow[2],
|
||||||
cursor,
|
cursor,
|
||||||
// The same point in UI space (y-up, ÷s through the 0582/0584 seam — the tooltip's
|
// The same point in UI space (y-up, ÷s through the 0582/0584 seam — the tooltip's
|
||||||
|
|||||||
@@ -295,6 +295,101 @@ pub(super) fn count_mesh_events(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `WOW_PART_CHURN=1`: per-second M2-part churn. `rm_frames` is the count of frames with ≥1
|
||||||
|
/// `MeshMaterial3d<WowModelMaterial>` removal — exactly the predicate that promotes
|
||||||
|
/// `classify_water_side` to its full walk (0930's twin-GC mark), so a regime reading ~60 here
|
||||||
|
/// full-walks every frame. `mat_mod` counts `WowModelMaterial` asset Modified events — the
|
||||||
|
/// global asset-changed tick that wakes every `AssetChanged` scan.
|
||||||
|
pub(super) fn count_part_churn(
|
||||||
|
added: Query<(), Added<MeshMaterial3d<benilla_assets::materials::WowModelMaterial>>>,
|
||||||
|
mut removed: RemovedComponents<MeshMaterial3d<benilla_assets::materials::WowModelMaterial>>,
|
||||||
|
mut mat_events: MessageReader<
|
||||||
|
bevy::asset::AssetEvent<benilla_assets::materials::WowModelMaterial>,
|
||||||
|
>,
|
||||||
|
time: Res<Time>,
|
||||||
|
mut acc: Local<(f32, u32, u32, u32, u32)>,
|
||||||
|
) {
|
||||||
|
let (last, add_n, rm_n, rm_frames, mat_mod) = &mut *acc;
|
||||||
|
*add_n += added.iter().count() as u32;
|
||||||
|
let rm = removed.read().count() as u32;
|
||||||
|
*rm_n += rm;
|
||||||
|
if rm > 0 {
|
||||||
|
*rm_frames += 1;
|
||||||
|
}
|
||||||
|
*mat_mod += mat_events
|
||||||
|
.read()
|
||||||
|
.filter(|e| matches!(e, bevy::asset::AssetEvent::Modified { .. }))
|
||||||
|
.count() as u32;
|
||||||
|
if time.elapsed_secs() - *last >= 1.0 {
|
||||||
|
eprintln!(
|
||||||
|
"[part-churn] parts +{add_n} -{rm_n} rm_frames={rm_frames}/s mat_modified={mat_mod}/s"
|
||||||
|
);
|
||||||
|
(*add_n, *rm_n, *rm_frames, *mat_mod) = (0, 0, 0, 0);
|
||||||
|
*last = time.elapsed_secs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `WOW_MESH_HOLDERS=1`: once a second, the set of mesh asset ids Modified in the last second
|
||||||
|
/// and the component signature of every entity holding one — [`count_mesh_events`] counts and
|
||||||
|
/// samples the ids, this names the writer by its holder's archetype (an id alone says nothing).
|
||||||
|
/// Exclusive, like [`arch_census`], for the same reason: the signature needs the live archetype.
|
||||||
|
pub(super) fn mesh_holders(
|
||||||
|
world: &mut World,
|
||||||
|
mut cursor: Local<Option<bevy::ecs::message::MessageCursor<bevy::asset::AssetEvent<Mesh>>>>,
|
||||||
|
mut acc: Local<(f32, std::collections::HashSet<bevy::asset::AssetId<Mesh>>)>,
|
||||||
|
) {
|
||||||
|
let messages = world.resource::<bevy::ecs::message::Messages<bevy::asset::AssetEvent<Mesh>>>();
|
||||||
|
let cursor = cursor.get_or_insert_with(|| messages.get_cursor());
|
||||||
|
let (last, ids) = &mut *acc;
|
||||||
|
ids.extend(cursor.read(messages).filter_map(|e| match e {
|
||||||
|
bevy::asset::AssetEvent::Modified { id } => Some(*id),
|
||||||
|
_ => None,
|
||||||
|
}));
|
||||||
|
let now = world.resource::<Time>().elapsed_secs();
|
||||||
|
if now - *last < 1.0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
*last = now;
|
||||||
|
if ids.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let short = |full: &str| -> String {
|
||||||
|
let base = full.split('<').next().unwrap_or(full);
|
||||||
|
let segs: Vec<&str> = base.split("::").collect();
|
||||||
|
segs[segs.len().saturating_sub(2)..].join("::")
|
||||||
|
};
|
||||||
|
let mut found = 0usize;
|
||||||
|
let mut holders = world.query::<(Entity, &Mesh3d)>();
|
||||||
|
let matches: Vec<Entity> = holders
|
||||||
|
.iter(world)
|
||||||
|
.filter(|(_, m)| ids.contains(&m.0.id()))
|
||||||
|
.map(|(e, _)| e)
|
||||||
|
.take(6)
|
||||||
|
.collect();
|
||||||
|
for e in matches {
|
||||||
|
found += 1;
|
||||||
|
let sig: Vec<String> = world
|
||||||
|
.entity(e)
|
||||||
|
.archetype()
|
||||||
|
.components()
|
||||||
|
.iter()
|
||||||
|
.filter_map(|&c| world.components().get_info(c))
|
||||||
|
.map(|i| short(&i.name().to_string()))
|
||||||
|
.collect();
|
||||||
|
eprintln!(
|
||||||
|
"[mesh-holders] {e} holds a modified mesh: {}",
|
||||||
|
sig.join("+")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if found == 0 {
|
||||||
|
eprintln!(
|
||||||
|
"[mesh-holders] {} modified id(s), no Mesh3d holder (2d or unowned)",
|
||||||
|
ids.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ids.clear();
|
||||||
|
}
|
||||||
|
|
||||||
/// When the one-shot archetype census fires (seconds of `Time` elapsed; `f32::MAX` = spent).
|
/// When the one-shot archetype census fires (seconds of `Time` elapsed; `f32::MAX` = spent).
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
pub(super) struct ArchCensusAt(pub(super) f32);
|
pub(super) struct ArchCensusAt(pub(super) f32);
|
||||||
|
|||||||
@@ -99,6 +99,17 @@ impl Plugin for PerfPlugin {
|
|||||||
if std::env::var_os("WOW_MESH_EVENTS").is_some() {
|
if std::env::var_os("WOW_MESH_EVENTS").is_some() {
|
||||||
app.add_systems(Update, census::count_mesh_events);
|
app.add_systems(Update, census::count_mesh_events);
|
||||||
}
|
}
|
||||||
|
// `WOW_PART_CHURN=1` — the moving-regime premise counters (1461): how many frames per
|
||||||
|
// second despawn an M2 part (= `classify_water_side` full-walk promotions), and how
|
||||||
|
// often model materials fire Modified (= the `AssetChanged` scan wake-ups).
|
||||||
|
if std::env::var_os("WOW_PART_CHURN").is_some() {
|
||||||
|
app.add_systems(Update, census::count_part_churn);
|
||||||
|
}
|
||||||
|
// `WOW_MESH_HOLDERS=1` — who OWNS the meshes `WOW_MESH_EVENTS` counts? Prints each
|
||||||
|
// holder's archetype signature once a second (1461's writer hunt).
|
||||||
|
if std::env::var_os("WOW_MESH_HOLDERS").is_some() {
|
||||||
|
app.add_systems(Last, census::mesh_holders);
|
||||||
|
}
|
||||||
// `WOW_CAM_CHANGED=1` — is the world camera's transform bit-stable at a parked pin?
|
// `WOW_CAM_CHANGED=1` — is the world camera's transform bit-stable at a parked pin?
|
||||||
// The premise counter for gating the per-submesh visibility sweep on frame-stable
|
// The premise counter for gating the per-submesh visibility sweep on frame-stable
|
||||||
// camera inputs: if the controller rewrites an equal transform every frame, change
|
// camera inputs: if the controller rewrites an equal transform every frame, change
|
||||||
|
|||||||
@@ -656,10 +656,18 @@ impl Run {
|
|||||||
struct BatchPools {
|
struct BatchPools {
|
||||||
entities: Vec<Entity>,
|
entities: Vec<Entity>,
|
||||||
meshes: Vec<Handle<Mesh>>,
|
meshes: Vec<Handle<Mesh>>,
|
||||||
/// Each slot's last-written content — the per-run skip gate (decision 1361): a slot whose
|
/// Each slot's last-written MESH content (base positions, before [`Self::offsets`]) — the
|
||||||
/// run is bit-identical to what its pooled mesh already holds is not rewritten, so one
|
/// per-run skip gate (decision 1361): a slot whose run is bit-identical to what its pooled
|
||||||
/// animating quad no longer drags every batch through the GPU mesh allocator.
|
/// mesh already holds is not rewritten, so one animating quad no longer drags every batch
|
||||||
|
/// through the GPU mesh allocator.
|
||||||
stored: Vec<StoredRun>,
|
stored: Vec<StoredRun>,
|
||||||
|
/// Each slot's current XY translation from its stored base, carried by the batch entity's
|
||||||
|
/// `Transform` instead of the mesh (decision 1463): a run that only *panned* — the minimap
|
||||||
|
/// tile was ~74% of all moving-regime rebuild triggers — moves without an `Assets<Mesh>`
|
||||||
|
/// write, because ONE Modified event per frame arms `AssetChanged` probes over every
|
||||||
|
/// `Mesh3d` row in the scene (1370's all-or-nothing fast path, ~0.8 ms/frame at 1461's
|
||||||
|
/// Goldshire pin).
|
||||||
|
offsets: Vec<Vec2>,
|
||||||
materials: std::collections::HashMap<MatKey, Handle<UiQuadMaterial>>,
|
materials: std::collections::HashMap<MatKey, Handle<UiQuadMaterial>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -676,6 +684,55 @@ struct StoredRun {
|
|||||||
key: MatKey,
|
key: MatKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl StoredRun {
|
||||||
|
/// The candidate's uniform XY offset from this base: `Some(d)` when every vertex is this
|
||||||
|
/// run's vertex plus one constant delta and everything else is bit-equal — `Some(ZERO)` is
|
||||||
|
/// exactly the old bit-identical skip case, so this subsumes 1361's gate. `None` means the
|
||||||
|
/// run genuinely changed shape and must be rebaked. The comparison is exact float
|
||||||
|
/// arithmetic on purpose: a miss only falls back to the rewrite path, so a widget whose
|
||||||
|
/// pan doesn't survive `b + d == n` bit-exactly loses the optimization, never correctness.
|
||||||
|
/// Why [`Self::translation_from`] missed — the pan-gate diagnostic (`WOW_UI_DIFF=1`).
|
||||||
|
fn translation_miss_reason(&self, new: &StoredRun) -> &'static str {
|
||||||
|
if self.key != new.key {
|
||||||
|
"key"
|
||||||
|
} else if self.z_bits != new.z_bits {
|
||||||
|
"z"
|
||||||
|
} else if self.positions.len() != new.positions.len() {
|
||||||
|
"len"
|
||||||
|
} else if self.indices != new.indices {
|
||||||
|
"indices"
|
||||||
|
} else if self.uvs != new.uvs {
|
||||||
|
"uvs"
|
||||||
|
} else if self.colors != new.colors {
|
||||||
|
"colors"
|
||||||
|
} else {
|
||||||
|
"delta-nonuniform"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn translation_from(&self, new: &StoredRun) -> Option<Vec2> {
|
||||||
|
if self.key != new.key
|
||||||
|
|| self.z_bits != new.z_bits
|
||||||
|
|| self.positions.len() != new.positions.len()
|
||||||
|
|| self.positions.is_empty()
|
||||||
|
|| self.indices != new.indices
|
||||||
|
|| self.uvs != new.uvs
|
||||||
|
|| self.colors != new.colors
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let d = Vec2::new(
|
||||||
|
new.positions[0][0] - self.positions[0][0],
|
||||||
|
new.positions[0][1] - self.positions[0][1],
|
||||||
|
);
|
||||||
|
self.positions
|
||||||
|
.iter()
|
||||||
|
.zip(&new.positions)
|
||||||
|
.all(|(b, n)| n[0] == b[0] + d.x && n[1] == b[1] + d.y && n[2] == b[2])
|
||||||
|
.then_some(d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A material's full identity: texture, blend/shape flags, mask texture + mask rect (as bits, so
|
/// A material's full identity: texture, blend/shape flags, mask texture + mask rect (as bits, so
|
||||||
/// NaN/-0.0 can't split cache entries byte-equal materials would share).
|
/// NaN/-0.0 can't split cache entries byte-equal materials would share).
|
||||||
type MatKey = (
|
type MatKey = (
|
||||||
@@ -696,8 +753,10 @@ fn retire_batches(pools: &mut BatchPools, commands: &mut Commands) {
|
|||||||
commands.entity(entity).despawn();
|
commands.entity(entity).despawn();
|
||||||
}
|
}
|
||||||
// The skip gate must forget with them (decision 1361): a retired slot's entity is gone, so
|
// The skip gate must forget with them (decision 1361): a retired slot's entity is gone, so
|
||||||
// "content unchanged" must not skip the respawn when the UI returns.
|
// "content unchanged" must not skip the respawn when the UI returns. The pan gate's
|
||||||
|
// offsets ride the same lifecycle (they index the same slots).
|
||||||
pools.stored.clear();
|
pools.stored.clear();
|
||||||
|
pools.offsets.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Per frame: when the BASE lane flagged a change ([`UiQuads::dirty`]) or the APPEND lane's
|
/// Per frame: when the BASE lane flagged a change ([`UiQuads::dirty`]) or the APPEND lane's
|
||||||
@@ -962,10 +1021,54 @@ fn rebuild_ui_mesh(
|
|||||||
z_bits: z.to_bits(),
|
z_bits: z.to_bits(),
|
||||||
key,
|
key,
|
||||||
};
|
};
|
||||||
if pools.stored.get(used) == Some(&stored) {
|
// The pan gate (1463): a run that matches its slot's base up to one constant XY delta
|
||||||
|
// moves on the batch entity's `Transform` — no mesh write, no `AssetChanged` arming.
|
||||||
|
// `Some(ZERO)` is the bit-identical case (1361's old skip gate), where even the
|
||||||
|
// `Transform` write is skipped unless a previous pan is being undone.
|
||||||
|
let pan = pools
|
||||||
|
.stored
|
||||||
|
.get(used)
|
||||||
|
.and_then(|prev| prev.translation_from(&stored).map(|d| (d, prev.z_bits)));
|
||||||
|
if let Some((d, z_bits)) = pan {
|
||||||
|
if pools.offsets[used] != d {
|
||||||
|
pools.offsets[used] = d;
|
||||||
|
if let Some(&entity) = pools.entities.get(used) {
|
||||||
|
commands.entity(entity).insert(Transform::from_xyz(
|
||||||
|
d.x,
|
||||||
|
d.y,
|
||||||
|
f32::from_bits(z_bits),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
used += 1;
|
used += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// Pan-gate miss diagnostic (`WOW_UI_DIFF=1`, ≤3 lines/s so a startup burst can't
|
||||||
|
// exhaust it): names the check that sent this slot to the rewrite path.
|
||||||
|
if *UI_DIFF {
|
||||||
|
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
|
||||||
|
static SHOWN: AtomicU32 = AtomicU32::new(0);
|
||||||
|
static LAST_SEC: AtomicU64 = AtomicU64::new(0);
|
||||||
|
static START: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
|
||||||
|
let sec = START
|
||||||
|
.get_or_init(std::time::Instant::now)
|
||||||
|
.elapsed()
|
||||||
|
.as_secs();
|
||||||
|
if LAST_SEC.swap(sec, Ordering::Relaxed) != sec {
|
||||||
|
SHOWN.store(0, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
if SHOWN.fetch_add(1, Ordering::Relaxed) < 3 {
|
||||||
|
let why = pools
|
||||||
|
.stored
|
||||||
|
.get(used)
|
||||||
|
.map_or("no-prev", |p| p.translation_miss_reason(&stored));
|
||||||
|
eprintln!(
|
||||||
|
"[ui-pan] slot {used} rewrite: {why} (quads={}, key.tex={:?})",
|
||||||
|
stored.positions.len() / 4,
|
||||||
|
stored.key.0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
// MAIN_WORLD too (not RENDER_WORLD-only): the pool rewrites this asset in place next
|
// MAIN_WORLD too (not RENDER_WORLD-only): the pool rewrites this asset in place next
|
||||||
// rebuild, so the main-world copy must survive extraction.
|
// rebuild, so the main-world copy must survive extraction.
|
||||||
let mut mesh = Mesh::new(
|
let mut mesh = Mesh::new(
|
||||||
@@ -981,6 +1084,13 @@ fn rebuild_ui_mesh(
|
|||||||
} else {
|
} else {
|
||||||
pools.stored.push(stored);
|
pools.stored.push(stored);
|
||||||
}
|
}
|
||||||
|
// A rebaked slot's mesh holds absolute positions again — its pan resets with it (the
|
||||||
|
// rewrite arm below writes `Transform::from_xyz(0, 0, z)`).
|
||||||
|
if pools.offsets.len() > used {
|
||||||
|
pools.offsets[used] = Vec2::ZERO;
|
||||||
|
} else {
|
||||||
|
pools.offsets.push(Vec2::ZERO);
|
||||||
|
}
|
||||||
let mesh_handle = match pools.meshes.get(used) {
|
let mesh_handle = match pools.meshes.get(used) {
|
||||||
Some(handle) => {
|
Some(handle) => {
|
||||||
// `WOW_FREEZE_UI_MESH=1` — never rewrite a pooled batch mesh in place (the UI
|
// `WOW_FREEZE_UI_MESH=1` — never rewrite a pooled batch mesh in place (the UI
|
||||||
@@ -1048,6 +1158,7 @@ fn rebuild_ui_mesh(
|
|||||||
}
|
}
|
||||||
pools.meshes.truncate(used);
|
pools.meshes.truncate(used);
|
||||||
pools.stored.truncate(used);
|
pools.stored.truncate(used);
|
||||||
|
pools.offsets.truncate(used);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deliberately-overlapping synthetic content proving the sort: 5 z strata × 40 quads each, offset both
|
/// Deliberately-overlapping synthetic content proving the sort: 5 z strata × 40 quads each, offset both
|
||||||
|
|||||||
@@ -787,12 +787,22 @@ pub struct FarSideOfWater;
|
|||||||
/// surfaces, and the eye's side — so a *clean* batch is skipped, and only the changed set
|
/// surfaces, and the eye's side — so a *clean* batch is skipped, and only the changed set
|
||||||
/// re-classifies (~2.6k of ~40k parts on a measured Stormwind frame; the full sweep cost 2.2 ms).
|
/// re-classifies (~2.6k of ~40k parts on a measured Stormwind frame; the full sweep cost 2.2 ms).
|
||||||
/// The frame promotes to the full walk — byte-identical to the old per-frame behaviour — whenever
|
/// The frame promotes to the full walk — byte-identical to the old per-frame behaviour — whenever
|
||||||
/// a global term moves: the eye crosses the surface (every verdict inverts), surfaces stream
|
/// a global term that changes VERDICTS moves: the eye crosses the surface (every verdict
|
||||||
/// in/out, or any part despawned (the twin GC below needs its full mark; between full frames a
|
/// inverts) or surfaces stream in/out. A part *despawn* changes no live verdict — it only
|
||||||
/// pair orphaned by a handle overwrite — a fade settling to its cutout — lingers harmlessly, kept
|
/// obligates the twin GC's exact mark eventually — so it schedules a full walk behind
|
||||||
/// fresh by the mirror above, until the next edge sweeps it). The room-claim trigger fans DOWN:
|
/// [`GC_DEADLINE_SECS`] instead of promoting the same frame (decision 1462: walking streams
|
||||||
/// the claim lives on the unit root, the batches are its descendants, and a claim can change with
|
/// parts out on 12–22 frames/s, and each promotion was a full re-classify of every part —
|
||||||
/// the unit standing still (a building streaming in under it re-claims at the same spot).
|
/// lane B of 1461's walking tax; between full frames a pair orphaned by a handle overwrite —
|
||||||
|
/// a fade settling to its cutout — lingers harmlessly, kept fresh by the mirror above, until
|
||||||
|
/// the next edge sweeps it, and a GC-due twin lingers the same way). The room-claim trigger
|
||||||
|
/// fans DOWN: the claim lives on the unit root, the batches are its descendants, and a claim
|
||||||
|
/// can change with the unit standing still (a building streaming in under it re-claims at the
|
||||||
|
/// same spot).
|
||||||
|
/// How long a despawn-obligated twin GC may wait for its full walk (1462). Bounds how long an
|
||||||
|
/// orphaned twin pair can outlive its last user — a handful of small material clones — while a
|
||||||
|
/// walking regime's continuous despawn stream costs ~0.5 full walks/s instead of 12–22.
|
||||||
|
const GC_DEADLINE_SECS: f32 = 2.0;
|
||||||
|
|
||||||
#[allow(clippy::type_complexity, clippy::too_many_arguments)]
|
#[allow(clippy::type_complexity, clippy::too_many_arguments)]
|
||||||
pub(crate) fn classify_water_side(
|
pub(crate) fn classify_water_side(
|
||||||
interleave: crate::particles::WaterInterleave,
|
interleave: crate::particles::WaterInterleave,
|
||||||
@@ -834,7 +844,9 @@ pub(crate) fn classify_water_side(
|
|||||||
reclaimed: Query<Entity, Changed<crate::wmo_portal::UnitWmoRoom>>,
|
reclaimed: Query<Entity, Changed<crate::wmo_portal::UnitWmoRoom>>,
|
||||||
children: Query<&Children>,
|
children: Query<&Children>,
|
||||||
mut removed: RemovedComponents<MeshMaterial3d<WowModelMaterial>>,
|
mut removed: RemovedComponents<MeshMaterial3d<WowModelMaterial>>,
|
||||||
|
time: Res<bevy::time::Time>,
|
||||||
mut eye_was_submerged: Local<Option<bool>>,
|
mut eye_was_submerged: Local<Option<bool>>,
|
||||||
|
mut gc_due: Local<Option<f32>>,
|
||||||
) {
|
) {
|
||||||
// Mirror any near-asset edit into the live twin. Since decision 1381 the texanim/tint lane
|
// Mirror any near-asset edit into the live twin. Since decision 1381 the texanim/tint lane
|
||||||
// animates through the shared table (the twin's clone carries the same slot and seed, so it
|
// animates through the shared table (the twin's clone carries the same slot and seed, so it
|
||||||
@@ -858,12 +870,22 @@ pub(crate) fn classify_water_side(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let eye = interleave.eye_submerged();
|
let eye = interleave.eye_submerged();
|
||||||
// `last()` drains the reader — `next()` would leave the tail readable and promote NEXT frame
|
let now = time.elapsed_secs();
|
||||||
// to a spurious second full walk.
|
// `last()` drains the reader — `next()` would leave the tail readable and re-arm NEXT frame
|
||||||
|
// as a spurious second deadline. The earliest despawn holds the deadline (`get_or_insert`):
|
||||||
|
// a steady despawn stream must not push the GC out forever.
|
||||||
|
if removed.read().last().is_some() {
|
||||||
|
gc_due.get_or_insert(now + GC_DEADLINE_SECS);
|
||||||
|
}
|
||||||
let full = *eye_was_submerged != Some(eye)
|
let full = *eye_was_submerged != Some(eye)
|
||||||
|| interleave.surfaces_changed()
|
|| interleave.surfaces_changed()
|
||||||
|| removed.read().last().is_some();
|
|| gc_due.is_some_and(|due| now >= due);
|
||||||
*eye_was_submerged = Some(eye);
|
*eye_was_submerged = Some(eye);
|
||||||
|
if full {
|
||||||
|
// Any full walk runs the GC's exact mark, whatever triggered it — a pending deadline
|
||||||
|
// is satisfied, not merely postponed.
|
||||||
|
*gc_due = None;
|
||||||
|
}
|
||||||
|
|
||||||
if full {
|
if full {
|
||||||
// The full walk — every part, plus the twin GC's exact mark-and-sweep.
|
// The full walk — every part, plus the twin GC's exact mark-and-sweep.
|
||||||
|
|||||||
Reference in New Issue
Block a user