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>
This commit is contained in:
Sam
2026-08-20 18:59:48 +07:00
co-authored by Claude Fable 5
parent 59e07d5a9a
commit 2fbe661a1d
12 changed files with 349 additions and 41 deletions
+1 -1
View File
@@ -114,7 +114,7 @@ pub(crate) use probe_taxi::ProbeTaxiPlugin;
pub(crate) use probes::{
fx_draw_census_plugin, EntityCensusPlugin, GroundCensusPlugin, LiveFpsPlugin, NodeProbePlugin,
ParticleCensusPlugin, ProbeChatPlugin, ProbeClock, ProbeExitPlugin, ProbeFocusPlugin,
ProbeKeyPlugin, ProbeLuaPlugin, ProbeResizePlugin, UnitVisualsPlugin,
ProbeKeyPlugin, ProbeLuaPlugin, ProbeResizePlugin, SchedCensusPlugin, UnitVisualsPlugin,
};
use scenarios::GlueScreen;
use scenarios::{Scenario, SubjectKind, UiFixture, GLUE_SCENARIOS, GROUND_EYE, SCENARIOS};
@@ -45,6 +45,11 @@ pub(crate) use visual_census::UnitVisualsPlugin;
mod world_census;
pub(crate) use world_census::{EntityCensusPlugin, NodeProbePlugin};
/// The schedule census — per schedule, every system with its executor-relevant flags, both
/// worlds: the structural inventory under the 1435 orchestration rows (decision 1437).
mod sched_census;
pub(crate) use sched_census::SchedCensusPlugin;
/// The clock **every probe schedule reads** — real time, never the virtual clock (decision 0789).
///
/// A probe knob is a wall-clock instruction: "send this at 20 s", "sample 300 frames from 25 s",
@@ -0,0 +1,139 @@
//! The schedule census (`WOW_SCHED_CENSUS=1`): the structural inventory behind the 1435 band
//! map's orchestration rows — per schedule, every system with its executor-relevant flags
//! (non-`Send`, exclusive, has-deferred), for BOTH worlds. Born with decision 1437: the parked
//! frame pays ~5.4 ms/f of pure scheduling (schedule selves + executor + empty command applies
//! across ~600 executed systems), and neither consolidation nor executor choices can be argued
//! about until the population has names and counts.
//!
//! Why runtime one-shots instead of a pre-run `Schedule::initialize` walk: bevy removes a
//! schedule from the `Schedules` resource WHILE it runs (`World::schedule_scope`), so no single
//! vantage sees everything — and pre-run initialization is the wrong tool twice over
//! (`Extract` params panic without the runtime-only `MainWorld`, and `Local<impl FromWorld>`
//! state may read resources Startup hasn't inserted yet). Two vantages per world cover each
//! other: `Update` and `PostUpdate` in the main world, `ExtractSchedule` and `Render` in the
//! render app. Only the `Main`/`RenderStartup` runners stay invisible (each is the fixed
//! one-system runner bevy ships).
use bevy::prelude::*;
use bevy::render::{ExtractSchedule, Render, RenderApp};
/// Frames to wait before dumping — deep enough that every schedule has run at least once (the
/// census reads the live graph, not what a first frame happens to have reached).
const CENSUS_FRAME: u32 = 10;
/// Frames to wait before the census run exits — the render app runs a frame behind the main
/// world (pipelined), so give its vantages room past [`CENSUS_FRAME`] before the app closes.
const EXIT_FRAME: u32 = 40;
/// The census instrument. Purely structural — it does not need the world, a server, or a
/// character: the schedule graph is fixed once the plugins have built, so a login-screen run
/// answers for every state.
pub(crate) struct SchedCensusPlugin;
impl Plugin for SchedCensusPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<DumpedSchedules>()
.add_systems(Update, census_vantage("main", "Update"))
.add_systems(PostUpdate, census_vantage("main", "PostUpdate"))
.add_systems(Last, census_exit);
// The render app still lives in the main `App` here (pipelining detaches it at cleanup,
// after every plugin has built). Its two vantages mirror the main world's pair.
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app
.init_resource::<DumpedSchedules>()
.add_systems(ExtractSchedule, census_vantage("render", "Extract"))
.add_systems(Render, census_vantage("render", "Render"));
}
}
}
/// Which schedules this world's earlier vantage already printed — the later vantage adds only
/// what the earlier one could not see (itself, chiefly).
#[derive(Resource, Default)]
struct DumpedSchedules(std::collections::HashSet<String>);
/// One vantage: an exclusive one-shot that, at [`CENSUS_FRAME`], prints every schedule visible
/// in `Schedules` that no earlier vantage in this world has printed.
fn census_vantage(
world_tag: &'static str,
vantage: &'static str,
) -> impl FnMut(&mut World, Local<u32>) {
move |world: &mut World, mut frame: Local<u32>| {
*frame += 1;
if *frame != CENSUS_FRAME {
return;
}
world.resource_scope::<DumpedSchedules, _>(|world, mut dumped| {
world.resource_scope::<Schedules, _>(|_, schedules| {
let mut labels: Vec<(String, &bevy::ecs::schedule::Schedule)> = schedules
.iter()
.map(|(label, sched)| (format!("{label:?}"), sched))
.filter(|(name, _)| !dumped.0.contains(name))
.collect();
labels.sort_by(|a, b| a.0.cmp(&b.0));
for (name, sched) in labels {
dump_schedule(world_tag, vantage, &name, sched);
dumped.0.insert(name);
}
});
});
}
}
/// Print one schedule: the tally line, then one line per system. `systems()` errors only on an
/// uninitialized graph — impossible by [`CENSUS_FRAME`] for a schedule that runs, and a
/// registered-but-never-run schedule is exactly worth flagging as such.
fn dump_schedule(
world_tag: &str,
vantage: &str,
name: &str,
sched: &bevy::ecs::schedule::Schedule,
) {
let Ok(systems) = sched.systems() else {
println!(
"SCHED_CENSUS world={world_tag} vantage={vantage} schedule={name} \
n={} uninitialized=1",
sched.systems_len()
);
return;
};
let (mut n, mut nonsend, mut exclusive, mut deferred) = (0u32, 0u32, 0u32, 0u32);
let mut rows: Vec<String> = Vec::new();
for (_, sys) in systems {
n += 1;
let mut flags = String::new();
if !sys.is_send() {
nonsend += 1;
flags.push('N');
}
if sys.is_exclusive() {
exclusive += 1;
flags.push('X');
}
if sys.has_deferred() {
deferred += 1;
flags.push('D');
}
rows.push(format!(
"SCHED_SYS world={world_tag} schedule={name} flags={} name={}",
if flags.is_empty() { "-".into() } else { flags },
sys.name()
));
}
println!(
"SCHED_CENSUS world={world_tag} vantage={vantage} schedule={name} \
n={n} nonsend={nonsend} exclusive={exclusive} deferred={deferred}"
);
for row in rows {
println!("{row}");
}
}
/// Close the run once both worlds have had time to print — the census is its own whole run.
fn census_exit(mut frame: Local<u32>, mut exit: MessageWriter<AppExit>) {
*frame += 1;
if *frame == EXIT_FRAME {
println!("SCHED_CENSUS_DONE");
exit.write(AppExit::Success);
}
}
+6
View File
@@ -199,6 +199,12 @@ impl Plugin for DevProbesPlugin {
if std::env::var("WOW_ENTITY_CENSUS").is_ok() {
app.add_plugins(crate::capture::EntityCensusPlugin);
}
// The schedule census: `WOW_SCHED_CENSUS=1` prints every schedule's systems with their
// executor-relevant flags, both worlds, then exits — the structural inventory under
// the orchestration bands (see `capture::SchedCensusPlugin`, decision 1437).
if std::env::var("WOW_SCHED_CENSUS").is_ok() {
app.add_plugins(crate::capture::SchedCensusPlugin);
}
// The melee live probe: `WOW_PROBE=melee` auto-fights the nearest enemy so the dbg-trace
// sink can record the combat-text timeline (see `capture::ProbeMeleePlugin`).
if std::env::var("WOW_PROBE").as_deref() == Ok("melee") {
+45 -8
View File
@@ -251,22 +251,29 @@ pub fn run(build: BuildId) -> AppExit {
// 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.
// both pins). `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);
});
}
// `WOW_ST_POSTUPDATE=1` — 1366's named open probe: PostUpdate on the single-threaded
// executor, an EXPERIMENT lever only. The expectation is a REGRESSION (propagation and
// visibility are real parallel bands there, not flag-serialized non-Send work); the lever
// exists so the expectation gets measured instead of assumed.
if std::env::var_os("WOW_ST_POSTUPDATE").is_some() {
// PostUpdate too (decision 1437, closing 1366's named open probe — which expected a
// REGRESSION here and was refuted by measurement). The census: 208 systems paying
// ~10 µs/system of MT dispatch parked (schedule self 2.09 ms/f in the 1435 band map) while
// the wide bands the 1366 expectation leaned on are exactly the ones later work gated or
// pinned (1429 idle-gates the animation par sweep, 1356 pins static-transform tracking).
// Graded −0.49 cpu_ms parked (all 5 rounds negative) and winning IN MOTION as part of the
// combo (−0.76/−0.80 across two 4-round LBRS walk sittings), parked tails flat, motion
// p99/max flipping sign wholesale between sittings on both executors — 1366's own
// noise-dominated shape. `WOW_MT_POSTUPDATE=1` is the lever back; the engagement line makes
// every leg log name the config it measured.
if std::env::var_os("WOW_MT_POSTUPDATE").is_none() {
app.edit_schedule(PostUpdate, |s| {
s.set_executor_kind(bevy::ecs::schedule::ExecutorKind::SingleThreaded);
});
println!(
"executor: PostUpdate -> single-threaded (1437 default; WOW_MT_POSTUPDATE=1 for MT)"
);
}
// …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
@@ -676,6 +683,36 @@ pub fn run(build: BuildId) -> AppExit {
// Register benilla-assets' loaders AFTER `AssetPlugin` (they go into the live `AssetServer`).
benilla_assets::register_asset_loaders(&mut app);
// The render app's `ExtractSchedule` runs SINGLE-THREADED too (decision 1437, same grading
// as the PostUpdate flip above): bevy_render never sets an executor kind, so it ran the
// multi-threaded one by default — 0.34 ms/f of schedule self in the 1435 parked band map
// over 165 systems (census) with zero true non-Send members. Graded −0.26 parked alone,
// −0.90 parked as the combo. `WOW_MT_EXTRACT=1` is the lever back.
//
// The `Render` schedule stays MULTI-threaded, and not as a tuning judgment (1437, measured
// the hard way): under pipelined rendering the MT executor is ALSO bevy's non-Send→main-
// thread routing, and `bevy_render::view::window::create_surfaces` is non-Send precisely to
// ride it — on macOS a Metal layer can only be made on the UI thread, and the ST executor
// runs everything on the render thread (`get_metal_layer cannot be called in non-ui thread`,
// a startup panic). One system pins the schedule; its 1.06 ms/f self stays on the table
// until that coupling changes upstream.
//
// Lives HERE, not beside its siblings: the render sub-app only exists once the plugin chain
// has built (pipelining detaches it at cleanup, later still).
if std::env::var_os("WOW_MT_EXTRACT").is_none() {
if let Some(render_app) = app.get_sub_app_mut(bevy::render::RenderApp) {
render_app.edit_schedule(bevy::render::ExtractSchedule, |s| {
s.set_executor_kind(bevy::ecs::schedule::ExecutorKind::SingleThreaded);
});
println!(
"executor: ExtractSchedule -> single-threaded (1437 default; WOW_MT_EXTRACT=1 for MT)"
);
} else {
// A silently missing sub-app would flip nothing — say so instead of measuring a ghost.
eprintln!("executor: no render app — ExtractSchedule flip NOT applied");
}
}
// **The probe fleet** — the capture harness and every scripted live probe, each armed by its
// own environment variable and inert without it. Added last so they observe the fully-built
// app; compiled out entirely by `--no-default-features` (decisions 1173/1174), which is why
+8 -1
View File
@@ -43,10 +43,17 @@ const SPLASH_DEPTH_FRAC: f32 = 0.4;
/// by entity and `WorldPoint` looks its room up.
type SplashQuery = (Entity, &'static Transform, Option<&'static CollisionHeight>);
/// Only units whose depth can have changed since last frame (decision 1436): depth is a pure
/// function of pose + collision height (the room claim follows the pose, liquid surfaces are
/// static), so an unmoved unit cannot cross the line — and the per-unit `water_surface_at`
/// lookup for EVERY unit priced 0.19 ms/f parked in the 1435 band map. A unit's first frame is
/// `Added ⊆ Changed`, so the silent first-seen arming below still happens.
type SplashGate = Or<(Changed<Transform>, Changed<CollisionHeight>)>;
/// Play the water splash on a unit's `0.4·h` depth-line crossing, either direction (module docs).
#[allow(clippy::too_many_arguments)] // the sound-play plumbing, one param per concern
fn water_splashes(
units: Query<SplashQuery, With<NetEntity>>,
units: Query<SplashQuery, (With<NetEntity>, SplashGate)>,
world: benilla_world::world_point::WorldPoint,
mut wet: Local<EntityHashMap<bool>>,
kits: Option<ResMut<SoundKits>>,
+2
View File
@@ -66,6 +66,7 @@ const BG_ENV_PREFIXES: &[&str] = &[
"WOW_PORTRAIT_TEST",
"WOW_PROBE",
"WOW_RIG",
"WOW_SCHED_CENSUS",
"WOW_WORLDVIEW_",
];
@@ -99,6 +100,7 @@ const NO_PIXEL_ENV_PREFIXES: &[&str] = &[
"WOW_PARTICLE_CENSUS",
"WOW_PROBE",
"WOW_RIG",
"WOW_SCHED_CENSUS",
// The engine boot check reads no pixels — it reads the ERROR LOG. Its sibling
// `WOW_WORLDVIEW_SHOT` is deliberately absent: that one photographs the frame, and the
// all-of rule below keeps a CHECK+SHOT pairing at full size.
+22
View File
@@ -91,4 +91,26 @@ pub fn tuned_default_plugins(primary_window: Window) -> PluginGroupBuilder {
// 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>()
}
+3 -3
View File
@@ -65,7 +65,7 @@ pub(super) fn flush_static_gx(mut gx: ResMut<StaticGx>, mut meshes: ResMut<Asset
state.items.len(),
baked.draws.last().map_or(0, |d| d.vertex_range.end),
);
world.cells.insert(cell, baked);
world.cells.insert(cell, std::sync::Arc::new(baked));
}
for (&instance, state) in wmos.iter_mut() {
if !bake_due(state, world.wmos.contains_key(&instance), frame) {
@@ -93,7 +93,7 @@ pub(super) fn flush_static_gx(mut gx: ResMut<StaticGx>, mut meshes: ResMut<Asset
baked.groups.len(),
baked.draws.last().map_or(0, |d| d.vertex_range.end),
);
world.wmos.insert(instance, baked);
world.wmos.insert(instance, std::sync::Arc::new(baked));
}
// The prop regions (B4): the WMO loop's shape with the referrer SET as the selection
// grain — the sort keeps runs set-homogeneous, and the published draw carries the
@@ -122,7 +122,7 @@ pub(super) fn flush_static_gx(mut gx: ResMut<StaticGx>, mut meshes: ResMut<Asset
baked.sets.len(),
baked.draws.last().map_or(0, |d| d.vertex_range.end),
);
world.props.insert(instance, baked);
world.props.insert(instance, std::sync::Arc::new(baked));
}
}
@@ -338,6 +338,10 @@ pub(super) fn cull_cells(
cell.settled = if unarmed_left { None } else { verdict };
if bits_changed {
if let Some(draw) = world.cells.get_mut(&key) {
// Copy-on-write (1436): the region is Arc-shared with the render world's
// extracted clone; a real bitmap change — the only writer — pays one region
// copy here so the per-frame publish/extract clones stay refcount bumps.
let draw = std::sync::Arc::make_mut(draw);
// Rebuild whole from the states — cheap, and immune to index drift across
// re-bakes (the flush refreshed `items` and set `bits_stale` this frame).
draw.killed.iter_mut().for_each(|w| *w = 0);
+8 -4
View File
@@ -122,18 +122,22 @@ pub(crate) enum GxDoodadVis {
Prop(Entity, Vec<bool>),
}
/// The published half the render world clones each frame (handles + ranges — cheap).
/// The published half the render world clones each frame. The baked regions sit behind `Arc`
/// (decision 1436): the 1435 band map priced the publish + extract clone pair at 0.39 ms/f —
/// tens of thousands of `GxItemDraw`s memcpy'd twice a frame — so the per-frame clones are
/// refcount bumps now, and the ONE writer that mutates a published region (the kill scan's
/// bitmap rebuild) pays a copy-on-write of that region alone, only on a real fade transition.
#[derive(Clone, Default, Resource, ExtractResource)]
pub(crate) struct GxWorld {
pub cells: HashMap<(i32, i32), GxCellDraw>,
pub cells: HashMap<(i32, i32), std::sync::Arc<GxCellDraw>>,
/// This frame's doodad-phase draw list, near-first across cells AND prop regions (B4):
/// frustum + farclip + exterior gate at cell/set granularity, PVS per set.
pub visible: Vec<GxDoodadVis>,
/// The WMO regions (slice 2), keyed by placement instance entity.
pub wmos: HashMap<Entity, GxCellDraw>,
pub wmos: HashMap<Entity, std::sync::Arc<GxCellDraw>>,
/// The prop regions (B4), keyed by the same instance entity as `wmos` (their lifecycle),
/// held apart so prop arrivals never re-bake building geometry.
pub props: HashMap<Entity, GxCellDraw>,
pub props: HashMap<Entity, std::sync::Arc<GxCellDraw>>,
/// This frame's per-group admission per region (indexed by absolute group index): the
/// portal flood's verdict collapsed to CPU range selection — the node draws exactly the
/// runs whose group bit is set.
+106 -24
View File
@@ -150,10 +150,21 @@ fn flare_ray_clear(height_under: impl Fn(Vec3) -> Option<f32>, cam_pos: Vec3, di
true
}
/// Ray-grid side count for [`flare_visible_fraction`] — 4×4 = 16 rays; the envelope slew smooths
/// the 1/16 quantization between frames.
/// Ray-grid side count for the occ3 fraction — 4×4 = 16 rays; the envelope slew smooths the 1/16
/// quantization between frames.
const FLARE_FRACTION_GRID: u32 = 4;
/// The grid's cell count — the fraction's denominator and the cached mask's width.
const FLARE_CELLS: u32 = FLARE_FRACTION_GRID * FLARE_FRACTION_GRID;
/// How many grid cells the probe re-marches per frame (decision 1436): the full 16×48-sample march
/// every frame priced 0.45 ms/f — the biggest single system in the 1435 parked band map — for a
/// value the slew rate-limits anyway. Two cells/frame refreshes the whole grid in 8 frames
/// (0.13 s at 60 Hz), inside BOTH slew time constants (rise 0.25 s, fall 0.66 s), so the drip is
/// invisible under the smoothing that already covers the grid's own quantization. The faithful
/// end-state stays the reference's GPU occlusion query (`0x7e5220`, recorded below).
const FLARE_RAYS_PER_FRAME: u32 = 2;
/// The **visible fraction** of the body's quad footprint against resident terrain ∈ [0, 1] — our
/// CPU stand-in for the reference's fractional occlusion probe (`0x7e5220`, Addendum #8): the real
/// client draws the disc's OWN quad (same size/position) depth-tested inside a GPU occlusion query
@@ -162,30 +173,61 @@ const FLARE_FRACTION_GRID: u32 = 4;
/// half-size), each ray the same quadratic terrain march — fraction = clear rays / total rays.
/// Terrain-only, like the march it generalizes; the reference's z-buffer probe also catches
/// WMOs/doodads — the GPU-occlusion-query instrument remains the faithful end-state (recorded).
/// Since decision 1436 the live path drips cells through [`flare_mask_update`] instead; the full
/// one-call march stays as the convergence oracle for the tests.
#[cfg(test)]
fn flare_visible_fraction(
height_under: impl Fn(Vec3) -> Option<f32>,
cam_pos: Vec3,
dir: Vec3,
half: f32,
) -> f32 {
let clear = (0..FLARE_CELLS)
.filter(|&c| flare_cell_clear(&height_under, cam_pos, dir, half, c))
.count();
clear as f32 / FLARE_CELLS as f32
}
/// March ONE grid cell of the quad's [−half, +half]² angular footprint — the unit the round-robin
/// re-prices per frame (decision 1436). Cell `c` = row `c / 4`, column `c % 4`, centre-sampled.
fn flare_cell_clear(
height_under: impl Fn(Vec3) -> Option<f32>,
cam_pos: Vec3,
dir: Vec3,
half: f32,
cell: u32,
) -> bool {
// Any orthonormal frame across the camera-facing quad works — the probe is symmetric.
let right = dir.cross(Vec3::Y).normalize_or_zero();
let right = if right == Vec3::ZERO { Vec3::X } else { right };
let up = right.cross(dir);
let n = FLARE_FRACTION_GRID;
let mut clear = 0u32;
for i in 0..n {
for j in 0..n {
// Cell centres over the quad's [−half, +half]² angular footprint.
let u = ((i as f32 + 0.5) / n as f32 - 0.5) * 2.0 * half;
let v = ((j as f32 + 0.5) / n as f32 - 0.5) * 2.0 * half;
let d = (dir + right * u + up * v).normalize();
if flare_ray_clear(&height_under, cam_pos, d) {
clear += 1;
}
let u = ((cell / n) as f32 + 0.5) / n as f32 - 0.5;
let v = ((cell % n) as f32 + 0.5) / n as f32 - 0.5;
let d = (dir + right * (u * 2.0 * half) + up * (v * 2.0 * half)).normalize();
flare_ray_clear(&height_under, cam_pos, d)
}
/// One frame's round-robin step over the cached cell mask (bit set = that cell's ray is clear):
/// re-march `cells` cells from `cursor`, then read the WHOLE mask as the fraction. Pure, for the
/// convergence test; [`FlareGate::envelope`] owns the state.
fn flare_mask_update(
mask: &mut u16,
cursor: &mut u32,
cells: u32,
clear_of: impl Fn(u32) -> bool,
) -> f32 {
for _ in 0..cells {
let c = *cursor % FLARE_CELLS;
let bit = 1u16 << c;
if clear_of(c) {
*mask |= bit;
} else {
*mask &= !bit;
}
*cursor = (c + 1) % FLARE_CELLS;
}
clear as f32 / (n * n) as f32
f32::from(mask.count_ones() as u16) / FLARE_CELLS as f32
}
/// The flare **envelope** — our build of the reference's `[glare+0x30]` smoothed intensity
@@ -217,6 +259,13 @@ pub(super) struct FlareGate<'w, 's> {
/// the visible layer, like the reference).
clouds: Res<'w, CloudCoverage>,
env: Local<'s, f32>,
/// The occ3 grid's cached per-cell verdicts (bit = clear) and the round-robin cursor
/// (decision 1436): [`FLARE_RAYS_PER_FRAME`] cells re-march per active frame. `primed`
/// falls when the probe is skipped (night, interior, below horizon) so the first active
/// frame after a gap marches the whole grid instead of trusting a stale mask.
mask: Local<'s, u16>,
cursor: Local<'s, u32>,
primed: Local<'s, bool>,
}
/// The weather **celestial-alpha seed** (Addendum #6): under active weather the recompute writes
@@ -244,18 +293,28 @@ impl FlareGate<'_, '_> {
) -> f32 {
let base = dn * horizon_gate(dir) * occ1;
let target = if base > 0.0 && self.camera_interior.0.is_none() {
base * flare_visible_fraction(
// Resident detailed terrain first; the coarse WDL surface everywhere else (it
// covers the whole map, so it also plugs the ADT-ring-to-farclip gap).
|p| {
terrain_height_under(&self.streamer, &self.adt_tiles, p)
.or_else(|| self.wdl.as_ref().and_then(|w| w.height_under(p)))
},
cam_pos,
dir,
half,
)
// Resident detailed terrain first; the coarse WDL surface everywhere else (it
// covers the whole map, so it also plugs the ADT-ring-to-farclip gap).
let (streamer, adt_tiles, wdl) = (&self.streamer, &self.adt_tiles, &self.wdl);
let oracle = |p| {
terrain_height_under(streamer, adt_tiles, p)
.or_else(|| wdl.as_ref().and_then(|w| w.height_under(p)))
};
// The round-robin drip (decision 1436): an unprimed mask marches every cell once,
// a primed one re-prices FLARE_RAYS_PER_FRAME — the slew smooths the ≤8-frame
// staleness exactly as it smooths the grid's own 1/16 quantization.
let cells = if *self.primed {
FLARE_RAYS_PER_FRAME
} else {
FLARE_CELLS
};
*self.primed = true;
let fraction = flare_mask_update(&mut self.mask, &mut self.cursor, cells, |c| {
flare_cell_clear(oracle, cam_pos, dir, half, c)
});
base * fraction
} else {
*self.primed = false;
0.0
};
*self.env = flare_slew(*self.env, target, rise, FLARE_FALL, self.time.delta_secs());
@@ -738,6 +797,29 @@ mod tests {
);
}
/// The 1436 round-robin drip is the full march, just spread over frames: priming reads the
/// exact full-march fraction, and a whole 2-cells-per-step cycle over an unchanged scene
/// lands back on it (the mask never reads a partial window — every step reads all 16 bits).
#[test]
fn flare_mask_drip_converges_to_the_full_march() {
let cam = Vec3::new(0.0, 2.0, 0.0);
let e = 5.4_f32.to_radians();
let dir = Vec3::new(e.cos(), e.sin(), 0.0); // straddles the ridge crest — a partial mask
let half = 2.0_f32.to_radians();
let full = flare_visible_fraction(ridge, cam, dir, half);
assert!((0.0..1.0).contains(&full), "oracle must be partial: {full}");
let (mut mask, mut cursor) = (0u16, 0u32);
let march = |c| flare_cell_clear(ridge, cam, dir, half, c);
let primed = flare_mask_update(&mut mask, &mut cursor, FLARE_CELLS, march);
assert_eq!(primed, full, "priming = the full march in one call");
let mut last = primed;
for _ in 0..(FLARE_CELLS / FLARE_RAYS_PER_FRAME) {
last = flare_mask_update(&mut mask, &mut cursor, FLARE_RAYS_PER_FRAME, march);
}
assert_eq!(last, full, "a full drip cycle re-derives the same fraction");
assert_eq!(cursor, 0, "the cursor wrapped exactly once");
}
#[test]
fn disc_span_feeds_the_per_vertex_fade_regimes() {
// Elevated (every fixture's regime): the whole span sits far above the ~1.9° fade band