the bevy 0.19 excursion: graded 5.5 ms slower lane-for-lane, rolled back

- the full bevy 0.19 / avian 0.7 / egui 0.35 port landed and rendered at visual parity, then profiling matched the lanes honestly: +5.5 ms against 0.18 in our scenes, priced to three named buckets — so it was reverted the same day, with the pins mapped for a future release
- back on 0.18 the client is faster than 0.18 ever ran here, keeping both performance days' wins
- what the hunt left behind: the material-animation scan visits only rows that can be registry keys, and a capture that settles on an all-black image now says EMPTY instead of grading it

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-08-16 17:54:09 +07:00
co-authored by Claude Fable 5
parent 11a889bc56
commit e3f9b7f400
10 changed files with 291 additions and 116 deletions
+17
View File
@@ -861,6 +861,23 @@ fn drive_capture(
watch.stable,
);
}
// The emptiness tripwire (1373): stability proves the image stopped changing, not
// that it contains anything — a lane that renders nothing is perfectly stable, and
// the 1371 black era sailed through this gate without a word. The bytes are already
// in hand; color channels only, since alpha is opaque even on a black frame.
if watch.stable >= stable_frames() || capped {
if let Some(px) = watch.prev.as_deref() {
if px
.chunks_exact(4)
.all(|p| p[0] == 0 && p[1] == 0 && p[2] == 0)
{
error!(
"capture: settled image is EMPTY — every color channel is zero; the \
scene rendered nothing (1373)"
);
}
}
}
if watch.stable < stable_frames() && !capped {
Phase::Building(n + 1)
} else if let Some((rw, rh)) = resize_request().filter(|_| !ctx.resized) {
@@ -29,6 +29,11 @@ use benilla_world::world_census::WorldCensus;
/// (scenario=`live`), and exits.
pub(crate) struct LiveFpsPlugin;
/// How long past `WOW_LIVE_FPS_AT` the probe waits for a world before declaring the run dead.
/// Entry normally lands well inside `at` itself; 60 s of grace covers a cold-cache load without
/// ever letting a stranded run ride out a harness timeout (see the `Waiting` arm).
const BOOT_DEADLINE_SECS: f32 = 60.0;
impl Plugin for LiveFpsPlugin {
fn build(&self, app: &mut App) {
let frames = std::env::var("WOW_LIVE_FPS")
@@ -146,6 +151,19 @@ fn drive_live_fps(
// (The un-occludable window that keeps the SETTLE phase from streaming the world at
// ~1 fps is [`ProbeFocusPlugin`]'s now — every probe needs it, not just this one.)
if time.elapsed_secs() < probe.at || self_player.is_empty() {
// The boot deadline: entering the world takes seconds, and `at` already grants a
// settle window on top — a run still worldless this far past it is stranded on a
// glue screen (dead server, refused login the marker arms didn't catch) and every
// second more is the 1371 sitting's dead wall-clock again. `FATAL` is the marker
// leg.sh keys on.
if time.elapsed_secs() > probe.at + BOOT_DEADLINE_SECS {
error!(
"live-fps: FATAL — still not in world {:.0}s past the probe delay; a measurement run with no world is dead. exiting",
BOOT_DEADLINE_SECS
);
exit.write(AppExit::error());
probe.phase = LiveFpsPhase::Done;
}
return;
}
if let Ok(mut w) = windows.single_mut() {
+13 -1
View File
@@ -277,6 +277,7 @@ fn apply_roster_policy(
// (decision 1174's always-present run fact) — absent in every ordinary run, which is the
// player answer and the one this screen was written for.
rig: Option<Res<crate::run_mode::RigCharacter>>,
mut exit: MessageWriter<AppExit>,
) {
if !roster.env_read {
roster.env_read = true;
@@ -357,7 +358,18 @@ fn apply_roster_policy(
info!("char select: WOW_CHAR={name} — fast path");
send_pick(&mut roster, &pick, guid);
}
None => warn!("char select: WOW_CHAR={name} not on this account — showing roster"),
None => {
warn!("char select: WOW_CHAR={name} not on this account — showing roster");
// A harness run (env creds, no smoke) can never pick a different row itself —
// parked here it burns its whole wall-clock. Same marker the login arm uses,
// same greppable verdict for leg.sh.
if crate::run_mode::unattended_login()
&& std::env::var_os("WOW_LOGIN_SMOKE").is_none()
{
error!("login: FATAL — WOW_CHAR={name} is not on this account; exiting");
exit.write(AppExit::error());
}
}
}
}
}
+24 -100
View File
@@ -17,9 +17,11 @@
//! re-authenticates on its own takes the account back off whoever just displaced it.
//!
//! Module split: this file (state, policy, input, dialogs, the saved-account persistence),
//! [`screen`] (the authored layout, transcribed from `AccountLogin.xml`).
//! [`screen`] (the authored layout, transcribed from `AccountLogin.xml`), [`smoke`] (the
//! `WOW_LOGIN_SMOKE` headless prover).
mod screen;
mod smoke;
use std::sync::atomic::Ordering;
@@ -42,6 +44,7 @@ use crate::portrait::{GluePreview, GlueScene};
use crate::sound::GlueSound;
pub(crate) use screen::LoginAction;
pub(crate) use smoke::smoke_character;
/// The flat resubmit pacing after a transport failure with pending credentials (decision 0065's
/// reconnect cadence, moved app-side by 0539 — the IO thread never sleeps).
@@ -79,7 +82,7 @@ impl Plugin for LoginPlugin {
)
.chain()
.run_if(in_state(ClientState::Login)),
(debug_login_smoke, screen::debug_login_shot),
(smoke::debug_login_smoke, screen::debug_login_shot),
)
.chain()
.after(benilla_world::schedule::WorldStage::Net),
@@ -205,8 +208,15 @@ fn drive_policy(
mut stages: MessageReader<LoginStageMessage>,
mut failures: MessageReader<LoginFailedMessage>,
mut disconnects: MessageReader<DisconnectedMessage>,
mut exit: MessageWriter<AppExit>,
) {
let now = time.elapsed_secs();
// A harness run (env creds, and not the smoke — the smoke owns its own verdict) has nobody at
// the keyboard: a login failure no resubmit can change leaves it parked on a dialog for its
// whole wall-clock, and every retry a runner grants it is spent the same way. Those failures
// exit non-zero instead, on one greppable marker — "login: FATAL" — that leg.sh keys on.
let harness =
crate::run_mode::unattended_login() && std::env::var_os("WOW_LOGIN_SMOKE").is_none();
let empty = GlueStrings::default();
let strings = strings.as_deref().unwrap_or(&empty);
@@ -239,6 +249,10 @@ fn drive_policy(
Err(why) => {
error!("login: REFUSING the env fast path — {why} Set WOW_ALLOW_ACCOUNT=1 if the cross-account login is deliberate.");
dialog.open_error(&why);
// The refusal is deterministic — the slot is baked into the binary — so the
// run can never get past this screen (the 1371 legs burned 3 × timeout on it).
error!("login: FATAL — account guard refused the only credentials this run has; exiting");
exit.write(AppExit::error());
}
}
}
@@ -258,6 +272,10 @@ fn drive_policy(
warn!("login: {}", msg.reason);
intent.clear();
dialog.open_error(&msg.reason);
if harness {
error!("login: FATAL — terminal login failure with nobody at the keyboard ({}); exiting", msg.reason);
exit.write(AppExit::error());
}
continue;
}
match msg.code {
@@ -267,6 +285,10 @@ fn drive_policy(
warn!("login: refused (code {code:#04x}) — {}", msg.reason);
intent.clear();
dialog.open_error(fail_text(strings, Some(code)));
if harness {
error!("login: FATAL — refused (code {code:#04x}) and no resubmit can change it; exiting");
exit.write(AppExit::error());
}
}
None if intent.announced => {
warn!("login: {}", msg.reason);
@@ -760,87 +782,6 @@ fn save_account(name: &str) {
}
}
// ── Instruments ──────────────────────────────────────────────────────────────────────────────────
/// Split `WOW_LOGIN_SMOKE=user:pass[:Character]` into its three fields. **Three-way, once**: a
/// `split_once(':')` here would hand the character name to the password, which is exactly what it
/// did on this instrument's first live run (`result 0x04`, and the wrong-password path is what
/// this smoke exists to prove, so it looked plausible).
fn smoke_spec(spec: &str) -> (&str, &str, Option<&str>) {
let mut parts = spec.splitn(3, ':');
let user = parts.next().unwrap_or_default();
let pass = parts.next().unwrap_or_default();
let character = parts.next().map(str::trim).filter(|n| !n.is_empty());
(user, pass, character)
}
/// The optional third field — the body to enter as, or `None` for the plain
/// reach-character-select smoke. [`crate::char_select`]'s roster policy is what actually answers
/// the roster with it.
pub(crate) fn smoke_character(spec: &str) -> Option<String> {
smoke_spec(spec).2.map(str::to_string)
}
/// The login smoke (`WOW_LOGIN_SMOKE=user:pass[:Character]`, decision 0539 §7): once the screen is
/// up, submit those credentials through the real screen path; exit success on reaching CharSelect,
/// log + exit failure on a refusal — the wrong-password path is provable headlessly.
///
/// **Naming a character keeps the run going into the world instead of exiting** (decision 1262).
/// This is the only headless way to reach the world down the *player's* path: `WOW_CHAR` also
/// enters the world, but setting it makes the run **unattended**
/// ([`crate::run_mode::unattended_login`]), and an unattended run takes the other branch at every
/// session decision — so the attended half was untestable without a person at the keyboard. The
/// pick itself stays `char_select`'s (`apply_roster_policy` reads the third field the same way it
/// reads `WOW_CHAR`); this only declines to exit. Pair with `WOW_PROBE_EXIT_AT` to bound the run.
#[allow(clippy::too_many_arguments)]
fn debug_login_smoke(
state: Res<State<ClientState>>,
mut intent: ResMut<LoginIntent>,
submit: Res<LoginSubmit>,
abandon: Res<LoginAbandon>,
mut failures: MessageReader<LoginFailedMessage>,
time: Res<Time>,
mut exit: MessageWriter<AppExit>,
mut phase: Local<u8>,
) {
let Ok(spec) = std::env::var("WOW_LOGIN_SMOKE") else {
return;
};
match *phase {
0 if *state.get() == ClientState::Login && time.elapsed_secs() > 2.0 => {
let (user, pass, _) = smoke_spec(&spec);
info!("login-smoke: submitting as {user}");
let (user, pass) = (user.to_string(), pass.to_string());
send_login(&mut intent, &submit, &abandon, &user, &pass, true);
*phase = 1;
}
1 => {
if let Some(f) = failures.read().last() {
error!("login-smoke: FAILED code={:?} reason={}", f.code, f.reason);
// `WOW_LOGIN_SMOKE_HOLD=1`: keep running on a refusal instead of exiting — the
// error dialog stays up, so a shot instrument can photograph it (the dialog is
// otherwise unreachable headlessly; pair with `WOW_PROBE_EXIT_AT`).
if std::env::var_os("WOW_LOGIN_SMOKE_HOLD").is_none() {
exit.write(AppExit::error());
}
*phase = 2;
} else if *state.get() == ClientState::CharSelect {
match smoke_character(&spec) {
Some(name) => {
info!("login-smoke: reached character select — entering as {name}");
}
None => {
info!("login-smoke: reached character select — done");
exit.write(AppExit::Success);
}
}
*phase = 2;
}
}
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -956,23 +897,6 @@ mod tests {
assert!(app.world().resource::<LoginDialog>().kind.is_none());
}
/// The smoke spec splits three ways — the character name must never end up in the password.
/// A `split_once` did exactly that on the instrument's first live run, and the refusal it
/// produced (`0x04`) is indistinguishable from the wrong-password case this smoke exists to
/// exercise.
#[test]
fn the_smoke_spec_splits_three_ways() {
assert_eq!(
smoke_spec("probe1:pprobe1:Probeone"),
("probe1", "pprobe1", Some("Probeone"))
);
assert_eq!(smoke_spec("probe1:pprobe1"), ("probe1", "pprobe1", None));
// A trailing colon names no body; a password may not itself contain one (the client caps
// both fields at 16 letters and vmangos accounts have none).
assert_eq!(smoke_spec("one:pass:"), ("one", "pass", None));
assert_eq!(smoke_spec("one"), ("one", "", None));
}
/// The code→string map quotes the client's own strings for the vmangos-verified rows.
#[test]
fn fail_text_maps_the_verified_codes() {
+109
View File
@@ -0,0 +1,109 @@
//! The login smoke (`WOW_LOGIN_SMOKE`) — the headless prover for the screen's own submit path.
//! Split from `mod.rs` when the file outgrew its budget; the policy and the screen stay there.
use bevy::prelude::*;
use super::{send_login, LoginIntent};
use crate::char_select::ClientState;
use crate::net::{LoginAbandon, LoginFailedMessage, LoginSubmit};
/// Split `WOW_LOGIN_SMOKE=user:pass[:Character]` into its three fields. **Three-way, once**: a
/// `split_once(':')` here would hand the character name to the password, which is exactly what it
/// did on this instrument's first live run (`result 0x04`, and the wrong-password path is what
/// this smoke exists to prove, so it looked plausible).
fn smoke_spec(spec: &str) -> (&str, &str, Option<&str>) {
let mut parts = spec.splitn(3, ':');
let user = parts.next().unwrap_or_default();
let pass = parts.next().unwrap_or_default();
let character = parts.next().map(str::trim).filter(|n| !n.is_empty());
(user, pass, character)
}
/// The optional third field — the body to enter as, or `None` for the plain
/// reach-character-select smoke. [`crate::char_select`]'s roster policy is what actually answers
/// the roster with it.
pub(crate) fn smoke_character(spec: &str) -> Option<String> {
smoke_spec(spec).2.map(str::to_string)
}
/// The login smoke (`WOW_LOGIN_SMOKE=user:pass[:Character]`, decision 0539 §7): once the screen is
/// up, submit those credentials through the real screen path; exit success on reaching CharSelect,
/// log + exit failure on a refusal — the wrong-password path is provable headlessly.
///
/// **Naming a character keeps the run going into the world instead of exiting** (decision 1262).
/// This is the only headless way to reach the world down the *player's* path: `WOW_CHAR` also
/// enters the world, but setting it makes the run **unattended**
/// ([`crate::run_mode::unattended_login`]), and an unattended run takes the other branch at every
/// session decision — so the attended half was untestable without a person at the keyboard. The
/// pick itself stays `char_select`'s (`apply_roster_policy` reads the third field the same way it
/// reads `WOW_CHAR`); this only declines to exit. Pair with `WOW_PROBE_EXIT_AT` to bound the run.
#[allow(clippy::too_many_arguments)]
pub(super) fn debug_login_smoke(
state: Res<State<ClientState>>,
mut intent: ResMut<LoginIntent>,
submit: Res<LoginSubmit>,
abandon: Res<LoginAbandon>,
mut failures: MessageReader<LoginFailedMessage>,
time: Res<Time>,
mut exit: MessageWriter<AppExit>,
mut phase: Local<u8>,
) {
let Ok(spec) = std::env::var("WOW_LOGIN_SMOKE") else {
return;
};
match *phase {
0 if *state.get() == ClientState::Login && time.elapsed_secs() > 2.0 => {
let (user, pass, _) = smoke_spec(&spec);
info!("login-smoke: submitting as {user}");
let (user, pass) = (user.to_string(), pass.to_string());
send_login(&mut intent, &submit, &abandon, &user, &pass, true);
*phase = 1;
}
1 => {
if let Some(f) = failures.read().last() {
error!("login-smoke: FAILED code={:?} reason={}", f.code, f.reason);
// `WOW_LOGIN_SMOKE_HOLD=1`: keep running on a refusal instead of exiting — the
// error dialog stays up, so a shot instrument can photograph it (the dialog is
// otherwise unreachable headlessly; pair with `WOW_PROBE_EXIT_AT`).
if std::env::var_os("WOW_LOGIN_SMOKE_HOLD").is_none() {
exit.write(AppExit::error());
}
*phase = 2;
} else if *state.get() == ClientState::CharSelect {
match smoke_character(&spec) {
Some(name) => {
info!("login-smoke: reached character select — entering as {name}");
}
None => {
info!("login-smoke: reached character select — done");
exit.write(AppExit::Success);
}
}
*phase = 2;
}
}
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
/// The smoke spec splits three ways — the character name must never end up in the password.
/// A `split_once` did exactly that on the instrument's first live run, and the refusal it
/// produced (`0x04`) is indistinguishable from the wrong-password case this smoke exists to
/// exercise.
#[test]
fn the_smoke_spec_splits_three_ways() {
assert_eq!(
smoke_spec("probe1:pprobe1:Probeone"),
("probe1", "pprobe1", Some("Probeone"))
);
assert_eq!(smoke_spec("probe1:pprobe1"), ("probe1", "pprobe1", None));
// A trailing colon names no body; a password may not itself contain one (the client caps
// both fields at 16 letters and vmangos accounts have none).
assert_eq!(smoke_spec("one:pass:"), ("one", "pass", None));
assert_eq!(smoke_spec("one"), ("one", "", None));
}
}
@@ -227,6 +227,14 @@ pub fn sample_mat_anim(
/// seq-band loop per play (arm cursor) and a gseq loop per instance (attach anchor,
/// `gseq-anchor.md`), but one uniform per material cannot phase per instance — meaningless for a
/// seamless scroll either way. Entries drop when the material asset does.
/// The scan marker (1375): a part whose material can ever be a [`UvAnimMaterials`]/
/// [`TintAnimMaterials`] key — inserted at spawn, beside the registration itself, and only for a
/// loop with a real period (a period-0 constant is fully served by its material seed). Without
/// it, [`tick_anim_materials`]'s draw scan visited every `WowModelMaterial` row in the world
/// (~48k at Stormwind) to find the placed instances of ~113 animated models.
#[derive(bevy::prelude::Component)]
pub struct AnimMatPart;
#[derive(Resource, Default)]
pub struct UvAnimMaterials(
pub std::collections::HashMap<
@@ -264,16 +272,21 @@ pub struct UvAnimMaterials(
/// It over-includes (a part left `Inherited` under a hidden ancestor counts as drawn), which is the
/// safe direction: an extra write costs a frame's uniform upload, a missed one would freeze a
/// visible scroll.
#[allow(clippy::too_many_arguments)]
pub(super) fn tick_anim_materials(
time: Res<Time>,
real: Res<Time<bevy::time::Real>>,
mut uv_reg: ResMut<UvAnimMaterials>,
mut tint_reg: ResMut<TintAnimMaterials>,
mut materials: ResMut<Assets<benilla_assets::materials::WowModelMaterial>>,
parts: Query<(
&MeshMaterial3d<benilla_assets::materials::WowModelMaterial>,
&Visibility,
)>,
parts: Query<
(
&MeshMaterial3d<benilla_assets::materials::WowModelMaterial>,
&Visibility,
),
With<AnimMatPart>,
>,
twins: Res<crate::model_render::FarSideTwins>,
mut drawn: Local<
bevy::platform::collections::HashSet<AssetId<benilla_assets::materials::WowModelMaterial>>,
>,
@@ -287,7 +300,12 @@ pub(super) fn tick_anim_materials(
drawn.clear();
for (mat, vis) in &parts {
if *vis != Visibility::Hidden {
drawn.insert(mat.id());
let id = mat.id();
// A far-classified instance carries the far TWIN's id — never a registry key. Count
// it as its near identity, or a batch whose every instance sits beyond the water
// plane marks its near entry not-drawn and freezes both variants' scroll (1375; the
// twin itself keeps getting written by `classify_water_side`'s Modified mirror).
drawn.insert(twins.near_of(id).unwrap_or(id));
}
}
let now = time.elapsed_secs();
@@ -298,23 +316,45 @@ pub(super) fn tick_anim_materials(
if !drawn.contains(id) {
return materials.contains(*id);
}
let Some(mat) = materials.get_mut(*id) else {
return false; // material unloaded with its cache — drop the entry
};
let uv = anim.sample(now);
mat.extension.sun_scale.z = uv[0];
mat.extension.sun_scale.w = uv[1];
// The write gate (`write_gated`'s reasoning, 1375): a `get_mut` deref alone marks the
// asset Modified — a uniform re-upload, a bind-group rebuild on the non-bindless Metal
// path, and the whole-population `AssetChanged` walks — so an unchanged value must be
// decided on the immutable view and never deref. Quantized because the input drifts
// continuously: 1/4096 of a texture repeat is below what any face can show.
let (zq, wq) = (
benilla_assets::quantize(uv[0], 4096.0),
benilla_assets::quantize(uv[1], 4096.0),
);
match materials.get(*id) {
None => return false, // material unloaded with its cache — drop the entry
Some(m) if m.extension.sun_scale.z == zq && m.extension.sun_scale.w == wq => {
return true;
}
Some(_) => {}
}
let Some(mat) = materials.get_mut(*id) else {
return false;
};
mat.extension.sun_scale.z = zq;
mat.extension.sun_scale.w = wq;
true
});
tint_reg.0.retain(|id, anim| {
if !drawn.contains(id) {
return materials.contains(*id);
}
let rgb = benilla_assets::quant255(anim.sample(now));
let tint = bevy::math::Vec4::new(rgb[0], rgb[1], rgb[2], 1.0);
match materials.get(*id) {
None => return false,
Some(m) if m.extension.tint == tint => return true,
Some(_) => {}
}
let Some(mat) = materials.get_mut(*id) else {
return false;
};
let rgb = anim.sample(now);
mat.extension.tint = bevy::math::Vec4::new(rgb[0], rgb[1], rgb[2], 1.0);
mat.extension.tint = tint;
true
});
}
+3 -1
View File
@@ -39,7 +39,9 @@ mod lazy;
mod mat_anim;
pub(crate) use lazy::{LazyRig, SkinnedTwin};
use mat_anim::tick_anim_materials;
pub use mat_anim::{playing_seq, sample_mat_anim, MatAnim, TintAnimMaterials, UvAnimMaterials};
pub use mat_anim::{
playing_seq, sample_mat_anim, AnimMatPart, MatAnim, TintAnimMaterials, UvAnimMaterials,
};
/// The client's single global `rand()` stream — the MSVC LCG at `0x7400e5`, returning `[0, 32767]`
/// (wow-re `doodad-anim-host.md` §5, decision 0768). Every doodad's variation roll draws from **one**
+10
View File
@@ -684,6 +684,16 @@ impl FarSideTwins {
) -> Option<&Handle<WowModelMaterial>> {
self.to_far.get(&near.id())
}
/// The near identity of a far twin, if `id` is one — the mat-anim draw scan asks, because
/// its registry keys are always near ids while a far-classified instance carries the twin's
/// (1375).
pub(crate) fn near_of(
&self,
id: AssetId<WowModelMaterial>,
) -> Option<AssetId<WowModelMaterial>> {
self.to_near.get(&id).map(bevy::asset::Handle::id)
}
}
/// The far-axis compose every material-handle OWNER applies at its own write site: the handle it
@@ -263,13 +263,16 @@ pub fn spawn_model_entities(
};
// Register both variants' materials for the per-frame UV scroll (idempotent per material —
// every instance of the batch lands on the same deduped handle).
if let Some(anim) = &sub.uv_anim {
// A period-0 loop is a constant the material seed already wrote (`sample(0.0)` IS its
// forever value) — registering it would buy a per-frame re-write of the same number
// (1375), so only a real loop enters the registry.
if let Some(anim) = sub.uv_anim.as_ref().filter(|a| a.period > 0.0) {
uv_reg.0.entry(cutout.id()).or_insert_with(|| anim.clone());
uv_reg.0.entry(blend.id()).or_insert_with(|| anim.clone());
}
// …and for the per-frame tint re-sample (the animated M2Color RGB, same shared clock —
// the same invisible seq-band phase divergence as the UV scroll, recorded there).
if let Some(anim) = &sub.rgb_anim {
if let Some(anim) = sub.rgb_anim.as_ref().filter(|a| a.period > 0.0) {
tint_reg
.0
.entry(cutout.id())
@@ -398,6 +401,16 @@ pub fn spawn_model_entities(
// same way since the 0355 re-lane gave the probe-slot payload its own alpha field (bits
// 0..=15) — the 0130-era "partial alpha can't show on the colour payload" deferral is
// collected (bug B30: the Undercity lightshaft's authored 0.10/0.05 dimming).
// The scan marker (1375), same predicate as the registration above — so a marked row
// always has a registry key to match, and `tick_anim_materials` never has to visit an
// unmarked one.
if sub.uv_anim.as_ref().is_some_and(|a| a.period > 0.0)
|| sub.rgb_anim.as_ref().is_some_and(|a| a.period > 0.0)
{
commands
.entity(entity)
.insert(crate::doodad_anim::AnimMatPart);
}
if let Some(anim) = &sub.alpha_anim {
commands
.entity(entity)
+30
View File
@@ -220,5 +220,35 @@ impl Plugin for WorldFoundation {
..Default::default()
});
}
// Direct draws on EVERY camera — the default, not a knob (1374/1376). Bevy's indirect
// lane is a per-draw indirect encode loop on Metal (no MULTI_DRAW_INDIRECT) plus the GPU
// preprocessing dispatches, and the 1374 bracket priced it ~4.3 cpu_ms at LBRS on 0.18 —
// shipped unclaimed for the campaign's whole run because 1364's knob under-measured it
// (the UI Camera2d leak, 1370). `WOW_INDIRECT=1` restores the upstream default for
// re-pricing. 1370's note that the lever must cover every camera, not just the world's,
// is why this is a sweep.
if std::env::var_os("WOW_INDIRECT").is_none() {
app.add_systems(bevy::app::Update, force_direct_draws);
}
}
}
/// Insert [`NoIndirectDrawing`] on any camera that lacks it — see the note at the registration
/// site. Runs every frame so late-spawned cameras (portrait booths, the glue booth) are covered;
/// the query is empty in steady state.
fn force_direct_draws(
mut commands: Commands,
cams: Query<
Entity,
(
With<bevy::camera::Camera>,
Without<bevy::render::view::NoIndirectDrawing>,
),
>,
) {
for e in &cams {
commands
.entity(e)
.insert(bevy::render::view::NoIndirectDrawing);
}
}