Files
benilla-octo/crates/benilla-world/src/boot.rs
T
SamandClaude Fable 5 447e92aed5 three parked costs burned, two schedules go single-threaded
- the sun flare's terrain-occlusion march re-prices 2 grid cells a frame instead of 768 height queries, the retained pass's published regions go behind an Arc instead of two memcpys, and a unit's splash line is re-read only when its pose changed — −0.66 ms parked at Goldshire, pixels bit-identical
- PostUpdate and the render extract schedule run single-threaded by default — ~620 mostly no-op systems were paying the multi-threaded dispatch premium — −0.90 ms parked at Goldshire, −0.8 walking in Lower Blackrock Spire; WOW_MT_POSTUPDATE and WOW_MT_EXTRACT are the levers back
- GltfPlugin, AntiAliasPlugin and GilrsPlugin leave the boot — machinery for types nothing makes, 31 systems a frame fewer
- WOW_SCHED_CENSUS lists every schedule's systems with their executor-relevant flags, both worlds — the inventory the flips were priced against

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

117 lines
7.5 KiB
Rust

//! The **tuned Bevy boot** — the `DefaultPlugins` set every benilla binary stands on.
//!
//! Extracted from `benilla_app::run` the moment there was a second binary to boot ([`crate::worldview`],
//! decision 1160): the window differs per binary, but the *engine* tuning — the asset root, the log
//! filter, the task-pool sizing and QoS, and the disabled audio plugin — must not. Each of those
//! four is load-bearing and each was learned the hard way; a second boot that quietly re-derived
//! them would be a second place for them to rot.
//!
//! The `Window` stays the caller's, because that is genuinely per-binary: the client's is shaped by
//! the capture harness and the background-run rules (decisions 0703/0709/1148), the world viewer's
//! is a plain window.
use bevy::app::{PluginGroupBuilder, TaskPoolOptions, TaskPoolPlugin};
use bevy::prelude::*;
use crate::thread_qos;
/// `DefaultPlugins` with benilla's engine tuning applied, around the caller's primary window.
pub fn tuned_default_plugins(primary_window: Window) -> PluginGroupBuilder {
DefaultPlugins
.set(WindowPlugin {
primary_window: Some(primary_window),
..default()
})
// NOTE: there is deliberately no `AssetPlugin::file_path` here (decision 1175). This used
// to bake `concat!(env!("CARGO_MANIFEST_DIR"), "/assets")` — the *build* machine's source
// tree — because a shim package builds the binary and Bevy's runtime `CARGO_MANIFEST_DIR`
// fallback would otherwise resolve `assets/` in the shim's dir (0993). It worked on the
// machine that compiled it and nowhere else: on a player's machine every shader resolved
// to nothing and the world drew bare, the "silently-no-shaders trap" `capture/mod.rs`'s
// header names. Every WGSL file in the tree is now compiled into the binary
// (`crate::shaders`, `benilla_app::shaders`, `benilla_assets::materials`) and addressed
// `embedded://<crate>/shaders/…`, so nothing reaches for a file root at all and 1171's
// engine/game line survives as the crate each shader is embedded from.
// Quiet wgpu/naga; our own crates stay at info.
.set(bevy::log::LogPlugin {
filter: "wgpu=error,naga=warn".into(),
..default()
})
// Asset streaming is this client's load bottleneck: every M2/WMO/BLP read decompresses from
// MPQ and parses **synchronously** on Bevy's IO task pool, and the AssetServer runs *all*
// loads there. Bevy's default caps that pool at 4 threads, so a teleport into a dense area —
// terrain + WMOs + their doodad props, all bursting at once — saturates it and the
// net-driven NPC/GameObject models queue behind the flood. Give IO more of the box (it sits
// idle when not streaming); the world-render path is GPU/IO-bound, not compute-bound, so
// trading some compute threads for streaming throughput is the right call.
// Thread QoS (decision 0609): Bevy's workers spawn at default QoS — the same Darwin
// scheduling class as rustc or an OBS encoder — so under a background build the frame's own
// threads queue behind the compiler for P-core time. Promote them at spawn: compute runs
// this frame's systems (user-interactive); IO/async-compute feed upcoming frames
// (user-initiated — above default, below the frame itself). The render thread has no spawn
// hook; `ThreadQosPlugin` promotes it from inside.
.set(TaskPoolPlugin {
task_pool_options: TaskPoolOptions {
io: bevy::app::TaskPoolThreadAssignmentPolicy {
min_threads: 2,
max_threads: 8,
percent: 0.5,
on_thread_spawn: Some(std::sync::Arc::new(|| {
thread_qos::promote_current_thread(thread_qos::QosClass::UserInitiated)
})),
on_thread_destroy: None,
},
async_compute: bevy::app::TaskPoolThreadAssignmentPolicy {
on_thread_spawn: Some(std::sync::Arc::new(|| {
thread_qos::promote_current_thread(thread_qos::QosClass::UserInitiated)
})),
..TaskPoolOptions::default().async_compute
},
compute: bevy::app::TaskPoolThreadAssignmentPolicy {
on_thread_spawn: Some(std::sync::Arc::new(|| {
thread_qos::promote_current_thread(thread_qos::QosClass::UserInteractive)
})),
// `WOW_THREADS=1` serialises the systems that run this frame. Not a performance
// dial — a **diagnostic**: a defect that alternates frame to frame with no
// camera, geometry or draw-order change behind it is what an unordered write
// between two systems looks like, and that is separable from every other cause
// only by taking the concurrency away. Anything that survives `WOW_THREADS=1`
// is not a race.
max_threads: match std::env::var("WOW_THREADS").ok().as_deref() {
Some("1") => 1,
_ => TaskPoolOptions::default().compute.max_threads,
},
..TaskPoolOptions::default().compute
},
..default()
},
})
// Sound is kira behind our own mixer seam (decision 0070); Bevy's AudioPlugin would only
// open a second, never-used OS output stream at startup. Off (0530). Its rodio/cpal stack
// still compiles in via bevy's default feature — trimming the feature set is a separate,
// wider call.
.disable::<bevy::audio::AudioPlugin>()
// The dead registrations (decision 1438): DefaultPlugins members whose only runtime
// trace here was per-frame machinery for types nothing instantiates — every registered
// asset/material type costs an `Assets<T>` event system in PostUpdate plus
// extract/prepare/sweep families in the render app, priced by the 1437 census against
// the 1435 band map. Each cut was usage-grepped NEGATIVE and then proven by a clean
// boot, and the boot vetoed two of the five candidates the greps had passed (its
// missing-resource panic names the dependent): gizmos are the bowstring/fishing-line
// renderer (0938, warmed through a bare-`Gizmos` param no `Gizmos<` pattern sees), and
// the sprite pair carries our OWN FrameXML quad pass — `UiQuadMaterial` is a
// `Material2d` riding `Mesh2dPipeline`. What else stays, stays for a reason:
// bevy_picking drives the world-interact lane, TextPlugin draws the glue-screen text,
// PostProcessPlugin is the EffectGlow bloom, and two members are pinned by upstream
// exactly like 1437's Render-MT — ScenePlugin (avian's collider backend reads
// `SceneSpawner` even in a meshless world; our own harness tests document it) and
// PbrPlugin's ForwardDecal family (registered inside PbrPlugin::build).
//
// We load M2/WMO/ADT through our own mpq:// loaders — no glTF anywhere.
.disable::<bevy::gltf::GltfPlugin>()
// No bevy AA: no Fxaa/TAA/SMAA/CAS component anywhere (MSAA is core render, unaffected).
.disable::<bevy::anti_alias::AntiAliasPlugin>()
// No gamepad input; 1.12's bindings are keyboard/mouse (0997).
.disable::<bevy::gilrs::GilrsPlugin>()
}