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