`ambientOcclusion` 3 (Indirect Light), the Ultra rung. Tier 3 of the AO row
rather than a row of its own, because it is one trace: the bitmask that
measures occlusion properly is what produces the bounce, so a player choosing
between them would be choosing between two halves of the same thing.
Classic SSAO sums a cosine term per tap, so two occluders covering the same
direction count twice and a cluttered corner over-darkens. A bitmask counts
DIRECTIONS: each slice owns a 32-bit word of angular sectors, a tap sets the
run it spans, overlap is a no-op. And the bits a tap sets that were NOT already
set are exactly the share of the hemisphere it is the nearest thing in — i.e.
the fraction of the receiver's light it alone is responsible for. Its own pixel
colour, weighted by that, IS the one-bounce indirect term. No second trace, no
probes, no extra pass.
## The lane was already shaped for it
- `STAGE_AO` already bound the resolved scene colour, for the bright-pixel
protection mask. That buffer is exactly what a screen-space gather reads.
- The apply needed no new pass and no dual-source blending: visibility in
`src.a`, bounce in `src.rgb`, blend `One, SrcAlpha` = `bounce + dst*vis`.
- Only the half-res stages grew, by one attachment each, and the uniform's two
spare floats carried the strength and thickness, so its layout never moved.
## The one part that is ours and not the paper's
**The bounce is scaled BY THE OCCLUSION.** A physically-measured bounce is
wrong here: 1.12's lighting already carries an ambient term standing in for
indirect light, so adding a measured one double-counts it. Unscaled it lifted a
daylight exterior's median from 85.7 to 101.3 and washed the frame flat. Scaled
by `occ`, the lane puts back only the share of the ambient the occlusion just
took away, in the occluder's colour. An open field gets nothing, because
nothing was removed there; a corner gets the colour of the wall shading it.
Redridge overlook noon median 85.7 -> 86.7
Goldshire inn noon median 26.7 -> 29.0
Neutral in daylight, +8.6% in the shaded room — the same shape as the
auto-exposure lane's floor of 1.0, for the same reason.
The gather runs in LINEAR (`c*c` in, `sqrt` out): the scene is stored in gamma,
and 0.5 gamma is 0.22 of the light, not half of it.
## Cost
Frame time is unchanged (inn 2.22 -> 2.21 ms over 600 frames; overlook 2.25 ->
2.11). That is the frame being CPU-bound, not the lane being free. The GPU
meter is the honest instrument: whole-frame GPU 1.68 -> 1.81 ms, and the bucket
these passes land in 0.40 -> 0.54. So **+0.14 ms of GPU over High and nothing
on the main thread** — which is why the Options text says "a fraction of a
millisecond" rather than what I first wrote before reading the meter.
## Two bugs, both found in the intermediate rather than the frame
- **Screen +y is view -y.** `view_at` flips clip y, so a slice direction built
as `vec3(omega, 0.0)` measures its taps against a mirrored normal. One slice
of two was right and the other inverted, averaging to A FLAT FLOOR READING
50% OCCLUDED. `WOW_AO_DEBUG=4` showed it in one run; the composited frame
never would have.
- **`a / b` and `a * (1.0 / b)` are not the same number.** Factoring the blur's
divisor into one reciprocal — a tidy-up, not a change — moved `water-lake` at
tier **1** by up to 53/255 on 0.021% of its pixels, because the AO is
multiplied into the colour the water lane's depth-march then refines by
bisection, and one ulp picks a different step. Restored to two divides.
## Control
Tiers 0-2 byte-identical to the build before this, on three deterministic
scenarios at three tiers each, nine of nine. `fence-shadowline-night` had to be
dropped mid-control, and it sharpens the determinism table added last commit:
it screens as deterministic at the DEFAULT tier and is not at tier 1. Screen a
scenario at the setting you are about to measure it at, not at its default.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn