MONKEY (perf): bleed fixtures bucket sources per placement and run at 10 Hz

update_bleed_fixtures walked every interior source in the world four times per
doorway every frame (~1.0 ms/frame main thread, traced, Northshire at High).
Sources are now bucketed per placement once (same rows, same order, same sums:
another placement's claimant weighs 0), the evaluation runs at 10 Hz with
arrivals and dial changes bypassing the cadence, and the light is written only
on a real change. Captures inn-noon and perf-abbey-night are pixel-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
pkuzic
2026-09-26 04:01:33 +01:00
co-authored by Claude Opus 5.5
parent d6f1cbe355
commit 68100e1120
+48 -6
View File
@@ -1811,9 +1811,14 @@ impl BleedSeed {
}
}
/// MONKEY (perf): seconds between two bleed evaluations (see [`update_bleed_fixtures`]).
const BLEED_EVAL_PERIOD: f64 = 0.1;
/// One interior-lane source as the bleed evaluation needs it — the packer's own arithmetic, minus
/// the flicker (a bleed must not breathe with the hearth it carries: the pool it feeds is a whole
/// doorway, and a wobbling doorway is a wobbling building).
// MONKEY (perf): Copy, so the per-placement buckets in `update_bleed_fixtures` can hold rows.
#[derive(Clone, Copy)]
struct BleedSource<'a> {
pos: Vec3,
/// Committed colour, NORMALISED like the shader's `c_norm`.
@@ -1981,7 +1986,22 @@ pub fn update_bleed_fixtures(
>,
time: Res<Time>,
mut last_dump: Local<f64>,
// MONKEY (perf): the 10 Hz cadence below, and the arrivals that bypass it.
mut next_eval: Local<f64>,
added: Query<(), Added<BleedFixture>>,
) {
// MONKEY (perf): a doorway carries the time of day and its rooms' static fixtures, so it is
// re-evaluated at 10 Hz rather than every frame (~0.5 ms/frame of main thread in Northshire at
// High). A new doorway, a dial move or the lane switching runs it at once.
let now_s = time.elapsed_secs_f64();
let urgent = !added.is_empty()
|| knobs.is_changed()
|| fire_gain.is_changed()
|| spell_gain.is_changed();
if !urgent && now_s < *next_eval && now_s >= *next_eval - BLEED_EVAL_PERIOD {
return;
}
*next_eval = now_s + BLEED_EVAL_PERIOD;
// The room lane being off is the feature being off, exactly as for a daylight fixture.
let on = knobs.enabled && bleed_enabled();
// The packed fill gain — `interiorFill x interiorGain`, the same product `build_light_data`
@@ -2057,6 +2077,24 @@ pub fn update_bleed_fixtures(
} else {
Vec::new()
};
// MONKEY (perf): bucket the sources per placement once. `claim_weight` gives a source that
// claims ANOTHER placement weight 0, so each doorway only needs its own placement's claimants
// plus the ungated ones — same rows, same order, same sums. Walking every interior source in
// the world four times per doorway measured ~1 ms/frame in Northshire at High.
let mut by_placement: HashMap<Entity, Vec<BleedSource<'_>>> =
live.iter().map(|i| (*i, Vec::new())).collect();
for s in &src {
match s.claims.filter(|c| !c.rooms.groups.is_empty()) {
Some(c) => {
if let Some(v) = by_placement.get_mut(&c.rooms.instance) {
v.push(*s);
}
}
None => by_placement.values_mut().for_each(|v| v.push(*s)),
}
}
let empty: Vec<BleedSource<'_>> = Vec::new();
let placement_src = |instance: Entity| by_placement.get(&instance).unwrap_or(&empty).as_slice();
/// One bleed fixture's per-frame working row — the two passes' shared scratch.
struct Row {
@@ -2082,8 +2120,8 @@ pub fn update_bleed_fixtures(
let r_eff = super::interior_reach(reach.map_or(fx.reach, |r| r.0), knobs.atten_scale);
let lit = if on {
brighter(
room_irradiance(&src, &[], bl.probe, fx.instance, bl.sides[0], k_fill),
room_irradiance(&src, &[], bl.probe, fx.instance, bl.sides[1], k_fill),
room_irradiance(placement_src(fx.instance), &[], bl.probe, fx.instance, bl.sides[0], k_fill),
room_irradiance(placement_src(fx.instance), &[], bl.probe, fx.instance, bl.sides[1], k_fill),
)
} else {
[0.0; 3]
@@ -2133,7 +2171,7 @@ pub fn update_bleed_fixtures(
let lit = if on {
brighter(
room_irradiance(
&src,
placement_src(r.instance),
&side_extra(r.sides[0]),
r.probe,
r.instance,
@@ -2141,7 +2179,7 @@ pub fn update_bleed_fixtures(
k_fill,
),
room_irradiance(
&src,
placement_src(r.instance),
&side_extra(r.sides[1]),
r.probe,
r.instance,
@@ -2164,8 +2202,12 @@ pub fn update_bleed_fixtures(
};
match pl {
Some(mut pl) if *intensity > 1e-4 => {
pl.color = *hue;
pl.intensity = 4.0 * std::f32::consts::PI * intensity;
// MONKEY (perf): write only a real change, so the packer's change ticks stay quiet.
let packed = 4.0 * std::f32::consts::PI * intensity;
if pl.color != *hue || pl.intensity != packed {
pl.color = *hue;
pl.intensity = packed;
}
}
Some(_) => {
commands.entity(r.e).remove::<WorldPointLight>();