From 11a889bc56bf88d20d2565c6a6e0520e3373e613 Mon Sep 17 00:00:00 2001 From: Sam Date: Sun, 16 Aug 2026 17:52:00 +0700 Subject: [PATCH] performance day two: the resting interface stops rebuilding itself MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - the resting player's blinking status glow was rebuilding the whole interface's batches every frame — a run whose bytes didn't move is no longer rewritten, worth 1.17 ms at Stormwind - doodad skeletons collapse into the pose buffer and parked rigs skip the model pass; doodad and prop hulls weld into batched trimeshes - the Update schedule runs on the single-threaded executor by default, measured faster than the multithreaded one for this frame - a parked camera stops claiming to move every frame Co-Authored-By: Claude Fable 5 --- crates/benilla-app/src/capture/probes/act.rs | 4 + crates/benilla-app/src/creature_anim/lod.rs | 10 +- crates/benilla-app/src/entities/wmo_props.rs | 16 +- crates/benilla-app/src/lib.rs | 14 + crates/benilla-app/src/perf.rs | 130 +++++++ crates/benilla-app/src/player/camera.rs | 42 +- crates/benilla-app/src/player/setup.rs | 21 +- crates/benilla-app/src/quest_markers/mod.rs | 18 +- crates/benilla-app/src/ui_pass.rs | 136 ++++++- .../benilla-app/src/ui_script/extract/mod.rs | 22 ++ crates/benilla-app/tests/world_api_wall.rs | 12 +- crates/benilla-ui/src/script/model.rs | 32 ++ crates/benilla-world/src/doodad_anim/lazy.rs | 72 ++-- crates/benilla-world/src/doodad_anim/mod.rs | 106 +++-- crates/benilla-world/src/rig_anim.rs | 1 + crates/benilla-world/src/rig_anim/compose.rs | 53 ++- .../benilla-world/src/rig_anim/global_seq.rs | 59 +++ crates/benilla-world/src/rig_anim/pose.rs | 84 ++++ crates/benilla-world/src/rig_palette.rs | 7 +- crates/benilla-world/src/terrain_stream.rs | 49 ++- .../src/terrain_stream/collider.rs | 28 +- .../src/terrain_stream/spawn/assemble.rs | 89 +++-- .../src/terrain_stream/spawn/fx.rs | 32 +- .../src/terrain_stream/spawn/mod.rs | 105 +++-- .../benilla-world/src/terrain_stream/weld.rs | 364 ++++++++++++++++++ crates/benilla-world/src/world_plugins.rs | 16 + 26 files changed, 1293 insertions(+), 229 deletions(-) create mode 100644 crates/benilla-world/src/terrain_stream/weld.rs diff --git a/crates/benilla-app/src/capture/probes/act.rs b/crates/benilla-app/src/capture/probes/act.rs index 183eb020..f69b5a35 100644 --- a/crates/benilla-app/src/capture/probes/act.rs +++ b/crates/benilla-app/src/capture/probes/act.rs @@ -167,6 +167,10 @@ fn probe_key_by_name(name: &str) -> Option { "X" => KeyCode::KeyX, "Z" => KeyCode::KeyZ, "Tab" => KeyCode::Tab, + // TOGGLEAUTORUN's 1.12 default. The moving-axis protocol (task: grade streaming while + // running) engages autorun with one tap and steers with `WOW_PROBE_LOOK` — the whole + // scripted drive out of two instruments that already existed. + "NumLock" => KeyCode::NumLock, // The nameplate toggles (`vplates`): bare `V` is enemy plates, `Shift`-held `V` is // friendly. A probe that wants plates over a town's *friendly* NPCs — the only way to // measure plate behaviour without standing in aggro range of something — presses this. diff --git a/crates/benilla-app/src/creature_anim/lod.rs b/crates/benilla-app/src/creature_anim/lod.rs index 8c4572ca..23ce576e 100644 --- a/crates/benilla-app/src/creature_anim/lod.rs +++ b/crates/benilla-app/src/creature_anim/lod.rs @@ -102,7 +102,15 @@ pub(super) fn gate_rig_animation( // events), `go_anim`'s state machine arms the player regardless of the marker, and the // wake samples the absolute clock. Booth/studio rigs are entity-joint-lane (no // `RigPose`), so this query can never park the character pane. - With, + // + // `Without` (decision 1365): placed doodads joined the collapsed lane, + // and their draw gate (`doodad_anim::gate_doodad_anim`) owns their `AnimParked` marker — + // it parks on the composed draw verdict + fade sphere, the doodad lane's own law, and + // two writers to one marker would fight every frame the two policies disagree. + ( + With, + Without, + ), >, self_hosts: Query>, instances: Query<&WmoPortalInstance>, diff --git a/crates/benilla-app/src/entities/wmo_props.rs b/crates/benilla-app/src/entities/wmo_props.rs index 7a2e6e74..8d89fbbb 100644 --- a/crates/benilla-app/src/entities/wmo_props.rs +++ b/crates/benilla-app/src/entities/wmo_props.rs @@ -310,15 +310,14 @@ pub(super) fn spawn_wmo_gameobject_props( if let Some(&root) = ents.first() { let placement = host_world.mul_transform(prop.local); for em in &m.emitters { - // The emitter rides its host bone's joint when the prop animates (the same - // ride as a terrain doodad — an unanimated chain reproduces the static path - // exactly); a boneless prop follows its root. Either owner's propagated + // The emitter rides its host bone's anchor when the prop animates (the + // same ride as a terrain doodad — an unanimated chain reproduces the static + // path exactly); a boneless prop follows its root. Either owner's propagated // global composes the moving hull. let owner = host .as_ref() - .map(|h| h.joints.as_slice()) - .and_then(|js| js.get(em.def.bone as usize)) - .map_or((root, [0.0; 3]), |&j| (j, em.bone_pivot)); + .and_then(|h| h.anchor(em.def.bone)) + .map_or((root, [0.0; 3]), |a| (a, em.bone_pivot)); if let Some(e) = particles::spawn_emitter( &mut commands, em, @@ -358,9 +357,8 @@ pub(super) fn spawn_wmo_gameobject_props( for rb in &m.ribbons { let (owner, use_pivot) = host .as_ref() - .map(|h| h.joints.as_slice()) - .and_then(|js| js.get(rb.def.bone as usize)) - .map_or((root, false), |&j| (j, true)); + .and_then(|h| h.anchor(rb.def.bone)) + .map_or((root, false), |a| (a, true)); if benilla_world::ribbons::spawn_ribbon( &mut commands, rb, diff --git a/crates/benilla-app/src/lib.rs b/crates/benilla-app/src/lib.rs index 2a87ced9..c0260061 100644 --- a/crates/benilla-app/src/lib.rs +++ b/crates/benilla-app/src/lib.rs @@ -243,6 +243,20 @@ pub fn run(build: BuildId) -> AppExit { // when MOST rows move in one frame (a load burst into a near-empty world), where // dirty-marking briefly costs more than it saves — a loading-band term, accepted knowingly. app.insert_resource(bevy::transform::systems::StaticTransformOptimizations::enabled()); + // The Update schedule runs on the SINGLE-THREADED executor (decision 1366). Not a tuning + // whim: ~60% of our per-frame Update systems are non-Send (mlua's UiScript, kira's audio + // handles) and serialize through the multi-threaded executor's one `local_thread_running` + // flag anyway, so the cross-thread dispatch machinery was pure overhead — measured + // −1.30 cpu_ms at the LBRS pin under 5-round grading with the wall tail (p95/p99/max) + // flat-to-better (the 1364 "fatter tail" reading did not reproduce; 1366 has the tables, + // both pins). PostUpdate stays multi-threaded — its wide parallel bands + // (propagation, visibility) are real parallelism; re-grading it is a named open probe. + // `WOW_MT_UPDATE=1` is the A/B lever back to the multi-threaded executor. + if std::env::var_os("WOW_MT_UPDATE").is_none() { + app.edit_schedule(Update, |s| { + s.set_executor_kind(bevy::ecs::schedule::ExecutorKind::SingleThreaded); + }); + } // …and one startup line says which build produced this log. Registered HERE, beside the stamp // it prints, rather than in `preflight` where it sat until decision 1179: **which build is // this** is the first thing a bug report from someone else's machine has to establish, and a diff --git a/crates/benilla-app/src/perf.rs b/crates/benilla-app/src/perf.rs index 13e873d8..3f27dcb4 100644 --- a/crates/benilla-app/src/perf.rs +++ b/crates/benilla-app/src/perf.rs @@ -98,6 +98,32 @@ impl Plugin for PerfPlugin { // whether or not anything is tracing, or the counters would accumulate forever. .add_systems(Last, trace_stream) .add_systems(EguiPrimaryContextPass, perf_hud_ui); + // `WOW_MESH_EVENTS=1` — who churns Mesh assets per frame? The premise counter behind + // the Stormwind trace's `allocate_and_free_meshes` row (0.86 ms/frame at a PARKED pin): + // the allocator answers every Modified with a free+realloc, so a steady scene should + // show ~zero here. Printed once a second with the top mutated ids' first sighting. + if std::env::var_os("WOW_MESH_EVENTS").is_some() { + app.add_systems(Update, count_mesh_events); + } + // `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 + // camera inputs: if the controller rewrites an equal transform every frame, change + // detection fires anyway and the gate can never hold — the controller's no-op write + // is then the first fix, not the sweep's. + if std::env::var_os("WOW_CAM_CHANGED").is_some() { + app.add_systems(bevy::app::PostUpdate, count_camera_changes); + } + // `WOW_ARCH_CENSUS=` — one archetype census at t=secs: every non-empty + // archetype's entity count beside its component set, largest first. The exact by-lane + // entity picture (1354's anchor census generalized to every lane at once), for sizing + // which populations are worth collapsing before designing any collapse. + if let Some(at) = std::env::var("WOW_ARCH_CENSUS") + .ok() + .and_then(|v| v.parse::().ok()) + { + app.insert_resource(ArchCensusAt(at)); + app.add_systems(Last, arch_census); + } #[cfg(target_os = "macos")] stall_sample::plugin(app); } @@ -1001,3 +1027,107 @@ mod stall_sample { } } } + +/// `WOW_MESH_EVENTS=1`: per-second Mesh asset-event counts (see the plugin registration). The +/// `sample` list names a few mutated asset ids so the writer can be found by grepping who holds +/// that handle. +fn count_mesh_events( + mut events: MessageReader>, + time: Res