diff --git a/Cargo.toml b/Cargo.toml index 66a817ca..79498609 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,3 +38,13 @@ opt-level = 1 # No debuginfo trimming: `debug = "line-tables-only"` was A/B-measured (decision 0192) at zero # recurring win on this stack (macOS unpacked split-debuginfo; sccache; fast ld) — not worth # invalidating every warm target and the sccache cache for. + +# Shipping-grade optimization, A/B-graded at the pin (decision 0736): −1.0 cpu_ms (−5 %) vs +# default release, flat at the empty-map and bare-Bevy floors. The campaign keeps grading on +# default `release` — same-day A/B is the law, and fat LTO's link time would tax every +# iteration for a constant offset. Build with `--profile ship` for actual releases (add +# `RUSTFLAGS=-Ctarget-cpu=native` for a machine-local binary; not set here — it isn't portable). +[profile.ship] +inherits = "release" +lto = "fat" +codegen-units = 1 diff --git a/crates/benilla-assets/src/wmo.rs b/crates/benilla-assets/src/wmo.rs index d5b4ab2a..43c65559 100644 --- a/crates/benilla-assets/src/wmo.rs +++ b/crates/benilla-assets/src/wmo.rs @@ -48,7 +48,8 @@ pub struct WmoGroupNav { /// A loaded WMO building: the render batches across all its groups, flattened, plus the flattened /// collidable triangles (the app bakes a collider from these per placement). -#[derive(Asset, TypePath, Clone)] +/// `Default` = the empty building (every field vacuous) — a test scaffold, never a loader product. +#[derive(Asset, TypePath, Clone, Default)] pub struct WmoModel { /// The building's `WMOAreaTable.WMOID` key (root `MOHD.wmoID`; NSabbey → 59). pub wmo_id: u32, diff --git a/crates/benilla/src/char_select/mod.rs b/crates/benilla/src/char_select/mod.rs index 033b43ea..512f1681 100644 --- a/crates/benilla/src/char_select/mod.rs +++ b/crates/benilla/src/char_select/mod.rs @@ -141,6 +141,15 @@ impl Roster { pub(super) fn selected_char(&self) -> Option<&Character> { self.selected.and_then(|i| self.chars.get(i)) } + + /// The pending pick's map — the loading screen resolves the *destination's* art from the + /// roster at the entry edge, before the server's `SMSG_LOGIN_VERIFY_WORLD` snap lands + /// (decision 0737). + pub(crate) fn pending_map(&self) -> Option { + self.pending_pick + .and_then(|g| self.chars.iter().find(|c| c.guid == g)) + .map(|c| c.map) + } } /// Ask the parked IO thread to log in as `guid` (the pick channel) and remember it as pending. diff --git a/crates/benilla/src/creature_anim/compose.rs b/crates/benilla/src/creature_anim/compose.rs index c72111ba..2abbe3ba 100644 --- a/crates/benilla/src/creature_anim/compose.rs +++ b/crates/benilla/src/creature_anim/compose.rs @@ -133,14 +133,21 @@ pub(crate) fn finalize_rig_worlds( .single() .ok() .map(|t| (*t.forward(), *t.right(), *t.up())); - // Which rigs refresh this frame: pose-dirty, model frame moved, or camera-faced live. + // Which rigs refresh this frame: pose-dirty, model frame moved, or camera-faced — and never + // a parked one (decision 0739). The LOD gate ruled a parked rig un-viewable, so neither an + // idle's pose_dirty, a patrol's root motion, nor a billboard face can matter until the wake + // — which drops the marker before the same frame's pose evaluation, re-raising `pose_dirty` + // before this pass runs, so a woken rig's rows are current before anything draws them. (The + // world-space rows of a moving parked rig go stale on purpose — nothing reads them; at the + // LBRS pin the parked-but-patrolling re-writes were ~260 of the ~620 per-frame refreshes.) let refresh: Vec = { let roots_changed = worlds_params.p0(); rigs.iter() .filter(|(_, rig, _, parked)| { - rig.pose_dirty - || roots_changed.contains(rig.joints_root) - || (rig.has_billboard && !parked) + !parked + && (rig.pose_dirty + || roots_changed.contains(rig.joints_root) + || rig.has_billboard) }) .map(|(holder, ..)| holder) .collect() @@ -148,6 +155,9 @@ pub(crate) fn finalize_rig_worlds( if refresh.is_empty() { return; } + // The `WOW_RIG_COST` compose meter (decision 0736): how many rigs refresh and what the + // whole finalize pass costs, beside the palette's copy/write and upload lines. + let cost_t0 = crate::rig_palette::rig_cost_enabled().then(std::time::Instant::now); let mut globals = worlds_params.p1(); // The rigid-child re-walk's do-not-enter set, exactly the entity pass's: a nested rig with // its own billboard output owns its interior (and its root keeps the propagated frame). @@ -230,6 +240,13 @@ pub(crate) fn finalize_rig_worlds( rig.pose_dirty = false; } } + if let Some(t0) = cost_t0 { + eprintln!( + "[rig-finalize] refreshed={} ms={:.3}", + refresh.len(), + t0.elapsed().as_secs_f32() * 1000.0 + ); + } } /// Register the model pass; the world pass is chained by [`crate::billboard::BillboardPlugin`] diff --git a/crates/benilla/src/creature_anim/lod.rs b/crates/benilla/src/creature_anim/lod.rs index 2a924a53..76f7ffa7 100644 --- a/crates/benilla/src/creature_anim/lod.rs +++ b/crates/benilla/src/creature_anim/lod.rs @@ -1,5 +1,8 @@ -//! The unit animation-LOD gate (decision 0448): park an off-frustum rig's **per-bone pose -//! evaluation**, keep **every clock** running. +//! The animation-LOD gate (decision 0448): park an off-frustum — or, since decision 0739, +//! portal-invisible — rig's **per-bone pose evaluation**, keep **every clock** running. Since +//! 0739 it governs the whole [`RigPose`] population — units, players, AND GameObject rigs (see +//! the query's comment for why the old `With` filter left every animated GO +//! sampling off-view). //! //! The reference has NO view cull on unit skeletons — every in-range, non-hidden unit is fully //! animated every frame (wow-re `unit-anim-visibility-gate.md`: worklist admission is the CGUnit @@ -23,18 +26,39 @@ //! gate on the same marker. Contrast the DOODAD gate ([`crate::doodad_anim`]), which faithfully //! stops the player and re-arms on the shared clock — correct there (no one consumes a doodad's //! events off-screen), the exact 0075 trap here. +//! +//! **The room leg (decision 0739).** The frustum is the wrong instrument indoors: a dungeon +//! camera's view cone passes through walls, so most of an instance's population stays "in +//! frustum" while only the current room chain is drawable. The portal PVS +//! ([`crate::wmo_portal`], the faithful WMO group cull) already knows which rooms the camera can +//! reach, and [`UnitWmoRoom`] already names each unit's room — so a unit whose room (and every +//! room one portal hop from it) is outside this frame's PVS parks too. Soundness: a unit is +//! room-parked exactly when the geometry *around* it — its room's walls, floor, furniture — is +//! portal-culled by the same bits, and a body can poke only through a portal, never a wall, so +//! the one-hop guard bounds the doorway straddle. Reveals carry no artifact by construction: a +//! mob can only become visible through a portal, and the moment that portal's screen window is +//! non-zero the flood marks its room visible (`compute_wmo_pvs` runs in `Update`, this gate in +//! `PostUpdate` before the evaluator — same-frame wake, absolute-clock pose). Fail-open at every +//! seam: no claim (outdoors, EXTERIOR groups), a despawned placement, a still-loading model, an +//! out-of-range index — all read visible, the [`crate::wmo_portal::WmoGroupVis::drawn_by`] +//! convention. +//! `WOW_NO_ROOM_LOD=1` disables just this leg (the A/B lever); `WOW_RIG_COST=1` prints the +//! per-frame `[rig-gate]` counters. use bevy::app::AnimationSystems; use bevy::camera::primitives::{Frustum, Sphere as CullSphere}; use bevy::ecs::entity::EntityHashMap; use bevy::prelude::*; +use benilla_assets::{WmoGroupNav, WmoModel, WmoPortalRef}; + use crate::entities::mount::MountBody; use crate::net::SelfPlayer; use crate::player::WorldCamera; use crate::target::SelectionRadius; +use crate::wmo_portal::{UnitWmoRoom, WmoPortalInstance, WmoRoom}; -use super::AnimDriver; +use super::RigPose; /// This rig's per-bone pose evaluation is parked (decision 0448): the pose evaluator and the pose /// post-passes skip it. The sequence clocks, the driver state machine, and the event scanner all @@ -55,12 +79,13 @@ const RADIUS_SCALE: f32 = 2.0; const RADIUS_PAD: f32 = 4.0; const FALLBACK_RADIUS: f32 = 6.0; -/// Park/wake streamed rigs by the padded sphere-vs-frustum test (decision 0448). PostUpdate, +/// Park/wake streamed rigs by the padded sphere-vs-frustum test (decision 0448) ANDed with the +/// portal-PVS room test (decision 0739 — see the module doc's "room leg"). PostUpdate, /// before [`AnimationSystems`] — a wake drops the marker in time for the same frame's pose /// evaluation, so the re-appearing unit samples the absolute-clock pose with no stale /// frame. Exempt: the self-avatar (the camera rides its attachment-17 pivot, and it must keep /// animating faded-out in first person) and its mount child. `WOW_NO_ANIM_LOD=1` disables -/// parking — the live-probe A/B lever. +/// parking — the live-probe A/B lever; `WOW_NO_ROOM_LOD=1` disables only the room leg. #[allow(clippy::type_complexity, clippy::too_many_arguments)] // one Bevy system's full input set pub(super) fn gate_rig_animation( time: Res