Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions docs/tickets.md
Original file line number Diff line number Diff line change
Expand Up @@ -1763,7 +1763,20 @@ behave. `rel` is now normalised to '/'. Run it on Windows: 0 failures.

---

## EN-053 — Shadows cost ~5.3 ms in SAMPLING, and four passes each pay it 🔴 *(2026-07-16 audit)*
## EN-053 — Shadows cost ~5.3 ms in SAMPLING, and four passes each pay it ❌ *closed same day — the premise did not survive re-measurement*

> **Measured before building (2026-07-16 evening, PERFTEST mode-0 bisect on
> the shooter's current main):** toggling shadows off buys **0.4 ms**
> (39.88 → 39.48 ms), with `shadow_pass` at 0.09 ms — while `no-ssgi` buys
> **4.9 ms**. The 5.3 ms figure was measured 2026-07-12 and the frame changed
> under it (house v2, 120k grass, photoscan terrain landed since). Building
> the shadow-mask pass tonight would have repeated the SH-049 attribution
> mistake with a week of renderer surgery attached. The file:line facts below
> stay true (16 taps in a ±1-texel disk, four independent consumers) — they
> are just not where the frame's time is. Re-open only from a fresh bisect
> that shows sampling as a top-3 lever; today's top lever is SSGI cost
> (quality-gated — the direction is making it cheaper, e.g. the HW ray-query
> path, not turning it off).

The shadow **pass** is fixed (EN-043/044/045 — 0.18 ms). What remains is the
per-pixel PCF: the deferred lighting path runs a fixed **16-tap** Poisson
Expand Down Expand Up @@ -1841,7 +1854,14 @@ budget for at 4K. All VERIFIED still present:
Fix: dirty-flag the lighting UBO, cache bloom + composite bind groups keyed
on the views they wrap, build the graph once and rebuild on topology change.

## EN-057 — Hi-Z occlusion runs every frame for zero consumers 🟡 *(2026-07-16 audit)*
## EN-057 — Hi-Z occlusion runs every frame for zero consumers ✅ *(shipped same day)*

> The occlusion reduce + readback now gates on a per-frame
> `set_has_consumers` flag the engine derives from the scene graph (any
> visible non-gi_only node). Going consumer-less also invalidates the grid,
> so a consumer appearing later reads the conservative "potentially visible"
> answer instead of a stale capture — the gate cannot wrongly cull a draw by
> construction. The Hi-Z pyramid itself still builds: SSAO consumes it.

The pyramid build + occlusion reduce + readback gate only on `ssao_enabled`
and the culler's own flag (`mod.rs:10707-10731`, `occlusion.rs:221-223`) —
Expand Down
6 changes: 6 additions & 0 deletions native/shared/src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,12 @@ impl EngineState {
// Collect last frame's occlusion-grid readback (if it
// completed) before culling against it.
self.renderer.occlusion.poll(&self.renderer.device);
// EN-057 — skip the occlusion reduce + readback entirely when no
// rasterized node could consume a verdict (gi_only proxies never
// draw; the shooter's 267 of them paid this every frame for
// nothing). The Hi-Z pyramid itself stays: SSAO consumes it.
self.renderer.occlusion.set_has_consumers(
self.scene.nodes.iter().any(|(_, n)| n.visible && !n.gi_only));
self.scene.prepare(
&self.renderer.device,
&self.renderer.queue,
Expand Down
22 changes: 21 additions & 1 deletion native/shared/src/renderer/occlusion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ pub struct OcclusionCuller {
grid_valid: bool,
grid_vp: [[f32; 4]; 4],
pub enabled: bool,
/// EN-057 — false when no rasterized scene node exists to consume the
/// culling verdicts (e.g. a scene whose only nodes are gi_only proxies:
/// they never draw, so the reduce + readback benefited zero draws every
/// frame). Set per frame by the engine from the scene graph; defaults to
/// true so hosts that never call the setter keep today's behaviour.
has_consumers: bool,
/// set when record() ran this frame so after_submit() knows to map
recorded_this_frame: bool,
}
Expand Down Expand Up @@ -173,10 +179,24 @@ impl OcclusionCuller {
grid_valid: false,
grid_vp: [[0.0; 4]; 4],
enabled: true,
has_consumers: true,
recorded_this_frame: false,
}
}

/// EN-057 — tell the culler whether any rasterized consumer exists this
/// frame. Going consumer-less also invalidates the grid, so if a
/// consumer appears later the interim frames read the conservative
/// "potentially visible" answer (test_aabb on an invalid grid) instead
/// of a stale capture — the gate cannot cost a wrongly-culled draw by
/// construction.
pub fn set_has_consumers(&mut self, has: bool) {
if !has {
self.grid_valid = false;
}
self.has_consumers = has;
}

/// Drop cached bind group (call when the Hi-Z chain is reallocated,
/// e.g. on resize / render-scale change).
pub fn invalidate_bindings(&mut self) {
Expand Down Expand Up @@ -218,7 +238,7 @@ impl OcclusionCuller {
vp: [[f32; 4]; 4],
) {
self.recorded_this_frame = false;
if !self.enabled {
if !self.enabled || !self.has_consumers {
return;
}
let rb = &mut self.readbacks[self.parity];
Expand Down
Loading