Files
benilla-octo/crates/benilla-world/src/world_census.rs
T
SamandClaude Fable 5 25c865c00a the ghost world gets its own sky, and a corpse cannot steer
- a dead player's sky is DeathClouds now — `lightSkyboxID` was named in the schema and read by nobody and `LightSkybox.dbc` was never loaded at all, so a ghost kept the ordinary gradient dome; the shipped data has exactly 5 of 426 LightParams rows carrying a skybox id, every one of them DeathClouds.mdx, reached from the death profile (slot 4) of all 374 Light rows and gated on the ghost bit, with a hardcoded weight of 1.0 that skips the WMO skybox outright; it resolves off the same (map, camera position) seed the atmosphere uses, so the ghost sky and the death-profile fog it hangs over switch on the same frame — and the lane sheds its `wmo_sky` name for `skybox`, no longer being WMO-only
- a corpse cannot steer: the reference's two movement-input predicates share a precondition benilla never modelled — `UNIT_FIELD_HEALTH > 0` — so at zero health the keyboard turn and the right-drag close together, exactly as they do inside an Ice Block, while the ghost (health 1) keeps moving; the wow-re §5 round then corrected three byte trails (the mouse turn has its own eight-conjunct predicate, the supposed on-taxi test is the Knockdown animation lockout — set on exactly one of 208 AnimationData rows — and the "charm bit" is the far-sight latch) and added three verified health gates we lacked: the jump IS health-gated, sit and stand are refused while the body reads dead (a feigner too), and a follow tears down only when BOTH predicates drop — we had that wrong in both directions
- the walk-gait cross-check folds back confirming `min(walk, run)` and the speed-only animation channel (by a 2,304-function closure census), and finds the trap we lacked: every incoming spline re-authors the gait, and the spline's RUNMODE flag is an INVERSE — a spline marked as a run clears walk mode, one not so marked SETS it — so a charge, taxi, knockback or flee hand-off leaves the body in whatever gait its path was authored with, applied once on the spline's start edge; the post-worldport zero-walk fallback is retracted on a re-run (the ctor's zero is a transient thirty instructions wide, never observable), leaving the plain minimum the bytes compute — the better failure mode too, since a fallback makes a walker silently RUN if a walk speed ever goes missing, where the transcription makes them visibly stop

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 20:18:16 +07:00

389 lines
18 KiB
Rust

//! **[`WorldCensus`] — what the engine drew this frame, published as a fact.**
//!
//! Every number an instrument wants about a rendered frame — how many submeshes exist, how many
//! survived to `ViewVisibility`, which subsystem each belongs to, what the exterior-scene gate did,
//! how many emitters are alive, what the asset caches are holding, what got rewritten since the
//! last frame — is a fact the *renderer* owns. Before this module the probes read it by querying
//! the renderer's own components and resources directly, and that one file
//! (`capture/probes/live_fps.rs`) named nine engine internals nobody else outside the engine
//! needed: four material aliases, the two exterior-cull types, the two portal types, and the
//! camera's interior claim. Decision 1164 sorts all nine into CLOSE with the same remedy —
//! *publish the census, not the components* — and this is that census.
//!
//! It is one read-only [`SystemParam`] and one snapshot type. An instrument adds the param, calls
//! [`WorldCensus::take`] on the frame it cares about, and formats the snapshot however its own
//! output contract wants. **No `println!` lives here**: the line shapes (`VIS_CENSUS`,
//! `MAT_CHURN`, `ASSET_DUMP`) are the probes' greppable contract, not the engine's, and an engine
//! that formats its callers' output has the boundary backwards.
//!
//! The one part that is opt-in is the churn window: counting `AssetEvent::Modified` per asset type
//! costs a `MessageReader` per type per frame, so [`WorldCensus::churn_counters`] installs them
//! and an ordinary run carries none. **Which** types are counted is engine knowledge (it is the
//! engine's material set); **whether** to count is the instrument's call.
use std::collections::HashMap;
use bevy::camera::primitives::Aabb;
use bevy::ecs::system::SystemParam;
use bevy::prelude::*;
use crate::doodad_anim::{TintAnimMaterials, UvAnimMaterials};
use crate::exterior_cull::{ExteriorCullVerdict, ExteriorScene};
use crate::interact::WorldObject;
use crate::model_render::{ModelKind, ModelPart};
use crate::particles::ParticleEmitter;
use crate::wmo_portal::{CameraInteriorClaim, ExteriorWindows, WmoGroupVis};
use benilla_assets::materials::WowModelMaterial;
/// A read-only view of the engine's drawn state — the whole of it, as one system parameter.
///
/// Deliberately one param and not eight: `drive_live_fps` sits at Bevy's 16-param ceiling, and
/// every instrument that wants any of this wants most of it, taken from the *same frame*. Two
/// instruments each assembling their own half is how the two disagree.
#[derive(SystemParam)]
pub struct WorldCensus<'w, 's> {
/// Every spawned model submesh, with the facts that make a visible one accountable.
parts: Query<'w, 's, CensusData>,
emitters: Query<'w, 's, &'static ParticleEmitter>,
/// The exterior-scene gate's two terms (decision 0774) and what the cull actually did with
/// them. Optional so the census works in a viewer that has not installed the portal system.
claim: Option<Res<'w, CameraInteriorClaim>>,
windows: Option<Res<'w, ExteriorWindows>>,
verdict: Option<Res<'w, ExteriorCullVerdict>>,
/// The visibility authority's own pose (see [`CensusReport::pvs_eye`]).
cull_probe: Option<Res<'w, crate::wmo_portal::WmoCullProbe>>,
/// Which backdrop is drawing — the gradient dome, or a building's own MOSB sky
/// ([`crate::skybox`]). Optional for the same reason as the portal terms above.
skybox: Option<Res<'w, crate::skybox::CameraSkybox>>,
/// The ribbon lane's own verdict — the third effect population, counted nowhere else.
ribbons: Option<Res<'w, crate::ribbons::RibbonVerdict>>,
mats: Res<'w, Assets<WowModelMaterial>>,
meshes: Res<'w, Assets<Mesh>>,
images: Res<'w, Assets<bevy::image::Image>>,
models: Res<'w, Assets<benilla_assets::M2Model>>,
uv_reg: Res<'w, UvAnimMaterials>,
tint_reg: Res<'w, TintAnimMaterials>,
server: Res<'w, AssetServer>,
/// Present only once [`WorldCensus::churn_counters`] has been installed.
churn: Option<ResMut<'w, ChurnCensus>>,
}
/// One frame's census, in the order an instrument prints it. Plain data on purpose: an instrument
/// holding this holds no engine component, no engine resource and no engine type it has to name.
#[derive(Default)]
pub struct CensusReport {
/// Every model submesh that exists.
pub submeshes: usize,
/// …and how many of them the render world will actually draw (`ViewVisibility`).
pub drawn: usize,
/// Visible submeshes per model subsystem — `(column name, visible, of-those-gated)` — in a
/// fixed column order, because a census whose columns move cannot be diffed across runs.
pub kinds: [(&'static str, usize, usize); 4],
/// Of every [`ExteriorScene`]-tagged submesh: how many exist, how many the cull wrote `Hidden`
/// on, how many were exempt (the camera's own placement — decision 0784), and how many carry
/// **no `Aabb`**, which is the cull's fail-open arm admitting them unconditionally.
pub tagged: usize,
pub hidden: usize,
pub exempt: usize,
pub no_aabb: usize,
/// Tagged, bounded, not exempt, and yet not `Hidden` — the escapees, `(label, is a billboard
/// card, count)`, most first. A tagged object the cull left un-hidden is a defect by
/// construction, which is why this is not behind a flag.
pub escaped: Vec<(String, bool, usize)>,
/// Visible submeshes per distinct label, `(label, gated, count)` — ungated first (the leak
/// candidates), then most-drawn first. The "so WHICH trees are they?" list.
pub labels: Vec<(String, bool, usize)>,
pub emitters: usize,
pub active_emitters: usize,
pub particles: usize,
/// Ribbon trails alive, and how many wrote a strip — `None` if the lane is not installed.
/// The `emitters`/`particles` pair above covers only the QUAD half of the effect stream;
/// trails are the other half and had no counter at all.
pub ribbons: Option<(usize, usize)>,
/// The WMO group the camera claims — `"g07"`, or `"none"` over open world. `None` when the
/// portal system is not installed at all, which is a different statement from "not indoors".
pub room: Option<String>,
/// `"unrestricted"`, or the number of window sub-frusta. `None` with [`CensusReport::room`].
pub windows: Option<String>,
/// **The eye the portal authority computed [`Self::room`], [`Self::windows`] and every group's
/// PVS from.** Not necessarily the eye this frame draws from: the authority runs in `Update`
/// off the camera's propagated transform, so it is answering about wherever the camera was
/// when that transform was last written. Walking makes the difference a centimetre; a
/// teleport makes it the whole jump, and this is the column that says so.
pub pvs_eye: Option<Vec3>,
/// The backdrop actually drawing: `"dome"` for the `Light.dbc` gradient, else the WMO skybox
/// model a building's PVS asked for. `None` when the WMO-sky lane is not installed.
///
/// Here because a skybox is the one draw that can repaint the whole frame while being
/// invisible to every other line in this report: its batches carry no [`ModelPart`], so they
/// are not in `submeshes`, not in `drawn`, not in `kinds`, and not in `labels`. Standing
/// outdoors in Tanaris with a purple sky and no purple *object* on any list is exactly the
/// state that has to be readable, and before this it was not.
pub sky: Option<String>,
/// What the exterior cull did this frame, `None` if it did not run.
pub cull: Option<CullTerms>,
/// Resident asset counts — the leak meter. A tour probe reading the same counts as a fresh
/// control is what "torn down" means, machine-checked.
pub mats: usize,
pub meshes: usize,
pub images: usize,
pub uv_anims: usize,
pub tint_anims: usize,
/// `AssetEvent::Modified` totals per asset type since the window opened, sorted by type name.
/// Empty unless [`WorldCensus::churn_counters`] is installed.
pub churn: Vec<(&'static str, usize)>,
}
/// [`ExteriorCullVerdict`] relayed as plain numbers — see that type for what each term means and
/// why `tested` is the one no other instrument can see.
pub struct CullTerms {
/// The window count it was given, or `"unrestricted"`.
pub windows: String,
pub frusta: usize,
pub tested: usize,
pub hidden: usize,
pub unbounded: usize,
/// The body leg, kept apart from `tested`/`hidden` — see the verdict's own note for why summing
/// a two-dozen audience into a tens-of-thousands one erases it.
pub bodies: usize,
pub bodies_hidden: usize,
/// The liquid subset of `tested`/`hidden` — see the verdict's own note for why a few dozen
/// surfaces summed into tens of thousands of terrain cells is an invisible leg (1652).
pub liquid: usize,
pub liquid_hidden: usize,
}
impl WorldCensus<'_, '_> {
/// Install the per-asset-type churn counters (in `First`, before anything can modify an
/// asset this frame). Off by default — see the module header for the split of who decides.
pub fn churn_counters(app: &mut App) {
app.init_resource::<ChurnCensus>();
Self::count_churn::<bevy::image::Image>(app, "image");
Self::count_churn::<Mesh>(app, "mesh");
Self::count_churn::<StandardMaterial>(app, "std");
Self::count_churn::<benilla_assets::materials::TerrainMaterial>(app, "terrain");
Self::count_churn::<WowModelMaterial>(app, "model");
Self::count_churn::<benilla_assets::materials::WdlMaterial>(app, "wdl");
Self::count_churn::<benilla_assets::materials::LiquidMaterial>(app, "liquid");
Self::count_churn::<crate::sky::SkyMaterial>(app, "sky");
Self::count_churn::<crate::sun::CelestialMaterial>(app, "celestial");
Self::count_churn::<crate::sun::StarMaterial>(app, "star");
Self::count_churn::<crate::clouds::CloudMaterial>(app, "cloud");
}
/// Add one more asset type to the churn window under `label`. The engine counts its own
/// materials in [`WorldCensus::churn_counters`]; a host with materials of its own (the UI
/// pass, a tool's overlay) folds them in here rather than keeping a second, disagreeing
/// census. Call after `churn_counters`, which is what creates the tally.
pub fn count_churn<A: bevy::asset::Asset>(app: &mut App, label: &'static str) {
app.add_systems(First, churn_counter::<A>(label));
}
/// Open a fresh churn window. Warmup noise — streaming, shader warms — otherwise reads as a
/// steady-state ratchet, so the instrument calls this on the first frame it actually samples.
pub fn restart_churn(&mut self) {
if let Some(churn) = self.churn.as_mut() {
churn.0.clear();
}
}
/// Snapshot this frame.
pub fn take(&self) -> CensusReport {
let own_instance = self
.claim
.as_ref()
.and_then(|c| c.0)
.map(|c| c.room.instance);
let mut kinds = [(0usize, 0usize); 4];
let mut labels: HashMap<(String, bool), usize> = HashMap::new();
let mut escaped: HashMap<(String, bool), usize> = HashMap::new();
let (mut tagged, mut hidden, mut exempt_n, mut no_aabb) = (0, 0, 0, 0);
let (mut submeshes, mut drawn) = (0usize, 0usize);
for (vis, part, gated, object, want, aabb, card, group) in self.parts.iter() {
submeshes += 1;
drawn += usize::from(vis.get());
if gated {
// The camera's own placement is not exterior scene to itself (decision 0784) and
// is *supposed* to draw; without that subtraction the escapee list is all
// room-you-are-in furniture and says nothing.
let exempt = group.is_some_and(|g| Some(g.instance) == own_instance);
tagged += 1;
hidden += usize::from(*want == Visibility::Hidden);
no_aabb += usize::from(aabb.is_none());
exempt_n += usize::from(exempt);
if *want != Visibility::Hidden && aabb.is_some() && !exempt {
let label = object.map_or("<unlabelled>", |o| o.label.as_str());
*escaped.entry((label.to_string(), card)).or_default() += 1;
}
}
if !vis.get() {
continue;
}
let slot = &mut kinds[kind_index(part.kind)];
slot.0 += 1;
slot.1 += usize::from(gated);
if let Some(o) = object {
*labels.entry((o.label.clone(), gated)).or_default() += 1;
}
}
let mut escaped: Vec<_> = escaped.into_iter().map(|((l, c), n)| (l, c, n)).collect();
escaped.sort_by(|a, b| b.2.cmp(&a.2).then(a.0.cmp(&b.0)));
let mut labels: Vec<_> = labels.into_iter().map(|((l, g), n)| (l, g, n)).collect();
labels.sort_by(|a, b| a.1.cmp(&b.1).then(b.2.cmp(&a.2)).then(a.0.cmp(&b.0)));
let (emitters, active_emitters, particles) = self
.emitters
.iter()
.fold((0usize, 0usize, 0usize), |(e, a, l), p| {
(e + 1, a + usize::from(p.live() > 0), l + p.live())
});
let (room, windows) = match self.windows.as_deref() {
Some(w) => (
Some(match self.claim.as_ref().and_then(|c| c.0) {
Some(claim) => format!("g{:02}", claim.room.group),
None => "none".to_string(),
}),
Some(match w {
ExteriorWindows::Unrestricted => "unrestricted".to_string(),
ExteriorWindows::Windows(rects) => rects.len().to_string(),
}),
),
None => (None, None),
};
let sky = self.skybox.as_ref().map(|s| {
s.0.as_deref()
.map_or_else(|| "dome".to_string(), str::to_ascii_lowercase)
});
CensusReport {
submeshes,
drawn,
kinds: std::array::from_fn(|i| (KIND_COLUMNS[i], kinds[i].0, kinds[i].1)),
tagged,
hidden,
exempt: exempt_n,
no_aabb,
escaped,
labels,
emitters,
active_emitters,
particles,
room,
windows,
pvs_eye: self.cull_probe.as_deref().map(|p| p.eye),
sky,
ribbons: self.ribbons.as_deref().map(|r| (r.trails, r.drawn)),
cull: self.verdict.as_deref().map(|v| CullTerms {
windows: v
.windows
.map_or("unrestricted".to_string(), |n| n.to_string()),
frusta: v.frusta,
tested: v.tested,
hidden: v.hidden,
unbounded: v.unbounded,
bodies: v.bodies,
bodies_hidden: v.bodies_hidden,
liquid: v.liquid,
liquid_hidden: v.liquid_hidden,
}),
mats: self.mats.len(),
meshes: self.meshes.len(),
images: self.images.len(),
uv_anims: self.uv_reg.0.len(),
tint_anims: self.tint_reg.0.len(),
churn: self
.churn
.as_ref()
.map(|c| c.0.iter().map(|(k, n)| (*k, *n)).collect())
.unwrap_or_default(),
}
}
/// Every resident image/mesh/model by asset-server path, sorted, plus the counts that have no
/// path (runtime-built) in that same order. Diffing a tour probe's inventory against a fresh
/// control's names exactly which files a teardown left behind — the leak meter's magnifying
/// glass, and the one census answer expensive enough to ask for separately.
pub fn resident_assets(&self) -> (Vec<String>, [usize; 3]) {
let mut lines: Vec<String> = Vec::new();
let mut unpathed = [0usize; 3];
let kinds: [(&str, Vec<bevy::asset::UntypedAssetId>); 3] = [
(
"image",
self.images.ids().map(|i| i.untyped()).collect::<Vec<_>>(),
),
(
"mesh",
self.meshes.ids().map(|i| i.untyped()).collect::<Vec<_>>(),
),
(
"model",
self.models.ids().map(|i| i.untyped()).collect::<Vec<_>>(),
),
];
for (slot, (kind, ids)) in kinds.into_iter().enumerate() {
for id in ids {
match self.server.get_path(id) {
Some(p) => lines.push(format!("{kind} {p}")),
None => unpathed[slot] += 1,
}
}
}
lines.sort();
(lines, unpathed)
}
}
/// What the census reads off every model submesh — the query shape.
type CensusData = (
&'static ViewVisibility,
&'static ModelPart,
Has<ExteriorScene>,
Option<&'static WorldObject>,
&'static Visibility,
Option<&'static Aabb>,
Has<crate::billboard::BillboardCard>,
Option<&'static WmoGroupVis>,
);
/// The census column order, pinned: entry `i` names [`kind_index`]'s slot `i`. Column positions
/// in a line other tools diff, so they move for nobody.
const KIND_COLUMNS: [&str; 4] = ["doodad", "wmo", "creature", "gameobject"];
/// [`ModelKind`] has a private index; the census pins its own — see [`KIND_COLUMNS`].
fn kind_index(kind: ModelKind) -> usize {
match kind {
ModelKind::Doodad => 0,
ModelKind::Wmo => 1,
ModelKind::Creature => 2,
ModelKind::GameObject => 3,
}
}
/// `AssetEvent::Modified` counts per asset type across a window. A modified material re-creates
/// its uniform buffers + bind group that frame (the Metal non-bindless path), and a modified
/// image/mesh re-uploads — the teleport leak's CPU engine was exactly a per-frame ratchet of this
/// shape, so the floor hunt names the types instead of guessing suspects one at a time.
#[derive(Resource, Default)]
struct ChurnCensus(std::collections::BTreeMap<&'static str, usize>);
/// One census counter for asset type `A`, folding this frame's `Modified` events in under `label`
/// (a short stable name — the `type_name` of an `ExtendedMaterial` alias is unreadable).
fn churn_counter<A: bevy::asset::Asset>(
label: &'static str,
) -> impl FnMut(MessageReader<bevy::asset::AssetEvent<A>>, ResMut<ChurnCensus>) {
move |mut reader, mut census| {
let n = reader
.read()
.filter(|e| matches!(e, bevy::asset::AssetEvent::Modified { .. }))
.count();
if n > 0 {
*census.0.entry(label).or_default() += n;
}
}
}